Walls and doors fall to sustained assault
"It is possible, though time-consuming, to punch a wall down. A wall takes 20 points of damage to destroy; a door takes 15. Any attack against an inanimate object counts as your one attack for the turn." Damage accumulates per edge in wallDamage (remapped through sector rotations, public in the view), fed two ways: a punchWall command for the bare-fisted (1 point, from a square touching the edge) and attack spells cast at an edge target — LOS to the wall for L.O.S. cards, touching it for same-square cards, amplify and power-attack honored, wand charges spent, no counteractions since stonework plays none. Thrown daggers and rocks clatter to the floor at the foot of the wall. At the threshold the edge opens through the same override path destroy-wall uses. On the table: damaged walls wear spreading cracks, an attack card's hint offers "or a wall line to batter it" with the edge layer live, and a "Punch a wall…" stamp arms a click-the-wall mode. The chronicle counts the blows: "alice batters the wall with bare fists — 3/20." Not deployed — a live game is in progress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
64454ffc3e
commit
41c384274d
@@ -24,6 +24,8 @@
|
||||
let peekCard = $state<CardInstance | null>(null);
|
||||
/** When the peeked card is a creature on the board, its live stats ride along. */
|
||||
let peekCreatureId = $state<string | null>(null);
|
||||
/** Bare-knuckle demolition: click a wall to punch it. */
|
||||
let punchWallMode = $state(false);
|
||||
let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play");
|
||||
let hotseatCount = $state(2);
|
||||
let setupName = $state("");
|
||||
@@ -105,7 +107,12 @@
|
||||
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner"]);
|
||||
|
||||
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu", "thief", "swap-meet", "remove-curse", "swarthmores-enchantment", "illusionary-attack"]);
|
||||
const edgeSelectMode = $derived(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId));
|
||||
const attackVsWall = $derived(
|
||||
selectedCard != null && cardDef(selectedCard.cardId).cardType === "attack" && !EDGE_CARDS.has(selectedCard.cardId),
|
||||
);
|
||||
const edgeSelectMode = $derived(
|
||||
(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId)) || attackVsWall || punchWallMode,
|
||||
);
|
||||
const cellSelectMode = $derived(
|
||||
(selectedCard != null && CELL_CARDS.has(selectedCard.cardId)) || pendingCellFor !== null,
|
||||
);
|
||||
@@ -117,6 +124,7 @@
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
punchWallMode = false;
|
||||
ambushVia = null;
|
||||
ambushTrigger = null;
|
||||
ambushSpell = null;
|
||||
@@ -461,11 +469,17 @@
|
||||
}
|
||||
|
||||
function clickEdge(cell: { x: number; y: number }, side: Side) {
|
||||
if (punchWallMode) {
|
||||
dispatch({ type: "punchWall", cell, side });
|
||||
punchWallMode = false;
|
||||
return;
|
||||
}
|
||||
if (!selectedCard || !edgeSelectMode) return;
|
||||
dispatch({
|
||||
type: "cast",
|
||||
instanceId: selectedCard.instanceId,
|
||||
target: { kind: "edge", cell, side },
|
||||
...(attackVsWall && attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}),
|
||||
});
|
||||
clearSelection();
|
||||
}
|
||||
@@ -1105,8 +1119,8 @@
|
||||
{#if selectedDef}
|
||||
<strong class="hint-name">{selectedDef.name}</strong>
|
||||
{#if edgeSelectMode}<span>— click a wall line</span>{/if}
|
||||
{#if selectedDef.cardType === "attack" && !edgeSelectMode && !cellSelectMode}
|
||||
<span>— click a target{attachedNumber ? ` (powered by a ${numberTotal})` : " (tap a number card to power it)"}</span>
|
||||
{#if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}
|
||||
<span>— click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}</span>
|
||||
{/if}
|
||||
{#if attachedMods.length > 0}
|
||||
<span class="hint-mods">[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]</span>
|
||||
@@ -1183,6 +1197,9 @@
|
||||
onclick={() => dispatch({ type: "pickUpObject", instanceId: obj.instanceId })}>
|
||||
Pick up {cardDef(obj.cardId).name}</button>
|
||||
{/each}
|
||||
<button class="stamp" class:primary={punchWallMode} disabled={view.turn.attackUsed && !punchWallMode}
|
||||
onclick={() => (punchWallMode = !punchWallMode)}>
|
||||
{punchWallMode ? "Click the wall to punch — or cancel" : "Punch a wall…"}</button>
|
||||
<button class="stamp" onclick={startDiscardMode}>Discard cards…</button>
|
||||
<label class="inline draw-pick">
|
||||
draw
|
||||
|
||||
@@ -312,6 +312,21 @@
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- battle damage: cracks spread as a wall or door takes attacks -->
|
||||
{#each Object.entries(view.wallDamage) as [key, dmg] (key)}
|
||||
{@const kind = key.split(":")[0]}
|
||||
{@const wx = Number(key.split(":")[1]?.split(",")[0])}
|
||||
{@const wy = Number(key.split(":")[1]?.split(",")[1])}
|
||||
{@const frac = Math.min(1, dmg / 20)}
|
||||
{#if kind === "V"}
|
||||
<line x1={(wx + 1) * CELL} y1={wy * CELL + 3} x2={(wx + 1) * CELL} y2={(wy + 1) * CELL - 3}
|
||||
class="crack" style:opacity={0.35 + frac * 0.65} />
|
||||
{:else}
|
||||
<line x1={wx * CELL + 3} y1={(wy + 1) * CELL} x2={(wx + 1) * CELL - 3} y2={(wy + 1) * CELL}
|
||||
class="crack" style:opacity={0.35 + frac * 0.65} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- illusions YOU know are fake: ghostly dashed lines -->
|
||||
{#each view.knownIllusionEdges as key (key)}
|
||||
{@const kind = key.split(":")[0]}
|
||||
@@ -604,6 +619,13 @@
|
||||
text-anchor: middle; pointer-events: none;
|
||||
}
|
||||
.carried { fill: gold; stroke: #111; stroke-width: 1; }
|
||||
.crack {
|
||||
stroke: #efe8d4;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 3 5;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
}
|
||||
.edge-hit { fill: rgba(30, 120, 240, 0.15); cursor: crosshair; }
|
||||
.edge-hit:hover { fill: rgba(30, 120, 240, 0.5); }
|
||||
</style>
|
||||
|
||||
@@ -129,6 +129,10 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "treasurePickedUp": return `${e.player} grabs ${e.owner}'s treasure!`;
|
||||
case "objectDropped": return `${e.player} sets down the ${cardDef(e.card.cardId).name}${e.forced ? " (forced)" : ""}.`;
|
||||
case "objectPickedUp": return `${e.player} picks up the ${cardDef(e.card.cardId).name} — actions over.`;
|
||||
case "wallDamaged": {
|
||||
const what = e.needed === 15 ? "door" : "wall";
|
||||
return `${e.player} batters the ${what} with ${e.source === "punch" ? "bare fists" : cardDef(e.source).name} — ${e.total}/${e.needed}.`;
|
||||
}
|
||||
case "treasureDropped": return e.onHomeOf ? `${e.player} drops a treasure on ${e.onHomeOf}'s home base!` : `${e.player} drops a treasure.`;
|
||||
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated — both treasures lost!` : null;
|
||||
case "cardsDiscarded": return `${e.player} discards ${e.cards.length} card(s).`;
|
||||
|
||||
Reference in New Issue
Block a user