Generic (non-5e) ruleset mode + UI polish + combat switch fix #7

Merged
robert merged 25 commits from feat/generic-ruleset into main 2026-07-08 22:54:04 -04:00
Collaborator

Generic (non-5e) ruleset mode + UI polish + combat switch fix

Ruleset modes

  • 5e mode: death saves, dying/stable/dead status, auto-unconscious condition, NPCs use death saves
  • Generic mode: negative HP, status: 'down', manual markDead, no death saves, ruleset-aware revive

Tablet-friendly encounter layout

  • Combat controls always left column (narrow w-32 portrait, wide w-64 desktop), never stack
  • Short labels below lg breakpoint (Start/Pause/Next/End/Un/Re), full labels at lg+
  • Sticky controls all viewports
  • Collapsible Add Participants section (auto-collapse during active combat, expand when paused/stopped)
  • Fullscreen encounter toggle (expand section to full viewport)
  • Modals via createPortal(body) to escape sticky stacking context
  • Reduced padding/margins in portrait orientation (p-2/space-y-3 below md)

Combat & encounter management

  • Campaign switch during combat (refs + manual override tracking)
  • Pause/resume excluded from undo (lifecycle, not player action)
  • Drag reorder: campaigns, encounters, characters (downward bug fixed)
  • Paused combat can reorder across current-turn pointer
  • Encounter timestamps (startedAt/endedAt)
  • addParticipants slots by initiative instead of appending
  • Undo/redo overhaul: writers store oldValues + newValues, redo uses stored forward state

Character management

  • Character writeback on encounter end (campaign-level toggle, syncs HP/Max HP/AC back to roster)
  • Character section rollup (clickable title + count, collapse persisted)
  • Character list stat badges (HP/Current HP/AC/Init)

Fields & forms

  • AC (Armor Class) field on participants, characters, monsters
  • HP formula rolling (rollHpFormula, parse NdM±X, stored on participant doc)
  • Inline editable fields (init/hp/maxhp/ac) — transparent bg, underline on hover/focus, editing overlay
  • Persist selection + scroll position (localStorage, Android-safe event handlers)

Player display

  • Animations via PlayerParticipantCard with displayParticipants state
  • Hides inactive; shows Dying/Dead/Unconscious
  • Separate PWA manifest for /display

Dev tooling

  • Dev bulk-delete endpoint (REACT_APP_DEV_TOOLS=1 gate, dynamic key bypasses DefinePlugin)
  • Docker optimization (node 22-alpine, BuildKit cache mount, .dockerignore)
  • dev-start.sh LAN support (auto-detect IP, 0.0.0.0 bind, wake lock instructions)

Wake lock

  • Coffee/Moon toggle + fullscreen on AdminView + DisplayView
  • Persisted via localStorage
  • Android fullscreenchange re-acquire

Tests: 357 (app 106, shared 211, server 40)

## Generic (non-5e) ruleset mode + UI polish + combat switch fix ### Ruleset modes - **5e mode**: death saves, dying/stable/dead status, auto-unconscious condition, NPCs use death saves - **Generic mode**: negative HP, `status: 'down'`, manual `markDead`, no death saves, ruleset-aware revive ### Tablet-friendly encounter layout - Combat controls always left column (narrow `w-32` portrait, wide `w-64` desktop), never stack - Short labels below `lg` breakpoint (Start/Pause/Next/End/Un/Re), full labels at `lg+` - Sticky controls all viewports - Collapsible Add Participants section (auto-collapse during active combat, expand when paused/stopped) - Fullscreen encounter toggle (expand section to full viewport) - Modals via `createPortal(body)` to escape sticky stacking context - Reduced padding/margins in portrait orientation (`p-2`/`space-y-3` below `md`) ### Combat & encounter management - Campaign switch during combat (refs + manual override tracking) - Pause/resume excluded from undo (lifecycle, not player action) - Drag reorder: campaigns, encounters, characters (downward bug fixed) - Paused combat can reorder across current-turn pointer - Encounter timestamps (`startedAt`/`endedAt`) - `addParticipants` slots by initiative instead of appending - Undo/redo overhaul: writers store `oldValues` + `newValues`, redo uses stored forward state ### Character management - Character writeback on encounter end (campaign-level toggle, syncs HP/Max HP/AC back to roster) - Character section rollup (clickable title + count, collapse persisted) - Character list stat badges (HP/Current HP/AC/Init) ### Fields & forms - AC (Armor Class) field on participants, characters, monsters - HP formula rolling (`rollHpFormula`, parse `NdM±X`, stored on participant doc) - Inline editable fields (init/hp/maxhp/ac) — transparent bg, underline on hover/focus, editing overlay - Persist selection + scroll position (localStorage, Android-safe event handlers) ### Player display - Animations via `PlayerParticipantCard` with `displayParticipants` state - Hides inactive; shows Dying/Dead/Unconscious - Separate PWA manifest for `/display` ### Dev tooling - Dev bulk-delete endpoint (`REACT_APP_DEV_TOOLS=1` gate, dynamic key bypasses DefinePlugin) - Docker optimization (node 22-alpine, BuildKit cache mount, `.dockerignore`) - `dev-start.sh` LAN support (auto-detect IP, `0.0.0.0` bind, wake lock instructions) ### Wake lock - Coffee/Moon toggle + fullscreen on AdminView + DisplayView - Persisted via localStorage - Android `fullscreenchange` re-acquire ### Tests: 357 (app 106, shared 211, server 40)
keen added 3 commits 2026-07-07 14:52:19 -04:00
New campaign/encounter ruleset toggle: 5e (default, unchanged) vs generic.

