forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { COA_ABILITIES_BY_CLASS } from "./coaAbilityCatalog";
|
||||
import { coaTreeEntryById } from "./coaLiveCatalog";
|
||||
import {
|
||||
COA_BARBARIAN_DECAPITATE_HEALTH_THRESHOLD,
|
||||
COA_BARBARIAN_DECAPITATE_TALENT_ENTRY_ID,
|
||||
coaBarbarianDecapitateTargetAllowed,
|
||||
isCoaBarbarianDecapitate,
|
||||
} from "./coaBarbarianExecuteRuntime";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { evaluateCombatRotation, type RotationContext } from "./combatRotation";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
clearPartyRuntimeRegistry,
|
||||
registerPartyRuntimePosition,
|
||||
updatePartyRuntimeTargetVisibility,
|
||||
} from "./partyRuntimeRegistry";
|
||||
import { partyAbilityTargetIsValid } from "./partyRuntime";
|
||||
import { createConfiguredPartyMember } from "./partyStore";
|
||||
import { resourceProfileForClass } from "./abilityCatalog";
|
||||
|
||||
const TARGET_ID = "coa-decapitate-target";
|
||||
const TALENT_RANKS = Object.freeze({ [`coa-entry-${COA_BARBARIAN_DECAPITATE_TALENT_ENTRY_ID}`]: 1 });
|
||||
|
||||
function decapitate() {
|
||||
const ability = COA_ABILITIES_BY_CLASS.barbarian.find((candidate) => (
|
||||
candidate.talentEntryId === COA_BARBARIAN_DECAPITATE_TALENT_ENTRY_ID
|
||||
));
|
||||
if (!ability) throw new Error("The source-backed CoA Barbarian Decapitate action is missing.");
|
||||
return ability;
|
||||
}
|
||||
|
||||
function setTargetHealth(health: number): void {
|
||||
useCombatStore.setState((state) => ({
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[TARGET_ID]: { ...state.mobs[TARGET_ID]!, health, dead: health <= 0 },
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function rotationContext(targetHealth: number): RotationContext {
|
||||
const ability = decapitate();
|
||||
const resource = resourceProfileForClass("barbarian").type;
|
||||
return {
|
||||
now: 1_000,
|
||||
classId: "barbarian",
|
||||
specializationId: "barbarian:brutality",
|
||||
role: "damage",
|
||||
level: 60,
|
||||
health: 1_000,
|
||||
maxHealth: 1_000,
|
||||
resources: { [resource]: 10_000 },
|
||||
maximumResources: { [resource]: 10_000 },
|
||||
primaryResource: resource,
|
||||
comboPoints: 0,
|
||||
comboTargetId: null,
|
||||
runes: [],
|
||||
abilities: [ability],
|
||||
hostileTarget: { id: TARGET_ID, health: targetHealth, maxHealth: 1_000 },
|
||||
hostileCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe("CoA Barbarian Decapitate execute window", () => {
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "barbarian",
|
||||
specializationId: "barbarian:brutality",
|
||||
role: "damage",
|
||||
level: 60,
|
||||
talentRanks: TALENT_RANKS,
|
||||
});
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||
name: "Decapitate target",
|
||||
maxHealth: 1_000,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||
useCombatStore.getState().selectMob(TARGET_ID);
|
||||
});
|
||||
|
||||
it("pins the execute threshold to the source-backed tree description and exact action identity", () => {
|
||||
const source = coaTreeEntryById(COA_BARBARIAN_DECAPITATE_TALENT_ENTRY_ID);
|
||||
expect(source?.rankDescriptions?.[0]).toMatchObject({ spellId: 804414 });
|
||||
expect(source?.description).toContain("Only usable on enemies below 35% health.");
|
||||
expect(COA_BARBARIAN_DECAPITATE_HEALTH_THRESHOLD).toBe(0.35);
|
||||
|
||||
const ability = decapitate();
|
||||
expect(isCoaBarbarianDecapitate(ability)).toBe(true);
|
||||
expect(coaBarbarianDecapitateTargetAllowed(ability, { health: 349, maxHealth: 1_000 })).toBe(true);
|
||||
expect(coaBarbarianDecapitateTargetAllowed(ability, { health: 350, maxHealth: 1_000 })).toBe(false);
|
||||
expect(coaBarbarianDecapitateTargetAllowed(ability, { health: 351, maxHealth: 1_000 })).toBe(false);
|
||||
for (const target of [undefined, null, { health: 349 }, { health: 349, maxHealth: 0 },
|
||||
{ health: 0, maxHealth: 1_000 }, { health: 349, maxHealth: 1_000, dead: true }]) {
|
||||
expect(coaBarbarianDecapitateTargetAllowed(ability, target)).toBe(false);
|
||||
}
|
||||
expect(isCoaBarbarianDecapitate({ classId: "guardian", talentEntryId: 5744 })).toBe(false);
|
||||
expect(isCoaBarbarianDecapitate({ classId: "barbarian", talentEntryId: 1234 })).toBe(false);
|
||||
expect(coaBarbarianDecapitateTargetAllowed({ classId: "barbarian", talentEntryId: 1234 }, undefined)).toBe(true);
|
||||
expect(coaBarbarianDecapitateTargetAllowed({ classId: "guardian", talentEntryId: 5744 }, undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects the player's boundary cast without spending resource or cooldown, then allows it below 35%", () => {
|
||||
const ability = decapitate();
|
||||
expect(useCombatStore.getState().talentRanks[`coa-entry-${ability.talentEntryId}`]).toBe(1);
|
||||
setTargetHealth(350);
|
||||
const before = useCombatStore.getState();
|
||||
const rejected = useCombatStore.getState().castAbility(ability.id, { now: 1_000, targetActorId: TARGET_ID });
|
||||
const after = useCombatStore.getState();
|
||||
expect(rejected).toMatchObject({ ok: false, reason: "condition-not-met" });
|
||||
expect(after.resourcePools).toEqual(before.resourcePools);
|
||||
expect(after.cooldowns).toEqual(before.cooldowns);
|
||||
expect(after.globalCooldownEndsAt).toBe(before.globalCooldownEndsAt);
|
||||
|
||||
setTargetHealth(349);
|
||||
expect(useCombatStore.getState().castAbility(ability.id, { now: 2_000, targetActorId: TARGET_ID }))
|
||||
.toMatchObject({ ok: true, abilityId: ability.id });
|
||||
});
|
||||
|
||||
it("applies the same target boundary to a Barbarian companion", () => {
|
||||
const ability = decapitate();
|
||||
const companion = createConfiguredPartyMember({
|
||||
name: "Brutality companion",
|
||||
classId: "barbarian",
|
||||
specialization: "Brutality",
|
||||
specializationId: "barbarian:brutality",
|
||||
talentTreeId: "barbarian:brutality",
|
||||
talentRanks: TALENT_RANKS,
|
||||
role: "damage",
|
||||
}, 60, 0, "coa-decapitate-companion");
|
||||
registerPartyRuntimePosition(companion.id, [0, 0, 0]);
|
||||
updatePartyRuntimeTargetVisibility(companion.id, TARGET_ID, true);
|
||||
|
||||
setTargetHealth(350);
|
||||
expect(partyAbilityTargetIsValid(companion, ability, TARGET_ID, undefined, 1_000)).toBe(false);
|
||||
setTargetHealth(349);
|
||||
expect(partyAbilityTargetIsValid(companion, ability, TARGET_ID, undefined, 1_000)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps Decapitate out of the companion rotation above and at the boundary", () => {
|
||||
const belowThreshold = evaluateCombatRotation(rotationContext(349));
|
||||
expect(belowThreshold?.ability.id).toBe(decapitate().id);
|
||||
expect(evaluateCombatRotation(rotationContext(350))?.ability.id).not.toBe(decapitate().id);
|
||||
expect(evaluateCombatRotation(rotationContext(500))?.ability.id).not.toBe(decapitate().id);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user