100 Commits
Author SHA1 Message Date
NilsBriggenandClaude Fable 5 036570d24f Merge deployed server fork (ttrpg.briggen.dev) with the audit-fix branch
The production site ran a divergent, more-evolved fork from /root/briggen-dev/ttrpg
that never made it back to git (3D physical dice + themes, extended dice notation,
world wiki with wikilinks/backlinks/GM-only visibility, quest hierarchies, session
replay, route-level code splitting, extra server hardening, useResyncedState). This
merges that fork (reconstructed from its true fork point 235cdec) with the local
262-finding audit-fix pass, resolving 104 conflicts across every subsystem.

Policy: the fork's architecture is the baseline (every prod feature kept); the audit's
correctness fixes are ported into it (no regressions). Hard invariants held:
- No auto-rolled dice — every roll originates from an explicit user click (NpcsPage,
  compendium, tracker, director all add combatants at initiative 0).
- One append-only Dexie history (v14–v19) that converges databases from EITHER fork
  idempotently; v19 re-runs both sides' character backfills.
- Unified client/server wire protocol (union of both message sets).

Notable reconciliations: PF2e AC/Class DC/armor/agile/striking folded into the fork's
gear model; wave-caster/psychic slot tables + Magus/Summoner never-Legendary ladder
verified against AoN; concentration split into boolean + concentratingOn; dual offline
indicators deduped; Node-25 localStorage shadowing fixed in the test setup.

Verified green: tsc (client+server), eslint, 1312 unit, 42 e2e, 5 realtime e2e,
client+server builds; 3D dice confirmed live (WebGL canvas mounts) in the merged build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 01:56:37 +02:00
NilsBriggenandClaude Fable 5 1420f4d0c5 Server-live fork as deployed to ttrpg.briggen.dev (built 2026-06-30)
Reconstructed from /root/briggen-dev/ttrpg — the tree the running prod
container was composed from. Forked from 235cdec (2026-06-08); adds 3D
dice + themes, extended dice notation, session replay, route-level code
splitting, and more.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:33:08 +02:00
NilsBriggenandClaude Fable 5 33ad1f92c2 Perfection audit: fix 262 findings across all subsystems
Full-app audit (15 finders + adversarial verification) and fix pass:
data-safety (backup/restore/cloud baseline), rules correctness for both
systems (rests, crits, slots, proficiency), combat engine (HP floor,
massive-damage overflow, idempotent condition ticks), no-auto-roll
enforcement everywhere, realtime protocol hardening (seat deny/takeover,
room end, image budgets), PWA update flow, PF2e sheet parity (Class DC,
armor picker, agile/striking), and 143 new unit/e2e tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:32:32 +02:00
NilsBriggenandClaude Sonnet 4.6 1ca8bb4482 MPMB-style ability score table: one row per ability, one column per source
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>
2026-06-10 22:31:08 +02:00
NilsBriggenandClaude Opus 4.8 ae676aa86c Fix empty-body JSON error on body-less admin requests
The shared cloud req() helper set content-type: application/json on every
request, so body-less DELETE/revoke/rotate calls tripped Fastify's
"Body cannot be empty" 400. Only send the header when there's a body, and
(defensively) make the server parse an empty application/json body as
undefined instead of erroring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:06:38 +02:00
NilsBriggen ab35b378af Merge: perfection pass + backend security hardening + admin panel 2026-06-10 20:55:58 +02:00
NilsBriggenandClaude Opus 4.8 5119fc6d1c Proper admin panel at /admin
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>
2026-06-10 20:49:59 +02:00
NilsBriggenandClaude Opus 4.8 0892b8f19c Backend security hardening for a small public server
Audit-driven; goal: one user can't exhaust the box or starve the shared Traefik stack.

Resource containment:
- compose: mem_limit 512m, memswap_limit, pids_limit 200, cpus 1.0 (blast radius = container)
- Enforce per-user cloud quota (default 30MB, admin override) on /api/save + /api/characters → 413
- Cap published-character size (2MB); cap rooms (200), images/room (40), image bytes/room (40MB)
- Cap WS image dataUrl length in the wire schema; per-IP WS connection cap (20)
- MAX_USERS registration ceiling → 503 when full

Availability + correctness:
- Async crypto.scrypt (was scryptSync) so password hashing can't freeze the event loop;
  constant-time on unknown users to avoid username probing
