Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -3,6 +3,7 @@ import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
||||
import referenceData from "./generated/azerothCoreReference.json";
|
||||
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
|
||||
import {
|
||||
normalizeEnemyAttack,
|
||||
@@ -10,6 +11,20 @@ import {
|
||||
type EnemyDamageSchool,
|
||||
} from "./enemyAttacks";
|
||||
|
||||
interface ImportedWailingAbilities {
|
||||
readonly wailingCaverns: {
|
||||
readonly abilitiesByEntry: Readonly<Record<string, readonly EnemyAttackDefinition[]>>;
|
||||
};
|
||||
}
|
||||
|
||||
const IMPORTED_WAILING = referenceData as unknown as ImportedWailingAbilities;
|
||||
const ANACONDRA_THORNS = IMPORTED_WAILING.wailingCaverns.abilitiesByEntry["3671"]
|
||||
.find((attack) => attack.spellId === 8148)!;
|
||||
const ANACONDRA_LIGHTNING_BOLT = IMPORTED_WAILING.wailingCaverns.abilitiesByEntry["3671"]
|
||||
.find((attack) => attack.spellId === 9532)!;
|
||||
const CROCOLISK_TENDON_RIP = IMPORTED_WAILING.wailingCaverns.abilitiesByEntry["5053"]
|
||||
.find((attack) => attack.spellId === 3604)!;
|
||||
|
||||
function spell(
|
||||
id: string,
|
||||
school: EnemyDamageSchool,
|
||||
@@ -45,6 +60,43 @@ function registerCaster(attacks: readonly EnemyAttackDefinition[], id = "caster"
|
||||
useCombatStore.getState().damageMob(id, 1, 1_000);
|
||||
}
|
||||
|
||||
function silenceMob(id: string, endsAt: number): void {
|
||||
useCombatStore.setState((state) => {
|
||||
const mob = state.mobs[id];
|
||||
if (!mob) return state;
|
||||
return {
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[id]: {
|
||||
...mob,
|
||||
statuses: [...mob.statuses, {
|
||||
kind: "silence",
|
||||
sourceAbilityId: "vow-of-silence",
|
||||
sourceActorId: PLAYER_AGGRO_ID,
|
||||
endsAt,
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setMobAttackReadyAt(id: string, attackId: string, readyAt: number): void {
|
||||
useCombatStore.setState((state) => {
|
||||
const mob = state.mobs[id];
|
||||
if (!mob) return state;
|
||||
return {
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[id]: {
|
||||
...mob,
|
||||
abilityReadyAt: { ...mob.abilityReadyAt, [attackId]: readyAt },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe("enemy casting and interrupts", () => {
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
@@ -129,6 +181,121 @@ describe("enemy casting and interrupts", () => {
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toBeNull();
|
||||
});
|
||||
|
||||
it("cancels a live imported Lightning Bolt cast when Silence lands", () => {
|
||||
expect(ANACONDRA_LIGHTNING_BOLT).toMatchObject({
|
||||
spellId: 9532,
|
||||
school: "nature",
|
||||
delivery: "projectile",
|
||||
source: { entry: 3671, comment: "Lady Anacondra - In Combat - Cast Lightning Bolt" },
|
||||
});
|
||||
registerCaster([ANACONDRA_LIGHTNING_BOLT]);
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(1_000);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toMatchObject({
|
||||
attackId: "spell-9532",
|
||||
spellId: 9532,
|
||||
school: "nature",
|
||||
startedAt: 1_000,
|
||||
completesAt: 2_500,
|
||||
});
|
||||
|
||||
silenceMob("caster", 9_500);
|
||||
useCombatStore.getState().advanceMobCombat(1_100);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toBeNull();
|
||||
for (const now of [2_500, 3_500, 7_500, 9_499]) {
|
||||
useCombatStore.getState().advanceMobCombat(now);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toBeNull();
|
||||
}
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(9_500);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toMatchObject({
|
||||
attackId: "spell-9532",
|
||||
spellId: 9532,
|
||||
startedAt: 9_500,
|
||||
completesAt: 11_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks Anacondra's imported Nature Thorns aura for the full Silence window", () => {
|
||||
expect(ANACONDRA_THORNS).toMatchObject({
|
||||
spellId: 8148,
|
||||
school: "nature",
|
||||
delivery: "melee",
|
||||
animation: "cast",
|
||||
effect: { kind: "aura", aura: "thorns" },
|
||||
});
|
||||
expect(ANACONDRA_THORNS.attackKind).toBeUndefined();
|
||||
expect(ANACONDRA_THORNS.damageClass).toBeUndefined();
|
||||
registerCaster([ANACONDRA_THORNS]);
|
||||
silenceMob("caster", 9_500);
|
||||
|
||||
const events: CombatPresentationEvent[] = [];
|
||||
const unsubscribe = subscribeCombatPresentation((message) => {
|
||||
if (message.kind === "event" && message.event.sourceActorId === "caster") {
|
||||
events.push(message.event);
|
||||
}
|
||||
});
|
||||
|
||||
for (const now of [1_000, 2_500, 4_000, 7_500, 9_499]) {
|
||||
useCombatStore.getState().advanceMobCombat(now);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toBeNull();
|
||||
}
|
||||
expect(events).toEqual([]);
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(9_500);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toMatchObject({
|
||||
attackId: "spell-8148",
|
||||
spellId: 8148,
|
||||
school: "nature",
|
||||
startedAt: 9_500,
|
||||
completesAt: 11_000,
|
||||
});
|
||||
expect(events.map((event) => event.phase)).toEqual(["cast-start"]);
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(11_000);
|
||||
expect(useCombatStore.getState().mobs.caster.activeCast).toBeNull();
|
||||
expect(events.map((event) => event.phase)).toEqual(["cast-start", "release", "aura-start"]);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("keeps physical melee and ranged attacks active under Silence but blocks physical spells", () => {
|
||||
expect(CROCOLISK_TENDON_RIP).toMatchObject({ school: "physical", delivery: "melee" });
|
||||
const physicalRanged = {
|
||||
...spell("physical-ranged", "physical", 1_500),
|
||||
attackKind: "ranged" as const,
|
||||
};
|
||||
const physicalSpell = {
|
||||
...spell("physical-spell", "physical", 1_500),
|
||||
damageClass: 1,
|
||||
};
|
||||
const physicalAttackKindSpell = {
|
||||
...spell("physical-kind-spell", "physical", 1_500),
|
||||
attackKind: "spell" as const,
|
||||
};
|
||||
registerCaster([CROCOLISK_TENDON_RIP], "physical-melee");
|
||||
registerCaster([physicalRanged], "physical-ranged");
|
||||
registerCaster([physicalSpell], "physical-spell");
|
||||
registerCaster([physicalAttackKindSpell], "physical-kind-spell");
|
||||
silenceMob("physical-melee", 9_500);
|
||||
silenceMob("physical-ranged", 9_500);
|
||||
silenceMob("physical-spell", 9_500);
|
||||
silenceMob("physical-kind-spell", 9_500);
|
||||
setMobAttackReadyAt("physical-melee", "spell-3604", 0);
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(1_000);
|
||||
|
||||
expect(useCombatStore.getState().mobs["physical-melee"].activeCast).toMatchObject({
|
||||
attackId: "spell-3604",
|
||||
school: "physical",
|
||||
});
|
||||
expect(useCombatStore.getState().mobs["physical-ranged"].activeCast).toMatchObject({
|
||||
attackId: "physical-ranged",
|
||||
school: "physical",
|
||||
});
|
||||
expect(useCombatStore.getState().mobs["physical-spell"].activeCast).toBeNull();
|
||||
expect(useCombatStore.getState().mobs["physical-kind-spell"].activeCast).toBeNull();
|
||||
});
|
||||
|
||||
it("marks an instant source spell release without publishing a synthetic cast event", () => {
|
||||
registerCaster([spell("shadow-bolt", "shadow", 0)]);
|
||||
const events: CombatPresentationEvent[] = [];
|
||||
|
||||
Reference in new issue
Block a user