Commit Graph

18 Commits

Author SHA1 Message Date
NilsBriggen cac5979820 D6: AI Director polish + parity hardening
- pf2e parity: a unit test confirms the director carries the system through
  (systemConstraint + prompt say Pathfinder 2e, no cross-system leak) and still
  produces a grounded deterministic turn. (Degree-of-success/conditions already
  branch per system via the shared kernel.)
- UX: the transcript pane is now a bounded, scrollable region (max-h-[60vh]) so
  long sessions don't blow the page layout; it still auto-scrolls to the newest line.
- e2e: e2e/director.spec.ts exercises the full flow against a real browser — DM
  narrates a grounded scene with no API key (deterministic badge, empty-state
  clears, Continue appears), and the player persona seats a party character and
  takes a turn.

396 unit tests + 2 director e2e green; lint clean; build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 03:25:41 +02:00
NilsBriggen ab8956f0cb D5: broadcast the AI Director to players at the table
For live co-GM play, the AI's narration now reaches the players' screens, and its
roll buttons already broadcast their results to the table (via rollAndShow →
broadcastGmRoll).

- When hosting a session (role gm + connected) and "Share narration with players"
  is on, each director narration is pushed over the table chat channel (sendChat)
  so players see the AI DM speak in their session feed. A no-op when not hosting.
- SceneControls gains the share toggle (default on); directorStore persists it.

Reuses the existing, e2e-tested realtime chat primitive — strictly additive, no
protocol/server change. GM stays authoritative. 395 tests green; lint clean;
build OK; page + controls verified rendering on a clean load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 03:21:52 +02:00
NilsBriggen 35e0dc4fc1 D4: token budget — rolling transcript summary keeps long sessions bounded
Director sessions can run for hours; the context would grow without bound. Now a
fixed live window of recent turns is sent each turn, and older turns are folded
into a rolling per-session summary.

- planContext (pure): summary covers [0, summarizedThrough); the live window is
  the rest. Once the window exceeds windowSize + SUMMARIZE_BATCH, the oldest turns
  fold down to the last windowSize — never leaving a gap between summary and window.
- maybeSummarize (after each turn): folds the batch into AiSession.summary via a
  separate complete() call when a key is set, else a deterministic compaction
  (one compact line per turn, capped) — bounded with or without an LLM.
- The summary already rides in the system prompt (it never displaces the grounded
  roster), so the closed-set anti-hallucination anchor is preserved as history scrolls.

Pure windowing + deterministic-summary covered by unit tests. 395 tests green;
lint clean; build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 03:17:45 +02:00
NilsBriggen 2f0700f8d3 D3: AI Player — pilot a real PC, with caster actions
The director can now play a party character, not just DM. A Mode switch
(Dungeon Master / Player) plus an AI-seat picker choose which PC the AI pilots.

- Scene injection: when seated, the controlled PC's real sheet detail is fed to
  the director — attacks with derived to-hit (sys.weaponAttack) + damage
  expressions, spells (level/concentration), resources, and spell slots — so the
  AI player acts from true numbers, in first person.
- Persona-aware prompt + cue already route DM→runs monsters / player→"it is your
  turn"; the seat sets controlledName.
- Caster actions (castSpell/spendResource) flow through the D2 approve-each apply
  bridge unchanged — slot/resource spend is enforced by the kernel, concentration
  mirrors to the combatant.
- UI: SceneControls (mode + seat), seat-gating ("pick a character"), and
  player-flavored buttons (Take turn). directorStore gains controlledCharacterId.

Verified in-app: switching to Player mode, seating "Lia the Brave · Fighter 3",
and taking a turn produced first-person narration. 388 unit tests green (incl. new
player-persona scene/ prompt tests); lint clean; build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 03:13:47 +02:00
NilsBriggen ce9bbc8220 D2: AI Director apply bridge — approve-each, routed through the rules kernel
The director's proposed state changes are now one-click "Apply" actions. Nothing
mutates a Character or Encounter until the human clicks Apply — the AI never
writes game state on its own.

- useDirectorAction (the 3rd anti-hallucination gate): re-resolves every action's
  target against the live roster/encounter and rejects unknowns, then routes
  through the pure kernel + combat engine — applyDamage/applyHealing/setTempHp/
  updateCombatant (damage/heal/tempHp/condition), nextTurn (advanceTurn),
  logEvent (log), and castSpell/spendResource for caster actions. Persists via the
  transactional encountersRepo.mutate / charactersRepo.update.
- No-auto-roll preserved: damaging a concentrating creature SURFACES a
  concentration save (concentrationDC) as a roll button — it is never auto-rolled.
  Massive-damage death is flagged. castSpell mirrors new concentration onto the
  combatant so later saves surface.
