forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,319 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AURA_ENTITY_ID } from "./combatStore";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
clearPartyRuntimeRegistry,
|
||||
registerPartyRuntimePosition,
|
||||
updatePartyRuntimeTargetVisibility,
|
||||
} from "./partyRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { abilitiesForClass } from "./abilityCatalog";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { requireDungeonDefinition } from "./dungeonRegistry";
|
||||
import { applyAura, type AuraDefinition } from "./combatAuras";
|
||||
import type { EnemyAttackDefinition } from "./enemyAttacks";
|
||||
import { COA_GUARDIAN_VANGUARD_AURA_ID } from "./coaGuardianVanguardRuntime";
|
||||
|
||||
const HAMHOCK_CHAIN_LIGHTNING = requireDungeonDefinition("stormwind-stockade")
|
||||
.entities["entry-1717"]!
|
||||
.combat!
|
||||
.attacks.find((attack) => attack.spellId === 421)!;
|
||||
|
||||
const VANGUARD_RANKS = { "coa-entry-34417": 1 };
|
||||
|
||||
const GUARANTEED_MELEE_BLOCK_FIXTURE: AuraDefinition = {
|
||||
id: "test:vanguard-guaranteed-melee-block",
|
||||
name: "Vanguard integration block fixture",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
stackBehavior: "replace",
|
||||
modifiers: [
|
||||
{ kind: "stat", stat: "block", operation: "flat", value: 1_000 },
|
||||
{ kind: "stat", stat: "block-value", operation: "flat", value: 20 },
|
||||
{ kind: "stat", stat: "dodge", operation: "flat", value: -1_000 },
|
||||
{ kind: "stat", stat: "parry", operation: "flat", value: -1_000 },
|
||||
{ kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: 100 },
|
||||
{ kind: "stat", stat: "incoming-spell-hit", operation: "flat", value: 100 },
|
||||
],
|
||||
};
|
||||
|
||||
const MELEE_BLOCK_FIXTURE_ATTACK: EnemyAttackDefinition = {
|
||||
id: "vanguard-integration-melee",
|
||||
name: "Vanguard integration melee",
|
||||
delivery: "melee",
|
||||
target: "primary",
|
||||
school: "physical",
|
||||
attackKind: "melee",
|
||||
animation: "attack",
|
||||
range: 4.5,
|
||||
cooldownMs: 60_000,
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [60_000, 60_000],
|
||||
damageMultiplier: 1,
|
||||
};
|
||||
|
||||
function vanguardAuras(actorId: string) {
|
||||
return useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.definition.id === COA_GUARDIAN_VANGUARD_AURA_ID
|
||||
&& aura.sourceId === actorId
|
||||
&& aura.targetId === actorId
|
||||
));
|
||||
}
|
||||
|
||||
function registerHamhock(id: string): void {
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: "Hamhock (Vanguard integration fixture)",
|
||||
serverEntry: 1717,
|
||||
level: 30,
|
||||
maxHealth: 100_000,
|
||||
xpReward: 0,
|
||||
attacks: [{
|
||||
...HAMHOCK_CHAIN_LIGHTNING,
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [60_000, 60_000],
|
||||
}],
|
||||
});
|
||||
setMobPosition(id, [2, 0, 0]);
|
||||
useCombatStore.getState().selectMob(id);
|
||||
}
|
||||
|
||||
function applyDeterministicBlockFixture(actorId: string, now: number): void {
|
||||
const auras = applyAura(useCombatStore.getState().auras, {
|
||||
definition: GUARANTEED_MELEE_BLOCK_FIXTURE,
|
||||
sourceId: actorId,
|
||||
targetId: actorId,
|
||||
now,
|
||||
}).auras;
|
||||
useCombatStore.setState({ auras: [...auras] });
|
||||
}
|
||||
|
||||
function registerMeleeBlockFixture(id: string, targetActorId: string, now: number): void {
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: "Vanguard melee block fixture",
|
||||
level: 30,
|
||||
maxHealth: 100_000,
|
||||
xpReward: 0,
|
||||
attacks: [MELEE_BLOCK_FIXTURE_ATTACK],
|
||||
});
|
||||
setMobPosition(id, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(id, 1, now, {
|
||||
actorId: targetActorId,
|
||||
role: "tank",
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock();
|
||||
// Match the imported level-30 Hamhock so its spell attack table is representative.
|
||||
useCombatStore.getState().initializeCharacter({ classId: "guardian", level: 30 });
|
||||
useCombatStore.setState((state) => ({
|
||||
talentRanks: { ...state.talentRanks, ...VANGUARD_RANKS },
|
||||
gearStats: { ...state.gearStats, ...EMPTY_ITEM_STATS, hitRating: 1_000_000 },
|
||||
}));
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
usePartyStore.getState().initializeParty(
|
||||
"coa-guardian-vanguard-integration",
|
||||
30,
|
||||
useCombatStore.getState().health,
|
||||
"tank",
|
||||
1,
|
||||
"coa",
|
||||
);
|
||||
usePartyStore.setState({ members: [], command: "stop", activeMobId: null });
|
||||
});
|
||||
|
||||
describe("CoA Guardian Vanguard combat integration", () => {
|
||||
it("does not count Ram, then reduces Hamhock's imported Chain Lightning before consuming the auto-hit stack", () => {
|
||||
const mobId = "coa-vanguard-hamhock-player";
|
||||
registerHamhock(mobId);
|
||||
|
||||
const ram = abilitiesForClass("guardian").find((ability) => ability.dbcSpellId === 802284);
|
||||
expect(ram, "Guardian Ram must be present as a normal melee action").toBeDefined();
|
||||
expect(useCombatStore.getState().castAbility(ram!.id, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)).toHaveLength(0);
|
||||
|
||||
// Ram starts the player's normal weapon swings. Tick the actual player combat loop.
|
||||
useCombatStore.getState().tick(0.1, 1_001);
|
||||
useCombatStore.getState().tick(2.5, 3_501);
|
||||
const afterAutoAttacks = vanguardAuras(PLAYER_AURA_ENTITY_ID);
|
||||
expect(afterAutoAttacks).toHaveLength(1);
|
||||
expect(afterAutoAttacks[0]?.stacks).toBe(2);
|
||||
|
||||
expect(HAMHOCK_CHAIN_LIGHTNING).toMatchObject({ spellId: 421, school: "nature", chain: { targetCount: 3 } });
|
||||
const playerHealthBeforeSpell = useCombatStore.getState().health;
|
||||
useCombatStore.getState().advanceMobCombat(5_000);
|
||||
useCombatStore.getState().advanceMobCombat(8_000);
|
||||
|
||||
const hit = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage"
|
||||
&& event.sourceActorId === mobId
|
||||
&& event.targetActorId === PLAYER_AURA_ENTITY_ID
|
||||
&& event.abilityId === HAMHOCK_CHAIN_LIGHTNING.id
|
||||
&& event.school === "nature"
|
||||
));
|
||||
expect(hit).toBeDefined();
|
||||
const criticalMultiplier = hit!.critical ? 2 : 1;
|
||||
expect(hit!.effectiveAmount).toBeCloseTo(hit!.rawAmount * criticalMultiplier * 0.96, 8);
|
||||
expect(playerHealthBeforeSpell - useCombatStore.getState().health).toBeCloseTo(hit!.effectiveAmount, 8);
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)).toHaveLength(1);
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)[0]?.stacks).toBe(1);
|
||||
});
|
||||
|
||||
it("adds a companion-owned stack on its real normal auto attack", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 60 });
|
||||
const partyId = "coa-vanguard-companion-party";
|
||||
const targetId = "coa-vanguard-companion-target";
|
||||
usePartyStore.getState().initializeParty(
|
||||
partyId,
|
||||
60,
|
||||
useCombatStore.getState().health,
|
||||
"damage",
|
||||
1,
|
||||
"coa",
|
||||
);
|
||||
const template = createConfiguredPartyMember({
|
||||
name: "Vanguard Guardian Companion",
|
||||
classId: "guardian",
|
||||
specialization: "Vanguard",
|
||||
specializationId: "guardian:vanguard",
|
||||
talentTreeId: "coa-guardian-87",
|
||||
talentRanks: VANGUARD_RANKS,
|
||||
role: "tank",
|
||||
}, 60, 0, partyId);
|
||||
const guardian = {
|
||||
...template,
|
||||
talentRanks: { ...template.talentRanks, ...VANGUARD_RANKS },
|
||||
nextActionAt: 100_000,
|
||||
};
|
||||
usePartyStore.setState({ members: [guardian], command: "attack", activeMobId: targetId });
|
||||
registerPartyRuntimePosition(guardian.id, [0, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.getState().registerMob(targetId, {
|
||||
name: "Guardian auto-attack target",
|
||||
level: 60,
|
||||
maxHealth: 100_000,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(targetId, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(targetId, 1, 1_000);
|
||||
updatePartyRuntimeTargetVisibility(guardian.id, targetId, true);
|
||||
|
||||
advanceDungeonPartyCombat(2_000);
|
||||
|
||||
const companionAura = vanguardAuras(guardian.id);
|
||||
expect(companionAura).toHaveLength(1);
|
||||
expect(companionAura[0]).toMatchObject({
|
||||
sourceId: guardian.id,
|
||||
targetId: guardian.id,
|
||||
stacks: 1,
|
||||
});
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("adds a player-owned stack after an actual deterministic melee block", () => {
|
||||
const mobId = "coa-vanguard-player-blocker";
|
||||
applyDeterministicBlockFixture(PLAYER_AURA_ENTITY_ID, 900);
|
||||
registerMeleeBlockFixture(mobId, PLAYER_AURA_ENTITY_ID, 1_000);
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(1_000);
|
||||
useCombatStore.getState().advanceMobCombat(2_000);
|
||||
|
||||
const blockedHit = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage"
|
||||
&& event.sourceActorId === mobId
|
||||
&& event.targetActorId === PLAYER_AURA_ENTITY_ID
|
||||
&& event.abilityId === MELEE_BLOCK_FIXTURE_ATTACK.id
|
||||
));
|
||||
expect(blockedHit).toMatchObject({ outcome: "block" });
|
||||
expect(useCombatStore.getState().lastCombatResult?.blocked).toBe(true);
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)).toHaveLength(1);
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)[0]?.stacks).toBe(1);
|
||||
});
|
||||
|
||||
it("lets a companion Guardian block, then mitigates Hamhock's spell before consuming one stack", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 30 });
|
||||
const partyId = "coa-vanguard-incoming-companion-party";
|
||||
const autoTargetId = "coa-vanguard-incoming-companion-auto-target";
|
||||
usePartyStore.getState().initializeParty(
|
||||
partyId,
|
||||
30,
|
||||
useCombatStore.getState().health,
|
||||
"damage",
|
||||
1,
|
||||
"coa",
|
||||
);
|
||||
const template = createConfiguredPartyMember({
|
||||
name: "Vanguard Guardian Companion",
|
||||
classId: "guardian",
|
||||
specialization: "Vanguard",
|
||||
specializationId: "guardian:vanguard",
|
||||
talentTreeId: "coa-guardian-87",
|
||||
talentRanks: VANGUARD_RANKS,
|
||||
role: "tank",
|
||||
}, 30, 0, partyId);
|
||||
const guardian = {
|
||||
...template,
|
||||
talentRanks: { ...template.talentRanks, ...VANGUARD_RANKS },
|
||||
nextActionAt: 100_000,
|
||||
};
|
||||
usePartyStore.setState({ members: [guardian], command: "attack", activeMobId: autoTargetId });
|
||||
registerPartyRuntimePosition(guardian.id, [0, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.getState().registerMob(autoTargetId, {
|
||||
name: "Companion auto-attack target",
|
||||
level: 30,
|
||||
maxHealth: 100_000,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(autoTargetId, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(autoTargetId, 1, 1_000);
|
||||
updatePartyRuntimeTargetVisibility(guardian.id, autoTargetId, true);
|
||||
advanceDungeonPartyCombat(2_000);
|
||||
expect(vanguardAuras(guardian.id)).toHaveLength(1);
|
||||
expect(vanguardAuras(guardian.id)[0]?.stacks).toBe(1);
|
||||
useCombatStore.getState().damageMob(autoTargetId, 100_000, 2_500, { actorId: guardian.id, role: guardian.role });
|
||||
|
||||
applyDeterministicBlockFixture(guardian.id, 2_500);
|
||||
const blockerId = "coa-vanguard-companion-blocker";
|
||||
registerMeleeBlockFixture(blockerId, guardian.id, 3_000);
|
||||
useCombatStore.getState().advanceMobCombat(3_000);
|
||||
useCombatStore.getState().advanceMobCombat(4_000);
|
||||
const blockedHit = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage"
|
||||
&& event.sourceActorId === blockerId
|
||||
&& event.targetActorId === guardian.id
|
||||
&& event.abilityId === MELEE_BLOCK_FIXTURE_ATTACK.id
|
||||
));
|
||||
expect(blockedHit).toMatchObject({ outcome: "block" });
|
||||
expect(vanguardAuras(guardian.id)).toHaveLength(1);
|
||||
expect(vanguardAuras(guardian.id)[0]?.stacks).toBe(2);
|
||||
|
||||
const hamhockId = "coa-vanguard-hamhock-companion";
|
||||
registerHamhock(hamhockId);
|
||||
useCombatStore.getState().damageMob(hamhockId, 1, 5_000, { actorId: guardian.id, role: guardian.role });
|
||||
const companionHealthBeforeSpell = useCombatStore.getState().actors[guardian.id]?.health;
|
||||
expect(companionHealthBeforeSpell).toBeDefined();
|
||||
useCombatStore.getState().advanceMobCombat(8_000);
|
||||
useCombatStore.getState().advanceMobCombat(10_000);
|
||||
|
||||
const hit = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage"
|
||||
&& event.sourceActorId === hamhockId
|
||||
&& event.targetActorId === guardian.id
|
||||
&& event.abilityId === HAMHOCK_CHAIN_LIGHTNING.id
|
||||
&& event.school === "nature"
|
||||
));
|
||||
expect(hit).toBeDefined();
|
||||
const criticalMultiplier = hit!.critical ? 2 : 1;
|
||||
expect(hit!.effectiveAmount).toBe(Math.round(hit!.rawAmount * criticalMultiplier * 0.96));
|
||||
expect(companionHealthBeforeSpell! - useCombatStore.getState().actors[guardian.id]!.health)
|
||||
.toBeCloseTo(hit!.effectiveAmount, 8);
|
||||
expect(vanguardAuras(guardian.id)).toHaveLength(1);
|
||||
expect(vanguardAuras(guardian.id)[0]?.stacks).toBe(1);
|
||||
expect(vanguardAuras(PLAYER_AURA_ENTITY_ID)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user