diff --git a/e2e/assistant-levelup.spec.ts b/e2e/assistant-levelup.spec.ts
new file mode 100644
index 0000000..05eef81
--- /dev/null
+++ b/e2e/assistant-levelup.spec.ts
@@ -0,0 +1,40 @@
+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('level-up advisor offers routes and expands a chosen one (deterministic)', async ({ page }) => {
+ await page.getByLabel('Settings').click();
+ await page.getByRole('button', { name: 'Load sample campaign' }).click();
+ await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible();
+
+ // Open Lia (level 3 Fighter) and start a level-up.
+ await page.getByLabel('Primary').getByRole('link', { name: 'Characters' }).click();
+ await page.getByRole('link', { name: /Lia the Brave/ }).click();
+ await page.getByRole('button', { name: 'Level up' }).click();
+
+ const advisor = page.getByTestId('levelup-advisor');
+ await page.getByRole('button', { name: 'Suggest build routes' }).click();
+ await expect(advisor).toBeVisible();
+
+ // Four system-aware routes; choose one and see concrete steps for the next level.
+ await expect(advisor.getByText('Offense')).toBeVisible();
+ await expect(advisor.getByRole('button', { name: 'Choose' })).toHaveCount(4);
+ await advisor.locator('li').filter({ hasText: 'Offense' }).getByRole('button', { name: 'Choose' }).click();
+ await expect(advisor.getByText('Level 4 focus')).toBeVisible();
+});
+
+test('assistant page renders the campaign insights section', async ({ page }) => {
+ await page.getByLabel('Settings').click();
+ await page.getByRole('button', { name: 'Load sample campaign' }).click();
+ await page.getByRole('link', { name: 'Assistant' }).click();
+ await expect(page.getByRole('heading', { name: 'Campaign insights' })).toBeVisible();
+ // Sample has a single encounter → not enough for a trend yet.
+ await expect(page.getByText(/No trends detected yet/)).toBeVisible();
+});
diff --git a/src/features/assistant/AssistantPage.tsx b/src/features/assistant/AssistantPage.tsx
index 9ea243b..d6714b1 100644
--- a/src/features/assistant/AssistantPage.tsx
+++ b/src/features/assistant/AssistantPage.tsx
@@ -17,6 +17,7 @@ import { useEncounters } from '@/features/combat/hooks';
import { Page, PageHeader, RequireCampaign } from '@/components/ui/Page';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/cn';
+import { CampaignInsights } from './CampaignInsights';
export function AssistantPage() {
return {(c) => };
@@ -126,6 +127,9 @@ function Assistant({ campaign }: { campaign: Campaign }) {
Session prep
+
+ Campaign insights
+
diff --git a/src/features/assistant/CampaignInsights.tsx b/src/features/assistant/CampaignInsights.tsx
new file mode 100644
index 0000000..ace4a87
--- /dev/null
+++ b/src/features/assistant/CampaignInsights.tsx
@@ -0,0 +1,72 @@
+import { useMemo, useState } from 'react';
+import type { Campaign, Character, Encounter, Note, Quest } from '@/lib/schemas';
+import { buildCampaignContext } from '@/lib/assistant/context';
+import { detectThemes, type CampaignTheme } from '@/lib/assistant/patterns';
+import { complete } from '@/lib/llm/client';
+import { getLlmConfig, useAssistantStore } from '@/stores/assistantStore';
+import { Button } from '@/components/ui/Button';
+import { cn } from '@/lib/cn';
+
+const SEV_CLS = { info: 'border-line', warn: 'border-warning/50', danger: 'border-danger/50' } as const;
+
+/** Deterministic campaign themes as cards, with an optional AI "expand" affordance. */
+export function CampaignInsights({ campaign, characters, encounters, quests, notes }: {
+ campaign: Campaign;
+ characters: Character[];
+ encounters: Encounter[];
+ quests: Quest[];
+ notes: Note[];
+}) {
+ const llmEnabled = useAssistantStore((s) => s.enabled);
+ const hasKey = useAssistantStore((s) => !!s.apiKey);
+ const canUseLlm = llmEnabled && hasKey;
+
+ const ctx = useMemo(
+ () => buildCampaignContext({ campaign, characters, encounters, quests, notes }),
+ [campaign, characters, encounters, quests, notes],
+ );
+ const themes = useMemo(() => detectThemes(ctx), [ctx]);
+
+ if (themes.length === 0) {
+ return No trends detected yet — play a few more sessions.
;
+ }
+ return (
+
+ {themes.map((t) => (
+
+ ))}
+
+ );
+}
+
+function InsightCard({ theme, ctx, canUseLlm }: { theme: CampaignTheme; ctx: ReturnType; canUseLlm: boolean }) {
+ const [expanded, setExpanded] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const expand = async () => {
+ setLoading(true);
+ const res = await complete(getLlmConfig(), {
+ system: `${ctx.systemConstraint}\n\nYou are a concise ${ctx.systemLabel} GM coach. Give 2–3 sentences of practical advice.`,
+ user: `Trend: ${theme.tendency} ${theme.evidence} How should the GM respond?`,
+ maxTokens: 400,
+ });
+ setExpanded(res.ok && 'text' in res ? res.text : 'Could not reach the assistant.');
+ setLoading(false);
+ };
+
+ return (
+
+ {theme.tendency}
+ {theme.evidence}
+ {theme.recommendation && {theme.recommendation}
}
+ {canUseLlm && (
+
+
+
+ )}
+ {expanded && {expanded}
}
+
+ );
+}
diff --git a/src/features/assistant/useLevelUpAdvisor.ts b/src/features/assistant/useLevelUpAdvisor.ts
new file mode 100644
index 0000000..aeba572
--- /dev/null
+++ b/src/features/assistant/useLevelUpAdvisor.ts
@@ -0,0 +1,70 @@
+import { useMemo, useState } from 'react';
+import type { Campaign, Character } from '@/lib/schemas';
+import { complete } from '@/lib/llm/client';
+import { getLlmConfig, useAssistantStore } from '@/stores/assistantStore';
+import { buildCampaignContext } from '@/lib/assistant/context';
+import {
+ buildLevelUpRoutesPrompt, buildLevelUpStepsPrompt, buildRoutesSchema, buildStepsSchema,
+ type BuildRoute, type BuildStep,
+} from '@/lib/assistant/prompts';
+import { deterministicRoutes, deterministicSteps } from '@/lib/assistant/levelup';
+
+export type AdvisorState = 'idle' | 'loading' | 'ready' | 'error';
+
+export function useLevelUpAdvisor(campaign: Campaign, character: Character) {
+ const llmEnabled = useAssistantStore((s) => s.enabled);
+ const hasKey = useAssistantStore((s) => !!s.apiKey);
+ const canUseLlm = llmEnabled && hasKey;
+ const nextLevel = Math.min(20, character.level + 1);
+
+ const ctx = useMemo(
+ () => buildCampaignContext({ campaign, characters: [character], encounters: [], quests: [], notes: [] }),
+ [campaign, character],
+ );
+
+ const [state, setState] = useState('idle');
+ const [routes, setRoutes] = useState();
+ const [steps, setSteps] = useState();
+ const [chosen, setChosen] = useState();
+ const [source, setSource] = useState<'llm' | 'deterministic'>('deterministic');
+ const [message, setMessage] = useState(null);
+
+ const fetchRoutes = async () => {
+ setState('loading');
+ setMessage(null);
+ setSteps(undefined);
+ setChosen(undefined);
+ if (canUseLlm) {
+ const prompt = buildLevelUpRoutesPrompt(ctx, character, nextLevel);
+ const res = await complete(getLlmConfig(), { system: prompt.system, user: prompt.user, schema: buildRoutesSchema, maxTokens: 700 });
+ if (res.ok && 'data' in res && res.data.routes.length) {
+ setRoutes(res.data.routes);
+ setSource('llm');
+ setState('ready');
+ return;
+ }
+ if (!res.ok) setMessage(`AI unavailable (${res.error}); showing general routes.`);
+ }
+ setRoutes(deterministicRoutes(campaign.system, character.className));
+ setSource('deterministic');
+ setState('ready');
+ };
+
+ const chooseRoute = async (title: string) => {
+ setChosen(title);
+ setSteps(undefined);
+ if (canUseLlm) {
+ const prompt = buildLevelUpStepsPrompt(ctx, character, nextLevel, title);
+ const res = await complete(getLlmConfig(), { system: prompt.system, user: prompt.user, schema: buildStepsSchema, maxTokens: 700 });
+ if (res.ok && 'data' in res && res.data.steps.length) {
+ setSteps(res.data.steps);
+ setSource('llm');
+ return;
+ }
+ }
+ setSteps(deterministicSteps(campaign.system, character.className, title, nextLevel));
+ setSource('deterministic');
+ };
+
+ return { state, routes, steps, chosen, source, message, canUseLlm, nextLevel, fetchRoutes, chooseRoute };
+}
diff --git a/src/features/characters/sheet/LevelUpAdvisor.tsx b/src/features/characters/sheet/LevelUpAdvisor.tsx
new file mode 100644
index 0000000..3d10574
--- /dev/null
+++ b/src/features/characters/sheet/LevelUpAdvisor.tsx
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import type { Campaign, Character } from '@/lib/schemas';
+import { Button } from '@/components/ui/Button';
+import { Input } from '@/components/ui/Input';
+import { useLevelUpAdvisor } from '@/features/assistant/useLevelUpAdvisor';
+
+/**
+ * Build-route advisor shown alongside the HP-only level-up flow. Presents ~4
+ * system-aware routes (plus a custom one); choosing one expands it into concrete
+ * steps. AI-powered when configured, deterministic otherwise.
+ */
+export function LevelUpAdvisor({ campaign, character }: { campaign: Campaign; character: Character }) {
+ const { state, routes, steps, chosen, source, message, canUseLlm, fetchRoutes, chooseRoute } =
+ useLevelUpAdvisor(campaign, character);
+ const [custom, setCustom] = useState('');
+
+ if (state === 'idle') {
+ return (
+
+
Not sure where to take this character?
+
+
+ );
+ }
+
+ return (
+
+
+
+ {source === 'llm' ? 'AI build routes' : 'Build routes'}
+
+ {state === 'loading' && Thinking…}
+
+ {message &&
{message}
}
+
+ {routes && (
+
+ {routes.map((r) => (
+ -
+
+
+
{r.title}
+
{r.summary}
+
{r.playstyle}
+
+
+
+ {chosen === r.title && steps && (
+
+ {steps.map((s, i) => (
+ -
+ {s.label}: {s.detail}
+
+ ))}
+
+ )}
+
+ ))}
+
+ )}
+
+
+ setCustom(e.target.value)}
+ placeholder="Or describe your own route…"
+ aria-label="Custom build route"
+ className="text-sm"
+ />
+
+
+ {chosen && !routes?.some((r) => r.title === chosen) && steps && (
+
+ {steps.map((s, i) => (
+ -
+ {s.label}: {s.detail}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/features/characters/sheet/LevelUpModal.tsx b/src/features/characters/sheet/LevelUpModal.tsx
index 9062da5..c4f6920 100644
--- a/src/features/characters/sheet/LevelUpModal.tsx
+++ b/src/features/characters/sheet/LevelUpModal.tsx
@@ -1,11 +1,12 @@
import { useState } from 'react';
-import type { Character } from '@/lib/schemas';
+import type { Campaign, 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';
+import { LevelUpAdvisor } from './LevelUpAdvisor';
const HIT_DICE = [6, 8, 10, 12] as const;
@@ -31,6 +32,11 @@ export function LevelUpModal({ character, onApply, onClose }: {
const preview = (method === 'average' ? average : `1d${die}`) + (conMod !== 0 ? ` ${conMod >= 0 ? '+' : ''}${conMod}` : '');
+ // The advisor only needs the system + id; synthesize a campaign from the character.
+ const campaign: Campaign = {
+ id: character.campaignId, name: '', system: character.system, description: '', createdAt: '', updatedAt: '',
+ };
+
return (
{preview} (CON {conMod >= 0 ? '+' : ''}{conMod})
{character.level >= 20 && Already at level 20.
}
+
+ {character.level < 20 && }
);
diff --git a/src/lib/assistant/levelup.test.ts b/src/lib/assistant/levelup.test.ts
new file mode 100644
index 0000000..b681cb6
--- /dev/null
+++ b/src/lib/assistant/levelup.test.ts
@@ -0,0 +1,55 @@
+import { describe, it, expect } from 'vitest';
+import { deterministicRoutes, deterministicSteps } from './levelup';
+import { buildRoutesSchema, buildStepsSchema, buildLevelUpRoutesPrompt, buildLevelUpStepsPrompt } from './prompts';
+import { buildCampaignContext } from './context';
+import { characterDefaults, type Campaign, type Character } from '@/lib/schemas';
+
+function ctxAndChar(system: Campaign['system']) {
+ const campaign: Campaign = { id: 'c', name: 'T', system, description: '', createdAt: '', updatedAt: '' };
+ const c: Character = {
+ id: 'p', campaignId: 'c', system, kind: 'pc', name: 'Hero', ancestry: '', className: 'Cleric', level: 3,
+ abilities: { str: 10, dex: 12, con: 13, int: 8, wis: 16, cha: 11 },
+ hp: { current: 20, max: 20, temp: 0 }, speed: 30, armorBonus: 0,
+ skillRanks: {}, saveRanks: {}, perceptionRank: 'trained',
+ ...characterDefaults(), notes: '', createdAt: '', updatedAt: '',
+ };
+ return { ctx: buildCampaignContext({ campaign, characters: [c], encounters: [], quests: [], notes: [] }), c };
+}
+
+describe('deterministic level-up fallback', () => {
+ it('returns 4 schema-valid routes for both systems', () => {
+ for (const system of ['5e', 'pf2e'] as const) {
+ const routes = deterministicRoutes(system, 'Cleric');
+ expect(routes).toHaveLength(4);
+ expect(buildRoutesSchema.safeParse({ routes }).success).toBe(true);
+ }
+ });
+ it('produces system-appropriate steps (pf2e vs 5e vocabulary)', () => {
+ const pf = deterministicSteps('pf2e', 'Cleric', 'Offense', 4);
+ const dnd = deterministicSteps('5e', 'Cleric', 'Offense', 4);
+ expect(buildStepsSchema.safeParse({ steps: pf }).success).toBe(true);
+ expect(JSON.stringify(pf)).toMatch(/ability boosts|skill feat|skill increase/i);
+ expect(JSON.stringify(dnd)).toMatch(/ability score improvement|feat/i);
+ });
+ it('handles a custom route title', () => {
+ const steps = deterministicSteps('5e', 'Wizard', 'Blaster control mage', 5);
+ expect(steps.length).toBeGreaterThan(0);
+ expect(JSON.stringify(steps)).toContain('Blaster control mage');
+ });
+});
+
+describe('level-up prompts', () => {
+ it('routes prompt names the system, class, and next level', () => {
+ const { ctx, c } = ctxAndChar('pf2e');
+ const { system, user } = buildLevelUpRoutesPrompt(ctx, c, 4);
+ expect(system).toMatch(/pathfinder/i);
+ expect(user).toContain('Cleric');
+ expect(user).toContain('4');
+ });
+ it('steps prompt includes the chosen route and constrains to the system', () => {
+ const { ctx, c } = ctxAndChar('5e');
+ const { system, user } = buildLevelUpStepsPrompt(ctx, c, 4, 'Defense');
+ expect(system).toMatch(/5e|dungeons/i);
+ expect(user).toContain('Defense');
+ });
+});
diff --git a/src/lib/assistant/levelup.ts b/src/lib/assistant/levelup.ts
new file mode 100644
index 0000000..fc14aae
--- /dev/null
+++ b/src/lib/assistant/levelup.ts
@@ -0,0 +1,62 @@
+import type { SystemId } from '@/lib/rules';
+import type { BuildRoute, BuildStep } from './prompts';
+
+/**
+ * Deterministic, system-aware build-route advice used when no LLM is configured.
+ * Generic archetypes keyed off the system's advancement vocabulary — not a
+ * substitute for the LLM's class-specific depth, but always available offline.
+ */
+
+const ARCHETYPES: { title: string; summary: string; playstyle: string }[] = [
+ { title: 'Offense', summary: 'Lean into raw damage and accuracy.', playstyle: 'Strike first, strike hard' },
+ { title: 'Defense', summary: 'Shore up survivability and staying power.', playstyle: 'Outlast the fight' },
+ { title: 'Utility', summary: 'Broaden what you can do outside combat.', playstyle: 'A tool for every problem' },
+ { title: 'Support', summary: "Amplify and protect your allies.", playstyle: 'Make the party better' },
+];
+
+function vocab(system: SystemId) {
+ return system === 'pf2e'
+ ? { boost: 'ability boosts', feat: 'a class or skill feat', train: 'a skill increase' }
+ : { boost: 'an Ability Score Improvement', feat: 'a feat', train: 'proficiency or expertise' };
+}
+
+export function deterministicRoutes(_system: SystemId, className: string): BuildRoute[] {
+ const cls = className || 'character';
+ return ARCHETYPES.map((a) => ({
+ title: a.title,
+ summary: `${a.summary} (${cls})`,
+ playstyle: a.playstyle,
+ }));
+}
+
+export function deterministicSteps(system: SystemId, className: string, routeTitle: string, nextLevel: number): BuildStep[] {
+ const v = vocab(system);
+ const cls = className || 'character';
+ const byRoute: Record = {
+ Offense: [
+ { label: 'Raise your attack stat', detail: `Put ${v.boost} into your primary attack ability.` },
+ { label: 'Pick a damage feat', detail: `Take ${v.feat} that adds damage or extra attacks for a ${cls}.` },
+ { label: 'Sharpen accuracy', detail: `Spend ${v.train} on your main weapon or attack proficiency.` },
+ ],
+ Defense: [
+ { label: 'Boost durability', detail: `Invest ${v.boost} in Constitution (and Dexterity for AC).` },
+ { label: 'Take a defensive feat', detail: `Choose ${v.feat} that improves AC, saves, or damage mitigation.` },
+ { label: 'Train a key save', detail: `Use ${v.train} on your weakest defense.` },
+ ],
+ Utility: [
+ { label: 'Expand your skills', detail: `Apply ${v.train} to skills that fit your concept.` },
+ { label: 'Pick a versatile feat', detail: `Take ${v.feat} that grants new options or actions.` },
+ { label: 'Round out abilities', detail: `Use ${v.boost} to cover a glaring weakness.` },
+ ],
+ Support: [
+ { label: 'Improve your key ability', detail: `Put ${v.boost} into your spellcasting or support ability.` },
+ { label: 'Take a team feat', detail: `Choose ${v.feat} that buffs, heals, or protects allies.` },
+ { label: 'Train coordination skills', detail: `Spend ${v.train} on social or healing-adjacent skills.` },
+ ],
+ };
+ const steps = byRoute[routeTitle] ?? [
+ { label: 'Pick complementary options', detail: `Choose ${v.feat} and ${v.boost} that support "${routeTitle}".` },
+ { label: 'Train toward the goal', detail: `Apply ${v.train} where it advances this plan.` },
+ ];
+ return [{ label: `Level ${nextLevel} focus`, detail: `Build toward "${routeTitle}" as a ${cls}.` }, ...steps];
+}
diff --git a/src/lib/assistant/prompts.ts b/src/lib/assistant/prompts.ts
index d089146..40c1a78 100644
--- a/src/lib/assistant/prompts.ts
+++ b/src/lib/assistant/prompts.ts
@@ -1,4 +1,5 @@
import { z } from 'zod';
+import type { Character } from '@/lib/schemas';
import type { CampaignContext, CreatureCandidate } from './context';
export const balanceSuggestionSchema = z.object({
@@ -44,3 +45,43 @@ export function buildBalancePrompt(
return { system, user };
}
+
+// ---------------- Level-up advisor ----------------
+
+export const buildRoutesSchema = z.object({
+ routes: z.array(z.object({
+ title: z.string(),
+ summary: z.string(),
+ playstyle: z.string(),
+ })).min(3).max(4),
+});
+export type BuildRoute = z.infer['routes'][number];
+
+export const buildStepsSchema = z.object({
+ steps: z.array(z.object({ label: z.string(), detail: z.string() })).min(1),
+});
+export type BuildStep = z.infer['steps'][number];
+
+function charLine(c: Character): string {
+ const abilities = Object.entries(c.abilities).map(([k, v]) => `${k.toUpperCase()} ${v}`).join(', ');
+ return `${c.name}, a level ${c.level} ${c.className || 'adventurer'} (${abilities})`;
+}
+
+export function buildLevelUpRoutesPrompt(ctx: CampaignContext, c: Character, nextLevel: number): { system: string; user: string } {
+ const system =
+ `${ctx.systemConstraint}\n\n` +
+ `You are a character-building advisor for ${ctx.systemLabel}. Propose 4 distinct build routes for the next level. ` +
+ `Each route needs a short title, a one-sentence summary, and a one-phrase playstyle. ` +
+ `Use only ${ctx.systemLabel} mechanics, feats, and class options — never another system's.`;
+ const user = `Character: ${charLine(c)}. Leveling up to ${nextLevel}. Suggest 4 build routes.`;
+ return { system, user };
+}
+
+export function buildLevelUpStepsPrompt(ctx: CampaignContext, c: Character, nextLevel: number, chosenRoute: string): { system: string; user: string } {
+ const system =
+ `${ctx.systemConstraint}\n\n` +
+ `You are a character-building advisor for ${ctx.systemLabel}. Give concrete, ordered steps to pursue the chosen build route at this level. ` +
+ `Each step has a short label and a one-sentence detail. Reference only ${ctx.systemLabel} options (feats, ability boosts, spells, skill increases as appropriate). Keep to 3–5 steps.`;
+ const user = `Character: ${charLine(c)}. Leveling up to ${nextLevel}. Chosen route: "${chosenRoute}". How should they achieve it?`;
+ return { system, user };
+}
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
index 91ae0b1..0095428 100644
--- a/tsconfig.app.tsbuildinfo
+++ b/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/router.tsx","./src/vite-env.d.ts","./src/app/RootLayout.tsx","./src/components/CommandPalette.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/assistant/AssistantPage.tsx","./src/features/assistant/EncounterTipCard.tsx","./src/features/assistant/useEncounterAdvisor.ts","./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/player/PlayerViewPage.tsx","./src/features/settings/AssistantSettings.tsx","./src/features/settings/SettingsPage.tsx","./src/features/world/CalendarPage.tsx","./src/features/world/DashboardPage.tsx","./src/features/world/HomebrewPage.tsx","./src/features/world/MapsPage.tsx","./src/features/world/NotesPage.tsx","./src/features/world/NpcsPage.tsx","./src/features/world/QuestsPage.tsx","./src/features/world/homebrew.ts","./src/features/world/hooks.ts","./src/lib/cn.ts","./src/lib/format.ts","./src/lib/ids.ts","./src/lib/rng.ts","./src/lib/sample.ts","./src/lib/useDebouncedCallback.ts","./src/lib/useRoll.ts","./src/lib/wikilinks.test.ts","./src/lib/wikilinks.ts","./src/lib/assistant/advisors.test.ts","./src/lib/assistant/advisors.ts","./src/lib/assistant/context.test.ts","./src/lib/assistant/context.ts","./src/lib/assistant/encounter.ts","./src/lib/assistant/patterns.test.ts","./src/lib/assistant/patterns.ts","./src/lib/assistant/prompts.test.ts","./src/lib/assistant/prompts.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/backup.test.ts","./src/lib/io/backup.ts","./src/lib/io/character.test.ts","./src/lib/io/character.ts","./src/lib/io/file.ts","./src/lib/llm/client.test.ts","./src/lib/llm/client.ts","./src/lib/llm/types.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/lib/sync/index.ts","./src/stores/assistantStore.ts","./src/stores/macroStore.ts","./src/stores/rollStore.ts","./src/stores/uiStore.ts","./vitest.setup.ts"],"version":"5.9.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/router.tsx","./src/vite-env.d.ts","./src/app/RootLayout.tsx","./src/components/CommandPalette.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/assistant/AssistantPage.tsx","./src/features/assistant/CampaignInsights.tsx","./src/features/assistant/EncounterTipCard.tsx","./src/features/assistant/useEncounterAdvisor.ts","./src/features/assistant/useLevelUpAdvisor.ts","./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/LevelUpAdvisor.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/player/PlayerViewPage.tsx","./src/features/settings/AssistantSettings.tsx","./src/features/settings/SettingsPage.tsx","./src/features/world/CalendarPage.tsx","./src/features/world/DashboardPage.tsx","./src/features/world/HomebrewPage.tsx","./src/features/world/MapsPage.tsx","./src/features/world/NotesPage.tsx","./src/features/world/NpcsPage.tsx","./src/features/world/QuestsPage.tsx","./src/features/world/homebrew.ts","./src/features/world/hooks.ts","./src/lib/cn.ts","./src/lib/format.ts","./src/lib/ids.ts","./src/lib/rng.ts","./src/lib/sample.ts","./src/lib/useDebouncedCallback.ts","./src/lib/useRoll.ts","./src/lib/wikilinks.test.ts","./src/lib/wikilinks.ts","./src/lib/assistant/advisors.test.ts","./src/lib/assistant/advisors.ts","./src/lib/assistant/context.test.ts","./src/lib/assistant/context.ts","./src/lib/assistant/encounter.ts","./src/lib/assistant/levelup.test.ts","./src/lib/assistant/levelup.ts","./src/lib/assistant/patterns.test.ts","./src/lib/assistant/patterns.ts","./src/lib/assistant/prompts.test.ts","./src/lib/assistant/prompts.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/backup.test.ts","./src/lib/io/backup.ts","./src/lib/io/character.test.ts","./src/lib/io/character.ts","./src/lib/io/file.ts","./src/lib/llm/client.test.ts","./src/lib/llm/client.ts","./src/lib/llm/types.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/lib/sync/index.ts","./src/stores/assistantStore.ts","./src/stores/macroStore.ts","./src/stores/rollStore.ts","./src/stores/uiStore.ts","./vitest.setup.ts"],"version":"5.9.3"}
\ No newline at end of file