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>
Knip 5 is effectively frozen: 5.88.1 (2026-03-19) was its last release,
with only three patches after 6.0.0 shipped and none since.
The reason for the earlier downgrade (968cc72) was misdiagnosed. The 6 GiB
allocation is not the Rust-side arena tracked in oxc-project/oxc#20513, but
a JS-side `new ArrayBuffer(BLOCK_SIZE + BLOCK_ALIGN)` in oxc-parser's
raw-transfer path, allocated by V8. Linux refuses it under heuristic
overcommit on the CI runner (3.7 GB RAM, no swap). The upstream allocator
work that has landed does not touch this path, so waiting on it would not
have helped.
Set KNIP_DISABLE_RAW_TRANSFER=1 in the `knip` script instead, and route
lefthook and `check` through the script rather than the bare binary — the
default path would otherwise crash with an opaque RangeError. Raw transfer
is worth ~5% on a repo this size (3.1s vs 3.15s); the upstream 2-4x figure
applies to large codebases. Knip 6 is still ~20% faster than 5 overall.
No change in unused-code detection. Verified under overcommit_memory=0 on
Linux as well as locally.
Closes#11
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>
.jscpd.json set pattern to an array, but jscpd's --pattern takes a single
glob string. An array matches nothing, so the gate had reported success
without reading a file since 2793a66 introduced it — its 0.074ms
"detection time" and absent stats table were the tell. Bisecting the
config confirmed that key alone reduces the run to zero files.
The ignore entries were also bare names rather than globs, so once the
pattern worked it swept apps/web/dist and .pnpm-store — 948 files.
Excludes __tests__, matching what .jsinspectrc already does. Duplication
between test cases is usually deliberate: parallel arrange/act/assert
blocks read better than shared setup, and the factories under
apps/web/src/__tests__/factories cover the deduplication worth having.
Real duplication is 1.72% across 209 source files, so the threshold drops
from 5% to 3%. jscpd measures a ratio rather than blocking each new clone,
and a 5% budget left roughly 3x headroom before it would ever fire.
Gives jscpd explicit paths behind `pnpm jscpd`, so lefthook and the check
script share one invocation, as jsinspect already does.
Also guards the pattern key in check-gates.mjs, since an array there fails
silently rather than erroring.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vitest.config.ts set passWithNoTests: true, added in the 7dd4abb
scaffolding commit when the repo genuinely had no tests yet. Once tests
existed it became the same hazard as the oxlint bugs fixed in 91f46ff:
if the include globs ever stopped matching, the suite would exit 0
having run nothing.
Vitest already defaults to failing in that case, so removing the line is
the whole fix — a broken glob now reports "No test files found, exiting
with code 1". Verified by pointing the globs at a nonexistent directory.
Extends the gate check to flag the option coming back, and renames it
from check-lint-gate to check-gates now that it covers more than oxlint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`scripts` sat in oxlint's ignorePatterns since c94c30e, grouped with
dist, coverage and .pnpm-store. Those are build artifacts; scripts holds
the quality gates themselves, which went unlinted as a result.
Removing it raises coverage from 265 to 273 files and surfaced four
findings:
prefer-regexp-exec in three files — swapped String#match for RegExp#exec.
All three patterns are non-global, where the two methods return the same
result, so behaviour is unchanged.
require-array-sort-compare in generate-bestiary-index.mjs — the rule
skips string arrays, and fired only because `new Set()` infers Set<any>.
Typed it Set<string>, which it already is, rather than adding a
comparator the bare sort does not need.
Also anchors the lint-gate probe's no-console canary to CLI scripts that
exist to write to the console, rather than to app error paths that could
reasonably be removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A gate that checks nothing exits 0, which is indistinguishable from a
clean run — that is why the two invocation bugs fixed in 91f46ff went
unnoticed across 49 commits.
The check runs the configured oxlint command with one rule forced to
warn and requires a non-zero exit. That single probe covers a missing
--deny-warnings, a command that discovers no files, a broken tsconfig
path, and an upstream flag rename. Two things the probe cannot see get
their own assertions: that every gate invokes `pnpm oxlint` with nothing
appended, and that --type-aware is still enabled, since the canary rule
is not type-aware.
Verified against four reconstructed states: the b6ee4c8 and 9b0cb38
configurations, --type-aware removed, and a tsconfig path that matches
no files. Each fails; the current configuration passes.
Follows the existing check-layer-boundaries.mjs pattern — an exported
function driven by a Vitest case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two separate defects, both of which made the gate pass without checking:
lefthook ran `pnpm oxlint -- --deny warnings`. pnpm forwards arguments
after `--` to the script, so oxlint received its own `--` terminator and
read the rest as file paths — the pre-commit oxlint job linted 0 files.
The check script used `--deny warnings` rather than `--deny-warnings`.
`--deny` takes a rule or category and `warnings` is neither, so it was
accepted and ignored. Without `--deny-warnings` the run reports warnings
but still exits 0, so CI was equally non-blocking.
Both now call `pnpm oxlint`, keeping the flags in one place.
Introduced 2026-03-29 in b6ee4c8, which claimed warnings would fail the
build. 9b0cb38 diagnosed the invalid flag but fixed only the npm script,
leaving both actual gates broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pnpm 10.32 stopped reading the "pnpm" field from package.json, so the
undici/picomatch overrides and the three GHSA suppressions had silently
stopped applying — only the stale lockfile still held undici at 7.24.8.
Move the surviving picomatch override to pnpm-workspace.yaml.
With the config live again, two high advisories surfaced:
- postcss was stuck at 8.5.15 (GHSA-r28c-9q8g-f849, patched in 8.5.18);
nothing pinned it, so refresh to 8.5.25 within vite's range.
- undici GHSA-4cwx-7wf7-3272 needed >=7.29.0, but our ~7.24.0 pin existed
because jsdom 29 crashed on undici 7.28+. jsdom 30 moved to undici ^8.9,
so bump jsdom and drop both the pin and all three suppressions.
15 vulnerabilities (5 high, 3 suppressed) down to 1 moderate (smol-toml
via knip, below the --audit-level=high gate).
Separately, ports.ts declared its members with method shorthand, which
TypeScript treats as this-dependent, so oxlint's unbound-method fired
wherever a port was passed by reference (use-bestiary.ts:236). The ports
are bags of plain module functions, so declare them as readonly function
properties instead of suppressing the one call site. This makes parameter
types contravariant rather than bivariant; typecheck passes unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extract persistent damage types and definitions into a leaf module so
events.ts and types.ts no longer import back into persistent-damage.ts.
Enable oxlint import/no-cycle (whole workspace via root tsconfig) to
keep cycles out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2024 DMG and PF2e GM Core define XP budgets as ceilings ("spend as
much of your XP budget as you can without going over"), not floors like
the 2014 DMG thresholds. The tier is now the lowest budget the XP does
not exceed: an encounter over the Moderate budget is High even if below
the High budget. PF2e now also uses the Trivial budget (40 or less),
GM Core remaster party-size adjustments (Low +20), and counts creatures
more than 4 levels below the party as 0 XP. 2014 rules are unchanged.
Co-Authored-By: Claude Fable 5 <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.
GHSA-vxpw-j846-p89q (WebSocket DoS via fragment count bypass) and
GHSA-hm92-r4w5-c3mj (SOCKS5 proxy pool cross-origin reuse) just
landed in the registry. Both are fixed in undici>=7.28.0 and both
sit in code paths we don't exercise from tests (no WebSocket
client, no SOCKS5 proxy). Same blocker as GHSA-vmh5-mc38-953g:
jsdom@29.1.1 reaches into undici 7's private module layout, so
we can't move the pin to 7.28+. Added them to the existing
ignoreGhsas list and consolidated the per-entry notes.
The PC id counter lived in a module-level let that reset to 0 on
every page load. After rehydrating PCs from localStorage, the next
create would hand out pc-1 again, colliding with an existing id.
That broke React's keyed reconciliation and caused the wrong PC
to be deleted (deletePlayerCharacter matches the first occurrence
of the id, so deleting the new pc-1 would remove the rehydrated
one instead).
Derive the next id from the max numeric suffix of existing
characters at the moment of creation. No more shared counter, so
no more reset on reload and no collision after import.
CR 1/4 Medium Fey with Gallop (advantage-cancelling skirmisher
trait) and Charge (bonus 1d6 on a 15 ft. straight-line melee),
matching the centaur PC traits players already have access to.
Bumps vite ^8.0.5 → ^8.0.16 (GHSA-fx2h-pf6j-xcff, server.fs.deny
bypass on Windows) and jsdom ^29.0.1 → ^29.1.1 to unblock the
pre-commit audit gate.
The existing >=7.24.0 undici override was floating to 8.x, which
broke jsdom (it reaches into undici 7's private module layout).
Tightened to ~7.24.0 to keep jsdom working. That leaves
GHSA-vmh5-mc38-953g (undici SOCKS5 ProxyAgent TLS bypass) open —
patched in 7.28+ but we can't move there until jsdom updates its
pin. We never use a SOCKS5 proxy in tests, so the vulnerable code
path is unreachable. Added an auditConfig.ignoreGhsas entry with
a note explaining the rationale and the condition for removing it.
Adds the monsters from appendix B (pages 163-199) of The Great Labors:
Anarch Boar, Blemys, Bronze Automaton/Strategos, Cerberus/Young Cerberus,
Empusa, Goatling/Trickster, Gygan, Keledone, Maenad, Thylean Manticore,
Marble Golem, Minotaur Berserker/Warrior, the three mythic beasts
(White Stag, Golden Lion, Golden Ram), the five nymph lineages
(Aurae, Naiad, Nereid, Oceanid, Oread), Satyr Minstrel, and
Soldier/Soldier Captain.
Generated by scripts/extract-great-labors.py from the source PDF.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
D&D creatures listed in data/bestiary/dnd-bundled.json are now merged into
the search index and pre-loaded into creatureMap, so they appear alongside
5etools creatures with no "Load source" step. Source codes are derived from
the JSON itself (each creature carries source + sourceDisplayName), so adding
a new book is a pure data change. Bundled sources are excluded from
getAllSourceCodes() so bulk-import skips them, and they never appear in the
source manager (which only lists cached sources).
Includes a reference extractor (scripts/extract-great-labors.py) for the
5.5e revised stat-block format and a /bundle-bestiary skill that future
agents can follow to add monsters from other PDF books.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Show Unicode action icons (◆/◆◆/◆◆◆ for actions, ◇ for free,
↺ for reaction) in ability names from the activity field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse and display traits (concentrate, divine, polymorph, etc.)
on ability entries, matching how attack traits are already shown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Broaden stripDiceBrackets to stripAngleBrackets to handle all
PF2e tools angle-bracket formatting (e.g. <10 feet>, <15 feet>),
not just dice notation. Also strip in damage text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Correct inaccurate PF2e condition descriptions against official AoN
rules (blinded, deafened, confused, grabbed, hidden, paralyzed,
unconscious, drained, fascinated, enfeebled, stunned). Sort condition
picker alphabetically per game system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Format senses with type (imprecise/precise) and range in feet,
and strip {@ability} tags (e.g. tremorsense)
- Strip angle-bracket dice notation in attack traits (<d8> → d8)
- Fix existing weakness/resistance tests to nest under defenses
- Fix non-null assertions in 5e bestiary adapter tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some PF2e creatures (e.g. Giant Mining Bee) have qualitative
weaknesses without a numeric amount, causing "undefined" to
render in the stat block. Handle missing amounts gracefully.
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>
Bestiary sources like AWM store 0 for unknown HP. Passing maxHp: 0
into addCombatant triggered domain validation rejection, silently
dropping the creature. Treat hp: 0 as undefined, matching existing
ac: 0 handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds disable-model-invocation and allowed-tools restrictions
that structurally enforce commit safety.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clarify that spec.md is a living capability document, plan.md/tasks.md
are bounded work packages, and tests are the executable ground truth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slim Vitest pre-commit output with dot reporter and coverage summary.
Ignore .agent-tests/ and docs/agents/research/ in git.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move niche conventions (component props, export compat) to
docs/conventions.md, trim Speckit/Constitution sections to link to
source files, and add a one-line project description.
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>
Fixes packages/app → packages/application path, expands scripts table,
documents the parallel merge gate, adds contributing workflow with
spec-driven process and Claude Code skills, and documents bestiary
index regeneration.
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>
Tests search/suggestion filtering, queued creature counting, form
submission with custom stats, browse mode, and dismiss/clear behavior.
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>
Exports encounterReducer and EncounterState for testing. Adds 26
pure-function tests covering all action types: CRUD, turn navigation,
HP/AC/conditions, undo/redo, bestiary add with auto-numbering,
player character add, import, and event accumulation.
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>
Adds missing tests for undoUseCase, redoUseCase, and setTempHpUseCase,
bringing application layer coverage from ~81% to 97%. Removes
autoUpdate from coverage thresholds and sets floors to actual values
so they enforce a real minimum.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uses ts.createProgram to parse real AST instead of regex + brace-depth
state machine. Immune to comments, strings, and complex type syntax.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Independent checks (audit, knip, biome, jscpd, jsinspect, custom
scripts) now run in parallel. Type-dependent checks (oxlint, vitest)
remain sequential after tsc --build via a piped group. Also reorder
pnpm check for fast-fail on cheap checks first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rehydration functions (reconstructing typed domain objects from untyped
JSON) lived in persistence adapters, duplicating domain validation.
Adding a field required updating both the domain type and a separate
adapter function — the adapter was missed for `level`, silently dropping
it on reload. Now adding a field only requires updating the domain type
and its co-located rehydration function.
- Add `rehydratePlayerCharacter` and `rehydrateCombatant` to domain
- Persistence adapters delegate to domain instead of reimplementing
- Add `tempHp` validation (was silently dropped during rehydration)
- Tighten initiative validation to integer-only
- Exhaustive domain tests (53 cases); adapter tests slimmed to round-trip
- Remove stale `jsinspect-plus` Knip ignoreDependencies entry
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 import/export feature bullet to README.md (constitution requires
README updates when user-facing capabilities change). Add research
scope note to CLAUDE.md RPI section: research phases should scan for
existing patterns and consolidation opportunities, not just what the
feature needs. Remove auto-generated Active Technologies / Recent
Changes sections that duplicated Tech Stack.
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>
Delete merged feature branches (005–037) that inflated the auto-increment
counter in create-new-feature.sh, and renumber the undo-redo spec to
follow the existing 001–005 sequence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the stat block / source manager panel when the last combatant
is removed or the encounter is cleared, giving a fully clean state.
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>
Add .rodney/ to gitignore. Remove redundant Active Technologies and
Recent Changes sections from CLAUDE.md — info already covered by
Tech Stack and Data & Storage sections.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
addFromBestiary and addFromPlayerCharacter rename existing combatants
before adding the new one. If the add fails, the renames were applied
without an undo entry. Restore the pre-operation snapshot on failure.
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>
addCombatant now accepts an optional init parameter for pre-filled stats
(HP, AC, initiative, creatureId, color, icon, playerCharacterId), making
combatant creation a single atomic operation with domain validation.
This eliminates the multi-step store.save() bypass in addFromBestiary and
addFromPlayerCharacter, and removes the CombatantOpts/applyCombatantOpts
helpers. Also extracts shared initiative sort logic into initiative-sort.ts
used by both addCombatant and setInitiative.
Closes#15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrate the rodney/showboat browser automation skill for headless
Chrome screenshots and testing. Exclude .rodney and .agent-tests
from Biome file scanning. Add picomatch override to resolve
high-severity ReDoS vulnerability in knip/jscpd transitive deps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ADR-003: Branded types for compile-time identity safety at zero
runtime cost.
ADR-004: On-demand bestiary via compact index + IndexedDB cache,
avoiding distribution of copyrighted content.
ADR-005: All quality gates at pre-commit for tight agent feedback
loops, with analysis of per-change hooks as a future option.
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>
Document the errors-as-values pattern (ADR-001) and domain events
as plain data objects (ADR-002) to capture the reasoning behind
these foundational design choices.
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>
- iOS zoom fix (16px input font)
- Safe area insets for notched phones
- viewport-fit=cover
- Action bar flex-wrap for narrow screens
- Slightly increased row padding on mobile
- Fix stat block panel showing wrong creature on first open
- Skip auto-opening stat block when adding on mobile
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>
useAutoStatBlock was overriding the user's creature selection when
the panel transitioned from closed to open. Now only auto-updates
when the active turn index changes (advance/retreat), not when the
panel mode changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>