/** * Small source-backed core for the selectable WoW Hunter pet families. * Bite ranks below are the local 3.3.5a Spell.dbc rows (17253, 17255-17261, * 27050, 52473-52474). Wolf and Turtle share these exact spell IDs. */ import { normalizeWow335HunterPetFamily, type Wow335HunterPetFamily, } from "./wow335HunterPetFamilySelection"; import { wow335HunterPetFamilySpellForLevel } from "./wow335HunterPetFamilyData"; export type { Wow335HunterPetFamily } from "./wow335HunterPetFamilySelection"; export interface Wow335HunterPetBaseStats { readonly level: number; readonly maxHealth: number; readonly strength: number; readonly attackPower: number; readonly minWeaponDamage: number; readonly maxWeaponDamage: number; readonly averageAutoAttackDamage: number; } export interface Wow335HunterPetOwnerCombatStats { readonly stamina: number; readonly rangedAttackPower: number; } export interface Wow335HunterPetEffectiveStats extends Wow335HunterPetBaseStats { readonly inheritedStamina: number; readonly inheritedRangedAttackPower: number; readonly inheritedSpellDamage: number; } export interface Wow335HunterPetProfile { readonly family: Wow335HunterPetFamily; readonly baseStats: Wow335HunterPetBaseStats; readonly effectiveStats: Wow335HunterPetEffectiveStats; readonly abilityId: string; readonly abilityName: "Bite" | "Claw"; readonly spellId: number; readonly rank: number; readonly requiredLevel: number; readonly baseDamage: number; readonly focusCost: number; readonly globalCooldownMs: number; readonly attackPowerCoefficient?: number; readonly spellDamageCoefficient?: number; readonly rake?: Wow335HunterPetRakeProfile; readonly prowl?: Wow335HunterPetProwlProfile; readonly furiousHowl?: Wow335HunterPetFuriousHowlProfile; readonly shellShield?: Wow335HunterPetShellShieldProfile; } export interface Wow335HunterPetRakeProfile { readonly abilityId: string; readonly spellId: number; readonly rank: number; readonly requiredLevel: number; readonly directDamage: number; readonly periodicDamage: number; /** Spell/effect mechanics retained to resolve core bleed armor rules. */ readonly directMechanic: number; readonly periodicMechanic: number; readonly periodicIgnoresArmor: boolean; readonly focusCost: number; readonly cooldownMs: number; readonly categoryCooldownMs: number; readonly globalCooldownMs: number; readonly durationMs: number; readonly intervalMs: number; readonly ticks: number; readonly attackPowerCoefficient: number; readonly spellDamageCoefficient: number; readonly periodicAttackPowerCoefficient: number; readonly periodicSpellDamageCoefficient: number; } export interface Wow335HunterPetProwlProfile { readonly abilityId: string; readonly spellId: number; readonly rank: number; readonly requiredLevel: number; readonly focusCost: number; readonly cooldownMs: number; readonly categoryCooldownMs: number; readonly globalCooldownMs: number; /** Source duration <= 0 represents the indefinite stance aura. */ readonly durationMs: number | null; readonly stealthLevel: number; readonly movementSpeedReductionPercent: number; readonly damageBonusPercent: number; readonly damageSchoolMask: number; readonly breaksOnIncomingDamage: boolean; readonly breaksOnSpellAttack: boolean; readonly breaksOnMeleeAutoAttack: boolean; } export interface Wow335HunterPetShellShieldProfile { readonly abilityId: string; readonly spellId: number; readonly requiredLevel: number; readonly focusCost: number; readonly durationMs: number; readonly cooldownMs: number; readonly globalCooldownMs: number; /** Spell.dbc AuraType 87's negative damage-taken amount, as a percent. */ readonly damageTakenPercent: number; } export interface Wow335HunterPetFuriousHowlProfile { readonly abilityId: string; readonly spellId: number; readonly rank: number; readonly requiredLevel: number; readonly attackPower: number; readonly focusCost: number; readonly durationMs: number; readonly cooldownMs: number; readonly globalCooldownMs: number; } export interface Wow335HunterPetGrowlProfile { readonly abilityId: string; readonly spellId: number; readonly rank: number; readonly requiredLevel: number; readonly focusCost: number; readonly cooldownMs: number; readonly globalCooldownMs: number; /** Flat threat from the rank's Spell.dbc SPELL_EFFECT_THREAT effect. */ readonly threatAmount: number; } export type Wow335HunterPetCommand = "attack" | "follow" | "stay"; export type Wow335HunterPetReactMode = "defensive" | "aggressive" | "passive"; export const WOW335_HUNTER_PET_DEFAULT_COMMAND = "follow" as const; export const WOW335_HUNTER_PET_DEFAULT_REACT_MODE = "defensive" as const; export interface Wow335HunterPetTargetPolicyInput { readonly command: Wow335HunterPetCommand; readonly reactMode: Wow335HunterPetReactMode; /** Candidate ids here are already filtered for life, hostility, and attackability. */ readonly eligibleTargetIds: ReadonlySet; readonly commandTargetId?: string | null; /** PetAI's transient command-attack flag can coexist with Follow/Stay command state. */ readonly isCommandAttack?: boolean; /** Null means the pet has no living, still-valid victim. */ readonly currentTargetId?: string | null; readonly petAttackerTargetIds?: readonly string[]; readonly ownerAttackerTargetIds?: readonly string[]; /** Supply the owner's victim only when the caller is processing an owner attack or target-selection event. */ readonly ownerTargetId?: string | null; readonly ambientTargetIds?: readonly string[]; readonly petAtStayPosition?: boolean; readonly isReturning?: boolean; /** PetAI flag set by an explicit Follow action; ordinary post-combat return clears it. */ readonly isCommandFollow?: boolean; readonly isFollowing?: boolean; readonly inMeleeRangeTargetIds?: ReadonlySet; } export interface Wow335HunterPetTargetPolicyDecision { readonly targetId: string | null; readonly source: "command" | "current" | "pet-attacker" | "owner-attacker" | "owner-target" | "ambient" | "none"; readonly movement: "chase-target" | "follow-owner" | "return-to-stay" | "hold" | "hold-stay"; readonly canAttackNow: boolean; } const WOLF_BITE_RANKS: readonly Omit[] = [ { spellId: 17_253, rank: 1, requiredLevel: 1, baseDamage: 4 }, { spellId: 17_255, rank: 2, requiredLevel: 8, baseDamage: 8 }, { spellId: 17_256, rank: 3, requiredLevel: 16, baseDamage: 12 }, { spellId: 17_257, rank: 4, requiredLevel: 24, baseDamage: 16 }, { spellId: 17_258, rank: 5, requiredLevel: 32, baseDamage: 21 }, { spellId: 17_259, rank: 6, requiredLevel: 40, baseDamage: 26 }, { spellId: 17_260, rank: 7, requiredLevel: 48, baseDamage: 35 }, { spellId: 17_261, rank: 8, requiredLevel: 56, baseDamage: 43 }, { spellId: 27_050, rank: 9, requiredLevel: 64, baseDamage: 54 }, { spellId: 52_473, rank: 10, requiredLevel: 72, baseDamage: 91 }, { spellId: 52_474, rank: 11, requiredLevel: 80, baseDamage: 118 }, ]; // Local AzerothCore 3.3.5a db_world.pet_levelstats entry 1, levels 1-80. // TrinityCore 3.3.5 Pet::InitStatsForLevel uses entry 1 for HUNTER_PET, while // Guardian::UpdateAttackPowerAndDamage defines ordinary pet AP as 2 * STR - 20. // Hunter pet base weapon bounds are set in that same initializer to level +/- // floor(level / 4), with a 2-second base attack time. const HUNTER_PET_HEALTH_BY_LEVEL: readonly number[] = [ 42, 55, 71, 86, 102, 120, 137, 156, 176, 198, 222, 247, 273, 300, 328, 356, 386, 417, 449, 484, 521, 562, 605, 651, 699, 750, 800, 853, 905, 955, 1006, 1057, 1110, 1163, 1220, 1277, 1336, 1395, 1459, 1524, 1585, 1651, 1716, 1782, 1848, 1919, 1990, 2062, 2138, 2215, 2292, 2371, 2453, 2533, 2614, 2699, 2784, 2871, 2961, 3052, 3144, 3237, 3331, 3425, 3524, 3624, 3728, 3834, 3941, 4051, 4162, 4273, 4384, 4495, 4606, 4717, 4828, 4939, 5050, 5161, ]; const HUNTER_PET_STRENGTH_BY_LEVEL: readonly number[] = [ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 38, 40, 42, 44, 45, 47, 49, 50, 52, 53, 55, 56, 58, 60, 61, 63, 64, 66, 67, 69, 70, 72, 74, 76, 78, 81, 86, 91, 97, 102, 104, 106, 108, 110, 113, 115, 117, 119, 122, 124, 127, 129, 131, 134, 136, 139, 141, 144, 146, 149, 151, 154, 156, 159, 162, 165, 168, 171, 174, 177, 180, 183, 186, 189, 192, ]; /** Stock 3.3.5a base stats for the generic Hunter pet entry. */ export function wow335HunterPetBaseStatsForLevel( level: number, ): Wow335HunterPetBaseStats { const safeLevel = Math.max(1, Math.min(80, Math.trunc(level))); const index = safeLevel - 1; const strength = HUNTER_PET_STRENGTH_BY_LEVEL[index]!; const attackPower = 2 * strength - 20; const minWeaponDamage = safeLevel - Math.floor(safeLevel / 4); const maxWeaponDamage = safeLevel + Math.floor(safeLevel / 4); return { level: safeLevel, maxHealth: HUNTER_PET_HEALTH_BY_LEVEL[index]!, strength, attackPower, minWeaponDamage, maxWeaponDamage, averageAutoAttackDamage: Math.round((minWeaponDamage + maxWeaponDamage) / 2 + attackPower / 7), }; } /** Apply the stock Hunter-pet stamina and ranged-AP inheritance rules. */ export function wow335HunterPetEffectiveStatsForOwner( baseStats: Wow335HunterPetBaseStats, ownerStats: Wow335HunterPetOwnerCombatStats, ): Wow335HunterPetEffectiveStats { const ownerStamina = Math.max(0, Number.isFinite(ownerStats.stamina) ? ownerStats.stamina : 0); const ownerRangedAttackPower = Math.max( 0, Number.isFinite(ownerStats.rangedAttackPower) ? ownerStats.rangedAttackPower : 0, ); const inheritedStamina = ownerStamina * 0.45; const inheritedRangedAttackPower = ownerRangedAttackPower * 0.22; const inheritedSpellDamage = ownerRangedAttackPower * WOW335_HUNTER_PET_RANGED_AP_TO_SPELL_DAMAGE; const attackPower = baseStats.attackPower + inheritedRangedAttackPower; return { ...baseStats, maxHealth: baseStats.maxHealth + Math.round(inheritedStamina * 10), attackPower, averageAutoAttackDamage: Math.round( (baseStats.minWeaponDamage + baseStats.maxWeaponDamage) / 2 + attackPower / 7, ), inheritedStamina, inheritedRangedAttackPower, inheritedSpellDamage, }; } // Spell.dbc rows 24604 and 64491-64495. DBC stores AP as basePoints + 1; // spellLevel is the pet level requirement. All ranks cost 20 Focus, last 20s, // have a 40s recovery, and have no StartRecoveryTime (pet GCD). const WOLF_FURIOUS_HOWL_RANKS: readonly Wow335HunterPetFuriousHowlProfile[] = [ { abilityId: "wow335-hunter-pet-furious-howl-rank-1", spellId: 24_604, rank: 1, requiredLevel: 1, attackPower: 18, focusCost: 20, durationMs: 20_000, cooldownMs: 40_000, globalCooldownMs: 0 }, { abilityId: "wow335-hunter-pet-furious-howl-rank-2", spellId: 64_491, rank: 2, requiredLevel: 16, attackPower: 43, focusCost: 20, durationMs: 20_000, cooldownMs: 40_000, globalCooldownMs: 0 }, { abilityId: "wow335-hunter-pet-furious-howl-rank-3", spellId: 64_492, rank: 3, requiredLevel: 32, attackPower: 70, focusCost: 20, durationMs: 20_000, cooldownMs: 40_000, globalCooldownMs: 0 }, { abilityId: "wow335-hunter-pet-furious-howl-rank-4", spellId: 64_493, rank: 4, requiredLevel: 48, attackPower: 120, focusCost: 20, durationMs: 20_000, cooldownMs: 40_000, globalCooldownMs: 0 }, { abilityId: "wow335-hunter-pet-furious-howl-rank-5", spellId: 64_494, rank: 5, requiredLevel: 64, attackPower: 204, focusCost: 20, durationMs: 20_000, cooldownMs: 40_000, globalCooldownMs: 0 }, { abilityId: "wow335-hunter-pet-furious-howl-rank-6", spellId: 64_495, rank: 6, requiredLevel: 80, attackPower: 320, focusCost: 20, durationMs: 20_000, cooldownMs: 40_000, globalCooldownMs: 0 }, ]; // Local 3.3.5a Spell.dbc Growl ranks (2649, 14916-14921, 27047, 61676). // Each rank uses SPELL_EFFECT_THREAT (63), costs 15 Focus, has a 5s category // cooldown, and starts no pet GCD. The DBC payload is flat threat; it does not // define a timed forced-target/taunt effect. const WOLF_GROWL_RANKS: readonly Wow335HunterPetGrowlProfile[] = [ { abilityId: "wow335-hunter-pet-growl-rank-1", spellId: 2_649, rank: 1, requiredLevel: 1, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 50 }, { abilityId: "wow335-hunter-pet-growl-rank-2", spellId: 14_916, rank: 2, requiredLevel: 10, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 65 }, { abilityId: "wow335-hunter-pet-growl-rank-3", spellId: 14_917, rank: 3, requiredLevel: 20, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 110 }, { abilityId: "wow335-hunter-pet-growl-rank-4", spellId: 14_918, rank: 4, requiredLevel: 30, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 170 }, { abilityId: "wow335-hunter-pet-growl-rank-5", spellId: 14_919, rank: 5, requiredLevel: 40, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 240 }, { abilityId: "wow335-hunter-pet-growl-rank-6", spellId: 14_920, rank: 6, requiredLevel: 50, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 320 }, { abilityId: "wow335-hunter-pet-growl-rank-7", spellId: 14_921, rank: 7, requiredLevel: 60, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 415 }, { abilityId: "wow335-hunter-pet-growl-rank-8", spellId: 27_047, rank: 8, requiredLevel: 70, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 664 }, { abilityId: "wow335-hunter-pet-growl-rank-9", spellId: 61_676, rank: 9, requiredLevel: 80, focusCost: 15, cooldownMs: 5_000, globalCooldownMs: 0, threatAmount: 1_234 }, ]; // Turtle family skill line 251's local SkillLineAbility.dbc / Spell.dbc row: // Shell Shield (26064), spell level 1, 0 Focus, 60s cooldown, 12s duration, // AuraType 87 with basePoints -50. Selection is level-gated to that source row. const TURTLE_SHELL_SHIELD: Wow335HunterPetShellShieldProfile = { abilityId: "wow335-hunter-pet-shell-shield", spellId: 26_064, requiredLevel: 1, focusCost: 0, durationMs: 12_000, cooldownMs: 60_000, globalCooldownMs: 0, damageTakenPercent: 50, }; const CAT_FAMILY_ID = 2; const WOW335_MECHANIC_BLEED = 15; const WOW335_AURA_INTERRUPT_FLAG_HIT_BY_SPELL = 0x0000_0001; const WOW335_AURA_INTERRUPT_FLAG_TAKE_DAMAGE = 0x0000_0002; const WOW335_AURA_INTERRUPT_FLAG_SPELL_ATTACK = 0x0000_2000; const WOW335_PROC_FLAG_DONE_MELEE_AUTO_ATTACK = 0x0000_0004; const WOW335_SPELL_SCHOOL_MASKS = [ { mask: 0x01, school: "physical" as const }, { mask: 0x02, school: "holy" as const }, { mask: 0x04, school: "fire" as const }, { mask: 0x08, school: "nature" as const }, { mask: 0x10, school: "frost" as const }, { mask: 0x20, school: "shadow" as const }, { mask: 0x40, school: "arcane" as const }, ] as const; // Local AzerothCore spell_bonus_data rows for all Cat Claw ranks (direct .119658 / AP .07) // and Rake ranks (direct and periodic spell .0299 / AP .0175). const WOW335_HUNTER_PET_CLAW_ATTACK_POWER_COEFFICIENT = 0.07; const WOW335_HUNTER_PET_CLAW_SPELL_DAMAGE_COEFFICIENT = 0.119658; const WOW335_HUNTER_PET_RAKE_ATTACK_POWER_COEFFICIENT = 0.0175; const WOW335_HUNTER_PET_RAKE_SPELL_DAMAGE_COEFFICIENT = 0.0299; function sourceRankNumber(rankText: string): number { return Number(rankText.match(/^Rank\s+(\d+)$/i)?.[1] ?? 1); } /** Simulator attacks use the midpoint of the Spell.dbc base-points/die-sides range. */ function sourceDamageMidpoint(effect: { readonly basePoints: number; readonly dieSides: number }): number { const minimum = effect.basePoints; const maximum = minimum + Math.max(0, Math.abs(effect.dieSides) - 1); return (minimum + maximum) / 2; } function catSpellEffect( spellName: "Claw" | "Rake" | "Prowl", level: number, ): NonNullable> { const spell = wow335HunterPetFamilySpellForLevel(CAT_FAMILY_ID, spellName, level); if (!spell) throw new Error(`Cat pet skill line is missing a learned ${spellName} rank.`); return spell; } function wow335HunterCatRakeProfileForLevel(level: number): Wow335HunterPetRakeProfile { const source = catSpellEffect("Rake", level); const direct = source.effects.find((effect) => effect.index === 0 && effect.effectType === 2); const periodic = source.effects.find((effect) => effect.index === 1 && effect.effectType === 6 && effect.auraType === 3); const intervalMs = periodic?.periodMs ?? 0; const ticks = intervalMs > 0 && source.durationMs > 0 && source.durationMs % intervalMs === 0 ? source.durationMs / intervalMs : 0; if (!direct || !periodic || ticks <= 0) { throw new Error(`Cat Rake rank ${source.spellId} has unsupported local damage effects.`); } return { abilityId: `wow335-hunter-pet-rake-rank-${sourceRankNumber(source.rankText)}`, spellId: source.spellId, rank: sourceRankNumber(source.rankText), requiredLevel: source.requiredLevel, directDamage: sourceDamageMidpoint(direct), periodicDamage: sourceDamageMidpoint(periodic), directMechanic: source.mechanic || direct.mechanic, periodicMechanic: source.mechanic || periodic.mechanic, periodicIgnoresArmor: source.mechanic === WOW335_MECHANIC_BLEED || periodic.mechanic === WOW335_MECHANIC_BLEED, focusCost: source.powerCost, cooldownMs: source.cooldownMs, categoryCooldownMs: source.categoryCooldownMs, globalCooldownMs: source.globalCooldownMs, durationMs: source.durationMs, intervalMs, ticks, attackPowerCoefficient: WOW335_HUNTER_PET_RAKE_ATTACK_POWER_COEFFICIENT, spellDamageCoefficient: WOW335_HUNTER_PET_RAKE_SPELL_DAMAGE_COEFFICIENT, periodicAttackPowerCoefficient: WOW335_HUNTER_PET_RAKE_ATTACK_POWER_COEFFICIENT, periodicSpellDamageCoefficient: WOW335_HUNTER_PET_RAKE_SPELL_DAMAGE_COEFFICIENT, }; } function wow335HunterCatProwlProfileForLevel(level: number): Wow335HunterPetProwlProfile | null { const source = wow335HunterPetFamilySpellForLevel(CAT_FAMILY_ID, "Prowl", level); if (!source) return null; const stealth = source.effects.find((effect) => effect.auraType === 16); const slow = source.effects.find((effect) => effect.auraType === 33); const damage = source.effects.find((effect) => effect.auraType === 79); if (!stealth || !slow || !damage) { throw new Error(`Cat Prowl rank ${source.spellId} has unsupported local aura effects.`); } return { abilityId: WOW335_HUNTER_PET_PROWL_TAG, spellId: source.spellId, rank: sourceRankNumber(source.rankText), requiredLevel: source.requiredLevel, focusCost: source.powerCost, cooldownMs: source.cooldownMs, categoryCooldownMs: source.categoryCooldownMs, globalCooldownMs: source.globalCooldownMs, durationMs: source.durationMs > 0 ? source.durationMs : null, stealthLevel: stealth.basePoints, movementSpeedReductionPercent: Math.abs(slow.basePoints), damageBonusPercent: damage.basePoints, damageSchoolMask: damage.miscValue, breaksOnIncomingDamage: Boolean(source.auraInterruptFlags & ( WOW335_AURA_INTERRUPT_FLAG_HIT_BY_SPELL | WOW335_AURA_INTERRUPT_FLAG_TAKE_DAMAGE )), breaksOnSpellAttack: Boolean(source.auraInterruptFlags & WOW335_AURA_INTERRUPT_FLAG_SPELL_ATTACK), breaksOnMeleeAutoAttack: source.procCharges > 0 && Boolean(source.procFlags & WOW335_PROC_FLAG_DONE_MELEE_AUTO_ATTACK), }; } export const WOW335_HUNTER_PET_KILL_COMMAND_TAG = "wow335-hunter-kill-command"; export const WOW335_HUNTER_PET_FURIOUS_HOWL_TAG = "wow335-hunter-pet-furious-howl"; export const WOW335_HUNTER_PET_SHELL_SHIELD_TAG = "wow335-hunter-pet-shell-shield"; export const WOW335_HUNTER_PET_PROWL_TAG = "wow335-hunter-pet-prowl"; export const WOW335_HUNTER_PET_RAKE_TAG = "wow335-hunter-pet-rake"; export const WOW335_HUNTER_PET_AUTO_ATTACK_TIME_MS = 2_000; export const WOW335_HUNTER_PET_KILL_COMMAND_DURATION_MS = 30_000; export const WOW335_HUNTER_PET_KILL_COMMAND_COOLDOWN_MS = 60_000; export const WOW335_HUNTER_PET_KILL_COMMAND_INITIAL_BONUS_PERCENT = 60; export const WOW335_HUNTER_PET_KILL_COMMAND_DECAY_PERCENT = 20; export const WOW335_HUNTER_PET_KILL_COMMAND_SPECIAL_CHARGES = 3; // Local AzerothCore spell_bonus_data rows for Bite ranks 17253-17261, 27050, // 52473, and 52474 list direct spell damage 0.119658 and AP 0.07 coefficients. // The 3.3.5a PaperDollFrame.lua UI source lists pet spell damage as 12.87% RAP. export const WOW335_HUNTER_PET_RANGED_AP_TO_SPELL_DAMAGE = 0.1287; export const WOW335_HUNTER_PET_BITE_SPELL_DAMAGE_COEFFICIENT = 0.119658; export const WOW335_HUNTER_PET_BITE_ATTACK_POWER_COEFFICIENT = 0.07; /** Bite base damage plus the source-listed pet-AP and pet-spell-damage bonuses. */ export function wow335HunterPetBiteDamage( profile: Wow335HunterPetProfile, additionalAttackPower = 0, ): number { const extraAttackPower = Number.isFinite(additionalAttackPower) ? additionalAttackPower : 0; const attackPower = Math.max(0, profile.effectiveStats.attackPower + extraAttackPower); return profile.baseDamage + attackPower * WOW335_HUNTER_PET_BITE_ATTACK_POWER_COEFFICIENT + profile.effectiveStats.inheritedSpellDamage * WOW335_HUNTER_PET_BITE_SPELL_DAMAGE_COEFFICIENT; } /** Claw's sourced per-rank bonus coefficients over the DBC base damage. */ export function wow335HunterPetClawDamage( profile: Wow335HunterPetProfile, additionalAttackPower = 0, ): number { const attackPower = Math.max(0, profile.effectiveStats.attackPower + (Number.isFinite(additionalAttackPower) ? additionalAttackPower : 0)); return profile.baseDamage + attackPower * (profile.attackPowerCoefficient ?? 0) + profile.effectiveStats.inheritedSpellDamage * (profile.spellDamageCoefficient ?? 0); } /** Rake's sourced direct or periodic spell/AP coefficients over its DBC range. */ export function wow335HunterPetRakeDamage( profile: Wow335HunterPetProfile, rake: Wow335HunterPetRakeProfile, periodic = false, additionalAttackPower = 0, ): number { const attackPower = Math.max(0, profile.effectiveStats.attackPower + (Number.isFinite(additionalAttackPower) ? additionalAttackPower : 0)); return (periodic ? rake.periodicDamage : rake.directDamage) + attackPower * (periodic ? rake.periodicAttackPowerCoefficient : rake.attackPowerCoefficient) + profile.effectiveStats.inheritedSpellDamage * (periodic ? rake.periodicSpellDamageCoefficient : rake.spellDamageCoefficient); } /** Furious Howl rank learned by this Wolf, selected from Spell.dbc spellLevel. */ export function wow335HunterPetFuriousHowlProfileForLevel( level: number, ): Wow335HunterPetFuriousHowlProfile { const safeLevel = Math.max(1, Math.min(80, Math.trunc(level))); return [...WOLF_FURIOUS_HOWL_RANKS].reverse().find((candidate) => candidate.requiredLevel <= safeLevel) ?? WOLF_FURIOUS_HOWL_RANKS[0]!; } /** Growl rank learned by this Wolf, selected from local Spell.dbc spellLevel. */ export function wow335HunterPetGrowlProfileForLevel(level: number): Wow335HunterPetGrowlProfile { const safeLevel = Number.isFinite(level) ? Math.max(1, Math.min(80, Math.trunc(level))) : 1; return [...WOLF_GROWL_RANKS].reverse().find((candidate) => candidate.requiredLevel <= safeLevel) ?? WOLF_GROWL_RANKS[0]!; } /** * Resolve stock 3.3.5 pet command/react target priority from already-eligible ids. * Owner-target candidates are supplied by the caller only for an OwnerAttacked * event or a source path that actually runs next-target selection. */ export function wow335HunterPetTargetPolicy( input: Wow335HunterPetTargetPolicyInput, ): Wow335HunterPetTargetPolicyDecision { const isEligible = (targetId: string | null | undefined): targetId is string => Boolean( targetId && input.eligibleTargetIds.has(targetId), ); let targetId: string | null = null; let source: Wow335HunterPetTargetPolicyDecision["source"] = "none"; const hasExplicitAttackOrder = input.command === "attack" || Boolean(input.isCommandAttack); // The Follow action calls AttackStop and marks the pet as returning. A pet // that has reached its follow position can engage again; an active victim // acquired after that point is still retained while the command state says // Follow. Stay does not clear the victim when its command is issued. const returningToOwner = input.command === "follow" && Boolean(input.isReturning) && Boolean(input.isCommandFollow); const isSelectable = (candidateId: string): boolean => input.eligibleTargetIds.has(candidateId) && (input.command !== "stay" || hasExplicitAttackOrder || Boolean(input.inMeleeRangeTargetIds?.has(candidateId))); if (hasExplicitAttackOrder) { if (isEligible(input.commandTargetId)) { targetId = input.commandTargetId; source = "command"; } } if (!targetId && !returningToOwner && isEligible(input.currentTargetId) && (input.reactMode !== "passive" || hasExplicitAttackOrder)) { // PetAI keeps a living victim until it dies/becomes invalid. An explicit // Follow action already clears the victim; issuing Stay does not. Switching // to Passive also calls AttackStop, unless the later Attack order is active. targetId = input.currentTargetId; source = hasExplicitAttackOrder && isEligible(input.commandTargetId) ? "command" : "current"; } else if (!targetId && !returningToOwner && input.reactMode !== "passive") { // Passive pets do not auto-select. A fresh explicit Attack is handled above. const firstEligible = (ids: readonly string[] | undefined): string | null => ( ids?.find(isSelectable) ?? null ); targetId = firstEligible(input.petAttackerTargetIds); if (targetId) source = "pet-attacker"; if (!targetId) { targetId = firstEligible(input.ownerAttackerTargetIds); if (targetId) source = "owner-attacker"; } if (!targetId && input.ownerTargetId && isSelectable(input.ownerTargetId)) { targetId = input.ownerTargetId; source = "owner-target"; } const canAggressivelyAutoSelect = !input.isReturning || input.isFollowing || input.petAtStayPosition; if (!targetId && input.reactMode === "aggressive" && canAggressivelyAutoSelect) { targetId = firstEligible(input.ambientTargetIds); if (targetId) source = "ambient"; } } const inMeleeRange = Boolean( targetId && input.inMeleeRangeTargetIds?.has(targetId), ); const canAttackNow = Boolean( targetId && inMeleeRange && (input.command !== "stay" || input.petAtStayPosition || hasExplicitAttackOrder), ); let movement: Wow335HunterPetTargetPolicyDecision["movement"]; if (targetId) { movement = input.command === "stay" && !hasExplicitAttackOrder ? "hold-stay" : "chase-target"; } else if (input.command === "follow") { movement = "follow-owner"; } else if (input.command === "stay") { movement = input.petAtStayPosition ? "hold" : "return-to-stay"; } else { movement = "hold"; } return { targetId, source, movement, canAttackNow }; } export function wow335HunterPetFuriousHowlAura(profile: Wow335HunterPetFuriousHowlProfile) { return { id: WOW335_HUNTER_PET_FURIOUS_HOWL_TAG, name: "Furious Howl", description: `Increases melee and ranged attack power by ${profile.attackPower} for the Wolf and its master for 20 seconds.`, disposition: "buff" as const, durationMs: profile.durationMs, maxStacks: 1, stackBehavior: "refresh" as const, modifiers: [ { kind: "stat" as const, stat: "attack-power", operation: "flat" as const, value: profile.attackPower }, { kind: "stat" as const, stat: "ranged-attack-power", operation: "flat" as const, value: profile.attackPower }, ], utilityTags: [WOW335_HUNTER_PET_FURIOUS_HOWL_TAG], }; } /** Turtle's DBC AuraType 87 applies a 50% all-damage-taken reduction. */ export function wow335HunterPetShellShieldAura(profile: Wow335HunterPetShellShieldProfile) { return { id: WOW335_HUNTER_PET_SHELL_SHIELD_TAG, name: "Shell Shield", description: `Reduces the Turtle's damage taken by ${profile.damageTakenPercent}% for 12 seconds.`, disposition: "buff" as const, durationMs: profile.durationMs, maxStacks: 1, stackBehavior: "refresh" as const, modifiers: [{ kind: "damage" as const, direction: "taken" as const, operation: "percent" as const, value: -profile.damageTakenPercent / 100, }], utilityTags: [WOW335_HUNTER_PET_SHELL_SHIELD_TAG], }; } /** Cat Prowl's local AuraType 16/33/79 effects belong to the pet itself. */ export function wow335HunterPetProwlAura(profile: Wow335HunterPetProwlProfile) { const damageModifiers = WOW335_SPELL_SCHOOL_MASKS.flatMap(({ mask, school }) => ( profile.damageSchoolMask & mask ? [{ kind: "damage" as const, direction: "dealt" as const, school, operation: "percent" as const, value: profile.damageBonusPercent / 100 }] : [] )); return { id: WOW335_HUNTER_PET_PROWL_TAG, name: "Prowl", description: `Stealthed; movement speed reduced by ${profile.movementSpeedReductionPercent}%; next attack gains ${profile.damageBonusPercent}% damage.`, disposition: "buff" as const, durationMs: profile.durationMs, maxStacks: 1, stackBehavior: "refresh" as const, modifiers: damageModifiers, utilityTags: ["stealth", WOW335_HUNTER_PET_PROWL_TAG], }; } /** * A flat pet AP increase adds AP / 14 * weapon speed seconds to each melee * swing. Hunter pets use the 2-second BASE_ATTACK_TIME in the local 3.3.5 * Pet::InitStatsForLevel source. */ export function wow335HunterPetFuriousHowlAutoAttackDamageBonus( profile: Wow335HunterPetFuriousHowlProfile, ): number { return profile.attackPower * (WOW335_HUNTER_PET_AUTO_ATTACK_TIME_MS / 1_000) / 14; } /** Select the highest Bite rank present in the local 3.3.5a DBC at this pet level. */ export function wow335HunterPetProfileForLevel( level: number, family: Wow335HunterPetFamily = "wolf", ownerStats: Wow335HunterPetOwnerCombatStats = { stamina: 0, rangedAttackPower: 0 }, ): Wow335HunterPetProfile { const safeLevel = Math.max(1, Math.min(80, Math.trunc(level))); const selectedFamily = normalizeWow335HunterPetFamily(family); const baseStats = wow335HunterPetBaseStatsForLevel(safeLevel); const effectiveStats = wow335HunterPetEffectiveStatsForOwner(baseStats, ownerStats); if (selectedFamily === "cat") { const clawSource = catSpellEffect("Claw", safeLevel); const clawDamage = clawSource.effects.find((effect) => effect.effectType === 2); if (!clawDamage) throw new Error(`Cat Claw rank ${clawSource.spellId} is missing its direct damage effect.`); const rank = sourceRankNumber(clawSource.rankText); return { family: selectedFamily, baseStats, effectiveStats, abilityId: `wow335-hunter-pet-claw-rank-${rank}`, abilityName: "Claw", spellId: clawSource.spellId, rank, requiredLevel: clawSource.requiredLevel, baseDamage: sourceDamageMidpoint(clawDamage), focusCost: clawSource.powerCost, globalCooldownMs: clawSource.globalCooldownMs, attackPowerCoefficient: WOW335_HUNTER_PET_CLAW_ATTACK_POWER_COEFFICIENT, spellDamageCoefficient: WOW335_HUNTER_PET_CLAW_SPELL_DAMAGE_COEFFICIENT, rake: wow335HunterCatRakeProfileForLevel(safeLevel), prowl: wow335HunterCatProwlProfileForLevel(safeLevel) ?? undefined, }; } const rank = [...WOLF_BITE_RANKS].reverse().find((candidate) => candidate.requiredLevel <= safeLevel) ?? WOLF_BITE_RANKS[0]!; const sourceFamilyId = selectedFamily === "turtle" ? 21 : 1; const biteSource = wow335HunterPetFamilySpellForLevel(sourceFamilyId, "Bite", safeLevel); return { ...rank, family: selectedFamily, baseStats, effectiveStats: wow335HunterPetEffectiveStatsForOwner(baseStats, ownerStats), abilityName: "Bite", abilityId: `wow335-hunter-pet-bite-rank-${rank.rank}`, focusCost: biteSource?.powerCost ?? 25, globalCooldownMs: biteSource?.globalCooldownMs ?? 1_500, ...(selectedFamily === "wolf" ? { furiousHowl: wow335HunterPetFuriousHowlProfileForLevel(safeLevel) } : {}), ...(selectedFamily === "turtle" && safeLevel >= TURTLE_SHELL_SHIELD.requiredLevel ? { shellShield: TURTLE_SHELL_SHIELD } : {}), }; } /** Kill Command starts at 60%; each landed pet special attack removes 20 points. */ export function wow335HunterPetKillCommandBonusPercent(remainingSpecialCharges: number): number { return Math.max(0, Math.min( WOW335_HUNTER_PET_KILL_COMMAND_INITIAL_BONUS_PERCENT, Math.trunc(remainingSpecialCharges) * WOW335_HUNTER_PET_KILL_COMMAND_DECAY_PERCENT, )); }