/** Keywords whose math the engine encodes directly (catalog `effect` is null). */
const ENGINE_DISPATCH_KEYWORDS = new Set([
    "lethal-hits",
    "sustained-hits",
    "devastating-wounds",
    "anti",
    "melta",
    "rapid-fire",
    "torrent",
    "ignores-cover",
]);
/**
 * Convert a single weapon-keyword reference (catalog effect + reference-site
 * parameters) into the buff contributions it makes against `context`.
 */
export function buffsFromKeyword(args) {
    const source = {
        kind: "weapon-keyword",
        weaponId: args.weaponId,
        keywordId: args.keywordId,
    };
    if (ENGINE_DISPATCH_KEYWORDS.has(args.keywordId)) {
        const ref = {
            keyword_id: args.keywordId,
            ...(args.parameters !== undefined ? { parameters: args.parameters } : {}),
        };
        return [{ source, contribution: { type: "extra-keyword", keywordRef: ref } }];
    }
    if (args.effect === null || args.effect === undefined)
        return [];
    return walk(args.effect, source, args.context);
}
function walk(node, source, ctx) {
    if (!isObject(node))
        return [];
    const type = node.type;
    switch (type) {
        case "re-roll":
            return rerollBuffs(node, source);
        case "roll-modifier":
            return rollModifierBuffs(node, source);
        case "feel-no-pain":
            return feelNoPainBuffs(node, source);
        case "keyword-grant":
            return keywordGrantBuffs(node, source);
        case "conditional":
            return conditionalBuffs(node, source, ctx);
        case "sequence":
            return walkChildren(node.steps, source, ctx);
        default:
            return [];
    }
}
function walkChildren(children, source, ctx) {
    if (!Array.isArray(children))
        return [];
    const out = [];
    for (const child of children)
        out.push(...walk(child, source, ctx));
    return out;
}
function rerollBuffs(node, source) {
    const modifier = node.modifier;
    if (!isObject(modifier))
        return [];
    const roll = modifier.roll;
    const subset = modifier.subset;
    if ((roll === "hit" || roll === "wound" || roll === "save" || roll === "damage") &&
        (subset === "ones" || subset === "all-failures")) {
        return [{ source, contribution: { type: "reroll", roll, subset } }];
    }
    return [];
}
function rollModifierBuffs(node, source) {
    const modifier = node.modifier;
    if (!isObject(modifier))
        return [];
    const operation = modifier.operation;
    if (operation !== "add")
        return []; // M1 supports additive only; multiplicative effects are out of scope.
    const value = typeof modifier.value === "number" ? modifier.value : NaN;
    if (!Number.isFinite(value))
        return [];
    const roll = modifier.roll;
    switch (roll) {
        case "hit":
            return [{ source, contribution: { type: "hit-mod", value } }];
        case "wound":
            return [{ source, contribution: { type: "wound-mod", value } }];
        case "save":
            return [{ source, contribution: { type: "save-mod", value } }];
        case "damage":
            return [{ source, contribution: { type: "damage-mod", value } }];
        default:
            return [];
    }
}
function feelNoPainBuffs(node, source) {
    const modifier = node.modifier;
    if (!isObject(modifier))
        return [];
    const threshold = typeof modifier.threshold === "number" ? modifier.threshold : NaN;
    if (!Number.isFinite(threshold))
        return [];
    return [{ source, contribution: { type: "feel-no-pain", threshold } }];
}
function keywordGrantBuffs(node, source) {
    const modifier = node.modifier;
    if (!isObject(modifier))
        return [];
    const id = modifier.keyword_id ?? modifier.id;
    if (typeof id !== "string" || id === "")
        return [];
    const params = isObject(modifier.parameters) ? modifier.parameters : undefined;
    const ref = {
        keyword_id: id,
        ...(params !== undefined ? { parameters: params } : {}),
    };
    return [{ source, contribution: { type: "extra-keyword", keywordRef: ref } }];
}
function conditionalBuffs(node, source, ctx) {
    const condition = node.condition;
    const effect = node.effect;
    if (!isObject(condition))
        return [];
    const negated = condition.negated === true;
    const verdict = evaluateCondition(condition, ctx);
    if (verdict === "unknown")
        return [];
    const active = negated ? !verdict : verdict;
    if (!active)
        return [];
    return walk(effect, source, ctx);
}
/**
 * Returns true/false when the engine can evaluate the condition against
 * `ctx`; "unknown" when the condition references state the M1 engine has no
 * channel for (the buff is then dropped — M2's diagnostic surface owns the
 * "cannot auto-apply" reporting).
 */
function evaluateCondition(condition, ctx) {
    switch (condition.type) {
        case "remained-stationary":
            return ctx.attackerStationary === true;
        case "target-has-keyword": {
            const parameters = isObject(condition.parameters) ? condition.parameters : {};
            const kw = parameters.keyword;
            if (typeof kw !== "string")
                return "unknown";
            return (ctx.targetKeywords ?? []).includes(kw.toLowerCase());
        }
        default:
            return "unknown";
    }
}
function isObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
//# sourceMappingURL=from-keyword.js.map