- ActionCard gains a working Apply button (idempotent: disables after applying,
  shows the result/skip reason); applied actions append a system transcript entry
  so the next turn sees the new ground truth.

8 integration tests against a real Dexie cover each action→kernel patch, the
concentration-save surfacing, and rejection of off-roster targets. 386 unit tests
green; lint clean; build OK; verified the page renders + runs a turn on a clean load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 03:07:16 +02:00
NilsBriggen 961fe8655a D1: AI Director (MVP) — grounded AI DM narration + no-auto-roll dice buttons
The first user-visible slice of the AI DM / AI Player "director": a new
/director page where the AI narrates the scene, voices NPCs/monsters, and runs
enemies — grounded entirely in the campaign's real party and active encounter,
and degrading to a deterministic narrator when no AI key is set.

Engine (pure, framework-free) in src/lib/assistant/director/:
- schema.ts: DeepSeek-tolerant directorTurnSchema (z.preprocess structural
  repair + z.coerce + .catch() enums + per-element safeParse-drop) → a single
  validated turn { narration, rollRequests[], actions[], suggestions[] }.
- context.ts: buildDirectorScene assembles a CLOSED roster from the active
  encounter (or the party) with deriveState badges — the only entities the
  director may name.
- prompt.ts: persona-aware system prompt (DM/player) leading with the
  systemConstraint; multi-turn message mapping from the transcript.
- engine.ts: runDirectorTurn + sanitizeTurn — the anti-hallucination gate that
  drops any action referencing an off-roster entity (and ungrounded
  addCombatant), mirroring the encounter advisor's candidate filter.
- fallback.ts: deterministic director that still surfaces roll buttons.

Hard rules, enforced:
- NEVER auto-roll: a roll fires only from RollRequestCard's onClick (rollAndShow).
  A unit test asserts the engine layer never imports the roll seam, making
  auto-roll structurally impossible.
- Approve-each: D1 is read-only (actions render as previews; the Apply bridge
  lands in D2). The director never writes game state.
- Always-on fallback: works with no API key.

Also: Dexie v18 `aiSessions` table + aiSessionsRepo (+ cascade delete), a small
directorStore, a Settings card, and the /director route + nav entry.

Verified in-app on the sample 5e campaign: grounded narration named the real
current combatant; on an enemy's turn a "Aller Rosk attack vs Pip Underbough"
roll card appeared with diceRolls UNCHANGED until the button was clicked, which
then fired exactly one roll and recorded the result back into the transcript.
381 unit tests green; lint clean (3 pre-existing); build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 02:55:42 +02:00
NilsBriggen 895807d6c9 Polish sprint 2/2: layouts, UX consistency, pf2e conditions
Layout:
- CombatantRow: action controls (damage/AC/move/remove) grouped into one
  right-aligned wrapper so they wrap together instead of scattering on narrow
  widths.
- top bar: the campaign switcher now shrinks/truncates instead of overflowing
  on tablet widths.
- character sheet header: stat coins go full-width grid on mobile; the name
  truncates and scales down on small screens.

UX consistency:
- player Cast and resource Spend/Regain no longer fail silently — Cast shows the
  reason, resource −/+ disable at their bounds.
- the AI encounter builder now catches load failures and reports them; level-up
  advisor shows the human error message, not the error-kind enum.
- section headers standardized on the .smallcaps design token across 16 files
  (replacing an ad-hoc uppercase class).
- player HP bar uses the shared Meter primitive.

pf2e depth: condition-effects table gains drained, dazzled, enfeebled, fatigued.
FeatCard: dropped a dead text-xl wrapper left from the emoji purge.

Left as documented (functional, off-theme, refactor-risky): the native
confirm/prompt in Settings cloud-conflict and the session-password flow.

Gate: 293 unit + 35 e2e + 2 realtime; tsc + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:38:24 +02:00
NilsBriggen dd694477b2 Polish sprint 1/2: restore crash fix, pf2e parity, full glyph purge
Crash fix:
- restoreBackup (file + cloud pull) now validates each row through its Zod
  schema, backfilling new fields — an older backup no longer lands characters
  missing feats/concentration/etc and crashing the sheet. + test.

pf2e parity (closing feature gaps vs 5e):
- +9 missing classes (Animist, Exemplar, Gunslinger, Inventor, Kineticist,
  Magus, Psychic, Summoner, Thaumaturge) with data + class tips.
- the wizard now enriches pf2e classes with their caster ability, so pf2e
  spellcasters get the Spells step + "Caster" tag like 5e.
- editable Perception rank and spellcasting proficiency on the pf2e sheet
  (fixes wrong initiative / spell DC at higher levels); rank-10 spell slots.
