Credibility pass: comments say what the code can't, and the seams are gone
Engine: pit-rim exits live in one helper (pitRimExits) shared by the resolver and the automaton; chargeReach names the six-stride cap; dust sight is inDust + dustAtEnd; PushPending is exported once; the force-field counter builds its events in one list; drawUnderSlowDeath says what it draws under. Test groups are named for the rule they pin, not the revision that introduced it. Client: one smoothstep; one edgeScar per battle-scarred edge; one board-zone rule (which also seats the caption strip on phones); the compass names live in SIDE_NAMES; net.flash() is the one toast; MediaQuery replaces two hand-rolled matchMedia listeners; sprites carry their sizes as plain numbers and their CSS in one rule each; canvas helpers are formatted like the rest of the tree. Comments that told how a cel or a fallback used to look now say what it draws. The actions-over notice no longer blames a pickup when slime ended the turn. Server and ops: dataRoot() is the one data directory; headlineOf builds its deeds without a cast; the rollup and skills read the same way the code behaves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
85690f0e23
commit
6e49fc9020
+27
-30
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { motion } from "./motion";
|
||||
import { attentionLabel, net, spellName } from "./net.svelte";
|
||||
import { attentionLabel, net, SIDE_NAMES, spellName } from "./net.svelte";
|
||||
import { MediaQuery } from "svelte/reactivity";
|
||||
import { FISTS } from "./fpv/paintedFx";
|
||||
import Board from "./Board.svelte";
|
||||
import LiveFirstPerson from "./LiveFirstPerson.svelte";
|
||||
@@ -16,7 +17,7 @@
|
||||
import { prefs, savePrefs } from "./prefs.svelte";
|
||||
import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art";
|
||||
import { local } from "./local.svelte";
|
||||
import { allCardDefs, cardDef, dreadDistance, isNumberCard, isPermanentDuration, SIDES, stepTarget, cellKey, edgeKey, isMovableObject, sightedCellsFor, bentSightedCellsFor, stackSightTrace, type GameView, eligibleCellsFor } from "@wizwar/engine";
|
||||
import { allCardDefs, cardDef, dreadDistance, isNumberCard, isPermanentDuration, opposite, SIDES, stepTarget, cellKey, edgeKey, isMovableObject, sightedCellsFor, bentSightedCellsFor, stackSightTrace, type GameView, eligibleCellsFor } from "@wizwar/engine";
|
||||
import type { CardInstance, GameEvent, Side } from "@wizwar/engine";
|
||||
|
||||
net.connect();
|
||||
@@ -305,7 +306,6 @@
|
||||
let rotateCW = $state(true);
|
||||
/** pick-lock / master-key: prop the door for others once it opens. */
|
||||
let holdDoor = $state(false);
|
||||
/** mega-monster: which stat the chosen monster doubles. */
|
||||
/** MEGA-MONSTER aimed at a monster: which of its two numbers to double. */
|
||||
let megaPrompt = $state<{ instanceId: string; creatureId: string } | null>(null);
|
||||
function castMega(boost: "life" | "movement") {
|
||||
@@ -345,8 +345,9 @@
|
||||
);
|
||||
const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you);
|
||||
const youMustShield = $derived(view?.chaosPending?.queue[0] === view?.you && view != null);
|
||||
/** Picking up an object ends the turn's actions: the hand goes quiet until
|
||||
* the draw — but never while a counter, shield, or discard is owed. */
|
||||
/** Picking up an object or sliding into slime ends the turn's actions: the
|
||||
* hand goes quiet until the draw — but never while a counter, shield, or
|
||||
* discard is owed. */
|
||||
const actionsSpent = $derived(
|
||||
view != null && isYourTurn && view.turn.actionsEnded &&
|
||||
!youMustRespond && !youMustShield && !youMustDiscard,
|
||||
@@ -465,16 +466,15 @@
|
||||
* the spell being built (or answered), controls and all — table talk
|
||||
* returns the moment the cast resolves. Phones keep the under-board
|
||||
* strips; a rail below the fold is no place to aim from. */
|
||||
const wideQuery = typeof matchMedia === "undefined" ? null : matchMedia("(min-width: 901px)");
|
||||
let wideScreen = $state(wideQuery?.matches ?? true);
|
||||
wideQuery?.addEventListener("change", (e) => (wideScreen = e.matches));
|
||||
const wideQuery = new MediaQuery("(min-width: 901px)", true);
|
||||
const wideScreen = $derived(wideQuery.current);
|
||||
/** A wide, short screen — a laptop in landscape — stands the hand beside
|
||||
* the board unless the player has said otherwise: a tall maze under a
|
||||
* bottom hand leaves a postage stamp, and a first turn deserves better. */
|
||||
const shortWideQuery = typeof matchMedia === "undefined" ? null : matchMedia("(min-width: 1180px) and (max-height: 820px)");
|
||||
let shortWide = $state(shortWideQuery?.matches ?? false);
|
||||
shortWideQuery?.addEventListener("change", (e) => (shortWide = e.matches));
|
||||
const handLeftOn = $derived(prefs.handLeftChosen ? prefs.handLeft : shortWide);
|
||||
* bottom hand leaves a postage stamp, and a first turn deserves better.
|
||||
* (The narrower "short window" rule in the stylesheet only shrinks the
|
||||
* cards; this one needs room for a card column, hence the wider floor.) */
|
||||
const shortWideQuery = new MediaQuery("(min-width: 1180px) and (max-height: 820px)", false);
|
||||
const handLeftOn = $derived(prefs.handLeftChosen ? prefs.handLeft : shortWideQuery.current);
|
||||
const castPanelOn = $derived(wideScreen && !local.active &&
|
||||
(selectedCard != null || (youMustRespond && !!view?.stack)));
|
||||
|
||||
@@ -588,14 +588,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Attachment plumbing for a cast command: the chosen mods and the
|
||||
* attached NUMBER ride along (an explicit numberInstanceIds wins). */
|
||||
/** Your ALTER EGO, when it is the selected token: casts leave from its square. */
|
||||
const castingDouble = $derived(
|
||||
selectedCreature && view
|
||||
? view.creatures.find((c) => c.id === selectedCreature && c.kind === "alter-ego" && c.controllerId === view!.you) ?? null
|
||||
: null,
|
||||
);
|
||||
/** Attachment plumbing for a cast command: the chosen mods, the attached
|
||||
* NUMBER (an explicit numberInstanceIds wins), and the double it casts through. */
|
||||
function withMods<T extends Parameters<typeof net.command>[0] & { type: "cast" }>(cmd: T): T {
|
||||
applyMods(cmd);
|
||||
if (castingDouble && cmd.target) cmd.via = castingDouble.id;
|
||||
@@ -669,7 +669,7 @@
|
||||
discardSelection = next;
|
||||
return;
|
||||
}
|
||||
if (actionsSpent) return; // picking up an object ended the turn's actions
|
||||
if (actionsSpent) return;
|
||||
if (!isYourTurn && !yourMoment) {
|
||||
// Live interruption: Interrupt / Opportunity Fire may be played during
|
||||
// another player's turn (when no attack is pending).
|
||||
@@ -945,8 +945,7 @@
|
||||
// A card in hand is aimed, not walked with: a tap on a bare square
|
||||
// while holding one is a miss, never a stride into whatever is there.
|
||||
if (selectedCard) {
|
||||
net.error = `${cardDef(selectedCard.cardId).name} is aimed by tapping a wizard, a creature, or a slime — put the card down to walk`;
|
||||
setTimeout(() => { if (net.error?.startsWith(cardDef(selectedCard!.cardId).name)) net.error = null; }, 5000);
|
||||
net.flash(`${cardDef(selectedCard.cardId).name} is aimed by tapping a wizard, a creature, or a slime — put the card down to walk`);
|
||||
return;
|
||||
}
|
||||
// A cell click is a move if the cell is one legal step away (the server
|
||||
@@ -981,7 +980,7 @@
|
||||
y: me.position.y + (side === "S" ? 1 : side === "N" ? -1 : 0) };
|
||||
if (view.squareContents[cellKey(pit)]?.kind !== "pit") continue;
|
||||
for (const out of SIDES) {
|
||||
if (out === (side === "N" ? "S" : side === "S" ? "N" : side === "E" ? "W" : "E")) continue;
|
||||
if (out === opposite(side)) continue;
|
||||
const foot = stepTarget(view.board, pit, out);
|
||||
if (foot.kind !== "blocked" && cellKey(foot.to) === cellKey(cell)) { tryMove(side, undefined, out); return; }
|
||||
}
|
||||
@@ -1533,10 +1532,10 @@
|
||||
// answer to "how can he even see me?" when sight ran through a warp mouth.
|
||||
const sightTrace = $derived(view ? stackSightTrace(view) : null);
|
||||
|
||||
/** The step light: with no card in hand, your square and the squares a
|
||||
* stride reaches are lit and the rest of the maze falls into shadow. */
|
||||
/** A first game's first turn lights itself; after that only by choice. */
|
||||
const firstTurnEver = $derived(!!view && prefs.finishedGames === 0 && view.turn.round === 1);
|
||||
/** The step light: with no card in hand, your square and the squares a
|
||||
* stride reaches are lit and the rest of the maze falls into shadow. */
|
||||
const stepLight = $derived(
|
||||
!!view && !!me && (prefs.stepLightAlways || firstTurnEver) && isYourTurn && !view.turn.actionsEnded && !selectedCard &&
|
||||
!net.spectating && !youMustRespond && view.turn.movementAllowance - view.turn.movementUsed > 0,
|
||||
@@ -1651,7 +1650,7 @@
|
||||
<span class="mast-sub">{fpvWorkshop ? "the eyes workshop" : tokenWorkshop ? "the token workshop" : "the flourish workshop"}</span>
|
||||
<a class="mast-leave" href="/">← back to the table</a>
|
||||
<a class="mast-leave" href="/clips">the clips</a>
|
||||
<a class="mast-leave mast-help-solo" href="/?tokens">tokens</a>
|
||||
<a class="mast-leave mast-right" href="/?tokens">tokens</a>
|
||||
<a class="mast-leave" href="/?fx">flourishes</a>
|
||||
<a class="mast-leave" href="/?fpv">eyes</a>
|
||||
</header>
|
||||
@@ -1813,7 +1812,7 @@
|
||||
<div class="attack-actions">
|
||||
{#each view.pushPending.exits as d (d)}
|
||||
<button class="stamp primary" onclick={() => dispatch({ type: "pushChoice", direction: d })}>
|
||||
{d === "N" ? "North" : d === "S" ? "South" : d === "E" ? "East" : "West"}
|
||||
{SIDE_NAMES[d]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -2803,10 +2802,10 @@
|
||||
{/if}
|
||||
{#if view.turn.actionsEnded}
|
||||
{#if me?.carriedTreasureId}
|
||||
picking up ended your actions for this turn, and the treasure comes with you —
|
||||
your actions for this turn are over, and the treasure comes with you —
|
||||
<strong>End turn</strong> now, then carry it home to score.
|
||||
{:else}
|
||||
picking up ended your actions for this turn — <strong>End turn</strong> when ready.
|
||||
your actions for this turn are over — <strong>End turn</strong> when ready.
|
||||
{/if}
|
||||
{:else}
|
||||
{view.turn.movementAllowance - view.turn.movementUsed} moves left{view.turn.attackUsed ? " · attack spent" : ""} · treasures home {treasuresHome} of 2
|
||||
@@ -3372,7 +3371,7 @@
|
||||
.workshop-mast { margin: 0 1rem 0; }
|
||||
.mast-home { text-decoration: none; }
|
||||
.mast-home:hover { color: #fff; }
|
||||
.mast-help-solo { margin-left: auto; }
|
||||
.mast-help-solo, .mast-right { margin-left: auto; }
|
||||
.hotseat-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; justify-content: center; margin-top: 1.3rem; }
|
||||
.hotseat-exp { margin-top: 0.4rem; margin-bottom: 0; font-size: 0.85rem; }
|
||||
|
||||
@@ -3500,7 +3499,6 @@
|
||||
margin: 0.2rem 0 0.1rem;
|
||||
}
|
||||
/* The eyes toggle floats over the board's corner so it costs the maze no height. */
|
||||
.board-zone { position: relative; flex-direction: column; align-items: stretch; }
|
||||
.view-toggle { position: absolute; top: 0.25rem; right: 0.25rem; z-index: 3; }
|
||||
.step-caption {
|
||||
align-self: flex-start;
|
||||
@@ -3816,7 +3814,7 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.board-zone { display: flex; align-items: flex-start; justify-content: center; min-width: 0; }
|
||||
.board-zone { position: relative; display: flex; flex-direction: column; align-items: stretch; min-width: 0; }
|
||||
/* Hand at the left: the cards stand in their own column beside the
|
||||
* board while the hint strips and action buttons stay beneath it —
|
||||
* the strip dissolves into the grid so its children place themselves.
|
||||
@@ -3831,7 +3829,7 @@
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
}
|
||||
.paper-rail { overflow-y: auto; min-height: 0; padding-right: 2px; }
|
||||
.board-zone { flex-direction: column; align-items: stretch; min-height: 0; }
|
||||
.board-zone { min-height: 0; }
|
||||
.table-stack { flex: 1 1 0; min-height: 0; }
|
||||
.board-frame { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
|
||||
.board-viewport { height: auto; flex: 1 1 0; min-height: 0; }
|
||||
@@ -3979,7 +3977,6 @@
|
||||
padding-left: 0.3rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.score-row.active .score-name { font-weight: 700; }
|
||||
.score-temper {
|
||||
font-family: "Caveat", cursive;
|
||||
font-size: 0.85rem;
|
||||
|
||||
Reference in New Issue
Block a user