Starting an encounter meant searching for each party member by name, one
at a time. The management view now carries an "Add party to encounter"
button that puts all of them on the board in one click and closes, so the
initiative list is what you see next.
Members already in the encounter are skipped. Matching is on
playerCharacterId, not on name — otherwise a second click would hand you
"Thorin 2" via the auto-numbering, and renaming a combatant to match a
mini (the reason the rename feature exists) would break the check. Rows
for those members show a muted swords icon, and the button disables once
nothing is left to add, so the greyed-out state has a visible cause.
The whole batch pushes a single undo entry rather than one per combatant:
undoing a four-person party should not take four keystrokes. That is why
the reducer loops internally instead of the UI dispatching N times, and
why addOneFromPlayerCharacter is now split out of the single-add handler
for both paths to share.
Spec 005 gains story PC-8, FR-020..FR-023, SC-010/SC-011 and the edge
cases around partial parties, renamed combatants and orphaned ones. The
existing "multiple copies of the same PC are allowed" edge case is marked
as still true for individual adds — the party button is the deliberate
exception. Key Entities now documents the combatant/PC link the dedup
relies on, which had never been written down.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A D&D stat block opened for a combatant now offers a Min/Avg/Max toggle
below the Hit Points line, deriving both ends from the Hit Dice formula
(3d6 + 6 -> 9 / 16 / 24). The switch is applied as a delta, so manual HP
edits and damage already taken survive it. Unlike the PF2e weak/elite
adjustment this is a convenience tool, not a rules mechanic: nothing but
HP changes and the combatant is not renamed. The toggle is hidden when
the HP field carries prose instead of a dice pool.
Also extracts a SegmentedControl primitive. Game system, theme and the
PF2e weak/elite toggle were three hand-rolled copies of the same markup,
which is why the D&D toggle first came out chunkier and in the wrong
accent color. All four now share one definition, and segments expose
aria-pressed instead of signalling the active state by color alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A combatant at 0 HP renders its row sections with opacity-50, and the HP
popover lived inside the HP section. CSS opacity applies to the whole
subtree, so position: fixed did not escape it — the popover you need to
heal a downed creature came up half-transparent.
Render it through createPortal into document.body, matching what
ConditionPicker and DetailPopover already do. Positioning now takes an
anchorRef instead of reading parentElement, since the portal's parent is
the body, and the z-index moves to z-50 alongside the other portaled
popovers.
The standalone popover test has to mount the anchor before the popover:
React attaches a parent's ref after its children's layout effects run, so
a same-mount anchorRef is still null when the popover measures itself.
That matches the app, where the popover only opens on click.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The native <dialog> wrapper rendered its children unconditionally
and only called dialog.close() on the underlying element when
open went false. The React subtree stayed mounted, so component
state (e.g. a ConfirmButton mid-confirm with a red checkmark
showing) survived a close/reopen cycle and reappeared the next
time the user opened the same dialog.
Gate children on open so the subtree unmounts on close. Next open
gets a fresh tree with default state.
Implements PF2e encounter difficulty alongside the existing D&D system.
PF2e uses creature level vs party level to derive XP, compares against
5-tier budgets (Trivial/Low/Moderate/Severe/Extreme), and adjusts
thresholds for party size. The indicator shows 4 bars in PF2e mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Render persistent damage tags before the "+" button, not after
- Use insertion order for conditions on the row instead of definition order
- Differentiate Undetected condition (EyeClosed/slate) from Invisible (Ghost/violet)
- Use purple for void persistent damage to distinguish from violet conditions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expands persistent damage from 7 to 12 types to cover all PF2e damage
types that have verified persistent damage sources in published content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Persistent damage displayed as compact tags with damage type icon and
formula (e.g., Flame + "2d6"). Supports fire, bleed, acid, cold,
electricity, poison, and mental types. One instance per type, added via
sub-picker in the condition picker. PF2e only, persists across reload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Weak/Normal/Elite toggle in PF2e stat block header applies standard
adjustments (level, AC, HP, saves, Perception, attacks, damage) to
individual combatants. Adjusted stats are highlighted blue (elite) or
red (weak). Persisted via creatureAdjustment field on Combatant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show inline on-hit effects on attack lines (e.g., "plus Grab"), frequency
limits on abilities (e.g., "(1/day)"), and perception details text alongside
senses. Strip redundant frequency lines from Foundry descriptions.
Also add resilient PF2e source fetching: batched requests with retry,
graceful handling of ad-blocker-blocked creature files (partial success
with toast warning and re-fetch prompt for missing creatures).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show inline on-hit effects on attack lines (e.g., "plus Grab"), frequency
limits on abilities (e.g., "(1/day)"), and perception details text alongside
senses. Strip redundant frequency lines from Foundry descriptions.
Also add resilient PF2e source fetching: batched requests with retry,
graceful handling of ad-blocker-blocked creature files (partial success
with toast warning and re-fetch prompt for missing creatures).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared DetailPopover shell from spell popovers. Normalize
weapon/consumable/equipment/armor items from Foundry data into
mundane (Items line) and detailed (Equipment section with clickable
popovers). Scrolls/wands show embedded spell info. Bump IDB cache v7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display Recall Knowledge line below trait tags showing DC (from level
via standard DC-by-level table, adjusted for rarity) and associated
skill derived from creature type trait. Omitted for D&D creatures and
creatures with no recognized type trait.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking a spell name in a PF2e creature's stat block now opens a
popover (desktop) or bottom sheet (mobile) showing full spell details:
description, traits, rank, range, target, area, duration, defense,
action cost icons, and heightening rules. All data is sourced from
the embedded Foundry VTT spell items already in the bestiary cache.
- Add SpellReference type replacing bare string spell arrays
- Extract full spell data in pf2e-bestiary-adapter (description,
traits, traditions, range, target, area, duration, defense,
action cost, heightening, overlays)
- Strip inline heightening text from descriptions to avoid duplication
- Bold save outcome labels (Critical Success/Failure) in descriptions
- Bump DB_VERSION to 6 for cache invalidation
- Add useSwipeToDismissDown hook for mobile bottom sheet
- Portal popover to document.body to escape transformed ancestors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
--deny warnings was a no-op (not a valid category); the correct flag
is --deny-warnings. Fixed all 8 pre-existing warnings and removed
every biome-ignore from source and test files. Simplified the check
script to zero-tolerance: any biome-ignore now fails the build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align cutout edges to 45° angles parallel to outer diamond shape.
Multi-action icons use outlined diamonds with matched border width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PF2e uses action-based spell sustaining, not damage-triggered
concentration checks. The Brain icon, purple border accent, and
damage pulse animation are now hidden when PF2e is active, and
the freed gutter column is reclaimed for row content. Concentration
state is preserved so switching back to D&D restores it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cap dying (4), doomed (3), wounded (3), and slowed (3) at their
rule-defined maximums. The domain clamps values in setConditionValue
and the condition picker disables the [+] button at the cap.
Closes#31
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the stagnant Pf2eTools bestiary with Foundry VTT PF2e system
data (github.com/foundryvtt/pf2e, v13-dev branch). This gives us 4,355
remaster-era creatures across 49 sources including Monster Core 1+2 and
all adventure paths.
Changes:
- Rewrite index generation script to walk Foundry pack directories
- Rewrite PF2e normalization adapter for Foundry JSON shape (system.*
fields, items[] for attacks/abilities/spells)
- Add stripFoundryTags utility for Foundry HTML + enrichment syntax
- Implement multi-file source fetching (one request per creature file)
- Add spellcasting section to PF2e stat block (ranked spells + cantrips)
- Add saveConditional and hpDetails to PF2e domain type and stat block
- Add size and rarity to PF2e trait tags
- Filter redundant glossary abilities (healing when in hp.details,
spell mechanic reminders, allSaves duplicates)
- Add PF2e stat block component tests (22 tests)
- Bump IndexedDB cache version to 5 for clean migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace unicode action cost chars with custom SVG icons (diamond
with chevron for actions, outlined diamond for free, curved arrow
for reaction) rendered inline via ActivityCost on TraitBlock
- Add activity icons to attacks (all Strikes default to single action)
- Add trigger/effect rendering for reaction abilities (bold labels)
- Fix nested tag stripping ({@b ...{@spell ...}...}) by looping
- Move icon after ability name to match AoN format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stat block traits containing 5etools list (e.g. Confusing Burble d4
effects) or table entries were silently dropped. The adapter now
produces structured TraitSegment[] instead of flat text, preserving
lists and tables as first-class data. The stat block component renders
labeled list items inline (bold label + flowing text) matching the
5etools layout. Also fixes support for the singular "entry" field on
list items and bumps the bestiary cache version to force re-normalize.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support the 2014 DMG encounter difficulty as an alternative to the 5.5e
system behind the existing Rules Edition toggle. The 2014 system uses
Easy/Medium/Hard/Deadly thresholds, an encounter multiplier based on
monster count, and party size adjustment (×0.5–×5 range).
- Extract RulesEdition to its own domain module
- Refactor DifficultyTier to abstract numeric values (0–3)
- Restructure DifficultyResult with thresholds array
- Add 2014 XP thresholds table and encounter multiplier logic
- Wire edition from context into difficulty hooks
- Edition-aware labels in indicator and breakdown panel
- Show multiplier, adjusted XP, and party size note for 2014
- Rename settings label from "Conditions" to "Rules Edition"
- Update spec 008 with issue #23 requirements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Combatants can now be assigned to party or enemy side via a toggle
in the difficulty breakdown panel. Party-side NPCs subtract their XP
from the encounter total, letting allied NPCs reduce difficulty.
PCs default to party, non-PCs to enemy — users who don't use sides
see no change. Side persists across reload and export/import.
Closes#22
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use a three-column grid (1fr / auto / 1fr) so the active combatant
name stays centered while round badge and difficulty indicator are
anchored in the left and right zones. Prevents layout jumps when
the name changes between turns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On touch devices, the Brain icon was fully hidden (opacity-0) unlike
the edit and condition buttons. Add pointer-coarse:opacity-50 so it
appears as a discoverable grey icon, matching the other action buttons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement issue #21: custom combatants can now have a challenge rating
assigned via a new breakdown panel, opened by tapping the difficulty
indicator. Bestiary-linked combatants show read-only CR with source name;
custom combatants get a CR picker with all standard 5e values. CR persists
across reloads and round-trips through JSON export/import.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace direct adapter/persistence imports with context-based injection
(AdapterContext + useAdapters) so tests use in-memory implementations
instead of vi.mock. Migrate component tests from context mocking to
AllProviders with real hooks. Extract export/import logic from ActionBar
into useEncounterExportImport hook. Add bestiary-cache and
bestiary-index-adapter test suites. Raise adapter coverage thresholds
(68→80 lines, 56→62 branches).
77 test files, 891 tests, all passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds void to floating promise in bestiary-cache.ts, extracts shared
polyfillDialog() helper to eliminate unbound-method warnings in 3 test
files. Adds --deny warnings to oxlint so future warnings fail the
build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dialog: open/close lifecycle, cancel event handling, DialogHeader.
Tooltip: show on pointer enter, hide on pointer leave. Raises
components/ui coverage threshold to enforce testing of future
primitives.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests browse mode toggle, export/import dialog opening, overflow menu
callbacks (manage players, settings), and custom stat field submission.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds tests for DifficultyIndicator, Toast, RollModeMenu, OverflowMenu,
useTheme, and useRulesEdition. Covers rendering, user interactions,
auto-dismiss timers, external store sync, and localStorage persistence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces 18 useCallback wrappers with a typed action union and
encounterReducer. Undo/redo wrapping is now systematic per-case in
the reducer instead of ad-hoc per operation. Complex cases (undo/redo,
bestiary add, player character add) are extracted into helper functions.
The stat block auto-show on bestiary add now uses lastCreatureId from
reducer state instead of the synchronous return value, with a useEffect
in use-action-bar-state to react to changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Live 3-bar difficulty indicator in the top bar showing encounter
difficulty (Trivial/Low/Moderate/High) based on the 2024 5.5e XP
budget system. Automatically derived from PC levels and bestiary
creature CRs.
- Add optional level field (1-20) to PlayerCharacter
- Add CR-to-XP and XP Budget per Character lookup tables in domain
- Add calculateEncounterDifficulty pure function
- Add DifficultyIndicator component with color-coded bars and tooltip
- Add useDifficulty hook composing encounter, PC, and bestiary contexts
- Indicator hidden when no PCs with levels or no bestiary-linked monsters
- Level field in PC create/edit forms, persisted in storage
Closes#18
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add optional filename field to export dialog with automatic .json
extension handling. Extract resolveFilename() for testability. Add
tests for includeHistory flag, bundleToJson, and filename resolution.
Add export format compatibility note to CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add export dialog with download/clipboard options and optional
undo/redo history inclusion (default off). Extract shared Dialog
component to ui/dialog.tsx, consolidating open/close lifecycle,
backdrop click, and escape key handling from all 6 dialog components.
Update spec to reflect export method dialog and optional history.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace direct file picker trigger with a modal offering two import
methods: file upload and paste JSON content. Uses a textarea instead
of navigator.clipboard.readText() to avoid browser permission prompts.
Also centers both import dialogs and updates spec for clipboard import.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Export and import encounter, undo/redo history, and player characters
as a downloadable .json file. Export/import actions are in the action
bar overflow menu. Import validates using existing rehydration functions
and shows a confirmation dialog when replacing a non-empty encounter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract addOneFromBestiary (no undo) and build addMultipleFromBestiary
on top so confirming N creatures from the bestiary panel creates one
undo entry that restores the entire batch, not N individual entries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Initiative rolls (single and bulk) called makeStore() directly from
useInitiativeRolls, bypassing the withUndo wrapper. Expose withUndo
from the encounter context and wrap both roll paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Memento-based undo/redo with full encounter snapshots. Undo stack
capped at 50 entries, persisted to localStorage. Triggered via
buttons in the top bar (inboard of turn navigation) and keyboard
shortcuts (Ctrl+Z / Ctrl+Shift+Z, Cmd on Mac, case-insensitive key
matching). Clear encounter resets both stacks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract useActionBarState hook with all search/queue/mode state and
handlers. Extract RollAllButton (context-consuming, zero props),
BrowseSuggestions, CustomStatFields, and refactor AddModeSuggestions
to use grouped SuggestionActions interface (11 props → 6).
ActionBar is now a ~120-line layout shell.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These D&D 2024 weapon mastery conditions are edition-gated: they only
appear in the condition picker when 5.5e rules are selected. Applied
conditions still render correctly regardless of edition setting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Edit and add-condition buttons now take no space when not hovered,
eliminating the gap between name and condition icons. They slide in
smoothly on hover with a 150ms transition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Init/AC/MaxHP inputs are hidden on phones — users set these values
directly in the combatant row after adding. Fixes uneven spacing
between action bar elements by using consistent gap-3.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On desktop the panel has room alongside the combatant list, but on
mobile it covers the screen and disrupts the add-combatant flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Input base font 16px on mobile to prevent iOS Safari auto-zoom
- Safe area insets for notched phones (top/bottom bars)
- viewport-fit=cover to enable safe area env() values
- Action bar flex-wrap for custom stat field overflow
- Slightly increased row padding on mobile (py-3 sm:py-2)
- Removed redundant font-size classes from Input usages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>