forked from phenom/healer-man
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,151 @@
|
||||
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, "runewaker")!);
|
||||
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), "runewaker")!
|
||||
* 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, "runewaker")!);
|
||||
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, "runewaker")!;
|
||||
const scaled = dungeonEnemyHealthBaseline(8, "runewaker")!;
|
||||
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, "runewaker") });
|
||||
expect(useCombatStore.getState().mobs.pull).toMatchObject({ dead: true, health: 0 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user