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,320 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user