- trustProxy so req.ip is the real client behind Traefik — rate limits are per-IP, not global
- Tighter auth-endpoint bucket (10/min) on /register + /login; prune stale rate buckets
- O(1) token->user index; log persist() failures instead of swallowing them
- Trim /healthz info; surface quota in /api/usage + the admin dashboard storage bar
- Client surfaces 413 (quota) / 429 (rate) clearly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:37:14 +02:00
NilsBriggenandClaude Opus 4.8 8fd530df73 Perfection pass: complete level-up/build flows + dying polish
Second adversarial audit of the freshly-shipped code; fixed bugs + completeness gaps.

- abilityBuild stays in sync with totals on level-up (appendLevelIncreases)
- 5e feat picker in level-up; PF2e feat-name autocomplete; expertise sets ranks
- Higher-level creation collects PF2e skill increases + 5e expertise
- PF2e heritage: schema field, wizard picker, sheet, Pathbuilder import
- 5e Hit Dice as a tracked resource (half-level long-rest recovery via recoverStep)
- Dying flow: knockout sets HP 0, stays unconscious at 0 HP, healing wakes +Wounded,
  Heroic Recovery (spend hero points); damage-while-dying + recovery reminders (pf2e)
- Healing caps at effective max (drained/exhaustion-4) on sheet, tracker, and rest;
  massive-damage death uses effective max; persistent-damage badge
- UX: in-place valued-condition steppers, point-buy budget caps, prepared-vs-known
  spell guidance, granted skills in review, system-gated score generator

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:37:01 +02:00
NilsBriggen 4ee004e25f Merge: bug hunt + 5e/PF2e parity + character-build UX overhaul 2026-06-10 14:14:09 +02:00
NilsBriggenandClaude Opus 4.8 b3914b1dda Bug hunt + 5e/PF2e parity + character-build UX overhaul
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>
2026-06-10 14:14:01 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 375e22ad6f D0: multi-turn complete() — optional messages[] for the director engine
Add an optional `messages` array to CompleteOptions so the upcoming AI DM /
AI Player "director" can carry a multi-turn transcript. A single `turns()`
helper feeds both the Anthropic and OpenAI request builders:

- No `messages` → byte-identical to today's single `[{role:'user',...}]` body,
  so all existing single-shot callers are untouched.
- With `messages` → enforces the shared provider invariants (first turn must be
  `user`; a trailing `assistant` prefill gets a "Continue." user turn appended,
  which Opus 4.6+ otherwise 400s on).

