Compare commits
2
Commits
d334b267a2
...
9cc0430ee2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cc0430ee2 | ||
|
|
7bcad12449 |
@@ -215,6 +215,9 @@ export interface CastStack {
|
||||
/** STRENGTH's tear: "this would be an attack" — the grab rides the stack
|
||||
* like a punch, and the clutch roll waits for the counteractions. */
|
||||
tearTreasure?: true;
|
||||
/** What a cardless stack's blow is called in the chronicle (the WARD's
|
||||
* bite); without it the source falls back to "punch from <attacker>". */
|
||||
sourceName?: string;
|
||||
}
|
||||
|
||||
export interface CastParams {
|
||||
@@ -4233,7 +4236,9 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
||||
// Square contents at the destination.
|
||||
let content = state.squareContents[cellKey(p.position)];
|
||||
if (content?.kind === "stone") return err("that square is solid stone");
|
||||
// BIG MAN: nobody enters his square.
|
||||
// BIG MAN: nobody enters his square — except a SHRUNK wizard, small
|
||||
// enough to slip between the giant's boots (table ruling).
|
||||
if (sustainedOn(state, p.id, "shrink").length === 0) {
|
||||
for (const other of state.players) {
|
||||
if (other.id !== p.id && other.alive && cellKey(other.position) === cellKey(p.position) &&
|
||||
sustainedOn(state, other.id, "big-man").length > 0) {
|
||||
@@ -4241,6 +4246,7 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
||||
return err("a giant fills that corridor");
|
||||
}
|
||||
}
|
||||
}
|
||||
// BIG MAN pushes: "You can push monsters or other players (in an adjacent
|
||||
// square) down the corridor ahead of you as you move". No
|
||||
// room to shove them onward means no way forward for the giant either.
|
||||
@@ -4663,7 +4669,7 @@ function doWardChoice(prev: GameState, play: boolean): CommandResult {
|
||||
// recorded the instant bite and replay it.
|
||||
if ((state.config.deckRev ?? 1) >= 9) {
|
||||
openCardlessStack(state, owner.id, taker.id, {
|
||||
params: { damage: 3 }, kind: "spell", trapped: true,
|
||||
params: { damage: 3 }, kind: "spell", trapped: true, sourceName: "warded treasure",
|
||||
});
|
||||
} else {
|
||||
applyDamage(state, events, taker, 3, "warded treasure", null);
|
||||
@@ -6120,7 +6126,7 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
||||
} else if (pipe.damage > 0) {
|
||||
const source = stack.reflectedBase
|
||||
? `${attackId} (reflected)`
|
||||
: attackId ??
|
||||
: attackId ?? stack.sourceName ??
|
||||
(attackingCreature ? `${attackingCreature.kind}'s blow` : `punch from ${attacker.id}`);
|
||||
applyDamage(state, events, defender, pipe.damage, source, attacker.id, pipe.kind);
|
||||
damageDealt = pipe.damage;
|
||||
|
||||
@@ -411,6 +411,33 @@ describe("big man", () => {
|
||||
throw new Error("setup: seed offered no walled pocket beside the home");
|
||||
});
|
||||
|
||||
it("a shrunk wizard slips between the giant's boots", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const mover = activePlayer(state);
|
||||
const giant = state.players.find((p) => p.id !== mover.id)!;
|
||||
state.sustained.push({
|
||||
id: "fx-big", cardId: "big-man", casterId: giant.id, targetId: giant.id,
|
||||
remainingTurns: 9, data: {},
|
||||
});
|
||||
const view = boardView(state);
|
||||
const side = SIDES.find((s) => stepTarget(view, mover.position, s).kind === "step");
|
||||
if (!side) throw new Error("setup: no open step beside the mover");
|
||||
const t = stepTarget(view, mover.position, side);
|
||||
if (t.kind !== "step") throw new Error("unreachable");
|
||||
giant.position = { ...t.to };
|
||||
// Full-size, the corridor is filled.
|
||||
expect(applyCommand(state, mover.id, { type: "move", direction: side }).ok).toBe(false);
|
||||
// Shrunk, the same step slips through.
|
||||
state.sustained.push({
|
||||
id: "fx-small", cardId: "shrink", casterId: mover.id, targetId: mover.id,
|
||||
remainingTurns: 1, data: {},
|
||||
});
|
||||
const r = applyCommand(state, mover.id, { type: "move", direction: side });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
expect(cellKey(r.state.players.find((p) => p.id === mover.id)!.position)).toBe(cellKey(giant.position));
|
||||
});
|
||||
|
||||
it("monsters cannot enter the giant's square", () => {
|
||||
const rig = bigRig();
|
||||
let state = rig.state;
|
||||
|
||||
@@ -292,7 +292,13 @@ describe("the Ward is played in the moment", () => {
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
// The bite rides the stack: a counteraction window opens for the thief.
|
||||
expect(r.state.stack).toBeTruthy();
|
||||
state = drain(r.state);
|
||||
const bite = applyCommand(r.state, r.state.stack!.waitingOn, { type: "pass" });
|
||||
if (!bite.ok) throw new Error(bite.error);
|
||||
// The chronicle names the bite for what it is, not a punch.
|
||||
expect(bite.events.some(
|
||||
(e) => e.type === "damaged" && e.source === "warded treasure",
|
||||
)).toBe(true);
|
||||
state = drain(bite.state);
|
||||
expect(state.wardPending).toBeNull();
|
||||
expect(state.players.find((p) => p.id === "thief")!.life).toBe(12);
|
||||
expect(state.players.find((p) => p.id === "owner")!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
||||
|
||||
@@ -3047,15 +3047,17 @@
|
||||
* Phones keep the bottom hand; the one-column flow re-orders anyway. */
|
||||
@media (min-width: 901px) {
|
||||
.game.hand-left {
|
||||
grid-template-columns: minmax(9.6rem, 10.4rem) minmax(340px, 1fr) minmax(250px, 330px);
|
||||
grid-template-columns: minmax(17.4rem, 18.2rem) minmax(340px, 1fr) minmax(250px, 330px);
|
||||
}
|
||||
.game.hand-left .table-edge { display: contents; }
|
||||
.game.hand-left .table-edge > * { grid-column: 2; min-width: 0; }
|
||||
.game.hand-left .table-edge > .hand {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 6;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 8.2rem);
|
||||
justify-content: center;
|
||||
align-content: start;
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: 0.5rem;
|
||||
@@ -3066,6 +3068,9 @@
|
||||
}
|
||||
.game.hand-left .board-zone { grid-column: 2; grid-row: 1; }
|
||||
.game.hand-left .paper-rail { grid-column: 3; grid-row: 1; }
|
||||
/* With the hand out of the bottom strip, the map claims the height. */
|
||||
.game.hand-left .board-viewport { height: calc(100dvh - 11rem); }
|
||||
.game.hand-left .fpv-primary .board-viewport { height: 30dvh; }
|
||||
}
|
||||
/* The first-person pane wears exactly the board's width, right above
|
||||
* it: the stack shrink-wraps to the board, and width:0 + min-width
|
||||
|
||||
Reference in New Issue
Block a user