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>
Building a character above level 1 now grants its Ability Score Improvements. The wizard
computes the ASI count for the input level + class (4/8/12/16/19, plus Fighter's 6/14 and
Rogue's 10) and shows an allocation panel (+1 each, capped at 20, can't over-spend, gated
on Next). Folds into the final scores alongside race bonuses; the ability card shows the
'base · +race · +ASI' breakdown.
Verified: a level-8 Fighter correctly offers 3 ASIs (levels 4, 6, 8 = 6 points). Build OK,
339 tests green.
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>
Fills DND5E_SUBCLASSES with the official archetypes missing from classes.json across all
12 classes (Battle Master, Hexblade, Assassin, the 8 wizard schools, all cleric domains,
etc.) — ~90 entries, merged + deduped by name ahead of the on-disk SRD+homebrew list.
Selectable options only; feature mechanics remain a separate task.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
classes.json ships the SRD subclass + third-party homebrew but is missing most official
PHB/XGE archetypes. New DND5E_SUBCLASSES supplement is merged (deduped by name) ahead of
the on-disk list in the builder, surviving any classes.json regeneration. First entry:
Rogue → Mastermind. (Subclass feature mechanics remain a separate task; this adds the
selectable options.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A player who dropped briefly got a fresh server playerId on auto-reconnect and silently
lost their seat: the UI still showed 'granted' but every HP/condition/spell patch and dice
roll was rejected server-side. Now:
- The client persists a stable playerId (localStorage) and sends it on join.
- The server reuses that id when it isn't already live on another connection (so a second
tab/peer can't hijack an active seat), and on reconnect re-binds the seat to the new
socket and re-sends seatGranted — control is restored seamlessly.
- Seats survive a disconnect for a 5-minute grace window (marked, not deleted); sweep()
evicts seats whose holder never returns.
Adds 3 server tests (reconnect reclaim, grace eviction, anti-hijack). 4 files: join
message gains optional playerId; server dispatch, RoomHub, and wsSync client.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inline Level field silently changed only proficiency-derived stats, leaving HP and
spell slots at the old level with no signal. Adds a hint pointing to the Level Up flow,
which applies HP/slots correctly. (Auto-recompute is intentionally avoided since HP/slots
are often hand-tuned.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The post-damage concentration save was keyed only off Character.concentration, which is
set just when a seated player casts via their own panel — so a GM running a monster/NPC
(or any unseated PC) never got the reminder, defeating the feature. Combatants now carry
an optional concentrating flag with a one-click toggle in the row; noteConcentrationCheck
keys off it (falling back to the linked Character). Verified in-app: toggle + 10 damage →
'roll a DC 10 Constitution save or lose concentration'.
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>
- NumberField keeps a focus-time string draft so min-bounded fields (point-buy, HP,
quantities) no longer snap to the minimum on every keystroke while retyping; the
committed value is still always a clamped integer.
- Combat tracker now logs the 5e death rules when a downed PC takes damage: instant
death from massive damage (leftover >= max HP), otherwise a reminder to mark a death
save failure (two on a crit). Surfaced, not auto-applied — the player owns their
character's death-save count (consistent with the no-auto-roll design). New tested
isMassiveDamageDeath() engine helper.
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>
Both cast UIs (SpellcastingSection + MyCharacterPanel) called castSpell without a level,
so it always tried the spell's base level — a warlock (only a pact slot at the pact
level) could never cast, and upcasting was impossible. New availableSlotLevels() lists
the castable levels (regular slots ≥ spell level with a charge, plus the pact level); the
UI shows a level selector and passes the chosen level. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MonsterDetail now renders all movement modes (fly/swim/climb/burrow were dropped for
174 monsters), plus Saving Throws and Skills lines (present in data, never shown).
- Passive Perception is computed from the creature's own WIS/Perception so it can't
disagree with its stats (fixes the 7 SRD monsters with a wrong passive, e.g. Blink
Dog 10->13, Bone/Chain Devil). Also surfaces damage vulnerabilities.
- loadPf2e dedupes entries by name (Remaster + legacy duplicates), so the compendium
shows one 'Longsword'/'Goblin Warrior' instead of two or three.
- races.json: Drow ASI corrected to +2 DEX / +1 CHA (was a non-SRD +2 INT).
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>
- parseDamage skips self-inflicted backlash ('you take …'), so Wish no longer
snapshots a phantom 1d10 necrotic onto the spell.
- parseDamage tolerates a flat modifier on the dice, so Magic Missile captures
'1d4+1 force' instead of nothing.
- parseSave scores candidate saves and prefers the operative one (introduced by
'must succeed on a' / 'makes a') over incidental 'disadvantage on X saving throws'
mentions — Irresistible Dance now resolves to Wis, not Dex.
Adds 3 regression tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PF2e clumsy/enfeebled/drained/stupefied now penalise their correct, independent
statistic family (Dex/Str/Con/mental) instead of collapsing into one max'd '−N status'.
Frightened/sickened are 'all checks'. Distinct badges per family.
- Slowed is action loss only (no roll penalty); fatigued shows flat −1 AC/saves —
neither is value-scaled anymore.
- 5e Exhaustion now derives its cumulative level effects (ability-check disadvantage L1,
speed halved L2, attack + all-save disadvantage L3, speed 0 L5, dead L6).
- 5e Prone badge is range-qualified (adv. melee / disadv. ranged).
- New tickConditionsEndOfTurn: PF2e Frightened decays by 1 at end of turn (wired into
nextTurn via the combat tracker), removed at 0.
MechanicalState.statusPenalty (single number) → statusPenalties (per-family map).
Tests updated + added. Deferred: Drained HP/max-HP reduction (needs creature level
threaded into the damage flow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed accuracy defects from the audit:
- AC now scales with level + armor proficiency (10 + Dex + (level+rank) + item),
defaulting to trained; was 10 + Dex only, wrong by +3..+28.
- Weapon potency (itemBonus) applies to the attack roll ONLY, not flat damage.
- weaponAttack now reports the Multiple Attack Penalty (-5/-10, agile -4/-8) and
expands striking runes into extra weapon dice; AttacksSection surfaces MAP.
- Full-caster spell slots: 2 at the rank's unlock level, 3 thereafter (was a flat 3,
giving a level-1 caster 3 first-rank slots instead of 2).
- PF2e Refocus restores one Focus Point (recoverStep), not the whole pool.
Adds 6 regression tests. types.ts gains WeaponInput.agile/striking, WeaponResult.map,
CharacterRulesInput.acRank, RestOption.recoverStep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
DeepSeek keeps inventing its own key for the additions array — first
"creatures", then "addCreatures". Chasing named aliases is whack-a-mole, so
detect it structurally: a known synonym first, else any top-level array of
{name, …} objects that isn't the "remove" list. Any invented key now resolves
to "add".
Tests cover creatures/addCreatures/monsters/an invented key, and that a lone
"remove" list is not mistaken for additions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The real cause of the advisor falling back to deterministic: DeepSeek returns
the additions array under the key "creatures" (nudged by the prompt wording
"the creatures to add"), but the schema requires "add" — so safeParse failed.
- Spell out the exact JSON keys in the prompt ("add", not "creatures"/"monsters").
- Normalize creatures/monsters/additions aliases to "add" before validation, so
a correct suggestion isn't discarded over a key-name synonym.
- Regression test using DeepSeek's exact wild payload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 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>
- Autosave: while signed in, a debounced full backup pushes whenever durable data
changes (useCloudAutosave watches a fingerprint of the main tables; high-churn
tables excluded). No-op signed out.
- Campaign-agnostic characters: PCs are now global — the Characters roster shows
every player character regardless of active campaign, and any PC can be added to
a fight. NPCs stay per-campaign. (charactersRepo.listAllPcs / useAllPcs; difficulty
fallback stays campaign-scoped.)
- Live "bring your own character": a joining player can push one of their own local
characters; the GM's grant inserts it into the campaign and seats them.
- Character builder: a system picker (5e / PF2e, defaults to the campaign) so creation
no longer assumes 5e; PF2e skill wording ("Trained" vs "proficient"); spell step now
explains how many to pick + that they're known spells prepared later.
230 unit + 34 e2e + 2 realtime green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
- Modal: focus into the dialog ONCE on open (not on every parent re-render), and
prefer the first field over the X button — fixes focus jumping to the close
button on each keystroke in the handout composer (and every other composer).
- Campaign edit: hide the System selector entirely (it's fixed after creation).
- Combat difficulty: rate against the PCs actually in the encounter (falls back to
the roster) so adding a player updates the reading.
- Character builder: ancestry/background description is now a scrollable panel
instead of clamped to 3 lines (no longer cut off on small screens).
223 unit + key e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the top nav with a left rail + top context bar ("GM mission control"):
gilt crown wordmark, nav grouped At the Table / Reference with Lucide icons and a
gold active state (accent-glow + gilt left edge), a collapse toggle
(uiStore.railCollapsed). A single responsive rail = icon strip (68px) on phones /
when collapsed, full labels on >= sm.
- Top bar: campaign switcher, a ⌘K search pill, and every existing control preserved
(PlayerSessionBadge, Session panel toggle, HandoutControl, SessionControl host,
theme toggle as Sun/Moon, Settings) — same aria-labels/testids so e2e is intact.
- bun add lucide-react. Rail is print:hidden (sheets export clean).
223 unit + 34 e2e + 2 realtime green; nav contract (aria-label="Primary", link names) preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the design tokens in globals.css with the Living Codex system (parchment
light · candlelit dark, gilt accent), keeping every existing --app-*/utility name
so nothing breaks. Adds surface-2/panel-2/line-strong/ink-soft/faint/accent-deep/
accent-soft/accent-glow/verdigris + --app-accent-hue (one-number reskin) and
paper-grain/gilt-rule/smallcaps helpers.
- Self-host the type system via @fontsource (offline-safe, no CDN/CSP change):
Spectral (serif headings), Hanken Grotesk (UI), Spline Sans Mono (stats).
- Kept: dice/crit animations, print rule, reduced-motion, scrollbars.
223 unit + 34 e2e green; no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Server: AccountStore gains admin gating (ADMIN_USERS env), per-user quotaBytes
override, listUsers + blobBytes. /api/usage now reports total bytes (backup blob +
cloud characters) + an admin flag. Admin routes GET /api/admin/users and POST
/api/admin/quota (gated). Quotas are tracked, not yet enforced. +1 test.
- Client: Settings cloud panel shows "Your cloud storage", and for an admin a users
table with per-user usage + an editable quota (GB). compose sets ADMIN_USERS=nilsb.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/lib/cloud/campaigns.ts wraps the campaign/character API.
- Settings "Shared campaigns (cloud)" panel (signed-in only): publish a local
campaign → get an invite code; join a campaign by code; publish a local character
to a campaign (stays yours, re-publish to sync); owners "View party" to read all
published characters (read-all, per the View+suggest model).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>