diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 3b33bde..ab35d15 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -247,6 +247,7 @@ export interface GameState { stack: CastStack | null; rng: RngState; winner: PlayerId | null; + winReason: "treasures" | "lastStanding" | null; pendingDiscard: PlayerId | null; /** Monotonic counter for sustained-effect ids. */ nextEffectId: number; @@ -2999,6 +3000,7 @@ export function createGame(config: GameConfig): { state: GameState; events: Game stack: null, rng, winner: null, + winReason: null, pendingDiscard: null, nextEffectId: 1, }; @@ -4668,6 +4670,7 @@ function checkVictory(state: GameState, events: GameEvent[]): void { if (stolenAtHome.length >= 2) { state.phase = "finished"; state.winner = p.id; + state.winReason = "treasures"; events.push({ type: "gameWon", player: p.id, reason: "treasures" }); return; } @@ -4677,6 +4680,7 @@ function checkVictory(state: GameState, events: GameEvent[]): void { if (alive.length === 1) { state.phase = "finished"; state.winner = alive[0]!.id; + state.winReason = "lastStanding"; events.push({ type: "gameWon", player: alive[0]!.id, reason: "lastStanding" }); } } diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index a73e3bb..2dbe8fd 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -37,6 +37,7 @@ export interface GameView { you: PlayerId; phase: GameState["phase"]; winner: PlayerId | null; + winReason: "treasures" | "lastStanding" | null; turn: TurnState; activePlayerId: PlayerId; /** Board with dynamic wall changes already merged in. */ @@ -88,6 +89,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView { you: playerId, phase: state.phase, winner: state.winner, + winReason: state.winReason, turn: state.turn, activePlayerId: state.players[state.turn.activeIndex]!.id, board, diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 36cf080..4d2abd3 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -15,6 +15,8 @@ let joinCode = $state(""); let claimPhrase = $state(""); let showHelp = $state(false); + /** The finished game whose victory fanfare has been dismissed. */ + let victorySeen = $state(false); /** Which pending attack the player has already acknowledged (modal dismissed). */ let attackNoticeSeen = $state(null); /** A face-up card being peeked at from the scoresheet (view-only). */ @@ -697,6 +699,25 @@ (showHelp = false)} /> {/if} + {#if view?.phase === "finished" && view.winner && !victorySeen} +
+
+
🏆
+
{view.winner}
+
+ {#if view.winReason === "treasures"} + carried two stolen treasures home — the maze is theirs + {:else if view.winReason === "lastStanding"} + the last wizard standing + {:else} + wins the game + {/if} +
+ +
+
+ {/if} + {#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey}
@@ -1750,6 +1771,17 @@ max-width: 11rem; } + .victory-notice { border-color: #a5842c; box-shadow: 0 0 0 4px rgba(201, 167, 42, 0.35), 0 18px 50px rgba(0, 0, 0, 0.6); } + .victory-trophy { font-size: 3rem; line-height: 1; } + .victory-name { + font-family: "Oswald", sans-serif; + font-size: 1.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #43331f; + } + .victory-how { font-family: "Caveat", cursive; font-size: 1.25rem; color: #6b5a41; } + .inspector { position: fixed; right: 0.9rem; @@ -1829,6 +1861,17 @@ max-width: 11rem; } + .victory-notice { border-color: #a5842c; box-shadow: 0 0 0 4px rgba(201, 167, 42, 0.35), 0 18px 50px rgba(0, 0, 0, 0.6); } + .victory-trophy { font-size: 3rem; line-height: 1; } + .victory-name { + font-family: "Oswald", sans-serif; + font-size: 1.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #43331f; + } + .victory-how { font-family: "Caveat", cursive; font-size: 1.25rem; color: #6b5a41; } + .inspector { right: 0.5rem; bottom: 0.5rem; } .inspector-card { transform: scale(1.3); } }