The Slice-0 vertical that proves the enforcement architecture end-to-end.
- kernel: castSpell (slot consumption + concentration set/replace, upcasting,
warlock pact slots), spendResource/regainResource, rollDeathSave — pure
functions returning Partial<Character> patches. 13 unit tests.
- applyRest now ends concentration on any rest (+ test).
- character sheet SpellcastingSection: per-spell Cast button, concentration
banner with Drop, concentration markers; ResourcesSection −/+ routed through
the kernel.
- player MyCharacterPanel: castable spell list + concentration banner, so a
seated player casts and it broadcasts via the existing playerPatch flow.
- live sync: partialCharacterDiffSchema carries concentration; GM applies it;
playerCharacterSchema mirrors slots/concentration/resources (PC-only) and the
player-facing AC now respects equippedArmor. Wire round-trip test added.
Verified live: casting decrements the right slot, switching concentration logs
"Concentration on X ends", banner + Drop render. Build + 258 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lay the foundation for full rules enforcement: a new pure, testable
mechanics kernel (src/lib/mechanics/) that derives structured, enforceable
game data from the loosely-typed compendium, plus the first user-facing
payoff (accurate armor AC + spell mechanics on the sheet).
- normalizers: spell (5e + pf2e), armor, monster defenses → typed
SpellMechanics / ArmorMechanics / DamageDefenses, with memoized derivers
wrapping the existing lazy compendium loaders (no new assets). 11 unit tests.
- character schema: spell entries snapshot concentration/save/damage at
compendium-link time; new top-level `concentration` slot and structured
`equippedArmor` (additive, defaulted). Dexie v14 migration backfills.
- AC model: baseArmorClass now consults equippedArmor (heavy = flat, medium =
Dex-capped) and falls back to the old 10+Dex formula when unarmored. Threaded
through every AC call site; armor picker added to the sheet (5e).
- compendium "Add spell to character" snapshots real mechanics via the kernel.
Backward-compatible: un-enriched spells/characters behave exactly as before.
Build + 244 unit tests green; armor→AC verified live in preview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DeepSeek is a reasoning model — its chain-of-thought counts against the
completion budget. The 1024 max_tokens default let it burn the whole budget
"thinking" and get cut off (finish_reason: length) with an empty content, which
read as a parse failure. Raise DEFAULT_MAX_TOKENS to 4096 so it can finish
reasoning AND emit the JSON. Also return a clear error on empty responses.
The spiral was triggered by a contradictory prompt: the fight was already
"deadly" yet the advisor still asked the model to "add creatures to reach
Deadly." Skip the LLM when the target tier isn't actually harder than the
current one and let the deterministic path ease the over-tuned fight instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DeepSeek keeps inventing its own key for the additions array — first
"creatures", then "addCreatures". Chasing named aliases is whack-a-mole, so
detect it structurally: a known synonym first, else any top-level array of
{name, …} objects that isn't the "remove" list. Any invented key now resolves
to "add".
Tests cover creatures/addCreatures/monsters/an invented key, and that a lone
"remove" list is not mistaken for additions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "creatures" alias fix wraps the balance schema in z.preprocess, whose input
type is `unknown`. complete()'s `schema?: ZodType<T>` pinned input=output=T, so
T inferred as unknown and `res.data.add` failed under tsc -b (project build).
Loosen schema input to `ZodType<T, ZodTypeDef, any>` — T stays pinned to the
schema output, but coerce/preprocess/transform schemas (input ≠ T) are accepted.
Pin the advisor call's type param to BalanceSuggestion, matching the other
complete<...> call sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real cause of the advisor falling back to deterministic: DeepSeek returns
the additions array under the key "creatures" (nudged by the prompt wording
"the creatures to add"), but the schema requires "add" — so safeParse failed.
- Spell out the exact JSON keys in the prompt ("add", not "creatures"/"monsters").
- Normalize creatures/monsters/additions aliases to "add" before validation, so
a correct suggestion isn't discarded over a key-name synonym.
- Regression test using DeepSeek's exact wild payload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The encounter-balance schema was the only AI schema with a numeric field.
LLMs routinely emit numbers as JSON strings ("count": "2"), which z.number()
rejects, failing the whole safeParse and falling back to deterministic with a
"parse" error. The all-string NPC/session/quest schemas never hit this.
- Coerce monster counts (z.coerce.number) and widen the upper bound 8->12 so a
string or slightly over-eager count is accepted, not rejected. min(1) stays.
- Tell the model in the prompt that count is a plain integer and reasoning /
targetDifficulty are required.
- Surface the real failure message in the advisor instead of the opaque kind.
- Add a regression test that string counts coerce to numbers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace all emoji icons (~26 instances) with Lucide icons across the app
(DashboardPage, SessionSidebar, PlayerBoards, MyCharacterPanel,
PlayerViewPage, EncounterTracker, CampaignsPage, HandoutControl,
SessionControl, CharacterSheet, EncounterTipCard, ActionGuide)
- Expose real 5e race data in wizard (ASI/vision/traits instead of stub desc);
add vision field to loadRaces5e return type
- Surface subclass descriptions after picking a subclass in the wizard
- Add per-class tips, race synergy notes, and ability/skill guidance in wizard
- New ActionGuide component: collapsible "What can I do on my turn?" in player view
- New SessionPrepCard: AI + fallback session hook generator on assistant page
- New NpcGenCard: AI + fallback quick NPC generator with "Add to campaign" action
- Inline NPC detail generator (wand button) on each NPC card in NpcsPage
- Quest hook generator button in QuestsPage header
- Condition tooltips in player party view (useConditionGlossary)
- Pass campaign.system through session snapshot so player view is system-aware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Autosave: while signed in, a debounced full backup pushes whenever durable data
changes (useCloudAutosave watches a fingerprint of the main tables; high-churn
tables excluded). No-op signed out.
- Campaign-agnostic characters: PCs are now global — the Characters roster shows
every player character regardless of active campaign, and any PC can be added to
a fight. NPCs stay per-campaign. (charactersRepo.listAllPcs / useAllPcs; difficulty
fallback stays campaign-scoped.)
- Live "bring your own character": a joining player can push one of their own local
characters; the GM's grant inserts it into the campaign and seats them.
- Character builder: a system picker (5e / PF2e, defaults to the campaign) so creation
no longer assumes 5e; PF2e skill wording ("Trained" vs "proficient"); spell step now
explains how many to pick + that they're known spells prepared later.
230 unit + 34 e2e + 2 realtime green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Player view: a Quest log now syncs to players (snapshot += quests; broadcaster
passes campaign quests; PlayerBoards renders titles + objective checkboxes).
- AI (DeepSeek etc.): robust JSON extraction — strip code fences anywhere + a
brace-balanced scanner that ignores prose/braces in strings. Fixes "AI unavailable
(parse)" with OpenAI-compatible providers. +4 tests.
- Encounter advisor: now bidirectional — for an over-tuned (too-hard) fight it
suggests REMOVING/swapping monsters instead of only ever adding. +3 tests.
- Character share: works on iPad — native share sheet → clipboard → a copyable
link modal fallback (was a silently-failing clipboard write).
230 unit + 34 e2e + 2 realtime green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Modal: focus into the dialog ONCE on open (not on every parent re-render), and
prefer the first field over the X button — fixes focus jumping to the close
button on each keystroke in the handout composer (and every other composer).
- Campaign edit: hide the System selector entirely (it's fixed after creation).
- Combat difficulty: rate against the PCs actually in the encounter (falls back to
the roster) so adding a player updates the reading.
- Character builder: ancestry/background description is now a scrollable panel
instead of clamped to 3 lines (no longer cut off on small screens).
223 unit + key e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the top nav with a left rail + top context bar ("GM mission control"):
gilt crown wordmark, nav grouped At the Table / Reference with Lucide icons and a
gold active state (accent-glow + gilt left edge), a collapse toggle
(uiStore.railCollapsed). A single responsive rail = icon strip (68px) on phones /
when collapsed, full labels on >= sm.
- Top bar: campaign switcher, a ⌘K search pill, and every existing control preserved
(PlayerSessionBadge, Session panel toggle, HandoutControl, SessionControl host,
theme toggle as Sun/Moon, Settings) — same aria-labels/testids so e2e is intact.
- bun add lucide-react. Rail is print:hidden (sheets export clean).
223 unit + 34 e2e + 2 realtime green; nav contract (aria-label="Primary", link names) preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the design tokens in globals.css with the Living Codex system (parchment
light · candlelit dark, gilt accent), keeping every existing --app-*/utility name
so nothing breaks. Adds surface-2/panel-2/line-strong/ink-soft/faint/accent-deep/
accent-soft/accent-glow/verdigris + --app-accent-hue (one-number reskin) and
paper-grain/gilt-rule/smallcaps helpers.
- Self-host the type system via @fontsource (offline-safe, no CDN/CSP change):
Spectral (serif headings), Hanken Grotesk (UI), Spline Sans Mono (stats).
- Kept: dice/crit animations, print rule, reduced-motion, scrollbars.
223 unit + 34 e2e green; no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Server: AccountStore gains admin gating (ADMIN_USERS env), per-user quotaBytes
override, listUsers + blobBytes. /api/usage now reports total bytes (backup blob +
cloud characters) + an admin flag. Admin routes GET /api/admin/users and POST
/api/admin/quota (gated). Quotas are tracked, not yet enforced. +1 test.
- Client: Settings cloud panel shows "Your cloud storage", and for an admin a users
table with per-user usage + an editable quota (GB). compose sets ADMIN_USERS=nilsb.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/lib/cloud/campaigns.ts wraps the campaign/character API.
- Settings "Shared campaigns (cloud)" panel (signed-in only): publish a local
campaign → get an invite code; join a campaign by code; publish a local character
to a campaign (stays yours, re-publish to sync); owners "View party" to read all
published characters (read-all, per the View+suggest model).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
- Level-up build-route advisor (src/features/characters/sheet/LevelUpAdvisor.tsx +
useLevelUpAdvisor): ~4 system-aware routes plus a custom one; choosing a route
expands it into concrete next-level steps. AI when configured, deterministic
fallback (src/lib/assistant/levelup.ts) otherwise. Embedded in the existing
HP-only LevelUpModal, which stays primary.
- Level-up prompts/schemas added to prompts.ts (buildRoutes/buildSteps), each
leading with the system constraint + class + next level.
- Campaign Insights section on the Assistant page renders deterministic themes,
with an optional 'ask the assistant to expand' affordance when AI is on.
levelup + prompt unit tests (4 routes both systems, system vocabulary, custom
route); e2e for the deterministic route→steps flow and the insights section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/lib/assistant/prompts.ts: balanceSuggestionSchema + buildBalancePrompt
(system message leads with the grounding constraint; restricts choices to the
provided compendium candidates).
- useEncounterAdvisor hook: builds context → picks system-correct candidates →
asks the LLM (when configured) for a structured pick, validates + filters names
to the candidate set, else falls back to the deterministic buildSuggestedEncounter.
Apply adds the creatures transactionally via encountersRepo.mutate.
- EncounterTipCard in the combat tracker: leads with the detected difficulty
tendency (or the current difficulty), offers a one-click grounded suggestion
(AI or deterministic) with propose→confirm Apply.
prompts unit tests + e2e for both the deterministic apply flow and the AI path
(stubbed provider); the AI test also confirms candidate-grounding (only
shortlisted creatures survive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/lib/assistant/context.ts: buildCampaignContext() assembles a system-aware
snapshot led by an explicit systemConstraint (anti cross-system hallucination),
party, recent encounters with computed difficulty, quest/note summaries.
pickCreatureCandidates() pulls level/CR-appropriate creatures from the correct
bestiary so LLM tips are grounded in real data.
- src/lib/assistant/patterns.ts: difficultyTendency() + detectThemes() classify
whether the DM's encounters skew too easy/hard, rating each fight against its
party-level snapshot.
- Encounter schema gains optional partyLevelsSnapshot + outcome (Dexie v7, additive).
Combat tracker snapshots PC levels at start and rates difficulty against them.
17 assistant unit tests (context + patterns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bring-your-own-key assistant config in a dedicated persisted store
(src/stores/assistantStore.ts); partialize drops the key from localStorage
when 'remember' is off. Key lives in localStorage only, so it's auto-excluded
from Dexie backups.
- Provider-agnostic LLM client (src/lib/llm) over fetch, no SDK: Anthropic
Messages + OpenAI-compatible Chat Completions (covers OpenAI/OpenRouter/
Ollama/LM Studio via custom base URL). Structured JSON output parsed +
Zod-validated; timeouts; typed error kinds; key never leaked in errors.
- Settings 'Assistant (AI)' section with provider/model/baseUrl/key, remember
+ enabled toggles, and Test connection.
- CSP connect-src widened to any https + localhost for the BYO endpoint.
10 client unit tests; settings e2e asserts config persists and the key is
never present in an exported backup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Deterministic advisors (src/lib/assistant): party resources (downed/bloodied/
spent slots/rest nudges), session prep (dangling [[wiki links]], active quests,
empty journal), live combat hints (turn, downed combatants) — pure + tested
- Data-driven encounter builder: greedily assembles level/CR-appropriate monsters
to a target difficulty via the Phase-3 budget; one click builds + opens combat
- Assistant page: suggestion cards with one-click actions (goto, long rest,
create note); wired into routes, dashboard, command palette
- 8 advisor/builder unit tests + assistant e2e
Conversational LLM layer intentionally deferred (no provider/key in this env);
the deterministic advisor is the offline default per the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>