Regression tests cover both providers, the byte-identical no-messages path, and
the alternation guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 02:37:04 +02:00
NilsBriggenandClaude Opus 4.8 4ce7a90534 Deepen feature engine: full 5e subclass depth + PF2e parity
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>
2026-06-10 02:11:26 +02:00
NilsBriggenandClaude Opus 4.8 82a2aef42f Phase 7: class feature & choice engine — every unlockable option is surfaced
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>
2026-06-10 01:51:23 +02:00
NilsBriggenandClaude Opus 4.8 ce6e2f430f Phase 4 (part 2): higher-level ASIs in the builder
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>
2026-06-10 01:36:39 +02:00
NilsBriggenandClaude Opus 4.8 f8ad8cb681 Phase 4 (part 1): backed feat & weapon pickers
- 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>
2026-06-10 01:31:27 +02:00
NilsBriggenandClaude Opus 4.8 4534865e74 Phase 3: full multiclass model + automatic spell slots (5e)
- 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>
2026-06-10 01:24:02 +02:00
NilsBriggenandClaude Opus 4.8 7775851bbe Phase 2: wire MPMB spell data in (XGtE/TCE + more sources)
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>
2026-06-10 01:10:30 +02:00
NilsBriggenandClaude Opus 4.8 11a9e87100 Phase 1: builder/UX quick wins (5e wording, no-campaign, details, ability UI)
- 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>
2026-06-10 00:56:26 +02:00
NilsBriggenandClaude Opus 4.8 f1e15d4011 5e builder: add the full set of official PHB/XGE/TCE subclasses
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>
2026-06-09 22:38:41 +02:00
NilsBriggenandClaude Opus 4.8 86cc3191ca 5e builder: add curated official subclasses (start with Rogue Mastermind)
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>
2026-06-09 22:34:49 +02:00
NilsBriggen ba26a9d41b chore: stop tracking the transient .claude/scheduled_tasks.lock 2026-06-09 22:02:34 +02:00
NilsBriggen e7a588904d docs: mark reconnect/concentration/level fixes resolved; all CRITICAL+HIGH cleared 2026-06-09 19:47:31 +02:00
NilsBriggenandClaude Opus 4.8 db829ff1a6 Fix CRITICAL player seat loss on WebSocket reconnect (stable playerId + seat grace)
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>
2026-06-09 19:46:58 +02:00
NilsBriggenandClaude Opus 4.8 80c5d2a828 Sheet: clarify that editing Level inline doesn't recompute HP/slots (use Level Up)
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>
2026-06-09 19:41:38 +02:00
NilsBriggenandClaude Opus 4.8 b0309c245b Combat: GM concentration flag on combatants so the save prompt actually fires
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>
2026-06-09 19:40:16 +02:00
NilsBriggen b491d57949 docs: mark PF2e boost-based ability generation as resolved 2026-06-09 19:08:39 +02:00
NilsBriggenandClaude Opus 4.8 9c53f74124 PF2e builder: boost-based ability generation (replaces incorrect 5e arrays)
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>
2026-06-09 19:08:02 +02:00
NilsBriggenandClaude Opus 4.8 f7e1c43612 Clear lint warnings from the audit branch (deps + extract NotFound)
- 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>
2026-06-09 17:50:27 +02:00
NilsBriggenandClaude Opus 4.8 4681119424 docs: record audit resolution status on the fix branch
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:46:42 +02:00
NilsBriggenandClaude Opus 4.8 bd78c87fb3 NumberField draft buffer; surface massive-damage / death-at-0 rules in combat
- 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>
2026-06-09 17:41:07 +02:00
NilsBriggenandClaude Opus 4.8 f2e4259c84 UX safety: confirm destructive deletes, scope dice clear, back up sessionLog, add 404
- 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>
2026-06-09 17:37:52 +02:00
NilsBriggenandClaude Opus 4.8 3e39e44a8a Add spell slot-level picker: warlocks can cast, any caster can upcast
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>
2026-06-09 17:30:01 +02:00
NilsBriggenandClaude Opus 4.8 ea60b16385 Fix monster statblock display + PF2e dedup + Drow ASI data
- 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>
2026-06-09 17:27:18 +02:00
NilsBriggenandClaude Opus 4.8 4d354571a5 Builder now applies race/background mechanics; fix skill data + pf2e template softlock
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>
2026-06-09 17:24:41 +02:00
NilsBriggenandClaude Opus 4.8 35728fab3d Fix spell data normalization: Wish backlash, Magic Missile dice, multi-save spells
- 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>
2026-06-09 17:17:47 +02:00
NilsBriggenandClaude Opus 4.8 f602c832e6 Fix condition mechanics: per-statistic PF2e penalties, 5e exhaustion, slowed, frightened decay
- 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>
2026-06-09 17:16:33 +02:00
NilsBriggenandClaude Opus 4.8 da3dbbedba Fix PF2e rules math: AC proficiency, weapon damage, MAP, striking, slots, Refocus
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>
2026-06-09 17:11:00 +02:00
NilsBriggenandClaude Opus 4.8 45d74ce5d7 Cleanup: pf2e subclasses + all native dialogs → themed Modals
- pf2e classes now show the defining 1st-level choice (instinct / doctrine /
  bloodline / mystery / racket / implement / thesis / …) in the wizard's
  subclass picker, like 5e. Curated PF2E_SUBCLASSES in pf2e/progression.ts,
  merged into the class during the wizard's pf2e enrich step.
- replaced every remaining native dialog with a themed Modal:
  - PlayerView session-password prompt → password Modal
  - Settings cloud-conflict confirm → "Use cloud / Keep this device" Modal
  - SessionControl host-password prompt → host Modal (optional password)
  - MapEditor text-label prompt → label Modal (captures the click point)
  No window.confirm / window.prompt / window.alert remain in the app.

