Phase 6: character creation & level-up

- Ability generation: standard array, point buy (27-pt, validated), 4d6kh3 roll;
  AbilityGenModal with assign-from-pool + steppers (pure abilityGen lib + tests)
- Level-up flow: increment level, HP gain (average or roll hit die + CON)
- Print / save-to-PDF: print button + @media print hides app chrome and roll tray
- Plan: added Phase 11 (data-driven Assistant) to the roadmap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 01:56:23 +02:00
parent d5977e4c63
commit 522ff8abce
9 changed files with 331 additions and 4 deletions
+31
View File
@@ -0,0 +1,31 @@
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.evaluate(async () => {
indexedDB.deleteDatabase('ttrpg-manager');
localStorage.clear();
});
await page.reload();
});
test('generate ability scores and level up', async ({ page }) => {
await page.getByRole('button', { name: '+ New campaign' }).first().click();
await page.locator('input[data-autofocus]').fill('Build');
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('link', { name: 'Characters' }).click();
await page.getByRole('button', { name: '+ New character' }).first().click();
await page.locator('input[data-autofocus]').fill('Builder');
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('link', { name: /Builder/ }).click();
// Generate scores via standard array (default assignment 15,14,13,12,10,8)
await page.getByRole('button', { name: 'Generate' }).click();
await page.getByRole('button', { name: 'Apply' }).click();
await expect(page.getByLabel('STR score')).toHaveValue('15');
// Level up from 1 -> 2
await page.getByRole('button', { name: 'Level up' }).click();
await page.getByRole('button', { name: 'Level up' }).last().click();
await expect(page.getByRole('spinbutton', { name: 'Level', exact: true })).toHaveValue('2');
});
+1 -1
View File
@@ -9,7 +9,7 @@ export function RollTray() {
if (!last) return null; if (!last) return null;
return ( return (
<div className="pointer-events-none fixed bottom-4 right-4 z-50 w-72"> <div data-roll-tray className="pointer-events-none fixed bottom-4 right-4 z-50 w-72 print:hidden">
<div <div
key={last.seq} key={last.seq}
className="animate-dice-settle pointer-events-auto rounded-lg border border-line bg-panel p-4 shadow-2xl" className="animate-dice-settle pointer-events-auto rounded-lg border border-line bg-panel p-4 shadow-2xl"
+19 -1
View File
@@ -17,6 +17,8 @@ import { AttacksSection } from './sheet/AttacksSection';
import { ResourcesSection } from './sheet/ResourcesSection'; import { ResourcesSection } from './sheet/ResourcesSection';
import { SpellcastingSection } from './sheet/SpellcastingSection'; import { SpellcastingSection } from './sheet/SpellcastingSection';
import { DefensesSection } from './sheet/DefensesSection'; import { DefensesSection } from './sheet/DefensesSection';
import { AbilityGenModal } from './sheet/AbilityGenModal';
import { LevelUpModal } from './sheet/LevelUpModal';
const ABILITIES: AbilityKey[] = ['str', 'dex', 'con', 'int', 'wis', 'cha']; const ABILITIES: AbilityKey[] = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
@@ -33,6 +35,8 @@ const RANK_LABEL: Record<ProficiencyRank, string> = {
export function CharacterSheet({ character }: { character: Character }) { export function CharacterSheet({ character }: { character: Character }) {
// Local editable copy; write-through to the DB on change (debounced + flush on unmount). // Local editable copy; write-through to the DB on change (debounced + flush on unmount).
const [c, setC] = useState<Character>(character); const [c, setC] = useState<Character>(character);
const [levelUp, setLevelUp] = useState(false);
const [genScores, setGenScores] = useState(false);
const save = useDebouncedCallback((next: Character) => { const save = useDebouncedCallback((next: Character) => {
// Persist everything except identity/timestamps (update() stamps updatedAt). // Persist everything except identity/timestamps (update() stamps updatedAt).
const { id, campaignId: _campaignId, createdAt: _createdAt, updatedAt: _updatedAt, ...rest } = next; const { id, campaignId: _campaignId, createdAt: _createdAt, updatedAt: _updatedAt, ...rest } = next;
@@ -84,7 +88,11 @@ export function CharacterSheet({ character }: { character: Character }) {
<span className="rounded-full bg-elevated px-2 py-0.5 text-xs uppercase tracking-wide text-muted"> <span className="rounded-full bg-elevated px-2 py-0.5 text-xs uppercase tracking-wide text-muted">
{c.kind === 'pc' ? 'PC' : 'NPC'} · {sys.label} {c.kind === 'pc' ? 'PC' : 'NPC'} · {sys.label}
</span> </span>
<div className="ml-auto text-sm text-muted">{profLabel}</div> <div className="ml-auto flex items-center gap-2">
<span className="text-sm text-muted">{profLabel}</span>
<Button size="sm" variant="secondary" onClick={() => setLevelUp(true)}>Level up</Button>
<Button size="sm" variant="ghost" onClick={() => window.print()} title="Print / save as PDF">Print</Button>
</div>
</div> </div>
<div className="mb-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <div className="mb-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
@@ -118,7 +126,10 @@ export function CharacterSheet({ character }: { character: Character }) {
{/* Abilities */} {/* Abilities */}
<section className="mb-6"> <section className="mb-6">
<div className="mb-2 flex items-center justify-between">
<SectionTitle>Ability Scores</SectionTitle> <SectionTitle>Ability Scores</SectionTitle>
<Button size="sm" variant="ghost" onClick={() => setGenScores(true)}>Generate</Button>
</div>
<div className="grid grid-cols-3 gap-3 sm:grid-cols-6"> <div className="grid grid-cols-3 gap-3 sm:grid-cols-6">
{ABILITIES.map((a) => { {ABILITIES.map((a) => {
const mod = sys.abilityModifier(c.abilities[a]); const mod = sys.abilityModifier(c.abilities[a]);
@@ -212,6 +223,13 @@ export function CharacterSheet({ character }: { character: Character }) {
className="min-h-32 w-full resize-y rounded-md border border-line bg-surface px-3 py-2 text-sm text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60" className="min-h-32 w-full resize-y rounded-md border border-line bg-surface px-3 py-2 text-sm text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60"
/> />
</section> </section>
{genScores && (
<AbilityGenModal onClose={() => setGenScores(false)} onApply={(abilities) => update({ abilities })} />
)}
{levelUp && (
<LevelUpModal character={c} onClose={() => setLevelUp(false)} onApply={(patch) => update(patch)} />
)}
</Page> </Page>
); );
} }
@@ -0,0 +1,123 @@
import { useState } from 'react';
import type { AbilityKey, AbilityScores } from '@/lib/rules';
import { ABILITY_ABBR, abilityModifier } from '@/lib/rules';
import { createRng } from '@/lib/rng';
import {
STANDARD_ARRAY,
rollAbilityScores,
pointBuyRemaining,
POINT_BUY_MIN,
POINT_BUY_MAX,
} from '@/lib/rules/abilityGen';
import { formatModifier } from '@/lib/format';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { Select } from '@/components/ui/Input';
import { NumberField } from '@/components/ui/NumberField';
import { cn } from '@/lib/cn';
const ABILITIES: AbilityKey[] = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
type Method = 'standard' | 'roll' | 'pointbuy';
export function AbilityGenModal({ onApply, onClose }: { onApply: (scores: AbilityScores) => void; onClose: () => void }) {
const [method, setMethod] = useState<Method>('standard');
const [pool, setPool] = useState<number[]>([...STANDARD_ARRAY]);
// assignment[i] = pool index assigned to ABILITIES[i], or -1
const [assignment, setAssignment] = useState<number[]>(ABILITIES.map((_, i) => i));
const [pb, setPb] = useState<number[]>([8, 8, 8, 8, 8, 8]);
const chooseMethod = (m: Method) => {
setMethod(m);
if (m === 'standard') { setPool([...STANDARD_ARRAY]); setAssignment(ABILITIES.map((_, i) => i)); }
if (m === 'roll') { const p = rollAbilityScores(createRng()); setPool(p); setAssignment(ABILITIES.map((_, i) => i)); }
};
const reroll = () => { setPool(rollAbilityScores(createRng())); setAssignment(ABILITIES.map((_, i) => i)); };
const usesPool = method === 'standard' || method === 'roll';
const remaining = pointBuyRemaining(pb);
const toScores = (): AbilityScores => {
const out = {} as AbilityScores;
ABILITIES.forEach((a, i) => {
out[a] = usesPool ? (pool[assignment[i]!] ?? 10) : pb[i]!;
});
return out;
};
const poolValid = !usesPool || new Set(assignment).size === ABILITIES.length;
const pbValid = method !== 'pointbuy' || remaining >= 0;
const valid = poolValid && pbValid;
return (
<Modal
open
onClose={onClose}
title="Generate ability scores"
className="max-w-xl"
footer={
<>
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button variant="primary" disabled={!valid} onClick={() => { onApply(toScores()); onClose(); }}>Apply</Button>
</>
}
>
<div className="mb-4 flex gap-1">
{([['standard', 'Standard array'], ['pointbuy', 'Point buy'], ['roll', '4d6 drop lowest']] as const).map(([m, label]) => (
<button
key={m}
onClick={() => chooseMethod(m)}
className={cn('rounded-md px-3 py-1.5 text-sm', method === m ? 'bg-accent text-accent-ink' : 'bg-elevated text-muted hover:text-ink')}
>
{label}
</button>
))}
</div>
{method === 'roll' && (
<Button size="sm" variant="secondary" className="mb-3" onClick={reroll}> Reroll</Button>
)}
{method === 'pointbuy' && (
<p className={cn('mb-3 text-sm', remaining < 0 ? 'text-danger' : 'text-muted')}>
Points remaining: <span className="font-semibold text-ink">{remaining}</span> / 27
</p>
)}
{usesPool && !poolValid && <p className="mb-3 text-sm text-warning">Assign each value to a different ability.</p>}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{ABILITIES.map((a, i) => {
const value = usesPool ? (pool[assignment[i]!] ?? 0) : pb[i]!;
return (
<div key={a} className="rounded-lg border border-line bg-surface p-3 text-center">
<div className="text-xs font-semibold uppercase tracking-wide text-muted">{ABILITY_ABBR[a]}</div>
{usesPool ? (
<Select
className="mt-1"
aria-label={`${ABILITY_ABBR[a]} value`}
value={assignment[i]}
onChange={(e) => setAssignment((prev) => prev.map((v, j) => (j === i ? Number(e.target.value) : v)))}
>
{pool.map((p, idx) => (
<option key={idx} value={idx} disabled={assignment.includes(idx) && assignment[i] !== idx}>
{p}
</option>
))}
</Select>
) : (
<NumberField
className="mt-1"
value={pb[i]!}
min={POINT_BUY_MIN}
max={POINT_BUY_MAX}
aria-label={`${ABILITY_ABBR[a]} score`}
onChange={(v) => setPb((prev) => prev.map((x, j) => (j === i ? Math.max(POINT_BUY_MIN, Math.min(POINT_BUY_MAX, v)) : x)))}
/>
)}
<div className="mt-1 text-xs text-accent">{formatModifier(abilityModifier(value))}</div>
</div>
);
})}
</div>
</Modal>
);
}
@@ -0,0 +1,67 @@
import { useState } from 'react';
import type { Character } from '@/lib/schemas';
import { abilityModifier } from '@/lib/rules';
import { rollDice } from '@/lib/dice/notation';
import { createRng } from '@/lib/rng';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { Select } from '@/components/ui/Input';
const HIT_DICE = [6, 8, 10, 12] as const;
export function LevelUpModal({ character, onApply, onClose }: {
character: Character;
onApply: (patch: Partial<Character>) => void;
onClose: () => void;
}) {
const [die, setDie] = useState<number>(8);
const [method, setMethod] = useState<'average' | 'roll'>('average');
const conMod = abilityModifier(character.abilities.con);
const average = Math.ceil(die / 2) + 1; // 5e fixed value per level
const apply = () => {
const base = method === 'average' ? average : rollDice(`1d${die}`, createRng()).total;
const gain = Math.max(1, base + conMod);
onApply({
level: Math.min(20, character.level + 1),
hp: { ...character.hp, max: character.hp.max + gain, current: character.hp.current + gain },
});
onClose();
};
const preview = (method === 'average' ? average : `1d${die}`) + (conMod !== 0 ? ` ${conMod >= 0 ? '+' : ''}${conMod}` : '');
return (
<Modal
open
onClose={onClose}
title={`Level up to ${Math.min(20, character.level + 1)}`}
footer={
<>
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button variant="primary" disabled={character.level >= 20} onClick={apply}>Level up</Button>
</>
}
>
<div className="space-y-4">
<label className="block text-xs text-muted">
Hit die
<Select value={die} onChange={(e) => setDie(Number(e.target.value))}>
{HIT_DICE.map((d) => <option key={d} value={d}>d{d}</option>)}
</Select>
</label>
<label className="block text-xs text-muted">
HP method
<Select value={method} onChange={(e) => setMethod(e.target.value as 'average' | 'roll')}>
<option value="average">Average (+{average})</option>
<option value="roll">Roll the hit die</option>
</Select>
</label>
<p className="text-sm text-muted">
HP gained: <span className="font-medium text-ink">{preview}</span> (CON {conMod >= 0 ? '+' : ''}{conMod})
</p>
{character.level >= 20 && <p className="text-sm text-warning">Already at level 20.</p>}
</div>
</Modal>
);
}
+34
View File
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { rollAbilityScores, pointBuySpent, pointBuyRemaining, pointCost, STANDARD_ARRAY } from './abilityGen';
import { createRng } from '@/lib/rng';
describe('ability generation', () => {
it('standard array is the canonical spread', () => {
expect([...STANDARD_ARRAY]).toEqual([15, 14, 13, 12, 10, 8]);
});
it('rolls six scores each in 3..18', () => {
const scores = rollAbilityScores(createRng('chargen'));
expect(scores).toHaveLength(6);
for (const s of scores) {
expect(s).toBeGreaterThanOrEqual(3);
expect(s).toBeLessThanOrEqual(18);
}
});
it('point-buy costs match the standard table', () => {
expect(pointCost(8)).toBe(0);
expect(pointCost(13)).toBe(5);
expect(pointCost(15)).toBe(9);
expect(pointCost(16)).toBeNull();
expect(pointCost(7)).toBeNull();
});
it('27-point standard build is exactly spent', () => {
// all 13s costs 30 (>27), but 15,15,15,8,8,8 = 9+9+9 = 27
expect(pointBuySpent([15, 15, 15, 8, 8, 8])).toBe(27);
expect(pointBuyRemaining([15, 15, 15, 8, 8, 8])).toBe(0);
expect(pointBuyRemaining([8, 8, 8, 8, 8, 8])).toBe(27);
expect(pointBuySpent([16, 8, 8, 8, 8, 8])).toBe(Infinity);
});
});
+39
View File
@@ -0,0 +1,39 @@
import type { Rng } from '@/lib/rng';
import { createRng } from '@/lib/rng';
import { rollDice } from '@/lib/dice/notation';
/** The classic 5e/PF2e-ish standard array. */
export const STANDARD_ARRAY = [15, 14, 13, 12, 10, 8] as const;
/** Roll six ability scores as 4d6, drop the lowest. */
export function rollAbilityScores(rng: Rng = createRng()): number[] {
return Array.from({ length: 6 }, () => rollDice('4d6kh3', rng).total);
}
/**
* Point-buy (27-point standard). Scores 815; costs 0,1,2,3,4,5,7,9 for 8..15.
* Returns the cost of a single score, or null if out of the buyable range.
*/
const POINT_COST: Record<number, number> = { 8: 0, 9: 1, 10: 2, 11: 3, 12: 4, 13: 5, 14: 7, 15: 9 };
export const POINT_BUY_BUDGET = 27;
export const POINT_BUY_MIN = 8;
export const POINT_BUY_MAX = 15;
export function pointCost(score: number): number | null {
return score in POINT_COST ? POINT_COST[score]! : null;
}
/** Total points spent across six scores (Infinity if any score is out of range). */
export function pointBuySpent(scores: number[]): number {
let total = 0;
for (const s of scores) {
const c = pointCost(s);
if (c === null) return Infinity;
total += c;
}
return total;
}
export function pointBuyRemaining(scores: number[]): number {
return POINT_BUY_BUDGET - pointBuySpent(scores);
}
+15
View File
@@ -85,6 +85,21 @@ body {
scrollbar-color: var(--app-line) transparent; scrollbar-color: var(--app-line) transparent;
} }
/* Print: hide app chrome so a character sheet prints (or saves to PDF) cleanly. */
@media print {
header,
[data-roll-tray] {
display: none !important;
}
main {
overflow: visible !important;
}
body {
background: #fff;
color: #000;
}
}
/* Dice roll animations */ /* Dice roll animations */
@keyframes dice-tumble { @keyframes dice-tumble {
0% { 0% {
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.tsx","./src/router.tsx","./src/vite-env.d.ts","./src/app/RootLayout.tsx","./src/components/ui/Button.tsx","./src/components/ui/ErrorBoundary.tsx","./src/components/ui/Input.tsx","./src/components/ui/Modal.tsx","./src/components/ui/NumberField.tsx","./src/components/ui/Page.tsx","./src/components/ui/RollTray.tsx","./src/features/campaigns/CampaignsPage.tsx","./src/features/campaigns/hooks.ts","./src/features/characters/CharacterSheet.tsx","./src/features/characters/CharacterSheetPage.tsx","./src/features/characters/CharactersPage.tsx","./src/features/characters/hooks.ts","./src/features/characters/sheet/AttacksSection.tsx","./src/features/characters/sheet/DefensesSection.tsx","./src/features/characters/sheet/InventorySection.tsx","./src/features/characters/sheet/ResourcesSection.tsx","./src/features/characters/sheet/SpellcastingSection.tsx","./src/features/characters/sheet/common.tsx","./src/features/combat/CombatPage.tsx","./src/features/combat/EncounterTracker.tsx","./src/features/combat/hooks.ts","./src/features/combat/useConditionGlossary.ts","./src/features/compendium/CompendiumPage.tsx","./src/features/compendium/MonsterDetail.tsx","./src/features/compendium/details.tsx","./src/features/compendium/registry.test.ts","./src/features/compendium/registry.tsx","./src/features/dice/DicePage.tsx","./src/features/world/CalendarPage.tsx","./src/features/world/DashboardPage.tsx","./src/features/world/NotesPage.tsx","./src/features/world/NpcsPage.tsx","./src/features/world/QuestsPage.tsx","./src/features/world/hooks.ts","./src/lib/cn.ts","./src/lib/format.ts","./src/lib/ids.ts","./src/lib/rng.ts","./src/lib/useDebouncedCallback.ts","./src/lib/useRoll.ts","./src/lib/wikilinks.test.ts","./src/lib/wikilinks.ts","./src/lib/combat/budget.test.ts","./src/lib/combat/budget.ts","./src/lib/combat/engine.test.ts","./src/lib/combat/engine.ts","./src/lib/compendium/index.ts","./src/lib/compendium/types.ts","./src/lib/db/db.ts","./src/lib/db/repositories.test.ts","./src/lib/db/repositories.ts","./src/lib/dice/check.test.ts","./src/lib/dice/check.ts","./src/lib/dice/notation.test.ts","./src/lib/dice/notation.ts","./src/lib/io/character.test.ts","./src/lib/io/character.ts","./src/lib/io/file.ts","./src/lib/rules/abilities.ts","./src/lib/rules/conditions.ts","./src/lib/rules/depth.test.ts","./src/lib/rules/index.ts","./src/lib/rules/rest.ts","./src/lib/rules/rules.test.ts","./src/lib/rules/types.ts","./src/lib/rules/dnd5e/index.ts","./src/lib/rules/dnd5e/skills.ts","./src/lib/rules/pf2e/index.ts","./src/lib/rules/pf2e/skills.ts","./src/lib/schemas/campaign.ts","./src/lib/schemas/character.ts","./src/lib/schemas/common.ts","./src/lib/schemas/dice.ts","./src/lib/schemas/encounter.ts","./src/lib/schemas/index.ts","./src/lib/schemas/world.ts","./src/stores/macroStore.ts","./src/stores/rollStore.ts","./src/stores/uiStore.ts","./vitest.setup.ts"],"version":"5.9.3"} {"root":["./src/main.tsx","./src/router.tsx","./src/vite-env.d.ts","./src/app/RootLayout.tsx","./src/components/ui/Button.tsx","./src/components/ui/ErrorBoundary.tsx","./src/components/ui/Input.tsx","./src/components/ui/Modal.tsx","./src/components/ui/NumberField.tsx","./src/components/ui/Page.tsx","./src/components/ui/RollTray.tsx","./src/features/campaigns/CampaignsPage.tsx","./src/features/campaigns/hooks.ts","./src/features/characters/CharacterSheet.tsx","./src/features/characters/CharacterSheetPage.tsx","./src/features/characters/CharactersPage.tsx","./src/features/characters/hooks.ts","./src/features/characters/sheet/AbilityGenModal.tsx","./src/features/characters/sheet/AttacksSection.tsx","./src/features/characters/sheet/DefensesSection.tsx","./src/features/characters/sheet/InventorySection.tsx","./src/features/characters/sheet/LevelUpModal.tsx","./src/features/characters/sheet/ResourcesSection.tsx","./src/features/characters/sheet/SpellcastingSection.tsx","./src/features/characters/sheet/common.tsx","./src/features/combat/CombatPage.tsx","./src/features/combat/EncounterTracker.tsx","./src/features/combat/hooks.ts","./src/features/combat/useConditionGlossary.ts","./src/features/compendium/CompendiumPage.tsx","./src/features/compendium/MonsterDetail.tsx","./src/features/compendium/details.tsx","./src/features/compendium/registry.test.ts","./src/features/compendium/registry.tsx","./src/features/dice/DicePage.tsx","./src/features/world/CalendarPage.tsx","./src/features/world/DashboardPage.tsx","./src/features/world/NotesPage.tsx","./src/features/world/NpcsPage.tsx","./src/features/world/QuestsPage.tsx","./src/features/world/hooks.ts","./src/lib/cn.ts","./src/lib/format.ts","./src/lib/ids.ts","./src/lib/rng.ts","./src/lib/useDebouncedCallback.ts","./src/lib/useRoll.ts","./src/lib/wikilinks.test.ts","./src/lib/wikilinks.ts","./src/lib/combat/budget.test.ts","./src/lib/combat/budget.ts","./src/lib/combat/engine.test.ts","./src/lib/combat/engine.ts","./src/lib/compendium/index.ts","./src/lib/compendium/types.ts","./src/lib/db/db.ts","./src/lib/db/repositories.test.ts","./src/lib/db/repositories.ts","./src/lib/dice/check.test.ts","./src/lib/dice/check.ts","./src/lib/dice/notation.test.ts","./src/lib/dice/notation.ts","./src/lib/io/character.test.ts","./src/lib/io/character.ts","./src/lib/io/file.ts","./src/lib/rules/abilities.ts","./src/lib/rules/abilityGen.test.ts","./src/lib/rules/abilityGen.ts","./src/lib/rules/conditions.ts","./src/lib/rules/depth.test.ts","./src/lib/rules/index.ts","./src/lib/rules/rest.ts","./src/lib/rules/rules.test.ts","./src/lib/rules/types.ts","./src/lib/rules/dnd5e/index.ts","./src/lib/rules/dnd5e/skills.ts","./src/lib/rules/pf2e/index.ts","./src/lib/rules/pf2e/skills.ts","./src/lib/schemas/campaign.ts","./src/lib/schemas/character.ts","./src/lib/schemas/common.ts","./src/lib/schemas/dice.ts","./src/lib/schemas/encounter.ts","./src/lib/schemas/index.ts","./src/lib/schemas/world.ts","./src/stores/macroStore.ts","./src/stores/rollStore.ts","./src/stores/uiStore.ts","./vitest.setup.ts"],"version":"5.9.3"}