/**
 * The flat `Buff` type every contribution flows through, and the
 * {@link resolveBuffs} resolver that collapses a stack into a
 * {@link ResolvedModifiers} read-out the engine can consume.
 *
 * The same shape carries weapon-keyword effects, ability buffs, stratagem
 * effects, and manual UI toggles — reroll-stacking, hit/wound caps, and
 * feel-no-pain-best-threshold all fall out of one resolver rather than each
 * source kind reinventing precedence.
 *
 * @packageDocumentation
 */
import type { Phase } from "../generated.js";
/** Where a buff originated. Drives stable tie-breaking inside `resolveBuffs`. */
export type BuffSource = {
    kind: "weapon-keyword";
    weaponId: string;
    keywordId: string;
} | {
    kind: "ability";
    abilityId: string;
    abilityKind: "army" | "detachment" | "detachment-stratagem" | "unit" | "attached" | "support";
    /**
     * For `abilityKind: "attached"`, the combined-unit member the ability
     * came from (so the UI can name it and show its leader/bodyguard role).
     * Absent for other kinds.
     */
    sourceUnitId?: string;
} | {
    kind: "manual";
    label: string;
};
/** A weapon-keyword reference (id + parameter map), as found on weapon profiles. */
export type WeaponKeywordRef = {
    keyword_id: string;
    parameters?: Record<string, unknown>;
};
/** One typed contribution; the engine reads `ResolvedModifiers` for the rest. */
export type BuffContribution = {
    type: "hit-mod";
    value: number;
} | {
    type: "wound-mod";
    value: number;
} | {
    type: "save-mod";
    value: number;
} | {
    type: "cover";
} | {
    type: "reroll";
    roll: "hit" | "wound" | "save" | "damage";
    subset: "ones" | "all-failures";
} | {
    type: "extra-keyword";
    keywordRef: WeaponKeywordRef;
}
/**
 * Feel-no-pain: roll one D6 per unsaved wound at `threshold`+, ignoring the
 * wound on a pass. `scope` controls which wound stream it applies to:
 *  - `"all"` (default): every unsaved wound (main + mortal).
 *  - `"mortal"`: mortal-wound stream only (e.g. Death Guard 5+ FNP vs
 *    mortals). A target may carry both an all-FNP and a mortal-FNP; the
 *    engine rolls both against mortals.
 */
 | {
    type: "feel-no-pain";
    threshold: number;
    scope?: "all" | "mortal";
} | {
    type: "damage-mod";
    value: number;
}
/** Additive modifier to the attacker's per-model attack count (A stat). */
 | {
    type: "attacks-mod";
    value: number;
}
/** Additive modifier to the attacker's Strength stat. */
 | {
    type: "strength-mod";
    value: number;
}
/** Additive modifier to the defender's Toughness stat. */
 | {
    type: "toughness-mod";
    value: number;
}
/**
 * Additive modifier to the attacker's weapon AP. AP is signed against the
 * defender's save (negative = more piercing), so a value of `-1` here makes
 * the weapon one AP more piercing.
 */
 | {
    type: "ap-mod";
    value: number;
}
/**
 * Defender-side: subtract `value` from each unsaved damage point (floored at
 * 1 by the engine). Multiple sources do NOT stack in 10e — the largest
 * reduction wins. The corpus also encodes `"half"` and `"to-zero"`
 * reductions; the buff layer only models the additive form because the
 * other two are typically one-use ablation that doesn't fold into the
 * expected-value math cleanly.
 */
 | {
    type: "damage-reduction";
    value: number;
}
/**
 * Defender-side: ability-granted invulnerable save threshold (e.g. a buff
 * that grants a 4+ invuln). Best (lowest) threshold wins; the engine then
 * picks the better of `printed Sv after AP/cover` and `effective invuln`
 * (invuln bypasses both AP and cover).
 */
 | {
    type: "invulnerable-save";
    threshold: number;
};
/** Optional gating; the resolver drops buffs whose gate fails. */
export type BuffApplicability = {
    phases?: Phase[];
    rollType?: "hit" | "wound" | "save" | "damage";
    /** Target must carry this keyword (case-insensitive). */
    requiresTargetKeyword?: string;
    /** Attacker must carry this keyword (case-insensitive). */
    requiresAttackerKeyword?: string;
};
/** A single buff: where it came from, when it applies, what it contributes. */
export type Buff = {
    source: BuffSource;
    applicableWhen?: BuffApplicability;
    contribution: BuffContribution;
};
/**
 * Shared engine context. Carries the phase plus a few attacker/target flags
 * the keyword translator and the resolver both need. The engine fills it from
 * its `EngineInput.context` plus the unit-keyword unions; the resolver reads
 * only the subset relevant to its `applicableWhen` checks.
 */