Realtime e2e updated for the host Modal (Host → Start hosting).
Gate: 293 unit + 35 e2e + 2 realtime; tsc + app/server build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:50:10 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 f75b50adb8 pf2e character creator: brief, readable overviews matching 5e
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>
2026-06-09 10:51:28 +02:00
NilsBriggenandClaude Opus 4.8 ba5ae37111 V2 P5-backend + P7 + P8: conflict-safe sync, revocable invites, feats, multiclass, tests
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>
2026-06-09 10:34:29 +02:00
NilsBriggenandClaude Opus 4.8 dc0e8f701c V2 Phase 5+6: connectivity, presence, navigation & friction
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>
2026-06-09 08:51:03 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 8ba38d893d V2 Phase 3: combat rules automation (conditions, damage types, reminders)
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>
2026-06-09 01:23:52 +02:00
NilsBriggenandClaude Opus 4.8 2651ac03b7 V2 Phase 2: spell + resource enforcement (sheet → combat → live)
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>
2026-06-09 01:10:07 +02:00
NilsBriggenandClaude Opus 4.8 026927b5f5 V2 Phase 1: mechanics kernel + compendium→sheet integration
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>
2026-06-09 00:58:31 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 53854deb81 Find the advisor additions array by shape, not a fixed alias list
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>
2026-06-08 23:59:26 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 71bca6e70a Fix combat advisor schema mismatch — accept "creatures" alias for add
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>
2026-06-08 23:48:39 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Sonnet 4.6 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
NilsBriggenandClaude Opus 4.8 235cdecb53 Fixes batch 3: autosave, campaign-agnostic PCs, live push, builder
- 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>
2026-06-08 22:12:18 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 426824fd78 Fixes batch 1: modal focus, campaign system, combat difficulty, origin desc
- 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>
2026-06-08 21:48:31 +02:00
NilsBriggenandClaude Opus 4.8 99c7657f96 UI overhaul "Living Codex" E: per-screen layouts
Ported all 9 screens to the prototype layout (visual only; all data wiring, text,
aria-labels, and testids preserved) via parallel agents + central verification:
- Dashboard: editorial hero, party roster w/ HP meters + avatars, quests, threats, rolls.
- Campaigns: cover-art deck cards + gilt rule + footer.
- Character sheet + roster: hero header, StatCoin abilities, prof rows; grimoire cards.
- Combat: glowing initiative rows (PC gilt / foe ember), condition badges, log.
- Dice: die-pool glyphs, adv/dis segmented toggle, crit/fumble stage, history.
- Compendium: Spectral stat blocks (ember headers, ability row).
- Settings: grouped icon-tile cards. Live Session: room-code card, seat grid, player board.
- Battle Map: carded list + tooled editor chrome (canvas untouched).

Regression fixes after the ports: dice die-buttons aria-label `d4` not `Roll 1d4`
(was colliding with the primary Roll button); map "Open player view" link uses an
ExternalLink icon (test updated, ↗ glyph removed); seat-claim card gets a stable
data-testid="seat-option" (redesign moved rounded-lg→rounded-xl).

223 unit + 34 e2e + 2 realtime green; tsc + eslint + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:37:00 +02:00
NilsBriggenandClaude Opus 4.8 e4b399eaa8 UI overhaul "Living Codex" C+D: Lucide icons + left-rail shell
- 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>
2026-06-08 21:05:10 +02:00
NilsBriggenandClaude Opus 4.8 542592972a UI overhaul "Living Codex" B: primitives
- Button: primary is now a struck-gilt gradient (from-accent-soft to-accent +
  inset highlight + accent-deep edge); add a `subtle` variant.
- Input/Textarea/Select: gold focus (border-accent + 3px ring-accent-glow).
- PageHeader: optional italic eyebrow + larger Spectral title + a gilt hairline rule.
- Modal: rounded-xl + paper-grain panel.
- New display primitives (Codex.tsx): Meter, Badge, Avatar, StatCoin — token-driven,
  ready for the screen ports.

223 unit + 34 e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:57:45 +02:00
NilsBriggenandClaude Opus 4.8 9244c91c23 UI overhaul "Living Codex" A: tokens + fonts
- 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>
2026-06-08 20:53:24 +02:00
NilsBriggenandClaude Opus 4.8 ea4522f877 P15d: storage usage display + owner admin panel
- 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>
2026-06-08 17:48:27 +02:00
NilsBriggenandClaude Opus 4.8 e8371f6d3b P15c: client UI for shared campaigns + publishing characters
- 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>
2026-06-08 17:43:39 +02:00
NilsBriggenandClaude Opus 4.8 a28183326e P15b: server data model for shared campaigns + member-owned characters
- CloudStore (server/src/campaigns.ts): file-backed campaigns (owner + invite-code
  members) and characters owned by the player who published them. Only the owner
  updates a character (last-write-wins); the campaign owner gets read access to all
  and may remove. Usage bytes per user for later quota tracking. +3 tests.
- API: POST/GET /api/campaigns, POST /api/campaigns/join, PUT /api/characters,
  GET /api/campaigns/:id/characters, DELETE /api/characters/:id, GET /api/usage —
  all bearer-authed via the existing accounts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:39:49 +02:00
