Player preferences: art set, auto-grab, flourishes

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-19 09:45:12 -04:00
co-authored by Claude Fable 5
parent 5310fbdca9
commit 6aae40ad5f
4 changed files with 110 additions and 11 deletions
+57
View File
@@ -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<string | null>(null);
/** The preferences slip (client-side taste, saved on this device). */
let prefsOpen = $state(false);
function setPref<K extends keyof typeof prefs>(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 @@
<header class="masthead">
<span class="mast-title">Wiz-War</span>
<span class="mast-sub">sixth edition</span>
<button class="mast-leave" title="preferences" onclick={() => (prefsOpen = !prefsOpen)}> preferences</button>
{#if local.active}
<span class="mast-room">hotseat</span>
<button class="mast-leave" onclick={() => local.leave()}>set the game aside</button>
@@ -1180,6 +1197,37 @@
</div>
{/if}
{#if prefsOpen}
<div class="scrim attack-scrim" role="button" tabindex="-1"
onclick={() => (prefsOpen = false)} onkeydown={() => {}}>
<div class="attack-notice prefs-slip" role="dialog" aria-label="preferences" tabindex="-1"
onclick={(ev) => ev.stopPropagation()} onkeydown={() => {}}>
<div class="attack-title">Preferences</div>
<div class="pref-row">
<span>Token art</span>
<button class="stamp tiny" class:lit={prefs.art === "photo"}
onclick={() => setPref("art", "photo")}>cardboard</button>
<button class="stamp tiny" class:lit={prefs.art === "drawn"}
onclick={() => setPref("art", "drawn")}>hand-drawn</button>
</div>
<label class="pref-row">
<input type="checkbox" checked={prefs.autoGrab}
onchange={(e) => setPref("autoGrab", e.currentTarget.checked)} />
<span>Ending my turn on a lone treasure picks it up (never on my own home)</span>
</label>
<label class="pref-row">
<input type="checkbox" checked={prefs.flourishes}
onchange={(e) => setPref("flourishes", e.currentTarget.checked)} />
<span>Spell flourishes (the animated effects)</span>
</label>
<div class="pref-note">Saved on this device. Rules are never affected.</div>
<div class="attack-actions">
<button class="hint-cancel" onclick={() => (prefsOpen = false)}>done</button>
</div>
</div>
</div>
{/if}
{#if faqCardId}
<Faq cardId={faqCardId} onclose={() => (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;