Constructs a combat result from an attack/defense pair, seeding the margin, Tactical Advantages, and weapon-break check to their initial (unresolved) values.
Must include both attackResult and defendResult; the remaining OpposedTestResult.Data fields are optional.
Result options; options.parent (the initiating Logic)
is required by the base TestResult constructor.
OptionalattackerImpact rolled for the attacker, when it lands a blow (else undefined).
Whether a tie should be broken (see resolveTieBreak) rather than reported as a tie.
OptionalcxImpact rolled for the counterstriker, when they land a blow — only possible on a
Counterstrike (else undefined).
Victory score: attacker.normSuccessLevel − defender.normSuccessLevel.
Foundry roll mode for chat output.
The initiating (source) actor's success test.
Tactical Advantages awarded by the exchange (display-only).
The responding (target) actor's success test.
Which side a tied contest is awarded to (an
OPPOSED_TEST_RESULT_TIEBREAK value) — NONE while the tie stands.
Set by resolveTieBreak, or supplied up front by a rule that dictates
the victor.
Which rule settled a broken tie, for the card to report; empty while the tie stands.
Whose weapon must roll for breakage as a result of the exchange.
Whether the attacker lands a blow — i.e. connects, so its impact may then be calculated. Derived from the resolved exchange. Note a landed blow can still be fully absorbed by armor downstream, so this means "connected", not "dealt damage".
margin >= 0).margin >= 0).The result of the attack test — aliases sourceTestResult.
Whether neither side succeeded.
Whether the defender lands a blow. Only a counterstrike lets the defender strike back (its response is an AttackResult), and only when its own roll succeeds — so both sides can land in the same exchange.
The defender's response — aliases targetTestResult. Its class encodes the kind of defense chosen (block/dodge → DefendResult; counterstrike → AttackResult).
Human-readable description shown on the result's chat card.
Whether this contest was a tie that the tie-break rule then settled — the
two sides reached the same success level, but tieBreak awarded the
contest to one of them (see resolveTieBreak).
Whether both sides reached the same success level (and at least one succeeded — cf. bothFail).
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.
Internal identifier for this result (distinct from the display title).
Whether the source prevails — its success level exceeds the target's, or a tie was broken its way (and not bothFail).
Speaker identity (actor/token/user) used when posting this result to chat.
Whether the target prevails — its success level exceeds the source's, or a tie was broken its way (and not bothFail).
The active tie-break offset — tieBreak unless both sides failed, in which case 0.
Title shown at the top of the result's chat card.
victoryStars as one entry per star for the card to draw — true
where the star is the tester's (drawn filled) and false where it is
the target's (drawn hollow), so a glance at the line says who won as
well as by how much. Empty when nobody won.
Victory Stars — how decisively the contest was won: one star per step between the two success levels, or exactly one for a tie settled by the tie-break rule.
The margin has no ceiling. It is measured on the raw (unclamped) levels, so a modifier that pushes a level past the four-point scale widens the margin with it — a Marginal Success against a Critical Failure worsened by −1 is three stars, not two. An unbroken tie and a mutual failure are both worth none.
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.
Evaluate the defender's side locally, then resolve the opposed combat outcome.
Unlike the inherited OpposedTestResult.evaluate, this does NOT
evaluate the attacker's side. The attackResult arrives as a read-only
snapshot already evaluated on the attacker's client; re-evaluating it
here would trip the attacker's _speaker.isOwner gate on the defender's
machine. Only defendResult (owned by the local user) is evaluated; the
attacker's outcome is read as-is by opposedTestEvaluate.
true if the defender's evaluation was allowed (and the exchange resolved), false otherwise.
Resolve the exchange: compute the margin, Tactical Advantages, and the weapon-break check. Who lands a blow is derived on demand by attackerLandsBlow/defenderLandsBlow. Idempotent — safe to call again after a re-evaluation.
Post the opposed-test card via the source's speaker — the request card
(opposed-request-card.hbs, with the Respond button) by default, or the
result card (opposed-result-card.hbs) when the caller supplies that
template (as sohl.entity.modifier.MasteryLevelModifier.opposedTestResume does).
Both sides are shaped into plain sourceTestResult / targetTestResult
data (title, token, item, mlMod display fields, roll, outcome flags) rather
than the live results, because the delegated
sohl.entity.result.SuccessTestResult.toChat folds this through
fvttMergeObject, which deep-copies and would strip a live instance's
getters. That delegation also honors the caller's template now (#845), so
the opposed card is no longer overridden by the standard test card.
Extra template data; template / title select and label
the card (request vs. result).
Serialize to a plain object satisfying OpposedTestResult.Data: the inherited TestResult fields plus both contestants' success tests and the tie-break configuration.
The plain-object representation.
StatictacticalAward |VS| − 1 Tactical Advantages to the side that won by 2+.
The victory spread (attacker margin minus defender margin).
The Tactical Advantages owed, attributed to the winning side.
The full result of a combat exchange — an OpposedTestResult composing an AttackResult and a DefendResult.
CombatResult is the culmination of the combat resolution pipeline:
What CombatResult determines
What CombatResult does NOT determine
The final damage is computed separately by the impact resolution stage, which takes the CombatResult's margin, the AttackResult's impact dice, and the target's armor/body location protection to produce the actual injury.
Resolution model
Outcomes are keyed off the victory score
VS = attacker.normSuccessLevel − defender.normSuccessLevel(range −3..+3 on the −1/0/1/2 success scale). This is deliberately the raw level difference, not the inherited OpposedTestResult.sourceWins/OpposedTestResult.isTied getters — those carve out a "both failed" case, whereas the SoHL combat tables resolve every exchange by relative margin (a less-bad failure can still beat a worse one).Per-defense outcome (who lands the blow):
VS >= 0(tie → also rolls defender weapon-break)VS >= 0VS > 0, or tie with the dodge roll lower than the attack rollTactical Advantages (display-only for now): the winner of a
|VS| >= 2exchange earns|VS| − 1TAs (attacker onVS >= 2, defender onVS <= -2).Specialized resolution methods