NilsBriggenandClaude Opus 4.8 9b5c63f23c P15a: hosting a shared session requires a signed-in GM
- The Host control now checks cloudUsername(): when the GM isn't signed in it shows
  "🔒 Sign in to host" (→ Settings) instead of "📡 Host". Players still join as guests.
- Realtime e2e: the GM marks itself signed in before hosting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:35:54 +02:00
NilsBriggenandClaude Opus 4.8 d6eda054bf P14d: guest display names for players
- Players set a display name in the session panel ("Your name"); it persists
  (sessionStore.guestName) and is sent via a new `setName` message on join + edit.
- Server stores the name per connection and uses it in the roster + chat; the roster
  entry now also carries the player's character, so the GM sees "Alice · Lia".
- Realtime e2e: a player names themselves and the GM's roster/share update to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:28:32 +02:00
NilsBriggenandClaude Opus 4.8 4e13a5bdf4 P14c: targeted private share (handout/note/image) from the roster
- GM clicks a player's 📨 in the session roster → composer (title/body/image via
  resizeToMax) → sendPrivateHandout to that player only (server-routed; non-
  recipients never receive it). Covers "share a secret map/note to the scout".
- The recipient sees a prominent "📨 Just for you" card on their player view
  (PlayerBoards renders playerSessionStore.privateHandout, image inline).
- Realtime e2e: GM shares a private note; only the targeted player sees it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:19:36 +02:00
NilsBriggenandClaude Opus 4.8 e562a270d1 P14b: session chat (table + whispers) + saved campaign recap
- Sidebar gains tabs: Chat / Rolls / (GM) Recap. Chat supports table messages and
  private whispers (GM→player via a recipient picker; player→GM via a toggle).
  Server routes table to all and whispers to the target only (+server tests).
- Rolls + chat are persisted to a per-campaign session recap (Dexie v13 sessionLog
  table + sessionLogRepo); the GM's "Recap" tab shows it (survives reloads/sessions).
- wsSync: chat send/receive, optimistic local echo, persistLog for the GM.
- Realtime e2e: GM table message reaches the player; the player's reply reaches the GM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:15:38 +02:00
NilsBriggenandClaude Opus 4.8 4cb834ad6c P14: dock the session sidebar (persistent column, not a temp drawer)
- The session panel now docks as a persistent right-hand column on desktop and
  only floats as an overlay drawer on phones (md breakpoint). Open state persists
  (uiStore.sessionDockOpen). The header button opens it (idempotent); the ✕ closes.
- Players get it auto-opened on join — they live in the panel during play.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:08:13 +02:00
NilsBriggenandClaude Opus 4.8 b2cf62f642 P14a: global session sidebar + roster + private-channel backend
- Session sidebar (header "☰ Session" toggle, mobile drawer) visible to GM AND
  players: which session you're in, who's here (GM + players), and the live roll
  feed. The GM's own public rolls now mirror into the local feed too.
- Server roster broadcast to everyone (sendRoster on join/seatGrant/disconnect/
  host-resume); includes the GM. Players now see the roster, not just the GM.
- Targeted private channel (backend, wired to UI next): privateState (GM→server→
  one player only) + privateHandout; image sent INLINE so non-recipients never get
  it. roster/privateHandout schemas + stores + wsSync handlers + senders.
- Adversarial-review fixes: GM included in roster; roster refreshes on GM
  disconnect; privateState returns a forbidden error on auth failure. +3 server tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:01:15 +02:00
NilsBriggenandClaude Opus 4.8 d94df41a95 P13: dice — crit/fumble animation + roll visibility
- Crit/fumble emphasis: RollTray (and the Dice page result) highlight a nat-20 /
  critical-success as "✦ Critical ✦" (gold pop + glow) and a nat-1 / critical-
  failure as "✦ Fumble ✦" (red shake). naturalD20() exported from notation.
- DM sees players' rolls: RollFeed now also renders on the Combat page (players'
  rolls already broadcast to the GM).
- Players see the DM's public rolls: new gmRoll message — a hosting GM's rolls
  (Dice page + rollAndShow) broadcast to the table as "GM".
