Files
healer-man/src/game/dungeonRunCombatIntegration.test.ts
T
2026-09-13 11:07:56 -04:00

96 lines
6.8 KiB
TypeScript

import { mkdirSync, writeFileSync } from "node:fs";
import { expect, it } from "vitest";
import { createCharacter } from "../app/accountRepository";
import { randomAppearance } from "../app/characterCatalog";
import { abilitiesForClass } from "./abilityCatalog";
import { useCombatStore } from "./combatStore";
import { subscribeCombatEvents } from "./combatEvents";
import { deriveDungeonEncounterCombatEvents, type DungeonEncounterMobCursor } from "./dungeonEncounterCombatEvents";
import { activateDungeonEncounterDefinitions, activeDungeonCompletedObjectiveIds, activeDungeonRunIsComplete,
advanceDungeonEncounters, dispatchActiveDungeonEntityEvent, exportDungeonEncounterCheckpointSnapshots, resetDungeonEncounters } from "./dungeonEncounterRuntime";
import { requireDungeonDefinition } from "./dungeonRegistry";
import { dungeonEnemyHealthBaseline, scaledDungeonEnemyLevel } from "./dungeonLevelScaling";
import { completedDungeonRun, finalDungeonBoss, useDungeonRunStore } from "./dungeonRunStore";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { usePartyStore } from "./partyStore";
import { advanceDungeonPartyCombat, partyCombatRangeBand, resetPartyRuntimeClock } from "./partyRuntime";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry";
import { useGameStore } from "./store";
it("turns an actual RoM party's lethal damage to Anglo into one timed completion and a resumable result", () => {
const dungeon = requireDungeonDefinition("barren-caves");
const boss = finalDungeonBoss(dungeon)!;
const spawn = dungeon.staticSpawns.find((entry) => entry.id === boss.id)!;
const entity = dungeon.entities[spawn.entityId];
const bossCombat = entity.combat;
if (!bossCombat) throw new Error("Anglo must have a combat definition");
const healthMultiplier = bossCombat.healthMultiplier ?? 1;
clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock();
useGameStore.setState({ activeDungeonId: dungeon.id, activeDifficultyId: "normal", gameMode: "dungeon", gmTestMode: false });
activateDungeonEncounterDefinitions(dungeon.id, dungeon.encounters ?? [], "normal");
resetDungeonEncounters("dungeon", dungeon.id);
const combat = useCombatStore.getState();
combat.initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 1 });
combat.setPlayerPosition([-18, 0, 0]);
usePartyStore.getState().initializeParty("completion-run", 1, useCombatStore.getState().health, "healer", 5, "rom");
combat.synchronizeActors();
const bossLevel = scaledDungeonEnemyLevel(1, true);
combat.registerMob(boss.id, { name: entity.name, boss: true, level: bossLevel, xpReward: 0,
maxHealth: dungeonEnemyHealthBaseline(bossLevel, "runewaker")! * healthMultiplier,
healthMultiplier, attacks: bossCombat.attacks });
setMobPosition(boss.id, [0, 0, 0]);
for (const member of usePartyStore.getState().members) {
registerPartyRuntimePosition(member.id, [-partyCombatRangeBand(member).preferred, 0, 0]);
updatePartyRuntimeTargetVisibility(member.id, boss.id, true);
}
const party = () => [{ name: "Runview", classId: "rom-priest", level: 1, role: "healer", npc: false },
...usePartyStore.getState().members.map((member) => ({ name: member.name, classId: member.classId,
level: member.level, role: member.role, npc: true }))];
const epoch = Date.now() - 181000;
const run = useDungeonRunStore.getState(); run.begin(dungeon.id, "normal"); run.start(epoch, party());
combat.damageMob(boss.id, 1, epoch);
const heal = abilitiesForClass("rom-priest").find((ability) => ability.name === "Urgent Heal")!;
let cursor: DungeonEncounterMobCursor = {};
let npcDamage = 0; let healing = 0; let deaths = 0;
const stop = subscribeCombatEvents((event) => {
if (event.kind === "damage" && event.targetActorId === boss.id && event.sourceActorId !== "player") npcDamage += event.effectiveAmount;
if (event.kind === "healing") healing += event.effectiveAmount;
});
try {
for (let now = epoch + 100; now <= epoch + 181000; now += 100) {
combat.tick(.1, now); advanceDungeonPartyCombat(now); combat.advanceMobCombat(now);
const tank = usePartyStore.getState().members.find((member) => member.role === "tank")!;
if (tank.health < tank.maxHealth * .65 && !useCombatStore.getState().activeCast) {
usePartyStore.getState().selectMember(tank.id); combat.castAbility(heal.id, undefined, now);
}
const mobs = useCombatStore.getState().mobs;
const events = deriveDungeonEncounterCombatEvents(cursor, mobs, now); cursor = events.cursor;
for (const event of events.events) { dispatchActiveDungeonEntityEvent(event); if (event.kind === "entity-death") deaths += 1; }
const dead = Object.values(mobs).filter((mob) => mob.dead).map((mob) => mob.id);
advanceDungeonEncounters(dungeon.id, { kind: "tick", now, facts: { registeredEntityIds: Object.keys(mobs), deadEntityIds: dead } });
run.advance(activeDungeonCompletedObjectiveIds(dungeon.id), dead, now, party());
if (useDungeonRunStore.getState().progress?.finishedAt !== null) break;
}
const progress = useDungeonRunStore.getState().progress!;
expect(useCombatStore.getState().mobs[boss.id].dead).toBe(true);
expect(activeDungeonRunIsComplete(dungeon.id)).toBe(true);
expect(npcDamage).toBeGreaterThan(0); expect(healing).toBeGreaterThan(0); expect(deaths).toBe(1);
expect(completedDungeonRun(progress)).toMatchObject({ fullClear: true, killedBossIds: [boss.id] });
expect(progress.durationMs).toBeGreaterThan(20000);
expect(usePartyStore.getState().members.every((member) => member.health > 0)).toBe(true);
const snapshots = exportDungeonEncounterCheckpointSnapshots(dungeon.id, 0);
if (process.env.DUNGEON_RUN_REPORT === "1") {
const values = new Map<string, string>();
const character = createCharacter({ ownerId: "qa-owner", name: "Runview", raceId: "rom-human", classId: "rom-priest",
categoryId: "rom", gender: "female", appearance: randomAppearance("rom-human", "female", () => 0) },
{ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); },
removeItem: (key) => { values.delete(key); } }).character!;
mkdirSync("artifacts/dungeon-runs", { recursive: true });
writeFileSync("artifacts/dungeon-runs/combat-completion.json", `${JSON.stringify({ npcDamage, healing, deaths, progress,
character: { ...character, dungeonRunCheckpoint: { version: 2, dungeonId: dungeon.id, difficultyId: "normal",
checkpointId: "entrance", spawn: { footPosition: dungeon.entrance.footPosition, yaw: dungeon.entrance.yaw },
encounters: snapshots, savedAt: Date.now(), run: progress } } }, null, 2)}\n`);
}
} finally { stop(); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); }
});