96 lines
6.8 KiB
TypeScript
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(); }
|
|
});
|