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

163 lines
6.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { useCombatStore, type MobCombatState } from "./combatStore";
import { DEFAULT_ENEMY_ATTACK, type EnemyAttackDefinition } from "./enemyAttacks";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { resetCombatSpatialProvider } from "./combatSpatial";
const FLAGGED_ENTRY = 4540; // Pinned template: Scarlet Monk, flags_extra 0x800.
function syntheticMainhand(): EnemyAttackDefinition {
return {
...DEFAULT_ENEMY_ATTACK,
initialCooldownMs: [60_000, 60_000],
repeatCooldownMs: [60_000, 60_000],
};
}
function registerFlaggedMob(id: string, overrides: {
readonly runewakerNpcId?: number;
} = {}): void {
useCombatStore.getState().registerMob(id, {
name: "Pinned flag fixture: Scarlet Monk",
serverEntry: FLAGGED_ENTRY,
level: 30,
maxHealth: 50_000,
xpReward: 0,
attacks: [syntheticMainhand()],
...overrides,
});
setMobPosition(id, [2, 0, 0]);
}
function engageAt(id: string, now: number): void {
expect(useCombatStore.getState().selectMob(id)).toBe(true);
useCombatStore.getState().damageMob(id, 1, now);
useCombatStore.getState().advanceMobCombat(now);
}
function firstOffhandAt(id: string, engagedAt: number): number {
const mob = useCombatStore.getState().mobs[id]!;
return engagedAt + Math.round(mob.swingMs * 0.5);
}
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetCombatSpatialProvider();
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 30 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
});
afterEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetCombatSpatialProvider();
});
describe("flag-driven AzerothCore creature offhand integration", () => {
it("registers a pinned flagged creature and schedules an independent offhand on engagement", () => {
const mobId = "enemy-automatic-offhand-flagged-scarlet-monk";
registerFlaggedMob(mobId);
let mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.serverEntry).toBe(FLAGGED_ENTRY);
expect(mob.dualWieldEnabled).toBe(true);
const engagedAt = 1_000;
engageAt(mobId, engagedAt);
mob = useCombatStore.getState().mobs[mobId]!;
const mainhandReadyAt = mob.abilityReadyAt["basic-attack"];
const firstOffhandSwingAt = firstOffhandAt(mobId, engagedAt);
expect(mob.dualWieldEnabled).toBe(true);
expect(mob.nextEnemyOffhandAttackAt).toBe(firstOffhandSwingAt);
expect(mainhandReadyAt).toBeGreaterThan(firstOffhandSwingAt);
// Synthetic cooldowns isolate the hand clocks; no hit-table result is forced.
useCombatStore.getState().advanceMobCombat(firstOffhandSwingAt);
mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.nextEnemyOffhandAttackAt).toBe(firstOffhandSwingAt + mob.swingMs);
expect(mob.abilityReadyAt["basic-attack"]).toBe(mainhandReadyAt);
});
it("keeps the template capability through evade, encounter reset, and death, then rearms its timer", () => {
const mobId = "enemy-automatic-offhand-flagged-lifecycle";
registerFlaggedMob(mobId);
engageAt(mobId, 1_000);
let mob = useCombatStore.getState().mobs[mobId]!;
const firstSwingAt = firstOffhandAt(mobId, 1_000);
expect(mob.nextEnemyOffhandAttackAt).toBe(firstSwingAt);
expect(useCombatStore.getState().resetMobForNavigation(mobId, false, 3_000)).toBe(true);
mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.dualWieldEnabled).toBe(true);
expect(mob.nextEnemyOffhandAttackAt).toBe(0);
const afterEvadeAt = 13_001; // The synthetic ten-second navigation evade has elapsed.
engageAt(mobId, afterEvadeAt);
mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.dualWieldEnabled).toBe(true);
expect(mob.nextEnemyOffhandAttackAt).toBe(firstOffhandAt(mobId, afterEvadeAt));
useCombatStore.getState().resetEncounter();
mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.dead).toBe(false);
expect(mob.dualWieldEnabled).toBe(true);
expect(mob.nextEnemyOffhandAttackAt).toBe(0);
const afterEncounterResetAt = 20_000;
engageAt(mobId, afterEncounterResetAt);
expect(useCombatStore.getState().mobs[mobId]?.nextEnemyOffhandAttackAt)
.toBe(firstOffhandAt(mobId, afterEncounterResetAt));
mob = useCombatStore.getState().mobs[mobId]!;
expect(useCombatStore.getState().damageMob(mobId, mob.health, 21_000).killed).toBe(true);
mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.dead).toBe(true);
expect(mob.dualWieldEnabled).toBe(true);
expect(mob.nextEnemyOffhandAttackAt).toBe(0);
useCombatStore.getState().resetEncounter();
mob = useCombatStore.getState().mobs[mobId]!;
expect(mob.dead).toBe(false);
expect(mob.dualWieldEnabled).toBe(true);
expect(mob.nextEnemyOffhandAttackAt).toBe(0);
const afterDeathResetAt = 30_000;
engageAt(mobId, afterDeathResetAt);
expect(useCombatStore.getState().mobs[mobId]?.nextEnemyOffhandAttackAt)
.toBe(firstOffhandAt(mobId, afterDeathResetAt));
});
it("keeps native RuneWaker, summoned, and character-defender actors outside the flag path", () => {
const nativeId = "enemy-automatic-offhand-native-exclusion";
const summonedId = "enemy-automatic-offhand-summon-exclusion";
const defenderId = "enemy-automatic-offhand-defender-exclusion";
registerFlaggedMob(nativeId, { runewakerNpcId: 7 });
registerFlaggedMob(summonedId);
registerFlaggedMob(defenderId);
// These actor tags are applied by their respective spawn paths after the
// shared registry creates a MobCombatState; setState keeps this fixture on
// the actual combat-store path without adding test-only registration APIs.
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
[summonedId]: { ...state.mobs[summonedId]!, summonedBy: "owner" },
[defenderId]: {
...state.mobs[defenderId]!,
characterDefender: {} as NonNullable<MobCombatState["characterDefender"]>,
},
},
}));
for (const id of [nativeId, summonedId, defenderId]) engageAt(id, 5_000);
useCombatStore.getState().advanceMobCombat(5_000);
for (const id of [nativeId, summonedId, defenderId]) {
expect(useCombatStore.getState().mobs[id]?.dualWieldEnabled).toBe(false);
expect(useCombatStore.getState().mobs[id]?.nextEnemyOffhandAttackAt).toBe(0);
}
});
});