import type { AbilityAiHints, AbilityDefinition, AbilityRuneCost, AbilityTargetingRules, } from "./abilityCatalog"; const CHAIN_ABILITIES = new Set(["chain heal", "chain lightning", "multi-shot", "prayer of mending"]); const GROUND_ABILITIES = new Set([ "blizzard", "rain of fire", "flamestrike", "death and decay", "consecration", "volley", "hurricane", "earthquake", "meteor", "healing rain", ]); const CONE_ABILITIES = new Set([ "cone of cold", "dragon's breath", "dragons breath", "shockwave", "cleave", "swipe", "swipe (cat)", "whirlwind", ]); const BEHIND_ABILITIES = new Set(["backstab", "ambush", "garrote", "ravage", "shred", "pounce", "blind spot", "sneak attack"]); const MOVEMENT_CAST_ABILITIES = new Set(["scorch", "steady shot", "spiritwalker's grace"]); const EMERGENCY_HEAL_ABILITIES = new Set([ "flash heal", "flash of light", "swiftmend", "lay on hands", "nature's swiftness", ]); const AREA_HEAL_ABILITIES = new Set([ "chain heal", "circle of healing", "divine hymn", "holy nova", "prayer of healing", "prayer of mending", "tranquility", "wild growth", ]); const RUNE_COSTS_BY_SPELL_ID: Readonly> = Object.freeze({ 49909: { frost: 1 }, 45524: { frost: 1 }, 49184: { frost: 1 }, 47528: { frost: 1 }, 49921: { unholy: 1 }, 55271: { unholy: 1 }, 49222: { unholy: 1 }, 51052: { unholy: 1 }, 49930: { blood: 1 }, 55262: { blood: 1 }, 49941: { blood: 1 }, 50842: { blood: 1 }, 48982: { blood: 1 }, 49005: { blood: 1 }, 49924: { frost: 1, unholy: 1 }, 51425: { frost: 1, unholy: 1 }, 49938: { blood: 1, frost: 1, unholy: 1 }, }); const RUNE_COSTS_BY_NAME: Readonly> = Object.freeze({ "icy touch": { frost: 1 }, "chains of ice": { frost: 1 }, "howling blast": { frost: 1 }, "mind freeze": { frost: 1 }, "plague strike": { unholy: 1 }, "scourge strike": { unholy: 1 }, "bone shield": { unholy: 1 }, "anti-magic zone": { unholy: 1 }, "blood strike": { blood: 1 }, "heart strike": { blood: 1 }, "blood boil": { blood: 1 }, "pestilence": { blood: 1 }, "rune tap": { blood: 1 }, "mark of blood": { blood: 1 }, "death strike": { frost: 1, unholy: 1 }, "obliterate": { frost: 1, unholy: 1 }, "death and decay": { blood: 1, frost: 1, unholy: 1 }, }); export function targetingRulesForAbility(ability: AbilityDefinition): AbilityTargetingRules { if (ability.targeting) return ability.targeting; const name = ability.name.toLowerCase(); const shape = CHAIN_ABILITIES.has(name) ? "chain" : GROUND_ABILITIES.has(name) || name.endsWith(" trap") ? "ground" : CONE_ABILITIES.has(name) || name.endsWith(" breath") ? "cone" : ability.radius && ability.radius > 0 ? "circle" : "unit"; const hostile = ability.target === "hostile" || ability.target === "any"; return Object.freeze({ shape, requiresLineOfSight: ability.target !== "self", // WoW friendly spells are deliberately facing independent. requiresFacing: hostile && shape !== "circle" && shape !== "ground", requiresBehindTarget: hostile && BEHIND_ABILITIES.has(name), attackArcRadians: shape === "cone" ? Math.PI / 2 : Math.PI, ...(ability.radius ? { radius: ability.radius } : {}), ...(shape === "chain" ? { maximumTargets: 3, jumpRange: name === "multi-shot" ? 10 : 12 } : {}), ...(shape === "ground" ? { groundRange: ability.range.max > 0 ? ability.range.max : name.endsWith(" trap") || name === "consecration" ? 0 : 30, radius: ability.radius ?? 8 } : {}), }); } export function canAbilityCastWhileMoving(ability: AbilityDefinition): boolean { return ability.castRules?.canCastWhileMoving ?? MOVEMENT_CAST_ABILITIES.has(ability.name.toLowerCase()); } export function abilityRequiresOutOfCombat(ability: AbilityDefinition): boolean { return (ability.source === "runewaker" || ability.source === "wow335-progression") && /\bcannot be used (?:when already )?in combat\b/i.test(ability.description); } export function abilityPushbackResistance(ability: AbilityDefinition): number { if (ability.castRules?.pushbackImmune) return 1; return Math.max(0, Math.min(1, ability.castRules?.pushbackResistance ?? 0)); } /** WotLK 3.3.5 rune costs for the active DK toolkit imported by the game. */ export function runeCostForAbility(ability: AbilityDefinition): AbilityRuneCost | null { if (ability.runeCost) return ability.runeCost; return RUNE_COSTS_BY_SPELL_ID[ability.dbcSpellId] ?? RUNE_COSTS_BY_NAME[ability.name.toLowerCase()] ?? null; } export function aiHintsForAbility(ability: AbilityDefinition): AbilityAiHints { if (ability.ai) return ability.ai; const name = ability.name.toLowerCase(); const categories = new Set(); for (const effect of ability.effects) { if (["damage", "dot", "finisher-damage"].includes(effect.kind)) categories.add("damage"); if (effect.kind === "execute") categories.add("execute"); if (effect.kind === "heal" || effect.kind === "hot" || effect.kind === "sacrifice-minion") categories.add("heal"); if (effect.kind === "shield") categories.add("mitigation"); if (effect.kind === "taunt") categories.add("taunt"); if (effect.kind === "interrupt") categories.add("interrupt"); if (effect.kind === "dispel" || effect.kind === "dispel-mechanic") categories.add("dispel"); if (effect.kind === "resurrection") categories.add("resurrection"); if (effect.kind === "status") { categories.add(effect.status === "interrupt" ? "interrupt" : "control"); } if (effect.kind === "dot" || (effect.kind === "apply-aura" && effect.aura.disposition === "debuff")) categories.add("debuff"); if (effect.kind === "apply-aura" && effect.aura.disposition === "buff") categories.add("buff"); if (effect.kind === "apply-aura" && ( effect.aura.absorbs?.length || effect.aura.modifiers?.some((modifier) => modifier.kind === "damage" && modifier.direction === "taken" && modifier.value < 0) || effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && ["block", "dodge", "parry"].includes(modifier.stat) && modifier.value > 0) )) categories.add("mitigation"); if (effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => ( modifier.kind === "stat" && modifier.stat === "threat" ))) categories.add("threat"); if (effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "resource")) { categories.add("resource"); } if (effect.kind === "resource" || effect.kind === "rune") categories.add("resource"); if (effect.kind === "summon" || effect.kind === "totem") categories.add("summon"); if (effect.kind === "pet") categories.add("pet"); if (effect.kind === "movement" || effect.kind === "pull") categories.add("movement"); if (effect.kind === "threat") categories.add("threat"); } if (EMERGENCY_HEAL_ABILITIES.has(name)) categories.add("emergency-heal"); if (((ability.radius ?? 0) > 0 || AREA_HEAL_ABILITIES.has(name)) && categories.has("heal")) categories.add("area-heal"); const targeting = targetingRulesForAbility(ability); if (categories.has("damage") && ( targeting.shape !== "unit" || (targeting.maximumTargets ?? 1) > 1 || (targeting.radius ?? ability.radius ?? 0) > 0 )) categories.add("area-damage"); return Object.freeze({ categories: Object.freeze([...categories]) }); }