Release Healer Man 0.1.4
This commit is contained in:
@@ -20,6 +20,7 @@ import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { abilitiesForClass, defaultActionBarForClass } from "./abilityCatalog";
|
||||
import { defaultActionBindings } from "./actionBindings";
|
||||
import { deriveCharacterStats } from "./wotlkStats";
|
||||
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
|
||||
|
||||
function allocateTalentRanks(name: string, ranks: number): void {
|
||||
const talent = TALENT_NODES.find((node) => node.name === name);
|
||||
@@ -94,6 +95,26 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().mobs.ravager.engaged).toBe(true);
|
||||
});
|
||||
|
||||
it("emits exactly one cast and release event and cancels presentation with the cast", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 });
|
||||
registerTarget("presentation-target", [10, 0, 0]);
|
||||
const events: CombatPresentationEvent[] = [];
|
||||
const unsubscribe = subscribeCombatPresentation((message) => {
|
||||
if (message.kind === "event") events.push(message.event);
|
||||
});
|
||||
|
||||
expect(useCombatStore.getState().castAbility("mage-frostbolt", [0, 0, 0], 1_000).ok).toBe(true);
|
||||
useCombatStore.getState().tick(0.1, 2_500);
|
||||
expect(events.filter((event) => event.abilityId === "mage-frostbolt").map((event) => event.phase))
|
||||
.toEqual(["cast-start", "release", "impact"]);
|
||||
|
||||
expect(useCombatStore.getState().castAbility("mage-frostbolt", [0, 0, 0], 4_000).ok).toBe(true);
|
||||
expect(useCombatStore.getState().cancelCast()).toBe(true);
|
||||
expect(events.filter((event) => event.abilityId === "mage-frostbolt").map((event) => event.phase))
|
||||
.toEqual(["cast-start", "release", "impact", "cast-start", "cast-cancel"]);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("does not cast or bind Priest Holy Concentration", () => {
|
||||
const abilityId = "wow335-priest-holy-concentration";
|
||||
expect(useCombatStore.getState().castAbility(abilityId, undefined, 1_000)).toMatchObject({
|
||||
@@ -385,6 +406,94 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().health).toBe(healthBefore - 12);
|
||||
});
|
||||
|
||||
it("automatically engages a dungeon pack when a living party actor enters its aggro range", () => {
|
||||
const partyMemberId = preparePartyMember(0, [40, 0, 0]);
|
||||
for (const [id, position] of [["proximity-pack:a", [20, 0, 0]], ["proximity-pack:b", [22, 0, 0]]] as const) {
|
||||
setMobPosition(id, position);
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: id,
|
||||
maxHealth: 100,
|
||||
});
|
||||
}
|
||||
setMobPosition("proximity-boss", [100, 0, 0]);
|
||||
useCombatStore.getState().registerMob("proximity-boss", {
|
||||
name: "Proximity Boss",
|
||||
boss: true,
|
||||
maxHealth: 100,
|
||||
});
|
||||
|
||||
expect(useCombatStore.getState().mobs["proximity-pack:a"].aggroRange).toBe(16);
|
||||
expect(useCombatStore.getState().mobs["proximity-boss"].aggroRange).toBe(20);
|
||||
|
||||
expect(useCombatStore.getState().engageNearbyMobs(1_000, [0, 0, 0])).toBe(0);
|
||||
expect(useCombatStore.getState().mobs["proximity-pack:a"].engaged).toBe(false);
|
||||
|
||||
updatePartyRuntimePosition(partyMemberId, 27, 0, 0);
|
||||
expect(useCombatStore.getState().engageNearbyMobs(1_100, [0, 0, 0])).toBe(1);
|
||||
expect(useCombatStore.getState().mobs["proximity-pack:a"]).toMatchObject({
|
||||
engaged: true,
|
||||
targetActorId: partyMemberId,
|
||||
homePosition: [20, 0, 0],
|
||||
});
|
||||
expect(useCombatStore.getState().mobs["proximity-pack:b"]).toMatchObject({
|
||||
engaged: true,
|
||||
targetActorId: partyMemberId,
|
||||
homePosition: [22, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("uses 3D distance for proximity aggro so stacked dungeon floors do not pull", () => {
|
||||
setMobPosition("upper-floor-mob", [0, 20, 0]);
|
||||
useCombatStore.getState().registerMob("upper-floor-mob", {
|
||||
name: "Upper Floor Mob",
|
||||
maxHealth: 100,
|
||||
});
|
||||
|
||||
expect(useCombatStore.getState().engageNearbyMobs(1_000, [0, 0, 0])).toBe(0);
|
||||
expect(useCombatStore.getState().mobs["upper-floor-mob"].engaged).toBe(false);
|
||||
expect(useCombatStore.getState().engageNearbyMobs(1_100, [0, 8, 0])).toBe(1);
|
||||
expect(useCombatStore.getState().mobs["upper-floor-mob"].engaged).toBe(true);
|
||||
});
|
||||
|
||||
it("anchors a roaming mob at the pull location and keeps aggro across vertical terrain", () => {
|
||||
const id = "kresh-patrol:kresh-roaming";
|
||||
setMobPosition(id, [0, -70, 0]);
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: "Kresh",
|
||||
boss: true,
|
||||
maxHealth: 100,
|
||||
leashRange: 55,
|
||||
xpReward: 0,
|
||||
});
|
||||
|
||||
// Kresh can travel far from the position where his model first mounted.
|
||||
// His leash must begin at the patrol position where combat actually starts.
|
||||
setMobPosition(id, [100, -106, 0]);
|
||||
useCombatStore.getState().damageMob(id, 10, 1_000);
|
||||
expect(useCombatStore.getState().mobs[id]).toMatchObject({
|
||||
health: 90,
|
||||
engaged: true,
|
||||
homePosition: [100, -106, 0],
|
||||
});
|
||||
|
||||
// A player falling from the ledge can be vertically farther than the leash
|
||||
// while remaining beside the encounter in the dungeon's horizontal plane.
|
||||
useCombatStore.getState().advanceMobCombat(1_450, [102, -30, 0]);
|
||||
expect(useCombatStore.getState().mobs[id]).toMatchObject({
|
||||
health: 90,
|
||||
engaged: true,
|
||||
combatPhase: "chasing",
|
||||
});
|
||||
|
||||
// Horizontal distance still enforces the normal evade/reset boundary.
|
||||
useCombatStore.getState().advanceMobCombat(1_500, [156, -106, 0]);
|
||||
expect(useCombatStore.getState().mobs[id]).toMatchObject({
|
||||
health: 100,
|
||||
engaged: false,
|
||||
combatPhase: "returning",
|
||||
});
|
||||
});
|
||||
|
||||
it("tracks damage and effective-healing threat independently for every engaged mob", () => {
|
||||
for (const id of ["threat-pack:a", "threat-pack:b"]) {
|
||||
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 500, xpReward: 0 });
|
||||
@@ -728,11 +837,13 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().talentRanks).toEqual({});
|
||||
|
||||
expect(useCombatStore.getState().settings.showThreatMeter).toBe(false);
|
||||
expect(useCombatStore.getState().settings.showMobAggroRanges).toBe(false);
|
||||
useCombatStore.getState().updateSettings({
|
||||
masterVolume: 9,
|
||||
uiScale: 0.1,
|
||||
minimapRotation: "north-up",
|
||||
showThreatMeter: true,
|
||||
showMobAggroRanges: true,
|
||||
threatMeterPosition: { x: -2, y: 4 },
|
||||
});
|
||||
expect(useCombatStore.getState().settings).toMatchObject({
|
||||
@@ -740,6 +851,7 @@ describe("combat store", () => {
|
||||
uiScale: 0.75,
|
||||
minimapRotation: "north-up",
|
||||
showThreatMeter: true,
|
||||
showMobAggroRanges: true,
|
||||
threatMeterPosition: { x: 0, y: 1 },
|
||||
});
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest" });
|
||||
|
||||
Reference in New Issue
Block a user