forked from phenom/healer-man
195 lines
7.2 KiB
TypeScript
195 lines
7.2 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { applyAura } from "./combatAuras";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { clearPartyRuntimeRegistry, updatePartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore";
|
|
import { usePartyStore } from "./partyStore";
|
|
import type { EnemyAttackDefinition } from "./enemyAttacks";
|
|
import { WOW335_DRUID_PREDATORY_INSTINCTS_TALENT_ID } from "./wow335DruidPredatoryInstinctsRuntime";
|
|
|
|
const TARGET_ID = "druid-predatory-instincts-target";
|
|
const ENEMY_ID = "druid-predatory-instincts-area-enemy";
|
|
const AREA_ATTACK: EnemyAttackDefinition = {
|
|
id: "predatory-area-hit",
|
|
name: "Predatory Instincts Area Hit",
|
|
delivery: "area",
|
|
target: "nearby-party",
|
|
school: "fire",
|
|
animation: "cast",
|
|
castTimeMs: 0,
|
|
range: 80,
|
|
radius: 80,
|
|
cooldownMs: 10_000,
|
|
initialCooldownMs: [0, 0],
|
|
repeatCooldownMs: [10_000, 10_000],
|
|
damageMultiplier: 1,
|
|
};
|
|
|
|
function stableUnit(seed: string): number {
|
|
let hash = 2_166_136_261;
|
|
for (let index = 0; index < seed.length; index += 1) {
|
|
hash ^= seed.charCodeAt(index);
|
|
hash = Math.imul(hash, 16_777_619);
|
|
}
|
|
return (hash >>> 0) / 0x1_0000_0000;
|
|
}
|
|
|
|
function areaHitTime(actorId: string): number {
|
|
for (let now = 3_101; now < 20_000; now += 1) {
|
|
if (stableUnit(`${ENEMY_ID}:${AREA_ATTACK.id}:${actorId}:0:${now}:outcome`) >= 0.2) return now;
|
|
}
|
|
throw new Error(`Could not find an area-hit roll for ${actorId}`);
|
|
}
|
|
|
|
function catFormAura(actorId: string, now = 1_000) {
|
|
return {
|
|
definition: {
|
|
id: "wow335:768:cat-form",
|
|
name: "Cat Form",
|
|
disposition: "buff" as const,
|
|
durationMs: null,
|
|
},
|
|
sourceId: actorId,
|
|
targetId: actorId,
|
|
now,
|
|
};
|
|
}
|
|
|
|
function setCatForm(actorId: string): void {
|
|
useCombatStore.setState((state) => ({
|
|
auras: [...applyAura(state.auras, catFormAura(actorId)).auras],
|
|
}));
|
|
}
|
|
|
|
function registerCriticalTarget(): void {
|
|
useCombatStore.getState().registerMob(TARGET_ID, { name: TARGET_ID, maxHealth: 1_000_000, xpReward: 0 });
|
|
useCombatStore.setState((state) => ({
|
|
mobs: {
|
|
...state.mobs,
|
|
[TARGET_ID]: {
|
|
...state.mobs[TARGET_ID]!,
|
|
characterDefender: {
|
|
level: 80,
|
|
armor: 0,
|
|
dodgePercent: 0,
|
|
parryPercent: 0,
|
|
blockPercent: 0,
|
|
blockValue: 0,
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
setMobPosition(TARGET_ID, [4, 0, 0]);
|
|
}
|
|
|
|
function criticalDamageForSource(sourceActorId: string, abilityId: string): number {
|
|
const member = usePartyStore.getState().members.find((candidate) => candidate.id === sourceActorId);
|
|
useCombatStore.getState().damageMob(TARGET_ID, 100, 2_000, {
|
|
actorId: sourceActorId,
|
|
role: member?.role ?? "damage",
|
|
abilityId,
|
|
}, "physical", {
|
|
attackKind: "melee",
|
|
seed: `druid-predatory-critical-${sourceActorId}-${abilityId}`,
|
|
abilityId,
|
|
criticalChanceBonusPercent: 1_000,
|
|
rules: { canMiss: false, canCrit: true, canDodge: false, canParry: false, canBlock: false },
|
|
});
|
|
return useCombatStore.getState().lastCombatResult!.damageBeforeMitigation;
|
|
}
|
|
|
|
function areaDamageForActor(actorKind: "player" | "companion", rank: number): number {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
|
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
|
usePartyStore.getState().initializeParty(`druid-predatory-area-${actorKind}`, 1, useCombatStore.getState().health);
|
|
const firstMember = usePartyStore.getState().members[0]!;
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member, index) => index === 0
|
|
? {
|
|
...member,
|
|
classId: "druid",
|
|
talentRanks: { ...member.talentRanks, [WOW335_DRUID_PREDATORY_INSTINCTS_TALENT_ID]: rank },
|
|
}
|
|
: member),
|
|
}));
|
|
for (const member of usePartyStore.getState().members) updatePartyRuntimePosition(member.id, 0, 0, 0);
|
|
useCombatStore.getState().registerMob(ENEMY_ID, {
|
|
name: ENEMY_ID,
|
|
level: 80,
|
|
maxHealth: 1_000,
|
|
xpReward: 0,
|
|
attacks: [{
|
|
...AREA_ATTACK,
|
|
target: actorKind === "player" ? "primary" : "nearby-party",
|
|
}],
|
|
});
|
|
setMobPosition(ENEMY_ID, [2, 0, 0]);
|
|
useCombatStore.setState((state) => ({
|
|
talentRanks: { ...state.talentRanks, [WOW335_DRUID_PREDATORY_INSTINCTS_TALENT_ID]: rank },
|
|
mobs: { ...state.mobs, [ENEMY_ID]: { ...state.mobs[ENEMY_ID]!, attackDamage: 100 } },
|
|
}));
|
|
|
|
const actorId = actorKind === "player" ? PLAYER_AURA_ENTITY_ID : firstMember.id;
|
|
if (actorKind === "player") setCatForm(actorId);
|
|
else {
|
|
useCombatStore.setState((state) => ({ auras: [...applyAura(state.auras, catFormAura(actorId)).auras] }));
|
|
}
|
|
const healthBefore = actorKind === "player"
|
|
? useCombatStore.getState().health
|
|
: usePartyStore.getState().members.find((member) => member.id === actorId)!.health;
|
|
useCombatStore.getState().damageMob(ENEMY_ID, 1, 1_449);
|
|
useCombatStore.getState().advanceMobCombat(1_450);
|
|
useCombatStore.getState().advanceMobCombat(areaHitTime(actorId));
|
|
const healthAfter = actorKind === "player"
|
|
? useCombatStore.getState().health
|
|
: usePartyStore.getState().members.find((member) => member.id === actorId)!.health;
|
|
return healthBefore - healthAfter;
|
|
}
|
|
|
|
describe("WoW Druid Predatory Instincts integration", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
|
|
usePartyStore.getState().initializeParty("druid-predatory-critical-tests", 1, useCombatStore.getState().health);
|
|
});
|
|
|
|
it("applies the Cat Form critical-damage rank to the player and generated companion", () => {
|
|
registerCriticalTarget();
|
|
useCombatStore.setState((state) => ({
|
|
talentRanks: { ...state.talentRanks, [WOW335_DRUID_PREDATORY_INSTINCTS_TALENT_ID]: 3 },
|
|
}));
|
|
const playerWithoutCat = criticalDamageForSource(PLAYER_AGGRO_ID, "predatory-player-test");
|
|
setCatForm(PLAYER_AURA_ENTITY_ID);
|
|
const playerWithCat = criticalDamageForSource(PLAYER_AGGRO_ID, "predatory-player-test-cat");
|
|
|
|
const member = usePartyStore.getState().members[0]!;
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((candidate) => candidate.id === member.id ? {
|
|
...candidate,
|
|
classId: "druid",
|
|
talentRanks: { ...candidate.talentRanks, [WOW335_DRUID_PREDATORY_INSTINCTS_TALENT_ID]: 3 },
|
|
} : candidate),
|
|
}));
|
|
const companionWithoutCat = criticalDamageForSource(member.id, "predatory-companion-test");
|
|
setCatForm(member.id);
|
|
const companionWithCat = criticalDamageForSource(member.id, "predatory-companion-test-cat");
|
|
|
|
expect(playerWithoutCat).toBe(200);
|
|
expect(playerWithCat).toBe(215);
|
|
expect(companionWithoutCat).toBe(200);
|
|
expect(companionWithCat).toBe(215);
|
|
});
|
|
|
|
it.each(["player", "companion"] as const)("reduces only area attack damage for a Cat Form %s", (actorKind) => {
|
|
const withoutTalent = areaDamageForActor(actorKind, 0);
|
|
const withTalent = areaDamageForActor(actorKind, 3);
|
|
|
|
expect(withoutTalent).toBeGreaterThan(0);
|
|
expect(withTalent).toBe(Math.round(withoutTalent * 0.9));
|
|
});
|
|
});
|