forked from phenom/healer-man
Release Healer Man 0.1.6
This commit is contained in:
1 parent
55cfd43d66
commit
b12fb84859
96 files changed
+10874
-609
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
|
||||
import { clearMobRuntimeRegistry, getMobPosition, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
|
||||
function registerTarget(id = "target", position: readonly [number, number, number] = [0, 0, 10]) {
|
||||
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000, xpReward: 0 });
|
||||
setMobPosition(id, position);
|
||||
useCombatStore.getState().selectMob(id);
|
||||
}
|
||||
|
||||
describe("dungeon combat fidelity integration", () => {
|
||||
beforeEach(() => {
|
||||
resetCombatSpatialProvider();
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 10 });
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
usePartyStore.getState().initializeParty("fidelity", 10, useCombatStore.getState().health);
|
||||
});
|
||||
afterEach(resetCombatSpatialProvider);
|
||||
|
||||
it("rejects obstructed and rear-arc hostile casts while friendly healing ignores facing", () => {
|
||||
registerTarget();
|
||||
let blocked = true;
|
||||
registerCombatSpatialProvider({
|
||||
actor: (id) => id === PLAYER_AGGRO_ID
|
||||
? { id, position: [0, 0, 0], yaw: Math.PI }
|
||||
: { id, position: getMobPosition(id) ?? [0, 0, 0], yaw: 0 },
|
||||
hasLineOfSight: () => !blocked,
|
||||
projectGroundPoint: (point) => point,
|
||||
});
|
||||
expect(useCombatStore.getState().castAbility("mage-frostbolt", { now: 1_000 })).toMatchObject({ ok: false, reason: "line-of-sight" });
|
||||
blocked = false;
|
||||
expect(useCombatStore.getState().castAbility("mage-frostbolt", { now: 1_000 })).toMatchObject({ ok: false, reason: "not-facing" });
|
||||
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 10 });
|
||||
const ally = usePartyStore.getState().members[0];
|
||||
registerPartyRuntimePosition(ally.id, [0, 0, 10]);
|
||||
usePartyStore.getState().selectMember(ally.id);
|
||||
usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === ally.id ? { ...member, health: 1 } : member) }));
|
||||
const friendlyResult = useCombatStore.getState().castAbility("wow335-priest-lesser-heal", { now: 2_000 });
|
||||
expect(friendlyResult.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("applies only the first two direct-damage pushbacks and ignores fully absorbed hits", () => {
|
||||
registerTarget();
|
||||
expect(useCombatStore.getState().castAbility("mage-frostbolt", { now: 1_000 }).ok).toBe(true);
|
||||
const original = useCombatStore.getState().activeCast!.completesAt;
|
||||
useCombatStore.getState().damagePlayer(1);
|
||||
useCombatStore.getState().damagePlayer(1);
|
||||
useCombatStore.getState().damagePlayer(1);
|
||||
expect(useCombatStore.getState().activeCast).toMatchObject({ completesAt: original + 1_000, pushbackHits: 2 });
|
||||
|
||||
useCombatStore.getState().cancelCast();
|
||||
useCombatStore.setState({ globalCooldownEndsAt: 0, shield: 10_000, shieldExpiresAt: Number.MAX_SAFE_INTEGER });
|
||||
expect(useCombatStore.getState().castAbility("mage-frostbolt", { now: 5_000 }).ok).toBe(true);
|
||||
const absorbedOriginal = useCombatStore.getState().activeCast!.completesAt;
|
||||
useCombatStore.getState().damagePlayer(100);
|
||||
expect(useCombatStore.getState().activeCast!.completesAt).toBe(absorbedOriginal);
|
||||
});
|
||||
|
||||
it("creates persistent collision-safe ground effects and dynamically ticks actors in their radius", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "paladin", level: 80 });
|
||||
registerTarget("late-target", [20, 0, 0]);
|
||||
registerCombatSpatialProvider({
|
||||
actor: (id) => ({ id, position: id === PLAYER_AGGRO_ID ? [0, 0, 0] : getMobPosition(id) ?? [0, 0, 0], yaw: 0 }),
|
||||
hasLineOfSight: () => true,
|
||||
projectGroundPoint: (point) => [point[0], 0, point[2]],
|
||||
});
|
||||
const groundResult = useCombatStore.getState().castAbility("paladin-consecration", { now: 1_000, groundPosition: [0, 3, 0] });
|
||||
expect(groundResult.reason).toBeUndefined();
|
||||
expect(useCombatStore.getState().areaEffects).toHaveLength(1);
|
||||
setMobPosition("late-target", [2, 0, 0]);
|
||||
const before = useCombatStore.getState().mobs["late-target"].health;
|
||||
useCombatStore.getState().tick(1, 2_000);
|
||||
expect(useCombatStore.getState().mobs["late-target"].health).toBeLessThan(before);
|
||||
});
|
||||
|
||||
it("spends independent Death Knight runes and exposes every combatant through actor snapshots", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 10 });
|
||||
registerTarget("rune-target", [0, 0, 10]);
|
||||
expect(useCombatStore.getState().castAbility("death-knight-icy-touch", { now: 1_000 }).ok).toBe(true);
|
||||
expect(useCombatStore.getState().deathKnightRunes.filter((rune) => rune.readyAt > 1_000)).toHaveLength(1);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
const actors = useCombatStore.getState().actors;
|
||||
expect(actors[PLAYER_AGGRO_ID].runes).toHaveLength(6);
|
||||
expect(actors["rune-target"].kind).toBe("enemy");
|
||||
for (const member of usePartyStore.getState().members) expect(actors[member.id].kind).toBe("companion");
|
||||
});
|
||||
|
||||
it("emits structured effective damage and healing records", () => {
|
||||
registerTarget("event-target", [0, 0, 10]);
|
||||
useCombatStore.getState().damageMob("event-target", 25, 1_000);
|
||||
useCombatStore.getState().damagePlayer(10);
|
||||
useCombatStore.getState().healPlayer(100);
|
||||
const events = useCombatStore.getState().combatEvents;
|
||||
expect(events.some((event) => event.kind === "damage" && event.targetActorId === "event-target" && event.effectiveAmount > 0)).toBe(true);
|
||||
expect(events.some((event) => event.kind === "healing" && event.overheal >= 0)).toBe(true);
|
||||
expect(events.every((event) => event.rawAmount >= event.effectiveAmount)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user