Initial mutable downs snapshot, same shape as
system.bms.downs.toObject() (an array of { downNumber, actions, effects } plain objects, where actions/effects are objects keyed
by each entry's own id). Stored by reference and mutated in place by
this pass's own methods and by external callers that read
ResolvePass#downs.
Register interest in this pass, incrementing its refcount. Call once per logical operation (a natural advance call, an instant commit-to-resolve span) that needs the pass to stay alive for its duration.
Capture the set of action/effect ids currently present in downs
as a baseline to later pass into mergeFromLive, replacing what
used to be locally-scoped originalActionIds/originalEffectIds
variables recomputed by each caller. Call this immediately before a
mutation phase begins (e.g. the rt-decrement/sweep loop in
_processCombatSteps, or right before an instant commit's push into
downs[0].actions) so the caller can later hand the returned baseline to
mergeFromLive to distinguish genuinely new entries (added by a
trigger outcome mid-pass) from ones that were already present when this
baseline was captured.
Deliberately returns the baseline instead of storing it on this:
because ResolvePass instances are shared per-combat across concurrent
callers (a natural advance and an in-flight instant resolve can attach to
the same pass — see the file doc comment), a singleton baseline field
would let a second caller's beginMutationPhase call silently
overwrite the first caller's baseline before the first caller's own
mergeFromLive runs, corrupting its new/moved/deleted
classification. Returning the baseline forces every caller to carry its
own copy through to its own mergeFromLive call, closing that
cross-talk window.
Baseline to
pass as the baseline argument of a later mergeFromLive call.
Claim an action id for the duration of a _resolveOneAction call, so a
second concurrent caller sees it as already claimed and skips it instead
of resolving it a second time.
Action id to claim.
Release interest in this pass, decrementing its refcount.
true if this was the last attached operation (refcount hit 0) and the caller should discard/deregister this pass; false if other operations are still attached and the pass must stay alive.
Drain (return and clear) the pending-swept ids accumulated by every
reentrant _mutateDowns call since the last flush, so the caller can
union them into its own sweptActionIds/sweptEffectIds before calling
mergeFromLive. Called from _flushResolvePass for every flush,
regardless of caller, since any flush writes the pass's full current
downs and is therefore the right place to finally reconcile a
reentrant deletion that skipped its own flush.
Find an action entry by id across every down in downs.
Action id to find.
The plain action object, or undefined if not found.
Action id to check.
true if id is currently claimed by any caller.
Action id to check.
true if id has completed resolution this pass
(always false for holds, which markResolved never records).
Mark an action as having genuinely completed resolution this pass — must
only be called after the real resolve work (_resolveOneAction) has
finished, never pre-emptively before it starts. No-ops entirely for holds:
hold-type actions re-fire every cycle while pinned at resolutionTime === 0 and must never transition to a "resolved" state, so this neither adds
the id to resolvedIds nor mutates its downs entry when
isHold is true.
Also mutates the matching entry's resolved field in downs (the
first matching action found across all downs), mirroring what the old
_resolveOnce wrote directly into its local downs snapshot — kept here
so downs and resolvedIds never disagree about an action's state.
Action id that finished resolving.
Optionaloptions: { isHold?: boolean } = {}
OptionalisHold?: booleanTrue when the resolved action's
type is "hold".
Reconcile downs with a fresh read of the live document's downs,
using the caller-supplied baseline (the value returned by that same
caller's own beginMutationPhase call). This is a direct,
logic-preserving port of the method body that used to live as
BodyMindSoulCombat#_mergePostResolveDowns in combat.mjs — same
three-step reconciliation, now operating on this.downs/baseline
instead of a caller-local downs/originalActionIds/originalEffectIds.
baseline is an explicit parameter rather than an instance field
specifically so two independent operations sharing one ResolvePass (see
the file doc comment) can never clobber each other's in-flight baseline —
each caller must thread through the exact object its own
beginMutationPhase call returned, not whatever the pass most
recently captured for someone else.
Three things this reconciles:
liveDowns.liveDowns than the snapshot's current bucket):
removed from its snapshot position and re-inserted at the live down,
using the pass's own current copy (preserves in-memory mutations like
resolved: true), never the live document's copy.liveDowns): removed from the snapshot so it isn't re-added by this
pass's own write.Idempotency guarantee — repeated flushes of the same pass against the
same baseline must never create a duplicate. A single swing resolution
can legitimately be flushed twice against one unchanged baseline: once by
the per-action flush inside _resolveOnce (right after markResolved),
and again by the caller's own end-of-cycle flush (e.g.
resolveCurrentDown()'s final flush). Between those two flushes, the
first flush's combat.update() write round-trips through
BodyMindSoulCombatModel.redistributeEntries() (called from
_preUpdate), which re-buckets every action by its current
resolutionTime — so a next-swing entry freshly queued into downs[0]
by system.resolve() (see module/data/item-action.mjs) is very likely
sitting on a different down in the live document by the second flush
than the down it still occupies in this pass's own in-memory snapshot.
Earlier versions of this method only ever repositioned entries already
present in baseline (the "moved originals" case), so a new entry's
stale in-memory copy was left in place and a second copy was merged in
from the live read at its now-correct down — two physical copies of the
same id. The move-detection below is therefore keyed on "does an entry
with this id exist in liveDowns at a different down than its current
snapshot bucket", with no dependency on whether the id is in baseline —
new-this-phase entries are repositioned exactly like original ones, so a
second flush against the same baseline can only ever converge an id onto
its one live-authoritative down, never duplicate it.
sweptActionIds/sweptEffectIds name entries that were deliberately
filtered out of downs this pass (rt decremented below 0) — their
liveDowns state is stale (pre-advance) and must NOT be re-added by the
merge-new-entries step, even though they're absent from the snapshot.
No explicit empty-array guard is needed for a 0-combatant combat or an
empty liveDowns — every loop/.filter/.find below degrades
gracefully on empty input.
A fresh plain-object downs array read
directly from the live document (e.g. combat.system.toObject().bms.downs).
Optionalbaseline: { actionIds?: Set<string>; effectIds?: Set<string> } = {}
The value returned by this same caller's own beginMutationPhase call. Missing sets default to empty (nothing treated as "original").
OptionalsweptActionIds: Set<string> = ...
Action ids swept out of downs
this pass (rt went negative) — excluded from re-merge.
OptionalsweptEffectIds: Set<string> = ...
Effect ids swept out of downs
this pass (rt went negative) — excluded from re-merge.
Record ids added/removed by a REENTRANT _mutateDowns call that skipped
its own flush. See the constructor's doc comment for why this exists.
Removing an id that was itself still pending-new (added and then removed
again, all before ever being flushed) simply forgets it — it was never
live, so there's nothing to sweep.
Optionaladded: { actionIds?: Set<string>; effectIds?: Set<string> } = {}
Ids
present after the reentrant call's mutatorFn ran that weren't present
in its own local pre-mutation baseline.
Optionalremoved: { actionIds?: Set<string>; effectIds?: Set<string> } = {}
Ids
present in the reentrant call's own local pre-mutation baseline that
are no longer present after mutatorFn ran.
Release a previously-claimed action id. Always call this in a finally
block paired with claim, regardless of success or failure of the
work done while claimed.
Action id to release.
Owns one combat's in-progress resolve-pass state: the mutable
downssnapshot every concurrent resolve caller reads/writes, which action ids are currently claimed (inside_resolveOneAction, by any caller), and which ids have already completed resolution this pass.Lifecycle: multiple independent operations (a natural advance, an instant commit-and-resolve) can be in flight for the same combat at once. Each one calls ResolvePass#attach when it starts needing the pass and ResolvePass#detach when it's done with it; the pass itself carries no more state once every attacher has detached, at which point the owning module-level registry (
_resolvePassesincombat.mjs) discards it. A freshResolvePassis created the next time any operation needs one for that combat id.