import sourceCatalog from "./generated/enemyDamageTypeSources.generated.json"; export type EnemyDamageOrigin = "wow335" | "runewaker"; export type EnemyAttackKind = "melee" | "ranged" | "spell"; export type EnemyDamageSchool = | "physical" | "arcane" | "fire" | "frost" | "nature" | "shadow" | "holy"; export interface WowEnemyHitSource { readonly attributes0: number; readonly attributes3: number; readonly attributes7: number; } export interface WowEnemyDamageTypeSource { readonly spellId: number; readonly name: string; readonly damageClass: number; readonly schoolMask: number; /** Raw hit-gate attribute words copied from a matched WoW Spell.dbc row. */ readonly hitSource?: WowEnemyHitSource; readonly schools: readonly EnemyDamageSchool[]; readonly school?: EnemyDamageSchool; readonly attackKind?: EnemyAttackKind; readonly damageEffects: readonly number[]; readonly source: string; } export interface RunewakerEnemyDamageTypeComponent { readonly componentId: number; /** MagicObject.Attack.CalType; direct packet class, independent of EffectType. */ readonly calculationType: number; /** Raw MagicObject.EffectType, retained as source metadata. */ readonly effectType: number; readonly magicType: number; /** Formula-specific handling; not a packet class and not the MagicCollect hit gate. */ readonly directFormula?: "throw" | "fixed" | "target-hp-percent"; /** Absorption branch selected by the direct formula and raw component flags. */ readonly formulaMitigation?: "weapon-absorb" | "magic-absorb" | "bypassed"; readonly sourceDamageClass?: "physical" | "magical"; readonly attackKind?: EnemyAttackKind; readonly sourceChannel?: "physical" | "earth" | "water" | "fire" | "wind" | "light" | "darkness"; /** Approximate mapping into HealerMan's seven-school resistance model. */ readonly school?: EnemyDamageSchool; } export interface RunewakerEnemyDamageTypeSource { readonly spellId: number; readonly name: string; readonly components: readonly RunewakerEnemyDamageTypeComponent[]; readonly source: string; } export type EnemyDamageTypeSource = WowEnemyDamageTypeSource | RunewakerEnemyDamageTypeSource; export interface ResolveEnemyDamageTypeInput { readonly origin?: EnemyDamageOrigin; readonly spellId?: number; readonly componentId?: number; /** Raw WoW Spell.dbc DmgClass when present. */ readonly damageClass?: number; /** Runtime delivery classification, authored or normalized by the importer. */ readonly attackKind?: EnemyAttackKind; /** Raw WoW Spell.dbc SchoolMask; bit values are preserved. */ readonly schoolMask?: number; /** Authored runtime school used when source school data is absent or ambiguous. */ readonly school?: EnemyDamageSchool; } export interface ResolvedEnemyDamageType { readonly attackKind?: EnemyAttackKind; readonly damageClass?: number; readonly sourceDamageClass?: "none" | "magic" | "melee" | "ranged" | "physical" | "magical"; readonly schoolMask?: number; /** Copied only from a matched WoW damage row; absent means unknown/not WoW. */ readonly hitSource?: WowEnemyHitSource; readonly schools: readonly EnemyDamageSchool[]; /** Present only when source evidence identifies exactly one runtime school. */ readonly school?: EnemyDamageSchool; readonly unknownSchoolMaskBits?: number; readonly sourceVerified: boolean; } type GeneratedCatalog = { readonly wow335: Readonly>; readonly runewaker: Readonly>; }; const catalog = sourceCatalog as GeneratedCatalog; const WOW_SCHOOL_BITS: readonly (readonly [number, EnemyDamageSchool])[] = [ [1, "physical"], [2, "holy"], [4, "fire"], [8, "nature"], [16, "frost"], [32, "shadow"], [64, "arcane"], ]; const KNOWN_WOW_SCHOOL_BITS = WOW_SCHOOL_BITS.reduce((mask, [bit]) => mask | bit, 0); export function enemyDamageTypeSourceForSpell( origin: EnemyDamageOrigin, spellId: number, ): EnemyDamageTypeSource | undefined { if (!Number.isSafeInteger(spellId) || spellId <= 0) return undefined; const key = String(spellId); return origin === "wow335" ? catalog.wow335[key] : catalog.runewaker[key]; } export function enemyDamageTypeComponentForSpell( spellId: number, componentId: number, ): RunewakerEnemyDamageTypeComponent | undefined { const source = enemyDamageTypeSourceForSpell("runewaker", spellId); if (!source || !("components" in source)) return undefined; return source.components.find((component) => component.componentId === componentId); } /** * Converts WotLK SchoolMask bits without imposing a strongest-school rule. * Composite masks remain a list; callers should keep the authored single-school * fallback until the damage resolver supports combined-school mitigation. */ export function wowSchoolsFromSchoolMask(mask: number): { readonly schools: readonly EnemyDamageSchool[]; readonly unknownBits: number; } { if (!Number.isSafeInteger(mask) || mask < 0) return { schools: [], unknownBits: 0 }; return { schools: WOW_SCHOOL_BITS.filter(([bit]) => (mask & bit) !== 0).map(([, school]) => school), unknownBits: (mask & ~KNOWN_WOW_SCHOOL_BITS) >>> 0, }; } function sourceComponentForResolution( source: RunewakerEnemyDamageTypeSource | undefined, componentId: number | undefined, ): RunewakerEnemyDamageTypeComponent | undefined { if (!source) return undefined; if (componentId !== undefined) { return source.components.find((component) => component.componentId === componentId); } if (source.components.length === 1) return source.components[0]; const first = source.components[0]; if (first && source.components.every((component) => ( component.sourceDamageClass === first.sourceDamageClass && component.attackKind === first.attackKind && component.school === first.school && component.sourceChannel === first.sourceChannel ))) return first; return undefined; } function attackKindForDamageClass(value: number | undefined): EnemyAttackKind | undefined { if (value === 1) return "spell"; if (value === 2) return "melee"; if (value === 3) return "ranged"; return undefined; } export function resolveEnemyDamageType(input: ResolveEnemyDamageTypeInput): ResolvedEnemyDamageType { const source = input.origin && input.spellId !== undefined ? enemyDamageTypeSourceForSpell(input.origin, input.spellId) : undefined; const wowSource = source && "schoolMask" in source ? source : undefined; const runewakerSource = source && "components" in source ? source : undefined; const runewakerComponent = sourceComponentForResolution(runewakerSource, input.componentId); const damageClass = input.damageClass ?? wowSource?.damageClass; const schoolMask = input.schoolMask ?? wowSource?.schoolMask; const maskResult = schoolMask === undefined ? { schools: [] as readonly EnemyDamageSchool[], unknownBits: 0 } : wowSchoolsFromSchoolMask(schoolMask); const exactWoWSchool = schoolMask !== undefined && maskResult.schools.length === 1 && maskResult.unknownBits === 0 ? maskResult.schools[0] : undefined; const sourceDamageClass = input.origin === "runewaker" ? runewakerComponent?.sourceDamageClass : damageClass === 0 ? "none" : damageClass === 1 ? "magic" : damageClass === 2 ? "melee" : damageClass === 3 ? "ranged" : undefined; const attackKind = input.origin === "runewaker" ? runewakerComponent?.attackKind ?? input.attackKind : input.attackKind ?? attackKindForDamageClass(damageClass); const school = exactWoWSchool ?? runewakerComponent?.school ?? input.school; return { ...(attackKind ? { attackKind } : {}), ...(damageClass !== undefined ? { damageClass } : {}), ...(sourceDamageClass ? { sourceDamageClass } : {}), ...(schoolMask !== undefined ? { schoolMask } : {}), ...(wowSource?.hitSource ? { hitSource: wowSource.hitSource } : {}), schools: maskResult.schools.length ? maskResult.schools : runewakerComponent?.school ? [runewakerComponent.school] : [], ...(school ? { school } : {}), ...(maskResult.unknownBits ? { unknownSchoolMaskBits: maskResult.unknownBits } : {}), sourceVerified: Boolean(wowSource || runewakerComponent), }; }