- DM secret-roll toggle (rollStore.secret) on the Dice page suppresses the broadcast.
- Realtime e2e: GM public roll appears in the player's table feed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:35:48 +02:00
NilsBriggenandClaude Opus 4.8 d4ae60f199 P12: persistent live session across navigation
- Player room connection lifted out of the /play route to an app-level hook
  (usePlayerConnection in RootLayout), keyed by a persisted joinIntent — so a
  player stays connected while browsing their sheet, the compendium, anywhere,
  and a reload reconnects automatically.
- Header "● Live: CODE · Leave" badge (PlayerSessionBadge); leaving is explicit
  (leaveRoom clears intent + disconnects). /play renders the table from the store.
- Realtime e2e: player navigates away, badge persists, returns to a synced table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:24:31 +02:00
NilsBriggenandClaude Opus 4.8 5527b7aa7a P11: hide fogged tokens from players + show combat conditions
- toPlayerProjection now drops tokens whose footprint is unrevealed when fog is on,
  so a token's name/position stays hidden in the dark (tested).
- Player initiative view shows each combatant's visible conditions (playerCombatant
  + wire schema + PlayerBoards chips) — monster conditions are now visible to players.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:17:05 +02:00
NilsBriggenandClaude Opus 4.8 76efc459bb Phase 10: accounts + cloud sync + Pathbuilder import
- Lean file-backed accounts (server/src/accounts.ts): scrypt-hashed passwords,
  hashed bearer tokens, per-user backup blob; persisted under DATA_DIR. /api routes
  (register/login/logout, PUT/GET /save) with per-IP throttle + 48MB body limit.
- Client cloud lib + Settings "Cloud sync": sign up / sign in, back up this device,
  restore from cloud (whole-backup push/pull, last-write-wins). Local-first default;
  the assistant key (localStorage) is never part of the synced Dexie backup.
- Pathbuilder 2e import: the existing character import now detects a Pathbuilder
  build JSON and converts it to a PF2e character (abilities/HP/saves/skills).
