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
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
// 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;
|
|
/** Pre-filled wizard name for creating and joining games. */
|
|
wizardName: string;
|
|
/** Preferred wizard color (0-5), claimed in lobbies when free. */
|
|
color: number | null;
|
|
/** "Are you sure?" prompts before easy-to-regret plays. */
|
|
cautions: boolean;
|
|
/** Default skill for automatons seated from the lobby. */
|
|
botTier: "apprentice" | "adept" | "archmage";
|
|
/** Instant replay: notable chronicle lines offer a first-person reel. */
|
|
instantReplay: boolean;
|
|
/** The live first-person pane above the board during play. */
|
|
liveFp: boolean;
|
|
/** The hand of cards stands in a column at the board's left instead of
|
|
* lying along the bottom (wide screens only). */
|
|
handLeft: boolean;
|
|
/** The hand's side was picked by hand; until then a wide, short screen
|
|
* stands the cards beside the board on its own. */
|
|
handLeftChosen: boolean;
|
|
/** The first-turn coaching slip has been read and dismissed. */
|
|
coached: boolean;
|
|
/** The automaton tier was picked by hand (so a finished game leaves it alone). */
|
|
tierChosen: boolean;
|
|
/** Games this browser has seen through to a winner. */
|
|
finishedGames: number;
|
|
/** Keep the step light on every turn: your square and the squares a
|
|
* stride reaches glow, the rest of the maze in shadow. A browser's
|
|
* first game lights its first turn on its own. */
|
|
stepLightAlways: boolean;
|
|
}
|
|
|
|
const KEY = "wizwar-prefs";
|
|
|
|
function load(): Prefs {
|
|
const fallback: Prefs = {
|
|
art: "photo", autoGrab: false, flourishes: true, wizardName: "", color: null,
|
|
cautions: true, botTier: "apprentice", instantReplay: true, liveFp: false, handLeft: false, handLeftChosen: false,
|
|
coached: false, tierChosen: false, finishedGames: 0, stepLightAlways: false,
|
|
};
|
|
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,
|
|
wizardName: typeof p.wizardName === "string" ? p.wizardName.slice(0, 20) : "",
|
|
color: typeof p.color === "number" && p.color >= 0 && p.color <= 5 ? p.color : null,
|
|
cautions: p.cautions !== false,
|
|
botTier: p.botTier === "adept" || p.botTier === "archmage" ? p.botTier : "apprentice",
|
|
instantReplay: p.instantReplay !== false,
|
|
liveFp: p.liveFp === true,
|
|
handLeft: p.handLeft === true,
|
|
handLeftChosen: p.handLeftChosen === true || p.handLeft === true,
|
|
coached: p.coached === true,
|
|
tierChosen: p.tierChosen === true,
|
|
finishedGames: typeof p.finishedGames === "number" ? p.finishedGames : 0,
|
|
stepLightAlways: p.stepLightAlways === true,
|
|
};
|
|
} 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.
|
|
}
|
|
}
|