Generic mode:
- No death saves (deathSave/stabilize throw)
- Negative HP allowed (no clamp at 0)
- <=0 HP = status 'down' (no pips, no unconscious auto-condition)
- Monster death = dead + inactive (same as 5e)
- markDead button: DM sets dead manually (both modes)
- revive: dead->conscious (generic), dead->stable (5e)

5e mode: zero behavior change.

shared/turn.js:
- applyHpChangeGeneric: negative HP, down status, no death-save logic
- markDead: status dead, monster auto-inactive
- reviveParticipant: ruleset-aware (generic=conscious, 5e=stable)
- deathSave/stabilize: throw in generic
- expandUndo: mark_dead case added

UI (src/App.js):
- CreateCampaignForm + CreateEncounterForm: ruleset radio toggle
- handleCreateCampaign/handleCreateEncounter: store ruleset field
- EncounterManager: fetch campaignDoc for default ruleset inheritance
- DM participant: down/dead labels, Mark Dead button (generic), Revive both
- death-save pips/buttons gated 5e-only
- DisplayView: down label, generic status derivation

Tests: 15 generic cases (turn.generic.test.js). 186 shared total.
Campaign + encounter cards: ruleset tag (5e/GEN), create date visible.
CreateEncounterForm keyed by campaignId so default ruleset syncs on campaign switch.

Campaign switch during active combat:
- encounterStartedRef (unpaused) blocks switch + toast
- encounterActiveRef (started paused-or-not) gates display-follow effect
- manualSelectRef tracks user clicks; external display change clears it
  (BUG-12 follow still works for replay/other-DM)
- Prevents revert race when EncounterManager unmounts and refs go false

UI:
- campaign card: ruleset tag bottom-right, opposite delete
- encounter card: tag inline title, date left of participants count
- EncounterManager fetches campaignDoc for default ruleset inheritance

Tests green: app 100, shared 186, server 40.
Match campaigns rollup pattern. Title button toggles collapse (chevron
left, like campaigns). Character count in header. Collapse state persisted
to localStorage key ttrpg.charactersCollapsed.
keen added 1 commit 2026-07-07 14:59:02 -04:00
keen added 4 commits 2026-07-07 15:41:14 -04:00
Encounter cards draggable (ChevronsUpDown handle). Drop reorders, batch
updates order field on affected encounter docs. Sort by order (fallback
createdAt). New encounter gets order = max+1.
Campaign cards draggable (ChevronsUpDown handle in header). Drop reorders,
batch updates order field on affected campaign docs. Sort by order (fallback
createdAt). New campaign gets order = max+1.
fetchDetails (db active path) sorted by createdAt, ignored order field.
Drag persisted but UI didn't re-sort. Now both paths sort by order
(fallback createdAt).
Character list items draggable (ChevronsUpDown handle). Drop reorders
players array on campaign doc (single updateDoc). Array order = display
order.
keen added 1 commit 2026-07-07 16:00:11 -04:00
UNTESTED work in progress. Do not assume correct.

