82648a4398
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
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 1m58s
The in-game hint badge ignored the payments hint wallet: loading a game clobbered app.profile.hintBalance with the deprecated StateView.wallet_balance (zeroed in the D31 domain removal but left in the protocol as a dead 0, then synced over the real balance at game load). Finish the removal honestly. StateView carries the per-game allowance alone (hints_remaining); the purchasable wallet lives solely on the profile and the client adds it (lib/hints). Removed StateView.wallet_balance across every layer — backend StateView/DTO, notify.PlayerState (live events), gateway StateResp + wire.StateView, the client model/codec/mock/localgame — and dropped the now-unused wallet arg from hintsRemaining. The FBS field is tombstoned `(deprecated)` (not deleted) so the vtable slots after it stay stable across a rolling deploy; no accessor is generated. HintResult keeps wallet_balance (the real post-spend payments balance the client adopts into the profile). The StateView type no longer has walletBalance, so the clobber cannot return without a compile error. Tests: hintsRemaining (2-arg); hints.hintsLeft (allowance + live wallet, no strip); the game state/hint integration (allowance-only HintsRemaining); gateway transcode; FBS regen.
46 lines
2.3 KiB
TypeScript
46 lines
2.3 KiB
TypeScript
// Hint-count derivation, kept out of the .svelte component so it is unit-testable.
|
|
//
|
|
// The badge shows the per-game hint allowance remaining plus the player's global hint wallet. The
|
|
// view's hints_remaining is the per-game allowance alone; the wallet is global (shared across every
|
|
// game) and read live from the profile (payments), never the per-game snapshot — so a wallet hint
|
|
// spent in another game stays reflected here.
|
|
|
|
import type { StateView } from './model';
|
|
|
|
/**
|
|
* hintsLeft is the hint count for the badge: the per-game allowance remaining (view.hintsRemaining)
|
|
* plus the live global wallet balance (passed in from the profile, so it stays correct when a wallet
|
|
* hint was spent in another game since this view was fetched).
|
|
*/
|
|
export function hintsLeft(
|
|
view: Pick<StateView, 'hintsRemaining'> | null,
|
|
walletBalance: number,
|
|
): number {
|
|
if (!view) return 0;
|
|
return Math.max(0, view.hintsRemaining) + Math.max(0, walletBalance);
|
|
}
|
|
|
|
/** HINT_GATE_MS is the idle time a vs_ai player must be stuck on a turn before a hint unlocks. */
|
|
export const HINT_GATE_MS = 30 * 60 * 1000;
|
|
|
|
/**
|
|
* hintGateRemainingMs returns how long (in milliseconds) until a vs_ai game's idle hint unlocks — 0
|
|
* once it is available. It counts down from a MONOTONIC clock (performance.now()), so a client clock
|
|
* change cannot skew it: gateStartMono is performance.now() captured when the source's "seconds left"
|
|
* was received, gateLeftMs is that seconds-left in ms, and monoNow is performance.now() now; the
|
|
* remaining is gateLeftMs minus the monotonic time elapsed since the anchor. gateStartMono null means
|
|
* the gate is open (the human's first move, or a non-gated game). The duration comes from the source
|
|
* (the SERVER clock online, the device clock offline) and is re-fetched on every load, so the wait
|
|
* both persists across a relaunch and stays immune to a live clock change.
|
|
*/
|
|
export function hintGateRemainingMs(gateStartMono: number | null, gateLeftMs: number, monoNow: number): number {
|
|
if (gateStartMono === null) return 0;
|
|
return Math.max(0, gateLeftMs - (monoNow - gateStartMono));
|
|
}
|
|
|
|
/** hintLockMinutes rounds a remaining-milliseconds gap up to whole minutes for the "available in N
|
|
* min." toast, so a gate that is still closed never reads 0. */
|
|
export function hintLockMinutes(remainingMs: number): number {
|
|
return Math.ceil(remainingMs / 60000);
|
|
}
|