Ops hardening plus two rules gaps close (rev 13)
Operational: auto-reboot for kernel patches (09:30 UTC), Caddy access logs at /var/lib/caddy (self-rotating; the sandbox denies /var/log), logrotate for the backup log, a restore drill proving a Spaces snapshot boots 56/56 rooms clean, the about page's plain sentence on permanent recording, and /wizwar-pulse to read it all weekly. Rules: safes now smash — attacks aimed at the box accumulate on it and the fifteenth point bursts it (widening, ungated). And SLOW DEATH's bites pause for a victim holding an ABSORB — the card named by the FAQ, not Absorb Spell — each soaking exactly one point, rev-gated at 13 with the instant-bite legacy pinned. Bots weigh the soak against their life. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
a66b058c68
commit
92613d7386
@@ -793,6 +793,13 @@
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
if (selectedCard && cardDef(selectedCard.cardId).cardType === "attack" &&
|
||||
view.squareContents[cellKey(cell)]?.kind === "safe") {
|
||||
// Brute force: attacks batter the safe itself (fifteen points bursts it).
|
||||
dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "cell", cell } }));
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
if (selectedCard?.cardId === "relocate-sector") {
|
||||
// Any board click (re-)picks the sector; the landing is chosen from the
|
||||
// dashed ghost slots beyond the maze, since every on-board slot is taken.
|
||||
@@ -1555,6 +1562,29 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if view?.slowDeathPending && view.slowDeathPending.playerId === view.you}
|
||||
{@const sdAbsorbs = view.yourHand.filter((c) => c.cardId === "absorb")}
|
||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="slow death bites">
|
||||
<div class="attack-notice">
|
||||
<div class="attack-title">Slow Death bites for {view.slowDeathPending.points}!</div>
|
||||
<div class="attack-power">
|
||||
"ABSORB against SLOW DEATH will only stop the damage incurred by
|
||||
one card" — each Absorb you spend soaks exactly one point.
|
||||
</div>
|
||||
<div class="attack-actions">
|
||||
{#if sdAbsorbs.length > 0}
|
||||
{@const firstAbsorb = sdAbsorbs[0]!}
|
||||
<button class="stamp primary"
|
||||
onclick={() => dispatch({ type: "slowDeathChoice", absorbInstanceId: firstAbsorb.instanceId })}>
|
||||
Absorb a point ({sdAbsorbs.length} in hand)</button>
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={() => dispatch({ type: "slowDeathChoice" })}>
|
||||
take {view.slowDeathPending.points === 1 ? "the point" : `all ${view.slowDeathPending.points}`}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if feedbackOpen && net.roomId}
|
||||
<div class="scrim attack-scrim" role="button" tabindex="-1"
|
||||
onclick={() => (feedbackOpen = false)} onkeydown={() => {}}>
|
||||
|
||||
@@ -158,6 +158,13 @@
|
||||
the <a href="/?fpv">first-person workshop</a> walks the maze through
|
||||
a wizard's own eyes (add <code>&demo=1</code> to watch a reel).
|
||||
</p>
|
||||
<h3>What the table remembers</h3>
|
||||
<p>
|
||||
Every game here is recorded permanently, move by move — table talk
|
||||
included — so finished games can be replayed, shared, and studied.
|
||||
Play under whatever name you like, but know that what you say and
|
||||
do at the table becomes part of its lasting record.
|
||||
</p>
|
||||
<h3>Send word</h3>
|
||||
<p>
|
||||
Feedback, bug reports, and faint praise all welcome — ravens fly to
|
||||
|
||||
@@ -80,6 +80,10 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
||||
case "madDash": return `${e.player} MAD DASHES — every stride doubles: ${e.newAllowance} movement this turn!`;
|
||||
case "safeOpened": return `${e.player}'s ${cardDef(e.withCardId).name} clicks the safe open — until turn's end.`;
|
||||
case "safeDamaged": return `${e.attacker} batters the safe — ${e.amount} damage (${e.total}/15).`;
|
||||
case "safeSmashed": return `💥 The safe BURSTS open under ${e.attacker}'s assault!`;
|
||||
case "slowDeathWindow": return `Slow Death bites ${e.player} for ${e.points} — an Absorb hovers over the wound…`;
|
||||
case "slowDeathAbsorbed": return `${e.player}'s Absorb soaks one point of the rot (${e.remaining} to go).`;
|
||||
case "spellSustained": return `${spellName(e.cardId)} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
||||
case "spellExpired": return `${spellName(e.cardId)} wears off ${e.target}.`;
|
||||
case "teleported": return e.by === e.player
|
||||
|
||||
Reference in New Issue
Block a user