Scope: SERVER BACKEND ONLY (SQLite/Express, REACT_APP_STORAGE=server).
Does NOT work with Firebase SDK mode — no HTTP backend there, different
transport/auth. Skill + doc explicitly call this out.

- .agents/skills/ttrpg-encounter-builder/: harness-agnostic skill
  (pi/claude/codex via .agents/skills + symlinks). SKILL.md + helper
  script that batch-writes encounters via REST, rolls initiative, verifies.
- docs/ENCOUNTER_BUILDER.md: add Path normalization section, Build flow
  (API/scripts) section with REST endpoint table, object templates,
  recipe. Server-mode-only caveat noted.

Helper script syntax-checked + campaign lookup verified against running
instance, but full seed flow not regression-tested against test suite.
keen added 1 commit 2026-07-07 16:07:37 -04:00
addParticipants (bulk add all characters) appended to list end, ignoring
initiative. Pre-combat list showed random order until start encounter.
Now slots each participant by initiative desc, preserves existing order +
drag ties. Matches addParticipant single-add semantics.

Tests: 5 bulk-add slot cases (turn.bulkadd.test.js).
keen added 1 commit 2026-07-07 16:41:55 -04:00
startEncounter sets startedAt (clears endedAt on restart). endEncounter sets
endedAt. snapshotOf includes both for undo/redo fidelity. expandUndo split
start/end cases to restore old timestamps.

