feat(ui): per-kind active-game limit lock on the New Game screen
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 28s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s

Carry the caller's per-tier active-game caps and each game's kind on the
wire (Profile.game_limits + GameView.kind, additive FBS + gateway transcode
+ client codec, committed regen). The New Game screen counts the player's
active games per kind from the lobby and locks a capped start: an outline
button with a lock that opens a funnel modal instead of a game -- a sign-in
prompt for a guest, a "finish a current game first" notice for a signed-in
account (native Telegram popup, in-app modal elsewhere). The lock lifts via
the existing profile refetch after a guest->durable upgrade.

Remove the lobby's old at_game_limit New-Game tab disable + notice: the flag
(now the random-kind cap) conflicted with the per-kind lock -- it hid the
screen where the lock lives and wrongly blocked an unfulfilled kind. The New
Game tab is always enabled; the per-kind start lock is the only gate. The
at_game_limit wire field stays (unused by the client) for a later cleanup.

Tests: client lock logic + codec kind/game_limits roundtrip + gateway
transcode encode + native popup builders (unit); a mock e2e for the lock
badge and the modal.
This commit is contained in:
Ilia Denisov
2026-07-10 10:09:45 +02:00
parent e45167041f
commit 3306a016a0
45 changed files with 811 additions and 130 deletions
+19 -10
View File
@@ -37,7 +37,7 @@ status — without re-deriving decisions.
| E5 | Payment intake | 2 | DONE |
| E6 | Ads | 2 | DONE |
| E7 | Admin, reports & catalog | 2 | DONE |
| E8 | Guest limits | — | WIP |
| E8 | Guest limits | — | DONE |
| E9 | Tournament fee | future | TODO |
**Release 1** = full mechanics with no real money, exercised via `admin_grant` (E0→E1→E2→E3).
@@ -782,7 +782,7 @@ become bootstrap-only.
## E8 — Guest limits
**Status:** WIP · **standalone** (game-behaviour change) · depends on: none · mechanics: PAYMENTS §6.
**Status:** DONE · **standalone** (game-behaviour change) · depends on: none · mechanics: PAYMENTS §6.
**Goal.** A registration funnel: cap what a guest can do, enforced **server-side** (today UI-only),
with configurable per-tier × per-kind limits so the pressure tunes without a release.
@@ -833,12 +833,19 @@ payments mixed in.
`game.Service.AtGameLimit`; the **`internal/gamelimits` config + hot cache** (loaded at boot,
invalidated on edit); the admin **kind column** in both game lists (`/_gm/games` + the user card)
and a **config editor** (`/_gm/limits`) for the six limits.
- **PR2 — wire + client:** `Profile.game_limits` (the caller's tier) + `GameView.kind` (FBS +
transcode + codec); the client per-kind active count from the lobby; the **lock badge** on a capped
new-game start button, with two messages by tier — a **guest** login-funnel modal ("Войдите или
создайте учётную запись…", Отмена / Вход→profile) and, for a **durable** account at its cap, a plain
notice ("Вы достигли лимита одновременных игр, сначала завершите текущие", ОК; **native** popup in
Telegram — no gesture-gated API follows it); the profile re-fetch after a guest→durable upgrade.
- ~~**PR2 — wire + client** — DONE.~~ `Profile.game_limits` (the caller's tier) + `GameView.kind` (FBS
+ gateway transcode + client codec, committed regen); the client counts active games per kind from
the lobby cache (`gamelimits.ts`) and locks a capped new-game start — an **outline 🔒** button that
opens `GameLimitModal` instead of a game, native (Telegram `showPopup`) or the in-app `Modal`
elsewhere, with two messages by tier: a **guest** sign-in funnel ("Войдите или создайте учётную
запись…", Отмена / Вход→`/settings`) and, for a **durable** account at its cap, a plain notice
("Вы достигли лимита одновременных игр, сначала завершите текущие", ОК). The lock lifts via the
existing profile re-fetch after a guest→durable upgrade.
- **Decision (owner-agreed): the lobby's old `at_game_limit` New-Game tab-disable + notice is
removed.** The `at_game_limit` flag (now the random-kind cap) conflicted with the per-kind lock —
it hid the New-Game screen where the lock lives, and wrongly blocked starting an unfulfilled kind.
The tab is always enabled; the per-kind lock on the start button is the only gate. The wire field
`GameList.at_game_limit` stays (unused by the client) for a later cleanup.
**Tests.**
@@ -846,8 +853,10 @@ payments mixed in.
(domain + HTTP 403); guest blocked from a 2nd vs_ai / 2nd random (+ `at_game_limit`); durable on the
higher tier; the durable friends cap + config cache reflecting an admin edit; accept stays exempt.
- unit (PR1, done): the per-tier/kind limit resolution (`Cap`, `LimitsFor`).
- UI (PR2): the lock + the two modals on a capped button (mock); the profile re-fetch lifts the lock
after register.
- unit + UI (PR2, done): the client lock logic (`gamelimits.ts` — count/cap/lock), the codec kind +
game_limits roundtrip, the gateway transcode game_limits encode, the popup builders, and a mock e2e
(`gamelimit.spec.ts`) — a capped start shows 🔒, opens the modal without navigating, and the lock
clears when the profile refetch lifts the cap.
**Done-criteria.** A guest is server-capped (default 1 vs_ai + 1 random, configurable) and cannot
friend/invite; a durable account is capped at 10 per kind (configurable); the client shows the lock +