import { describe, expect, it } from "vitest"; import { RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, STORMWIND_STOCKADE_DUNGEON_ID, } from "./dungeonContentSource"; import { DUNGEON_REGISTRY, RANDOMIZED_BITE_SIZED_CREATURE_CATALOG, dungeonDefinitionById, } from "./dungeonRegistry"; 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): 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 independent deterministic trash and boss rolls 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 bossEntityIds = new Set(catalog.filter((candidate) => candidate.kind === "boss").map((candidate) => candidate.entityId)); const observedBossEntityIds = new Set(); const observedTrashLineups = new Set(); 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) => bossEntityIds.has(entityId))).toBe(true); expect(observedBossEntityIds.size).toBeGreaterThan(100); expect(observedTrashLineups.size).toBeGreaterThan(1_900); expect(activateDungeonSession(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: "shared-run" })).toBe(true); expect(dungeonDefinitionById(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID)?.staticSpawns).toEqual(first.staticSpawns); }); it("keeps the randomized boss on the location's stable completion objective", () => { 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 base = createBiteSizedStockadesDefinition(source, "random-boss-objective:objective-slot"); const originalSpawn = base.staticSpawns.find((candidate) => candidate.id === boss.id)!; const originalBoss = base.entities[originalSpawn.entityId]; expect(entity.kind).toBe("boss"); expect(boss.name).toBe(entity.name); expect(catalog.some((candidate) => candidate.kind === "boss" && candidate.entityId === spawn.entityId)).toBe(true); expect(entity.combat?.serverEntry).not.toBe(originalBoss.combat?.serverEntry); expect(randomized.encounters?.[0].rules[0].trigger).toEqual({ kind: "entities-dead", entityIds: [boss.id] }); expect(entity.combat).toMatchObject({ healthMultiplier: 1, damageMultiplier: 1 }); }); });