Files
healer-man/src/game/enemySmartAiRuntime.test.ts
T
2026-09-09 11:21:33 -04:00

451 lines
16 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import {
deterministicEnemyCooldown,
useCombatStore,
} from "./combatStore";
import { WAILING_CAVERNS_DEFINITION } from "./dungeons/wailingCaverns";
import {
clearMobRuntimeRegistry,
getMobPosition,
setMobPosition,
} from "./mobRuntimeRegistry";
import { useWailingEncounterStore } from "./wailingCavernsEncounter";
describe("server-backed enemy ability runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
});
afterEach(() => {
clearMobRuntimeRegistry();
useWailingEncounterStore.getState().reset();
});
it("selects seeded cooldowns reproducibly inside the exact SmartAI range", () => {
const first = Array.from({ length: 12 }, (_, activation) => (
deterministicEnemyCooldown("cobrahn", "spell-8040", activation, [10_000, 20_000])
));
const second = Array.from({ length: 12 }, (_, activation) => (
deterministicEnemyCooldown("cobrahn", "spell-8040", activation, [10_000, 20_000])
));
expect(second).toEqual(first);
expect(first.every((cooldown) => cooldown >= 10_000 && cooldown <= 20_000)).toBe(true);
expect(new Set(first).size).toBeGreaterThan(1);
});
it("honors imported fear immunities while leaving Kresh controllable", () => {
useCombatStore.getState().registerMob("kresh", {
name: "Kresh",
serverEntry: 3653,
boss: true,
maxHealth: 100,
});
useCombatStore.getState().registerMob("vancleef", {
name: "Edwin VanCleef",
serverEntry: 639,
boss: true,
maxHealth: 100,
});
setMobPosition("kresh", [3, 0, 0]);
setMobPosition("vancleef", [5, 0, 0]);
expect(useCombatStore.getState().castAbility(
"priest-psychic-scream",
[0, 0, 0],
1_000,
).ok).toBe(true);
expect(useCombatStore.getState().mobs.kresh.statuses).toContainEqual({
kind: "fear",
sourceAbilityId: "priest-psychic-scream",
endsAt: 9_000,
});
expect(useCombatStore.getState().mobs.vancleef.statuses).toEqual([]);
});
it("heals the lowest-health eligible friendly in range only after the SQL threshold", () => {
const combat = WAILING_CAVERNS_DEFINITION.entities["lord-cobrahn"].combat!;
useCombatStore.getState().registerMob("cobrahn", {
name: "Lord Cobrahn",
serverEntry: combat.serverEntry,
boss: true,
maxHealth: 1_000,
attacks: combat.attacks,
});
useCombatStore.getState().registerMob("eligible", {
name: "Eligible Ally",
maxHealth: 1_000,
xpReward: 0,
});
useCombatStore.getState().registerMob("lower-but-distant", {
name: "Distant Ally",
maxHealth: 1_000,
xpReward: 0,
});
setMobPosition("cobrahn", [2, 0, 0]);
setMobPosition("eligible", [8, 0, 0]);
setMobPosition("lower-but-distant", [60, 0, 0]);
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
eligible: { ...state.mobs.eligible, health: 500 },
"lower-but-distant": { ...state.mobs["lower-but-distant"], health: 100 },
},
}));
useCombatStore.getState().damageMob("cobrahn", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(4_000, [0, 0, 0]);
expect(useCombatStore.getState().mobs.eligible.health).toBeGreaterThan(500);
expect(useCombatStore.getState().mobs["lower-but-distant"].health).toBe(100);
});
it("casts a non-repeatable health-triggered form exactly once", () => {
const combat = WAILING_CAVERNS_DEFINITION.entities["lord-cobrahn"].combat!;
useCombatStore.getState().registerMob("cobrahn-form", {
name: "Lord Cobrahn",
serverEntry: combat.serverEntry,
boss: true,
maxHealth: 1_000,
attacks: combat.attacks.filter((attack) => attack.id === "spell-7965"),
});
setMobPosition("cobrahn-form", [2, 0, 0]);
useCombatStore.getState().damageMob("cobrahn-form", 750, 1_000);
for (const now of [1_000, 1_000, 2_000, 20_000, 40_000, 80_000]) {
useCombatStore.getState().advanceMobCombat(now, [0, 0, 0]);
}
const cobrahn = useCombatStore.getState().mobs["cobrahn-form"];
expect(cobrahn.auras).toContainEqual({
id: "form:serpent",
sourceAbilityId: "spell-7965",
endsAt: 0,
});
expect(cobrahn.abilityCastCounts["spell-7965"]).toBe(1);
expect(cobrahn.abilityReadyAt["spell-7965"]).toBe(Number.POSITIVE_INFINITY);
});
it("executes data-driven summon effects and removes summons on encounter reset", () => {
useCombatStore.getState().registerMob("summoner", {
name: "Summoner",
maxHealth: 1_000,
attacks: [{
id: "summon-vipers",
name: "Summon Vipers",
delivery: "area",
target: "self",
school: "nature",
animation: "cast",
range: 0,
cooldownMs: 60_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [60_000, 60_000],
maximumCasts: 1,
damageMultiplier: 0,
effect: { kind: "summon", creatureEntry: 5048, count: 2 },
}],
});
setMobPosition("summoner", [2, 0, 0]);
useCombatStore.getState().damageMob("summoner", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]);
const summonIds = Object.keys(useCombatStore.getState().mobs)
.filter((id) => id.startsWith("summoner:summon-vipers:summon:1:"));
expect(summonIds).toHaveLength(2);
for (const id of summonIds) {
expect(useCombatStore.getState().mobs[id]).toMatchObject({
serverEntry: 5048,
summonedBy: "summoner",
engaged: true,
xpReward: 0,
});
expect(getMobPosition(id)).not.toBeNull();
}
useCombatStore.getState().resetEncounter();
expect(Object.keys(useCombatStore.getState().mobs)).toEqual(["summoner"]);
expect(summonIds.every((id) => getMobPosition(id) === null)).toBe(true);
});
it("uses content-authored summon namespaces and spawn-group ownership", () => {
useCombatStore.getState().registerMob("wave-controller", {
name: "Wave Controller",
maxHealth: 1_000,
attacks: [{
id: "release-wave",
name: "Release Wave",
delivery: "area",
target: "self",
school: "shadow",
animation: "cast",
range: 0,
cooldownMs: 60_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [60_000, 60_000],
maximumCasts: 1,
damageMultiplier: 0,
effect: {
kind: "summon",
creatureEntry: 5048,
count: 2,
entityId: "forsaken-abbey:necromage:add",
spawnGroupId: "forsaken-abbey:necromage:wave-one",
},
}],
});
setMobPosition("wave-controller", [2, 0, 0]);
useCombatStore.getState().damageMob("wave-controller", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]);
const summonIds = Object.keys(useCombatStore.getState().mobs)
.filter((id) => id.startsWith("forsaken-abbey:necromage:add:"));
expect(summonIds).toEqual([
"forsaken-abbey:necromage:add:1:1",
"forsaken-abbey:necromage:add:1:2",
]);
expect(summonIds.every((id) => (
useCombatStore.getState().mobs[id].spawnGroupId === "forsaken-abbey:necromage:wave-one"
))).toBe(true);
});
it("applies a modular target aura as a dispellable player debuff", () => {
useCombatStore.getState().registerMob("runewaker-debuffer", {
name: "RuneWaker Debuffer",
maxHealth: 1_000,
attacks: [{
id: "conductivity",
name: "Conductivity",
delivery: "projectile",
target: "primary",
school: "arcane",
animation: "cast",
castTimeMs: 1_000,
range: 20,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
damageMultiplier: 0,
effect: { kind: "target-aura", aura: "conductivity", durationMs: 6_000 },
}],
});
setMobPosition("runewaker-debuffer", [2, 0, 0]);
useCombatStore.getState().damageMob("runewaker-debuffer", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(2_000, [0, 0, 0]);
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
sourceId: "runewaker-debuffer",
targetId: PLAYER_AGGRO_ID,
expiresAt: 8_000,
definition: expect.objectContaining({
disposition: "debuff",
dispelCategory: "magic",
}),
}));
});
it("resolves compound enemy damage and control against the same target", () => {
useCombatStore.getState().registerMob("compound-hammer", {
name: "Compound Hammer",
maxHealth: 1_000,
attacks: [{
id: "hammer-pair",
name: "Hammer Pair",
delivery: "projectile",
target: "primary",
school: "holy",
animation: "cast",
castTimeMs: 0,
range: 20,
cooldownMs: 25_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [25_000, 25_000],
damageMultiplier: 1.3,
effect: {
kind: "control",
mechanic: "stun",
durationMs: 2_500,
damageMultiplier: 1.3,
},
}],
});
setMobPosition("compound-hammer", [2, 0, 0]);
useCombatStore.getState().damageMob("compound-hammer", 1, 1_000);
const healthBefore = useCombatStore.getState().health;
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
expect(useCombatStore.getState().health).toBeLessThan(healthBefore);
expect(useCombatStore.getState()).toMatchObject({
controlMechanic: "stun",
controlledUntil: 3_500,
});
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
sourceId: "compound-hammer",
targetId: PLAYER_AGGRO_ID,
expiresAt: 3_500,
}));
});
it("arms, limits, and consumes a next-melee critical proc only from main-hand autos", () => {
useCombatStore.getState().registerMob("melee-proc-boss-1", {
name: "Melee Proc Boss",
level: 20,
maxHealth: 1_000,
damageMultiplier: 0.05,
attacks: [
{
id: "vengeance-driver",
name: "Vengeance",
delivery: "area",
target: "self",
school: "holy",
animation: "cast",
castTimeMs: 0,
range: 0,
cooldownMs: 1,
initialCooldownMs: [0, 0],
repeatCooldownMs: [1, 1],
maximumCasts: 1,
damageMultiplier: 0,
effect: {
kind: "aura",
aura: "vengeance",
durationMs: 0,
meleeAutoCriticalProc: {
chance: 1,
triggeredAura: "vengeance-next-melee-critical",
durationMs: 6_000,
criticalChancePercent: 100,
charges: 1,
},
},
},
{
id: "melee-special",
name: "Melee Special",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
castTimeMs: 0,
range: 5,
cooldownMs: 1_000,
initialCooldownMs: [500, 500],
repeatCooldownMs: [1_000, 1_000],
damageMultiplier: 1,
},
{
id: "basic-attack",
name: "Basic Attack",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
castTimeMs: 0,
range: 4.5,
cooldownMs: 2_000,
initialCooldownMs: [1_000, 1_000],
repeatCooldownMs: [2_000, 2_000],
damageMultiplier: 1,
},
],
});
setMobPosition("melee-proc-boss-1", [2, 0, 0]);
useCombatStore.getState().damageMob("melee-proc-boss-1", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(1_500, [0, 0, 0]);
expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras.map((aura) => aura.id))
.toEqual(["vengeance"]);
useCombatStore.getState().advanceMobCombat(2_000, [0, 0, 0]);
expect(useCombatStore.getState().lastCombatResult?.landed).toBe(true);
expect(useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent).toBe(5);
expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras).toContainEqual({
id: "vengeance-next-melee-critical",
sourceAbilityId: "vengeance-driver",
endsAt: 8_000,
nextMeleeCriticalChancePercent: 100,
charges: 1,
});
useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]);
expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras).toContainEqual(
expect.objectContaining({ id: "vengeance-next-melee-critical", endsAt: 8_000, charges: 1 }),
);
useCombatStore.getState().advanceMobCombat(4_000, [0, 0, 0]);
expect(useCombatStore.getState().lastCombatResult).toMatchObject({
outcome: "crit",
critical: true,
landed: true,
});
expect(useCombatStore.getState().lastCombatResult?.damageBeforeMitigation)
.toBe((useCombatStore.getState().lastCombatResult?.rawDamage ?? 0) * 2);
expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras).toContainEqual(
expect.objectContaining({ id: "vengeance-next-melee-critical", endsAt: 10_000, charges: 1 }),
);
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
"melee-proc-boss-1": {
...state.mobs["melee-proc-boss-1"],
auras: state.mobs["melee-proc-boss-1"].auras.map((aura) => (
aura.id === "vengeance" ? { ...aura, meleeAutoCriticalProc: undefined } : aura
)),
},
},
}));
useCombatStore.getState().advanceMobCombat(5_000, [0, 0, 0]);
useCombatStore.getState().advanceMobCombat(6_000, [0, 0, 0]);
expect(useCombatStore.getState().lastCombatResult?.critical).toBe(true);
expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras.some(
(aura) => aura.id === "vengeance-next-melee-critical",
)).toBe(false);
});
it("honors encounter-state conditions before selecting an ability", () => {
useWailingEncounterStore.getState().reset();
useCombatStore.getState().registerMob("event-caster", {
name: "Event Caster",
maxHealth: 1_000,
attacks: [{
id: "event-strike",
name: "Event Strike",
delivery: "projectile",
target: "primary",
school: "shadow",
animation: "cast",
range: 30,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
damageMultiplier: 1,
effect: { kind: "damage", multiplier: 1 },
condition: {
kind: "encounter-state",
encounterId: "wailing-caverns:naralex",
states: ["mutanus"],
},
}],
});
setMobPosition("event-caster", [2, 0, 0]);
useCombatStore.getState().damageMob("event-caster", 1, 1_000);
const initialHealth = useCombatStore.getState().health;
expect(useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0])).toBe(0);
useWailingEncounterStore.setState({ phase: "mutanus" });
expect(useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0])).toBe(0);
expect(useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0])).toBeGreaterThan(0);
expect(useCombatStore.getState().health).toBeLessThan(initialHealth);
});
});