Commit Graph

37 Commits

Author SHA1 Message Date
soroush.asadi 6530096994 music: re-enable background loop (default = playful) + profile on/off; coins 2000
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m8s
- 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>
2026-06-15 13:23:08 +03:30
soroush.asadi 6502b17356 balance(achievements): strictly-escalating milestone coin rewards
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 32s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s
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>
2026-06-15 11:28:08 +03:30
soroush.asadi 974a6bf0ae feat: shop buy-coins CTA, pin chats (max 3), surrender cooldown, OTP hardening
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 29s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m6s
- 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>
2026-06-15 11:01:14 +03:30
soroush.asadi bc695bc8e9 feat: OTP rate limit, private-room invite UX, in-game UI fixes
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 54s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m11s
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>
2026-06-14 00:30:20 +03:30
soroush.asadi fdf4235fbd feat(auth): real SMS OTP via Kavenegar (replaces the mock 1234 code)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 50s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 34s
- OtpService: generates a 5-digit code, stores it (in-memory, 120s TTL, max 5
  tries, single-use), and sends it via Kavenegar verify/lookup
  (template "hokmotp", %token = code). Normalizes +98/98 → 09xxxxxxxxx.
- /api/auth/otp/request + /verify now use it. No SMS_API_KEY ⇒ dev mode
  (accepts a fixed code, returns devCode for local testing).
- Config: Sms section (appsettings) + Sms__* compose mapping + SMS_* in the
  ENV_FILE template.

