- 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>
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>
- 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>
- 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>
- 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>
- 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>
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>