import type { RomNativeNpcSpellBinding } from "./romNativeCombatResolution"; import { romNativeNpcCombatant } from "./romNativeCombatResolution"; import type { RomNativeDerivedStats } from "./romNativeStatsTypes"; import { matchesNativeTrainingGear } from "./romNativeCombatAmount"; type SourceComponent = Readonly>; export interface RomNativeEnemyTargetAbsorbInputs { /** Fin.AB, added to signed damage before the source Abr rate. */ readonly physicalFlatAbsorb: number; /** Fin.Abr, the native rate subtracted from one in CalWeaponDMG. */ readonly physicalAbsorbRate: number; /** Fin.MAbr, the native rate subtracted from one in CalMagicDMG. */ readonly magicalAbsorbRate: number; /** WearEqType_AEMagicDefRate BodyRate; used only for non-target magical rows. */ readonly aoeMagicDefenseRate: number; readonly provenance: readonly string[]; } export type RomNativeEnemyTargetAbsorbInputResult = | { readonly status: "supported"; readonly value: RomNativeEnemyTargetAbsorbInputs } | { readonly status: "unsupported"; readonly reason: string }; /** * The canonical source training set has neutral AB/Abr/MAbr and AoE magic-defense * channels. Require both the source-gear match and an explicit aura-neutrality * assertion; shared WoW armor is never used as a substitute for those channels. */ export function romNativeTrainingEnemyAbsorbInputs( stats: RomNativeDerivedStats, nativeAbsorbAurasVerifiedNeutral: boolean, ): RomNativeEnemyTargetAbsorbInputResult { if (!nativeAbsorbAurasVerifiedNeutral) { return { status: "unsupported", reason: "native-target-absorb-aura-state-unverified" }; } if (!matchesNativeTrainingGear(stats, stats.sourceInputs.level)) { return { status: "unsupported", reason: "native-target-training-gear-profile-unverified" }; } return Object.freeze({ status: "supported", value: Object.freeze({ physicalFlatAbsorb: 0, physicalAbsorbRate: 0, magicalAbsorbRate: 0, aoeMagicDefenseRate: 1, provenance: Object.freeze([ "RoleValueData::Init_BaseEQValue zeroes Body and initializes every BodyRate to 1", "all 999 selected native training items/affixes have fully resolved channels and none supplies AB/Abr/MAbr/AEMagicDefRate", "caller-verified no native absorb-channel aura is active", ]), }), }); } export interface RomNativeEnemyDirectDamageInput { readonly npcId: number; /** Runtime NPC level used only by CalType 2 source DLvRate; MagicLv stays on the source binding. */ readonly npcLevel?: number; readonly sourceSkillId: number; readonly componentId: number; /** Required only by CalType 8; source uses the recipient's Fin.MaxHP. */ readonly targetMaxHp?: number; readonly targetCount?: number; /** Ordinal in the final selected-recipient order used by MagicProcess. */ readonly targetOrdinal?: number; /** Explicit source target Fin.AB/Abr/MAbr and AoE magic-defense BodyRate. */ readonly targetAbsorb?: RomNativeEnemyTargetAbsorbInputs; /** NPC WearEqType_AEMagicPowerRate; required for non-target magical packets. */ readonly sourceAoeMagicPowerRate?: number; /** GetValue(FixType) only for nonzero fixed terms outside the known None type. */ readonly fixedAttributeValue?: number; } export interface RomNativeEnemyDirectDamageSourceStages { readonly bindingIdentityVerified: true; readonly npcMagicLevelIncluded: true; readonly formulaPowerApplied: true; readonly sourceLevelDamageRate: "applied" | "not-applicable"; readonly sourceNpcDamageRate: "applied" | "not-applicable"; readonly sourceMagicPowerRate: "applied" | "not-applicable"; readonly sourceTypedResistance: "applied" | "not-applicable"; readonly targetFormulaAbsorption: "applied" | "pending-after-combat-table" | "bypassed"; readonly sourceAoeMagicRates: "applied" | "not-applicable"; readonly sourceMultiTargetFactor: "applied" | "pending-after-target-formula"; readonly sourceOrdinalPowerRate: number; readonly sourceShareDamageRate: number; readonly authoredEncounterDamageMultiplier: "not-applied"; readonly genericAuras: "not-applied"; readonly ordinaryShieldAbsorption: "not-applied"; } export interface RomNativeEnemyDirectDamageValue { readonly npcId: number; readonly sourceSkillId: number; readonly componentId: number; readonly calculationType: 2 | 4 | 8; readonly magicLevel: number; /** Positive amount suitable as native base damage before authored encounter scaling. */ readonly amount: number; /** Signed damage after the source-owned formula absorption when that stage is before hit resolution. */ readonly signedPower: number; readonly formulaMitigation: "weapon-absorb" | "magic-absorb" | "bypassed"; readonly nativeDamageBudgetScale: "skip-authored-native-damage-budget"; readonly sourceHitEffectType: number; readonly componentEffectType: number; readonly magicType: number; readonly sourceMultiTargetFactor: number; readonly resolverDamageChannel: "none" | "physical"; readonly resolverCriticalKind?: "physical"; readonly resolverRules: Readonly<{ canCrit: boolean; canBlock: boolean; canParry: boolean; ignoreArmor: boolean; }>; /** True means the formula has resolved or deliberately omitted these source power stages. */ readonly sourcePowerStagesAlreadyApplied: boolean; readonly sourceStages: RomNativeEnemyDirectDamageSourceStages; readonly authoredBudgetPolicy: "apply-imported-attack-damageMultiplier; skip-native-damage-budget-scale-for-source-formula; keep-authored-encounter-health"; } export type RomNativeEnemyDirectDamageResult = | { readonly status: "supported"; readonly value: RomNativeEnemyDirectDamageValue } | { readonly status: "unsupported"; readonly reason: string }; const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value); const numeric = (source: SourceComponent, key: string): number | undefined => { const value = source[key]; return finite(value) ? value : undefined; }; const flag = (source: SourceComponent, key: string): boolean | undefined => { const value = source[key]; return typeof value === "boolean" ? value : undefined; }; const unsupported = (reason: string): RomNativeEnemyDirectDamageResult => ({ status: "unsupported", reason }); function sourceMagicLevel(binding: RomNativeNpcSpellBinding): number | undefined { return Number.isSafeInteger(binding.magicLevel) && binding.magicLevel >= 0 ? binding.magicLevel : undefined; } function targetAbsorbedPower(power: number, kind: "weapon" | "magic", input: RomNativeEnemyTargetAbsorbInputs): number { if (power >= 0) return power; const value = kind === "weapon" ? (power + input.physicalFlatAbsorb) * (1 - input.physicalAbsorbRate) : power * (1 - input.magicalAbsorbRate); return Math.min(-1, Math.trunc(value)); } function targetHpPercentPower( powerPercent: number, targetMaxHp: number, kind: "weapon" | "magic", input: RomNativeEnemyTargetAbsorbInputs | undefined, bypassAbsorb: boolean, ): number | undefined { const rawPower = targetMaxHp * powerPercent; if (bypassAbsorb) return rawPower / 100; if (!input) return undefined; if (kind === "weapon") { if (![input.physicalFlatAbsorb, input.physicalAbsorbRate].every(finite)) return undefined; const absorbed = Math.min(-1, Math.trunc((rawPower + input.physicalFlatAbsorb) * (1 - input.physicalAbsorbRate))); return absorbed / 100; } if (!finite(input.magicalAbsorbRate)) return undefined; const absorbed = Math.min(-1, Math.trunc(rawPower * (1 - input.magicalAbsorbRate))); return absorbed / 100; } function bindingFor(input: RomNativeEnemyDirectDamageInput): { readonly binding: RomNativeNpcSpellBinding; readonly component: SourceComponent; readonly magicLevel: number; } | RomNativeEnemyDirectDamageResult { if (![input.npcId, input.sourceSkillId, input.componentId].every((value) => Number.isSafeInteger(value) && value > 0)) { return unsupported("native-enemy-source-identity-invalid"); } if (input.npcLevel !== undefined && (!Number.isSafeInteger(input.npcLevel) || input.npcLevel < 1)) { return unsupported("native-enemy-runtime-level-invalid"); } const npc = romNativeNpcCombatant(input.npcId, input.npcLevel); if (!npc) return unsupported("native-enemy-npc-binding-missing"); const binding = npc.spellBindings.find((entry) => entry.sourceSkillId === input.sourceSkillId); if (!binding) return unsupported("native-enemy-skill-binding-missing"); const component = binding.components.find((entry) => numeric(entry, "componentId") === input.componentId); if (!component) return unsupported("native-enemy-component-binding-missing"); const magicLevel = sourceMagicLevel(binding); if (magicLevel === undefined) return unsupported("native-enemy-magic-level-missing"); if (!binding.magicCollect || ![0, 1].includes(binding.magicCollect.effectType)) { return unsupported("native-enemy-collect-hit-binding-incomplete"); } if (numeric(component, "magicFunc") !== 0 || numeric(component, "attackType") !== 0) { return unsupported("native-enemy-component-is-not-direct-hp-damage"); } return { binding, component, magicLevel }; } function baseValue( input: RomNativeEnemyDirectDamageInput, formula: 2 | 4 | 8, magicLevel: number, amount: number, signedPower: number, formulaMitigation: RomNativeEnemyDirectDamageValue["formulaMitigation"], resolverDamageChannel: RomNativeEnemyDirectDamageValue["resolverDamageChannel"], sourceHitEffectType: number, componentEffectType: number, magicType: number, sourceMultiTargetFactor: number, sourceOrdinalPowerRate: number, sourceShareDamageRate: number, stages: RomNativeEnemyDirectDamageSourceStages, options: { readonly sourcePowerStagesAlreadyApplied: boolean; readonly crit?: "physical" }, ): RomNativeEnemyDirectDamageResult { if (![amount, signedPower].every(Number.isFinite) || amount < 0) return unsupported("native-enemy-formula-produced-invalid-damage"); return Object.freeze({ status: "supported", value: Object.freeze({ npcId: input.npcId, sourceSkillId: input.sourceSkillId, componentId: input.componentId, calculationType: formula, magicLevel, amount, signedPower, formulaMitigation, nativeDamageBudgetScale: "skip-authored-native-damage-budget", sourceHitEffectType, componentEffectType, magicType, sourceMultiTargetFactor, resolverDamageChannel, ...(options.crit ? { resolverCriticalKind: options.crit } : {}), resolverRules: Object.freeze({ canCrit: formula === 2, canBlock: formula === 2, canParry: formula === 2, ignoreArmor: formula === 2, }), sourcePowerStagesAlreadyApplied: options.sourcePowerStagesAlreadyApplied, sourceStages: stages, authoredBudgetPolicy: "apply-imported-attack-damageMultiplier; skip-native-damage-budget-scale-for-source-formula; keep-authored-encounter-health", }), }); } const sourceStages = ( values: Omit, ): RomNativeEnemyDirectDamageSourceStages => Object.freeze({ bindingIdentityVerified: true, npcMagicLevelIncluded: true, formulaPowerApplied: true, authoredEncounterDamageMultiplier: "not-applied", genericAuras: "not-applied", ordinaryShieldAbsorption: "not-applied", ...values, }); /** Resolve an exact imported NPC spell component; no skill/name/Day-of-Rain inference is used. */ export function resolveRomNativeEnemyDirectDamage( input: RomNativeEnemyDirectDamageInput, ): RomNativeEnemyDirectDamageResult { const resolved = bindingFor(input); if ("status" in resolved) return resolved; const { binding, component, magicLevel } = resolved; const calculationType = numeric(component, "calculationType"); const damagePower = numeric(component, "damagePower"); const damagePowerSkillLevelArg = numeric(component, "damagePowerSkillLevelArg"); const fixedValue = numeric(component, "fixedValue"); const fixedType = numeric(component, "fixedType"); const fixedDamageSkillLevelArg = numeric(component, "fixedDamageSkillLevelArg"); const ignoreMagicAbsorb = flag(component, "ignoreMagicAbsorb"); const isFixDotPower = flag(component, "isFixDotPower"); const collect = binding.magicCollect; const rangeType = numeric(collect as unknown as SourceComponent, "rangeType"); const decreaseArg = numeric(collect as unknown as SourceComponent, "decreaseArg"); const isShareDamage = flag(collect as unknown as SourceComponent, "isShareDamage"); const magicType = numeric(component, "magicType"); const componentEffectType = numeric(component, "effectType"); if (calculationType === undefined || damagePower === undefined || damagePowerSkillLevelArg === undefined || fixedValue === undefined || fixedType === undefined || fixedDamageSkillLevelArg === undefined || rangeType === undefined || decreaseArg === undefined || magicType === undefined || componentEffectType === undefined || ignoreMagicAbsorb === undefined || isFixDotPower === undefined || isShareDamage === undefined) { return unsupported("native-enemy-component-source-fields-incomplete"); } if (![2, 4, 8].includes(calculationType)) return unsupported("native-enemy-calculation-type-unsupported"); if ((calculationType === 2 || calculationType === 4) && fixedType !== 0 && fixedValue !== 0 && !finite(input.fixedAttributeValue)) { return unsupported("native-enemy-fixed-attribute-input-required"); } if (input.fixedAttributeValue !== undefined && !finite(input.fixedAttributeValue)) { return unsupported("native-enemy-fixed-attribute-input-invalid"); } const fixedAttributeValue = fixedType === 0 ? 1 : (input.fixedAttributeValue ?? 0); const orgPower = damagePower * (1 + damagePowerSkillLevelArg * magicLevel / 100); const fixedPower = fixedValue * fixedAttributeValue * (1 + magicLevel * fixedDamageSkillLevelArg / 100); if (!Number.isFinite(orgPower) || !Number.isFinite(fixedPower)) return unsupported("native-enemy-component-power-invalid"); const targetCount = input.targetCount ?? 1; const targetOrdinal = input.targetOrdinal ?? 0; if (!Number.isInteger(targetCount) || targetCount < 1 || !Number.isInteger(targetOrdinal) || targetOrdinal < 0 || targetOrdinal >= targetCount) return unsupported("native-enemy-target-order-invalid"); const shareRate = Math.max(decreaseArg / 100, 1 / targetCount); const ordinalPowerRate = (100 - decreaseArg) / 100; const ordinalRate = isShareDamage ? 1 : ordinalPowerRate ** targetOrdinal; if (calculationType === 4) { let power = orgPower * ordinalRate + fixedPower; if (power >= 0) return unsupported("native-enemy-fixed-formula-is-not-negative-direct-damage"); let mitigation: RomNativeEnemyDirectDamageValue["formulaMitigation"] = "bypassed"; if (!ignoreMagicAbsorb) { if (!input.targetAbsorb || ![input.targetAbsorb.magicalAbsorbRate, input.targetAbsorb.aoeMagicDefenseRate].every(finite)) { return unsupported("native-enemy-target-magic-absorb-input-required"); } power = targetAbsorbedPower(power, "magic", input.targetAbsorb); mitigation = "magic-absorb"; } if (rangeType !== 0) { if (!input.targetAbsorb || !finite(input.targetAbsorb.aoeMagicDefenseRate) || !finite(input.sourceAoeMagicPowerRate)) return unsupported("native-enemy-source-aoe-magic-rates-required"); power *= input.sourceAoeMagicPowerRate * input.targetAbsorb.aoeMagicDefenseRate; } const afterShare = power * (isShareDamage ? shareRate : 1); const signedPower = Math.trunc(-afterShare) * -1; return baseValue(input, 4, magicLevel, Math.max(0, -signedPower), signedPower, mitigation, "none", binding.magicCollect.effectType, componentEffectType, magicType, isShareDamage ? shareRate : ordinalRate, ordinalRate, isShareDamage ? shareRate : 1, sourceStages({ sourceLevelDamageRate: "not-applicable", sourceNpcDamageRate: "not-applicable", sourceMagicPowerRate: "not-applicable", sourceTypedResistance: "not-applicable", targetFormulaAbsorption: mitigation === "bypassed" ? "bypassed" : "applied", sourceAoeMagicRates: rangeType === 0 ? "not-applicable" : "applied", sourceMultiTargetFactor: "applied", sourceOrdinalPowerRate: ordinalRate, sourceShareDamageRate: isShareDamage ? shareRate : 1 }), { sourcePowerStagesAlreadyApplied: true }); } if (calculationType === 8) { if (orgPower >= 0) return unsupported("native-enemy-component-is-not-negative-direct-damage"); if (!finite(input.targetMaxHp) || (input.targetMaxHp as number) <= 0) { return unsupported("native-enemy-target-max-hp-required"); } const percentPower = orgPower * ordinalRate; if (percentPower >= 0) return unsupported("native-enemy-percent-formula-is-not-negative-direct-damage"); let mitigation: RomNativeEnemyDirectDamageValue["formulaMitigation"] = "bypassed"; const absorbBypass = ignoreMagicAbsorb || isFixDotPower; const effectType = componentEffectType; if (effectType !== 0 && effectType !== 1) return unsupported("native-enemy-percent-effect-type-unsupported"); const absorbKind = effectType === 1 ? "weapon" : "magic"; const power = targetHpPercentPower(percentPower, input.targetMaxHp as number, absorbKind, input.targetAbsorb, absorbBypass); if (power === undefined) return unsupported(absorbBypass ? "native-enemy-percent-formula-invalid" : "native-enemy-target-absorb-input-required"); if (!absorbBypass) mitigation = absorbKind === "weapon" ? "weapon-absorb" : "magic-absorb"; let adjustedPower = power; // FixDotPower takes the source's early raw-MaxHP path and skips the AoE // BodyRate multiplication below the ordinary CalMagicDMG branch. if (absorbKind === "magic" && rangeType !== 0 && !isFixDotPower) { if (!input.targetAbsorb || !finite(input.targetAbsorb.aoeMagicDefenseRate) || !finite(input.sourceAoeMagicPowerRate)) return unsupported("native-enemy-source-aoe-magic-rates-required"); adjustedPower *= input.sourceAoeMagicPowerRate * input.targetAbsorb.aoeMagicDefenseRate; } const integerDamage = Math.trunc(-adjustedPower); const sourceShareDamageRate = isShareDamage ? shareRate : 1; const signedPower = -(integerDamage * sourceShareDamageRate); return baseValue(input, 8, magicLevel, Math.max(0, -signedPower), signedPower, mitigation, "none", binding.magicCollect.effectType, effectType, magicType, isShareDamage ? shareRate : ordinalRate, ordinalRate, sourceShareDamageRate, sourceStages({ sourceLevelDamageRate: "not-applicable", sourceNpcDamageRate: "not-applicable", sourceMagicPowerRate: "not-applicable", sourceTypedResistance: "not-applicable", targetFormulaAbsorption: mitigation === "bypassed" ? "bypassed" : "applied", sourceAoeMagicRates: rangeType === 0 || absorbKind === "weapon" || isFixDotPower ? "not-applicable" : "applied", sourceMultiTargetFactor: "applied", sourceOrdinalPowerRate: ordinalRate, sourceShareDamageRate }), { sourcePowerStagesAlreadyApplied: true }); } // Throw's source target CalWeaponDMG stage runs after the shared critical, // block, and parry table. The runtime currently has no completion hook for // that exact order, and the imported NPC catalog has no bound CalType 2 row. // Do not return an apparently final amount while absorption/share are pending. return unsupported("native-enemy-throw-post-table-absorption-runtime-unwired"); }