The full result of a combat exchange — an OpposedTestResult composing an AttackResult and a DefendResult.

CombatResult is the culmination of the combat resolution pipeline:

  1. Attacker rolls → AttackResult (success level, pre-defense damage, allowed defenses).
  2. Defender chooses a defense type and rolls → DefendResult.
  3. CombatResult compares the two via opposed test resolution (inherited from OpposedTestResult) to determine who prevails.
  • Whether the attacker or defender wins the exchange (sourceWins / targetWins, inherited).
  • The margin of victory (difference in success levels).
  • Mishaps on either side (weapon break, shield break, stumble, fumble).

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.

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):

Defense Attacker delivers Defender delivers
Block VS >= 0 (tie → also rolls defender weapon-break) never
Counterstrike VS >= 0 whenever the defender succeeds
Dodge VS > 0, or tie with the dodge roll lower than the attack roll never
Ignore the attack itself succeeds (no defender contest) never

Tactical Advantages (display-only for now): the winner of a |VS| >= 2 exchange earns |VS| − 1 TAs (attacker on VS >= 2, defender on VS <= -2).

Hierarchy (View Summary)

Constructors

Properties

attackerImpact?: sohl.entity.result.ImpactResult

Impact rolled for the attacker, when it lands a blow (else undefined).

breakTies: boolean

Whether a tie should be broken (see resolveTieBreak) rather than reported as a tie.

Impact rolled for the counterstriker, when they land a blow — only possible on a Counterstrike (else undefined).

margin: number

Victory score: attacker.normSuccessLevel − defender.normSuccessLevel.

rollMode: string

Foundry roll mode for chat output.

The initiating (source) actor's success test.

tacticalAdvantages: TacticalAdvantages

Tactical Advantages awarded by the exchange (display-only).

The responding (target) actor's success test.

tieBreak: number

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.

tieBreakReason: TieBreakReason

Which rule settled a broken tie, for the card to report; empty while the tie stands.

weaponBreakCheck: CombatSide

Whose weapon must roll for breakage as a result of the exchange.

Accessors

  • get attackerLandsBlow(): boolean
  • 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".

    • Counterstrike (defender side is an AttackResult): the attack ties or wins (margin >= 0).
    • Ignore: the unopposed attack simply has to succeed.
    • Dodge: the attack out-margins the dodge, or ties with a lower dodge roll than the attack roll (a lower successful roll is the weaker result).
    • Block: the attack ties or wins (margin >= 0).

    Returns boolean

  • get defenderLandsBlow(): boolean
  • 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.

    Returns boolean

  • get description(): string
  • Human-readable description shown on the result's chat card.

    Returns string

  • get isTieBroken(): boolean
  • 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).

    Returns boolean

  • get kind(): string
  • 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.

    Returns string

  • get name(): string
  • Internal identifier for this result (distinct from the display title).

    Returns string

  • get parent(): SohlLogic<any>
  • The Logic that owns this entity. Always present (the constructor rejects a missing parent) and transient — it is not serialized and is re-supplied when the entity is revived or cloned.

    Returns SohlLogic<any>

  • get sourceWins(): boolean
  • Whether the source prevails — its success level exceeds the target's, or a tie was broken its way (and not bothFail).

    Returns boolean

  • get targetWins(): boolean
  • Whether the target prevails — its success level exceeds the source's, or a tie was broken its way (and not bothFail).

    Returns boolean

  • get victoryStarMarks(): boolean[]
  • 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.

    Returns boolean[]

    Marks, not markup: the card turns each entry into a Font Awesome star (fa-solid / fa-regular), matching how the sheets already draw a filled or hollow flag. Building the <i> elements here would put HTML in the Foundry-free layer for no gain.

  • get victoryStars(): number
  • 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.

    Returns number

    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.

Methods

  • 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.

    Returns Promise<boolean>

    true if the defender's evaluation was allowed (and the exchange resolved), false otherwise.

  • 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.

    Parameters

    • data: PlainObject = {}

      Extra template data; template / title select and label the card (request vs. result).

    Returns Promise<void>