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 }); }); });