Files
healer-man/src/game/romCombatRuntime.test.ts
T
2026-09-13 15:09:35 -04:00

302 lines
15 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { defaultActionBarForCharacter, resourceProfileForClass } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
clearPartyRuntimeRegistry,
registerPartyRuntimePosition,
} from "./partyRuntimeRegistry";
import { usePartyStore } from "./partyStore";
import { useCombatStore } from "./combatStore";
import { companionActorFromMember, companionActorWithMemberRuntime } from "./combatActorAuthority";
describe("RuneWaker combat runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
});
it("creates keyed pools for both classes and changes the active resource on orientation swap", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-warrior",
secondaryClassId: "rom-scout",
raceId: "rom-human",
level: 70,
xp: 456,
});
expect(useCombatStore.getState()).toMatchObject({
classId: "rom-warrior",
secondaryClassId: "rom-scout",
resourceName: "Rage",
xp: 456,
});
expect(useCombatStore.getState().maxResourcePools).toMatchObject({ rage: 100, focus: 100 });
expect(new Set(Object.values(useCombatStore.getState().actionBindings).filter(Boolean))).toEqual(
new Set(defaultActionBarForCharacter("rom-warrior", "rom-scout", 70).slice(0, 24)),
);
useCombatStore.getState().initializeCharacter({
classId: "rom-scout",
secondaryClassId: "rom-warrior",
raceId: "rom-human",
level: 70,
xp: 456,
});
expect(useCombatStore.getState()).toMatchObject({
classId: "rom-scout",
secondaryClassId: "rom-warrior",
resourceName: "Focus",
xp: 456,
});
expect(useCombatStore.getState().maxResourcePools).toMatchObject({ focus: 100, rage: 100 });
});
it("aggregates multi-cost rows by resource before validating a cast", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-mage",
secondaryClassId: "rom-warden",
raceId: "rom-elf",
level: 50,
});
const maximumMana = useCombatStore.getState().maxResourcePools.mana!;
const availableMana = Math.max(100, Math.round(maximumMana * 0.05));
useCombatStore.setState((state) => ({
resource: availableMana,
resourcePools: { ...state.resourcePools, mana: availableMana },
}));
expect(useCombatStore.getState().castAbility("rom-mage-rom-warden-elite-50", undefined, 1_000)).toMatchObject({
ok: false,
reason: "not-enough-resource",
});
});
it.each(["rom-warrior", "rom-scout", "rom-rogue"] as const)(
"preserves %s secondary mana in player and companion resource snapshots",
(classId) => {
useCombatStore.getState().initializeCharacter({
classId, secondaryClassId: "rom-warden", raceId: "rom-elf", level: 80,
});
const state = useCombatStore.getState();
expect(state.maxResourcePools.mana).toBe(100);
expect(state.resourcePools.mana).toBe(100);
state.synchronizeActors();
expect(useCombatStore.getState().actors[PLAYER_AGGRO_ID].resources.mana).toMatchObject({ current: 100, maximum: 100 });
usePartyStore.getState().initializeParty("rom-secondary-mana", 80, state.health);
const member = {
...usePartyStore.getState().members[0]!, classId, raceId: state.raceId,
resourceType: resourceProfileForClass(classId).type, maxResource: state.maxResource,
resourcePools: state.resourcePools, gearStats: state.gearStats,
};
const actor = companionActorFromMember(member);
expect(actor.resources.mana).toMatchObject({ current: 100, maximum: 100 });
expect(companionActorWithMemberRuntime(actor, member).resources.mana)
.toMatchObject({ current: 100, maximum: 100 });
},
);
it.each([
["rom-priest", "rom-human", "rom-priest-general-1"],
["rom-druid", "rom-elf", "rom-druid-primary-0"],
] as const)("lets a level-one %s cast its starting attack", (classId, raceId, abilityId) => {
useCombatStore.getState().initializeCharacter({ classId, raceId, level: 1 });
useCombatStore.getState().registerMob("starter-dummy", { name: "Starter Dummy", maxHealth: 1_000 });
setMobPosition("starter-dummy", [3, 0, 0]);
useCombatStore.getState().selectMob("starter-dummy");
expect(useCombatStore.getState().castAbility(abilityId, undefined, 1_000)).toMatchObject({ ok: true });
});
it("uses source coefficients for direct damage even without equipment", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", raceId: "rom-human", level: 20 });
useCombatStore.getState().registerMob("coefficient-dummy", { name: "Coefficient Dummy", maxHealth: 1_000 });
setMobPosition("coefficient-dummy", [3, 0, 0]);
useCombatStore.getState().selectMob("coefficient-dummy");
useCombatStore.setState((state) => ({
resource: 100,
resourcePools: { ...state.resourcePools, rage: 100 },
}));
expect(useCombatStore.getState().castAbility("rom-warrior-primary-1", undefined, 1_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().mobs["coefficient-dummy"]!.health).toBeLessThan(1_000);
});
it("finishes a RuneWaker direct heal on the selected party member", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 20 });
usePartyStore.getState().initializeParty("rom-urgent-heal", 20, useCombatStore.getState().health, "healer", 2, "rom");
const member = usePartyStore.getState().members[0]!;
usePartyStore.getState().damageMember(member.id, 500);
registerPartyRuntimePosition(member.id, [3, 0, 0]);
usePartyStore.getState().selectMember(member.id);
const injuredHealth = usePartyStore.getState().members[0]!.health;
expect(useCombatStore.getState().castAbility("rom-priest-general-0", undefined, 1_000)).toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 2_100);
expect(useCombatStore.getState().activeCast).toBeNull();
expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(injuredHealth);
});
it("uses native RoM Rage gains from auto attacks and health damage", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-warrior",
raceId: "rom-human",
level: 1,
});
useCombatStore.getState().registerMob("rage-dummy", { name: "Rage Dummy", maxHealth: 1_000 });
setMobPosition("rage-dummy", [3, 0, 0]);
useCombatStore.getState().selectMob("rage-dummy");
useCombatStore.setState({ shield: 100 });
expect(useCombatStore.getState().damagePlayer(5)).toBe(0);
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
useCombatStore.setState({ shield: 0 });
expect(useCombatStore.getState().castAbility("rom-warrior-general-0", undefined, 1_000)).toMatchObject({
ok: false,
reason: "not-enough-resource",
});
expect(useCombatStore.getState().autoAttackTargetId).toBe("rage-dummy");
useCombatStore.getState().tick(0.1, 1_000);
expect(useCombatStore.getState().resourcePools.rage).toBe(6);
expect(useCombatStore.getState().resource).toBe(6);
const healthAfterFirstSwing = useCombatStore.getState().mobs["rage-dummy"]!.health;
expect(healthAfterFirstSwing).toBeLessThan(1_000);
useCombatStore.getState().tick(1, 2_000);
expect(useCombatStore.getState().mobs["rage-dummy"]!.health).toBe(healthAfterFirstSwing);
expect(useCombatStore.getState().resourcePools.rage).toBe(6);
expect(useCombatStore.getState().damagePlayer(5)).toBeGreaterThan(0);
expect(useCombatStore.getState().resourcePools.rage).toBe(13);
useCombatStore.getState().tick(1, 3_400);
const secondSwing = useCombatStore.getState().lastCombatResult!;
if (secondSwing.finalDamage > 0) {
expect(useCombatStore.getState().mobs["rage-dummy"]!.health).toBeLessThan(healthAfterFirstSwing);
expect(useCombatStore.getState().resourcePools.rage).toBe(19);
} else {
expect(useCombatStore.getState().mobs["rage-dummy"]!.health).toBe(healthAfterFirstSwing);
expect(useCombatStore.getState().resourcePools.rage).toBe(13);
}
});
it("uses Champion's attack cadence instead of Warrior's per-hit Rage rules", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 1 });
useCombatStore.getState().registerMob("champion-dummy", { name: "Champion Dummy", maxHealth: 1_000 });
setMobPosition("champion-dummy", [3, 0, 0]);
useCombatStore.getState().selectMob("champion-dummy");
useCombatStore.getState().castAbility("rom-champion-general-0", undefined, 1_000);
useCombatStore.getState().tick(1, 1_000);
expect(useCombatStore.getState().resourcePools.rage).toBe(6);
useCombatStore.getState().damagePlayer(5);
expect(useCombatStore.getState().resourcePools.rage).toBe(6);
useCombatStore.getState().tick(1, 2_000);
expect(useCombatStore.getState().resourcePools.rage).toBe(12);
});
it("decays native RoM Rage while out of combat", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", raceId: "rom-human", level: 1 });
useCombatStore.setState((state) => ({
resource: 20,
resourcePools: { ...state.resourcePools, rage: 20 },
}));
useCombatStore.getState().tick(1, 1_000);
expect(useCombatStore.getState().resource).toBe(17);
expect(useCombatStore.getState().resourcePools.rage).toBe(17);
});
it("keeps native summons as temporary allied combat actors", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-warden", raceId: "rom-elf", level: 20 });
const maximumMana = useCombatStore.getState().maxResourcePools.mana!;
useCombatStore.getState().castAbility("rom-warden-primary-1", undefined, 1_000);
useCombatStore.getState().tick(0.1, 5_000);
expect(useCombatStore.getState().summons).toEqual([
expect.objectContaining({ sourceAbilityId: "rom-warden-primary-1", creatureId: 102297 }),
]);
expect(useCombatStore.getState().resourcePools.mana).toBe(maximumMana - Math.round(maximumMana * 0.15));
useCombatStore.getState().tick(0.1, 20_001);
expect(useCombatStore.getState().summons).toEqual([]);
});
it("raises an incapacitated party member with source resurrection behavior", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 20 });
usePartyStore.getState().initializeParty("rom-resurrection", 20, useCombatStore.getState().health, "healer", 2, "rom");
const member = usePartyStore.getState().members[0]!;
usePartyStore.getState().damageMember(member.id, member.maxHealth * 10);
registerPartyRuntimePosition(member.id, [3, 0, 0]);
usePartyStore.getState().selectMember(member.id);
expect(usePartyStore.getState().members[0]!.health).toBe(0);
expect(useCombatStore.getState().castAbility("rom-priest-primary-5", undefined, 1_000)).toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 7_000);
expect(useCombatStore.getState()).toMatchObject({ activeCast: null, feedback: { kind: "success" } });
expect(usePartyStore.getState().members[0]).toMatchObject({
health: Math.round(member.maxHealth * 0.35),
status: "ready",
});
});
it("chains Plasma Arrow, Electric Compression, and Static Field through their source states", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-mage", raceId: "rom-human", level: 20 });
useCombatStore.getState().registerMob("static-dummy", { name: "Static Dummy", maxHealth: 10_000 });
setMobPosition("static-dummy", [3, 0, 0]);
useCombatStore.getState().selectMob("static-dummy");
expect(useCombatStore.getState().castAbility("rom-mage-primary-55", undefined, 1_000)).toMatchObject({
ok: false,
reason: "condition-not-met",
});
expect(useCombatStore.getState().castAbility("rom-mage-primary-52", undefined, 2_000)).toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 4_100);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-charged"))).toBe(true);
expect(useCombatStore.getState().castAbility("rom-mage-primary-55", undefined, 4_200)).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-charged"))).toBe(false);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-static-field-charge"))).toBe(true);
expect(useCombatStore.getState().castAbility("rom-mage-primary-56", undefined, 5_800)).toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 6_900);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-static-field-charge"))).toBe(false);
});
it("lets Remodeled Body prevent lethal damage once", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 80 });
const now = Date.now();
expect(useCombatStore.getState().castAbility("rom-champion-primary-16", undefined, now)).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-remodeled-body"))).toBe(true);
useCombatStore.getState().damagePlayer(useCombatStore.getState().maxHealth * 10);
expect(useCombatStore.getState().health).toBe(useCombatStore.getState().maxHealth);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-remodeled-body"))).toBe(false);
});
it("builds and consumes Revival charges from damage taken", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue",
secondaryClassId: "rom-warden",
raceId: "rom-elf",
level: 80,
});
useCombatStore.getState().damagePlayer(100);
useCombatStore.getState().damagePlayer(100);
useCombatStore.getState().damagePlayer(100);
const revival = useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("rom-revival-charge"));
expect(revival?.stacks).toBe(3);
const injuredHealth = useCombatStore.getState().health;
const manaBefore = useCombatStore.getState().resourcePools.mana!;
expect(useCombatStore.getState().castAbility("rom-rogue-rom-warden-elite-60", undefined, Date.now())).toMatchObject({ ok: true });
expect(useCombatStore.getState().health).toBeGreaterThan(injuredHealth);
expect(useCombatStore.getState().resourcePools.mana).toBe(manaBefore - 15);
expect(useCombatStore.getState().resourcePools.energy).toBe(100);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(false);
});
});