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,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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user