import { describe, expect, it } from "vitest"; import { DUNGEON_DEFINITIONS, prepareDungeonSessionDefinition, requireDungeonDefinition } from "./dungeonRegistry"; import { FORSAKEN_ABBEY_DEFINITION } from "./dungeons/forsakenAbbey"; import { WOW_DUNGEON_LIGHTING, withWowDungeonEnvironment, wowDungeonLightingProfile } from "./wowDungeonEnvironment"; import { materializeBiteSizedLayout } from "./biteSizedLayoutRuntime"; import stairs from "./generated/wowDungeonStairs.generated.json"; import stockadesOverride from "../../dungeon-pipeline/runtime-overrides/stormwind-stockade.json"; import { BITE_SIZED_DEFAULT_CONFIG, selectBiteSizedRun } from "../../shared/bite-sized-layouts.mjs"; import { DUNGEON_REGISTRY } from "./dungeonRegistry"; import { dungeonContentSourceId, isBiteSizedDungeonId } from "./dungeonContentSource"; describe("fleet-wide WoW dungeon environment policy", () => { const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => ( dungeon.expansion !== "runewaker" && !isBiteSizedDungeonId(dungeon.id) )); const wowBiteSizedDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => ( dungeon.expansion !== "runewaker" && isBiteSizedDungeonId(dungeon.id) )); it("gives every WoW dungeon readable ambient and hemisphere fill without changing fog draw distance or material roles", () => { expect(wowDungeons).toHaveLength(54); for (const dungeon of wowDungeons) { const profile = WOW_DUNGEON_LIGHTING[wowDungeonLightingProfile(dungeon.id)]; expect(dungeon.presentation.ambientLight).toEqual(profile.ambientLight); expect(dungeon.presentation.hemisphereLight).toEqual(profile.hemisphereLight); expect(dungeon.presentation.directionalLight).toEqual(profile.directionalLight); expect(dungeon.presentation.background).toBe(profile.background); expect(dungeon.presentation.fog.color).toBe(profile.fog.color); expect(dungeon.presentation.fog.near).toBe(profile.fog.near); expect(dungeon.presentation.fog.far).toBeGreaterThan(dungeon.presentation.fog.near); const updated = withWowDungeonEnvironment(dungeon); expect(updated.presentation.materials).toBe(dungeon.presentation.materials); expect(updated.assets).toBe(dungeon.assets); expect(updated.validation).toBe(dungeon.validation); } }); it("preserves adapted source lighting on every WoW Bite-Sized catalog and prepared definition", () => { expect(wowBiteSizedDungeons.map((dungeon) => dungeon.id).sort()).toEqual([ "bite-sized-deadmines-ship", "bite-sized-stormwind-stockade", "bite-sized-utgarde-pinnacle-skadi", "bite-sized-wailing-caverns", "randomized-bite-sized-stormwind-stockade", "randomized-bite-sized-stormwind-stockade-available", ]); const presentationLighting = (definition: typeof wowBiteSizedDungeons[number]) => ({ background: definition.presentation.background, fog: { color: definition.presentation.fog.color, near: definition.presentation.fog.near, far: definition.presentation.fog.far, }, ambientLight: definition.presentation.ambientLight, hemisphereLight: definition.presentation.hemisphereLight, directionalLight: definition.presentation.directionalLight, }); for (const catalogDefinition of wowBiteSizedDungeons) { const source = requireDungeonDefinition(dungeonContentSourceId(catalogDefinition.id)); const expectedLighting = presentationLighting(source); expect(presentationLighting(catalogDefinition), catalogDefinition.id).toEqual(expectedLighting); const prepared = prepareDungeonSessionDefinition(catalogDefinition.id, `lighting-${catalogDefinition.id}`); expect(prepared, catalogDefinition.id).not.toBeNull(); const preparedSource = requireDungeonDefinition( prepared!.contentSourceDungeonId ?? dungeonContentSourceId(catalogDefinition.id), ); expect(prepared!.expansion, `${catalogDefinition.id} prepared expansion`).toBe(preparedSource.expansion); const sourceSlice = wowBiteSizedDungeons.find((definition) => ( !definition.id.startsWith("randomized-") && dungeonContentSourceId(definition.id) === preparedSource.id )) ?? DUNGEON_DEFINITIONS.find((definition) => ( definition.expansion === preparedSource.expansion && isBiteSizedDungeonId(definition.id) && !definition.id.startsWith("randomized-") && dungeonContentSourceId(definition.id) === preparedSource.id )); expect(sourceSlice, `${catalogDefinition.id} source slice ${preparedSource.id}`).toBeDefined(); expect(presentationLighting(prepared!), `${catalogDefinition.id} prepared from ${preparedSource.id}`) .toEqual(presentationLighting(sourceSlice!)); if (preparedSource.id === dungeonContentSourceId(catalogDefinition.id)) { expect(presentationLighting(prepared!), `${catalogDefinition.id} prepared`).toEqual(expectedLighting); } } }); it("uses the selected source lighting and point lights when a custom layout baseline belongs elsewhere", () => { const source = requireDungeonDefinition("wailing-caverns"); const pointLights = [{ position: [3, 4, 5] as const, color: "#80d8ff", intensity: 2, distance: 12 }]; const customSource = { ...source, presentation: { ...source.presentation, pointLights }, }; const baseline = requireDungeonDefinition("bite-sized-paspers-shrine-hafiz"); const selection = selectBiteSizedRun( BITE_SIZED_DEFAULT_CONFIG, "bite-sized-wailing-caverns", "cross-source-environment-test", ); const materialized = materializeBiteSizedLayout( customSource, baseline, selection, DUNGEON_REGISTRY, ); expect(materialized.presentation.background).toBe(source.presentation.background); expect(materialized.presentation.fog).toEqual(source.presentation.fog); expect(materialized.presentation.ambientLight).toEqual(source.presentation.ambientLight); expect(materialized.presentation.hemisphereLight).toEqual(source.presentation.hemisphereLight); expect(materialized.presentation.directionalLight).toEqual(source.presentation.directionalLight); expect(materialized.presentation.pointLights).toEqual(pointLights); expect(materialized.presentation.theme).toBe("Bite-Sized Dungeon"); }); it("keeps tuned lighting from a custom baseline whose explicit source matches", () => { const source = requireDungeonDefinition("wailing-caverns"); const selection = selectBiteSizedRun( BITE_SIZED_DEFAULT_CONFIG, "bite-sized-wailing-caverns", "explicit-baseline-source-test", ); const tunedPresentation = { ...source.presentation, background: "#101820", ambientLight: { color: "#c1d2c2", intensity: 0.91 }, hemisphereLight: { skyColor: "#abc4b5", groundColor: "#1f2a23", intensity: 1.23 }, directionalLight: { ...source.presentation.directionalLight, color: "#d9ecc8", intensity: 1.34 }, pointLights: [{ position: [6, 7, 8] as const, color: "#b8f1ff", intensity: 1.8, distance: 14 }], }; const customBaseline = { ...requireDungeonDefinition("bite-sized-paspers-shrine-hafiz"), id: "custom-lighting-baseline", contentSourceDungeonId: source.id, presentation: tunedPresentation, }; const materialized = materializeBiteSizedLayout( source, customBaseline, selection, DUNGEON_REGISTRY, ); expect(materialized.presentation.background).toBe(tunedPresentation.background); expect(materialized.presentation.ambientLight).toEqual(tunedPresentation.ambientLight); expect(materialized.presentation.hemisphereLight).toEqual(tunedPresentation.hemisphereLight); expect(materialized.presentation.directionalLight).toEqual(tunedPresentation.directionalLight); expect(materialized.presentation.pointLights).toEqual(tunedPresentation.pointLights); expect(materialized.presentation.location).toContain(selection.spot.name); }); it("keeps Stockades' reviewed lighting, ramps and navigation links intact", () => { const stockades = requireDungeonDefinition("stormwind-stockade"); // The existing campaign compiler stores package vectors at four decimals. const compiled = (value: unknown) => JSON.parse(JSON.stringify(value, (_key, item) => typeof item === "number" ? Number(item.toFixed(4)) : item)); expect(stockades.traversalRamps).toEqual(compiled(stockadesOverride.traversalRamps)); expect(stockades.navigationLinks).toEqual(compiled(stockadesOverride.navigationLinks)); expect(stockades.presentation.directionalLight).toEqual(stockadesOverride.presentation.directionalLight); }); it("does not apply WoW changes to RuneWaker environments", () => { expect(withWowDungeonEnvironment(FORSAKEN_ABBEY_DEFINITION)).toBe(FORSAKEN_ABBEY_DEFINITION); }); it("records a scan for every shipped WoW dungeon and installs only package-matched proxies", () => { expect(Object.keys(stairs.dungeons).sort()).toEqual(wowDungeons.map((dungeon) => dungeon.id).sort()); for (const dungeon of wowDungeons) { const record = (stairs.dungeons as Record)[dungeon.id]; if (record.ramps.length) expect(dungeon.traversalRamps).toEqual(record.ramps); } }); it("rejects stale ramp coordinates when collision content is replaced", () => { const dungeon = wowDungeons.find((entry) => entry.id !== "stormwind-stockade" && entry.traversalRamps?.length)!; expect(dungeon).toBeDefined(); const replaced = { ...dungeon, traversalRamps: undefined, assets: { ...dungeon.assets, collision: dungeon.assets.collision.map((asset) => ({ ...asset, checksum: "replacement" })) } }; expect(withWowDungeonEnvironment(replaced).traversalRamps).toBeUndefined(); }); });