Implements PF2e as an alternative game system alongside D&D 5e/5.5e. Settings modal "Game System" selector switches conditions, bestiary, stat block layout, and initiative calculation between systems. - Valued conditions with increment/decrement UX (Clumsy 2, Frightened 3) - 2,502 PF2e creatures from bundled search index (77 sources) - PF2e stat block: level, traits, Perception, Fort/Ref/Will, ability mods - Perception-based initiative rolling - System-scoped source cache (D&D and PF2e sources don't collide) - Backwards-compatible condition rehydration (ConditionId[] → ConditionEntry[]) - Difficulty indicator hidden in PF2e mode (excluded from MVP) Closes dostulata/initiative#19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
61 lines
1.4 KiB
TypeScript
61 lines
1.4 KiB
TypeScript
import type { RulesEdition } from "@initiative/domain";
|
|
import { useCallback, useSyncExternalStore } from "react";
|
|
|
|
const STORAGE_KEY = "initiative:game-system";
|
|
const OLD_STORAGE_KEY = "initiative:rules-edition";
|
|
|
|
const listeners = new Set<() => void>();
|
|
let currentEdition: RulesEdition = loadEdition();
|
|
|
|
function loadEdition(): RulesEdition {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw === "5e" || raw === "5.5e" || raw === "pf2e") return raw;
|
|
// Migrate from old key
|
|
const old = localStorage.getItem(OLD_STORAGE_KEY);
|
|
if (old === "5e" || old === "5.5e") {
|
|
localStorage.setItem(STORAGE_KEY, old);
|
|
localStorage.removeItem(OLD_STORAGE_KEY);
|
|
return old;
|
|
}
|
|
} catch {
|
|
// storage unavailable
|
|
}
|
|
return "5.5e";
|
|
}
|
|
|
|
function saveEdition(edition: RulesEdition): void {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, edition);
|
|
} catch {
|
|
// quota exceeded or storage unavailable
|
|
}
|
|
}
|
|
|
|
function notifyAll(): void {
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
function subscribe(callback: () => void): () => void {
|
|
listeners.add(callback);
|
|
return () => listeners.delete(callback);
|
|
}
|
|
|
|
function getSnapshot(): RulesEdition {
|
|
return currentEdition;
|
|
}
|
|
|
|
export function useRulesEdition() {
|
|
const edition = useSyncExternalStore(subscribe, getSnapshot);
|
|
|
|
const setEdition = useCallback((next: RulesEdition) => {
|
|
currentEdition = next;
|
|
saveEdition(next);
|
|
notifyAll();
|
|
}, []);
|
|
|
|
return { edition, setEdition } as const;
|
|
}
|