refactor(core): adopt dcc-core-lib via a Foundry adapter (Phases 0-7)#720
Open
cyface wants to merge 232 commits into
Open
refactor(core): adopt dcc-core-lib via a Foundry adapter (Phases 0-7)#720cyface wants to merge 232 commits into
cyface wants to merge 232 commits into
Conversation
Adopts dcc-core-lib as the pure-functional engine and maps a 7-phase migration path for thinning the Foundry system into an adapter. This is the design doc for the refactor/dcc-core-lib-adapter branch.
Sets up the adapter layer that Phases 1–6 will fill in:
- module/adapter/{character-accessors,foundry-roller,foundry-events,foundry-data-loader,chat-renderer}.mjs
are documented stubs. Each header JSDoc describes where the Foundry↔
dcc-core-lib boundary lives.
- docs/dev/EXTENSION_API.md catalogs every emitted dcc.* hook and every
game.dcc.* export, tagging each stable / internal / dead based on
actual usage by dcc-qol, xcc, mcc-classes, dcc-crawl-classes, and the
content packs.
- docs/00-progress.md is the running handoff log for this refactor.
No runtime behavior change. dcc-core-lib is linked via npm link for dev;
runtime loading strategy is still an open question (see progress log §Blockers).
Retroactive amendment to the handoff log so future sessions can find the commit by SHA.
…prep) Resolves open question #1 in docs/00-progress.md: Foundry has no bundler and loads module/dcc.js directly, so the lib's built dist/ is vendored under module/vendor/dcc-core-lib/ and imported via relative paths. scripts/sync-core-lib.mjs builds the linked source at \$DCC_CORE_LIB_SRC (default /Users/timwhite/WebstormProjects/dcc-core-lib) and refreshes the vendor dir, writing a VERSION.json that records the lib's version, git SHA, and dirty flag. Added module/vendor/** to standard.ignore so the linter skips vendored output. Initial sync: @moonloch/dcc-core-lib@0.2.1 (fa908c2, clean tree), ~4.3 MB across 577 files. Also folds in: - trailing-newline fixes on the five Phase 0 adapter stubs (standard --fix collateral, zero behavior change) - .nvmrc pinning Node 24 (matches package.json engines) - docs/01-session-start.md: fresh-session resume prompt, updated now that the runtime-loading blocker is resolved Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in the wave-1 modifier redesign (tagged-union RollModifier + async check pipeline). See dcc-core-lib/docs/MODIFIERS.md for the design. This sync unlocks Phase 1 of the adapter refactor — the adapter layer can now consume rollAbilityCheckAsync, pass an async custom roller backed by Foundry's Roll.evaluate, and receive structured modifier breakdowns for chat rendering. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up sync for the f78cbdf hotfix in the lib repo — adds the
async check exports (rollCheckAsync, rollAbilityCheckAsync,
rollSavingThrowAsync, RollCheckOptionsAsync) to checks/index.ts's
public surface. Without this sync, top-level
`import { rollAbilityCheckAsync } from '../vendor/dcc-core-lib/index.js'`
throws "is not a function" at runtime — the function existed in
roll.js but wasn't re-exported.
Version still 0.3.0 (patch-level fix).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates DCCActor.rollAbilityCheck end-to-end through the adapter
layer, the first roll type to move in the architecture refactor.
Flow: DCCActor._rollAbilityCheckViaAdapter →
actorToCharacter (character-accessors.mjs) →
rollAbilityCheckAsync (dcc-core-lib 0.3.0) with
createFoundryRoller as the injected async roller →
renderAbilityCheck (chat-renderer.mjs) →
ChatMessage.create.
The public signature of DCCActor.rollAbilityCheck is unchanged, so
dcc-qol and token-action-hud-dcc are unaffected. A dispatcher at the
top of rollAbilityCheck routes to the legacy code path when the
request is one the adapter can't yet handle:
- options.rollUnder === true (Luck check)
- options.showModifierDialog === true
- str/agl with computeCheckPenalty set (CheckPenalty display term)
The legacy method is preserved verbatim as _rollAbilityCheckLegacy
so behavior in those cases is bit-identical to before.
Adapter modules filled:
- module/adapter/character-accessors.mjs — actorToCharacter builds
a minimal lib Character from DCCActor. Handles the save-id remap
(frt/ref/wil ↔ fortitude/reflex/will) and parses Foundry's signed
save strings ("+2", "-1") into numbers. Reads post-AE values by
relying on Foundry having already applied Active Effects before
prepareDerivedData runs.
- module/adapter/foundry-roller.mjs — createFoundryRoller returns
an async roller of shape (expression) => Promise<number> suitable
for rollAbilityCheckAsync. Each invocation creates a Foundry Roll,
awaits .evaluate(), and stashes the Roll on a shared context so
chat-renderer can reattach it to the ChatMessage for DSN and
breakdown rendering.
- module/adapter/chat-renderer.mjs — renderAbilityCheck preserves
the flag contract the legacy path emits (dcc.RollType,
dcc.Ability, dcc.isAbilityCheck, checkPenaltyCouldApply,
FleetingLuck updates) and adds a structured system.libResult
payload for downstream consumers.
Tests:
- New module/__tests__/adapter-ability-check.test.js (3 tests)
covers the actor → character shape mapping, the adapter round-trip
for a default Luck check (which takes the adapter path), and a
rollUnder Luck check (which falls through to legacy). Locks the
adapter contract before saves/skills/init migrate.
- Existing module/__tests__/actor.test.js "roll ability check"
reduced from asserting 3 dccRollCreateRollMock calls to 2 — the
third (lck, no rollUnder) now adapter-routes and does not invoke
DCCRoll.createRoll. The rollToMessageMock assertion for that case
relaxes to expect.objectContaining(...) since the adapter output
includes an additional system.libResult field.
Docs:
- docs/00-progress.md gains a 2026-04-18 session entry, marks the
current phase as "Phase 1 — Adopt the lib for simple rolls",
documents the lib-side wave-1 work and async siblings, and
rewrites Next steps to sequence the save/skill/init migrations.
- docs/01-session-start.md updated so a fresh Claude session
resumes pointing at the save migration as the next target.
683 unit tests pass across 19 test files. Sync with
@moonloch/dcc-core-lib@0.3.0 (f78cbdf) already landed in the prior
vendor commit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs surfaced during in-Foundry testing of Phase 1's ability
check adapter path:
1. Display total was wrong on the chat card.
The previous async roller (createFoundryRoller + rollAbilityCheckAsync)
gave Foundry only the bare dice expression (e.g. "1d20"), because
the lib's roller contract passes *per-dice-expression* expressions,
not the full formula with modifiers. The Foundry Roll's total then
reflected only the dice (17), but the lib's real total included
modifiers (18). Foundry's ChatMessage renders the Roll's total, so
the card showed the wrong number.
Fix: switch to the two-pass pattern originally discussed in design.
Pass 1 calls libRollAbilityCheck with { mode: 'formula' } to get the
full formula string. Pass 2 creates a Foundry Roll from that full
formula, awaits evaluate() so Foundry computes the correct display
total, then calls the lib again in evaluate mode with a sync roller
returning the pre-rolled natural — so the lib classifies against
the exact dice outcome that will be shown. Neither pass re-rolls
the dice.
2. Dispatcher missed cmd-click on str/agl.
actor-sheet.js builds options via `rollModifierDefault ^ (ctrlKey
|| metaKey)` — a bitwise XOR returning 0 or 1, not true/false. The
dispatcher's `showModifierDialog === true` strict check saw `1` as
not-true and routed to the adapter, so cmd-click silently stopped
showing the modifier dialog. Switched to truthy checks.
Also narrowed the str/agl-with-CheckPenalty exclusion to only fire
when the penalty is actually non-zero — a configured penalty of 0 is
an empty display term and safely goes through the adapter.
Moved the structured lib result from system.libResult to
flags['dcc.libResult'] — Foundry's ChatMessage schema drops unknown
keys from system, while flags accept free-form namespaced JSON. This
restores the structured-payload integration point for downstream
consumers and the F12 verification trick in the docs.
Tests updated: actor.test.js's `roll ability check` now expects the
str path to go through the adapter (mock actor's checkPenalty is 0);
adapter-ability-check.test.js reads libResult from flags; 683 tests
green.
Removed the unused foundry-roller.mjs import from actor.js — the
roller file stays in place for future adapter paths that legitimately
want an async roller callback (e.g. saves/skills where the two-pass
model still applies but is the right shape).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds module/__integration__/adapter-ability-check.test.js covering
the Phase 1 adapter pipeline with the real Foundry Roll engine —
catches the display-total regression we hit in manual testing
(Foundry Roll total matching lib result.total after modifiers).
Eleven test cases across two describes:
- actorToCharacter (integration): verifies the Foundry-shape →
lib-Character translation without real dice. Runs in every
environment. 3 tests: ability score mapping, save-id remap
(frt/ref/wil ↔ fortitude/reflex/will), level + class-id carry.
- adapter two-pass flow (real Foundry dice): runs only when the
dice engine is loaded (.foundry-dev/client/dice/ populated via
`npm run setup:foundry`). 8 tests covering formula shape for
non-zero and zero ability mods, Roll total = natural + mod across
40 d20 iterations, result.total == Roll.total across 25
iterations, critical on natural max, fumble on natural 1, modifier
breakdown carries origin.category='ability' with applied=true,
and no level modifier on default ability checks.
ChatMessage rendering is intentionally out of scope — it requires a
full client environment. The mock-based test at
module/__tests__/adapter-ability-check.test.js covers that path.
Uses a tiny makeActor() builder for DCCActor-shaped fixtures without
subclassing Actor; the adapter never touches Foundry Document APIs
so the minimal shape is sufficient.
686 tests pass + 32 skipped (24 pre-existing dice-engine cases +
8 new adapter cases that also require the dice engine).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-sync after the lib was rebased onto remote main. The lib's remote had a 0.2.2 version bump we didn't have locally; rather than overwrite it, our breaking change is stacked on top and versioned 0.4.0 (skipping 0.3.0 which only ever existed locally before the rebase). The actual dist code is unchanged from the previous 0.3.0 sync — only VERSION.json moves (name/version/commit all update). Adapter imports continue to work untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites docs/01-session-start.md to accurately describe the committed adapter pattern (two-pass sync with Foundry Roll owning the full formula), not the earlier async-roller design that silently dropped modifiers from the chat total. Replaces the generic "continuation" framing with specific save-throw instructions: the two-pass code shape to copy, why NOT to reach for rollSavingThrowAsync + createFoundryRoller (with pointer to the display-total bugfix commit 5a9bae6), how to translate Foundry's frt/ref/wil save-ids to the lib's fortitude/reflex/will at the adapter boundary, the expected modifier breakdown (ability-mod + save-bonus), and the test files to mirror from the ability-check work. Also bumps the vendored lib reference to 0.4.0 (up from the stale 0.3.0 in the prior version) and points at MODIFIERS.md as required reading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior version mentioned writing integration tests but was vague about how to run them. Updated to: - Enumerate the three test kinds required before committing (unit mock-based, integration real-dice, existing-test update) with specific file paths modeled on the ability-check work. - Document the three npm scripts (test, test:unit, test:integration) and when to use each during iteration vs. pre-commit. - Explicitly flag that dice-engine-gated cases skip silently unless .foundry-dev/client/dice/ is populated, and point at `npm run setup:foundry` as the one-time prerequisite. - Add a note to verify the skip count drops by the number of new dice cases before claiming the integration suite is green — otherwise the new assertions never ran. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rollSkillCheck now dispatches through the lib's two-pass rollCheck for built-in slots and skill items with a die. _resolveSkill shares skill resolution across paths; _buildSkillDefinition / _buildSkillCheckModifiers emit the lib SkillDefinition + situational add modifiers (skill value, level, Mighty Deed, armor check penalty). Carve-outs routed to legacy: modifier dialog, cleric disapproval, CONFIG.DCC.skillTables entries, and no-die / description-only skill items. Bundles the previously uncommitted saving-throw work from session 3: the save-throw unit + integration tests and the shared logDispatch helper in module/adapter/debug.mjs that every dispatcher branch uses. renderSkillCheck preserves the legacy flag contract (dcc.RollType, dcc.ItemId/dcc.SkillId, dcc.isSkillCheck) plus dcc.libResult and the skill-description content for item-backed skills. 746 tests pass (+13: 5 unit + 8 dice-gated integration for skills). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split `DCCActor.getInitiativeRoll` into a dispatcher over
`_getInitiativeRollViaAdapter` (default) and `_getInitiativeRollLegacy`
(invoked when `options.showModifierDialog` is set). The adapter path
asks the lib for a formula via `rollCheck(mode: 'formula')` against a
synthetic `{ id: 'initiative', type: 'check' }` SkillDefinition and
returns `new Roll(formula)`; Foundry's `Combat#rollInitiative`
callback chain, the `core.initiativeRoll` flag, and the
`emoteInitiativeRoll` integration at module/chat.js:492 are all
preserved.
Init has no gameplay crit/fumble in vanilla DCC, so this is single-pass
(formula only) — save / ability / skill migrations remain two-pass
where classification drives gameplay. `system.attributes.init.value`
already bakes in agl mod + otherMod + class level via
`computeInitiative`, so the SkillDefinition omits `roll.ability` and
init.value flows in as one aggregate `add` modifier with origin
`{ category: 'other', id: 'initiative-total' }`. Weapon-die overrides
(two-handed, custom `initiativeDieOverride`) stay Foundry-side; the
`[Two-Handed]` / `[Weapon]` die label is re-injected into the lib's
formula string after the call returns.
Adds `module/__tests__/adapter-initiative.test.js` (6 mock-based
tests) and `module/__integration__/adapter-initiative.test.js` (7
dice-gated tests). Relaxes the two `actor.test.js` tests that
asserted `DCCRoll.createRoll` was called — the adapter path uses
`new Roll(formula)` instead. 759 tests pass (up from 746).
`logDispatch('rollInit', …)` wired in both branches. `debug.mjs` +
every call site gets removed in one cleanup commit at Phase 1 close,
after the skill + init paths are exercised in Foundry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives each public roll method (rollAbilityCheck, rollSavingThrow, rollSkillCheck, getInitiativeRoll) in a live Foundry and asserts the `[DCC adapter] <rollType> → <via adapter|LEGACY path>` console line from module/adapter/debug.mjs. Covers the adapter path plus every documented legacy carve-out (rollUnder, showModifierDialog, useDisapprovalRange + skillTable, description-only skill item, and the pre-built-Roll short-circuit that should emit no log at all). Spec is gated on the dispatch logging being present; header notes that once the Phase 1 cleanup commit strips logDispatch, this file should either be deleted or retargeted at the `dcc.libResult` chat-message flags that survive the cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/dev/TESTING.md: new Browser tests (Playwright) section covering Node 24, fvtt CLI configure installPath/dataPath, correct world name (`v14`, not `automated_testing`), and the "close your manual tab first" pitfall. - browser-tests/e2e/*.spec.js: correct the PREREQUISITES blocks (the old `--world=automated_testing` instruction pointed at a world that doesn't exist). Now each header points at TESTING.md for the full recipe and lists the working TL;DR command. - docs/00-progress.md + docs/01-session-start.md: reflect Phase 1 closed, dispatch-logging cleanup commit cancelled (see below), and Phase 2 (spell checks) as the next goal. - module/adapter/debug.mjs JSDoc: rewrite to describe the helper as permanent infrastructure rather than a phase-scoped scaffold. Dispatch-logging decision: `module/adapter/debug.mjs` + `logDispatch` call sites are kept indefinitely. The Playwright spec `browser-tests/e2e/phase1-adapter-dispatch.spec.js` asserts on the `[DCC adapter]` console lines, and `getInitiativeRoll` emits no chat message that could serve as a substitute assertion target. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the prompt paste-and-go for a fresh session: scaffolds `module/adapter/spell-events.mjs` + `spell-input.mjs`, adds a dispatcher split on `rollSpellCheck` that migrates only the generic-cast path while leaving wizard / cleric / patron side effects on the legacy `processSpellCheck`. XCC's `game.dcc.processSpellCheck` shim stays intact. Unit + Playwright test coverage extended in the same session. Explicitly flags the lib wave-2 modifier migration as a prerequisite for deeper sessions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route generic-castingMode spell items (no patron, not Cleric) through _rollSpellCheckViaAdapter; wizard / cleric / patron-bound / naked paths stay on the legacy body verbatim, preserving game.dcc.processSpellCheck and its side-effect orchestration. Adds module/adapter/spell-input.mjs (buildSpellCastInput with a synthetic generic caster profile), module/adapter/spell-events.mjs (header-only stub), and a renderSpellCheck export on module/adapter/chat-renderer.mjs. Unit coverage (4 tests) plus 3 new Playwright cases — 18/18 green against live V14 Foundry, 763/763 Vitest green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llCheck
Broadens the adapter gate for rollSpellCheck to cover wizard-castingMode
items on non-cleric, non-patron-bound actors. Wizard casts now route
through libCalculateSpellCheck with a real getCasterProfile('wizard')
profile and a single-entry spellbook built from the Foundry item; when
the lib reports spellLost, the onSpellLost event bridge in
spell-events.mjs marks the item system.lost:true via item.update,
replacing the actor.loseSpell(item) side effect in processSpellCheck.
Adapter-side pre-check mirrors DCCItem.rollSpellCheck:260 — warns and
returns early when spellItem.system.lost && automateWizardSpellLoss.
Classes with no lib-side caster profile fall back to the legacy path so
homebrew / spinoff classes still work.
spell-input.mjs: syntheticGenericProfile, buildSpellbookEntry,
buildSpellCheckArgs (returns null on unknown class for legacy fallback).
spell-events.mjs: createSpellEvents({ actor, spellItem }) wires
onSpellLost; sessions 3–5 add disapproval / patron / spellburn /
mercurial callbacks.
Tests: 8 unit tests in adapter-spell-check.test.js (up from 4) — new:
wizard-on-wizard adapter, lost-spell warn+return, wizard-on-patron
legacy, createSpellEvents.onSpellLost bridge. Playwright spec at 19
tests (wizard-mode adapter added). Full suite 767 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ellCheck
Broadens the adapter gate for rollSpellCheck to cover cleric-castingMode
items on non-patron-bound Cleric actors. Cleric casts now route through
libCalculateSpellCheck with a real getCasterProfile('cleric') profile,
character.state.classState.cleric.disapprovalRange seeded from
actor.system.class.disapproval, and a Foundry-loaded disapproval table
adapted to the lib's SimpleTable shape.
When the natural roll triggers disapproval, the lib's
handleClericDisapproval rolls 1d4 × range via the adapter's formula-
dispatching roller (pre-rolled in Foundry), looks up the table entry,
and fires onDisapprovalIncreased; the event bridge updates
actor.system.class.disapproval and posts the DCC.DisapprovalGained
EMOTE chat (replaces actor.applyDisapproval()). The adapter then posts
the disapproval roll chat from result.disapprovalResult via new
renderDisapprovalRoll (replaces actor.rollDisapproval(natural) +
RollTable.draw).
Dispatcher gate now:
castingMode === 'generic' && !isCleric && !hasPatron → adapter
castingMode === 'wizard' && !isCleric && !hasPatron → adapter
castingMode === 'cleric' && isCleric && !hasPatron → adapter
everything else → legacy
Narrower than the session slice's (castingMode === 'cleric' || isCleric)
gate — wizard-on-cleric and cleric-on-non-cleric would swap the legacy
side-effect set (wizard spell loss vs. disapproval), so those stay on
legacy.
spell-input.mjs: loadDisapprovalTable(actor) async loader + local
toLibSimpleTable helper (extract to shared loader when sessions 4–5
add a second consumer). buildSpellCheckArgs populates cleric
disapprovalRange.
spell-events.mjs: createSpellEvents now returns onDisapprovalIncreased
when an actor is provided. Mirrors legacy applyDisapproval: NPC
early-return, actor.update, DCC.DisapprovalGained EMOTE chat.
chat-renderer.mjs: renderDisapprovalRoll builds a deterministic
${total}d1 Roll so the lib's rolled value renders through Foundry's
normal chat pipeline with the drawn table entry's description.
actor.js: _castViaCalculateSpellCheck loads the disapproval table
when cleric, pre-rolls 1d4 in Foundry when natural ≤ range, uses a
formula-dispatching roller for pass 2, and posts the disapproval
chat after the main spell-check chat.
Tests: adapter-spell-check.test.js now 14 tests (up from 8) — cleric
adapter fires, patron-bound cleric → legacy, cleric item on non-cleric
→ legacy, onDisapprovalIncreased updates actor, NPC bails, no-actor
skips wiring. Playwright phase1-adapter-dispatch.spec.js now 20 tests
(up from 19) — cleric-castingMode → adapter (cleric) assertion. All
773 Vitest tests pass; all 20 Playwright tests pass against live
v14 Foundry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 1 section still described the spec as 18 tests "after the Phase 2 session 1 extension" — sessions 2 + 3 have pushed it to 20, all passing against live v14 Foundry as of 2026-04-18. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patron-bound wizard / elf actors now flow through the adapter for wizard-castingMode spell checks. `buildSpellCheckArgs` populates `character.state.classState.<wizard|elf>.patron` so `getPatronId` resolves; the lib's RAW patron-taint pipeline stays dormant (no `fumbleTable` plumbed in) and `_runLegacyPatronTaint` ports the legacy `processSpellCheck:623-660` d100-vs-creeping-chance mechanic verbatim, preserving the +1% bump on every patron-related cast. Decision rationale + RAW alignment open question tracked in docs/00-progress.md. 777 Vitest tests pass (+4 net); 20 Playwright tests pass against live v14 Foundry (existing wizard+patron case rescoped to assert adapter dispatch + chance bump). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lculateSpellCheck
Wizard / elf casts now plumb spellburn commitments through the adapter
and pre-roll first-cast mercurial magic adapter-side.
- `buildSpellCheckArgs` forwards `options.spellburn` into
`input.spellburn`; `buildSpellbookEntry` reads
`system.mercurialEffect.value` into the lib entry so already-rolled
effects surface on `result.mercurialEffect`.
- New `onSpellburnApplied` bridge subtracts the burn commitment from
`system.abilities.<str|agl|sta>.value` (clamped at 1, NPC-aware).
- New `loadMercurialMagicTable()` walks `CONFIG.DCC.mercurialMagicTable`
(compendium → world fallback) via the shared `foundryTableEntries`
helper factored out of session-3's `toLibSimpleTable`.
- `_rollMercurialIfNeeded` pre-rolls `1d100` + luckMod × 10 through
the lib's `rollMercurialMagic`, persists the effect to the Foundry
item, and attaches it to the in-flight spellbook entry.
- Mercurial display chat renders directly from `result.mercurialEffect`
via the new `renderMercurialEffect` (mirrors session-3's
`renderDisapprovalRoll`). Pass-1 events tightened to `{}` — the lib
fires `onSpellburnApplied` / `onMercurialEffect` unconditionally on
both passes, so a bridge-based render would double-post.
13 new Vitest tests (790 total, up from 777). 2 new Playwright
dispatch tests (22 total) — all pass against live v14 Foundry.
Open question #6 added: spellburn dialog integration (pre-existing
session-2 regression — adapter bypasses `DCCRoll.createRoll`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session 5's pre-commit hook `git add .` swept in an ephemeral lock file from the Claude Code harness. Gitignore the scheduled-task family so future commits don't pick it up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atron-taint Phase 2 close-out session. Both gate items resolved as doc-only decisions; no runtime behavior changed. Gate 1 — game.dcc.processSpellCheck consumer audit: Inventoried 5 call sites (3 DCC-internal + 2 XCC). XCC's sheet calls are structural peers of DCC's own internal callers, not public-API consumers. Deprecation / shim / parallel-entry options all rejected. Decision: processSpellCheck is permanent stable API. Adapter dispatcher routes happy-paths through _castViaCastSpell / _castViaCalculateSpellCheck; everything else (result tables, manifestation, forceCrit, naked path, skill-table spells, XCC sheets) continues through processSpellCheck. Incremental per-route migration replaces the earlier delete-after-migration plan. Gate 2 — Open question #5 (patron-taint RAW alignment): _runLegacyPatronTaint is permanent adapter infrastructure. Legacy creeping-chance mechanic preserved verbatim; RAW alignment deferred to backlog (requires fumble-table effect-tag migration across sibling content modules + per-patron taint-table resolution). Updates: - docs/dev/EXTENSION_API.md — processSpellCheck row + recommendation #5 reflect permanent-stable designation - docs/dev/ARCHITECTURE_REIMAGINED.md §7 — Phase 2 marked CLOSED, Phase 3 marked ACTIVE, "Delete processSpellCheck" goal removed - docs/00-progress.md — Phase 2 close-out session with inventory + decision rationale; question #5 moved to Closed; Next steps rewritten for Phase 3 - docs/01-session-start.md — rewritten as Phase 3 session 1 prompt (attack migration scaffold options) - module/actor.js — _runLegacyPatronTaint JSDoc pinned as permanent Tests: 790 Vitest pass (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `module/adapter/roll-dialog.mjs` with `promptSpellburnCommitment` bridges the wizard/elf adapter cast path to a DialogV2-based Spellburn input, fixing the latent regression from Phase 2 session 2 where wizard adapter casts silently lost the legacy Spellburn UI. The `rollSpellCheck` dispatcher now awaits the prompt when `options.showModifierDialog && !options.spellburn && !isNPC`; cancel aborts, commit threads the `SpellburnCommitment` through to the existing `onSpellburnApplied` bridge. Closes open question #6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split DCCActor.rollToHit into dispatcher + _rollToHitLegacy (verbatim) + _rollToHitViaAdapter. Gate routes the simplest-weapon happy-path (no deed die, no backstab, no two-weapon, no showModifierDialog, non-dice toHit, automateDamageFumblesCrits on) through the adapter; everything else through legacy. Adapter preserves dcc.modifyAttackRollTerms + DCCRoll.createRoll for chat rendering and dcc-qol compatibility, then feeds the natural d20 into the lib's makeAttackRoll so the lib owns classification + appliedModifiers. Result surfaces as flags['dcc.libResult'] in the attack chat card — no template changes needed. New module/adapter/attack-input.mjs (buildAttackInput) + attack-events.mjs stub. 9 new Vitest cases in adapter-weapon-attack.test.js (803 total, up from 794). 4 new Playwright cases in phase1-adapter-dispatch.spec.js (26 total) — all passing against live v14 Foundry (3.0 min run). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Teach `_rollToHitViaAdapter` to reflect `dcc.modifyAttackRollTerms`- pushed `Modifier` terms into the lib's `bonuses[]`. Snapshots `terms.length` before the hook, slices additions, translates pure signed-integer Modifier terms into `RollBonus` entries via a new `hookTermsToBonuses` helper in `attack-input.mjs`, and surfaces the per-bonus breakdown as `libResult.bonuses` on the chat flag. dcc-qol's firing-into-melee and medium-range penalties now participate in the lib's `totalBonus` / `appliedModifiers` aggregation. In-place dice- chain mutations of `terms[0].formula` (long-range bump) are not yet translated — session 4 work. 808 Vitest tests pass (up from 803, +5 translator coverage). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session 3's hook-translation bridge is closed, so the session-start prompt now points at session 4 as the active work. Updates line references in `module/actor.js` (hook fires at 2829, not 2766), renames "Session 9 slice" → "Session 10 slice", refreshes the baseline test count (808 Vitest), and swaps the "next slice" lean to option (a) long-range dice-chain translation — the remaining gap where dcc-qol mutates `terms[0].formula` in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y real consumers The "single-class homebrew vertical" candidate was a synthetic stand-in from before the real sibling migrations landed. 2026-06-08 audit confirms it's already validated more thoroughly by real consumers: dcc-crawl-classes ships 9 homebrew classes on base DCC (Ranger/Paladin/Orc/2 Halfling subclasses/Gnome/Elven Rogue/Dwarven Priest/Bard) driving registerClassMixin + registerSheetPart + registerClassDefaults + registerHomebrewClassForProgressionLoad + registerActorSheet + 9 DCCSheet stubs; mcc-classes exercises the same registries across 7 variant classes. The only registries no sibling exercises — registerClassStartingItems (built-in dwarf only) and registerVariant for a base-DCC homebrew (n/a) — already have dedicated coverage (P5-2, P6-5). Group E is fully done. Docs-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… table The 32-entry activeEffectKeys table (added in PR #611) had zero runtime consumers — system OR all four sibling modules — ever. The V14 Active Effects workflow uses Foundry's native config UI, and the human-facing reference is the independent docs/user-guide/Active-Effects.md "Common Attribute Keys" section (a curated markdown list, not derived from this table). The code table only cost a schema-drift sync burden. Removed module/config/active-effect-keys.mjs + the CONFIG.DCC.activeEffectKeys re-composition in config.js + the unit guard; the e2e probe flips from "surface survives extraction" to "surface is removed". Not documented as a stable surface in EXTENSION_API.md, so no public-API note needed. Resolves the session-38 latent open item (deprecate/remove the unconsumed table). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The session-41 "dead needsValueRoll/rollValue formula path" finding was an orphaned FEATURE, not dead code. DCC treasure is often listed as rollable (e.g. a hoard worth 3d100 gp), and the surrounding scaffolding is still formula-aware: the treasure sheet inputs are type="text" data-dtype="String", every value reader uses parseInt(), the inventory categorizer guards on needsValueRoll(), and the "Roll Treasure Value" button + handler exist. Only a V14 schema tightening had orphaned the feature — the item value field became an integer CurrencyField and base-item migrateData parseInt()- destroyed any formula. Un-broke it: - New TreasureValueField (StringField per pp/ep/gp/sp/cp) for an item's system.value, so a die formula persists; needsValueRoll() flags it and rollValue() resolves it. - base-item migrateData now String()s legacy integers (no data loss) instead of parseInt()-destroying formulas. - Actor system.currency stays integer CurrencyField — Item Piles / §2.12 ecosystem currency-walking is unaffected; only an item's worth is formula-capable. Resolved values are strings post-restore (the parseInt readers handle them). Tests: +3 integration (data-models.test.js — formula survives migrateData, legacy int -> string, TreasureData constructs with a formula intact); the e2e currency probe rewritten to assert the restored feature end-to-end (a 2d6 hoard persists + needsValueRoll true + resolves in [2,12] + posts the LootValue card), conversion assertions moved to strings. 1592 Vitest. Resolves the session-41 latent open item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the rollable-treasure-value restore (eefeb2d), found by a full-stack audit: 1. calculateTreasureWeight (actor-sheet/items.mjs) summed coin denominations with a bare `+`. With the value fields now formula-capable StringFields, an unresolved coin's "0"+"0"+"12"+"0"+"0" string-concatenated to "001200" -> 1200 -> /coinWeight, yielding a wildly wrong encumbrance weight. (Latent before the restore: the integer schema kept needsValueRoll always-false, so unresolved coins never reached this branch.) Fixed with parseInt per denomination, mirroring the parseInt-guarded coin-merge block above it. +1 Vitest regression (string-valued unresolved coin -> 1.2 lb, not 120). 2. rollValue (item/currency-mixin.mjs) fired this.update(updates) without awaiting, so callers (and the e2e probe) could observe the pre-resolution value. Now awaited — the method is `async` and callers `await` it, so it should complete its write. 3. e2e treasure probe polls until the formula hoard resolves before asserting (the probe's earlier race is what the prior full-suite run caught). 1593 Vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08) found spell-duel.js had zero dedicated tests despite carrying the counterspell momentum math + a fragile die-from-table-HTML regex. +15 Vitest (spell-duel.test.js): getSpellDuelDie (attacker-column regex parse, PD cells, the [12,28] clamp, table-absent + column-absent null paths); the participant state machine (addParticipant seed/dup-guard/re-entrancy, removeParticipant, adjustMomentum floor-of-1 clamp, resetMomentum, isParticipant); and resolveExchange (missing- participant bail, identical-checks->phlogiston + defender momentum, attacker-win attackerHigh column selection, the [1,10] momentum-modified-roll clamp). Test-only, no production change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08) found parser.js was never imported by any test. The birth-augur effect application has four silent guard returns (no luck-mod match, empty augur name, missing compendium pack, no index match) where a regex regression or a renamed pack would drop the effect on every imported PC unnoticed. Exposed the helper via a named export (default export shape unchanged) and added +7 Vitest (parser-birth-augur.test.js): luck-mod +N/-N parse + effect apply with _id stripped, the no-trailing-mod path, the empty-name no-op, missing-pack + no-match quiet returns, and case-insensitive startsWith matching. createActors stays integration-only (heavy parse->dialog->create orchestration). Test-only + one named export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped -> warn) Audit (2026-06-08): the pure migrationOutcome policy + the per-document transforms were tested in isolation, but the migrateWorld integration was not — so a bug in the catch->failures wiring or the version-stamping gate could mark a partially-migrated world "done" and never retry it. +3 Vitest (migrate-world.test.js): a clean run stamps systemMigrationVersion + posts the complete toast + returns migrationComplete:true; a per-doc update rejection is caught and accumulated (the other docs still migrate), leaves the version UNSTAMPED, fires the DCC.MigrationFailures warn with the count, and returns migrationComplete:false; multi-doc failures are all counted. Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08): party-sheet.js had 0 dedicated tests. +9 Vitest (party-sheet.test.js) for the public instance methods invoked with a faked `this`: _validateMember (unknown-actor + no-party-in-party rejections, valid accept), the membership mutators _addMember/_removeMember/_updateMember, and the _processFormData->_processSubmitData weapon round-trip (active melee/ranged staged into _pendingMemberUpdates, members with no change skipped, pending updates applied via _updateMember then cleared) with the parent prototype's super methods stubbed. The static #roll* action handlers stay e2e-territory (action-map-private thin delegators to actor.rollXxx, like the actor-sheet handlers). Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08): both modules were vi.mock-ed everywhere, so only their call sites were verified. +8 Vitest (table-result.test.js): _adjustCritResult / _adjustFumbleResult guard branches (no container, missing table-name/NaN roll), the out-of-bounds warn, and the key dual-write to message content AND system.critResult/critRollTotal (resp. fumble), incl. the NPC-vs-PC fumble-table routing and the +1/-1 step direction. +7 Vitest (spell-result.test.js): addChatMessage flag construction (SpellCheck/RollType/ ItemId + the crit/fumble Fleeting-Luck branches) and processChatMessage's GM guard + SpellCheck-only shift-up/down click wiring. DOM stubbed (no jsdom dependency). Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nstruction Audit (2026-06-08). +3 Vitest (item-currency-rollvalue.test.js): rollValue resolves each denomination + writes system.value.* + posts the LootValue card, rolls a die formula (2d6) to a concrete total, and the bad-formula catch warns (DCC.BadValueFormulaWarning) + skips that denomination while still completing. +6 Vitest (roll-modifier-dialog.test.js): getTermByIndex attack-vs-'damage-' routing, and _constructRoll / _constructDamageFormula terms-array (no-DOM) branch joining the attack + damage formulas with + and firing each term callback. Exposed RollModifierDialog via a named export for testing (existing exports unchanged). Test-only + one named export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e catch Final audit (2026-06-08) coverage backfill. +12 Vitest (fleeting-luck.test.js): updateFlags nat-20->Gain / nat-1->Lose / mid-range no-op / no-dice no-op / pre-set isNaturalCrit|isFumble, updateFlagsForCrit|Fumble (incl. null-safe), and give/take/ spend balance math with the floor-at-zero clamp on take/spend. +4 Vitest (actor-init-die.test.js): _initDieAdditiveTerms regex (1d20+1d3 -> +1d3, bare die -> '', empty/null -> '', flat-mod tail) — the integration test re-implemented the regex rather than calling it. +1 Vitest (container.test.js): deleteDialog's cascade-delete catch returns null + logs when parent.deleteEmbeddedDocuments throws. Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on.type
Found by the 2026-06-08 drag/drop audit. In _createDCCWeaponMacro the weapon item is
`data.system.weapon` (the drag payload maps dccData -> system), so the icon-fallback
line's `data.data.weapon.type` was wrong twice: `data.data` is the weapon item itself
(drag-drop sets `data: weapon`), so `data.data.weapon` is undefined and `.type` THROWS
when a default/mystery-man-icon weapon is dragged to the hotbar. Fixed to `weapon.type`.
The existing unit test passed only because it fed a bogus `data: { weapon: { type } }`
shape that doesn't match production — corrected the four cases to the real
`system.weapon` shape (with a `type`) so they exercise the production path.
Also a lint-nit cleanup pass (pre-existing, surfaced by the audit): drop unused globals
from the /* global */ directives in item.js (ui/ChatMessage/CONFIG/CONST),
item/container-mixin.mjs + item/currency-mixin.mjs (Item, JSDoc-only), the unused
ContainerItemMixin import in item-currency-mixin.test.js, and name the Promise
constructor params `resolve` in the extension-api e2e poll helpers. `npx standard` now
fully clean. 1668 Vitest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two behaviour-neutral quick wins (the pre-commit `git add .` hook folds them into one commit): Stray debug console.log removal: - spell-result.js: 7 "Updating spell lastResult"/"Found spell item" traces (surrounding lookup logic preserved and slightly tightened) - item-sheet.js: bare _rollManifestation / _rollMercurialMagic / _lookupMercurialMagic breadcrumbs - actor/roll-data-mixin.mjs: bare console.log(err) in the action-dice catch upgraded to console.error with context Vendor sourcemap strip: - The vendored dcc-core-lib shipped 274 .map files whose `sources` point at the lib's original .ts (not vendored), so Vitest logged ~20 "Sourcemap points to missing source files" warnings per run. Foundry never reads sourcemaps, so they are dead weight. Removed all .map files, stripped the //# sourceMappingURL= comments, and added a stripSourcemaps() step to scripts/sync-core-lib.mjs so future syncs stay self-contained. No behaviour change; all 1668 unit tests green and the sourcemap warnings are gone from the test log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 7 actor.js shrinkage, continued. Lift the spell-check dispatch layer (public `rollSpellCheck` + its 9 private dispatchers, ~986 lines) out of actor.js into module/actor/rolls-spell-mixin.mjs, following the established mixin pattern (active-effects / derived-stats / roll-data). DCCActor now composes RollsSpellMixin into its extends chain; every method stays an instance method with byte-identical behavior and `this` semantics (this.rollDisapproval / getRollData / classId still resolve up the prototype chain). Also extract the shared applyForceCritToFoundryRoll free function (used by both the skill and spell dispatchers) into module/actor/force-crit.mjs. actor.js 3999 -> 2982. Behaviour-neutral: all 1668 existing Vitest pass unchanged (proving transparent composition) + 5 new shape/behaviour guards in actor-rolls-spell-mixin.test.js. Note: this intentionally reverses the 2026-06-08 backlog note that no further actor.js file-shrinkage was warranted — per owner direction the dispatchers are being extracted for readability; docs updated separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 7 actor.js shrinkage, continued. Lift the weapon attack/damage/ crit/fumble dispatch layer (public `rollWeaponAttack` + `rollCritical` wrappers plus `_rollWeaponAttackDispatch`, `rollToHit`, `_rollDamage`, `_buildLibDamageResult`, `_structureDamageInput`, `_rollCritical`, `_rollFumble`, ~984 lines) out of actor.js into module/actor/rolls-weapon-mixin.mjs. DCCActor composes RollsWeaponMixin into its extends chain; behaviour and `this` semantics unchanged. actor.js 2982 -> 1989. Behaviour-neutral: all existing Vitest pass unchanged + 2 new shape guards in actor-rolls-weapon-mixin.test.js (imports the foundry mock first since the mixin destructures TextEditor at module load). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mixin Phase 7 actor.js shrinkage, continued. Lift the ability/luck/initiative/ hit-dice/saving-throw dispatch layer (public rollAbilityCheck, getInitiativeRoll, rollInit, rollHitDice, rollSavingThrow + their private adapters/dialog helpers, ~777 lines) out of actor.js into module/actor/rolls-check-mixin.mjs. DCCActor composes RollsCheckMixin into its extends chain; behaviour and `this` semantics unchanged (this._stripDieCount resolves to the skill block, always co-composed). actor.js 1989 -> 1212. Behaviour-neutral: all existing Vitest pass unchanged + 2 new shape guards in actor-rolls-check-mixin.test.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 7 actor.js shrinkage, continued. Lift the skill-check dispatch layer (public rollSkillCheck + its 8 private resolvers/adapters/builders, ~624 lines) out of actor.js into module/actor/rolls-skill-mixin.mjs. DCCActor composes RollsSkillMixin into its extends chain; behaviour and `this` semantics unchanged. _stripDieCount lives here but is also reached by the check mixin's initiative path via this._stripDieCount (both mixins are always co-composed on DCCActor). actor.js 1212 -> 583 (3999 -> 583 across the five roll-dispatch slices). What remains is the document lifecycle (prepareBaseData/prepareDerivedData/ _getConfig/levelChange/computeSpeedValue), rollLuckDie, and the damage/ disapproval resolution methods. Behaviour-neutral: all existing Vitest pass unchanged + 2 new shape guards in actor-rolls-skill-mixin.test.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…probe Reverse the 2026-06-08 "no further actor.js file-shrinkage warranted" note now that the five roll-dispatch mixins have landed: - 02-slice-backlog.md: Appendix-A note updated with the extraction summary - ARCHITECTURE_REIMAGINED.md: Appendix A actor.js target (575 achieved) + §8.6 clarification that "stays on DCCActor" is about the public method surface, not the source file (wrappers live in their mixin files) - 00-progress.md: Recent-slices entry for the extraction Add an e2e probe (adapter-dispatch.spec.js) asserting a live DCCActor carries every extracted roll dispatcher, so a future mis-wired `extends` chain localizes to this test instead of failing diffusely across the suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add @vitest/coverage-v8 and a `test:coverage` script (vitest run --project unit --coverage) that measures the system runtime source and enforces ratchet-floor thresholds in vitest.config.js: statements 60 / branches 60 / functions 63 / lines 60 — set just below the 2026-06-09 unit baseline (60.5 / 60.65 / 64.22 / 60.95) so coverage can only hold or climb. Scoped to the unit project so the gate is deterministic with or without Foundry (the integration project only adds coverage). The default `npm test` stays coverage-free (fast pre-commit hook); coverage is opt-in and is the natural CI gate. Reports → coverage/ (gitignored). Docs: docs/dev/TESTING.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigated item #2 (the "two-pass scaffolding"). It is NOT redundant double-computation: Foundry rolls the dice once and the lib re-classifies those same naturals via a sequenced roller; warnIfDivergent (6 sites) is a single integer comparison that catches silent lib drift. Removing it would lose a cheap safety net for no real gain. So rather than remove, document WHEN to: add an exit criterion to the warnIfDivergent JSDoc (debug.mjs) — delete the calls once dcc-core-lib is version-pinned and has shipped >=2 consecutive vendor syncs with zero divergence warnings; the two-pass itself stays (it is the adapter design, not scaffolding). Bidirectional cross-reference added to ARCHITECTURE_REIMAGINED.md §8.6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate the Hooks.once('init', …) body out of module/dcc.js into a focused
module/init-hook.mjs, split into named bootstrap steps (registerBuiltInRegistries,
registerDocumentConfig, registerDataModels, assembleGameDccNamespace,
registerSheets, loadSystemTemplates, registerEarlySettings) wired via
registerInitHook(). Matches the established register*() + named-handler
extraction pattern (settings-table-hooks.mjs, chat-and-hook-wiring.mjs).
dcc.js now calls registerInitHook(); foundry.* destructures moved into the
functions that use them (behaviour-neutral — only run during init).
- Vitest: module/__tests__/init-hook.test.js (17 cases) covering each step.
- E2E: extension-api.spec.js probe asserting all init-time side effects
(document classes, V14 AE phases + diceChain change type, data models,
game.dcc stable surface, sheet registration, early Fleeting Luck setting)
survive against live Foundry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k.mjs
Relocate the Hooks.once('ready', …) body plus checkReleaseNotes /
_onShowJournal / _onShowURI out of module/dcc.js into module/ready-hook.mjs,
wired via registerReadyHook(). checkReleaseNotes is exported so its
version-flag branch is unit-testable. Matches the init-hook.mjs extraction
pattern. dcc.js now calls registerReadyHook() and drops the imports only the
ready hook used (migrations, release-notes, key-state, status-icons, settings,
welcomeDialog, table-loading's registerTables/setupCoreBookCompendiumLinks,
foundry-data-loader).
- Vitest: module/__tests__/ready-hook.test.js (13 cases) covering
checkReleaseNotes branches, the onReady bootstrap steps (settings, KeyState,
Fleeting Luck / Spell Duel / table / status-icon init), theme body-class
branches, welcome-dialog gating, loader-failure tolerance, and the
migration-status threading onto dcc.ready.
- E2E: extension-api.spec.js probe asserting the ready-time side effects
(world settings, KeyState, theme body classes) survive against live Foundry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to scene-control-hooks.mjs
Relocate the getSceneControlButtons handler out of module/dcc.js into
module/scene-control-hooks.mjs, wired via registerSceneControlHooks(). Adds the
token-layer Spell Duel (always) + Fleeting Luck (setting-gated) scene-control
buttons. Completes the Phase 7 dcc.js decomposition: dcc.js is now a thin
orchestrator of seven register*() calls with no inline hook bodies.
- Vitest: module/__tests__/scene-control-hooks.test.js (7 cases) covering the
always-Spell-Duel / setting-gated-Fleeting-Luck branches, both onChange
delegations, and the setting-read try/catch swallow.
- E2E: extension-api.spec.js probe drives the registered hook via
Hooks.callAll('getSceneControlButtons', controls) and asserts the tools +
onChange delegations against live Foundry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… rotate archive - 00-progress.md: new Recent-slices entry (sessions 52-54, dcc.js 470 → 87), Current-phase note that the Appendix-A entry-point split is done; rotate sessions 48-49 down into the phase-7 archive to restore the 5-entry cap. - ARCHITECTURE_REIMAGINED.md: Appendix A dcc.js row + §5.3 line updated to "achieved 2026-06-09, 87 lines". - 01-session-start.md: fresh 2026-06-09 status block; note git's authoritative session numbering (52-54 = this batch) vs the older doc-only "session 52" label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves 15 conflicted files. The branch forked main at v0.67.2 and independently re-implemented several later PRs in restructured (Phase 0–7 decomposed) form, so most conflicts are rewrite-vs-small-edit artifacts resolved take-HEAD. Substantive integrations: - #732 spell-check extension seams (the only genuinely-missing logic): re-homed main's dcc.js/item.js processSpellCheck edits into their decomposed homes — * module/spell-check-processor.mjs: suppressPatronTaint opt-out flag, forceFumble (ctrl/meta+shift) force block, and the dcc.afterSpellCheckResult post-result hook (full payload incl. spellburn). * module/item/spell-mixin.mjs: spellburnTotal capture in the Spellburn term callback + threads forceCrit/forceFumble/ suppressPatronTaint/spellburn into processSpellCheck. * module/actor/force-crit.mjs: extended the shared force-crit helper to also force fumbles (covers the naked-actor + skill adapter paths symmetrically). NOTE: the seams fire for ITEM spell-checks (the path MCC consumes). The branch routes NAKED-actor casts through the lib's own patron-taint pipeline (onPatronTaint), which does not emit afterSpellCheckResult — flagged as a follow-up design decision, not wired here. - #743 de.json: applied main's German terminology fix (Zauberwürfe / Zauberbrand) while keeping the branch's new Spellburn-dialog keys, aligning their wording to the fix. - #742 NPC skill action die, #736 token.* AE overrides, #739 Speed Modifier + base-speed seeding migration: already present on the branch in decomposed homes; verified, conflicts resolved take-HEAD. Adapted main's #742 createRoll-based test to the branch's adapter architecture (_resolveSkill + _buildSkillCheckRollTerms). - #733/#738 SCSS (multidigit AC/HP, portrait letterbox, party-name): already present verbatim in the branch's styles/_*.scss partials; dcc.scss kept as the branch's partial-manifest; dcc.css regenerated. - lang ordering, speed-compute.test.js add/add: unioned. - package.json/system.json/latest.json/version.txt: took main's 0.67.7. 1738 Vitest green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aint on adapter spell paths Completes the #732 spell-check-seam integration for the branch's adapter architecture. The item path (DCCItem.rollSpellCheck → game.dcc.processSpellCheck) already fires the seam after the merge re-homing; this extends it to the adapter cast paths that bypass processSpellCheck: - New shared emitter module/actor/spell-result-hook.mjs (emitAfterSpellCheckResult + sumSpellburn). Normalizes the lib SpellCheckResult to the processSpellCheck payload contract so one listener works for both paths. `result` and `patronTaint` are null on the adapter path (the lib classifies tiers internally and routes patron taint via the onPatronTaint event); listeners key off `naturalRoll`. - Fired from all three adapter terminals in rolls-spell-mixin.mjs: _castNakedViaAdapter (naked casts — the primary gap), _castViaCastSpell (generic), and _castViaCalculateSpellCheck (wizard/cleric/elf). - suppressPatronTaint honored in _castViaCalculateSpellCheck by clearing input.isPatronSpell, which collapses the patron-taint table load + d100/d6 pre-rolls so the lib's RAW creeping-chance runs no taint sub-roll — mirroring the legacy `!suppressPatronTaint` guard. (Naked/generic paths carry no patron taint, so the flag is moot there.) Tests: new module/__tests__/spell-result-hook.test.js (payload contract + sumSpellburn); extends adapter-spell-check.test.js with end-to-end assertions that the naked path fires the hook, surfaces spellburn, and that suppressPatronTaint:true skips the patronTaintChance bump. 1747 Vitest green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-core-lib-adapter Conflict/adaptation notes: - system.json: kept refactor-branch 0.70.0 version + pre-release manifest URLs - .gitignore: dropped duplicate coverage/test-results entries (branch already had them) - chat.test.js: unavailable-table test adapted to this branch's crit-lookup-hint contract (main shows DCC.CritTableUnavailable; this branch shows a manual lookup hint instead) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/ PR #720 endgame Brings the handoff docs current through 2026-06-12: - 00-progress.md: rotate sessions 50-51 + the unnumbered latent-items slice to the phase-7 archive (relabeled — git's authoritative 52-54 = the dcc.js batch); add Recent-slices entries for the 2026-06-09 coverage backfill + tooling batch (previously unrecorded), the 2026-06-10 #732 seam completion + v0.67.7 merge + 0.70.0 pin, and the 2026-06-12 v0.67.8 merge (#744 crit fix). Current phase rewritten: refactor COMPLETE, branch in pre-merge holding, 1752 Vitest / 205 E2E clean. Next steps replaced with the sequenced PR #720 endgame (review pass -> pre-release -> sibling PRs -> squash-merge). - 01-session-start.md: new 2026-06-12 status block; fix the nvm instruction (this machine uses fnm, Node 24 default). - 02-slice-backlog.md: active queue marked DRAINED with pointer to the endgame list. - TESTING.md: e2e recipe no longer says 'nvm use 24' (no nvm here). - phase-7.md: receive the three rotated entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Long-running refactor branch that moves DCC rules logic out of the Foundry system (
module/actor.jsand friends) into@moonloch/dcc-core-lib, accessed through a thin Foundry-side adapter (module/adapter/*.mjs). Foundry keeps dice evaluation, chat rendering, and hooks; the lib owns rules classification, modifier aggregation, and result shape. This PR now contains the entire Phase 0–7 arc (the original 0–3 scope plus everything that previously "would land on follow-up branches"), squash-merged ontomain. Seedocs/00-progress.mdfor the authoritative per-session log anddocs/dev/ARCHITECTURE_REIMAGINED.mdfor the target design.Branch scope: 229 commits / 596 files vs
main. Mergesmainup to v0.67.7.Phases 0–3 — adapter dispatch for the core roll surfaces
dcc-core-libundermodule/vendor/dcc-core-lib/+scripts/sync-core-lib.mjsfor version-pinned re-syncs. Adapter seam undermodule/adapter/with permanent dispatch-logging (debug.mjs/logDispatch).rollAbilityCheck,rollSavingThrow,rollSkillCheck, initiative route through the adapter.rollSpellCheckroutes throughcalculateSpellCheckfor generic / wizard spell-loss / cleric disapproval / patron-bound wizard-elf / spellburn + mercurial flows. Patron taint is now RAW-aligned in the lib (D3a/D3b).rollToHit+ damage + crit + fumble route through the adapter for every common path (backstab, deed dice, two-weapon, modifier dialog, NPC adjustments, multi-damage-type + bracket-flavor formulas, magic bonuses, lossless passthrough).dcc.modifyAttackRollTermsbridged into the adapter.Group B/C/D — extension API, cruft retirement, legacy decommission
_xxxLegacyroll body retired; every publicrollXxxdispatcher is now single-path through the adapter, with Playwright regression guards asserting the retired methods are gone from the prototype.dcc.registerItemSheet/dcc.registerActorSheet/dcc.afterComputeSpellCheckextension hooks.critText/fumbleTextshims (C1), pruned pre-V14 migrations + addedMINIMUM_SUPPORTED_VERSIONguard (C2), closed the halfling i18n-localize anti-pattern with a guard test (C3).Phases 4–6 — class-registration / extension API
The class-registration API (
classMixins/classDefaults/sheetParts/variants) was validated end-to-end by migrating two real sibling content modules onto it —dcc-crawl-classes(PR #40) andmcc-classes(PR #38) — the first homebrew-vertical consumers. The §2.1 schema-slimming question was closed as architecturally bounded (Foundry's static one-schema-per-subtype model blocks full per-class field removal; the lib is the class-clean read-side source of truth) — decision record indocs/dev/SCHEMA_SLIMMING.md.Phase 7 + Appendix-A — god-object decomposition
Every Appendix-A god-object target had its cohesive non-dispatch groups lifted into focused modules, all behaviour-neutral:
config.js845 → 451 — data tables →module/config/{monster-data,images,dice,active-effect-keys,actor-importer}.mjs.item.js975 → 339 —DCCItem extends SpellItemMixin(CurrencyItemMixin(ContainerItemMixin(Item))).actor-sheet.js1890 → ~1040 — AE builders /prepareItems/ presentation helpers / drag-drop / drop handlers →module/actor-sheet/*.mjs(free functions; sheets can't mixin#private).actor.js4574 → 3999 —RollDataMixin(DerivedStatsMixin(ActiveEffectsMixin(Actor)))+ spell/skill dispatch mixins; what remains is the adapter dispatch layer that stays co-located with the publicrollXxxwrappers.dcc.js470 → 87 — a pure orchestrator of sevenregister*()modules (init / ready / scene-control hooks, settings, table loading, chat-and-hook wiring).mainmerge (v0.67.7)Integrates everything that landed on
mainafter the branch's v0.67.2 fork. Most was independently re-implemented in restructured form (resolved take-HEAD); the substantive integrations:suppressPatronTaintopt-out,forceFumble(ctrl/meta+shift), and thedcc.afterSpellCheckResultpost-result hook (withspellburnin the payload). Fires for both the legacy item path (processSpellCheck) and the adapter cast paths (newmodule/actor/spell-result-hook.mjs, fired from all three adapter terminals;suppressPatronTainthonored by skipping the lib's patron-taint pipeline). For variant modules (e.g. MCC) that key off spell-check outcomes.token.*AE overrides, fix(sheets): shrink AC/HP/save font for multi-digit values so they fit #733/fix(sheets): portrait aspect ratio, party rename (#735), AE token overrides (#736) #738 multi-digit AC/HP font + portrait/party-sheet polish, Update de.json #743 de.json i18n fixes.Test coverage (added, not replaced)
adapter-*), plus guard specs (migrations-version-gate-guard,class-dispatch-i18n-guard) and the newspell-result-hook/spell-check-hooksseam coverage.adapter-dispatch,active-effects,data-models,extension-api,sheet-ui) runs green against live v14 Foundry.npm run compare-langclean across all 7 languages.Test plan
npm test— 1747 Vitest passbrowser-tests/e2e/Playwright suite — green against live v14 Foundrynpm run compare-lang— 0 missing / 0 extra across 7 languages../../modules/dcc-qol,../../modules/xcc,../../mcc-classes,../../dcc-crawl-classes/release(branch now atmain's 0.67.7 base)🤖 Generated with Claude Code