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

152 lines
7.3 KiB
TypeScript

import { afterAll, describe, expect, it } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { useCombatStore } from "./combatStore";
import { usePartyStore } from "./partyStore";
import { dungeonEnemyHealthBaseline, scaledDungeonEnemyLevel } from "./dungeonLevelScaling";
import { BARREN_CAVES_ENTITIES } from "./generated/barrenCavesPopulation.generated";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { advanceDungeonPartyCombat, partyCombatRangeBand, resetPartyRuntimeClock } from "./partyRuntime";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry";
import { subscribeCombatEvents } from "./combatEvents";
import { abilitiesForClass } from "./abilityCatalog";
const report: unknown[] = [];
const trash = BARREN_CAVES_ENTITIES["rw-barren-caves-101456"];
const boss = BARREN_CAVES_ENTITIES["rw-barren-caves-101462"];
afterAll(() => {
if (process.env.ROM_DUNGEON_HEALTH_REPORT !== "1") return;
const directory = resolve("artifacts/rom-dungeon-health");
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, "report.json"), `${JSON.stringify({ results: report }, null, 2)}\n`);
});
function initializePull(level: number, seed: string, maxHealth: number, isBoss = false): void {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetPartyRuntimeClock();
useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level });
useCombatStore.getState().setPlayerPosition([-18, 0, 0]);
usePartyStore.getState().initializeParty(seed, level, useCombatStore.getState().health, "healer", 5, "rom");
useCombatStore.getState().synchronizeActors();
const definition = isBoss ? boss : trash;
useCombatStore.getState().registerMob("pull", {
name: definition.name,
boss: isBoss,
level: scaledDungeonEnemyLevel(level, isBoss),
maxHealth,
healthMultiplier: definition.combat.healthMultiplier,
attacks: definition.combat.attacks,
xpReward: 0,
});
setMobPosition("pull", [0, 0, 0]);
for (const member of usePartyStore.getState().members) {
registerPartyRuntimePosition(member.id, [-partyCombatRangeBand(member).preferred, 0, 0]);
updatePartyRuntimeTargetVisibility(member.id, "pull", true);
}
useCombatStore.getState().damageMob("pull", 1, 100);
}
function measurePull(level: number, seed: string, maxHealth: number, isBoss = false) {
initializePull(level, seed, maxHealth, isBoss);
let firstDamageAt: number | null = null;
let diedAt: number | null = null;
let openingHealth = maxHealth;
const stop = subscribeCombatEvents((event) => {
if (event.targetActorId !== "pull") return;
if (event.kind === "damage" && event.effectiveAmount > 0) firstDamageAt ??= event.occurredAt;
});
try {
for (let now = 1_000; now <= 121_000; now += 100) {
useCombatStore.getState().tick(0.1, now);
advanceDungeonPartyCombat(now);
const mob = useCombatStore.getState().mobs.pull;
if (firstDamageAt !== null && now <= firstDamageAt + 1_000) openingHealth = mob.health;
if (mob.dead) {
diedAt = mob.diedAt ?? now;
break;
}
}
return { maxHealth, seconds: diedAt === null || firstDamageAt === null ? null : (diedAt - firstDamageAt) / 1_000,
openingHealth, classes: usePartyStore.getState().members.map((member) => member.classId) };
} finally {
stop();
}
}
describe("RoM dungeon enemy durability", () => {
for (const level of [1, 2, 4, 8, 9, 20, 40, 70, 80]) {
it(`Barren Caves trash survives a full RoM party's opening volley at level ${level}`, () => {
for (let seed = 0; seed < 12; seed += 1) {
const partySeed = `rom-health-${seed}`;
const before = measurePull(level, partySeed, 72 + level * 22);
const after = measurePull(level, partySeed, dungeonEnemyHealthBaseline(level, "rom"));
report.push({ level, seed: partySeed, before, after });
expect(after.openingHealth, JSON.stringify({ level, before, after })).toBeGreaterThan(0);
expect(after.seconds, JSON.stringify({ level, before, after })).not.toBeNull();
expect(after.seconds!, JSON.stringify({ level, before, after })).toBeGreaterThanOrEqual(3);
expect(after.seconds!, JSON.stringify({ level, before, after })).toBeLessThanOrEqual(30);
}
});
}
it("retains Barren Caves' authored boss health multiplier", () => {
const level = 8;
const maxHealth = dungeonEnemyHealthBaseline(scaledDungeonEnemyLevel(level, true), "rom")
* boss.combat.healthMultiplier;
const result = measurePull(level, "rom-health-0", maxHealth, true);
report.push({ level, boss: boss.name, ...result });
expect(result.seconds).not.toBeNull();
expect(result.seconds!).toBeGreaterThanOrEqual(20);
expect(result.seconds!).toBeLessThanOrEqual(120);
});
it("allows enemy attacks and Priest healing before an opening pull ends", () => {
const level = 1;
initializePull(level, "rom-health-0", dungeonEnemyHealthBaseline(level, "rom"));
const heal = abilitiesForClass("rom-priest").find((ability) => ability.name === "Urgent Heal")!;
let incomingDamage = 0;
let healing = 0;
const stop = subscribeCombatEvents((event) => {
if (event.sourceActorId === "pull" && event.kind === "damage") incomingDamage += event.effectiveAmount;
if (event.kind === "healing") healing += event.effectiveAmount;
});
try {
for (let now = 1_000; now <= 31_000; now += 100) {
useCombatStore.getState().tick(0.1, now);
advanceDungeonPartyCombat(now);
useCombatStore.getState().advanceMobCombat(now);
const tank = usePartyStore.getState().members.find((member) => member.role === "tank")!;
if (tank.health < tank.maxHealth && !useCombatStore.getState().activeCast) {
usePartyStore.getState().selectMember(tank.id);
useCombatStore.getState().castAbility(heal.id, undefined, now);
}
if (useCombatStore.getState().mobs.pull.dead) break;
}
expect(useCombatStore.getState().mobs.pull.dead).toBe(true);
expect(incomingDamage).toBeGreaterThan(0);
expect(healing).toBeGreaterThan(0);
expect(usePartyStore.getState().members.every((member) => member.health > 0)).toBe(true);
} finally {
stop();
}
});
it("updates loaded enemies on level-up while retaining wounds and fallen enemies", () => {
const initial = dungeonEnemyHealthBaseline(1, "rom");
const scaled = dungeonEnemyHealthBaseline(8, "rom");
initializePull(1, "rom-health-level-up", initial);
useCombatStore.getState().damageMob("pull", initial / 2 - 1, 1_000);
useCombatStore.getState().registerMob("pull", { name: trash.name, level: 8, maxHealth: scaled });
expect(useCombatStore.getState().mobs.pull).toMatchObject({ level: 8, maxHealth: scaled, health: scaled / 2,
attackDamage: 20 });
useCombatStore.getState().registerMob("pull", { name: trash.name, level: 8, maxHealth: scaled });
expect(useCombatStore.getState().mobs.pull.health).toBe(scaled / 2);
useCombatStore.getState().damageMob("pull", scaled, 2_000);
useCombatStore.getState().registerMob("pull", { name: trash.name, level: 20,
maxHealth: dungeonEnemyHealthBaseline(20, "rom") });
expect(useCombatStore.getState().mobs.pull).toMatchObject({ dead: true, health: 0 });
});
});