Release Healer Man 0.1.40

This commit is contained in:
phenom committed 2026-09-13 13:21:27 -04:00
1 parent ed0d2fea3d
commit 197a64117c
215 files changed
+5131 -181

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
import { afterAll, describe, expect, it } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import { dungeonEnemyScaling } from "./dungeonBossScaling";
import { requireDungeonDefinition } from "./dungeonRegistry";
import { finalDungeonBoss } from "./dungeonRunStore";
import { useCombatStore } from "./combatStore";
import { usePartyStore } from "./partyStore";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry";
import { advanceDungeonPartyCombat, partyCombatRangeBand, resetPartyRuntimeClock } from "./partyRuntime";
import { subscribeCombatEvents } from "./combatEvents";
import { bossDifficultyMultipliers } from "./dungeonDifficulty";
import { abilitiesForClass } from "./abilityCatalog";
const dungeon = requireDungeonDefinition("ragefire-chasm");
const final = finalDungeonBoss(dungeon)!;
const samples: { level: number; seed: string; boss: unknown; final: unknown }[] = [];
afterAll(() => {
if (process.env.BOSS_BALANCE_REPORT !== "1") return;
mkdirSync("artifacts/boss-difficulty", { recursive: true });
writeFileSync("artifacts/boss-difficulty/ragefire-balance.json", `${JSON.stringify({ samples }, null, 2)}\n`);
});
function fight(bossId: string, level: number, difficulty: number, seed: string, mechanics = false, playerDamage = false) {
clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock();
const entity = dungeon.entities[dungeon.staticSpawns.find((spawn) => spawn.id === bossId)!.entityId];
const scaling = dungeonEnemyScaling(dungeon, entity, bossId, level, "normal", difficulty);
const combat = useCombatStore.getState();
const classId = playerDamage ? "rom-mage" : "rom-priest";
combat.initializeCharacter({ classId, raceId: "rom-human", level });
combat.setPlayerPosition([-18, 0, 0]);
usePartyStore.getState().initializeParty(seed, level, useCombatStore.getState().health, playerDamage ? "damage" : "healer", 5, "rom");
combat.synchronizeActors();
combat.registerMob(bossId, { name: entity.name, boss: true, serverEntry: entity.combat?.serverEntry,
...scaling, attacks: entity.combat?.attacks, xpReward: 0 });
setMobPosition(bossId, [0, 0, 0]);
for (const member of usePartyStore.getState().members) {
registerPartyRuntimePosition(member.id, [-partyCombatRangeBand(member).preferred, 0, 0]);
updatePartyRuntimeTargetVisibility(member.id, bossId, true);
}
let firstDamage: number | undefined;
let incomingDamage = 0; let healing = 0;
const heal = abilitiesForClass("rom-priest").find((ability) => ability.name === "Urgent Heal")!;
const damage = abilitiesForClass(classId).find((ability) => ability.target === "hostile"
&& ability.effects.some((effect) => effect.kind === "damage"))!;
combat.selectMob(bossId);
const stop = subscribeCombatEvents((event) => {
if (event.kind === "damage" && event.targetActorId === bossId && event.effectiveAmount > 0) firstDamage ??= event.occurredAt;
if (event.kind === "damage" && event.sourceActorId === bossId) incomingDamage += event.effectiveAmount;
if (event.kind === "healing") healing += event.effectiveAmount;
});
combat.damageMob(bossId, 1, 1000);
try {
for (let now = 1100; now <= 241000; now += 100) {
combat.tick(.1, now); advanceDungeonPartyCombat(now);
if (playerDamage && !useCombatStore.getState().activeCast) combat.castAbility(damage.id, undefined, now);
if (mechanics) {
combat.advanceMobCombat(now);
const injured = usePartyStore.getState().members.filter((member) => member.health > 0)
.sort((left, right) => left.health / left.maxHealth - right.health / right.maxHealth)[0];
if (injured && injured.health < injured.maxHealth * .8 && !useCombatStore.getState().activeCast) {
usePartyStore.getState().selectMember(injured.id); combat.castAbility(heal.id, undefined, now);
}
}
const mob = useCombatStore.getState().mobs[bossId];
if (mob.dead) return { seconds: ((mob.diedAt ?? now) - firstDamage!) / 1000,
incomingDamage, healing, attacks: mob.attackRevision,
maxHealth: mob.maxHealth, classes: usePartyStore.getState().members.map((member) => member.classId) };
}
return { seconds: null, maxHealth: scaling.maxHealth };
} finally { stop(); }
}
describe("dungeon boss durability and difficulty", () => {
it("increases boss health and damage at every slider step without changing trash", () => {
const spawn = dungeon.staticSpawns.find((entry) => entry.id === final.id)!;
const entity = dungeon.entities[spawn.entityId];
const trash = dungeon.staticSpawns.find((entry) => dungeon.entities[entry.entityId].kind === "mob")!;
const easy = dungeonEnemyScaling(dungeon, entity, final.id, 8, "normal", 1);
for (let difficulty = 2; difficulty <= 10; difficulty += 1) {
const scaled = dungeonEnemyScaling(dungeon, entity, final.id, 8, "normal", difficulty);
expect(scaled.maxHealth).toBe(easy.maxHealth * bossDifficultyMultipliers(difficulty).health);
expect(scaled.damageMultiplier).toBe(easy.damageMultiplier * bossDifficultyMultipliers(difficulty).damage);
expect(dungeonEnemyScaling(dungeon, dungeon.entities[trash.entityId], trash.id, 8, "normal", difficulty))
.toEqual(dungeonEnemyScaling(dungeon, dungeon.entities[trash.entityId], trash.id, 8, "normal", 1));
}
});
it("allows real boss attacks and Priest healing before Ragefire's final boss dies", () => {
const result = fight(final.id, 8, 1, "rom-health-1", true);
expect(result.seconds).not.toBeNull(); expect(result.seconds!).toBeGreaterThanOrEqual(45);
expect(result.attacks).toBeGreaterThan(5); expect(result.incomingDamage).toBeGreaterThan(0);
expect(result.healing).toBeGreaterThan(0);
expect(usePartyStore.getState().members.every((member) => member.health > 0)).toBe(true);
});
it("retains encounter time with a damage-role Mage contributing to the RoM party", () => {
for (const level of [1, 8, 20, 40, 80]) {
const ordinary = dungeon.bosses.find((boss) => boss.id !== final.id)!;
const boss = fight(ordinary.id, level, 1, "rom-health-1", false, true);
const last = fight(final.id, level, 1, "rom-health-1", false, true);
samples.push({ level, seed: "player-mage", boss, final: last });
expect(boss.seconds).not.toBeNull(); expect(last.seconds).not.toBeNull();
expect(boss.seconds!, JSON.stringify({ level, boss, last })).toBeGreaterThanOrEqual(25);
expect(last.seconds!, JSON.stringify({ level, boss, last })).toBeGreaterThanOrEqual(45);
}
});
for (const level of [1, 8, 20, 40, 80]) {
it(`gives a RoM party time to play Ragefire boss mechanics at level ${level}`, () => {
const ordinary = dungeon.bosses.find((boss) => boss.id !== final.id)!;
for (let index = 0; index < 12; index += 1) {
const seed = `rom-health-${index}`;
const boss = fight(ordinary.id, level, 1, seed);
const last = fight(final.id, level, 1, seed);
samples.push({ level, seed, boss, final: last });
expect(boss.seconds).not.toBeNull(); expect(last.seconds).not.toBeNull();
expect(boss.seconds!).toBeGreaterThanOrEqual(25);
expect(last.seconds!).toBeGreaterThanOrEqual(45);
}
}, 30000);
}
});