Replaces the ability score card grid in both the character sheet and
creation wizard with a proper breakdown table matching MPMB's layout:
- Sheet: "Breakdown" button opens a modal with every source labeled as a
column (Ancestry, Race, ASI, Level boost, etc.), a Manual override ±
cell per ability, and Total/Mod columns. Clicking any ability card also
opens it. Legacy characters without abilityBuild show a single Base column.
- 5e wizard: method buttons (standard/point-buy/roll/manual) remain, but
the ability grid is replaced by a table — Base column adapts to method
(select for array/roll, number field for buy/manual), Race and ASI columns
appear only when relevant, point-buy remaining shown in footer.
- PF2e wizard: replaces dropdown grid with a click-to-assign boost table —
fixed ancestry boosts shown as +2 chips, free-boost slot columns let you
click any cell to assign/unassign that slot to that ability (with source
uniqueness enforced inline).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the table tucked into Settings with a real instance dashboard,
gated server-side to ADMIN_USERS (frontend nav entry appears for admins only).
Backend (/api/admin/*, admin-token required):
- GET overview: uptime, RSS/heap, data-dir bytes, account count vs MAX_USERS,
default quota, campaign/character totals, live-room load
- GET users: created date, usage vs effective quota, session count, admin flag
- POST users/:name/revoke — log a user out everywhere
- DELETE users/:name — delete account + backup + owned campaigns + published
characters (admin accounts are protected from deletion)
- GET/DELETE campaigns — oversight + removal incl. published characters
- GET rooms — players/seats/images/idle per room; join codes never exposed
Frontend: new /admin page (health cards, account management with quota editor +
two-click destructive confirms, campaign table, live-room table), ShieldCheck nav
entry via useIsAdmin(), Settings now links to the panel instead of embedding it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited every calculation (26-agent adversarial workflow + hand-verification).
Core math was sound; fixed real bugs, closed parity gaps, and overhauled the
character-build UX. 396 -> 424 tests; lint clean; build green.
Correctness fixes:
- Heavy armor no longer applies a negative DEX penalty to AC (3 paths + default)
- 5e Exhaustion 4 (HP-max halved) implemented via deriveEffectiveMaxHp + badge
- PF2e dazzled no longer makes a creature easier to hit
- PF2e immunity 'all' zeroes all damage (was misfiled as a condition immunity)
- Director schema bounds; PF2e spell-save basis from the `basic` flag; 5e
disadvantage-only save guard; remove bogus Concentrating, add Broken/Quickened
- Fix 3 lint errors (useCloud hooks naming, useless escape, explicit any)
PF2e survival subsystem (parity with 5e death saves):
- mechanics/dying.ts (maxDying/isDead/knockOut/applyRecovery/pf2eRestDecay)
- DefensesSection knock-out + recovery-check + death UI; rest-decay in applyRest;
drained HP reduction
Character-build UX:
- Persisted abilityBuild (base + per-source contributions); sheet & wizard show
every ability source; higher-level PF2e gains L5/10/15/20 boosts
- Creation surfaces granted skills (incl. new PF2e background skill parsing)
- LevelUpModal enumerates features gained + prompts every owed choice (subclass,
feats, fighting style, ...) via collectChoices/collectFeatures/subclassPrompt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
No-shortcuts pass on the class feature/choice engine, and PF2e brought level with 5e.
5e subclass depth (subclass.5e.ts):
- SUBCLASS_PROGRESSION_5E: per-level features for ~22 subclasses (SRD cores + popular),
and subclass-specific CHOICES — Battle Master maneuvers (3/5/7/9 by level), Arcane Archer
shots, Rune Knight runes, Way of Four Elements disciplines, Hunter's selectable features
(Hunter's Prey/Defensive Tactics/Multiattack/Superior Defense), Totem Spirit, Dragon
Ancestor. collectFeatures5e now expands the chosen subclass into its real features.
PF2e feature/choice engine (data.pf2e.ts) — full parity:
- CLASS_PROGRESSION_PF2E: signature features by level for all 24 classes.
- Universal choices generated for every class: Class feats (per-class levels incl. the
1st-level martial feat), Skill feats (even levels), General feats (3/7/11/15/19),
Ancestry feats (1/5/9/13/17), plus the 1st-level subclass (Instinct/Doctrine/Bloodline/
Racket/…) from PF2E_SUBCLASSES with options.
Engine generalized: collectChoices(system)/collectFeatures(system) dispatch 5e vs pf2e;
ClassFeaturesSection now renders for BOTH systems (pf2e subclass resolves via choices[]).
Verified in-app: PF2e Barbarian 5 → Instinct (6 options) + Class(3)/Skill(2)/General(1)/
Ancestry(2) feats = '9 to choose' + Rage/Deny Advantage; selecting Dragon Instinct resolves
and shows as a feature. 5e Fighter+Battle Master → Maneuvers (choose 3) + Combat Superiority.
352 tests green (+ pf2e & subclass suites), build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A character's class choices were never prompted (the user's example: a Warlock must
choose a Pact Boon + Eldritch Invocations). Now:
- CLASS_PROGRESSION_5E authors all 12 classes' features by level AND every decision point:
Fighting Style, Pact Boon, Eldritch Invocations (count grows by level), Metamagic,
Expertise, Favored Enemy/Terrain, with full option lists.
- collectChoices5e / collectFeatures5e compute, across a (multiclass) character's classes,
exactly which choices are unlocked (and how many to pick at the current level) and which
features are gained. Subclass selection (classes editor) and ASI/feat (builder/level-up)
are handled elsewhere and excluded. Unit-tested incl. the Warlock pact example.
- New 'Class Features & Choices' sheet section lists features by level and presents a picker
per unlocked choice, with a 'N to choose' badge so nothing is missed. Resolved choices
persist (character.choices, v17 migration).
Verified in-app: Fighter 3 / Warlock 5 surfaces Fighting Style (1), Pact Boon (1), Eldritch
Invocations (3) + Pact Magic feature; selecting one drops the pending count. 346 tests green
(7 new), build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- FeatPickerModal: browse the 104-feat compendium (PHB/XGtE/TCE) and add a real feat with
its source + description, instead of typing a name. Wired into FeatsSection (5e).
- WeaponPickerModal: pick a 5e weapon and get a ready-to-roll attack with the correct
damage dice/type and a sensible ability default (ranged/finesse → DEX, else STR).
Wired into AttacksSection (5e).
Directly addresses 'feats are missing' and 'selectable weapons are missing'. Verified
in-app (Rapier→finesse attack; Great Weapon Master + XGtE/TCE feats listed). 339 tests
green, build OK. (Subclass/background mechanical effects + higher-level auto-build are the
remaining Phase 4 depth.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Character gains a classes[] array (per-class levels + subclass); className/level become
derived mirrors (primaryClass/totalLevel/normalizeClassMirror helpers). Dexie v16
migration backfills classes[] from className/level and pulls Subclass:/Background: out
of the notes free-text (whole-line, keeping the rest).
- Correct 5e multiclass derivation (unit-tested): dnd5eMulticlassHp (first character level
= max die, rest avg+CON), dnd5eMulticlassSpellLevel, dnd5eClassesSlots (one caster uses
its own table; two+ use the PHB multiclass table; warlock pact stays separate).
- Sheet ClassesEditor (5e): add/remove classes, set subclass/level; editing auto-recomputes
the level/class mirrors and reconciles spell-slot maxes (preserving spent slots) — so
spell slots are now calculated automatically. A 'Recalc HP' button (never clobbers
hand-rolled HP silently).
- LevelUpModal is class-aware: pick which class to advance or multiclass into a new one;
applies to classes[], re-derives mirrors + combined slots. pf2e path unchanged.
- Wizard writes classes[] on creation; subclass no longer stored in notes.
Verified in-app: existing char migrated to classes[]; Fighter 3 -> +Wizard 5 = total 8,
spell slots auto-appear at 4/3/2, reconcile preserves spent, removal restores. 339 tests
green (10 multiclass), build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app loaded SRD-only spells; the parsed MPMB spell file was shallow (names only) and
unused. Now:
- parse_mpmb.py extracts the rich spell fields (level, school, casting time, range,
components, duration, save, ritual, classes) and writes to src/data/srd.
- New compendium/mpmb.ts normalizes MPMB spells to the Open5e Spell shape (school/time
expansion, source-code → label) — pure, unit-tested.
- loadSpells merges 204 non-SRD MPMB spells (95 XGtE, 21 TCE, + Eberron/Theros/Strixhaven/
Fizban's/etc.) into the 321 SRD spells (525 total), deduped by name.
- Spell type gains a field; the compendium spell list shows it ('Cantrip · XGtE')
and adds a Source filter.
Verified in-app (Toll the Dead → 'Cantrip · XGtE'). 333 tests green, build OK.
Only mpmb-spells.json regenerated; feats/others untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 5e skill/save proficiency shows 'Not proficient/Proficient/Expertise' instead of the
PF2e rank names (Untrained/Trained/Expert); PF2e keeps its ranks. (shared rankLabel)
- Characters can be created without a campaign: campaignId is optional ('' = unassigned),
CharactersPage no longer requires an active campaign, the wizard's campaign prop is
optional, and the sheet has a Campaign selector to attach/move later.
- New character details: appearance, personality/behaviour, alignment, and a real
background field (promoted out of notes); a Details wizard step + a Details sheet card.
- Ability step shows a clearer total + 'base · +race' breakdown.
- Point-buy verified correct (8:0..15:9 / 27, clamped 8-15, Next gated on budget).
All schema additions default-safe (characterDefaults updated). 329 tests green, build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PF2e characters were generated with the 5e method (standard array / 27-pt point-buy /
4d6) and never applied ancestry/background/class attribute boosts, so every derived stat
was wrong. The PF2e ability step is now boost-based:
- Every score starts at 10; ancestry fixed boosts + flaw, ancestry free boost(s),
background (choose-one + free), class key ability, and 4 free boosts are applied with
the +2-below-18 / +1 rule (pf2eApplyBoosts).
- Boost data parsed from the bundled ancestry/background fields (parseAncestryBoosts,
parseBackgroundBoosts, parseFlaw). Defaults are a legal, class-favoring assignment;
same-source duplicate boosts are disabled in the pickers (PF2e doesn't allow them).
- Live ability cards show the computed scores. 5e keeps array/point-buy/manual.
Verified in-app (Dwarf Alchemist Acolyte → STR 14/DEX 12/CON 14/INT 18/WIS 12/CHA 8,
flaw + fixed boosts correct). Pure core has 8 unit tests. 5e path unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CreationWizard: add sys.skills to the loader effect's deps (stable singleton).
- Extract NotFound to src/app/NotFound.tsx so router.tsx no longer trips
react-refresh/only-export-components.
Branch is now warning-clean (the 3 remaining eslint errors are pre-existing on master,
in files this branch does not touch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New themed useConfirm() hook (no native dialogs). World deletes (notes, quests,
homebrew, maps) now confirm before removing.
- Dice 'Clear history' is disabled with no active campaign (it used to wipe EVERY
campaign's rolls) and now confirms; only ever clears the active campaign.
- sessionLog is included in backup/restore and the campaign cascade-delete — it was
silently dropped on backup and orphaned on campaign delete.
- Unknown URLs render a themed 'Page not found' with a link home instead of a blank shell.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wizard collected race/background/skill choices but applied almost none of them.
Now (5e):
- Racial ASI is parsed and folded into the final ability scores (Human +1 each, etc.);
every downstream stat (HP, AC, attacks, saves, DCs) reflects it. Shown on the
Abilities step and Review.
- Background fixed skills and racial 'proficiency in the X skill' traits are granted
as trained, on top of class picks.
- Skill step prefers the curated class table: canonical 5e skill lists (fixes the
'and Survival'/'Animal Handling' tokenization that dropped skills) and the correct
pf2e free-choice skill count (was inflated for ~13 classes).
UX:
- 'Ready-made hero' templates no longer softlock the Abilities step: scores above the
point-buy cap switch to a new Manual entry mode (1-30) instead of '-Infinity / 27'.
- Standard-array/roll assignment swaps values on collision instead of disabling them.
Parsers extracted to builder/origin.ts with unit tests; buildCharacter gains
grantedSkills. Deferred: pf2e ancestry/background/class attribute boosts (needs the
boost-based generator), subclass/class-feature/feat mechanical effects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pf2e creator showed long, truncated run-ons where 5e shows clean, brief
overviews: classes were cut mid-word at 400 chars, backgrounds rendered the raw
Foundry "<Name> Source Core Rulebook pg. N …" citation prefix, and ancestry
summaries ended in a stray ellipsis.
- new src/features/characters/builder/overview.ts: briefOverview() strips the
Foundry citation prefix, drops a trailing ellipsis, and keeps the first clean
sentence (or a word-boundary trim — never mid-word). A no-op on already-short
text, so 5e is unchanged. Applied to pf2e class/ancestry/background overviews.
- dedupeByName(): the pf2e data carries reprints under duplicate names (94
ancestries, 612 backgrounds) which duplicated options and triggered React
duplicate-key warnings — now de-duplicated at load.
- 6 unit tests for both helpers.
Verified live: pf2e class cards now read as clean single sentences; no citation
junk; no duplicate-key warnings. Gate: 289 unit + 35 e2e + 2 realtime, build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P5 backend (the deferred items):
- conflict-aware cloud sync via optimistic concurrency: the server versions each
backup blob (file mtime) and rejects a push whose base version is stale (409),
so a second device can't silently overwrite a newer cloud copy. The client
tracks its last-synced version; the SyncStatusIndicator surfaces a "Sync
conflict" with one-click resolution (Use cloud / Keep this device), and the
Settings push offers the same choice.
- revocable invites: owner can rotate a campaign's invite code (kills the old
one) and remove a member (drops them + deletes their published characters).
(Skipped editor/player roles — no enforcement target in the current model.)
- image storage: deliberately deferred (live images already stream de-duped on a
separate channel; a backup blob-store is infra with no correctness gain).
P7 — subclass/feat/multiclass depth:
- feats are now first-class tracked records (name + effect text) in a Feats &
Features sheet section, not buried in notes. Dexie v15 migration.
- 5e multiclass spell-slot calculator: a pure multiclassSlots() (full ×1, half
÷2, third ÷3 → full-caster table) + a sheet control to fill the slot table for
multiclass casters. (Honest scope: tracking + slots, not per-subclass feature
automation.)
P8 — test hardening: v2-surfaces e2e (feats, multiclass calc, signals bell,
World nav) + server tests (blob versioning, invite rotation, member removal) +
multiclass unit tests.
Gate green: 283 unit + 35 e2e + 2 realtime. App + server build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5 — live-session & multi-device hardening:
- connectivityStore + SyncStatusIndicator in the shell: shows "Offline" when the
browser is offline and the cloud autosave state (saving / saved / failed) when
signed in; useCloudAutosave now reports its status. Owns online/offline events.
- server presence heartbeat: protocol-level ping/pong per socket terminates a
vanished connection (~30s), so a player who closes their laptop drops out of
the GM's roster instead of lingering. No client changes needed.
Phase 6 — onboarding & friction:
- navigation: surfaced the previously orphaned routes in the rail — a new
"World" group (Notes, NPCs, Quests, Calendar) and Homebrew + Assistant under
Reference. (Empty-state hints and map rename already existed.)
- combat keyboard control: n/→ next turn, p/← previous turn, guarded so it never
fires while typing; button tooltips document it.
Test hygiene: scoped now-ambiguous nav links in e2e to the Primary landmark, and
fixed pre-existing stale emoji selectors in the realtime suite (📡 Host → Host,
📨 Just for you → Just for you) left over from the Living Codex emoji purge.
Gate green: tsc + 277 unit + 34 e2e + 2 realtime. App + server build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Completes the deployable Phase 1-3 chunk: the combat tracker now runs the math.
- condition-effects engine: declarative CONDITION_EFFECTS_5E/PF2E table +
deriveState(system, speed, conditions) → effective speed / advantage state /
incapacitation / pf2e status penalty. Tracker shows effect badges
("Speed 0", "Disadv. attacks", "−2 status"); pure + 10 unit tests.
- damage types: applyDamage(c, amount, type?) consults snapshotted monster
DamageDefenses (immune→0, resist→halve, vulnerable→double), back-compatible.
Monster add snapshots damageDefenses; tracker shows a type picker only when a
creature has typed defenses, and the log notes resisted/vulnerable amounts.
- concentration + death saves are surfaced as REMINDERS, never auto-rolled:
damaging a concentrating PC logs "roll a DC N Con save…"; a downed PC's turn
logs "make a death saving throw." The player rolls and resolves via the Drop
button / death-save pips they already own. (Per user: no auto dice rolls —
it removes the player from their character.)
Build + 271 unit tests green. Verified live: grapple → Speed 0 badge;
fire-immune creature shows the immune-labelled damage picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Slice-0 vertical that proves the enforcement architecture end-to-end.
- kernel: castSpell (slot consumption + concentration set/replace, upcasting,
warlock pact slots), spendResource/regainResource, rollDeathSave — pure
functions returning Partial<Character> patches. 13 unit tests.
- applyRest now ends concentration on any rest (+ test).
- character sheet SpellcastingSection: per-spell Cast button, concentration
banner with Drop, concentration markers; ResourcesSection −/+ routed through
the kernel.
- player MyCharacterPanel: castable spell list + concentration banner, so a
seated player casts and it broadcasts via the existing playerPatch flow.
- live sync: partialCharacterDiffSchema carries concentration; GM applies it;
playerCharacterSchema mirrors slots/concentration/resources (PC-only) and the
player-facing AC now respects equippedArmor. Wire round-trip test added.
Verified live: casting decrements the right slot, switching concentration logs
"Concentration on X ends", banner + Drop render. Build + 258 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lay the foundation for full rules enforcement: a new pure, testable
mechanics kernel (src/lib/mechanics/) that derives structured, enforceable
game data from the loosely-typed compendium, plus the first user-facing
payoff (accurate armor AC + spell mechanics on the sheet).
- normalizers: spell (5e + pf2e), armor, monster defenses → typed
SpellMechanics / ArmorMechanics / DamageDefenses, with memoized derivers
wrapping the existing lazy compendium loaders (no new assets). 11 unit tests.
- character schema: spell entries snapshot concentration/save/damage at
compendium-link time; new top-level `concentration` slot and structured
`equippedArmor` (additive, defaulted). Dexie v14 migration backfills.
- AC model: baseArmorClass now consults equippedArmor (heavy = flat, medium =
Dex-capped) and falls back to the old 10+Dex formula when unarmored. Threaded
through every AC call site; armor picker added to the sheet (5e).
- compendium "Add spell to character" snapshots real mechanics via the kernel.
Backward-compatible: un-enriched spells/characters behave exactly as before.
Build + 244 unit tests green; armor→AC verified live in preview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Pure geometry library src/lib/map/* (grid, distance 5e/pf2e/euclid, AoE
circle/cone/line/square, polygon rasterize + point-in-polygon, brush, fog set
algebra, player projection) with unit tests.
- Schema: map tokens gain size/kind/characterId/hp/conditions/gmOnly; maps gain
drawings/gridUnit(feet)/gridType/fogVersion + point/drawing sub-schemas. Dexie v8
additive migration; mapsRepo.get + transactional mutate.
- Editor split into a shared MapCanvas renderer + MapEditor tool state machine:
fog via brush/rectangle/polygon (+ size, reveal-all/hide-all), measurement
(system-aware distance in feet), AoE templates (circle/cone/line/square preview),
drawing annotations (freehand/line/arrow/rect/circle/text, GM-only), pings, and
richer tokens (NxN size, character link with live HP ring, condition dots, edit popover).
- Player-facing projection: toPlayerProjection strips GM-only content; PlayerMapView
renders it read-only with opaque fog; uiStore.activeMapId + 'Show to players'; the
player view now shows the live battle map. enemyStatus extracted to
src/lib/combat/playerProjection.ts (reused, and for Phase 18).
9 geometry unit tests; maps e2e covers upload→token→reveal-all→player projection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bare name/class/level form left new characters as shells (1 HP, all-10
abilities, no proficiencies). Now creation and level-up are guided and auto-populate.
- Hand-authored progression tables (no SRD class/slot data exists on disk):
src/lib/rules/{dnd5e,pf2e}/progression.ts — core classes' hit die/HP, key
abilities, save & perception proficiencies, trained-skill counts, caster type;
5e full/half/pact spell-slot tables, pf2e slot approximation; ASI/boost/skill
schedules.
- src/lib/rules/progression.ts: getClassDefs/getClassDef, classSkillChoices/Count,
buildCharacter() (HP from hit die + CON over levels, trained saves/skills, spell
slots + ability), planLevelUp() (HP gain, new slots, ASI/boost/skill choices),
applyIncreases/bumpRank. Pure + 12 unit tests.
- Multi-step CreationWizard (Basics → Abilities → Skills → Review) replaces the
old form; pulls PF2e ancestry HP/speed from the bestiary; review previews HP/AC/
saves/skills/slots; lands on a ready-to-play sheet.
- LevelUpModal reworked into a guided wizard: auto HP + spell slots, presents the
level's actual choices (5e ASI/feat, pf2e ability boosts + skill increase) and
applies them; keeps the strategic build-route advisor.
e2e helper drives the wizard; updated 7 specs to the new flow; new character-wizard
spec asserts a complete level-3 Wizard (HP 17, slots 4xL1 2xL2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
- 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>
- src/lib/assistant/context.ts: buildCampaignContext() assembles a system-aware
snapshot led by an explicit systemConstraint (anti cross-system hallucination),
party, recent encounters with computed difficulty, quest/note summaries.
pickCreatureCandidates() pulls level/CR-appropriate creatures from the correct
bestiary so LLM tips are grounded in real data.
- src/lib/assistant/patterns.ts: difficultyTendency() + detectThemes() classify
whether the DM's encounters skew too easy/hard, rating each fight against its
party-level snapshot.
- Encounter schema gains optional partyLevelsSnapshot + outcome (Dexie v7, additive).
Combat tracker snapshots PC levels at start and rates difficulty against them.
17 assistant unit tests (context + patterns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bring-your-own-key assistant config in a dedicated persisted store
(src/stores/assistantStore.ts); partialize drops the key from localStorage
when 'remember' is off. Key lives in localStorage only, so it's auto-excluded
from Dexie backups.
- Provider-agnostic LLM client (src/lib/llm) over fetch, no SDK: Anthropic
Messages + OpenAI-compatible Chat Completions (covers OpenAI/OpenRouter/
Ollama/LM Studio via custom base URL). Structured JSON output parsed +
Zod-validated; timeouts; typed error kinds; key never leaked in errors.
- Settings 'Assistant (AI)' section with provider/model/baseUrl/key, remember
+ enabled toggles, and Test connection.
- CSP connect-src widened to any https + localhost for the BYO endpoint.
10 client unit tests; settings e2e asserts config persists and the key is
never present in an exported backup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
- Homebrew entity (monster/spell/item/feat/condition) — Dexie v6, repo, hook,
cascade-deleted with campaign
- Homebrew editor page: per-kind fields + description, create/edit/delete
- Compendium merges campaign homebrew of the matching kind (HB badge, generic
HomebrewDetail) with the same cross-links (add monster->combat, spell/item->character)
- Content packs: export/import homebrew as JSON (validated, ids reassigned)
- System extensibility documented via the existing RulesSystem registry seam
- Fix: card editors (homebrew/npc/quest/note) debounce-save the FULL snapshot, not
partial patches — editing two fields fast no longer drops an edit
New homebrew e2e + cascade test coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Player View (/play): read-only projector screen — party HP bars + conditions,
active-encounter initiative with current turn; enemy HP hidden behind a
Healthy/Bloodied/Down status band; Fullscreen button
- SyncAdapter seam (src/lib/sync): local-first default via Dexie liveQuery;
documents where a future networked (WebSocket/CRDT) backend plugs in
- Dashboard + routes get a Player View entry
- Robustness: encountersRepo.mutate() does transactional read-modify-write so
rapid combat mutations can't overwrite each other (old C19 race); tracker uses it
New player-view e2e. Networked multiplayer backend intentionally deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
- Dice notation: exploding (Nd6!) and reroll-below (Nd6r2), capped + tested
- Degrees of success / roll-vs-DC (PF2e ±10 + nat 20/1 steps; 5e crit on nat 20/1)
- Global roll tray (shared store + RollTray in layout) with animated result + degree
- Roll from the character sheet: skills, saves, and attack to-hit/damage are
clickable and post to the tray + history (rollAndShow/rollCheck helpers)
- Saved roll macros per campaign (persisted) on the Dice page
10 new unit tests (notation explode/reroll, degrees), interactive-dice e2e.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Condition durations + auto-expiry: timed conditions tick down on the affected
combatant's turn and drop at 0 (engine tickConditions; UI rounds field + (Nr) chip)
- Initiative: Roll-all (d20 + per-combatant bonus, re-sort anchored) + group/quantity add
- Combat log: per-round event feed (turns, round changes, damage/heal, expiries,
removals) + in-memory Undo of the last change
- Encounter difficulty budget: 5e XP thresholds + PF2e level budget (pure lib +
tests); live difficulty/XP-award readout from monster combatants vs the party
- Combatants carry initBonus + cr/level; compendium add-to-combat passes them
18 new unit tests (durations, applyInitiatives, budget), new combat-depth e2e.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Scraped PF2e reference from Archives of Nethys ES into public/data/pf2e/
(spells, creatures, feats, equipment, weapons, armor, ancestries, heritages,
backgrounds, archetypes, actions, conditions, deities) via scripts/fetch_pf2e.ts
- Registry-driven compendium: system toggle + per-category nav + generic filters
- 5e expanded: weapons, armor, feats, conditions added alongside spells/monsters/items
- PF2e data served as static assets (fetched, not bundled) for fast native parse
- Cross-links: add spell->spellbook / item->inventory to a campaign character;
add monster/creature -> open encounter
- Condition tooltips in combat sourced from the conditions glossary
- Tests: registry filter unit tests, compendium e2e (browse + cross-link)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- System-aware condition list (5e PHB / pf2e) in src/lib/rules/conditions.ts
- Dropdown replaces free-text input; non-valued conditions add instantly,
valued ones (Exhaustion, Frightened N, etc.) show a value field, plus Custom
- Already-applied conditions disabled in the list; chips remain click-to-remove
- New e2e covering preset + valued add and tag removal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Inventory + currency + encumbrance vs carrying capacity
- Class resources with correct short/long/daily rest recovery (applyRest)
- Spellcasting: slots (+pact), known/prepared spells, derived DC + attack
- Defenses: 5e death saves/inspiration/exhaustion, pf2e dying/wounded/hero points
- Derived weapon attacks + passive perception via extended RulesSystem
- Dexie v2 migration backfills new fields; debounced save flushes on pagehide
- 9 new unit tests, new character-depth e2e; all green
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>