Release Healer Man 0.1.39
This commit is contained in:
1 parent
b417b7c3c3
commit
ed0d2fea3d
51 files changed
+17811
-49
No files matched your search
@@ -0,0 +1,95 @@
|
||||
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(); }
|
||||
});
|
||||
Reference in new issue
Block a user