- audio: default background music is now the santoor track (calm Persian),
rebuilt as a real plucked-santoor loop — fast metallic attack, shimmer
overtones, soft tonic drone, longer Dastgah-e-Shur phrase
- site: marketing logo is now the app's card-fan icon (Logo.tsx + icon.svg);
hero features the big logo with gold halo, floating suit motifs, and
polished section dividers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server logs showed REST working but ZERO hub activity — the SignalR WebSocket
upgrade isn't getting through the nginx/CDN stack and auto-fallback wasn't
recovering, so StartMatchmaking never reached the server (matchmaking spun
forever). Force the HttpTransportType.LongPolling transport — plain HTTP that
already works (same path as REST); SignalR holds the poll open so it's
effectively real-time for a turn-based game. Revertable once the api block
proxies WS upgrades.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
api.bargevasat.ir is now CDN-bypassed (origin answers directly), so the
negotiate POST works again. Drop the WS-only skipNegotiation workaround and use
the standard negotiate flow, which auto-falls back WS → SSE → long-poll if a
WebSocket upgrade isn't available.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WCDN rejects the SignalR negotiate POST (404, wcdn-nfc-reason: Http_Method), so
the hub never connects and online matchmaking never starts. Connect directly via
WebSockets with skipNegotiation so there's no negotiate POST; the JWT rides the
?access_token query the server already accepts for /hub. The proper fix remains
bypassing the CDN for api.bargevasat.ir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OtpService: a designated test phone (default 09120000000 / code 453115,
overridable via Sms__TestPhone/Sms__TestCode) skips real SMS and always
verifies — for Google Play / Bazaar / Myket reviewers. Give them these creds.
- Matchmaking UX: tapping a league now navigates to the matchmaking screen
BEFORE awaiting the SignalR handshake, so the button can't freeze. Added a
watchdog hint after 28s ("connection took too long, cancel & retry") so it
never spins forever when the hub doesn't connect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Private rooms were 100% client-simulated (the "friend" auto-accepted then bots
filled invited seats). Now they're server-authoritative over SignalR:
Server (GameManager.PrivateRooms + GameHub):
- Room registry with create/invite/accept/decline/addBot/clearSeat/start/leave.
- Invite pushes a `roomInvite` to that user (Clients.User); the seat stays
"invited" (a pending guest with their real profile, resolved server-side) — it
is NEVER replaced by a bot.
- StartPrivate refuses while any invite is pending; only EMPTY seats fill with
bots. Then it spins up a live GameRoom and matchFound → both devices enter.
- Host leave / disconnect closes the room (roomClosed); members free their seat.
Client:
- signalr-service implements the room methods over the hub (+ room/roomInvite/
roomClosed events, room mapping, onRoomInvite); mock keeps offline no-ops.
- online-store accept/declineInvite; RoomScreen blocks "Start" while an invite
is pending and auto-enters the live game on matchFound (host + friend).
- New global InviteModal (accept/decline) + i18n (invite.*, room.waitAccept).
Addresses: (1) no bot replacement, (2) game waits for acceptance, (3) invited
friend shown as a pending guest with their name/avatar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sound.ts: restored startMusic (was a no-op stub) playing the selected track
through musicGain (in-game mute still applies); default track switched to
"playful" (per user). Music auto-starts on init when enabled.
- Profile → Audio: re-added a music on/off toggle (so players can disable it
outside the game too); SFX toggle unchanged.
- Economy tune: starting coins 1000 → 2000 (mock defaultProfile + server
ProfileDto) so new players start a bit richer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old reward formula rounded (40+6g)/50 on the raw goal, so adjacent
milestones could pay the same (e.g. "1 win" and "5 wins" both 50) and the curve
was lumpy — not a standard escalating-reward ladder. Reward now scales by tier
index: 50, 150, 250 … capped at 1500, strictly increasing per milestone.
Mirrored client (gamification.ts) + server (Gamification.cs) so live grants match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Shop: when short on coins the detail sheet now shows "{n} more coins" + a
"Get coins" CTA that opens the buy-coins page (was a dead disabled button).
- Chat: pin/unpin conversations (max 3, persisted to localStorage); pinned float
to the top with a gold pin. i18n chat.pin/unpin/pinLimit.
- Surrender: server now rate-limits forfeit asks at a human teammate
(45s per-user cooldown) so it can't be spammed. (Bot teammate still ends
immediately; teammate confirm dialog already existed.)
- OTP login hardening: Kavenegar send now parses the API status from the body
(HTTP 200 can still be a failure) + logs it + 12s timeout; client auth fetch
gets a 20s AbortController timeout so a lost response surfaces an error
instead of freezing on "sending…".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auth / security
- Rate-limit real SMS OTP sends (dev mode unlimited): 60s resend cooldown,
5 per phone/hour, 300/hour global backstop. OtpService.CheckAndRecordRate;
POST /api/auth/otp/request returns 429 {error,retryAfter}; AuthScreen shows
auth.rateLimited. Knobs in appsettings Sms (Sms__* env).
Private rooms (invite)
- Cancel-invite button on pending seats; friend picker shows presence
(online/offline/in-game, sorted online-first) and flags in-game players.
- Mock invite stays pending ~3.5s and a cancel truly stops the auto-accept
(was a bug that re-seated cancelled invites).
In-game UI
- Scoreboard is compact + shrink-safe (no overflow on narrow screens).
- Played trick cards land dead-center (were ~2px off the corner anchor).
Plus the in-flight typing-indicator work (GameHub, ChatScreen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mock service intentionally KEPT the persisted profile (hokm.profile) on
signOut, and getProfile() reloads it — so after logout the previous user's
name/gender/avatar resurrected from localStorage. Now signOut clears the
in-memory + persisted profile, and the SignalR service also clears its mock
fallback so the post-logout guest profile is fresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GameTable reactions button (and its tray) moved up from the bottom-right so it
no longer overlaps the player's cards on mobile portrait.
- Gender options are now Male / Female / Unknown — removed "other" from the
Gender type, GENDER_META, and the profile picker; the empty value renders as
«نامشخص» / "Unknown".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AuthScreen gated the code-entry step on devCode != null, so with real SMS
(no devCode) it got stuck after "send". Gate on a `sent` flag instead; add
sending state, send-failure message, "code sent" hint, change-number, and
raise the code input cap to 6 (codes are 5 digits).
- signOut now resets the store to a fresh guest profile, and the SignalR
service clears its cachedProfile — so the previous user's name/avatar no
longer linger after logout.
- i18n: auth.sending / sendFailed / codeSent / invalidPhone / changeNumber.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements real Myket IAB for the Capacitor app (Myket has no purchase
deep-link like Bazaar — it uses the classic Google Play IAB v3 AIDL bound to
the Myket app):
- AIDL: com.android.vending.billing.IInAppBillingService (Myket-compatible).
- MyketBillingPlugin (Capacitor): binds ir.mservices.market via
"ir.mservices.market.InAppBillingService.BIND", runs getBuyIntent →
startIntentSenderForResult, verifies INAPP_DATA_SIGNATURE with the RSA key
(Security.java, SHA1withRSA), returns the purchaseToken; consume() too.
- MainActivity registers the plugin + forwards the purchase activity result.
- Manifest: ir.mservices.market.BILLING permission + <queries> for Android 11+
package visibility.
- build.gradle: enable buildFeatures.aidl (AGP 8 disables it by default).
- storeBilling: Myket goes through the plugin (RSA key embedded); after server
verify, BuyCoins consumes the purchase so coins can be re-bought.
Bazaar (deep-link) and web (ZarinPal) paths unchanged. Needs on-device testing
with the Myket app installed + published products.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pack ids now equal the Bazaar/Myket SKUs (Coin5K / Coin12K / Coin28K /
Coin65K) in both the server source-of-truth (ProfileService.Packs) and the
mock, so the IAB credit path (Id == ProductId) grants the right coins.
Coin totals + prices already matched the registered products.
- storeBilling.getStore(): when running inside the Capacitor Android shell and
no explicit NEXT_PUBLIC_STORE flavor is set, default to "bazaar" so the APK
uses Cafe Bazaar IAB (the web build stays on the ZarinPal gateway). Myket's
native bridge still overrides when present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- storeBilling.ts and IabService PackageName defaulted to com.bargevasat.hokm,
but the real app id is com.bargevasat.app (capacitor + android applicationId).
The mismatch would break Bazaar deep-link purchases and server validation.
- Add IabOptions.BazaarRsaPublicKey to hold the Bazaar in-app billing RSA public
key (documented; for the Poolakey local-signature flow, unused by the current
deep-link + server pardakht verification).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NavRail: one rounded "pill" tab bar on every screen (matches home). ScreenShell
lays out as a portrait column and floats the nav with margins + safe-area;
dropped the landscape side-rail variant.
- Home: the three mode cards now stack vertically as full-width rows (portrait
friendly) instead of a 3-up landscape row.
- Disconnect: removed the simulated random opponent "disconnect" in local games
(DISCONNECT_CHANCE) and the in-game DisconnectBanner — bots/filled seats just
auto-play their turn; no message, no pause. (Live reconnect grace still tracked
internally but no longer shows a banner.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Photo upload:
- Lower the custom profile-photo gate from level 25 to level 3 (client const +
i18n hint + server gate in ProfileService.Update). The level-25 "Expert" title
is unrelated and unchanged.
Report a player:
- New ReportReason type + service.reportUser(targetId, reason, details?).
- Report entry points: a "گزارش تخلف" button + reason picker (nudity / insult /
other) in the public-profile modal, and a flag button in the chat header
(reports the peer for an insulting chat) with a confirmation toast.
- Mock records reports to localStorage; SignalR POSTs /api/report.
- Server: POST /api/report → ProfileService.ReportUser stores the report in the
write-only ledger (kind="report", ref="{targetId}|{reason}|{details}") so no
schema change is needed (server uses EnsureCreated, not migrations).
- i18n: report.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both the mock and the .NET server already waited then bot-filled, but used a
random 12-18s window. Make it exactly 15s on both sides so the rule is clear:
wait 15s for real online players to join, then replace any unfilled seats with
bots and start.
- client: new MATCH_QUEUE_WAIT_MS = 15000 in gamification.ts; mock beginSearch
uses it instead of randInt(12000,18000).
- server: GameManager QueueWaitMs = 15000 (was randomized 12-18s per ticket).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New searchable city picker (src/lib/iran-cities.ts, ~60 Iranian cities,
fa/en search) shown as a gold reward card at the top of the profile Basic tab.
- First time a non-empty city is set, the player earns 500 coins (CITY_REWARD),
granted server-authoritatively. Collapses to a compact summary afterwards with
a "change city" option (no re-reward).
- Frontend: UserProfile.city + cityRewardClaimed; mock-service grants on first
set; session/service updateProfile accept `city`; celebratory toast + sfx.
- Backend (.NET): ProfileDto.City/CityRewardClaimed (JSON blob → no migration);
ProfileService.Update grants +500 once and writes a "city" ledger entry.
- i18n: city.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OnlineLobbyScreen: each league row is now a tappable play button (queues a
ranked match at that league's stake) with a forward arrow; the cheapest
enterable league is highlighted gold. Drops the redundant separate "ranked
random" CTA and the select-then-play step.
- Remove the background-music feature entirely: deleted the floating MusicToggle,
the TopBar music button, and the Profile audio music toggle + style picker.
sound.startMusic() is now an inert no-op so music never plays (sfx unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PostMatchRewardsModal: short-height (landscape) compaction so the win/forfeit
result fits without overflow (smaller emoji/coins/padding, max-h 94dvh, wider).
- Chat: emoji/sticker picker (owned reactions) — tap to send; hidden on focus.
- Unread messages: online-store now tracks a total `unread` (from
listConversations); NavRail Friends icon shows a badge (unread + requests),
refreshed every 12s on every screen; Friends «پیامها» tab badged too.
(Per-conversation unread badges already existed.)
- Remove "XP گران است" / "XP is expensive" from shop.xpHint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Room: teams side-by-side in landscape so all 4 seats fit (still scrolls).
- Achievements: rename the 5 rating tiers from «لیگ» (league) to «رتبه» (rank)
+ category «رتبه» — so "league" only means the 3 playable match leagues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NavRail: vertically center items in the side rail (was top-aligned).
- ScreenHeader: showXp defaults off — the level/XP bar no longer clutters every
sub-page (it lives on Home's chip + the Profile page).
- Shop: category tabs (avatars / fronts / backs / reactions / stickers / titles
/ XP) so only one category shows at a time — no more endless scroll.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild HomeScreen to UNO's home layout: top bar (avatar+coins) + 3 big glossy
3D mode cards in the center (Online[gold,live-count badge] / vs-Computer[teal] /
Private Room[violet]) + a bottom icon nav bar (NavRail bottom variant, drops the
redundant home item). Speed toggle + language sit in a slim controls row. Online
card shows live player count; room card creates a private room then enters it.
New menu.room/menu.roomDesc i18n.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild ScreenShell into a UNO-style app shell: a persistent NavRail (vertical
side rail in landscape, bottom tab bar in portrait — Home/Profile/Shop/Friends/
Leaderboard/Achievements, active highlighted gold) + a content panel that owns
its own scroll so the page never scrolls as a whole and uses the width in
landscape. Reskins all 10 menu screens at once. Transient screens (auth,
matchmaking, room) opt out via hideNav. New nav.home i18n key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move orientation lock + RotatePrompt to app root → whole app is landscape-
first now (UNO-style), not just the game. Generalized rotate copy.
- Home: portrait unchanged; in landscape it becomes a 2-column app layout
(col A = branding + play actions, col B = tiles + footer) that fits the
short height with no scroll (landscape: Tailwind variants, overflow-hidden).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hokm plays best wide (UNO-style). On phones held in portrait, the game screen
shows a "rotate your phone" overlay (with a play-anyway escape hatch so OS
rotation-lock can't trap anyone). Best-effort screen.orientation.lock('landscape')
on Android/PWA; iOS/desktop reject it harmlessly. i18n rotate.* (fa+en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each notification now navigates to its related screen when tapped (toast or
list): friend_request/invite -> Friends, achievement/reward -> Achievements,
daily -> opens the daily-reward modal, coin-purchase success -> Shop. An
explicit per-notification 'route' overrides the kind default.
List rows are swipeable (drag aside) and have an X to dismiss individually,
plus a Clear-all button; the toast can be flicked up to dismiss or tapped to
open. New store actions: markRead/remove/clearAll + openNotification navigator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No more earned-only (rank/wins) cosmetics — every avatar, card back/front,
reaction & sticker pack now has a coin price. Rank/wins/achievement become
purchase requirements (coin · coin+rank · coin+rank+achievement), enforced
client (mock + ShopScreen lock label) and server (ProfileService.ItemGate,
keyed by kind:id). Ownership = default + purchased only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaced the childish animal emoji avatars with custom inline-SVG "deity
medallions" (gradient disc + gold ring + heraldic emblem) — Athena, Zeus,
Poseidon, Horus, Odin, Thor, Cyrus, Simorgh, Ishtar, Nike, etc. IDs unchanged
so owned avatars keep working; Avatar renders the art (emoji fallback for legacy
ids). Shop now shows the art + the god name (was generic "Avatar").
Files: components/online/avatarArt.tsx (new art + pantheon map), Avatar.tsx
(render art), ShopScreen Preview (avatar → <Avatar/>), mock-service avatar shop
names from AVATAR_ART.
Verified: tsc + next build clean; web rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MusicToggle: global floating button (enable/disable music from any screen;
hidden on the table, which has its own audio control in its HUD). Uses
sound-store toggleMusic.
- Card sounds now use a synthesized card-draw "swish" (filtered noise burst with
a downward sweep) for cardPlay (+ soft landing tap) and deal (a flurry),
replacing the old beep tones.
Verified: tsc + next build pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds ~100 new purchasable gifts that are LOCKED until a level/rating gate is met,
then buyable with coins — value scales with the gate:
- 45 gift avatars (types.ts), 35 gift titles + 20 gift card backs (gamification.ts),
all reusing existing renderers. Tier (1-5) encoded in the id (-t<n>-).
- Gate model: GIFT_TIERS (shared) → reqLevel/reqRating on AvatarDef/TitleDef/
CardBackDef + ShopItem. Tiers: t1 free, t2 Lv10, t3 Lv20, t4 Lv35, t5 Rating1700.
- Shop UI: locked cards dim + show the requirement (Lock + "Level 20"), buy
disabled until met; mock buyItem enforces it offline.
- Server enforces generically — ProfileService parses the tier from the id and
checks the player's level/rating (no 100-entry mirror). Mirrors GIFT_TIERS.
- i18n shop.reqLevel/reqRating (fa+en).
Verified: tsc + sim + next build + dotnet build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- One running game per player: server rejects a 2nd matchmake while in a live
room (re-syncs the existing game); client guards Home vs-computer + Lobby
random/create — resumes the running match + notifies instead of starting another
(game-store hasActiveMatch()).
- Background music is now selectable: santoor (سنتی, calm Persian loop) and
playful (bouncy UNO-like) — sound.ts TRACKS + setMusicTrack (persisted),
sound-store musicTrack, picker in Profile → Audio. i18n added.
- Production config for bargevasat.ir (prepare-only; no live deploy):
appsettings.Production.example (CORS + ZarinPal + IAB to the domain),
docker-compose.caddy.yml + Caddyfile (auto-HTTPS reverse proxy
bargevasat.ir→web, api.bargevasat.ir→server), ENV_FILE PRODUCTION block,
PRODUCTION.md go-live + Cafe Bazaar publish/IAB checklist. Fixed IAB package
name to match Capacitor appId (com.bargevasat.app).
Verified: tsc + next build + dotnet build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Daily reward now routes through the global CelebrationOverlay: new "daily"
variant + coins count-up; claiming closes the daily modal and fires
celebrate({variant:"daily", coins}). Unifies the "you earned X" moment.
- Premium (pro) gold chat is now visible to the OTHER player: ChatMessage gains
senderPro; server resolves each participant's plan once (SocialService.IsPro)
and stamps it on ChatMessageDto; ChatScreen styles incoming bubbles with
.premium-chat when senderPro. Mock marks ~half its friends pro so it's visible
offline too.
Verified: tsc + next build + dotnet build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MatchIntroOverlay: UNO-style pre-game reveal — the 4 seats animate into the
table (with "?" placeholders until each player's data streams in for live
matches), a 3-2-1-GO countdown, then the table shows. Wired via game-store
matchIntroPending/consumeIntro, rendered online-only in GameScreen.
- Fix: intro.found / intro.getReady / intro.go existed only in the Persian dict;
added the English strings (would have shown raw keys to EN users).
- Checkpoint of the in-progress UI/social batch (CoinsPill, shop titles section,
friend-request rate limit, etc.) — all green.
Verified: tsc + next build + scripts/sim.ts + dotnet build server/Hokm.slnx all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forfeit penalty reworked (client + server gamification, in sync):
- Surrendering team loses DOUBLE the entry coins; winner takes the stake.
- Forfeiter earns NO XP. No kot is applied or mentioned anymore.
- MatchSummary/Dto carry a `forfeit` flag; GameRoom.FinalizeForfeit →
ApplyRewardsAsync(team) with Forfeit=true (dropped the kot path).
- Forfeit confirm dialogs now alert the real penalty (double coins, no XP).
End-of-game roster: SeatPlayerDto/ServerSeatPlayer + game-store SeatPlayer gain
userId/isBot. New <MatchPlayersList> lists everyone at the table on the final
screen (PostMatchRewardsModal + AI MatchOverlay) with a tactile "Add" button to
send a friend request to real (non-bot, non-self) players ("Sent" after).
Verified: tsc + sim + dotnet + next build clean; stack rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New global celebration system: celebration-store (queue) + CelebrationOverlay
(animated: count-up XP, filling bar, level-up pop, achievement cards; plays
levelUp/award sounds; tap or auto-dismiss). Rendered in page.tsx.
- Shop: every purchase now celebrates — XP packs animate XP gain + level-up,
cosmetics show a "purchased!" pop. Newly-unlocked achievements (diffed from
the profile before/after) animate too.
- XP purchases now actually evaluate achievements: gamification.evaluateAchievements
(client) + Gamification.EvaluateAchievements (server, called in ShopBuy xp path)
unlock level milestones + grant their coins.
Verified live: buying XP took L1→L5, unlocked level_5 server-side and credited its
reward. tsc + dotnet + next build clean; images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Answers "what's in this pack / how much XP": shop items now carry contents
(sticker ids / emojis), xp amount, and a fa/en description. Cards are tactile
(press-3d), show the artwork + name + a quick hint (item count or +XP) + price,
with an owned check badge. Tapping a card opens a detail sheet that shows the
full contents (every sticker/emoji rendered), the XP granted, a description,
and a Buy button (gold when affordable, "need coins" otherwise).
Verified: tsc + next build clean; web rebuilt :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mock emitted random "a friend is online / event is live" notifications on a
35s timer and the live service forwarded them. Dropped both — only real
notifications now fire (friend requests, achievements, daily reward, payment,
match-ended, and server hub events).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- XP packs in the store (coin-priced, intentionally expensive): xp1 200/5k,
xp2 600/12k, xp3 1500/25k. Consumable (grant XP, can level up) — server
ShopBuy handles kind "xp" via an authoritative XpPacks map + Gamification.GrantXp;
mock mirrors. New shop section + shop.xp/xpHint i18n.
- Every game grants XP and the WINNER earns 2x: matchXp is now
base*(won?2:1)*leagueFactor (was a flat +80 win bonus). Mirrored server-side.
- Premium (pro) perks: 1.5x XP multiplier (applied in applyMatchResult /
ApplyMatch by plan), plus animated shimmering gold chat bubbles for your own
messages (premium-chat CSS; ChatScreen gates on plan).
Verified: tsc + next + dotnet build clean; sim passes; live server — buying xp2
took L1→L3 and deducted 12k coins under the new curve. Images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosmetics — many new variants, the rarer ones gated behind higher ranks:
- Card backs: +midnight/jade/onyx (buy) + crimson/aurora/obsidian/imperial
(earned by wins/rating up to Master). Card fronts: +sunset/velvet/onyx (buy)
+ goldleaf/crystal/imperial (earned).
- Titles: +marksman, untouchable, sweeper, ruler, platinum_star, diamond_ace,
immortal, the_one (gated by kots/streak/shutouts/hakem/rating/level/wins),
mirrored on the server so live games grant them.
- Avatars: list expanded + rank/wins-earned tier (robot/wizard/ninja/king/
genie/crown) via new ownedAvatarIds(); profile picker shows earned ones,
shop sells the priced ones.
- Stickers: new Persian-text stamp pack (کوت! / دمت گرم / باریکلا / آخه؟) plus a
rank-earned Victory pack (بردیم!/حکم) — new inline-SVG art.
Leveling: XP per level now grows (100*l + 15*l²) so each level is harder; higher
leagues grant more XP (×1.5 at 500 stake, ×2 at 1000) so you progress by playing
up. Hard cap at level 100. Mirrored in server Gamification (XpForLevel/MatchXp/
AddXp). Sim now tops out lower (level 20 vs 35 over 500 matches) as intended.
Verified: tsc + next build + dotnet build clean; sim passes; images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Leaderboard: each row now shows the player avatar (photo or emoji) with a level
badge ring and a progress-to-next-level bar (LeaderboardEntry gained
levelProgress + avatarImage; mock fills real XP for you, random for others).
Mobile table: the played-card pile now scales inward on narrow screens so it no
longer overlaps the opponents' side stacks (trickScale by viewport); seat
avatars render above the stacks (z-20) so the side player isn't hidden; side
hands nudged to the edges + top hand raised slightly on phones.
Verified: tsc + next build clean; web image rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Achievements: generator-driven, now 100+ across 7 categories (added Rulership)
mirrored client + server with identical ids/goals/coins. New tracked stats:
hakemRounds (be the hakem — incl. "7× Hakem"), roundsWon, plus losses metric.
Custom achievement-only sticker packs (Rulership 👑, Firestorm 🔥) with new
inline-SVG art (crown-gold, seven-zip, streak-fire), unlocked by hakem_7 /
streak_10. Server GameRoom tallies hakem rounds per seat + rounds won per team;
client tallies the same for vs-computer/private games (dealId-deduped).
Forfeit (surrender): a player can request forfeit; if the teammate is a bot it
auto-confirms, otherwise the human teammate gets a confirm/decline prompt
(20s timeout). Result: forfeiting with ≥1 round won = normal loss; 0 rounds = Kot.
Wired client↔server over the hub (RequestForfeit/ConfirmForfeit/DeclineForfeit
+ "forfeit" event); offline/vs-computer ends immediately in the store. Flag
button + confirm dialogs in the table.
Online count: never shows below 50 — live service floors the real count with a
drifting believable number (mock base lowered to ~50–170).
Matchmaking: real players get a longer priority window (9s) before bots fill;
bots now occasionally react after winning a trick (humanize).
Coins: starter pack is 95,000 Toman (50k coins); packs rescaled up (server + mock).
Verified: dotnet build + tsc + next build clean; sim unlocks 57 achievements/500
matches; live server: starter=95000, a 7-hakem win unlocks hakem_7 + wins_1 with
hakemRounds/roundsWon persisted. Images rebuilt on :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Achievements (client + server mirror, metric-driven so the list is one source):
- 37 achievements across 6 categories (Victories, Kot, Streaks, Levels, Ranks,
Veterancy) incl. 7–0 sweeps, kot milestones (1/5/10/25/50/100), win streaks
(3/5/10/15), level milestones every 5 (5..50), rank floors, games/tricks.
- New AchievementsScreen with category tabs, progress bars, coin + sticker-unlock
badges, and unlocked/locked states; summary header (unlocked count + coins).
- Some achievements unlock sticker packs: Seven–Zip→Hokm, 25 Kots→Taunts,
100 Wins→Persian (ownedStickerPackIds now also honors profile.unlocked).
- Prestige titles added: Expert, Professional, Captain, Leader (+ existing).
- Tracks new stat shutoutWins; MatchSummary.shutout (7–0). Profile shows a
6-item preview + "view all" link.
Leagues: 3 ranked entry tiers — Starter (100, lvl1), Pro (500, lvl10),
Expert (1000, lvl20). Higher league stakes more, so wins/losses swing bigger;
kot bonus now scales to the stake (40%). OnlineLobby shows league cards with
level gating.
Profile photo upload gated to level 25 (client button + server Update guard).
Win animation: PostMatchRewardsModal now shows an animated coins-won count-up
hero on a win.
Verified: dotnet build + tsc + next build clean; sim unlocks 26 achievements
over 500 matches; live server grants first_win/first_kot/shutout_1 and pays
2050 coins on an expert-league shutout+kot win. Images rebuilt on :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Leaving the table (back button, browser/hardware back) no longer resets the
game — it minimizes it and stays resumable:
- game-store: add `paused` + minimize()/resume(). Single-player (AI) matches
pause their local timers so nothing happens while away; live (server-run)
matches keep streaming via the still-active subscription (the .NET GameRoom
already runs the match to completion and re-broadcasts state on reconnect).
- GameScreen: an unmount effect minimizes any in-progress match no matter how
you leave; only a finished match (reward dismissed) tears down.
- ResumeGameBar: floating "return to game" pill shown from any screen while a
match is alive, or while a finished match still has an unseen reward.
- page.tsx: after a full reload, re-enter live mode (minimized) when the server
re-broadcasts state, and notify when a match you left finishes while away.
Verified: tsc + next build clean; web image rebuilt and serving on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Social: EF-backed friends graph + chat (SocialService/SocialModels);
REST endpoints (friends add/accept/decline/remove/list/requests,
chat conversations/messages/send) with real-time hub events
(friendRequest/social/chat). GameManager tracks online users for presence.
- Client SignalrService: friends + chat now hit the server and react to
hub events (refetch + emit); no longer delegated to the mock.
- IAB: /api/coins/iab/verify endpoint + IabVerifyReq for Cafe Bazaar/Myket
(token verification is a documented TODO pending store accounts/SKUs).
- Persistence: EF Core Design package + DesignTimeDbContextFactory (Postgres),
Program auto-migrate/EnsureCreated, appsettings.Production.json.example
with Supabase connection + live ZarinPal template.
Verified end-to-end (two users, SQLite dev): request -> accept ->
bidirectional friends, chat send with per-user fromMe, unread count +
read-on-fetch. Server + client builds clean (dotnet build, tsc, next build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ZarinpalService (request/verify) + /api/coins/pay/request (JWT) and
/api/coins/pay/callback (verify → credit via ProfileService.BuyCoins → redirect
back with ?pay=success); merchant id from config (sandbox default)
- Client buyCoins (live) returns the StartPay redirect URL; BuyCoinsScreen
redirects; page.tsx handles the ?pay return (notify + refresh)
- Verified: sandbox request returns a real StartPay URL
- Documented Cafe Bazaar (Poolakey) / Myket IAB as the required store payment path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>