Files
healer-man/src/game/enemyInfectedWound.integration.test.ts
2026-10-06 22:25:34 -04:00

321 lines
12 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { applyAura } from "./combatAuras";
import { abilitiesForClass, abilityAtLevel } from "./abilityCatalog";
import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore";
import { requireDungeonDefinition } from "./dungeonRegistry";
import { DEFAULT_ENEMY_ATTACK, type EnemyAttackDefinition } from "./enemyAttacks";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { usePartyStore } from "./partyStore";
const SOURCE_START_AT = 1_000;
const SOURCE_ID = "stockade-infected-wound-caster";
const TARGET_AURA_ID = "enemy-source-spell-17230";
const stockadeWound = requireDungeonDefinition("stormwind-stockade").entities["entry-1707"]?.combat;
const importedWound = stockadeWound?.attacks.find((attack) => attack.spellId === 17_230);
if (!stockadeWound || !importedWound) {
throw new Error("Stockade entry 1707 is missing imported Infected Wound 17230.");
}
type DamageCase = "normal" | "direct-physical" | "periodic-physical" | "direct-nature" | "direct-magic";
interface ScenarioResult {
readonly applicationAt?: number;
readonly event: ReturnType<typeof damageEvents>[number] | undefined;
readonly combatResult: ReturnType<typeof useCombatStore.getState>["lastCombatResult"];
}
function initCombat(): void {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.setState({
mobs: {},
auras: [],
actors: {},
enemyPeriodicEffects: [],
combatEvents: [],
effects: [],
lastCombatResult: null,
});
useCombatStore.getState().initializeCharacter({
classId: "priest",
level: 30,
});
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty(
"stockade-infected-wound", 30, useCombatStore.getState().health, "damage", 1, "wow",
);
useCombatStore.getState().synchronizeActors();
}
function registerMob(id: string, attacks: readonly EnemyAttackDefinition[], serverEntry?: number): void {
useCombatStore.getState().registerMob(id, {
name: id,
...(serverEntry === undefined ? {} : { serverEntry }),
level: 30,
maxHealth: 50_000,
xpReward: 0,
attacks,
});
setMobPosition(id, [2, 0, 0]);
}
function engageMob(id: string, now: number): void {
useCombatStore.getState().selectMob(id);
useCombatStore.getState().damageMob(id, 1, now);
}
function sourceWoundAttack(): EnemyAttackDefinition {
return {
...importedWound!,
castTimeMs: 0,
initialCooldownMs: [0, 0],
repeatCooldownMs: [300_000, 300_000],
maximumCasts: 1,
};
}
function applyImportedWoundAndRemoveCaster(cleanup: "remove" | "kill" = "remove"): number {
expect(importedWound).toMatchObject({
spellId: 17_230,
effect: {
kind: "target-aura",
wowAuraSource: { spellId: 17_230, sourceProfile: "ascension" },
},
});
registerMob(SOURCE_ID, [sourceWoundAttack()], stockadeWound!.serverEntry);
engageMob(SOURCE_ID, SOURCE_START_AT);
useCombatStore.getState().advanceMobCombat(SOURCE_START_AT);
const applicationAt = SOURCE_START_AT + 1;
useCombatStore.getState().advanceMobCombat(applicationAt);
const wound = useCombatStore.getState().auras.find((aura) => (
aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID
));
expect(wound).toBeDefined();
expect(wound).toMatchObject({
sourceId: SOURCE_ID,
targetId: PLAYER_AURA_ENTITY_ID,
appliedAt: expect.any(Number),
expiresAt: expect.any(Number),
definition: {
name: "Infected Wound",
disposition: "debuff",
dispelCategory: "disease",
durationMs: 300_000,
},
});
expect(wound!.expiresAt! - wound!.appliedAt).toBe(300_000);
expect(useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage" && event.sourceActorId === SOURCE_ID
))).toEqual([]);
// A sourced timer belongs to the target. Removing the caster must leave the
// target's five-minute disease active until a cleanse or its own expiry.
if (cleanup === "kill") {
const killed = useCombatStore.getState().damageMob(SOURCE_ID, 100_000, applicationAt + 1);
expect(killed.killed).toBe(true);
} else {
useCombatStore.getState().removeMob(SOURCE_ID);
}
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID
))).toBe(true);
return wound!.appliedAt;
}
function attackForCase(scenario: DamageCase): EnemyAttackDefinition {
const common: EnemyAttackDefinition = {
...DEFAULT_ENEMY_ATTACK,
id: `infected-wound-${scenario}`,
name: `Fixture ${scenario}`,
range: 5,
// Deterministic fixture cadence; source spell 17230 itself is still the
// unchanged imported attack exercised in applyImportedWoundAndRemoveCaster.
castTimeMs: 0,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
maximumCasts: 1,
};
if (scenario === "normal") return { ...common, attackKind: "melee", animation: "attack" };
if (scenario === "periodic-physical") {
return {
...common,
attackKind: "spell",
school: "physical",
delivery: "projectile",
animation: "cast",
effect: { kind: "dot", multiplier: 1, ticks: 1, intervalMs: 1_000 },
};
}
return {
...common,
attackKind: "spell",
school: scenario === "direct-nature" ? "nature" : scenario === "direct-magic" ? "fire" : "physical",
delivery: "projectile",
animation: "cast",
effect: { kind: "damage", multiplier: 1 },
};
}
function damageEvents(sourceId: string) {
return useCombatStore.getState().combatEvents.filter((event) => event.kind === "damage"
&& event.sourceActorId === sourceId && event.targetActorId === PLAYER_AURA_ENTITY_ID);
}
function runDamageScenario(scenario: DamageCase, applyWound: boolean): ScenarioResult {
initCombat();
const applicationAt = applyWound ? applyImportedWoundAndRemoveCaster() : undefined;
const attackerId = "infected-wound-damage-fixture";
registerMob(attackerId, [attackForCase(scenario)]);
const attackAt = 5_000;
engageMob(attackerId, attackAt);
useCombatStore.getState().advanceMobCombat(attackAt);
useCombatStore.getState().advanceMobCombat(attackAt + 1);
if (scenario === "periodic-physical") {
useCombatStore.getState().advanceMobCombat(attackAt + 1_001);
}
return {
applicationAt,
event: damageEvents(attackerId).at(-1),
combatResult: useCombatStore.getState().lastCombatResult,
};
}
function cureDiseaseAbility() {
const ability = abilitiesForClass("priest").find((candidate) => candidate.name === "Cure Disease");
if (!ability) throw new Error("Level-30 Priest catalog is missing Cure Disease.");
return ability;
}
beforeEach(initCombat);
describe("imported Stockade Infected Wound 17230", () => {
it.each(["remove", "kill"] as const)(
"applies its sourced disease without a hit and survives caster %s",
(cleanup) => {
const playerHealth = useCombatStore.getState().health;
const appliedAt = applyImportedWoundAndRemoveCaster(cleanup);
const wound = useCombatStore.getState().auras.find((aura) => (
aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID
));
expect(wound?.appliedAt).toBeGreaterThanOrEqual(SOURCE_START_AT);
expect(wound?.appliedAt).toBeLessThanOrEqual(appliedAt);
expect(useCombatStore.getState().health).toBe(playerHealth);
if (cleanup === "remove") expect(useCombatStore.getState().mobs[SOURCE_ID]).toBeUndefined();
else expect(useCombatStore.getState().mobs[SOURCE_ID]?.dead).toBe(true);
expect(damageEvents(SOURCE_ID)).toEqual([]);
},
);
it.each(["normal", "direct-physical", "periodic-physical"] as const)(
"adds exactly six physical damage before mitigation to %s attacks",
(scenario) => {
const baseline = runDamageScenario(scenario, false);
const infected = runDamageScenario(scenario, true);
expect(baseline.event).toBeDefined();
expect(infected.event).toBeDefined();
expect(infected.event!.rawAmount - baseline.event!.rawAmount).toBe(6);
expect(infected.event!.school).toBe("physical");
expect(infected.combatResult?.rawDamage).toBe(infected.event!.rawAmount);
expect(infected.combatResult?.damageBeforeMitigation).toBeGreaterThanOrEqual(infected.event!.rawAmount);
},
);
it.each(["direct-nature", "direct-magic"] as const)(
"does not change %s damage",
(scenario) => {
const baseline = runDamageScenario(scenario, false);
const infected = runDamageScenario(scenario, true);
expect(baseline.event).toBeDefined();
expect(infected.event).toBeDefined();
expect(infected.event!.school).toBe(scenario === "direct-nature" ? "nature" : "fire");
expect(infected.event!.rawAmount).toBe(baseline.event!.rawAmount);
},
);
it("places the physical bonus ahead of armor and block, with the remaining packet then absorbed", () => {
initCombat();
applyImportedWoundAndRemoveCaster();
useCombatStore.setState((state) => ({ gearStats: { ...state.gearStats, armor: 50_000 } }));
const guard = applyAura(useCombatStore.getState().auras, {
definition: {
id: "fixture:infected-wound-block-and-absorb",
name: "Test-only block and absorb",
disposition: "buff",
durationMs: 30_000,
modifiers: [
{ kind: "stat", stat: "block", operation: "flat", value: 100, perStack: false },
{ kind: "stat", stat: "block-value", operation: "flat", value: 8, perStack: false },
],
absorbs: [{ id: "test-only-absorb", amount: 1_000, school: "physical" }],
},
sourceId: "fixture:infected-wound-block-and-absorb",
targetId: PLAYER_AURA_ENTITY_ID,
now: 4_000,
}).auras;
useCombatStore.setState({ auras: [...guard] });
const attack = {
...attackForCase("normal"),
id: "infected-wound-blocked-normal",
repeatCooldownMs: [1, 1] as const,
maximumCasts: 100,
};
const attackerId = "infected-wound-blocked-attacker";
registerMob(attackerId, [attack]);
engageMob(attackerId, 5_000);
for (let now = 5_000; now < 5_200; now += 1) {
useCombatStore.getState().advanceMobCombat(now);
if (useCombatStore.getState().lastCombatResult?.blocked) break;
}
const result = useCombatStore.getState().lastCombatResult;
const event = damageEvents(attackerId).find((candidate) => candidate.outcome === "block");
expect(result?.blocked).toBe(true);
expect(result?.landed).toBe(true);
expect(result?.effectiveArmor).toBeGreaterThan(0);
expect(result?.armorMitigatedAmount).toBeGreaterThan(0);
expect(result?.blockedAmount).toBeGreaterThan(0);
expect(event).toBeDefined();
expect(event!.rawAmount).toBe(result!.rawDamage);
expect(event!.rawAmount).toBe(useCombatStore.getState().mobs[attackerId]!.attackDamage + 6);
expect(result!.damageBeforeMitigation).toBe(event!.rawAmount);
expect(event!.absorbed).toBeGreaterThan(0);
expect(event!.effectiveAmount).toBe(0);
});
it("cleanses the imported disease with the actual level-30 Priest Cure Disease ability", () => {
const appliedAt = applyImportedWoundAndRemoveCaster();
const cure = abilityAtLevel(cureDiseaseAbility(), 30);
expect(cure).toMatchObject({
dbcSpellId: 528,
effects: [expect.objectContaining({
kind: "dispel",
mode: "cleanse",
categories: ["disease"],
})],
});
// Cure Disease targets the player's self slot when no party actor is selected.
expect(useCombatStore.getState().selectFriendlyActor(null)).toBe(true);
const result = useCombatStore.getState().castAbility(cure.id, {
now: appliedAt + 1,
});
expect(result.ok, JSON.stringify(result)).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === TARGET_AURA_ID
&& aura.targetId === PLAYER_AURA_ENTITY_ID)).toBe(false);
});
it("expires at the sourced five-minute duration after the caster has been removed", () => {
const appliedAt = applyImportedWoundAndRemoveCaster();
const expirationAt = useCombatStore.getState().auras.find((aura) => aura.definition.id === TARGET_AURA_ID)!.expiresAt!;
expect(expirationAt - appliedAt).toBe(300_000);
useCombatStore.getState().advanceMobCombat(expirationAt);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === TARGET_AURA_ID
&& aura.targetId === PLAYER_AURA_ENTITY_ID)).toBe(false);
});
});