feat: UNO-style table, social hub, cosmetics, speed mode, store IAB
Game table & play - UNO-style restyle: suit-aware bolder cards (+xl size), pulsing playable glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain. - Per-league turn time via turnMsForStake: 15s starter/AI, 10s pro, 7s expert; mirrored server-side in GameRoom.TurnMs. - Speed (Blitz) mode for vs-AI/private: 5s turns, race to 5, ~halved pacing. - Matchmaking waits ~15s (randomized 12-18s) then fills bots; elapsed timer + hint. Rewards / gifts - Richer post-match modal (floating coins, XP bar), celebration overlay reveals the unlocked sticker pack, boosted daily rewards (client+server synced), themed 7-day daily with special day-7. Social - Public profile modal (identity, stats, achievement board) from leaderboard / friends / discover / end-of-game roster; rate-limited add-friend (10/hour). - Social hub: Friends / Discover (player search + suggestions) / Messages inbox. - Profile gender (shown in finder/profile) + social links with public/friends/ hidden visibility, enforced server-side. Cosmetics - Distinct card backs: per-design pattern families (stripes/argyle/grid/dots/ rays/scales/crosshatch/royal/filigree/gem) + luxury motifs (lib/cardBack.ts), consistent on table/shop/profile; +Peacock/Rose-Gold backs. - Purchasable titles (shop Titles section); title shown under the seat on the table and in discover/public profile. - 10 new sticker packs (banter/kol-kol, Persian trends, court cards, moods). - Persistent level+XP bar on Home and every inner screen. Payments - Buy-coins gateway opens in a new tab (no SPA dead-end) + focus refresh. - Store IAB scaffolding: Cafe Bazaar deep-link purchase + redirect-token capture, Myket native-bridge contract, server-side IabService.Verify for both stores, config-driven via Iab__* env. POST /api/coins/iab/verify (JWT). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+21
-7
@@ -60,16 +60,27 @@ npm run build # next static export
|
||||
## 3. Feature status (DONE)
|
||||
|
||||
- Full offline vs-AI game (engine, AI, turn timer + auto-play, disconnect/reconnect sim).
|
||||
- Online multiplayer over SignalR: matchmaking (pro skips queue, bots fill after wait), live server-run ranked games, server-authoritative entry/rewards.
|
||||
- Online multiplayer over SignalR: matchmaking (pro skips queue, bots fill after wait), live server-run ranked games, server-authoritative entry/rewards. **Matchmaking waits ~15s (randomized 12–18s) for humans, then bots fill** (`GameManager.NextQueueWaitMs`; mock mirrors it in `beginSearch`). MatchmakingScreen shows the elapsed timer + a bot-fill hint.
|
||||
- **Per-league turn time** (think faster in higher leagues): Starter/vs-AI/private → **15s**, Pro (stake ≥500) → **10s**, Expert (stake ≥1000) → **7s**. Single source: `turnMsForStake(stake, speed?)` in `gamification.ts`; the live server mirrors it in `GameRoom.TurnMs`. The turn-timer bar reads it from `matchMeta.stake`.
|
||||
- **Speed (Blitz) mode** — CLIENT-ONLY (vs-AI + private rooms; ranked stays standard). Flat **5s** turn clock (`SPEED_TURN_MS`), races to **5** points (`SPEED_TARGET_SCORE`), and ~½ pacing on animations/pauses (the `fast()` scaler in `game-store.scheduleAuto`). Threaded via `matchMeta.speed` + `GameSettings.speed`/`OnlineMatchConfig.speed`. Toggle on Home's vs-Computer card; a `SpeedBadge` (⚡) shows on the table HUD. No server change needed — private rooms are client-driven even in live mode, and ranked is intentionally excluded.
|
||||
- **Economy:** coins; ranked entry = stake (win +stake [+kot 40], lose −stake); free vs-computer/private rooms. Buy-coins via **ZarinPal sandbox** (merchant `299685fb-cadf-4dfc-98e2-d4af5d81528d`, config-driven). Coin packs: starter 50k/95,000﷼, … Stores (Bazaar/Myket) must use their **IAB** (`/api/coins/iab/verify` scaffolded; token verification TODO).
|
||||
- **XP/levels:** every game grants XP, **winner ×2**; **premium (pro) ×1.5**; max level 100; curve `100*lvl + 15*lvl²`. **Store sells XP packs** (xp1 +200/5k, xp2 +600/12k, xp3 +1500/25k coins; consumable; unlocks level achievements).
|
||||
- **Achievements:** ~100, metric-driven generator (categories: victory/kot/streak/hakem/level/rank/veteran), incl. "7× hakem", "7–0 sweep". Dedicated **AchievementsScreen** (tabbed) + Profile summary. Some unlock **sticker packs**.
|
||||
- **Cosmetics:** avatars, titles (incl. expert/professional/captain/leader ladder), card **front**+**back**, reaction packs, sticker packs (custom SVG art incl. crown/seven-zip/streak-fire). Profile **photo upload gated at level ≥ 25**.
|
||||
- **Social:** friends + chat **server-persisted** (`Social/SocialService`, REST + hub `friendRequest`/`social`/`chat`); friend remove needs confirm. **Premium chat = animated gold bubbles**.
|
||||
- **Cosmetics:** avatars, titles (incl. expert/professional/captain/leader ladder), card **front**+**back**, reaction packs, **16 sticker packs** (all custom inline-SVG art in `Sticker.tsx`). **✨ Luxury tier** (premium giftable items): luxury avatars (🦢🎩💎💰🏆 + 💠 rank-gated), luxury card backs (Diamond/Black Gold/Platinum/Peacock/Rose-Gold) + fronts (Diamond/Black Gold), luxury titles (Hokm Sultan/Emperor/Grandmaster). Shop tags items priced ≥2000 with a gold **«ویژه/Luxury»** badge + ring.
|
||||
- **Card backs are pattern-distinct** (not just recoloured): each `CardBackDef` carries a `pattern` (`stripes/argyle/grid/dots/rays/scales/crosshatch/royal/filigree/gem`) + optional `motif` glyph. Rendering lives in `src/lib/cardBack.ts` (`cardBackVisual`/`cardBackMotif`/`backVisualFromDef`), used by `PlayingCard`, the shop preview, and the profile picker so all three match.
|
||||
- **Purchasable titles:** `TitleDef.price` makes a title buyable; shop **Titles** section (`ShopItemKind` now includes `"title"`, server `ShopBuy` handles `title → OwnedTitles`, mock mirrors it). The equipped title shows **under your name on the table** (`SeatPlayer.title` → `SeatAvatar`, seat 0 from your profile in every mode incl. live via `applyServerState`) and in the **Discover/find list** (`PlayerSummary.title`, server `ToSummary`). Localize via `titleById(id)`. New themed packs: **کلکل/banter** (kolkol, tikeh, shakkak, raghib), **Persian trends/praise** (trends, tashvigh), **court cards** (khanevadeh: تکخال/آس دل/شاه خشت/بیبی گشنیز), moods (ehsasat). Banter uses the `Stamp` helper (rounded badge + Persian phrase); court cards use `CourtCard`. Profile **photo upload gated at level ≥ 25**. New sticker packs are **client-only** (server `ShopBuy` is generic) — add art to `Sticker.tsx` + an entry in `STICKER_PACKS`.
|
||||
- **Daily rewards** (boosted): `[300, 500, 750, 1000, 1500, 2500, 7500]` — **must stay in sync** between client `gamification.ts DAILY_REWARDS` and server `ProfileService.DailyRewards` (server is authoritative for the claim). Day 7 is the gold "special" tier.
|
||||
- **Social:** friends + chat **server-persisted** (`Social/SocialService`, REST + hub `friendRequest`/`social`/`chat`); friend remove needs confirm. **Premium chat = animated gold bubbles**. **Friend-request rate limit = 10 / rolling hour** per user (server `SocialService.TryRecordRequest`, static in-memory; mirrored in the mock). Both `addFriend(query)` and `addFriendById(userId)` funnel through it.
|
||||
- **Public profiles:** tap any player in the **leaderboard / friends list / end-of-game roster** → `PublicProfileModal` (global, `ui-store.viewProfile(id)`) shows their identity, stats, and **achievement board** + a rate-limited **Add-friend** button. Server `GET /api/profile/{id}/public` → `PublicProfileDto` (no coins/phone/email); mock synthesizes deterministic stats seeded from the id. Client `OnlineService.getPublicProfile(id)` / `addFriendById(id)`.
|
||||
- **Social hub:** `FriendsScreen` is now tabbed — **Friends / Discover / Messages**. **Discover** = find-friends search (debounced) + suggested players, each row taps to the public profile and has a rate-limited Add button. **Messages** = conversation inbox (`listConversations`, unread badges, relative time) → opens `ChatScreen`. New: `OnlineService.searchPlayers(q)` / `suggestedPlayers()` → `PlayerSummary[]`; server `GET /api/players/search?q=` + `/api/players/suggested` (`SocialService.SearchPlayers`/`Suggested`, online-first, excludes friends/self). Mock synthesizes results.
|
||||
- **Profile gender + social links:** `UserProfile.gender` (`""|male|female|other`, shown as ♂/♀/⚧ in Discover + public profile + edited in `ProfileScreen`'s `SocialSettings`), `socials` (instagram/telegram/x/youtube handles or URLs, rendered as tappable chips), and `socialsVisibility` (**public / friends / hidden**). Helpers in `src/lib/social.ts` (`GENDER_META`, `SOCIAL_PLATFORMS`, `socialUrl`, `hasSocials`). **Privacy is server-enforced:** `SocialService.GetPublicProfile` only includes `Socials` when `public`, or `friends` && the viewer is a friend, or it's you; `hidden` → never. `PlayerSummary.gender` carried in discovery. Server fields on `ProfileDto` (`Gender`/`Socials`/`SocialsVisibility`); `ProfileService.Update` parses them; `updateProfile` patch widened (client interface + session-store + mock + signalr).
|
||||
- **Forfeit:** request + teammate-confirm (server `GameRoom` forfeit flow); penalty = **lose 2× coins + 0 XP** (NO kot, and never mention kot); confirm dialog alerts the penalty.
|
||||
- **End-of-game roster:** `MatchPlayersList` on the final screen (reward modal + AI match-over) lists everyone; **Add-friend** button for real non-bot players (seat `userId` threaded from server).
|
||||
- **Celebrations:** `celebration-store` + `CelebrationOverlay` — animated XP count-up, level-up pop, achievement unlock; fires from shop purchases (XP/cosmetic) and unlocks. Reusable via `celebrate({...})`.
|
||||
- **UX/UI:** "Persian luxury" palette (navy/teal/gold, glass) + **UNO-style tactile UX** rolled out to Home (hero Play), Shop (detail sheets), Lobby, Matchmaking, Profile, Leaderboard. Primitives in `globals.css`: `.press-3d` (tactile press), `.safe-top/.safe-bottom/.safe-x` (notch), `.hud-shadow`, `.premium-chat`. Online count floored at **≥50**. Match stays alive on exit (minimize/resume + ResumeGameBar). **No fake/periodic notifications** (removed as spam).
|
||||
- **Celebrations:** `celebration-store` + `CelebrationOverlay` — animated XP count-up, level-up pop, achievement unlock; fires from shop purchases (XP/cosmetic) and unlocks. Reusable via `celebrate({...})`. Achievement rows (here + `PostMatchRewardsModal`) now reveal the **sticker pack** an achievement unlocks (`stickerPackForAchievement`).
|
||||
- **Persistent level + XP bar:** `LevelXpBar` (avatar + Lv + progress, taps to profile) shows on Home (`TopBar`) and atop every inner screen (`ScreenHeader`, `showXp` default on) so level/XP is always visible.
|
||||
- **Buy-coins gateway** opens in a **new tab** (`window.open(_blank)`, same-tab fallback if popup-blocked) so a slow/blocked ZarinPal page can't dead-end the SPA; balance refreshes on window focus. (Fixes the old `window.location.href` "page couldn't load" crash.)
|
||||
- **UX/UI:** "Persian luxury" palette (navy/teal/gold, glass) + **UNO-style tactile UX** rolled out to Home (hero Play), Shop (detail sheets), Lobby, Matchmaking, Profile, Leaderboard. Primitives in `globals.css`: `.press-3d` (tactile press), `.safe-top/.safe-bottom/.safe-x` (notch), `.hud-shadow`, `.premium-chat`, **`.tap`** (44px min hit area). Online count floored at **≥50**. Match stays alive on exit (minimize/resume + ResumeGameBar). **No fake/periodic notifications** (removed as spam).
|
||||
- **Accessibility pass:** global **`:focus-visible`** gold ring (keyboard/controller/switch nav — no pointer required); **reduced-motion** honored app-wide via a `@media (prefers-reduced-motion)` block (kills decorative CSS loops) **and** `<MotionConfig reducedMotion="user">` in `page.tsx` (tames all Framer Motion). Cramped 32px icon buttons (Friends accept/decline/msg/remove, Chat back/send, Room invite/bot/clear) bumped to **44px**. Empty states (Friends/Chat/Notifications) and loading **skeletons** (Leaderboard/Shop) added.
|
||||
- **Capacitor Android APK** builds (Myket maven mirror at root `https://maven.myket.ir`; init script pins buildTools 36 + JDK17). See `ANDROID.md`.
|
||||
- **CI/CD** (Gitea Actions + Nexus mirror) + Docker stack. See `DEPLOY.md`.
|
||||
|
||||
@@ -87,8 +98,8 @@ npm run build # next static export
|
||||
## 5. TODO / next
|
||||
|
||||
1. **Generate real EF migrations** (`dotnet ef migrations add Init`, DesignTimeDbContextFactory targets Postgres) + point at live **Supabase**; today the server uses `EnsureCreated()` (auto-switches to `Migrate()` once migrations exist).
|
||||
2. **Deeper game-table UNO restyle** (bigger tactile cards, clearer turn/HUD, punchier win/trick feedback) — the last UI surface not yet refreshed.
|
||||
3. Store **IAB** token verification (Cafe Bazaar Poolakey / Myket) — `/api/coins/iab/verify` is a stub.
|
||||
2. **Game-table UNO restyle — DONE** (bolder suit-aware cards + `xl` size, pulsing playable-card glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain).
|
||||
3. Store **IAB** — **scaffolded** (`server/.../Payments/IabService.cs`, config-driven via `Iab__*`). **Cafe Bazaar** path is end-to-end pure-web: client deep-links `bazaar://in_app?...&sku=...&redirect_url=...` (`src/lib/storeBilling.ts`), Bazaar returns `?purchaseToken=`, `page.tsx` captures it → `verifyIab` → server OAuth-refresh→validate→credit. **Myket** verification is wired server-side, but its purchase trigger needs a **native Capacitor bridge** (`window.MyketBilling.purchase(sku)` → `{purchaseToken,productId}`) — see §6. **Remaining:** fill `IAB_*` creds (Bazaar client id/secret/refresh token, Myket access token) in `ENV_FILE`, confirm the exact Bazaar/Myket validate endpoints against your panels, write the Myket native plugin, and set per-build `NEXT_PUBLIC_STORE`=`bazaar|myket` + `NEXT_PUBLIC_APP_PACKAGE`. SKU == coin-pack id (`p1`–`p4`). `IAB_ALLOW_UNVERIFIED=true` credits without verifying — **dev only**.
|
||||
4. Iranian **push** provider for closed-app notifications (FCM/APNs blocked); in-app + real-time notifications already work.
|
||||
5. Optional: colored-chat visibility to OTHER players (needs sender-plan on chat messages); route daily-reward through the celebration overlay.
|
||||
|
||||
@@ -101,3 +112,6 @@ npm run build # next static export
|
||||
- After schema changes in SQLite dev with `EnsureCreated()`, delete `server/src/Hokm.Server/hokm.db*` to recreate.
|
||||
- Background `dotnet run &` from a Git-Bash shell dies when the shell exits; use a tracked background runner.
|
||||
- Commit messages end with `Co-Authored-By: Claude …`. Both `messages/`-style i18n strings live in `src/lib/i18n.tsx` (fa+en).
|
||||
- **Myket native bridge contract** (for the Capacitor plugin to inject on `window`):
|
||||
`window.MyketBilling = { available: true, purchase(sku): Promise<{purchaseToken, productId}>, consume?(token): Promise<void> }`.
|
||||
`storeBilling.getStore()` returns `"myket"` when `available` is true; the client then calls `purchase(sku)` and POSTs the token to `/api/coins/iab/verify`. Until the plugin exists, Myket purchases report "unavailable" and fall back to the web gateway. Cafe Bazaar needs **no** native code (deep-link only).
|
||||
|
||||
Reference in New Issue
Block a user