Builds an attack result, defaulting the impact modifier, aimed body part, and strike spread when not supplied.
Attack data; impact, aimBodyPartCode, and spread
default to an empty ImpactModifier, "", and 0 respectively.
Result options; options.parent is required by the base
TestResult constructor.
The attacking combatant's logic, resolved in the constructor from the
persisted data.combatantUuid (a uuid on the wire, the live logic in
memory).
The impact (damage) formula/capability for this attack, e.g. 2d6+5
edged. Not rolled here — CombatResult produces an ImpactResult
from it only when the blow lands. evaluate disables it on a miss.
The label for the weapon or form of attack result (shown on card).
The live strike mode for this attack (pointer-on-wire, live-in-memory).
undefined when the weapon is not present on the current client (e.g.
the defending client during cross-client combat resolution).
Foundry roll mode (public / private GM / blind / self) used when posting to chat.
The body part shortcode this attack aims at (empty when unaimed). Read-through to impact — the single source of truth.
Context-menu responses available as follow-ups to this result — e.g. resuming an opposed test when this is the opening roll.
Whether a Fate Point may be spent on this test — true only when the owning
item has an eligible, charged Fate Mystery (availableFate) and the test
permits it. Fate is a post-roll success-level bump, never a re-roll: a
spend raises this result's stored successLevel (#854).
Whether criticals are possible — i.e. the modifier defines any critical success or failure digits.
Human-readable description shown on the result's chat card.
The shortcode of the skill an attack's Fate is rolled against — the
melee skill behind this strike mode (its assocSkillCode), not the weapon
item itself. A caller resolves it against the actor (e.g. with
sohl.document.item.logic.resolveAssocSkill) to reach that skill's
fateMasteryLevel and availableFate, then runs its fateTest with this
result as context.scope.priorTestResult (#854). null when the mode
names no skill (an untrained/innate strike).
Whether the effective mastery level was constrained (capped) below its raw effective value.
Whether this result is a critical (success or failure). Always false when
critAllowed is false — except a forced auto-Critical-Failure
(#568), which is always critical.
Whether the test succeeded (success level at marginal success or better).
Whether this is a Success Value test (#848) — its roll is graded into a Success Value and Success Stars rather than a plain pass/fail. Drives the card's Success Value / Success Stars rows.
The item logic this test was rolled from (its skill/attribute/weapon).
The serialization discriminator for this instance — the concrete class's static Kind. Written into the JSON by toJSON under the kind key and read back by sohl.utils.defaultFromJSON to select the constructor. Derived from the class, never stored per-instance.
The ones digit of the roll total, tested against the modifier's critical digit lists.
The mastery-level modifier rolled against; its constrainedEffective value is the roll-under target for this test.
Set of mishap codes flagged for this result (e.g. fumble, stumble); lazily initialized.
Tactical movement state recorded for this test (stationary, etc.).
Internal identifier for this result (distinct from the display title).
Success level normalized to the canonical four-point scale (−1/0/1/2) from isSuccess and isCritical. Opposed and combat resolution compare two results by this value.
Success level before the four-point clamp — the stored level with every
successLevelMod folded in, so it can sit outside −1…2 (a Critical Failure
pushed down by −1 reads −2).
Opposed resolution compares this rather than successLevel because a contest's victory margin has no ceiling: each step between the two levels is one Victory Star, and a modifier that shifts a level widens the margin accordingly. Everything that asks "did it succeed, and how well?" wants the clamped successLevel / normSuccessLevel instead.
Longer result description for the chat card, derived on read from the description table (empty when no table is supplied). Never stored — see successStars.
Short result label for the chat card, derived on read from the description table (empty when no table is supplied). Never stored — see successStars.
The d100 SimpleRoll. May be pre-seeded before evaluate (e.g. for fate or a deterministic outcome).
The player-entered situational modifier from the attack dialog.
Speaker identity (actor/token/user) used when posting this result to chat.
Strike spread governing hit-location scatter from aimBodyPartCode. Read-through to impact — the single source of truth.
Success level clamped to the four-point scale: critical failure (−1),
marginal failure (0), marginal success (1), or critical success (2). The
raw internal level (which successLevelMod can push beyond this range) is
normalized here.
Number of success "stars" (quality grade), derived on read from the description table. Never stored (issue #205) — recomputed from the table plus the evaluated success level / target value / roll last-digit.
The test's target value — targetValueFunc(successLevel). For a plain
success test this is just the success level; success-value tests map it to
a quality/quantity outcome used to index the
description table.
Which kind of test this is — a TEST_TYPE id (e.g. success test, attack, block).
Title shown at the top of the result's chat card.
The token this test is associated with, if any.
Raise this result's stored success level by delta — the post-roll Fate
bump (#854). This mutates the already-settled outcome: it does not
re-roll and does not re-evaluate. Because the outcome text/stars are
derived on read (see resultText / successStars), re-posting
the card after a bump re-resolves the description table against the new
level automatically.
Fate is defined as successLevel += delta on the original result's stored
level; the successLevel getter re-clamps to the four-point scale on
read (e.g. a marginal failure bumped by +2 reads as a critical success).
Success levels to add (Fate contributes +1 or +2).
This result, for chaining.
Deep-copy this entity, re-parenting the copy under parent with no other
changes. Shorthand for clone({}, { parent }).
The Logic to own the cloned entity.
The cloned entity.
Deep-copy this entity, optionally overriding fields and clone options.
Field overrides applied to the clone.
Clone options (e.g. a new parent).
The cloned entity.
Re-open the standard test dialog on this already-settled result, pre-filled with its current situational and success-level modifiers, and fold the submitted values back into masteryLevelModifier.
How to collect the new modifiers; see SuccessTestResult.ModifierEditOptions.
{ changed } — whether either modifier actually moved — or
undefined when the dialog was dismissed, which cancels the edit.
This is the shared core of the GM result-edit: the single-test pencil (sohl.document.item.logic.SohlItemBaseLogic.resultEdit, #856) and the opposed-contest pencil (sohl.document.actor.logic.SohlActorBaseLogic.opposedResultEdit, #1082) both fold their sides through it. It never rolls — the die stays frozen and the caller re-evaluates on it.
A situational modifier of 0 removes the delta rather than recording a
zero, so an edited target is never left carrying a stale modifier.
Roll the attack and apply attack-specific outcomes on top of the base SuccessTestResult.evaluate.
false if the base evaluation disallows the result; otherwise
true.
On a failed roll this flags mishaps from a critical failure — for melee, fumble (last digit 0) or stumble (last digit 5); for missile, fumble (0) or misfire (5) — and disables impact ("Attack missed"). Impact is never rolled here; that happens in CombatResult when the blow lands.
Extend the base test dialog with an impact situational modifier: the
value entered in the dialog is added to impact (as a PLAYER
delta) before the supplied callback is chained.
Base dialog data; this override injects impact.
Invoked with the submitted form data after the impact modifier is applied.
The dialog result from the base SuccessTestResult.testDialog.
Include this attack's impact modifier in the chat-card data.
Base chat-card data to merge the impact modifier into.
The rendered chat data from the base SuccessTestResult.toChat.
Serialize to a plain object satisfying AttackResult.Data: the inherited SuccessTestResult fields plus the combatant reference, strike-mode pointer, impact modifier, aim, spread, and label.
The plain-object representation.
The combatant is persisted by combatantUuid (resolved back in the
constructor). situationalModifier is deliberately not emitted — it was
folded into masteryLevelModifier as a PLAYER delta at
construction and is already carried by that modifier's serialized deltas;
re-emitting it would double-apply on revival.
The attacker's side of a combat exchange — a SuccessTestResult with attack-specific data (the impact formula and the targeted body part).
Key properties
ImpactResultfrom this modifier (the roll happens then). Final damage is determined downstream by impact resolution against the target's armor/body location.Evaluation
evaluate performs the attack roll, determines success/failure, and checks for attack-specific mishaps (stumble, fumble, missile misfire). On a self-miss it disables impact; it does not roll impact (that is done when the blow lands).