Display: encounter card shows Started/Ended independently (endedAt shows
even without startedAt). Undo sets null (can't delete via merge), test snap
strips only startedAt/endedAt null for compare.

Reviewed by pi gpt-5.5: stale endedAt on restart fixed, snap null-strip
narrowed. Medium-low risk.
keen added 1 commit 2026-07-07 16:44:46 -04:00
Android Chrome only reads manifest at install time from current page. Root
manifest has start_url '.' (root). Installing from /display launched root.

Fix: separate display-manifest.json (start_url /display, scope /, landscape).
App.js swaps link[rel=manifest] href when on /display path. Install from
/display now launches /display standalone.
Owner

I can't wait to test this. Going to see Backrooms tonight so I probably won't get to play around with it until tomorrow. Thanks again for all the additions. Great stuff!!

I can't wait to test this. Going to see Backrooms tonight so I probably won't get to play around with it until tomorrow. Thanks again for all the additions. Great stuff!!
keen added 2 commits 2026-07-08 11:47:21 -04:00
hpFormula field persisted on participant doc (makeParticipant + builder).
Add handler stores formula. Edit modal loads participant.hpFormula, reroll
button sets maxHp field (no save until save). Edit submit persists formula.
Formula only for monster/npc types.
Gate was process.env.NODE_ENV === 'development' — unsafe default. react-scripts
inlines NODE_ENV=development when unset, so prod deploys forgetting the env var
exposed the delete-all button. Switched to explicit opt-in REACT_APP_DEV_TOOLS=1.

process.env.REACT_APP_DEV_TOOLS as static literal gets inlined by DefinePlugin at
webpack build time — runtime mutations in tests had no effect, and dev-start
without the env produced bundles with the branch dead-stripped. Extracted gate
to src/config/devTools.js using dynamic key access
(process.env['REACT_APP_' + 'DEV_TOOLS']) so DefinePlugin cannot inline it; the
value is read at runtime. dev-start.sh now exports REACT_APP_DEV_TOOLS=1.

Button had also drifted outside the campaigns collapse block to the page bottom;
moved it back inside the campaigns section after the grid.

Tests cover both paths: gate logic (unset/0/arbitrary/1) in BulkDelete.gate.test.js,
prod safety render (button absent when unset) in BulkDelete.render-hidden.test.js,
dev feature render (button present when DEV_TOOLS=1) in BulkDelete.render-shown.test.js.
keen added 2 commits 2026-07-08 13:21:18 -04:00
AC (Armor Class) optional field across all participant entry points:
- shared: ac field on makeParticipant + buildMonsterParticipant +
  buildCharacterParticipant, defaults null
- CharacterManager: defaultAc state + add form field + inline edit field +
  display in character list
- Monster add form: AC field
- EditParticipantModal: AC field next to Initiative
- ParticipantManager (DM list): AC badge on name row (sky-blue, stylized,
  large value, small label) for at-a-glance reading
- Player display: no AC (DM only)

Layout polish:
- Add participants form: 12-col grid, 5 fields single row (Init Mod, Initiative,
  AC, Max HP, HP Formula), shrunk from oversized fields
- Character add form: 12-col grid, name grows (col-span-6), Init Mod/AC/HP
  small right-aligned, order matches add participants
- Character inline edit: labels added (Name/HP/Init Mod/AC), name flex-grows
- HP Formula: label trimmed (example moved to placeholder 'e.g. 2d6+9'),
  Reroll button in edit modal
- ParticipantManager init input shrunk (w-8, centered)

Tests: 6 new AC builder tests (turn.ac.test.js). Existing test labels updated
for renamed fields.
Wake lock (Prevent Sleep) toggles now persist across reloads via
localStorage in both AdminView and DisplayView. Buttons repositioned
inline in AdminView campaigns header bar (was floating overlay causing
overlap on tablets). DisplayView buttons persist localStorage too.

Wake lock acquire failure now shows toast with fix hint (HTTPS or
Chrome flag). Fullscreenchange listener re-acquires wake lock (Android
discards on screen off).

dev-start.sh: auto-detects LAN IP (en0/en1), frontend binds 0.0.0.0,
backend URL inlined as LAN IP so phones reach backend. DANGEROUSLY_DISABLE_HOST_CHECK
for LAN access. Outputs LAN URL + wake lock flag instructions.

Docs: README 'Prevent Sleep (Wake Lock)' section covering secure context
requirement, Android Chrome flag workaround for LAN testing, iOS Safari
standalone PWA bug. DEVELOPMENT.md LAN access + wake lock note.
keen added 1 commit 2026-07-08 14:06:18 -04:00
Participant card fields now editable in place (D-style): transparent bg,
underline on hover/focus, no spinners. Covers Initiative, Current HP, Max HP,
and AC badge value.

Click any value to edit. Blur/Enter saves. Escape cancels. Status recomputes
on HP change (conscious/dying/dead/down per ruleset).

Editing overlay: when a field is focused, amber ring highlights the card and
a centered pointer-events-none label shows '✎ Editing {field}'. Label floats
over card middle without blocking input — taps pass through to field.

Handlers: handleInlineCurrentHp (recomputes status), handleInlineMaxHp,
handleInlineAc. Keys prefixed to avoid React key collisions when values match.

Tests: selectors updated to use element id (monsterMaxHp) since inline
aria-labels now match form label queries.
Author
Collaborator

Inline editable fields (init/hp/maxhp/ac)

Participant card fields editable in place — no modal needed for quick HP updates.

• Initiative, Current HP, Max HP, AC badge value all editable
• D-style: transparent bg, underline on hover/focus, no spinners
• Click value → type → blur/Enter saves. Escape cancels
• HP change recomputes status (conscious/dying/dead/down per ruleset)
• Editing overlay: amber ring + centered "✎ Editing {field}" label
(pointer-events-none, input stays usable)

Speeds up updating 4-6 players' HP mid-combat.

352 tests green.

## Inline editable fields (init/hp/maxhp/ac) Participant card fields editable in place — no modal needed for quick HP updates. • Initiative, Current HP, Max HP, AC badge value all editable • D-style: transparent bg, underline on hover/focus, no spinners • Click value → type → blur/Enter saves. Escape cancels • HP change recomputes status (conscious/dying/dead/down per ruleset) • Editing overlay: amber ring + centered "✎ Editing {field}" label (pointer-events-none, input stays usable) Speeds up updating 4-6 players' HP mid-combat. 352 tests green.
keen added 1 commit 2026-07-08 14:13:05 -04:00
UI reset to top on every reload/code update. Now:
- selectedCampaignId persisted via localStorage
- selectedEncounterId persisted via localStorage
- scrollY saved (beforeunload + pagehide + visibilitychange + 2s interval
  for Android Chrome reliability)
- scroll restored after campaigns data loads (300ms delay for mobile render)
keen added 1 commit 2026-07-08 16:33:48 -04:00
Campaign-level toggle (CharacterManager, live flip). When ON, ending an
encounter writes each character participant's current HP, Max HP, and AC back
to the campaign roster so they carry into the next encounter.

- shared: buildCharacterParticipant uses defaultCurrentHp (fallback maxHp)
- endEncounter: syncs maxHp/ac/currentHp to campaign players if syncCharacters,
  snapshots old values into undo payload for restore. Works retroactive
  (ctx.campaignId fallback for encounters lacking campaignId field).
- New encounters store campaignId field.
- Undo end-encounter restores character values client-side.
- CharacterManager: styled sync toggle (checkbox badge), stat badges in list
  (HP/Current HP/AC/Init), edit box supports Current HP field.

Tests: 5 writeback cases (off/on/skip monsters/undo snapshot/missing char).
Author
Collaborator

Character writeback on encounter end

Campaign toggle (Character Manager section, live flip). When ON, ending encounter writes each character's current HP, Max HP, AC back to campaign roster — carries into next encounter.

  • Toggle persists on campaign doc
  • Undo end-encounter reverses writeback (restores old char values)
  • Works retroactive (old encounters without campaignId field)
  • Character list: stat badges (HP/Current HP/AC/Init), edit box supports all fields

357 tests green.

## Character writeback on encounter end Campaign toggle (Character Manager section, live flip). When ON, ending encounter writes each character's current HP, Max HP, AC back to campaign roster — carries into next encounter. - Toggle persists on campaign doc - Undo end-encounter reverses writeback (restores old char values) - Works retroactive (old encounters without campaignId field) - Character list: stat badges (HP/Current HP/AC/Init), edit box supports all fields 357 tests green.
Owner

Campaign toggle (Character Manager section, live flip). When ON, ending encounter writes each character's current HP, Max HP, AC back to campaign roster — carries into next encounter.

Bloody brilliant!

> Campaign toggle (Character Manager section, live flip). When ON, ending encounter writes each character's current HP, Max HP, AC back to campaign roster — carries into next encounter. Bloody brilliant!
keen added 1 commit 2026-07-08 19:29:16 -04:00
- Combat controls always left column (narrow portrait, wide desktop)
- Short labels below lg breakpoint, full labels at lg+
- Sticky controls all viewports
- Collapsible Add Participants section (auto-collapse during combat)
- Fullscreen encounter toggle
- Modals via createPortal to escape stacking context
- Reduced padding/margins in portrait orientation
keen added 1 commit 2026-07-08 19:39:57 -04:00
- Revive/Mark Dead buttons moved to init/hp/maxhp row (right justified)
- Separate from damage/heal controls
- Stat inputs bumped text-sm -> text-base, widths increased
keen added 1 commit 2026-07-08 19:41:56 -04:00
- Pulsing red ⚔ IN PROGRESS badge on encounter card title
- Red ring around card when combat started and not ended
keen added 2 commits 2026-07-08 20:00:35 -04:00
- Start encounter only claims display if slot empty or already ours
  (prevents stealing display from another live encounter)
- End encounter only clears display if THIS encounter is the one showing
  (prevents killing display for a different live encounter)
- Use unwrapped activeDisplayData (not snapshot wrapper activeDisplayInfo)
- Tests: 4 display guard cases (claim empty, no-steal busy, clear own,
  no-clear other)
- selectedEncounter key: ttrpg.selectedEncounter.{campaignId}
- scrollY key: ttrpg.scrollY.{campaignId}
- Restore scoped encounter selection on campaign switch
- Global keys unchanged (selectedCampaign, wakeLock, collapses)
- Prevents same-browser tabs on different campaigns from fighting
- Tests: 3 scoping cases (scoped encounter, scoped scroll, global intact)
keen added 1 commit 2026-07-08 21:36:59 -04:00
Temp HP:
- setTempHp() in shared/turn.js (replaces, no stacking)
- Damage absorbs temp HP first (both 5e + generic rulesets)
- Inline temp HP input on participant card (cyan when active)
- Temp HP field in edit modal
- makeParticipant tempHp field (default 0)
- 8 shared tests

Character isNpc:
- isNpc field on character roster model
- buildCharacterParticipant: type 'npc' when isNpc
- Add form checkbox + edit form checkbox
- NPC badge on character list row
- Writeback preserves isNpc

Initiative box:
- Amber-bordered box around initiative input
- Gold text (amber-300), disabled stays gold
- Stone-950 bg matches page
- Bumped maxHp (stone-200) + tempHp (stone-300/cyan) visibility
robert merged commit 5c062bf944 into main 2026-07-08 22:54:04 -04:00
Sign in to join this conversation.