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

+82
View File
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it } from "vitest";
import { abilitiesForClass, abilityAtLevel, resourceProfileForClass } from "./abilityCatalog";
import { aiHintsForAbility } from "./abilityRules";
import { useCombatStore } from "./combatStore";
import { commitPartyAbilityUse, choosePartyAbility, partyAbilityIsReady } from "./partyAbilityAi";
import { usePartyStore, type PartyMember } from "./partyStore";
function memberFor(classId: PartyMember["classId"], role: PartyMember["role"]): PartyMember {
const base = usePartyStore.getState().members[0];
const profile = resourceProfileForClass(classId);
return {
...base,
classId,
role,
level: 80,
resourceType: profile.type,
resourceName: profile.name,
resource: profile.maximum,
maxResource: profile.maximum,
resourcePools: { [profile.type]: profile.maximum },
cooldowns: {},
globalCooldownEndsAt: 0,
nextActionAt: 0,
tauntReadyAt: 0,
};
}
describe("catalog-driven party rotations", () => {
beforeEach(() => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
usePartyStore.getState().initializeParty("party-ai", 80, useCombatStore.getState().health);
useCombatStore.getState().registerMob("caster", { name: "Caster", maxHealth: 1_000, xpReward: 0 });
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
caster: {
...state.mobs.caster,
activeCast: {
attackId: "test-cast",
name: "Test Cast",
school: "shadow",
interruptible: true,
targetActorId: "__player__",
startedAt: 1_000,
completesAt: 5_000,
},
},
},
}));
});
it("prioritizes an interrupt over ordinary damage deterministically", () => {
const member = memberFor("rogue", "damage");
const context = { now: 2_000, target: useCombatStore.getState().mobs.caster };
const first = choosePartyAbility(member, context);
const second = choosePartyAbility(member, context);
expect(first?.id).toBe(second?.id);
expect(first && aiHintsForAbility(first).categories).toContain("interrupt");
});
it("selects a real healer catalog spell and spends its resource/GCD", () => {
const member = memberFor("priest", "healer");
const ability = choosePartyAbility(member, {
now: 2_000,
target: null,
injuredAllies: 1,
lowestHealthFraction: 0.2,
});
expect(ability && aiHintsForAbility(ability).categories).toContain("heal");
const committed = commitPartyAbilityUse(member, ability!, 2_000)!;
expect(committed.resource).toBeLessThan(member.resource);
expect(committed.globalCooldownEndsAt).toBeGreaterThan(2_000);
});
it("will not cast a mana spell after mana exhaustion", () => {
const member = { ...memberFor("priest", "healer"), resource: 0, resourcePools: { mana: 0 } };
const heal = abilitiesForClass("priest")
.map((ability) => abilityAtLevel(ability, 80))
.find((ability) => aiHintsForAbility(ability).categories.includes("heal") && ability.cost.amount > 0)!;
expect(partyAbilityIsReady(member, heal, 2_000)).toBe(false);
});
});