Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { abilitiesForClass, learnedAbilityForBuild, type AbilityDefinition } from "./abilityCatalog";
|
||||
import { aiHintsForAbility } from "./abilityRules";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import {
|
||||
applyCoaGuardianParagonEvent,
|
||||
COA_GUARDIAN_PARAGON_TALENT_ENTRY_ID,
|
||||
projectCoaGuardianParagonAbility,
|
||||
} from "./coaGuardianParagonRuntime";
|
||||
import { choosePartyAbility, partyAbilityIsReady } from "./partyAbilityAi";
|
||||
import { evaluateCombatRotation } from "./combatRotation";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import type { ActiveAura } from "./combatAuras";
|
||||
|
||||
const RANKS = {
|
||||
[`coa-entry-${COA_GUARDIAN_PARAGON_TALENT_ENTRY_ID}`]: 1,
|
||||
"coa-entry-31169": 1,
|
||||
"coa-entry-30057": 1,
|
||||
};
|
||||
|
||||
function readyAuras(actorId: string, talentRanks: typeof RANKS, now: number): readonly ActiveAura[] {
|
||||
const actor = { id: actorId, classId: "guardian" as const, talentRanks };
|
||||
const heavyBlow = abilitiesForClass("guardian").find((ability) => ability.talentEntryId === 31169)!;
|
||||
let auras: readonly ActiveAura[] = [];
|
||||
for (let cast = 0; cast < 3; cast += 1) {
|
||||
auras = applyCoaGuardianParagonEvent(auras, actor, {
|
||||
type: "cast", ability: heavyBlow, result: "success",
|
||||
}, now - 3_000 + cast * 1_000).auras;
|
||||
}
|
||||
return applyCoaGuardianParagonEvent(auras, actor, { type: "block" }, now).auras;
|
||||
}
|
||||
|
||||
function guardian() {
|
||||
const member = createConfiguredPartyMember({
|
||||
name: "Paragon AI Guardian",
|
||||
classId: "guardian",
|
||||
specialization: "Vanguard",
|
||||
specializationId: "guardian:vanguard",
|
||||
talentTreeId: "coa-guardian-87",
|
||||
talentRanks: RANKS,
|
||||
role: "tank",
|
||||
}, 30, 0, "coa-paragon-ai");
|
||||
return { ...member, talentRanks: { ...member.talentRanks, ...RANKS }, nextActionAt: 0 };
|
||||
}
|
||||
|
||||
function install(member: ReturnType<typeof guardian>, now: number) {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "guardian", role: "tank", level: 30 });
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
usePartyStore.getState().initializeParty("coa-paragon-ai", 30,
|
||||
useCombatStore.getState().health, "tank", 1, "coa");
|
||||
usePartyStore.setState({ members: [member], command: "attack", activeMobId: "coa-paragon-ai-target" });
|
||||
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.setState({ auras: [...readyAuras(member.id, member.talentRanks, now)] });
|
||||
useCombatStore.getState().registerMob("coa-paragon-ai-target", {
|
||||
name: "Paragon AI target", level: 30, maxHealth: 100_000, xpReward: 0,
|
||||
});
|
||||
setMobPosition("coa-paragon-ai-target", [2, 0, 0]);
|
||||
}
|
||||
|
||||
function target() {
|
||||
return useCombatStore.getState().mobs["coa-paragon-ai-target"]!;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock();
|
||||
});
|
||||
|
||||
describe("CoA Guardian Paragon companion choice", () => {
|
||||
it("beats a ready routine Ballad upkeep action for a healthy Vanguard tank", () => {
|
||||
const member = guardian();
|
||||
const now = 10_000;
|
||||
install(member, now);
|
||||
const ballad = abilitiesForClass("guardian")
|
||||
.map((ability) => learnedAbilityForBuild(ability, member.level, member.talentRanks))
|
||||
.find((ability) => ability?.name === "Ballad of the Conqueror");
|
||||
expect(ballad, "Ballad of the Conqueror must be an ordinary learned candidate").toBeDefined();
|
||||
expect(partyAbilityIsReady(member, ballad!, now)).toBe(true);
|
||||
|
||||
const chosen = choosePartyAbility(member, {
|
||||
now,
|
||||
target: target(),
|
||||
friendlyTargets: [{ id: member.id, health: member.maxHealth, maxHealth: member.maxHealth, role: "tank" }],
|
||||
hostileCount: 1,
|
||||
});
|
||||
expect(chosen?.name).toBe("Paragon Smash");
|
||||
expect(chosen).toMatchObject({ coaGuardianParagonVariant: "paragon-smash" });
|
||||
});
|
||||
|
||||
it("leaves low-health mitigation ahead of Smash", () => {
|
||||
const full = guardian();
|
||||
const now = 10_000;
|
||||
install(full, now);
|
||||
// Prepare Tower normally so the scorer evaluates Brace against the tank's
|
||||
// active formation rather than choosing setup upkeep first.
|
||||
advanceDungeonPartyCombat(now);
|
||||
const prepared = usePartyStore.getState().members.find((member) => member.id === full.id)!;
|
||||
const injured = { ...prepared, health: Math.floor(prepared.maxHealth * 0.5), nextActionAt: now };
|
||||
usePartyStore.setState({ members: [injured] });
|
||||
useCombatStore.getState().commitCompanionActors([injured]);
|
||||
const liveAuras = useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.sourceId === injured.id && (aura.expiresAt === null || aura.expiresAt > now)
|
||||
));
|
||||
const activeAuraExclusiveGroups = new Set(liveAuras
|
||||
.flatMap((aura) => aura.definition.exclusiveGroup ? [aura.definition.exclusiveGroup] : []));
|
||||
|
||||
const chosen = choosePartyAbility(injured, {
|
||||
now,
|
||||
target: target(),
|
||||
friendlyTargets: [{ id: injured.id, health: injured.health, maxHealth: injured.maxHealth, role: "tank" }],
|
||||
hostileCount: 1,
|
||||
activeAuraExclusiveGroups,
|
||||
});
|
||||
expect(chosen).toBeDefined();
|
||||
expect(chosen?.name).not.toBe("Paragon Smash");
|
||||
expect(aiHintsForAbility(chosen!).categories).toContain("mitigation");
|
||||
});
|
||||
|
||||
it("keeps urgent reactive taunt and interrupt ahead of the authored priority", () => {
|
||||
const member = guardian();
|
||||
const now = 10_000;
|
||||
install(member, now);
|
||||
const heavyBlow = abilitiesForClass("guardian").find((ability) => ability.talentEntryId === 31169)!;
|
||||
const actor = {
|
||||
id: member.id,
|
||||
classId: member.classId,
|
||||
talentRanks: member.talentRanks,
|
||||
role: member.role,
|
||||
specializationId: member.specializationId,
|
||||
health: member.health,
|
||||
maxHealth: member.maxHealth,
|
||||
};
|
||||
const smash = projectCoaGuardianParagonAbility(
|
||||
heavyBlow,
|
||||
actor,
|
||||
readyAuras(member.id, member.talentRanks, now),
|
||||
now,
|
||||
);
|
||||
const synthetic = (name: string, kind: "taunt" | "interrupt"): AbilityDefinition => ({
|
||||
...heavyBlow,
|
||||
id: `test:paragon-priority-${kind}`,
|
||||
name,
|
||||
ai: { categories: [kind] },
|
||||
effects: kind === "taunt"
|
||||
? [{ kind: "taunt", durationMs: 3_000 }]
|
||||
: [{ kind: "interrupt", lockoutMs: 3_000 }],
|
||||
});
|
||||
const base = {
|
||||
now,
|
||||
classId: "guardian" as const,
|
||||
specializationId: "guardian:vanguard",
|
||||
role: "tank" as const,
|
||||
level: member.level,
|
||||
health: member.health,
|
||||
maxHealth: member.maxHealth,
|
||||
selfId: member.id,
|
||||
resources: { energy: 100 },
|
||||
maximumResources: { energy: 100 },
|
||||
primaryResource: "energy" as const,
|
||||
comboPoints: 0,
|
||||
comboTargetId: null,
|
||||
runes: [],
|
||||
hostileCount: 1,
|
||||
canUseAbility: () => true,
|
||||
};
|
||||
const taunt = synthetic("Taunt", "taunt");
|
||||
const urgentTaunt = evaluateCombatRotation({
|
||||
...base,
|
||||
abilities: [smash, taunt],
|
||||
hostileTarget: { id: "loose-target", health: 1_000, maxHealth: 1_000 },
|
||||
targetIsLoose: true,
|
||||
});
|
||||
expect(urgentTaunt?.ability).toBe(taunt);
|
||||
|
||||
const interrupt = synthetic("Interrupt", "interrupt");
|
||||
const urgentInterrupt = evaluateCombatRotation({
|
||||
...base,
|
||||
abilities: [smash, interrupt],
|
||||
hostileTarget: {
|
||||
id: "casting-target",
|
||||
health: 1_000,
|
||||
maxHealth: 1_000,
|
||||
activeCast: { interruptible: true },
|
||||
},
|
||||
});
|
||||
expect(urgentInterrupt?.ability).toBe(interrupt);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user