The reveal remembers the hand a wizard fell holding
Elimination destroys the evidence: a killed wizard's cards pass to the killer, a treasure-lost wizard's go to the discard — so the post-game reveal showed "empty-handed" for exactly the player whose hand everyone wants to see. Both elimination paths now snapshot the hand at the moment of death (finalHand), the finished view serves it, and the gallery marks those rows "as they fell" in script. Because rooms replay from the command log, already-finished games show their fallen hands too. Test kills a wizard holding a Fireball and a 6 and finds them in the reveal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e7b17f7643
commit
da4ab03135
@@ -61,6 +61,8 @@ export interface PlayerState {
|
|||||||
hand: CardInstance[];
|
hand: CardInstance[];
|
||||||
/** Instance ids of cards displayed face-up (Master Key, Wizardblade, stones). */
|
/** Instance ids of cards displayed face-up (Master Key, Wizardblade, stones). */
|
||||||
displayed: string[];
|
displayed: string[];
|
||||||
|
/** The hand held at the moment of elimination — for the post-game reveal. */
|
||||||
|
finalHand?: CardInstance[];
|
||||||
carriedTreasureId: string | null;
|
carriedTreasureId: string | null;
|
||||||
lostTurns: number;
|
lostTurns: number;
|
||||||
extraTurns: number;
|
extraTurns: number;
|
||||||
@@ -4473,6 +4475,7 @@ function applyDamage(
|
|||||||
if (target.life > 0) return;
|
if (target.life > 0) return;
|
||||||
|
|
||||||
target.alive = false;
|
target.alive = false;
|
||||||
|
target.finalHand = [...target.hand];
|
||||||
events.push({ type: "died", player: target.id, killedBy: attackerId });
|
events.push({ type: "died", player: target.id, killedBy: attackerId });
|
||||||
events.push({ type: "playerEliminated", player: target.id, reason: "killed" });
|
events.push({ type: "playerEliminated", player: target.id, reason: "killed" });
|
||||||
state.sustained = state.sustained.filter((s) => s.targetId !== target.id && s.casterId !== target.id);
|
state.sustained = state.sustained.filter((s) => s.targetId !== target.id && s.casterId !== target.id);
|
||||||
@@ -4656,6 +4659,7 @@ function checkVictory(state: GameState, events: GameEvent[]): void {
|
|||||||
});
|
});
|
||||||
if (lost) {
|
if (lost) {
|
||||||
p.alive = false;
|
p.alive = false;
|
||||||
|
p.finalHand = [...p.hand];
|
||||||
state.discard.push(...p.hand.splice(0));
|
state.discard.push(...p.hand.splice(0));
|
||||||
p.displayed = [];
|
p.displayed = [];
|
||||||
events.push({ type: "playerEliminated", player: p.id, reason: "treasuresLost" });
|
events.push({ type: "playerEliminated", player: p.id, reason: "treasuresLost" });
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
|||||||
})),
|
})),
|
||||||
yourHand: you ? [...you.hand] : [],
|
yourHand: you ? [...you.hand] : [],
|
||||||
revealedHands: state.phase === "finished"
|
revealedHands: state.phase === "finished"
|
||||||
? Object.fromEntries(state.players.map((p) => [p.id, [...p.hand]]))
|
? Object.fromEntries(state.players.map((p) => [p.id, p.alive ? [...p.hand] : [...(p.finalHand ?? p.hand)]]))
|
||||||
: null,
|
: null,
|
||||||
treasures: state.treasures.map((t) => ({ ...t })),
|
treasures: state.treasures.map((t) => ({ ...t })),
|
||||||
deckCount: state.deck.length,
|
deckCount: state.deck.length,
|
||||||
|
|||||||
@@ -315,3 +315,26 @@ describe("speedstone", () => {
|
|||||||
expect(state.turn.movementAllowance).toBe(4);
|
expect(state.turn.movementAllowance).toBe(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the post-game reveal", () => {
|
||||||
|
it("shows an eliminated player's hand as they fell, not their emptied one", async () => {
|
||||||
|
const { viewFor } = await import("../src/view");
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const attacker = activePlayer(state);
|
||||||
|
const defender = state.players.find((p) => p.id !== attacker.id)!;
|
||||||
|
defender.position = { ...attacker.position };
|
||||||
|
defender.life = 1;
|
||||||
|
defender.hand = [
|
||||||
|
{ instanceId: "fireball#J", cardId: "fireball" },
|
||||||
|
{ instanceId: "number-6#J", cardId: "number-6" },
|
||||||
|
];
|
||||||
|
state = must(state, attacker.id, { type: "punch", targetId: defender.id });
|
||||||
|
state = must(state, defender.id, { type: "pass" });
|
||||||
|
expect(state.phase).toBe("finished");
|
||||||
|
// The killer took the cards — but the reveal remembers the fallen hand.
|
||||||
|
const view = viewFor(state, attacker.id);
|
||||||
|
const fallen = view.revealedHands?.[defender.id] ?? [];
|
||||||
|
expect(fallen.map((c) => c.cardId).sort()).toEqual(["fireball", "number-6"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1219,6 +1219,7 @@
|
|||||||
<div class="reveal-row">
|
<div class="reveal-row">
|
||||||
<span class="reveal-name" class:reveal-winner={p.id === view.winner}>
|
<span class="reveal-name" class:reveal-winner={p.id === view.winner}>
|
||||||
{p.id === view.winner ? "🏆 " : ""}{p.id}
|
{p.id === view.winner ? "🏆 " : ""}{p.id}
|
||||||
|
{#if !p.alive}<span class="reveal-note">as they fell</span>{/if}
|
||||||
</span>
|
</span>
|
||||||
<div class="reveal-cards">
|
<div class="reveal-cards">
|
||||||
{#each view.revealedHands[p.id] ?? [] as card (card.instanceId)}
|
{#each view.revealedHands[p.id] ?? [] as card (card.instanceId)}
|
||||||
@@ -1818,6 +1819,7 @@
|
|||||||
padding-top: 0.4rem;
|
padding-top: 0.4rem;
|
||||||
}
|
}
|
||||||
.reveal-name.reveal-winner { font-weight: 700; }
|
.reveal-name.reveal-winner { font-weight: 700; }
|
||||||
|
.reveal-note { display: block; font-family: "Caveat", cursive; font-size: 0.85rem; color: #8a7a5e; }
|
||||||
.reveal-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
.reveal-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
.reveal-cards :global(.card) { transform: scale(0.82); transform-origin: top left; margin: -0.35rem -0.8rem -1.1rem 0; }
|
.reveal-cards :global(.card) { transform: scale(0.82); transform-origin: top left; margin: -0.35rem -0.8rem -1.1rem 0; }
|
||||||
.reveal-empty { font-family: "Caveat", cursive; color: #8a7a5e; padding-top: 0.5rem; }
|
.reveal-empty { font-family: "Caveat", cursive; color: #8a7a5e; padding-top: 0.5rem; }
|
||||||
@@ -1938,6 +1940,7 @@
|
|||||||
padding-top: 0.4rem;
|
padding-top: 0.4rem;
|
||||||
}
|
}
|
||||||
.reveal-name.reveal-winner { font-weight: 700; }
|
.reveal-name.reveal-winner { font-weight: 700; }
|
||||||
|
.reveal-note { display: block; font-family: "Caveat", cursive; font-size: 0.85rem; color: #8a7a5e; }
|
||||||
.reveal-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
.reveal-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
.reveal-cards :global(.card) { transform: scale(0.82); transform-origin: top left; margin: -0.35rem -0.8rem -1.1rem 0; }
|
.reveal-cards :global(.card) { transform: scale(0.82); transform-origin: top left; margin: -0.35rem -0.8rem -1.1rem 0; }
|
||||||
.reveal-empty { font-family: "Caveat", cursive; color: #8a7a5e; padding-top: 0.5rem; }
|
.reveal-empty { font-family: "Caveat", cursive; color: #8a7a5e; padding-top: 0.5rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user