Release Healer Man 0.1.6

This commit is contained in:
phenom committed 2026-08-20 14:05:50 -04:00
1 parent 55cfd43d66
commit b12fb84859
96 files changed
+10874 -609

No files matched your search

@@ -0,0 +1,195 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { abilitiesForClass, abilityAtLevel, abilityById, type AbilityDefinition } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { useCombatStore } from "./combatStore";
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
import { targetingRulesForAbility } from "./abilityRules";
import { clearMobRuntimeRegistry, getMobPosition, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { usePartyStore } from "./partyStore";
function registerMob(id: string, position: readonly [number, number, number], boss = false): void {
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 10_000, xpReward: 0, boss });
setMobPosition(id, position);
}
describe("extended dungeon combat fidelity", () => {
beforeEach(() => {
resetCombatSpatialProvider();
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty("actor-authority", 80, useCombatStore.getState().health);
useCombatStore.getState().synchronizeActors();
});
afterEach(resetCombatSpatialProvider);
it("keeps companion vitals authoritative when compatibility party records are stale", () => {
const member = usePartyStore.getState().members[0];
const actor = useCombatStore.getState().actors[member.id];
useCombatStore.setState((state) => ({
actors: { ...state.actors, [member.id]: { ...actor, health: 7, alive: true } },
}));
usePartyStore.setState((state) => ({
members: state.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: candidate.maxHealth }
: candidate),
}));
useCombatStore.getState().synchronizeActors();
expect(useCombatStore.getState().actors[member.id].health).toBe(7);
expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)?.health).toBe(7);
});
it("binds combo points to the enemy that generated them", () => {
useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 20 });
registerMob("combo-a", [0, 0, 3]);
registerMob("combo-b", [1, 0, 2]);
useCombatStore.getState().selectMob("combo-a");
expect(useCombatStore.getState().castAbility("rogue-sinister-strike", { now: 1_000 }).ok).toBe(true);
expect(useCombatStore.getState()).toMatchObject({ comboPoints: 1, comboTargetId: "combo-a" });
useCombatStore.setState((state) => ({
globalCooldownEndsAt: 0,
cooldowns: {},
resource: state.maxResource,
resourcePools: { ...state.resourcePools, energy: state.maxResource },
}));
useCombatStore.getState().selectMob("combo-b");
const secondStrike = useCombatStore.getState().castAbility("rogue-sinister-strike", { now: 2_000 });
expect(secondStrike.ok, secondStrike.reason).toBe(true);
expect(useCombatStore.getState()).toMatchObject({ comboPoints: 1, comboTargetId: "combo-b" });
});
it("applies boss control and taunt diminishing returns through immunity", () => {
registerMob("dr-boss", [0, 0, 3], true);
const durations: number[] = [];
for (const now of [1_000, 2_000, 3_000]) {
expect(useCombatStore.getState().controlMob("dr-boss", `stun-${now}`, "stun", 8_000, now)).toBe(true);
const status = useCombatStore.getState().mobs["dr-boss"].statuses.find((entry) => entry.kind === "stun")!;
durations.push(status.endsAt - now);
}
expect(durations).toEqual([3_000, 1_500, 750]);
expect(useCombatStore.getState().controlMob("dr-boss", "stun-immune", "stun", 8_000, 4_000)).toBe(false);
const tauntDurations: number[] = [];
for (const now of [5_000, 6_000, 7_000]) {
expect(useCombatStore.getState().tauntMob("dr-boss", { actorId: PLAYER_AGGRO_ID, role: "tank" }, 3_000, now)).toBe(true);
tauntDurations.push(useCombatStore.getState().mobs["dr-boss"].forcedTarget!.endsAt - now);
}
expect(tauntDurations).toEqual([3_000, 1_950, 1_268]);
expect(useCombatStore.getState().tauntMob("dr-boss", { actorId: PLAYER_AGGRO_ID, role: "tank" }, 3_000, 8_000)).toBe(false);
});
it("moves and confirms a collision-projected ground reticle", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
const groundAbility = abilitiesForClass("mage")
.map((ability) => abilityAtLevel(ability, 80))
.find((ability) => targetingRulesForAbility(ability).shape === "ground" && ability.range.max > 1)!;
expect(groundAbility).toBeDefined();
registerCombatSpatialProvider({
actor: (id) => ({ id, position: [0, 0, 0], yaw: 0 }),
hasLineOfSight: () => true,
projectGroundPoint: (point, maximumDistance, origin = [0, 0, 0]) => {
const dx = point[0] - origin[0];
const dz = point[2] - origin[2];
const distance = Math.hypot(dx, dz);
const scale = distance > maximumDistance ? maximumDistance / distance : 1;
return [origin[0] + dx * scale, 0, origin[2] + dz * scale];
},
});
expect(useCombatStore.getState().beginGroundTarget(groundAbility.id)).toBe(true);
const before = useCombatStore.getState().pendingGroundTarget!;
expect(before.valid).toBe(true);
useCombatStore.getState().moveGroundTarget(1, 0, 0, 0.1);
const moved = useCombatStore.getState().pendingGroundTarget!;
expect(moved.position).not.toEqual(before.position);
expect(Math.hypot(moved.position[0], moved.position[2])).toBeLessThanOrEqual(moved.maximumRange);
expect(useCombatStore.getState().confirmGroundTarget(undefined, 1_000).ok).toBe(true);
expect(useCombatStore.getState().pendingGroundTarget).toBeNull();
const activeCast = useCombatStore.getState().activeCast;
if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt);
expect(useCombatStore.getState().areaEffects).toHaveLength(1);
});
it("keeps summons targetable, credits their owner, replaces totems, and expires them", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-warden", raceId: "rom-elf", level: 20 });
useCombatStore.getState().synchronizeActors();
registerMob("summon-target", [0, 0, 3]);
useCombatStore.getState().selectMob("summon-target");
expect(useCombatStore.getState().castAbility("rom-warden-primary-1", { now: 1_000 }).ok).toBe(true);
useCombatStore.getState().tick(0.1, 5_000);
const summon = useCombatStore.getState().summons[0];
expect(summon).toBeDefined();
expect(useCombatStore.getState().actors[summon.id]).toMatchObject({ kind: "summon", ownerId: PLAYER_AGGRO_ID, alive: true });
expect(useCombatStore.getState().selectFriendlyActor(summon.id)).toBe(true);
const beforeDamage = summon.health;
expect(useCombatStore.getState().damageSummon(summon.id, 5)).toBe(5);
expect(useCombatStore.getState().actors[summon.id].health).toBe(beforeDamage - 5);
expect(useCombatStore.getState().healActor(summon.id, 5, { sourceActorId: PLAYER_AGGRO_ID }).effectiveAmount).toBe(5);
const baseAbility = abilityById("rom-warden-primary-1")!;
const combatSummonAbility: AbilityDefinition = { ...baseAbility, id: "test-guardian", name: "Test Guardian" };
useCombatStore.getState().summonActor(
PLAYER_AGGRO_ID,
combatSummonAbility,
{ kind: "summon", creatureId: 2, coefficient: 0.5, durationMs: 10_000 },
[0, 0, 0],
"summon-target",
6_000,
);
useCombatStore.getState().tick(1, 7_500);
expect(useCombatStore.getState().mobs["summon-target"].threatByActor[PLAYER_AGGRO_ID]).toBeGreaterThan(0);
expect(useCombatStore.getState().mobs["summon-target"].threatByActor[summon.id] ?? 0).toBe(0);
const totemAbility: AbilityDefinition = { ...baseAbility, id: "test-totem", name: "Test Totem" };
const totemEffect = { kind: "summon" as const, creatureId: 1, coefficient: 0.2, durationMs: 10_000 };
useCombatStore.getState().summonActor(PLAYER_AGGRO_ID, totemAbility, totemEffect, [0, 0, 0], null, 8_000);
useCombatStore.getState().summonActor(PLAYER_AGGRO_ID, totemAbility, totemEffect, [1, 0, 0], null, 8_100);
expect(useCombatStore.getState().summons.filter((candidate) => candidate.exclusiveGroup === `${PLAYER_AGGRO_ID}:totem`)).toHaveLength(1);
useCombatStore.getState().tick(0.1, 20_001);
expect(useCombatStore.getState().summons.some((candidate) => candidate.id === summon.id)).toBe(false);
expect(useCombatStore.getState().actors[summon.id]).toBeUndefined();
});
it("executes damaging abilities from Classic, RuneWaker, and CoA catalogs through one runtime", () => {
const cases = [
{ classId: "mage" as const, level: 20, source: "classic" },
{ classId: "rom-mage" as const, level: 20, source: "runewaker" },
{ classId: "stormbringer" as const, level: 60, source: "coa" },
];
registerCombatSpatialProvider({
actor: (id) => ({ id, position: id === PLAYER_AGGRO_ID ? [0, 0, 0] : getMobPosition(id) ?? [0, 0, 0], yaw: 0 }),
hasLineOfSight: () => true,
projectGroundPoint: (point) => point,
});
for (const entry of cases) {
useCombatStore.getState().initializeCharacter({ classId: entry.classId, level: entry.level });
const ability = abilitiesForClass(entry.classId)
.map((candidate) => abilityAtLevel(candidate, entry.level))
.find((candidate) => (
!candidate.passive
&& candidate.target === "hostile"
&& candidate.talentEntryId === undefined
&& candidate.effects.some((effect) => effect.kind === "damage")
));
expect(ability, `${entry.source} executable damage ability`).toBeDefined();
const id = `${entry.source}-target`;
const range = Math.max(1, Math.min(10, ability!.range.max));
registerMob(id, [0, 0, range]);
useCombatStore.getState().selectMob(id);
const before = useCombatStore.getState().mobs[id].health;
const cast = useCombatStore.getState().castAbility(ability!.id, { now: 1_000 });
expect(cast.ok, `${entry.source} cast: ${cast.reason ?? "ok"}`).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt);
expect(useCombatStore.getState().mobs[id].health, `${entry.source} damage`).toBeLessThan(before);
clearMobRuntimeRegistry();
}
});
});