Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* Small source-backed core for the current WoW Hunter pet baseline.
|
||||
*
|
||||
* The player and generated companion paths do not yet expose a stable/pet-family
|
||||
* picker, so they use the documented Wolf baseline. Bite rank values below are
|
||||
* the local 3.3.5a Spell.dbc rows (17253, 17255-17261, 27050, 52473-52474).
|
||||
* 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.
|
||||
*/
|
||||
export type Wow335HunterPetFamily = "wolf";
|
||||
import {
|
||||
normalizeWow335HunterPetFamily,
|
||||
type Wow335HunterPetFamily,
|
||||
} from "./wow335HunterPetFamilySelection";
|
||||
import { wow335HunterPetFamilySpellForLevel } from "./wow335HunterPetFamilyData";
|
||||
|
||||
export type { Wow335HunterPetFamily } from "./wow335HunterPetFamilySelection";
|
||||
|
||||
export interface Wow335HunterPetBaseStats {
|
||||
readonly level: number;
|
||||
@@ -33,14 +37,75 @@ export interface Wow335HunterPetProfile {
|
||||
readonly baseStats: Wow335HunterPetBaseStats;
|
||||
readonly effectiveStats: Wow335HunterPetEffectiveStats;
|
||||
readonly abilityId: string;
|
||||
readonly abilityName: "Bite";
|
||||
readonly abilityName: "Bite" | "Claw";
|
||||
readonly spellId: number;
|
||||
readonly rank: number;
|
||||
readonly requiredLevel: number;
|
||||
readonly baseDamage: number;
|
||||
readonly focusCost: number;
|
||||
readonly globalCooldownMs: number;
|
||||
readonly furiousHowl: Wow335HunterPetFuriousHowlProfile;
|
||||
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 {
|
||||
@@ -225,8 +290,136 @@ const WOLF_GROWL_RANKS: readonly Wow335HunterPetGrowlProfile[] = [
|
||||
{ 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<ReturnType<typeof wow335HunterPetFamilySpellForLevel>> {
|
||||
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;
|
||||
@@ -252,6 +445,33 @@ export function wow335HunterPetBiteDamage(
|
||||
+ 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,
|
||||
@@ -375,6 +595,47 @@ export function wow335HunterPetFuriousHowlAura(profile: Wow335HunterPetFuriousHo
|
||||
};
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -393,19 +654,53 @@ export function wow335HunterPetProfileForLevel(
|
||||
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 baseStats = wow335HunterPetBaseStatsForLevel(safeLevel);
|
||||
const sourceFamilyId = selectedFamily === "turtle" ? 21 : 1;
|
||||
const biteSource = wow335HunterPetFamilySpellForLevel(sourceFamilyId, "Bite", safeLevel);
|
||||
return {
|
||||
...rank,
|
||||
family,
|
||||
family: selectedFamily,
|
||||
baseStats,
|
||||
effectiveStats: wow335HunterPetEffectiveStatsForOwner(baseStats, ownerStats),
|
||||
abilityName: "Bite",
|
||||
abilityId: `wow335-hunter-pet-bite-rank-${rank.rank}`,
|
||||
focusCost: 25,
|
||||
globalCooldownMs: 1_500,
|
||||
furiousHowl: wow335HunterPetFuriousHowlProfileForLevel(safeLevel),
|
||||
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 }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user