forked from phenom/healer-man
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,211 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
clearPartyRuntimeRegistry,
|
||||
registerPartyRuntimePosition,
|
||||
updatePartyRuntimeTargetVisibility,
|
||||
} from "./partyRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { abilitiesForCharacter } from "./abilityCatalog";
|
||||
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
||||
import { healingRecipientIds } from "./healingAbilityRules";
|
||||
import type { CombatActorState } from "./combatActors";
|
||||
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
|
||||
|
||||
const groupHeal = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490281)!;
|
||||
const groupHealEffect = groupHeal.effects.find((effect) => effect.kind === "heal" && effect.scope === "party");
|
||||
const GROUP_HEAL_ABILITY_ID = "rom-priest-primary-8";
|
||||
|
||||
function partyMember(
|
||||
classId: "rom-priest" | "rom-knight" | "rom-scout" | "rom-rogue" | "rom-mage",
|
||||
secondaryClassId: "rom-knight" | "rom-scout" | "rom-rogue" | "rom-warrior" | "rom-warlock",
|
||||
role: "healer" | "tank" | "damage",
|
||||
name: string,
|
||||
index: number,
|
||||
seed: string,
|
||||
) {
|
||||
return createConfiguredPartyMember({
|
||||
name,
|
||||
classId,
|
||||
secondaryClassId,
|
||||
raceId: classId === "rom-mage" ? "rom-dwarf" : classId === "rom-scout" || classId === "rom-rogue" ? "rom-elf" : "rom-human",
|
||||
statRuleset: "rom",
|
||||
specialization: "Primary",
|
||||
specializationId: `${classId}:${secondaryClassId}`,
|
||||
talentTreeId: "",
|
||||
role,
|
||||
}, 30, index, seed);
|
||||
}
|
||||
|
||||
function installLineOfSightBlock(x: number): void {
|
||||
registerCombatSpatialProvider({
|
||||
actor: () => null,
|
||||
hasLineOfSight: (_from, to) => to[0] !== x,
|
||||
projectGroundPoint: (position) => position,
|
||||
});
|
||||
}
|
||||
|
||||
function createFixture(seed: string) {
|
||||
const healer = partyMember("rom-priest", "rom-knight", "healer", "Priest", 0, seed);
|
||||
const near = partyMember("rom-knight", "rom-scout", "tank", "Near ally", 1, seed);
|
||||
const secondNear = partyMember("rom-scout", "rom-rogue", "damage", "Second near ally", 2, seed);
|
||||
const far = partyMember("rom-rogue", "rom-warrior", "damage", "Far ally", 3, seed);
|
||||
const blocked = partyMember("rom-mage", "rom-warlock", "damage", "Blocked ally", 4, seed);
|
||||
return { healer, near, secondNear, far, blocked };
|
||||
}
|
||||
|
||||
function placeParty(members: readonly ReturnType<typeof partyMember>[], positions: Readonly<Record<string, readonly [number, number, number]>>): void {
|
||||
usePartyStore.setState({ members });
|
||||
for (const member of members) {
|
||||
const position = positions[member.name];
|
||||
if (position) registerPartyRuntimePosition(member.id, position);
|
||||
}
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
}
|
||||
|
||||
function groupHealTargets(sourceId: string): readonly string[] {
|
||||
return useCombatStore.getState().combatEvents
|
||||
.filter((event) => event.kind === "healing" && event.sourceActorId === sourceId
|
||||
&& event.abilityId === GROUP_HEAL_ABILITY_ID && event.effectiveAmount > 0)
|
||||
.map((event) => event.targetActorId);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetCombatSpatialProvider();
|
||||
resetPartyRuntimeClock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetCombatSpatialProvider();
|
||||
});
|
||||
|
||||
describe("RoM Group Heal radius", () => {
|
||||
it("copies the authored radius onto only its party-heal effect and resolves living visible allies within 12m", () => {
|
||||
expect(groupHeal.radius).toBe(12);
|
||||
expect(groupHealEffect).toMatchObject({ kind: "heal", scope: "party", radius: 12 });
|
||||
if (groupHealEffect?.kind !== "heal") throw new Error("Group Heal party-heal effect is missing");
|
||||
|
||||
const actors = Object.fromEntries([
|
||||
[PLAYER_AGGRO_ID, { id: PLAYER_AGGRO_ID, kind: "player", alive: true, health: 50, maxHealth: 100 }],
|
||||
["inside", { id: "inside", kind: "companion", alive: true, health: 50, maxHealth: 100 }],
|
||||
["outside", { id: "outside", kind: "companion", alive: true, health: 50, maxHealth: 100 }],
|
||||
["dead", { id: "dead", kind: "companion", alive: false, health: 0, maxHealth: 100 }],
|
||||
["blocked", { id: "blocked", kind: "companion", alive: true, health: 50, maxHealth: 100 }],
|
||||
["enemy", { id: "enemy", kind: "enemy", alive: true, health: 50, maxHealth: 100 }],
|
||||
["legacy-range", { id: "legacy-range", kind: "companion", alive: true, health: 50, maxHealth: 100 }],
|
||||
]) as unknown as Readonly<Record<string, CombatActorState>>;
|
||||
const positions: Readonly<Record<string, readonly [number, number, number]>> = {
|
||||
[PLAYER_AGGRO_ID]: [0, 0, 0],
|
||||
inside: [12, 0, 0],
|
||||
outside: [12.01, 0, 0],
|
||||
dead: [3, 0, 0],
|
||||
blocked: [6, 0, 0],
|
||||
enemy: [2, 0, 0],
|
||||
"legacy-range": [20, 0, 0],
|
||||
};
|
||||
const recipients = healingRecipientIds(
|
||||
groupHealEffect,
|
||||
PLAYER_AGGRO_ID,
|
||||
null,
|
||||
actors,
|
||||
(id) => positions[id] ?? null,
|
||||
[],
|
||||
(_from, to) => to[0] !== 6,
|
||||
);
|
||||
expect(recipients).toEqual(expect.arrayContaining([PLAYER_AGGRO_ID, "inside"]));
|
||||
for (const excluded of ["outside", "dead", "blocked", "enemy"]) expect(recipients).not.toContain(excluded);
|
||||
|
||||
const soulSource = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490288)!;
|
||||
const soulHeal = soulSource.effects.find((effect) => effect.kind === "heal" && effect.scope === "party");
|
||||
expect(soulSource.radius).toBe(9);
|
||||
expect(soulHeal).toMatchObject({ kind: "heal", scope: "party" });
|
||||
expect(soulHeal && "radius" in soulHeal ? soulHeal.radius : undefined).toBeUndefined();
|
||||
if (soulHeal?.kind === "heal") {
|
||||
expect(healingRecipientIds(
|
||||
soulHeal,
|
||||
PLAYER_AGGRO_ID,
|
||||
null,
|
||||
actors,
|
||||
(id) => positions[id] ?? (id === "legacy-range" ? [20, 0, 0] : null),
|
||||
[],
|
||||
() => true,
|
||||
)).toContain("legacy-range");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the 12m living and line-of-sight recipient set for player Group Heal", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer",
|
||||
});
|
||||
usePartyStore.getState().initializeParty("rom-group-heal-player-radius", 30,
|
||||
useCombatStore.getState().health, "healer", 5, "rom");
|
||||
const fixture = createFixture("rom-group-heal-player-radius");
|
||||
const party = [fixture.near, fixture.secondNear, fixture.far, fixture.blocked].map((member) => ({
|
||||
...member,
|
||||
health: Math.round(member.maxHealth * 0.65),
|
||||
}));
|
||||
placeParty(party, {
|
||||
"Near ally": [2, 0, 0],
|
||||
"Second near ally": [10, 0, 0],
|
||||
"Far ally": [13, 0, 0],
|
||||
"Blocked ally": [6, 0, 0],
|
||||
});
|
||||
useCombatStore.getState().damagePlayer(Math.round(useCombatStore.getState().maxHealth * 0.25));
|
||||
installLineOfSightBlock(6);
|
||||
expect(useCombatStore.getState().castAbility(GROUP_HEAL_ABILITY_ID, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 4_100);
|
||||
|
||||
const recipients = groupHealTargets(PLAYER_AGGRO_ID);
|
||||
expect(recipients).toEqual(expect.arrayContaining([PLAYER_AGGRO_ID, party[0]!.id, party[1]!.id]));
|
||||
expect(recipients).not.toContain(party[2]!.id);
|
||||
expect(recipients).not.toContain(party[3]!.id);
|
||||
});
|
||||
|
||||
it("uses the same 12m living and line-of-sight recipient set for companion Group Heal", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 30 });
|
||||
usePartyStore.getState().initializeParty("rom-group-heal-companion-radius", 30,
|
||||
useCombatStore.getState().health, "damage", 5, "rom");
|
||||
const fixture = createFixture("rom-group-heal-companion-radius");
|
||||
const healerAbilities = abilitiesForCharacter("rom-priest", "rom-knight");
|
||||
const party = [
|
||||
{
|
||||
...fixture.healer,
|
||||
cooldowns: Object.fromEntries(healerAbilities.filter((ability) => ability.id !== GROUP_HEAL_ABILITY_ID)
|
||||
.map((ability) => [ability.id, 60_000])),
|
||||
},
|
||||
{ ...fixture.near, health: Math.round(fixture.near.maxHealth * 0.7) },
|
||||
{ ...fixture.secondNear, health: Math.round(fixture.secondNear.maxHealth * 0.75) },
|
||||
{ ...fixture.far, health: Math.round(fixture.far.maxHealth * 0.75) },
|
||||
{ ...fixture.blocked, health: Math.round(fixture.blocked.maxHealth * 0.75) },
|
||||
];
|
||||
placeParty(party, {
|
||||
Priest: [0, 0, 0],
|
||||
"Near ally": [2, 0, 0],
|
||||
"Second near ally": [5, 0, 0],
|
||||
"Far ally": [13, 0, 0],
|
||||
"Blocked ally": [6, 0, 0],
|
||||
});
|
||||
installLineOfSightBlock(6);
|
||||
useCombatStore.getState().registerMob("rom-group-heal-radius-boss", {
|
||||
name: "Group Heal target", boss: true, maxHealth: 100_000, xpReward: 0,
|
||||
});
|
||||
setMobPosition("rom-group-heal-radius-boss", [2, 0, 2]);
|
||||
useCombatStore.getState().damageMob("rom-group-heal-radius-boss", 1, 100);
|
||||
usePartyStore.setState({ command: "attack", activeMobId: "rom-group-heal-radius-boss" });
|
||||
for (const member of party) updatePartyRuntimeTargetVisibility(member.id, "rom-group-heal-radius-boss", true);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
|
||||
advanceDungeonPartyCombat(1_000);
|
||||
advanceDungeonPartyCombat(8_000);
|
||||
|
||||
const recipients = groupHealTargets(fixture.healer.id);
|
||||
expect(recipients).toEqual(expect.arrayContaining([fixture.near.id, fixture.secondNear.id]));
|
||||
expect(recipients).not.toContain(fixture.far.id);
|
||||
expect(recipients).not.toContain(fixture.blocked.id);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user