- pf2e monster resistances/weaknesses/immunities now apply in combat (flat
  amounts: resistance subtracts, weakness adds, 'all' matches any type). + tests.

Glyph purge (finishing the emoji→Lucide migration): replaced ~40 leftover
text-glyphs (✕ − + ✦ 🤫 ⬆ ⬇ ☑ ☐ ▶ ◀ ‹ › ★ ↻ ▸ ↑ ●) with Lucide icons across
Modal (every dialog), the sheet sections, dice, settings, player panels, world
pages, map editor, roll tray, and session UI.

Gate: 293 unit + e2e green; tsc + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:29:36 +02:00
NilsBriggen 3d2c68a1a1 V2 Phase 4: Signals engine — proactive, app-wide one-click assists
The "buttons, not chatbot" core: the app recognizes a situation and offers the
fix; the user never has to formulate a request.

- src/lib/assistant/signals.ts: buildSignals() aggregates the existing advisor
  detectors with new kernel-aware ones — quest-complete (all objectives done →
  "Mark complete"), caster out-of-slots → "Short rest", concentrating-while-
  bloodied warning — deduped and severity-sorted. 6 unit tests.
- extended SuggestAction with shortRest + completeQuest; one shared dispatcher
  (useSignalAction) used by both surfaces so behaviour can't drift.
- SignalsBell: an ambient bell + popover in the top bar, visible from every
  screen, with a count badge, one-click actions, and per-signal/clear-all
  dismiss. AssistantPage refactored onto the same buildSignals + dispatcher.
- the AI cards were already actionable (NpcGen → npcsRepo, EncounterTip → add
  to encounter, Assistant encounter builder → combat).

Also fixed a pre-existing invalid-HTML bug: ConditionPicker rendered a <Check>
SVG inside <option> (React hydration error spam) → plain "✓" text.

