From 6aae40ad5fcf847b45b308f3b10cbf0718dbb2d8 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 19 Aug 2026 09:45:12 -0400 Subject: [PATCH] Player preferences: art set, auto-grab, flourishes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A ⚙ preferences slip in the masthead, saved to this device: - Token art, all or nothing: the photographed cardboard or the hand-drawn vectors (the workshop's per-category query params stay as the undocumented editing override). - Ending a turn on a lone grabbable treasure picks it up on the way out — never on your own home, never when two share the square. - Spell flourishes on or off, honored by the live board and the replay reel alike (trap notices still fire; they are information). Preferences change what you see or what routine commands the client sends for you — never what is legal. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc --- packages/web/src/App.svelte | 57 ++++++++++++++++++++++++++++++++ packages/web/src/Replay.svelte | 3 +- packages/web/src/art.ts | 20 +++++------ packages/web/src/prefs.svelte.ts | 41 +++++++++++++++++++++++ 4 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 packages/web/src/prefs.svelte.ts diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 219f1b1..d9a7aa1 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -9,6 +9,7 @@ import FxGallery from "./FxGallery.svelte"; import TokenGallery from "./TokenGallery.svelte"; import { scheduleFx, type BoardFx } from "./fx"; + import { prefs, savePrefs } from "./prefs.svelte"; import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art"; import { local } from "./local.svelte"; import { allCardDefs, cardDef, isNumberCard, isPermanentDuration, SIDES, stepTarget, cellKey, edgeKey, isMovableObject, sightedCellsFor, stackSightTrace, type GameView, eligibleCellsFor } from "@wizwar/engine"; @@ -56,6 +57,7 @@ for (const e of events) { if (e.type === "trapSprung" && e.player === view.you && e.cardId) trapNotice = e.cardId; } + if (!prefs.flourishes) return; fxCancels.push(scheduleFx( events, view, (fx) => (boardFx = [...boardFx, fx]), @@ -850,6 +852,13 @@ function endTurn() { clearSelection(); + // Preference: a lone grabbable treasure underfoot is picked up on the + // way out (pickup ends actions; ending the turn is still legal after). + if (prefs.autoGrab && view && me && !me.carriedTreasureId && + !view.turn.actionsEnded && treasuresHere.length === 1 && + cellKey(me.position) !== cellKey(me.home)) { + dispatch({ type: "pickUpTreasure" }); + } dispatch({ type: "endTurn", draw: drawCount }); } @@ -942,6 +951,13 @@ // Press-and-hold on a wall/door/fire segment (touch has no hover). let peekEdge = $state(null); + /** The preferences slip (client-side taste, saved on this device). */ + let prefsOpen = $state(false); + function setPref(k: K, v: (typeof prefs)[K]) { + prefs[k] = v; + savePrefs(); + } + // A shimmering illusion wall, tapped or bumped: offer the belief test. let illusionPrompt = $state<{ cell: { x: number; y: number }; side: Side } | null>(null); function illusionEdgeParts(key: string): { cell: { x: number; y: number }; side: Side } { @@ -1097,6 +1113,7 @@
Wiz-War sixth edition + {#if local.active} hotseat @@ -1180,6 +1197,37 @@ {/if} + {#if prefsOpen} +
(prefsOpen = false)} onkeydown={() => {}}> + +
+ {/if} + {#if faqCardId} (faqCardId = null)} /> {/if} @@ -2394,6 +2442,15 @@ .stamp.big { font-size: 0.95rem; padding: 0.6rem 1.3rem; } .stamp.tiny { font-size: 0.62rem; padding: 0.22rem 0.5rem; box-shadow: 1px 1px 0 rgba(0,0,0,0.3); } .stamp.discard-one { background: #8a7a5e; } + .prefs-slip { text-align: left; } + .pref-row { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.55rem 0; + font-size: 0.85rem; + } + .pref-note { font-size: 0.72rem; color: #8a7a5e; margin-top: 0.6rem; } .slip.council { background: #ece4f1; border-color: #7a6f9a; } .council-head { font-weight: 700; diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte index b013152..0163c6f 100644 --- a/packages/web/src/Replay.svelte +++ b/packages/web/src/Replay.svelte @@ -2,6 +2,7 @@ import Board from "./Board.svelte"; import { humanize } from "./net.svelte"; import { scheduleFx, type BoardFx } from "./fx"; + import { prefs } from "./prefs.svelte"; import { stackSightTrace } from "@wizwar/engine"; import type { GameEvent, GameView } from "@wizwar/engine"; @@ -30,7 +31,7 @@ let boardFx = $state([]); $effect(() => { const step = steps[Math.min(idx, steps.length - 1)]; - if (!step) return; + if (!step || !prefs.flourishes) return; const cancel = scheduleFx( step.events, step.view, (fx) => (boardFx = [...boardFx, fx]), diff --git a/packages/web/src/art.ts b/packages/web/src/art.ts index 590c3ef..d6d8fdf 100644 --- a/packages/web/src/art.ts +++ b/packages/web/src/art.ts @@ -1,19 +1,19 @@ -// The art workshop's side door (deliberately undocumented): query params -// like ?svgPlayers=true&svgTerrain=true swap token categories to the -// hand-drawn SVG set in public/tokens-svg. Under `npm run dev`, editing -// those files shows on the next refresh. +// Which art set a token draws from: the player's preference, with the +// workshop's per-category query params (?svgPlayers=true, ...) as a +// deliberate override for editing sessions under `npm run dev`. +import { prefs } from "./prefs.svelte"; + export type ArtCategory = "players" | "creatures" | "terrain" | "objects"; const q = new URLSearchParams(typeof location === "undefined" ? "" : location.search); -const SVG_ART: Record = { - players: q.get("svgPlayers") === "true", - creatures: q.get("svgCreatures") === "true", - terrain: q.get("svgTerrain") === "true", - objects: q.get("svgObjects") === "true", +const PARAM: Record = { + players: "svgPlayers", creatures: "svgCreatures", terrain: "svgTerrain", objects: "svgObjects", }; export function tokenArt(file: string, cat: ArtCategory): string { - return SVG_ART[cat] ? `/tokens-svg/${file}.svg` : `/tokens/${file}.png`; + const override = q.get(PARAM[cat]); + const drawn = override !== null ? override === "true" : prefs.art === "drawn"; + return drawn ? `/tokens-svg/${file}.svg` : `/tokens/${file}.png`; } // Which token file depicts each board thing (shared by the board renderer diff --git a/packages/web/src/prefs.svelte.ts b/packages/web/src/prefs.svelte.ts new file mode 100644 index 0000000..df37988 --- /dev/null +++ b/packages/web/src/prefs.svelte.ts @@ -0,0 +1,41 @@ +// Player preferences: purely client-side taste, saved on this device. +// Nothing here touches the rules — a preference may change what you SEE +// or what routine commands the client sends on your behalf, never what +// is legal. + +export interface Prefs { + /** Token art: the photographed cardboard, or the hand-drawn vectors. */ + art: "photo" | "drawn"; + /** Standing on lone grabbable gold at end of turn: pick it up first. */ + autoGrab: boolean; + /** Spell flourishes (the animated effects layer). */ + flourishes: boolean; +} + +const KEY = "wizwar-prefs"; + +function load(): Prefs { + const fallback: Prefs = { art: "photo", autoGrab: false, flourishes: true }; + try { + const raw = localStorage.getItem(KEY); + if (!raw) return fallback; + const p = JSON.parse(raw) as Partial; + return { + art: p.art === "drawn" ? "drawn" : "photo", + autoGrab: p.autoGrab === true, + flourishes: p.flourishes !== false, + }; + } catch { + return fallback; + } +} + +export const prefs = $state(load()); + +export function savePrefs(): void { + try { + localStorage.setItem(KEY, JSON.stringify(prefs)); + } catch { + // A full or blocked localStorage loses persistence, not the session. + } +}