Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

@@ -0,0 +1,373 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { resolveDamageValue } from "./combatAuras";
import { auraAdjustedCharacterStats, PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { requireDungeonDefinition } from "./dungeonRegistry";
import { DEFAULT_ENEMY_ATTACK, type EnemyAttackDefinition } from "./enemyAttacks";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import { usePartyStore } from "./partyStore";
import { EMPTY_ITEM_STATS } from "./itemStats";
import { deriveCharacterStats } from "./wotlkStats";
const sourceAttack = (dungeonId: string, entry: number, spellId: number): EnemyAttackDefinition => {
const found = requireDungeonDefinition(dungeonId).entities[`entry-${entry}`]?.combat?.attacks
.find((attack) => attack.spellId === spellId);
if (!found) throw new Error(`Missing imported attack ${dungeonId}:${entry}:${spellId}`);
return found;
};
const sourceCast = (
attack: EnemyAttackDefinition,
initialCooldownMs = 0,
): EnemyAttackDefinition => ({
...attack,
initialCooldownMs: [initialCooldownMs, initialCooldownMs],
repeatCooldownMs: [60_000, 60_000],
maximumCasts: 1,
});
function registerMob(
id: string,
serverEntry: number,
attacks: readonly EnemyAttackDefinition[],
position: readonly [number, number, number],
): void {
useCombatStore.getState().registerMob(id, {
name: id,
serverEntry,
level: 30,
maxHealth: 50_000,
xpReward: 0,
attacks,
});
setMobPosition(id, position);
}
function engageMob(id: string, now: number): void {
useCombatStore.getState().selectMob(id);
useCombatStore.getState().damageMob(id, 1, now);
}
function createPartyPositions(
positions: readonly (readonly [number, number, number])[],
downIndex?: number,
) {
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 30 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty(
"enemy-source-aura-integration", 30, useCombatStore.getState().health, "damage", 5, "wow",
);
const members = usePartyStore.getState().members.map((member, index) => ({
...member,
...(index === downIndex ? { health: 0, status: "down" as const } : {}),
}));
usePartyStore.setState({ members });
members.forEach((member, index) => {
registerPartyRuntimePosition(member.id, positions[index] ?? [40 + index, 0, 0]);
});
useCombatStore.getState().synchronizeActors();
return members;
}
function runSingleImportedCast(id: string, startAt = 1_000): number {
engageMob(id, startAt);
const mob = useCombatStore.getState().mobs[id]!;
const cast = mob.attacks[0]!;
useCombatStore.getState().advanceMobCombat(startAt);
const releaseAt = startAt + (cast.castTimeMs ?? 0) + 1;
useCombatStore.getState().advanceMobCombat(releaseAt);
return releaseAt;
}
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetCombatSpatialProvider();
});
afterEach(() => resetCombatSpatialProvider());
describe("imported enemy source auras in combat", () => {
it("applies Stockade Enrage to physical damage and basic swing phase only", () => {
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 30 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
const enrage = sourceCast(sourceAttack("stormwind-stockade", 1696, 8599));
expect(enrage.effect).toMatchObject({
kind: "aura",
wowAuraSource: { spellId: 8599, sourceProfile: "ascension" },
});
const unrelatedSpell: EnemyAttackDefinition = {
...DEFAULT_ENEMY_ATTACK,
id: "unrelated-spell",
name: "Unrelated Spell",
delivery: "projectile",
target: "primary",
school: "fire",
attackKind: "spell",
range: 30,
cooldownMs: 4_000,
initialCooldownMs: [10_000, 10_000],
repeatCooldownMs: [4_000, 4_000],
};
const basicAttack: EnemyAttackDefinition = {
...DEFAULT_ENEMY_ATTACK,
cooldownMs: 2_000,
initialCooldownMs: [10_000, 10_000],
repeatCooldownMs: [2_000, 2_000],
};
const mobId = "stockade-enraged-mob";
registerMob(mobId, 1696, [basicAttack, unrelatedSpell, enrage], [2, 0, 0]);
engageMob(mobId, 1_000);
const mob = useCombatStore.getState().mobs[mobId]!;
useCombatStore.setState({
mobs: {
...useCombatStore.getState().mobs,
[mobId]: {
...mob,
health: 15_000,
abilityReadyAt: {
"basic-attack": 3_600,
"unrelated-spell": 4_000,
[enrage.id]: 1_100,
},
},
},
});
useCombatStore.getState().advanceMobCombat(1_100);
const sourceCastTime = useCombatStore.getState().mobs[mobId]!.attacks
.find((attack) => attack.id === enrage.id)!.castTimeMs ?? 0;
const appliedAt = 1_100 + sourceCastTime;
useCombatStore.getState().advanceMobCombat(appliedAt);
let state = useCombatStore.getState();
const enrageAura = state.auras.find((aura) => (
aura.definition.id === "enemy-source-spell-8599" && aura.targetId === mobId
));
expect(enrageAura).toBeDefined();
expect(enrageAura?.expiresAt).toBe(appliedAt + 120_000);
expect(enrageAura?.definition.dispelCategory).toBe("enrage");
expect(resolveDamageValue(100, [enrageAura!], {
direction: "dealt", school: "physical", attackKind: "melee",
})).toBe(130);
expect(resolveDamageValue(100, [enrageAura!], {
direction: "dealt", school: "fire", attackKind: "spell",
})).toBe(100);
expect(state.mobs[mobId]?.meleeAttackSpeedMultiplier).toBeCloseTo(1 / 1.3);
expect(state.mobs[mobId]?.abilityReadyAt["basic-attack"])
.toBe(appliedAt + Math.round((3_600 - appliedAt) / 1.3));
expect(state.mobs[mobId]?.abilityReadyAt["unrelated-spell"]).toBe(4_000);
const firstHastedSwingAt = state.mobs[mobId]!.abilityReadyAt["basic-attack"];
useCombatStore.getState().advanceMobCombat(firstHastedSwingAt);
state = useCombatStore.getState();
expect(state.mobs[mobId]?.abilityReadyAt["basic-attack"])
.toBe(firstHastedSwingAt + Math.round(2_000 / 1.3));
const navigationResetAt = firstHastedSwingAt + 1;
expect(useCombatStore.getState().resetMobForNavigation(mobId, true, navigationResetAt)).toBe(true);
state = useCombatStore.getState();
expect(state.mobs[mobId]?.meleeAttackSpeedMultiplier).toBe(1);
expect(state.mobs[mobId]?.abilityReadyAt).toEqual({});
expect(state.auras.some((aura) => (
aura.definition.id === "enemy-source-spell-8599"
&& (aura.sourceId === mobId || aura.targetId === mobId)
))).toBe(false);
const reengageAt = navigationResetAt + 10_001;
engageMob(mobId, reengageAt);
const resetMob = useCombatStore.getState().mobs[mobId]!;
useCombatStore.setState({
mobs: {
...useCombatStore.getState().mobs,
[mobId]: {
...resetMob,
health: 15_000,
abilityReadyAt: {
"basic-attack": reengageAt + 5_000,
"unrelated-spell": reengageAt + 6_000,
[enrage.id]: reengageAt,
},
},
},
});
useCombatStore.getState().advanceMobCombat(reengageAt);
const reengageCastTime = useCombatStore.getState().mobs[mobId]!.attacks
.find((attack) => attack.id === enrage.id)!.castTimeMs ?? 0;
const reengageAppliedAt = reengageAt + reengageCastTime;
useCombatStore.getState().advanceMobCombat(reengageAppliedAt);
state = useCombatStore.getState();
expect(state.mobs[mobId]?.meleeAttackSpeedMultiplier).toBeCloseTo(1 / 1.3);
expect(state.mobs[mobId]?.abilityReadyAt["unrelated-spell"]).toBe(reengageAt + 6_000);
const reengageExpiresAt = reengageAppliedAt + 120_000;
useCombatStore.setState((current) => {
const currentMob = current.mobs[mobId]!;
return {
mobs: {
...current.mobs,
[mobId]: {
...currentMob,
abilityReadyAt: {
...currentMob.abilityReadyAt,
"basic-attack": reengageExpiresAt + 400,
"unrelated-spell": reengageExpiresAt + 600,
},
},
},
};
});
useCombatStore.getState().advanceMobCombat(reengageExpiresAt);
state = useCombatStore.getState();
expect(state.mobs[mobId]?.meleeAttackSpeedMultiplier).toBe(1);
expect(state.mobs[mobId]?.abilityReadyAt["basic-attack"]).toBe(reengageExpiresAt + 520);
expect(state.mobs[mobId]?.abilityReadyAt["unrelated-spell"]).toBe(reengageExpiresAt + 600);
const epochAttack = sourceCast(sourceAttack("the-nexus", 26723, 8599));
expect(epochAttack.effect).toMatchObject({
kind: "aura",
wowAuraSource: { spellId: 8599, sourceProfile: "epoch" },
});
const epochId = "nexus-epoch-enraged-mob";
registerMob(epochId, 26723, [epochAttack], [2, 0, 0]);
useCombatStore.setState((current) => ({
mobs: { ...current.mobs, [epochId]: { ...current.mobs[epochId]!, health: 10_000 } },
}));
const epochReleaseAt = runSingleImportedCast(epochId, 200_000);
const epochAura = useCombatStore.getState().auras.find((aura) => (
aura.definition.id === "enemy-source-spell-8599" && aura.targetId === epochId
));
expect(epochAura?.expiresAt).toBe(epochReleaseAt + 120_000);
expect(resolveDamageValue(100, [epochAura!], {
direction: "dealt", school: "physical", attackKind: "melee",
})).toBeCloseTo(110);
expect(resolveDamageValue(100, [epochAura!], {
direction: "dealt", school: "fire", attackKind: "spell",
})).toBe(100);
});
it("applies Stockade Battle Shout to living nearby friendly mobs with LOS and suppresses duplicate casters", () => {
const members = createPartyPositions([[30, 0, 0], [31, 0, 0], [32, 0, 0], [33, 0, 0]]);
registerCombatSpatialProvider({
actor: () => null,
hasLineOfSight: (_source, target) => target[0] !== 3,
projectGroundPoint: (position) => position,
});
const shout = sourceAttack("stormwind-stockade", 1716, 9128);
const casterA = "stockade-shout-a";
const casterB = "stockade-shout-b";
registerMob(casterA, 1716, [sourceCast(shout)], [0, 0, 0]);
registerMob(casterB, 1716, [sourceCast(shout, 5_000)], [5, 0, 0]);
registerMob("shout-friend", 1716, [], [10, 0, 0]);
registerMob("shout-near-friend", 1716, [], [14, 0, 0]);
registerMob("shout-blocked-friend", 1716, [], [3, 0, 0]);
registerMob("shout-far-friend", 1716, [], [27, 0, 0]);
registerMob("shout-dead-friend", 1716, [], [12, 0, 0]);
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
"shout-dead-friend": { ...state.mobs["shout-dead-friend"]!, health: 0, dead: true },
},
}));
engageMob(casterA, 1_000);
engageMob(casterB, 1_000);
useCombatStore.getState().advanceMobCombat(1_000);
const castDuration = useCombatStore.getState().mobs[casterA]!.attacks[0]!.castTimeMs ?? 0;
const firstReleaseAt = 1_000 + castDuration + 1;
useCombatStore.getState().advanceMobCombat(firstReleaseAt);
useCombatStore.getState().advanceMobCombat(6_000);
const secondReleaseAt = 6_000 + castDuration + 1;
useCombatStore.getState().advanceMobCombat(secondReleaseAt);
const activeShouts = useCombatStore.getState().auras.filter((aura) => (
aura.definition.id === "enemy-source-spell-9128"
));
const targetIds = new Set(activeShouts.map((aura) => aura.targetId));
expect(targetIds).toEqual(new Set([casterA, casterB, "shout-friend", "shout-near-friend"]));
for (const targetId of targetIds) {
expect(activeShouts.filter((aura) => aura.targetId === targetId)).toHaveLength(1);
}
const friendAura = activeShouts.find((aura) => aura.targetId === "shout-friend")!;
expect(friendAura.sourceId).toBe(casterA);
expect(friendAura.appliedAt).toBe(firstReleaseAt);
expect(friendAura.expiresAt).toBe(firstReleaseAt + 120_000);
expect(resolveDamageValue(100, [friendAura], {
direction: "dealt", school: "physical", attackKind: "melee",
})).toBeCloseTo(115);
expect(resolveDamageValue(100, [friendAura], {
direction: "dealt", school: "fire", attackKind: "spell",
})).toBe(100);
expect(activeShouts.some((aura) => (
aura.targetId === PLAYER_AGGRO_ID || members.some((member) => member.id === aura.targetId)
))).toBe(false);
expect(useCombatStore.getState().combatEvents.some((event) => (
(event.sourceActorId === casterA || event.sourceActorId === casterB) && event.kind === "damage"
))).toBe(false);
});
it("applies Demoralizing Shout only to living visible actors in the 10-yard area and adjusts AP", () => {
const members = createPartyPositions([
[7, 0, 0], [12, 0, 0], [8, 0, 0], [4, 0, 0],
], 3);
registerCombatSpatialProvider({
actor: () => null,
hasLineOfSight: (_source, target) => target[0] !== 5,
projectGroundPoint: (position) => position,
});
const caster = "stockade-demo-caster";
registerMob(caster, 1711, [sourceCast(sourceAttack("stormwind-stockade", 1711, 16244))], [0, 0, 1]);
const releaseAt = runSingleImportedCast(caster);
const demoralized = useCombatStore.getState().auras.filter((aura) => (
aura.definition.id === "enemy-source-spell-16244"
));
expect(new Set(demoralized.map((aura) => aura.targetId))).toEqual(new Set([
PLAYER_AGGRO_ID, members[0]!.id, members[2]!.id,
]));
const playerDebuff = demoralized.find((aura) => aura.targetId === PLAYER_AGGRO_ID)!;
expect(playerDebuff.expiresAt).toBe(releaseAt + 15_000);
expect(resolveDamageValue(100, [playerDebuff], {
direction: "dealt", school: "physical", attackKind: "melee",
})).toBe(90);
expect(resolveDamageValue(100, [playerDebuff], {
direction: "dealt", school: "fire", attackKind: "spell",
})).toBe(100);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.sourceActorId === caster && event.kind === "damage"
))).toBe(false);
const apCaster = "stockade-ap-debuff-caster";
registerMob(apCaster, 1715, [sourceCast(sourceAttack("stormwind-stockade", 1715, 13730))], [0, 0, 1]);
const apReleaseAt = runSingleImportedCast(apCaster, 10_000);
const apAura = useCombatStore.getState().auras.find((aura) => (
aura.definition.id === "enemy-source-spell-13730" && aura.targetId === PLAYER_AGGRO_ID
));
expect(apAura?.expiresAt).toBe(apReleaseAt + 30_000);
const baseStats = deriveCharacterStats("warrior", 30, EMPTY_ITEM_STATS, "human");
const adjustedStats = auraAdjustedCharacterStats(
baseStats, useCombatStore.getState().auras, PLAYER_AURA_ENTITY_ID, apReleaseAt,
);
expect(adjustedStats.attackPower).toBeCloseTo(baseStats.attackPower * 0.85);
});
it("does not use the home leash as immunity to a sourced hostile area shout", () => {
const members = createPartyPositions([[10, 0, 0], [50, 0, 0], [60, 0, 0], [70, 0, 0]]);
useCombatStore.getState().setPlayerPosition([20, 0, 0]);
const caster = "stockade-demo-leash";
registerMob(caster, 1711, [sourceCast(sourceAttack("stormwind-stockade", 1711, 16244))], [12, 0, 0]);
useCombatStore.getState().damageMob(caster, 1, 1_000, {
actorId: members[0]!.id, role: "damage", abilityId: "shout-leash-fixture",
});
useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [caster]: {
...state.mobs[caster]!, homePosition: [0, 0, 0], leashRange: 11,
} } }));
useCombatStore.getState().advanceMobCombat(1_000);
const castTime = useCombatStore.getState().mobs[caster]!.attacks[0]!.castTimeMs ?? 0;
useCombatStore.getState().advanceMobCombat(1_001 + castTime);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "enemy-source-spell-16244"
&& aura.targetId === PLAYER_AGGRO_ID)).toBe(true);
});
});