import { afterAll, describe, expect, it } from "vitest"; import { mkdirSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore } from "./combatStore"; import { subscribeCombatEvents } from "./combatEvents"; import { subscribeCombatPresentation } from "./combatPresentation"; import { useGameStore } from "./store"; import { usePartyStore, createConfiguredPartyMember } from "./partyStore"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry"; import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities"; import { requireDungeonDefinition } from "./dungeonRegistry"; import { dungeonEnemyScaling } from "./dungeonBossScaling"; import { BenchmarkController } from "./benchmark/benchmarkController"; import { BENCHMARK_SCENARIOS } from "./benchmark/benchmarkScenarios"; import type { EnemyAttackDefinition } from "./enemyAttacks"; const dungeon = requireDungeonDefinition("barren-caves"); const bossEntity = dungeon.entities["rw-barren-caves-101462"]; const bossSpawn = dungeon.staticSpawns.find((spawn) => spawn.entityId === bossEntity.id)!; const BOSS_ID = bossSpawn.id; const priest = ROM_FIVE_IDENTITIES.find((identity) => identity.classId === "rom-priest")!; const report: unknown[] = []; afterAll(() => { if (process.env.ROM_NATIVE_PRESSURE_REPORT !== "1") return; const directory = resolve("artifacts/rom-enemy-followup"); mkdirSync(directory, { recursive: true }); writeFileSync(resolve(directory, "pressure.json"), `${JSON.stringify({ scope: "Explicit native five / Barren Caves Anglo diagnostic, levels 10/30/60/80. Scripted positions, normal HP budgets; no scene navigation or readiness certification.", samples: report, }, null, 2)}\n`); }); function setup(level = 30, stacked = false, attacks?: readonly EnemyAttackDefinition[], activeIdentity = priest) { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useGameStore.getState().activateDungeon("barren-caves", "normal", { bossDifficulty: 1 }); const combat = useCombatStore.getState(); combat.initializeCharacter({ ...activeIdentity, level, statRuleset: "rom" }); combat.setPlayerPosition([stacked || activeIdentity.role === "tank" ? 2 : 10, 0, 0]); usePartyStore.getState().initializeParty("native-pressure", level, useCombatStore.getState().health, activeIdentity.role, 5, "rom"); const members = ROM_FIVE_IDENTITIES.filter((identity) => identity !== activeIdentity).map((identity, index) => createConfiguredPartyMember({ ...identity, statRuleset: "rom", specialization: "Primary", talentTreeId: "" }, level, index, "native-pressure")); usePartyStore.setState({ members, command: "defend" }); combat.synchronizeActors(); const scaling = dungeonEnemyScaling(dungeon, bossEntity, BOSS_ID, level, "normal", 1, "rom", "rom"); combat.registerMob(BOSS_ID, { name: "Anglo", boss: true, runewakerNpcId: 101462, ...scaling, xpReward: 0, attacks: attacks ?? bossEntity.combat?.attacks }); setMobPosition(BOSS_ID, [0, 0, 0]); for (const member of members) { const distance = stacked || member.classId === "rom-knight" || member.classId === "rom-rogue" ? 2 : member.classId === "rom-scout" ? 8 : 10; registerPartyRuntimePosition(member.id, [distance, 0, 0]); updatePartyRuntimeTargetVisibility(member.id, BOSS_ID, true); } const tank = members.find((member) => member.role === "tank") ?? { id: PLAYER_AGGRO_ID, maxHealth: useCombatStore.getState().maxHealth }; combat.damageMob(BOSS_ID, 1, 1_000, { actorId: tank.id, role: "tank" }); combat.selectMob(BOSS_ID); return { tank, members }; } function measurePressure(level: number, stacked: boolean, activeHealer: boolean) { const { tank } = setup(level, stacked); const controller = new BenchmarkController(); let incoming = 0; let rawIncoming = 0; let absorbed = 0; let effectiveHealing = 0; let overheal = 0; let healerCasts = 0; let invalid = 0; let resourceBlocked = 0; let tankLow = 1; let partyLow = 1; let manaLow = 1; let endAt = 1_000; const outgoingByAbility: Record = {}; const castsByAbility: Record = {}; const controls = new Set(); const otherDamage: Record = {}; const invalidDetails: unknown[] = []; const castAbility = useCombatStore.getState().castAbility; useCombatStore.setState({ castAbility: (...args) => { const result = castAbility(...args); if (!result.ok && result.reason) invalidDetails.push({ abilityId: result.abilityId, reason: result.reason, duringCompletion: Boolean(args[3]) }); return result; } }); const stopCasts = subscribeCombatPresentation((message) => { if (message.kind !== "event" || message.event.sourceActorId !== PLAYER_AGGRO_ID || message.event.phase !== "release") return; const id = `${message.event.abilityId}:${message.event.name}`; castsByAbility[id] = (castsByAbility[id] ?? 0) + 1; }); const stop = subscribeCombatEvents((event) => { if (event.sourceActorId === BOSS_ID && event.kind === "damage") { incoming += event.effectiveAmount; rawIncoming += event.rawAmount; absorbed += event.absorbed; } if (event.sourceActorId === PLAYER_AGGRO_ID && event.kind === "healing") { effectiveHealing += event.effectiveAmount; overheal += event.overheal; const id = event.abilityId ?? "unknown"; outgoingByAbility[id] = (outgoingByAbility[id] ?? 0) + event.effectiveAmount; } if (event.kind === "damage" && event.sourceActorId !== BOSS_ID && event.targetActorId !== BOSS_ID) { const id = `${event.sourceActorId}:${event.abilityId}`; otherDamage[id] = (otherDamage[id] ?? 0) + event.effectiveAmount; } }); try { for (let now = 1_100; now <= 121_000; now += 100) { endAt = now; const combat = useCombatStore.getState(); combat.tick(0.1, now); combat.advanceMobCombat(now); advanceDungeonPartyCombat(now); if (activeHealer && !useCombatStore.getState().mobs[BOSS_ID].dead && useCombatStore.getState().health > 0) { const decision = controller.act(now, "healer", { ...BENCHMARK_SCENARIOS[0], hostileTargets: 1 }, false); healerCasts += Number(decision.cast); invalid += decision.invalidFailures; resourceBlocked += Number(decision.resourceBlocked); } const state = useCombatStore.getState(); const members = usePartyStore.getState().members; tankLow = Math.min(tankLow, state.actors[tank.id]?.health! / tank.maxHealth); partyLow = Math.min(partyLow, state.health / state.maxHealth, ...members.map((member) => member.health / member.maxHealth)); manaLow = Math.min(manaLow, (state.resourcePools.mana ?? 0) / (state.maxResourcePools.mana ?? 1)); for (const aura of state.auras) if (aura.sourceId === BOSS_ID && aura.definition.id.startsWith("enemy-control:")) { controls.add(aura.targetId); } if (state.mobs[BOSS_ID].dead || state.health <= 0) break; } } finally { stop(); stopCasts(); useCombatStore.setState({ castAbility }); } const state = useCombatStore.getState(); const result = { level, stacked, activeHealer, seconds: (endAt - 1_000) / 1_000, clear: state.mobs[BOSS_ID].dead, damage: incoming, rawIncoming, absorbed, effectiveHealing, overheal, playerMaxHealth: state.maxHealth, tankMaxHealth: state.actors[tank.id].maxHealth, healerCasts, resourceBlocked, invalid, tankLow, partyLow, manaLow, finalMana: state.resourcePools.mana, maxMana: state.maxResourcePools.mana, playerDead: state.health <= 0, deadCompanions: usePartyStore.getState().members.filter((member) => member.health <= 0).map((member) => member.classId), mechanics: state.mobs[BOSS_ID].abilityCastCounts, controlledActors: [...controls], outgoingByAbility, castsByAbility, otherDamage, invalidDetails }; report.push(result); return result; } describe("native RoM encounter and healing pressure", () => { it.each(["Trample", "Bash"])("resolves %s against current positions after its warning", (name) => { const attack = bossEntity.combat!.attacks.find((entry) => entry.name === name)!; const { members } = setup(30, true, [{ ...attack, initialCooldownMs: [0, 0] }]); const releases: string[] = []; const stopPresentation = subscribeCombatPresentation((message) => { if (message.kind === "event" && message.event.sourceActorId === BOSS_ID) releases.push(message.event.phase); }); useCombatStore.getState().advanceMobCombat(1_100); expect(useCombatStore.getState().mobs[BOSS_ID].activeCast?.name).toBe(name); useCombatStore.getState().setPlayerPosition([8, 0, 0]); const mage = members.find((member) => member.classId === "rom-mage")!; updatePartyRuntimePosition(mage.id, 12, 0, 0); useCombatStore.getState().advanceMobCombat(2_899); expect(useCombatStore.getState().combatEvents.some((event) => event.abilityId === attack.id)).toBe(false); useCombatStore.getState().advanceMobCombat(2_900); stopPresentation(); const hits = useCombatStore.getState().combatEvents.filter((event) => event.kind === "damage" && event.abilityId === attack.id); expect(new Set(hits.map((event) => event.targetActorId))).toEqual(new Set(members.filter((member) => member.id !== mage.id).map((member) => member.id))); expect(hits.every((event) => event.school === "physical")).toBe(true); expect(releases).toContain("cast-start"); expect(releases).toContain("release"); }); it("lets dispelling Gulp restore the controlled actor before natural expiry", () => { const attack = bossEntity.combat!.attacks.find((entry) => entry.name === "Gulp")!; const { tank } = setup(30, true, [{ ...attack, initialCooldownMs: [0, 0] }]); useCombatStore.getState().advanceMobCombat(1_100); useCombatStore.getState().advanceMobCombat(2_900); expect(useCombatStore.getState().controlMechanic).toBe("stun"); expect(useCombatStore.getState().actors[tank.id].controlledUntil).toBeGreaterThan(2_900); expect(useCombatStore.getState().dispelActor(PLAYER_AGGRO_ID, ["magic"], 1, 3_000)).toBe(1); expect(useCombatStore.getState().controlledUntil).toBeLessThanOrEqual(3_000); expect(useCombatStore.getState().dispelActor(tank.id, ["magic"], 1, 3_000)).toBe(1); expect(useCombatStore.getState().actors[tank.id].controlledUntil).toBeLessThanOrEqual(3_000); }); it("lets a native companion counter an interruptible boss warning", () => { const attack = bossEntity.combat!.attacks.find((entry) => entry.name === "Trample")!; setup(30, true, [{ ...attack, initialCooldownMs: [0, 0] }]); useCombatStore.getState().advanceMobCombat(1_100); expect(useCombatStore.getState().mobs[BOSS_ID].activeCast).not.toBeNull(); const counters: string[] = []; const stop = subscribeCombatPresentation((message) => { if (message.kind === "event" && message.event.sourceActorId === BOSS_ID) counters.push(message.event.phase); }); for (let now = 1_200; now < 2_900; now += 100) { useCombatStore.getState().tick(0.1, now); advanceDungeonPartyCombat(now); } const boss = useCombatStore.getState().mobs[BOSS_ID]; stop(); expect(boss.activeCast).toBeNull(); expect(counters).toContain("cast-cancel"); expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "damage" && event.abilityId === attack.id)).toBe(false); }); it("lets a ranged native Priest companion dispel Gulp from the player tank", () => { const attack = bossEntity.combat!.attacks.find((entry) => entry.name === "Gulp")!; const knight = ROM_FIVE_IDENTITIES.find((identity) => identity.classId === "rom-knight")!; const { members } = setup(30, false, [{ ...attack, initialCooldownMs: [0, 0] }], knight); const healer = members.find((member) => member.classId === "rom-priest")!; useCombatStore.getState().advanceMobCombat(1_100); useCombatStore.getState().advanceMobCombat(2_900); expect(useCombatStore.getState().controlledUntil).toBe(5_900); expect(useCombatStore.getState().actors[healer.id].controlledUntil).toBeLessThan(2_900); for (let now = 3_000; now < 5_900 && useCombatStore.getState().controlledUntil > now; now += 100) { useCombatStore.getState().tick(0.1, now); advanceDungeonPartyCombat(now); } expect(useCombatStore.getState().controlledUntil).toBe(0); expect(useCombatStore.getState().controlMechanic).toBeNull(); }); it("records active-healer and idle-healer pressure in the explicit native cohort", () => { for (const level of [10, 30, 60, 80]) for (const stacked of [false, true]) { const active = measurePressure(level, stacked, true); const idle = measurePressure(level, stacked, false); // Record failures before asserting the cohort, retaining every level in // the pressure report when a new demand pattern exposes a policy gap. expect(active.damage).toBeGreaterThan(0); expect(active.healerCasts).toBeGreaterThan(0); // A safe pull may permit the healer to deal damage throughout; require // healing across the cohort rather than inventing demand in every pull. expect(active.effectiveHealing).toBeGreaterThanOrEqual(0); expect(idle.healerCasts).toBe(0); expect(idle.effectiveHealing).toBe(0); expect(Number.isFinite(active.manaLow)).toBe(true); expect(Number.isFinite(active.partyLow)).toBe(true); } expect(report.some((sample) => (sample as { effectiveHealing: number }).effectiveHealing > 0)).toBe(true); expect(report.filter((sample) => (sample as { activeHealer: boolean }).activeHealer) .map((sample) => (sample as { invalid: number }).invalid)).toEqual(Array(8).fill(0)); }, 180_000); });