fix(games): carry game kind on live events + fix the New Game lock funnel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s

Address review of the active-game limit lock:

- Live-event GameViews (opponent_moved, match_found, game_started, ...) did
  not carry game_kind, so a lobby patch from an event zeroed a game's kind
  and the client's per-kind count under-counted — a capped kind read as
  free (a disabled start instead of the lock, and a wrong per-kind result).
  Thread Kind through notify.GameSummary → the event GameView.

- New Game screen refreshes the lobby games on mount so the per-kind count
  reflects the current set, not a stale cached snapshot.

- The guest funnel's login button routes to the profile screen (the account
  controls), not settings.

- Copy: the guest prompt is "sign in to use all the game's features"; the
  durable notice is "finish your active games to start a new one".

Regression tests: the notify opponent-moved payload carries kind; the client
lock locks only the reached kind (vs_ai at cap leaves random open).
This commit is contained in:
Ilia Denisov
2026-07-10 10:55:48 +02:00
parent 3306a016a0
commit e40adfb0c7
9 changed files with 36 additions and 10 deletions
+16 -3
View File
@@ -18,7 +18,7 @@
import { validDisplayName } from '../lib/profileValidation';
import { verifyPin, type PinLock } from '../lib/pin';
import { commitName, rosterReady, buildSeats, shuffleSeeded, type RosterRow } from '../lib/roster';
import type { AccountRef, Variant } from '../lib/model';
import type { AccountRef, GameView, Variant } from '../lib/model';
import {
availableVariants,
VARIANT_FLAG,
@@ -60,7 +60,10 @@
// funnel modal instead of enqueuing (the server also refuses it); offline never locks (local
// games are uncapped). The lock lifts when the profile is re-fetched after a guest→durable upgrade.
const autoKind = $derived(opponent === 'ai' ? GameKind.vsAi : GameKind.random);
const autoLocked = $derived(!offlineMode.active && isKindLocked(getLobby(false)?.games ?? [], app.profile?.gameLimits, autoKind));
// The player's current games for the per-kind lock: seeded from the lobby snapshot for an instant
// render, then refreshed on mount (below) so a game created since the last lobby visit is counted.
let lobbyGames = $state<GameView[]>(getLobby(false)?.games ?? []);
const autoLocked = $derived(!offlineMode.active && isKindLocked(lobbyGames, app.profile?.gameLimits, autoKind));
let limitOpen = $state(false);
// --- auto-match ---
@@ -129,6 +132,16 @@
);
onMount(async () => {
// Refresh the lobby games so the per-kind lock counts the current set — the cached snapshot can
// lag a game created since the last lobby visit. Online only; the lock never applies offline, and
// the server enforces every cap regardless.
if (!offlineMode.active && connection.online) {
try {
lobbyGames = (await gateway.gamesList()).games;
} catch {
// Keep the cached seed on a transient failure.
}
}
// Offline mode offers only the local vs_ai flow (the friends section is hidden), and the network
// is off, so skip the friend fetch — it would be refused by the transport and toast an error.
if (guest || offlineMode.active) return;
@@ -349,7 +362,7 @@
open={limitOpen}
{guest}
onclose={() => (limitOpen = false)}
onlogin={() => { limitOpen = false; navigate('/settings'); }}
onlogin={() => { limitOpen = false; navigate('/profile'); }}
/>
{:else if offlineMode.active}
<!-- Offline pass-and-play (hotseat): a device-local 2-4 human game. The host sets a master