- Dockerfile creates a node-owned /data; compose mounts a named ttrpg-data volume.
- (Spectator links = existing /play?room=CODE; PDF export = existing Print.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:04:28 +02:00
NilsBriggenandClaude Opus 4.8 f01e8ffe32 Phase 9: onboarding, mobile & accessibility
- First-run welcome on the Campaigns page: what-the-app-is blurb, three feature
  highlights, and one-click "Try the sample campaign" (seedSampleCampaign) plus
  "Start your first campaign".
- Map token palette starts collapsed on small screens so the map gets the width.
- (Reduced-motion is already honoured globally in styles/globals.css.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:53:33 +02:00
NilsBriggenandClaude Opus 4.8 7f1ab8f094 Phase 8: session tooling — handouts to players
- GM "📣 Handout" composer in the header (title + body + optional image) sets
  uiStore.activeHandout; a clear button hides it.
- snapshot gains an optional handout {title, body, imageId}; buildSnapshot streams
  the handout image over the existing image channel (id "handout").
- Player view (local + networked) shows the handout as a prominent card above the
  boards. resizeToMax() keeps handout images compact.
- (Live scene/map switching already works via "Show to players".)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:48:23 +02:00
NilsBriggenandClaude Opus 4.8 f923375ebb Phase 6: combat ↔ map fusion
- Map combat HUD on the editor (Round, current combatant, Prev/Next turn) that
  drives the active encounter via encountersRepo.mutate(nextTurn/previousTurn).
- The current combatant's linked token glows (MapCanvas activeTokenId).
- Token edit modal gains a "Combat HP" control (−5/−1/+1/+5) for combatant-linked
  tokens, writing damage/heal back to the tracker (applyDamage/applyHealing +
  logEvent + updateCombatant) — HP now syncs both ways between map and combat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:41:06 +02:00
NilsBriggenandClaude Opus 4.8 06615020f6 Phase 5: data-driven, new-player-friendly character builder
Rebuilt CreationWizard to consume the structured ruleset data (Phase 7):
- Class step: rich class cards (description, hit die, key ability, caster, playstyle
  tag) + subclass picker, plus "ready-made hero" quick-start templates that prefill
  class + abilities for instant play.
- Origin step: searchable ancestry/race + background pickers with descriptions (auto
  speed/ancestry-HP where known).
- Abilities: key-ability hints from the chosen class. Skills: choices from class data.
- Spells step (casters only): searchable starting-spell picker → spellcasting.spells.
- Review applies derived build + data-driven saves (esp. PF2e classes).
- Updated the e2e helper + wizard spec to the new flow (class-card data-testid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:35:15 +02:00
NilsBriggenandClaude Opus 4.8 e1ba3fe1b9 Phase 7: structured ruleset data (Open5e 5e + Foundry PF2e classes)
- Scrapers scripts/fetch_open5e.ts (classes/races/backgrounds/feats, CC-BY/OGL) and
  fetch_foundry_pf2e.ts (25 PF2e classes, OGL/ORC) → normalized JSON in src/data/srd
  + public/data/pf2e/classes.json.
- src/lib/ruleset/normalize.ts: pure, tested normalizers → unified RulesetClass
  (hit die, key abilities, saves/ranks, perception, skills, caster, subclasses, profs).
- compendium loaders (loadClasses/loadRaces5e/loadBackgrounds5e) + a "Classes" tab for
  both systems (ClassDetail) with full stats; Bestiary stays the default category.
- Data-source attribution in Settings. Feeds the Phase 5 builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:23:25 +02:00
NilsBriggenandClaude Opus 4.8 f61fabadb5 Phase 4: dynamic vision, walls & doors
- src/lib/map/vision.ts (pure, tested): blockingSegments (walls + closed doors),
  segment intersection, computeVisibleCells (raycast viewer→cell-centre, sight radius).
- BattleMap += dynamicVision + sightRadiusFeet (Dexie v12, backfilled).
- MapEditor: new "Walls" tool (draw wall polylines; click a door to open/close),
  a "Vision" toggle (auto-enables fog and reveals from the party), sight-radius field,
  and "Reveal from party". Moving a PC token accumulates revealed cells via LoS when
  Vision is on. Doors/walls render for the GM (amber=closed, green=open).
- Networked players need no new protocol: revealed cells already travel in the snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:02:49 +02:00
NilsBriggenandClaude Opus 4.8 36f0ea66b3 Phase 3: Universal VTT (.dd2vtt/.uvtt) import + export
- src/lib/vtt/uvtt.ts: pure, tested parser converting UVTT grid-unit geometry to
  image pixels (pixels_per_grid → gridSize), extracting the base64 image, walls
  (line_of_sight), doors (portals) and lights; plus toUvtt() for export.
- BattleMap gains walls/doors/lights (Dexie v11, backfilled). mapsRepo.createFromVtt.
- MapsPage "+ Add map" now accepts .dd2vtt/.uvtt/.df2vtt (text) and images, and a
  per-map "Export .uvtt" (image dims → map_size). downloadText() helper.
- Walls render faintly for the GM (CanvasView.walls / drawScene) so imported
  line-of-sight is visible; Phase 4 will turn walls into dynamic vision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:51:56 +02:00
NilsBriggenandClaude Opus 4.8 1aff63f29c Phase 2: character portraits → token icons (local + realtime)
- character.portrait + mapToken.image (data URLs, Dexie v10, additive).
- src/lib/img/resize.ts: center-crop + downscale helper (160px tokens / 256px
  portraits) keeping IndexedDB + wire payloads small.
- Portrait uploader on the character sheet; token icon uploader in the map token
  modal (falls back to the linked character's portrait). Palette PC entries show
  the portrait thumbnail.
- Tokens render the image clipped to the circle (HP ring + condition badge on top).
- Realtime: generalized the image channel to many images (map bg + token/portrait
  icons), deduped by content and resent on reconnect. Player tokens/party carry an
  imageId resolved from the session image cache; party panel shows portraits.
- Also: fix the encounter-picker dropdown text being clipped (drop fixed height).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:42:06 +02:00
NilsBriggenandClaude Opus 4.8 ca3769eb6b Phase 1: PWA auto-update, player-view map fit fix, encounter picker
- PWA registerType autoUpdate + skipWaiting/clientsClaim/cleanupOutdatedCaches so
  a fresh deploy reaches browsers without a manual hard-reload (the old 'prompt'
  served the stale cached bundle, hiding shipped fixes like the polygon outline).
- MapCanvas: always mount the outer container so the ResizeObserver binds even when
  the map image arrives late over WebSocket — fixes the networked player view
  showing the map at 100% instead of fit ("3x magnified").
- TokenPalette: encounter picker for planned AND active encounters (was active-only),
  with per-encounter "+ All" and counts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:26:00 +02:00
NilsBriggenandClaude Opus 4.8 2879059ba5 P20 fix: crisp device-resolution map rendering + live draw previews
Render the map on a single viewport-sized canvas with a camera transform at
devicePixelRatio (image, fog, drawings, overlay) and draw the grid as crisp 1px
device-space lines — instead of CSS-scaling a natural-resolution bitmap, which
blurred the grid/fog/text at any zoom. Tokens now position in screen space so
their labels stay sharp too. Add live tool feedback via a 'hover' phase: polygon
shows its outline + vertices + rubber-band + fill preview, the brush shows its
footprint, and circle/square AoE preview under the cursor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:56:28 +02:00
NilsBriggenandClaude Opus 4.8 360d9ff842 P21: player seats + two-way live play + offline handoff
Players claim a seat (GM approves) and drive their own character live —
HP/slots/resources/conditions/dice broadcast to the table — via new seat
protocol messages and per-seat ownership enforced in RoomHub. GM persists
player edits (charactersRepo.update → liveQuery → rebroadcast). Offline dev:
"Share with player" encodes a claim link (src/lib/sync/playerLink.ts) → /player
imports the character locally for full-sheet editing, bundled back as an
offlineSnapshot on seat claim for GM review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:34:00 +02:00
NilsBriggenandClaude Opus 4.8 c28c29a6c6 P20: map viewport zoom/pan/fit + token placement palette
Render maps at natural resolution inside a CSS-transform world layer (zoom,
pan, fit-to-view) instead of shrinking to maxWidth — large maps are now usable.
Pure viewport math in src/lib/map/viewport.ts (unit-tested). New TokenPalette
places party PCs / active-encounter combatants / blanks in one click with dup
detection; tokens gain optional combatantId (Dexie v9) for live encounter HP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:34:00 +02:00
NilsBriggenandClaude Opus 4.8 a8cb7d65f4 Phase 18: internet realtime collaboration (GM-authoritative) + server
- Shared Zod wire protocol (src/lib/sync/messages.ts): hosted/joined/snapshot/
  mapImage/error; player-safe projections only (enemy HP masked on the GM before
  broadcast via src/lib/combat/playerProjection.ts).
- wsSync adapter (src/lib/sync/wsSync.ts) behind the existing SyncAdapter seam:
  GM hostSession + debounced snapshot broadcast (useSessionBroadcaster), player
  joinSession into an ephemeral playerSessionStore, exponential-backoff reconnect
  with seamless room resume (GM secret). localSync remains the offline default.
- buildSnapshot (src/lib/sync/snapshot.ts) reuses the same projection as the local
  /play view, guaranteeing parity. Player view refactored into shared PlayerBoards;
  /play?room=CODE = networked read-only mode.
- SessionControl in the header: Host (optional password) → shareable room code/link.
- Server (server/): Fastify + @fastify/websocket + @fastify/static, in-memory
  GM-authoritative rooms (crypto room id/secret hashed, optional join password,
  TTL sweep, players strictly read-only), origin allowlist, per-frame size cap +
  rate limit, Zod validation. esbuild bundle (server/build.mjs), tsconfig.server.json.
- Dockerfile (multi-stage) + deploy/ttrpg.compose.yml (own project on external
  'proxy' net, Traefik labels for ttrpg.briggen.dev, non-root, no-new-privileges).
  CSP connect-src now allows same-origin wss:.

Tests: protocol round-trip, room hub (auth/password/resume/TTL/read-only), enemy
masking, Fastify+ws integration (host→join→snapshot, player push rejected), and a
two-context Playwright realtime spec (separate config) — GM hosts, player device
sees live combat with masked enemy HP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:49:31 +02:00
NilsBriggenandClaude Opus 4.8 fb459ad92c Phase 17: map & fog VTT overhaul + player-facing projection
- 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>
2026-06-08 10:31:26 +02:00
NilsBriggenandClaude Opus 4.8 ffe24495b6 Phase 16: advantage/disadvantage applies to any single die
applyRollMode (src/lib/dice/notation.ts) now rewrites the first single-die term
of any size (d4/d6/d20/...) to 2dN kh1/kl1, not just d20. Multi-die pools (2d6)
and terms with an existing keep/drop (4d6kh3) are left untouched. Dice page copy
updated; unit tests for d4/d6/d8 + skip cases; e2e toggles Advantage and rolls a
d4 → 2d4kh1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:18:57 +02:00
NilsBriggenandClaude Opus 4.8 04fa90e580 Proper guided character builder + level-up wizard
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>
2026-06-08 09:55:48 +02:00
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 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
NilsBriggenandClaude Opus 4.8 f84e429ca4 Phase 13: grounding context + deterministic pattern detection
- 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>
2026-06-08 09:08:14 +02:00