forked from phenom/healer-man
143 lines
8.7 KiB
TypeScript
143 lines
8.7 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { buildBossTestingCatalog, BOSS_TESTING_CATALOG, createBossTestingRoom, randomTestingBoss } from "./bossTesting";
|
|
import { clearBossTestingDungeons, DUNGEON_DEFINITIONS, registerBossTestingDungeon, requireDungeonDefinition } from "./dungeonRegistry";
|
|
import { activateDungeonSession, restartActiveDungeonSession } from "./dungeonSession";
|
|
import { useGameStore } from "./store";
|
|
import { useCombatStore } from "./combatStore";
|
|
import { bossTestIsDefeated } from "./BossTestingBridge";
|
|
import type { DungeonDefinition } from "./dungeonTypes";
|
|
import { dungeonEnemyScaling } from "./dungeonBossScaling";
|
|
import { creatureRuntimeBaseStats } from "./azerothCoreReference";
|
|
import { bossDifficultyMultipliers } from "./dungeonDifficulty";
|
|
import { finalDungeonBoss } from "./dungeonRunStore";
|
|
|
|
afterEach(() => {
|
|
useGameStore.getState().activateDungeon("wailing-caverns");
|
|
clearBossTestingDungeons();
|
|
});
|
|
|
|
describe("Boss Testing", () => {
|
|
const boss = BOSS_TESTING_CATALOG.find((entry) => entry.dungeonId === "wailing-caverns" && entry.name === "Lady Anacondra")!;
|
|
const source = requireDungeonDefinition(boss.dungeonId);
|
|
|
|
it("includes unready and event-only bosses and isolates catalog changes from shipped content", () => {
|
|
const blocked: DungeonDefinition = { ...source, availability: "blocked", staticSpawns: [], roamingPacks: [],
|
|
encounterSpawnGroups: [{ id: "event", spawns: [boss.spawn] }], entities: { [boss.entityId]: source.entities[boss.entityId] } };
|
|
expect(buildBossTestingCatalog([blocked])).toHaveLength(1);
|
|
const count = DUNGEON_DEFINITIONS.length;
|
|
const room = createBossTestingRoom(source, boss);
|
|
registerBossTestingDungeon(room.definition, source.id);
|
|
expect(requireDungeonDefinition(source.id)).toBe(source);
|
|
expect(DUNGEON_DEFINITIONS).toHaveLength(count);
|
|
expect(room.definition.staticSpawns).toHaveLength(1);
|
|
expect(room.definition.roamingPacks).toEqual([]);
|
|
expect(room.definition.entities[boss.entityId].combat).toEqual(source.entities[boss.entityId].combat);
|
|
expect(room.definition.staticSpawns[0].position).toEqual(boss.spawn.position);
|
|
expect(room.definition.assets).toEqual({ visual: [], collision: [] });
|
|
});
|
|
|
|
it("preserves boss phases and scripted summons without spawning unrelated dungeon packs", () => {
|
|
const fixture: DungeonDefinition = { ...source, encounters: [
|
|
{ id: "boss-mechanics", dungeonId: source.id, initialPhase: "idle", rules: [{ id: "adds", trigger: { kind: "entity-health", entityId: boss.spawn.id, atOrBelowPercent: 50 },
|
|
actions: [{ id: "summon", kind: "activate-spawn-group", spawnGroupId: "boss-adds" }, { id: "loot", kind: "grant-loot", lootTableId: "test" }] }] },
|
|
{ id: "other-boss", dungeonId: source.id, initialPhase: "idle", rules: [{ id: "other", trigger: { kind: "entity-engaged", entityId: "unrelated" }, actions: [] }] },
|
|
], encounterSpawnGroups: [{ id: "boss-adds", spawns: [{ ...boss.spawn, id: "add", entityId: boss.entityId }] }, { id: "trash", spawns: [] }] };
|
|
const room = createBossTestingRoom(fixture, boss);
|
|
expect(room.definition.encounters?.map((entry) => entry.id)).toEqual([`${room.definition.id}:boss-mechanics`]);
|
|
expect(room.definition.encounters?.[0].dungeonId).toBe(room.definition.id);
|
|
expect(room.definition.encounters?.[0].rules[0].actions).toEqual([{ id: "summon", kind: "activate-spawn-group", spawnGroupId: "boss-adds" }]);
|
|
expect(room.definition.encounterSpawnGroups?.map((entry) => entry.id)).toEqual(["boss-adds"]);
|
|
expect(room.definition.staticSpawns).toHaveLength(1);
|
|
});
|
|
|
|
it("starts an unready boss room, survives scene retries, and retains testing mode when restarted", () => {
|
|
const room = createBossTestingRoom(source, boss);
|
|
registerBossTestingDungeon(room.definition, source.id);
|
|
expect(activateDungeonSession(room.definition.id)).toBe(false);
|
|
expect(activateDungeonSession(room.definition.id, undefined, { gmTestMode: true, bossTest: room.session })).toBe(true);
|
|
expect(restartActiveDungeonSession()).toBe(true);
|
|
useGameStore.getState().resetAtEntrance();
|
|
expect(useGameStore.getState()).toMatchObject({ gmTestMode: true, bossTest: room.session, overlay: null });
|
|
});
|
|
|
|
it("uses Celebras's native health at the player's scaled level without campaign health floors", () => {
|
|
const celebras = BOSS_TESTING_CATALOG.find((entry) => entry.name === "Celebras the Cursed")!;
|
|
const maraudon = requireDungeonDefinition(celebras.dungeonId);
|
|
const room = createBossTestingRoom(maraudon, celebras);
|
|
const entity = room.definition.entities[celebras.entityId];
|
|
const scale = (level: number, difficulty = 1) => dungeonEnemyScaling(room.definition, entity,
|
|
celebras.spawn.id, level, room.definition.defaultDifficultyId, difficulty);
|
|
const nativeHealth = creatureRuntimeBaseStats(entity.combat!.serverEntry!, 2)!.health
|
|
* entity.combat!.healthMultiplier!;
|
|
expect(scale(1)).toMatchObject({ level: 2, maxHealth: nativeHealth });
|
|
expect(Math.round(scale(1).maxHealth)).toBe(575);
|
|
expect(scale(8).maxHealth).toBeGreaterThan(scale(1).maxHealth);
|
|
expect(scale(20).maxHealth).toBeGreaterThan(scale(8).maxHealth);
|
|
expect(scale(1, 10).maxHealth).toBe(nativeHealth * bossDifficultyMultipliers(10).health);
|
|
const campaign = dungeonEnemyScaling(maraudon, maraudon.entities[celebras.entityId], celebras.spawn.id,
|
|
1, maraudon.defaultDifficultyId, 1);
|
|
expect(campaign.maxHealth).toBe(900);
|
|
expect(scale(1).damageMultiplier).toBe(campaign.damageMultiplier);
|
|
expect(entity.combat?.attacks).toEqual(maraudon.entities[celebras.entityId].combat?.attacks);
|
|
});
|
|
|
|
it("keeps low extrapolated boss health alive long enough to test mechanics", () => {
|
|
const zereketh = BOSS_TESTING_CATALOG.find((entry) => entry.name === "Zereketh the Unbound")!;
|
|
const arcatraz = requireDungeonDefinition(zereketh.dungeonId);
|
|
const room = createBossTestingRoom(arcatraz, zereketh);
|
|
const entity = room.definition.entities[zereketh.entityId];
|
|
const nativeHealth = creatureRuntimeBaseStats(entity.combat!.serverEntry!, 2)!.health
|
|
* entity.combat!.healthMultiplier!;
|
|
const testing = dungeonEnemyScaling(
|
|
room.definition,
|
|
entity,
|
|
zereketh.spawn.id,
|
|
1,
|
|
room.definition.defaultDifficultyId,
|
|
1,
|
|
);
|
|
|
|
expect(nativeHealth).toBeLessThan(100);
|
|
expect(testing).toMatchObject({ level: 2, maxHealth: 540 });
|
|
});
|
|
|
|
it("omits the campaign finale bonus even when testing the source dungeon's final boss", () => {
|
|
const final = finalDungeonBoss(source)!;
|
|
const finalBoss = BOSS_TESTING_CATALOG.find((entry) => entry.dungeonId === source.id && entry.spawn.id === final.id)!;
|
|
const room = createBossTestingRoom(source, finalBoss);
|
|
const entity = room.definition.entities[finalBoss.entityId];
|
|
const testing = dungeonEnemyScaling(room.definition, entity, finalBoss.spawn.id, 1, source.defaultDifficultyId, 1);
|
|
const campaign = dungeonEnemyScaling(source, entity, finalBoss.spawn.id, 1, source.defaultDifficultyId, 1);
|
|
expect(testing.maxHealth).toBe(Math.max(
|
|
creatureRuntimeBaseStats(entity.combat!.serverEntry!, 2)!.health
|
|
* (entity.combat?.healthMultiplier ?? 1),
|
|
540,
|
|
));
|
|
expect(campaign.maxHealth).toBe(Math.max(testing.maxHealth, 900 * 1.8));
|
|
});
|
|
|
|
it("recognizes actual boss deaths and awards no progression or collectible rewards", () => {
|
|
const room = createBossTestingRoom(source, boss);
|
|
registerBossTestingDungeon(room.definition, source.id);
|
|
useGameStore.getState().activateDungeon(room.definition.id, undefined, { bossTest: room.session, gmTestMode: true });
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20 });
|
|
expect(bossTestIsDefeated()).toBe(false);
|
|
useCombatStore.getState().registerMob(boss.spawn.id, { name: boss.name, boss: true, maxHealth: 10, xpReward: 500, hasLoot: true, lootSourceId: boss.entityId });
|
|
const xp = useCombatStore.getState().xp;
|
|
useCombatStore.getState().damageMob(boss.spawn.id, 100000, Date.now());
|
|
expect(bossTestIsDefeated()).toBe(true);
|
|
const combat = useCombatStore.getState();
|
|
expect(combat.xp).toBe(xp);
|
|
expect(combat.mobs[boss.spawn.id]).toMatchObject({ hasLoot: false, pendingLoot: [] });
|
|
expect(combat.mobs[boss.spawn.id].bossCardDrop).toBeFalsy();
|
|
});
|
|
|
|
it("draws a boss only from the selected filters and handles empty queues", () => {
|
|
expect(randomTestingBoss([], () => 0.5)).toBeNull();
|
|
const candidates = BOSS_TESTING_CATALOG.slice(0, 2);
|
|
expect(randomTestingBoss(candidates, () => 0)).toBe(candidates[0]);
|
|
expect(randomTestingBoss(candidates, () => 1)).toBe(candidates[1]);
|
|
expect(randomTestingBoss(candidates, () => Number.NaN)).toBe(candidates[0]);
|
|
});
|
|
});
|