Security: sanitized deploy/ENV_FILE.example back to placeholders (it had picked
up real secrets) and added /deploy/ENV_FILE.local to .gitignore as the real
master copy (never committed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:50:33 +03:30
soroush.asadi 83d9c1c7d0 fix(iab): correct Myket purchase verification to the documented POST /verify API
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 29s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 41s
Myket's server-to-server validation is POST
/api/partners/applications/{pkg}/purchases/products/{sku}/verify with the
purchase token in the JSON body ({"tokenId": ...}) + X-Access-Token header —
not a GET with the token in the path. purchaseState 0 = valid.
Ref: https://myket.ir/kb/pages/server-to-server-payment-validation-api/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:12:57 +03:30
soroush.asadi 9cce016b90 config: fix IAB package name + flatten Production Iab example
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s
- appsettings.json + docker-compose default: PackageName com.bargevasat.hokm →
  com.bargevasat.app (the validate API URL embeds it; wrong value breaks
  Bazaar/Myket token verification).
- appsettings.Production.json.example: Iab keys were nested (Iab.Bazaar.*,
  Iab.Myket.*) which don't bind to the flat IabOptions; flattened to
  PackageName / BazaarClientId / ... / MyketAccessToken.

MyketAccessToken is already wired end to end: ENV_FILE IAB_MYKET_ACCESS_TOKEN →
compose Iab__MyketAccessToken → IabOptions.MyketAccessToken → VerifyMyket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:33:58 +03:30
soroush.asadi cd5742d623 iap: wire coin packs to Cafe Bazaar SKUs + auto-select Bazaar billing in the APK
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 32s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m7s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s
- 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>
2026-06-12 14:37:13 +03:30
soroush.asadi 7f08249fa7 fix(iab): correct package name to com.bargevasat.app + slot for Bazaar RSA key
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 36s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m5s
- 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>
2026-06-12 08:55:17 +03:30
soroush.asadi 857287fa84 mobile: fullscreen (immersive Android + PWA) + auto-hide reported nudity avatars
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 23s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m3s
Fullscreen on mobile:
- Android (Capacitor): MainActivity now runs edge-to-edge and hides the status +
  navigation bars (immersive, transient-on-swipe), re-asserted on focus.
- PWA: manifest display -> "fullscreen" with display_override fallback chain;
  viewport gains viewport-fit: cover for proper safe-area/edge-to-edge handling.

Moderation auto-hide:
- ProfileService.ReportUser now de-dupes nudity reports per reporter and, once
  NudityHideThreshold (3) distinct players flag a target's avatar as nudity,
  auto-removes their custom photo (reverts to default avatar). Counted from the
  ledger, so still no schema change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:32:49 +03:30
soroush.asadi 6641741669 feat: photo upload at level 3 + report a player (nudity avatar / chat insult)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m58s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s
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>
2026-06-11 19:12:02 +03:30
soroush.asadi 8033023a1f matchmaking: deterministic 15s wait before bots fill empty seats
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 22s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m4s
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>
2026-06-11 18:27:46 +03:30
soroush.asadi ad5b42db06 feat(profile): "set your city" gamification box → one-time 500-coin reward
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m6s
- 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>
2026-06-11 18:11:45 +03:30
soroush.asadi 5d38312ef0 Marketing site (bargevasat.ir) + admin-editable store links + subdomain split
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 4m40s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m7s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 41s
- New standalone Next.js marketing site under site/ (static export, SEO):
  landing, download/install guide (Bazaar/Myket/iOS-PWA/web), FAQ (JSON-LD),
  privacy, terms, support, /admin link editor. fa RTL, sitemap/robots/manifest.
- Backend: SiteLinksService (JSON-file persisted) + GET /api/site/links (public)
  + POST /api/admin/site/links (X-Admin-Token). ADMIN_TOKEN + Site__DataDir via env.
- compose: hokm-site service (:1520) + hokm_data volume for links JSON.
- CI deploy job builds + deploys the site container.
- deploy/SUBDOMAIN_SPLIT.md: nginx blocks, cert reissue, DNS, ENV split.
- Exclude site/ from root tsc + web docker context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:19:43 +03:30
soroush.asadi 72efc03e2d Shop: every item is coin-priced; level/rank/achievement only gate the purchase
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 6m29s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled
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>
2026-06-07 21:27:25 +03:30
soroush.asadi 0847d2c7cf fix(deploy): don't let docker compose build require runtime JWT_KEY
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 5m58s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m5s
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled
docker compose build interpolates the whole file, so the ${JWT_KEY:?} guard
failed the build step when ENV_FILE lacked JWT_KEY. Default it empty (${JWT_KEY:-})
so build/db steps succeed, and enforce the secret at runtime instead: the server
throws on boot in Production if Jwt:Key is missing/dev/<32 chars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:41:26 +03:30
soroush.asadi 38ac8b06d1 100 gated gifts (level/rating-locked) + requirement system
CI/CD / CI - API (dotnet build + engine sim) (push) Has been cancelled
CI/CD / CI - Web (tsc + next build) (push) Has been cancelled
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled
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>
2026-06-07 00:02:28 +03:30
soroush.asadi e49df07c0f Prod hardening: one-game-per-player, selectable music, bargevasat.ir config
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 7m47s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 1s
- 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>
2026-06-06 23:05:52 +03:30
soroush.asadi 82b2bc0648 Polish: daily reward via celebration overlay + premium chat to recipient
CI/CD / CI - API (dotnet build + engine sim) (push) Has been cancelled
CI/CD / CI - Web (tsc + next build) (push) Has been cancelled
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled
- 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>
2026-06-06 22:26:28 +03:30
soroush.asadi 03dfbe1e67 Match intro "players joining" loading screen + i18n fix; checkpoint
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 7m38s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 1s
- 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>
2026-06-06 21:58:54 +03:30
soroush.asadi cb27a16dc1 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>
2026-06-06 18:39:24 +03:30
soroush.asadi b739b503eb Forfeit = 2x coin loss + 0 XP (no kot); end-of-game roster + add friend
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 21s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m0s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 0s
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>
2026-06-05 10:40:14 +03:30
soroush.asadi b661385a00 Celebration animations for purchases, XP gains & achievement unlocks
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m1s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 0s
- 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>
2026-06-05 09:52:28 +03:30
soroush.asadi 96c8abbeb3 CI/Docker: use SSL-free Nexus endpoints (mirror serves partial chain)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 6m21s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m3s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 1s
The HTTPS Nexus serves an incomplete cert chain that container trust stores
reject (NU1301 PartialChain / UNABLE_TO_GET_ISSUER), failing CI restore/install.
- NuGet has no strict-ssl flag → point CI + Dockerfile + compose at the plain-HTTP
  Nexus (http://171.22.25.73:8081, allowInsecureConnections) — no TLS, no cert check.
- npm: add --strict-ssl=false to the CI web-check install (Dockerfile already had it);
  Docker npm registry default also moved to the HTTP Nexus.
- ENV_FILE.example documents NUGET_INDEX/NPM_REGISTRY overrides.

Local dev (Windows trusts the cert) + image base pulls (Docker trusts it) are
unaffected — only in-container package feeds switch to HTTP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:53:46 +03:30
soroush.asadi fd33f85e9c Store XP packs (expensive), winner 2x XP, premium perks
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
- 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>
2026-06-05 00:08:19 +03:30
soroush.asadi 4199a82c9d More cosmetics (rank-gated) + steeper level curve capped at 100
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
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>
2026-06-04 23:43:21 +03:30
soroush.asadi b66e7f77a5 100+ achievements, forfeit, leagues floor, bot humanize, 95k starter
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
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>
2026-06-04 22:47:36 +03:30
soroush.asadi 7a18bc39e6 Achievements overhaul: 37 achievements, page with tabs, leagues, gating
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m21s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
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>
2026-06-04 21:47:38 +03:30
soroush.asadi fde8b93206 Fix Docker build/runtime: 0.0.0.0 bind, npm ci, HTTP-mirror fallback
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m31s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
Issues found bringing the stack up locally and fixed:
- Server was loopback-only inside the container (appsettings "Urls=localhost"
  wins over ASPNETCORE_URLS) → published port returned "empty reply". Force the
  bind with command-line args: ENTRYPOINT dotnet Hokm.Server.dll --urls 0.0.0.0:5005.
- Web image: npm install crashed on alpine ("Exit handler never called"); root
  cause was UNABLE_TO_GET_ISSUER_CERT_LOCALLY — the Nexus mirror serves a partial
  chain that Node's CA bundle can't complete. Use npm ci + strict-ssl=false.
- .NET restore hit the same partial chain (NU1301 PartialChain). Both registries
  are now build ARGs (NUGET_INDEX / NPM_REGISTRY) defaulting to the HTTPS mirror
  (CI runner trusts it); local .env overrides to the plain-HTTP Nexus
  (http://171.22.25.73:8081) which has no TLS. NuGet feed is generated inline with
  allowInsecureConnections so .NET 10 accepts the HTTP source.

Verified on local Docker (Postgres-backed): db+server+web all healthy; API + web
reachable from host on 1505/1500; auth → profile (1000 coins) → friend add/accept
(bidirectional) → chat (unread) all 200; rows persisted in Postgres
(Profiles=2, Friends=2, Messages=1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:59:27 +03:30
soroush.asadi 89d42184a1 Add Soroush CI/CD (Gitea + Nexus) + self-host fonts for offline build
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
Pipeline (.gitea/workflows/ci-cd.yml), all images/packages via Nexus mirror:
- CI api-build: dotnet restore/build server/Hokm.slnx + run Hokm.Sim (rules).
- CI web-check: npm install + tsc --noEmit + next build (static export).
- deploy (self-hosted): pre-deploy pg_dump backup, rollback image tag, build,
  bring up db -> server -> web with stop+rm+up --no-deps (no force-recreate,
  no bare compose down), health-wait each, prune.

Local stack (docker-compose.yml), ports in 1500-1600 so it coexists with manual
dev on 3000/5005:  web :1500 (nginx static) -> server :1505 (.NET) -> db :1510
(postgres, named volume + backups). Dockerfiles: server (.NET, NuGet via
nuget.docker.config, binds 0.0.0.0, busybox wget healthcheck) + web (Next static
export -> nginx, NEXT_PUBLIC_* baked as build args). nginx.conf SPA fallback.

Config: server CORS is now config-driven (Cors__Origins) so the deployed web
origin is allowed without code edits. deploy/ENV_FILE.example documents the
Gitea ENV_FILE secret; DEPLOY.md covers setup/run/LAN-IP/rollback/migrations.

Fonts: switch Vazirmatn + Plus Jakarta Sans from next/font/google (build-time
Google fetch -> fails on the Iran CI runner) to self-hosted @fontsource-variable
packages. Build is offline and ~3x faster; 7 woff2 emitted into out/.

Verified locally: dotnet build slnx + Hokm.Sim (300 matches, exit 0); tsc clean;
next build clean with self-hosted fonts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:09:31 +03:30
soroush.asadi e778e8b5bd Server-backed friends, chat, IAB scaffold + EF migrations/Postgres
- 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>
2026-06-04 18:26:22 +03:30
soroush.asadi cfed2950b2 Add ZarinPal sandbox payments for buying coins (config-driven merchant)
- 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>
2026-06-04 17:59:30 +03:30
soroush.asadi 4f2e4e14ea Server-authoritative economy: wire client to server; entry + rewards on hub
Server:
- daily (/api/daily, /api/daily/claim) + shop (/api/shop/buy) + ChargeEntry
- GameRoom (via IServiceScopeFactory) deducts ranked entry at match start and
  applies match rewards at match-over, broadcasting profile + reward over the hub
- tested: daily, shop (owned-guard), ranked entry deduction pushed over hub

Client:
- SignalrService routes profile/coins/plan/daily/shop/match to the server (Bearer);
  onProfile/onReward hub events; guest/offline fall back to local
- session-store syncs profile from hub; game-store serverReward; GameScreen shows
  live ranked reward from hub (no double submit), submits client-run games
- single source of truth in live mode (no economy divergence)

Postgres-ready via config (Provider=postgres); EnsureCreated for now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:32:47 +03:30
soroush.asadi d0b8976713 Server persistence: EF Core profiles + coin ledger + authoritative rewards
- EF Core (SQLite dev / Postgres prod via config); ProfileRow JSON blob +
  LedgerRow audit; EnsureCreated at startup
- C# Gamification port (ranks/elo/coins/xp/achievements/titles) → server
  computes match rewards; ProfileService (get/update/plan/buyCoins/applyMatch)
- JWT endpoints: profile GET/PUT, plan, coins packs/buy, match/result;
  auth upserts the profile
- Tested end-to-end (buy + ranked win+kot persisted & server-computed)
- Client still mock-backed for now (wiring is the next step)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:52:25 +03:30
soroush.asadi d04605d118 Brand the app as «برگ وسط» / Barg-e Vasat
- Name + tagline («بازی حکم آنلاین») across i18n (app.title/subtitle),
  layout metadata, PWA manifest, app icon, package name, server health
- Gameplay term «حکم» unchanged; repo/folder stay hokm/HokmPlay

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:01:14 +03:30
soroush.asadi 292dedd843 Show live online-players count on the home screen
- OnlineService.getOnlineCount(); mock random-walks a believable number,
  SignalrService reads GET /api/stats/online (server tracks hub connections)
- Home screen badge with pulsing dot, polls every 8s, localized digits

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:20:51 +03:30
soroush.asadi aaf66b921f Phase G: scaffold .NET 10 + SignalR backend (engine port + hub + auth)
- server/ monorepo: Hokm.Engine (C# port of TS engine+AI, validated by sim),
  Hokm.Server (SignalR GameHub, in-memory matchmaking/rooms, server-side turn
  timers + bot fill + disconnect handling, per-seat state broadcast), Hokm.Sim
- JWT dev auth (OTP 1234 + email); CORS for the Next client; /hub/game
- NuGet restored from mirrors (Soroush Nexus + Liara); NuGetAudit off
- README + .NET .gitignore; static class Engine renamed Rules (namespace clash)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:42:15 +03:30