The server is authoritative with ABSOLUTE seats and tells each client its own
seat via mySeat, but the client copied seats verbatim — so any player not at
absolute seat 0 had their hand at players[mySeat] while the table read players[0]
and "your turn" checked turn===0. Result: they couldn't play (server auto-played
after the timeout → "hang"), and the turn highlight was identical for everyone
instead of rotating per viewer.
Fix (client-only; server was correct): new viewerRot(mySeat) rotates every
seat-indexed value into the viewer's frame (viewer → local seat 0): players/hands,
turn, hakem, leadSeat, lastTrickWinner, currentTrick, hakemDraw, seat roster,
disconnectedSeat, and the team arrays (matchScore/roundTricks/lastRoundResult/
matchWinner — odd seats swap team order). Store mySeat and rotate reaction bubbles
too (they carried absolute seats).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the uploaded profile photo only appeared in a few places (profile,
top bar, leaderboard, public profile); chat, friends, game table, match intro,
post-match roster and private rooms showed the emoji avatar only.
- carry avatarImage end-to-end:
- server DTOs: FriendDto, SeatPlayerDto, RoomPlayerDto, MatchmakeRequest +
Player/SeatSlot/PSeat; ResolveProfile now returns avatarImage; FriendDtoFor
fills it from the profile.
- client types: Friend, RoomSeat.player, MatchmakingState.players,
ServerSeatPlayer, SeatPlayer (adds avatarId + avatarImage).
- signalr-service: send my avatarImage on StartMatchmaking/CreatePrivateRoom;
carry it through mapRoom.
- game-store: applyServerState + newOnlineMatch + offline match now populate
avatarId/avatarImage (seat 0 uses your own profile photo).
- render every avatar through the shared <Avatar> component (image → emoji
fallback): ChatScreen, FriendsScreen (requests/friends/chats), GameTable
seats, MatchIntroOverlay, MatchPlayersList, RoomScreen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Playable hand cards are now draggable (Framer drag + dragSnapToOrigin): drag one
up toward the table center and release to play it; release short and it snaps
back. Tapping still plays as before. touch-action:none so the drag gesture works
on mobile without scrolling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compact the in-game scoreboard to a single small pill: team label + score with
trick count inline in parens (e.g. "0 (3)"), a thin dot separator, and a tiny
target number — dropping the tall 3-line columns and large fonts. Frees more
HUD room and reads at a glance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: the trick cards used a Tailwind -translate-x-1/2 -translate-y-1/2 to
center on the felt, but Framer Motion owns `transform` (from x/y/scale), so that
centering class was clobbered. In RTL the auto-positioned card then anchored to
the right edge and the whole trick cross drifted left of center.
Fix: drop the size-0 anchor; position each card at left-1/2 top-1/2 and use
Framer `transformTemplate` to prepend translate(-50%,-50%) before the animated
translate(x,y) scale — so centering survives and the pile sits dead-center in
both LTR and RTL. Burst particles re-centered too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The felt was w-[min(96vw,560px)] inside a p-3 flex container; on phones 96vw
exceeds the padded width, and an overflowing flex item under justify-center in
an RTL layout pins to the start (right) and overflows left — so the trick area
(centered on the felt) drifted off-center. Added max-w-full to cap the felt to
its container so justify-center truly centers it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On narrow phones the top HUD crowded the scoreboard against the trump/speed
badges + action buttons. The badges now show icon/symbol-only on mobile (labels
hidden < sm) with tighter padding, freeing horizontal space so the scoreboard
renders cleanly. Buttons stay shrink-0; scoreboard keeps shrink min-w-0.
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>
- 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>
- Lock the app to portrait: AndroidManifest screenOrientation="portrait" and PWA
manifest orientation "portrait".
- GameTable felt now occupies the middle band (between top HUD and the hand) with
portrait proportions (w<=560, tall) so the you/partner/opponents diamond fits a
tall screen comfortably instead of a wide landscape ellipse.
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>
- GameTable Backdrop (Hakem/Trump/Round/Match-over): scroll when taller than the
viewport via overflow-y-auto + min-h-full centering — no more clipped panels.
- DailyRewardModal: cap height + scroll (was overflow-hidden, clipped the 7-day grid).
- PostMatchRewardsModal: max-h uses dvh (mobile chrome safe).
- ScreenShell: add overflow-x-hidden so a too-wide child can't scroll horizontally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- nginx: serve the HTML shell with Cache-Control no-store so a new deploy (new
chunk hashes) is picked up immediately — fixes the recurring stale-bundle
"page couldn't load" at the source. Hashed /_next/static stays immutable.
- Trick offsets set to a clean symmetric cross.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Trick area: smaller offsets (±50/52) + retuned scale so the played pile sits
centered in the felt instead of flung out to the side seats.
- ErrorBoundary around screens + overlays: a render error now shows a recoverable
in-app message with the cause (and logs componentStack) instead of the browser's
blank "page couldn't load" — helps pinpoint the post-purchase crash.
Verified: tsc + next build clean; web rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Game table: played-card pile now uses sm cards on phones (vw<480) + slightly
tighter scale, so the center trick no longer crowds/overlaps the side seats'
avatars on a tall portrait screen.
- Profile: the screen showed two XP bars (the global header bar + the identity
card's detailed bar). Hide the header bar on Profile (showXp=false).
Verified: tsc + next build clean; web rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The "This page couldn't load" after a redeploy was a stale bundle: a tab open
across a deploy requests JS chunks that no longer exist (ChunkLoadError). Added
a global error/unhandledrejection guard that reloads once to fetch the fresh
bundle (sessionStorage-guarded against loops, cleared after a healthy run).
- Reaction tray width → w-[min(270px,86vw)] so it never overflows narrow phones.
Verified: tsc + next build pass; web image rebuilt on :1500.
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>
Grounded in the two installed design skills:
- Safe-area insets (notch/home-bar): .safe-top/.safe-bottom/.safe-x helpers
applied to the game-table HUD + bottom hand + reaction button, and to
ScreenShell + HomeScreen (covers Profile/Shop/Leaderboard/etc.).
- Touch targets ≥44px: table HUD buttons (mute/forfeit/exit) and the reaction
button now meet the 44/48px minimum.
- HUD readability: seat name/level labels (which float over the felt) get a
text-shadow (.hud-shadow) and stronger contrast.
Verified: tsc + next build clean; web image rebuilt on :1500.
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>
- AuthScreen: phone+OTP is the only active method; email & Google shown as
disabled "coming soon" buttons (until SMTP/Gmail are configured)
- GameTable responsive: compact scoreboard, smaller seat avatars, and
turn-indicator/timer positions tuned for small screens
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PlayerHand measures viewport and compresses the fan (responsive card size +
dynamic overlap) so all 13 cards are visible and tappable on phones — no
off-screen cards, no horizontal scroll; added tap feedback
- globals: html/body overflow-x hidden + overscroll-behavior none
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HUD mute is a master toggle (sound effects + music) via sound-store.toggleAll;
icon reflects fully-muted state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Card design:
- Separate cardFront + cardBack (each own/equip independently)
- Fronts: classic (free), ivory/rosegold (buy), parchment/mint (earned)
- Backs: classic (free), sapphire/emerald (buy), ruby/royal (earned)
- PlayingCard `front` prop; table applies front to all faces, back to opponents
- Profile has front + back pickers; shop has both sections
Audio:
- Web Audio synth engine (no asset files): SFX for card/deal/trump/trick,
win/lose, message, notify, award, levelup, purchase, kot + ambient music
- Toggles in profile (Audio) + mute button in game HUD; prefs persisted
- Wired across game-store, rewards, daily, shop, chat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reactions/emotes in-game: tray of owned emojis + animated per-seat bubbles
(feature named "شکلک / Sheklak"). Packs: starter (free), champion/legend
(earned by rating/wins), emotions/taunt (purchasable in shop)
- OnlineService.sendReaction/onReaction; mock echoes you + random opponents
- Fix: human hakem's 5 cards were blurred behind the trump-chooser overlay —
raise hand to z-50 during choosing-trump so cards stay readable
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>