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 ( + + ); +} + +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) => ( +
      1. + {s.label}: {s.detail} +
      2. + ))} +
      + )} +
    • + ))} +
    + )} + +
    + 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) => ( +
    1. + {s.label}: {s.detail} +
    2. + ))} +
    + )} +
    + ); +} 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