Victory gets its moment: a gold-ringed proclamation
Winning showed only as a small slip in the rail — anticlimactic, as playtesting put it. The engine now records HOW the game was won (winReason: treasures carried home, or last wizard standing) alongside the winner, the view carries it, and the finish opens a gold-ringed modal: trophy, the winner's name in tall caps, and the manner of victory in a handwritten line. "Behold the final board" dismisses it to the finished table. In hotseat the one screen proclaims to the whole table at once; in async games each player is greeted by it when they next open the game. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8f7beb5ef3
commit
13aeeea0ef
@@ -247,6 +247,7 @@ export interface GameState {
|
|||||||
stack: CastStack | null;
|
stack: CastStack | null;
|
||||||
rng: RngState;
|
rng: RngState;
|
||||||
winner: PlayerId | null;
|
winner: PlayerId | null;
|
||||||
|
winReason: "treasures" | "lastStanding" | null;
|
||||||
pendingDiscard: PlayerId | null;
|
pendingDiscard: PlayerId | null;
|
||||||
/** Monotonic counter for sustained-effect ids. */
|
/** Monotonic counter for sustained-effect ids. */
|
||||||
nextEffectId: number;
|
nextEffectId: number;
|
||||||
@@ -2999,6 +3000,7 @@ export function createGame(config: GameConfig): { state: GameState; events: Game
|
|||||||
stack: null,
|
stack: null,
|
||||||
rng,
|
rng,
|
||||||
winner: null,
|
winner: null,
|
||||||
|
winReason: null,
|
||||||
pendingDiscard: null,
|
pendingDiscard: null,
|
||||||
nextEffectId: 1,
|
nextEffectId: 1,
|
||||||
};
|
};
|
||||||
@@ -4668,6 +4670,7 @@ function checkVictory(state: GameState, events: GameEvent[]): void {
|
|||||||
if (stolenAtHome.length >= 2) {
|
if (stolenAtHome.length >= 2) {
|
||||||
state.phase = "finished";
|
state.phase = "finished";
|
||||||
state.winner = p.id;
|
state.winner = p.id;
|
||||||
|
state.winReason = "treasures";
|
||||||
events.push({ type: "gameWon", player: p.id, reason: "treasures" });
|
events.push({ type: "gameWon", player: p.id, reason: "treasures" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4677,6 +4680,7 @@ function checkVictory(state: GameState, events: GameEvent[]): void {
|
|||||||
if (alive.length === 1) {
|
if (alive.length === 1) {
|
||||||
state.phase = "finished";
|
state.phase = "finished";
|
||||||
state.winner = alive[0]!.id;
|
state.winner = alive[0]!.id;
|
||||||
|
state.winReason = "lastStanding";
|
||||||
events.push({ type: "gameWon", player: alive[0]!.id, reason: "lastStanding" });
|
events.push({ type: "gameWon", player: alive[0]!.id, reason: "lastStanding" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface GameView {
|
|||||||
you: PlayerId;
|
you: PlayerId;
|
||||||
phase: GameState["phase"];
|
phase: GameState["phase"];
|
||||||
winner: PlayerId | null;
|
winner: PlayerId | null;
|
||||||
|
winReason: "treasures" | "lastStanding" | null;
|
||||||
turn: TurnState;
|
turn: TurnState;
|
||||||
activePlayerId: PlayerId;
|
activePlayerId: PlayerId;
|
||||||
/** Board with dynamic wall changes already merged in. */
|
/** Board with dynamic wall changes already merged in. */
|
||||||
@@ -88,6 +89,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
|||||||
you: playerId,
|
you: playerId,
|
||||||
phase: state.phase,
|
phase: state.phase,
|
||||||
winner: state.winner,
|
winner: state.winner,
|
||||||
|
winReason: state.winReason,
|
||||||
turn: state.turn,
|
turn: state.turn,
|
||||||
activePlayerId: state.players[state.turn.activeIndex]!.id,
|
activePlayerId: state.players[state.turn.activeIndex]!.id,
|
||||||
board,
|
board,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
let joinCode = $state("");
|
let joinCode = $state("");
|
||||||
let claimPhrase = $state("");
|
let claimPhrase = $state("");
|
||||||
let showHelp = $state(false);
|
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). */
|
/** Which pending attack the player has already acknowledged (modal dismissed). */
|
||||||
let attackNoticeSeen = $state<string | null>(null);
|
let attackNoticeSeen = $state<string | null>(null);
|
||||||
/** A face-up card being peeked at from the scoresheet (view-only). */
|
/** A face-up card being peeked at from the scoresheet (view-only). */
|
||||||
@@ -697,6 +699,25 @@
|
|||||||
<Help initialTab={helpTab} onclose={() => (showHelp = false)} />
|
<Help initialTab={helpTab} onclose={() => (showHelp = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if view?.phase === "finished" && view.winner && !victorySeen}
|
||||||
|
<div class="scrim attack-scrim" role="alertdialog" aria-label="the game is won">
|
||||||
|
<div class="attack-notice victory-notice">
|
||||||
|
<div class="victory-trophy">🏆</div>
|
||||||
|
<div class="victory-name">{view.winner}</div>
|
||||||
|
<div class="victory-how">
|
||||||
|
{#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}
|
||||||
|
</div>
|
||||||
|
<button class="stamp big" onclick={() => (victorySeen = true)}>Behold the final board</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey}
|
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey}
|
||||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="you are under attack">
|
<div class="scrim attack-scrim" role="alertdialog" aria-label="you are under attack">
|
||||||
<div class="attack-notice">
|
<div class="attack-notice">
|
||||||
@@ -1750,6 +1771,17 @@
|
|||||||
max-width: 11rem;
|
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 {
|
.inspector {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0.9rem;
|
right: 0.9rem;
|
||||||
@@ -1829,6 +1861,17 @@
|
|||||||
max-width: 11rem;
|
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 { right: 0.5rem; bottom: 0.5rem; }
|
||||||
.inspector-card { transform: scale(1.3); }
|
.inspector-card { transform: scale(1.3); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user