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:
co-authored by
Claude Fable 5
parent
5310fbdca9
commit
6aae40ad5f
@@ -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;
|
||||
|
||||
@@ -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<BoardFx[]>([]);
|
||||
$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]),
|
||||
|
||||
+10
-10
@@ -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<ArtCategory, boolean> = {
|
||||
players: q.get("svgPlayers") === "true",
|
||||
creatures: q.get("svgCreatures") === "true",
|
||||
terrain: q.get("svgTerrain") === "true",
|
||||
objects: q.get("svgObjects") === "true",
|
||||
const PARAM: Record<ArtCategory, string> = {
|
||||
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
|
||||
|
||||
@@ -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<Prefs>;
|
||||
return {
|
||||
art: p.art === "drawn" ? "drawn" : "photo",
|
||||
autoGrab: p.autoGrab === true,
|
||||
flourishes: p.flourishes !== false,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export const prefs = $state<Prefs>(load());
|
||||
|
||||
export function savePrefs(): void {
|
||||
try {
|
||||
localStorage.setItem(KEY, JSON.stringify(prefs));
|
||||
} catch {
|
||||
// A full or blocked localStorage loses persistence, not the session.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user