export type EngineContext = {
    phase: Phase;
    /** Attacker has not moved this turn — Heavy fires its +1 to hit. */
    attackerStationary?: boolean;
    /**
     * Attacker made a charge move this turn — drives the `charged-this-turn`
     * condition (e.g. World Eaters' Relentless Rage). Left undefined when the
     * caller can't determine it — the condition then evaluates as `"unknown"` and
     * the SPA surfaces a diagnostic (mirrors `attackerStationary` / `timing`).
     */
    attackerCharged?: boolean;
    /** Within half the weapon's range — Melta / Rapid Fire fire. */
    withinHalfRange?: boolean;
    /** Attacker benefits from cover (mostly informational; cover applies to defenders). */
    attackerInCover?: boolean;
    /** Target is in cover — the resolver flips on `cover`, the engine applies +1 to save. */
    targetInCover?: boolean;
    /** Attacker keywords (union of unit.keywords + faction_keywords), lower-cased. */
    attackerKeywords?: ReadonlyArray<string>;
    /** Target keywords (union of unit.keywords + faction_keywords), lower-cased. */
    targetKeywords?: ReadonlyArray<string>;
    /**
     * Sub-phase timing flag (e.g. `"start-of-phase"`, `"end-of-phase"`,
     * `"on-destroyed"`). Consumed by the `timing-is` condition. Left undefined
     * when the caller can't pin a sub-phase down — the condition then evaluates
     * as `"unknown"` and the SPA surfaces a diagnostic.
     */
    timing?: string;
    /**
     * The buffed unit is part of a combined ("attached") unit — a leader is
     * attached to a bodyguard, or vice-versa. Drives the `is-attached` and
     * `model-is-leader` conditions. Derived from a non-empty
     * `EligibilityInput.attachedUnitIds`. Left undefined when the caller can't
     * determine attachment — the conditions then evaluate as `"unknown"` and the
     * SPA surfaces a diagnostic (mirrors how `timing` undefined behaves).
     */
    attackerAttached?: boolean;
};
/** Back-compat alias — `resolveBuffs` accepts the shared engine context. */
export type ResolveContext = EngineContext;
/** Read-out of a resolved buff stack, with provenance per field. */
export type ResolvedModifiers = {
    hitMod: {
        value: number;
        dominantSource: BuffSource | null;
    };
    woundMod: {
        value: number;
        dominantSource: BuffSource | null;
    };
    saveMod: {
        value: number;
        sources: BuffSource[];
    };
    cover: {
        active: boolean;
        source: BuffSource | null;
    };
    rerolls: Partial<Record<"hit" | "wound" | "save" | "damage", {
        subset: "ones" | "all-failures";
        dominantSource: BuffSource;
    }>>;
    extraKeywords: {
        keywordRef: WeaponKeywordRef;
        source: BuffSource;
    }[];
    /** All-wound FNP — fires on the main and mortal damage streams alike. */
    feelNoPain: {
        threshold: number;
        dominantSource: BuffSource;
    } | null;
    /** Mortal-only FNP — fires only on the mortal-wound damage stream. */
    feelNoPainMortal: {
        threshold: number;
        dominantSource: BuffSource;
    } | null;
    damageMod: {
        value: number;
        sources: BuffSource[];
    };
    attacksMod: {
        value: number;
        sources: BuffSource[];
    };
    strengthMod: {
        value: number;
        sources: BuffSource[];
    };
    toughnessMod: {
        value: number;
        sources: BuffSource[];
    };
    apMod: {
        value: number;
        sources: BuffSource[];
    };
    /**
     * Defender-side damage reduction. Highest-wins (multiple sources do not
     * stack in 10e); the dominant source is the one whose value matches the
     * surviving reduction.
     */
    damageReduction: {
        value: number;
        dominantSource: BuffSource | null;
    };
    /**
     * Ability-granted invulnerable save. Best (lowest) threshold wins. `null`
     * when no ability granted one; the engine still uses the unit's printed
     * `invuln_sv` from the profile in that case.
     */
    invulnerable: {
        threshold: number;
        dominantSource: BuffSource;
    } | null;
};
/**
 * Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure
 * function; the engine — and any UI that wants to render the resolved table
 * before crunching — both go through this.
 */
export declare function resolveBuffs(buffs: Buff[], ctx: ResolveContext): ResolvedModifiers;
//# sourceMappingURL=buffs.d.ts.map