Build + 277 unit tests green; bell verified live (popover, actions, no errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:30:26 +02:00
NilsBriggen 1b275ed5e6 Give reasoning models room; skip the advisor's self-contradictory ask
DeepSeek is a reasoning model — its chain-of-thought counts against the
completion budget. The 1024 max_tokens default let it burn the whole budget
"thinking" and get cut off (finish_reason: length) with an empty content, which
read as a parse failure. Raise DEFAULT_MAX_TOKENS to 4096 so it can finish
reasoning AND emit the JSON. Also return a clear error on empty responses.

The spiral was triggered by a contradictory prompt: the fight was already
"deadly" yet the advisor still asked the model to "add creatures to reach
Deadly." Skip the LLM when the target tier isn't actually harder than the
current one and let the deterministic path ease the over-tuned fight instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:14:49 +02:00
NilsBriggen bde21992bd Let complete() accept coerce/preprocess schemas (fix tsc -b build)
The "creatures" alias fix wraps the balance schema in z.preprocess, whose input
type is `unknown`. complete()'s `schema?: ZodType<T>` pinned input=output=T, so
T inferred as unknown and `res.data.add` failed under tsc -b (project build).

Loosen schema input to `ZodType<T, ZodTypeDef, any>` — T stays pinned to the
schema output, but coerce/preprocess/transform schemas (input ≠ T) are accepted.
Pin the advisor call's type param to BalanceSuggestion, matching the other
complete<...> call sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:52:51 +02:00
NilsBriggen 9a81c04199 Fix combat advisor "AI unavailable (parse)" — coerce numeric counts
The encounter-balance schema was the only AI schema with a numeric field.
LLMs routinely emit numbers as JSON strings ("count": "2"), which z.number()
rejects, failing the whole safeParse and falling back to deterministic with a
"parse" error. The all-string NPC/session/quest schemas never hit this.

- Coerce monster counts (z.coerce.number) and widen the upper bound 8->12 so a
  string or slightly over-eager count is accepted, not rejected. min(1) stays.
- Tell the model in the prompt that count is a plain integer and reasoning /
  targetDifficulty are required.
- Surface the real failure message in the advisor instead of the opaque kind.
- Add a regression test that string counts coerce to numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:43:03 +02:00
NilsBriggen 740cf20b93 UI/UX "Living Codex" 4/n: assistant features + emoji purge
- Replace all emoji icons (~26 instances) with Lucide icons across the app
  (DashboardPage, SessionSidebar, PlayerBoards, MyCharacterPanel,
  PlayerViewPage, EncounterTracker, CampaignsPage, HandoutControl,
  SessionControl, CharacterSheet, EncounterTipCard, ActionGuide)
- Expose real 5e race data in wizard (ASI/vision/traits instead of stub desc);
  add vision field to loadRaces5e return type
- Surface subclass descriptions after picking a subclass in the wizard
- Add per-class tips, race synergy notes, and ability/skill guidance in wizard
- New ActionGuide component: collapsible "What can I do on my turn?" in player view
- New SessionPrepCard: AI + fallback session hook generator on assistant page
- New NpcGenCard: AI + fallback quick NPC generator with "Add to campaign" action
- Inline NPC detail generator (wand button) on each NPC card in NpcsPage
- Quest hook generator button in QuestsPage header
- Condition tooltips in player party view (useConditionGlossary)
- Pass campaign.system through session snapshot so player view is system-aware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:29:06 +02:00
NilsBriggen 3f524ce82c Fixes batch 2: player quests, AI parse, advisor, iPad share
- Player view: a Quest log now syncs to players (snapshot += quests; broadcaster
  passes campaign quests; PlayerBoards renders titles + objective checkboxes).
- AI (DeepSeek etc.): robust JSON extraction — strip code fences anywhere + a
  brace-balanced scanner that ignores prose/braces in strings. Fixes "AI unavailable
  (parse)" with OpenAI-compatible providers. +4 tests.
- Encounter advisor: now bidirectional — for an over-tuned (too-hard) fight it
  suggests REMOVING/swapping monsters instead of only ever adding. +3 tests.
- Character share: works on iPad — native share sheet → clipboard → a copyable
  link modal fallback (was a silently-failing clipboard write).

230 unit + 34 e2e + 2 realtime green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:56:10 +02:00
NilsBriggen 7271978d7d Assistant: deterministic encounter advisor reinforces the existing fight
Instead of proposing a fresh alternative encounter, the non-AI path now:
- adds MORE of the creatures already present ('Add 2 more Goblin'), computing the
  exact count needed and accounting for the 5e encounter multiplier, or
- adds a thematically related stronger creature from the bestiary (shared name
  token, level/CR-appropriate) when that reaches the target with fewer bodies.
Only builds a fresh group when the encounter is empty. Apply clones the existing
combatant (works for homebrew/custom too) or pulls from the pool.
Existing creatures are also offered to the AI as candidates so it can likewise
say 'add another goblin'.

New suggestReinforcements() + baseName() in encounter.ts with unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:33:50 +02:00
NilsBriggen e39def8f02 Phase 15: level-up strategy advisor + campaign insights
- Level-up build-route advisor (src/features/characters/sheet/LevelUpAdvisor.tsx +
  useLevelUpAdvisor): ~4 system-aware routes plus a custom one; choosing a route
  expands it into concrete next-level steps. AI when configured, deterministic
  fallback (src/lib/assistant/levelup.ts) otherwise. Embedded in the existing
  HP-only LevelUpModal, which stays primary.
- Level-up prompts/schemas added to prompts.ts (buildRoutes/buildSteps), each
  leading with the system constraint + class + next level.
- Campaign Insights section on the Assistant page renders deterministic themes,
  with an optional 'ask the assistant to expand' affordance when AI is on.

levelup + prompt unit tests (4 routes both systems, system vocabulary, custom
route); e2e for the deterministic route→steps flow and the insights section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:23:16 +02:00
NilsBriggen dbbf68752e Phase 14: LLM-grounded encounter-balancing tip
- src/lib/assistant/prompts.ts: balanceSuggestionSchema + buildBalancePrompt
  (system message leads with the grounding constraint; restricts choices to the
  provided compendium candidates).
- useEncounterAdvisor hook: builds context → picks system-correct candidates →
  asks the LLM (when configured) for a structured pick, validates + filters names
  to the candidate set, else falls back to the deterministic buildSuggestedEncounter.
  Apply adds the creatures transactionally via encountersRepo.mutate.
- EncounterTipCard in the combat tracker: leads with the detected difficulty
  tendency (or the current difficulty), offers a one-click grounded suggestion
  (AI or deterministic) with propose→confirm Apply.

prompts unit tests + e2e for both the deterministic apply flow and the AI path
(stubbed provider); the AI test also confirms candidate-grounding (only
shortlisted creatures survive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:17:23 +02:00
NilsBriggen 0930136c46 Phase 11: data-driven Assistant
- Deterministic advisors (src/lib/assistant): party resources (downed/bloodied/
  spent slots/rest nudges), session prep (dangling [[wiki links]], active quests,
  empty journal), live combat hints (turn, downed combatants) — pure + tested
- Data-driven encounter builder: greedily assembles level/CR-appropriate monsters
  to a target difficulty via the Phase-3 budget; one click builds + opens combat
- Assistant page: suggestion cards with one-click actions (goto, long rest,
  create note); wired into routes, dashboard, command palette
- 8 advisor/builder unit tests + assistant e2e

Conversational LLM layer intentionally deferred (no provider/key in this env);
the deterministic advisor is the offline default per the plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:06:20 +02:00