forked from phenom/healer-man
158 lines
8.7 KiB
TypeScript
158 lines
8.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
RANDOMIZED_BITE_SIZED_AVAILABLE_DUNGEON_ID,
|
|
STORMWIND_STOCKADE_DUNGEON_ID,
|
|
} from "./dungeonContentSource";
|
|
import {
|
|
DUNGEON_REGISTRY,
|
|
RANDOMIZED_BITE_SIZED_CREATURE_CATALOG,
|
|
AVAILABLE_RANDOMIZED_CREATURE_CATALOG,
|
|
BITE_SIZED_RANDOMIZED_CREATURE_CATALOG,
|
|
dungeonDefinitionById,
|
|
randomizedBiteSizedDefinitionForSeed,
|
|
dungeonCanEnter,
|
|
} from "./dungeonRegistry";
|
|
import { dungeonLeaderboardCatalog } from "./dungeonLeaderboardCatalog";
|
|
import { seededBossIndex } from "./dungeons/seededBossSelection";
|
|
import { activateDungeonSession } from "./dungeonSession";
|
|
import { createBiteSizedStockadesDefinition } from "./dungeons/biteSizedStockades";
|
|
import { createRandomizedBiteSizedStockadesDefinition } from "./dungeons/randomizedBiteSizedStockades";
|
|
|
|
const source = DUNGEON_REGISTRY[STORMWIND_STOCKADE_DUNGEON_ID];
|
|
const catalog = RANDOMIZED_BITE_SIZED_CREATURE_CATALOG;
|
|
|
|
function create(seed: string) {
|
|
return createRandomizedBiteSizedStockadesDefinition(source, seed, catalog);
|
|
}
|
|
|
|
function bossEntityId(definition: ReturnType<typeof create>): string | undefined {
|
|
const boss = definition.bosses[0];
|
|
const spawn = definition.staticSpawns.find((candidate) => candidate.id === boss.id);
|
|
return spawn?.entityId;
|
|
}
|
|
|
|
describe("Randomized Bite-Sized Stockades", () => {
|
|
it("keeps source geometry and both existing Stockades definitions immutable", () => {
|
|
const originalSpawns = source.staticSpawns.map((spawn) => ({ ...spawn }));
|
|
const randomized = create("random-layout-a");
|
|
|
|
expect(randomized.id).toBe(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
|
|
expect(randomized.navigationLinks).toEqual(source.navigationLinks);
|
|
expect(randomized.traversalRamps).toEqual(source.traversalRamps);
|
|
expect(randomized.roamingPacks).toEqual([]);
|
|
expect(randomized.serverGameObjects.filter((object) => object.id.startsWith("bite-sized-stockades-"))).toHaveLength(2);
|
|
expect(source.staticSpawns).toEqual(originalSpawns);
|
|
});
|
|
|
|
it("uses the complete model-backed cross-dungeon catalog regardless of installed packs", () => {
|
|
const mobs = catalog.filter((candidate) => candidate.kind === "mob");
|
|
const bosses = catalog.filter((candidate) => candidate.kind === "boss");
|
|
|
|
expect(mobs.length).toBeGreaterThan(500);
|
|
expect(bosses.length).toBeGreaterThan(100);
|
|
expect(new Set(catalog.map((candidate) => candidate.sourceDungeonId)).size).toBeGreaterThan(50);
|
|
expect(catalog.some((candidate) => candidate.definition.visual.model?.url.startsWith("/assets/creatures/wailing-caverns/"))).toBe(true);
|
|
expect(catalog.some((candidate) => candidate.definition.visual.model?.url.startsWith("/assets/game/creatures/shared/"))).toBe(true);
|
|
expect(catalog.some((candidate) => candidate.definition.visual.model?.url.startsWith("/assets/game/dungeons/")
|
|
&& candidate.sourceDungeonId !== STORMWIND_STOCKADE_DUNGEON_ID)).toBe(true);
|
|
expect(catalog.some((candidate) => candidate.sourceDungeonId === STORMWIND_STOCKADE_DUNGEON_ID)).toBe(true);
|
|
expect(catalog.some((candidate) => candidate.key.startsWith("wow:"))).toBe(true);
|
|
expect(catalog.some((candidate) => candidate.key.startsWith("rom:"))).toBe(true);
|
|
expect(catalog.filter((candidate) => candidate.key.startsWith("rom:")).length).toBeGreaterThan(400);
|
|
expect(catalog.every((candidate) => /^(wow|rom):(mob|boss):[1-9][0-9]*$/.test(candidate.key))).toBe(true);
|
|
const romCandidate = catalog.find((candidate) => candidate.key.startsWith("rom:"))!;
|
|
expect(romCandidate.sourceEntry).toBeGreaterThan(0);
|
|
expect(romCandidate.definition.combat).not.toHaveProperty("serverEntry");
|
|
});
|
|
|
|
it("replaces every corridor spawn and deals distinct mobs without replacement", () => {
|
|
const randomized = create("every-slot-is-random");
|
|
const bossId = randomized.bosses[0].id;
|
|
const trash = randomized.staticSpawns.filter((spawn) => spawn.id !== bossId);
|
|
const selectedIds = trash.map((spawn) => spawn.entityId);
|
|
const base = createBiteSizedStockadesDefinition(source, "every-slot-is-random:objective-slot");
|
|
|
|
expect(trash).toHaveLength(12);
|
|
expect(new Set(selectedIds).size).toBe(trash.length);
|
|
expect(selectedIds.every((entityId) => /^randomized:(wow|rom):mob:/.test(entityId))).toBe(true);
|
|
for (const spawn of trash) {
|
|
const entity = randomized.entities[spawn.entityId];
|
|
const originalSpawn = base.staticSpawns.find((candidate) => candidate.id === spawn.id)!;
|
|
const original = base.entities[originalSpawn.entityId];
|
|
expect(entity.kind).toBe("mob");
|
|
expect(entity.combat?.serverEntry).not.toBe(original.combat?.serverEntry);
|
|
expect(entity.combat).toMatchObject({
|
|
healthMultiplier: 1,
|
|
damageMultiplier: 1,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("uses deterministic cross-dungeon trash and bosses across run ids", () => {
|
|
const first = create("shared-run");
|
|
const second = create("shared-run");
|
|
expect(second.staticSpawns).toEqual(first.staticSpawns);
|
|
expect(second.bosses).toEqual(first.bosses);
|
|
|
|
const observedBossEntityIds = new Set<string>();
|
|
const observedTrashLineups = new Set<string>();
|
|
for (let index = 0; index < 2_000; index += 1) {
|
|
const definition = create(`roll-${index}`);
|
|
const entityId = bossEntityId(definition);
|
|
if (entityId) observedBossEntityIds.add(entityId);
|
|
const bossId = definition.bosses[0].id;
|
|
observedTrashLineups.add(definition.staticSpawns
|
|
.filter((spawn) => spawn.id !== bossId)
|
|
.map((spawn) => spawn.entityId)
|
|
.join("|"));
|
|
}
|
|
expect([...observedBossEntityIds].every((entityId) => catalog.some((candidate) => candidate.kind === "boss" && candidate.entityId === entityId))).toBe(true);
|
|
expect(observedBossEntityIds.size).toBeGreaterThan(50);
|
|
expect(observedTrashLineups.size).toBeGreaterThan(1_900);
|
|
|
|
expect(activateDungeonSession(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: "shared-run" })).toBe(true);
|
|
const selected = dungeonDefinitionById(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID)!;
|
|
expect(selected.biteSizedSelection?.dungeonId).toBe(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
|
|
expect(activateDungeonSession(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: "shared-run", biteSizedSelection: selected.biteSizedSelection })).toBe(true);
|
|
expect(dungeonDefinitionById(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID)?.staticSpawns).toEqual(selected.staticSpawns);
|
|
});
|
|
|
|
it("uses the selected cross-dungeon boss for combat and completion", () => {
|
|
const randomized = create("random-boss-objective");
|
|
const boss = randomized.bosses[0];
|
|
const spawn = randomized.staticSpawns.find((candidate) => candidate.id === boss.id)!;
|
|
const entity = randomized.entities[spawn.entityId];
|
|
const candidate = catalog.find((entry) => `randomized:${entry.key}` === boss.id)!;
|
|
|
|
expect(entity.kind).toBe("boss");
|
|
expect(boss.name).toBe(entity.name);
|
|
expect(candidate.kind).toBe("boss");
|
|
expect(entity.combat?.serverEntry).toBe(candidate.definition.combat?.serverEntry);
|
|
expect(randomized.encounters?.[0].rules[0].trigger).toEqual({ kind: "entities-dead", entityIds: [boss.id] });
|
|
expect(entity.combat?.healthMultiplier).toBe(1);
|
|
});
|
|
|
|
it("keeps both selectable pools available and excludes unavailable source dungeons", () => {
|
|
expect(BITE_SIZED_RANDOMIZED_CREATURE_CATALOG.length).toBeGreaterThan(12);
|
|
expect(AVAILABLE_RANDOMIZED_CREATURE_CATALOG.length).toBeGreaterThan(BITE_SIZED_RANDOMIZED_CREATURE_CATALOG.length);
|
|
const allKeys = new Set(AVAILABLE_RANDOMIZED_CREATURE_CATALOG.map((entry) => entry.key));
|
|
expect(BITE_SIZED_RANDOMIZED_CREATURE_CATALOG.every((entry) => allKeys.has(entry.key))).toBe(true);
|
|
expect(AVAILABLE_RANDOMIZED_CREATURE_CATALOG.every((entry) => dungeonCanEnter(entry.sourceDungeonId))).toBe(true);
|
|
for (const [dungeonId, pool] of [
|
|
[RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, AVAILABLE_RANDOMIZED_CREATURE_CATALOG],
|
|
[RANDOMIZED_BITE_SIZED_AVAILABLE_DUNGEON_ID, BITE_SIZED_RANDOMIZED_CREATURE_CATALOG],
|
|
] as const) {
|
|
const definition = randomizedBiteSizedDefinitionForSeed("shared-pool-seed", dungeonId);
|
|
expect(definition.id).toBe(dungeonId);
|
|
expect(definition.staticSpawns.every((spawn) => pool.some((entry) => (
|
|
entry.kind === "boss" ? entry.entityId === spawn.entityId : `randomized:${entry.key}` === spawn.entityId
|
|
)))).toBe(true);
|
|
expect(definition.bosses[0].id).toMatch(/^randomized:(wow|rom):boss:/);
|
|
const board = dungeonLeaderboardCatalog.dungeons.find((entry) => entry.id === dungeonId)!;
|
|
const finalBossIds = board.difficulties[0].finalBossIds!;
|
|
expect(definition.bosses[0].id).toBe(finalBossIds[seededBossIndex("shared-pool-seed:boss", finalBossIds.length)]);
|
|
}
|
|
});
|
|
});
|