import { describe, expect, it } from 'vitest'; import * as THREE from 'three'; import type { EnvironmentChapter } from '@/journey/types'; import { renderQualityFor } from '@/scene/render-quality'; import { SceneResourceBag } from '@/scene/resources'; import { CHAPTER_PALETTES, createMutableChapterPalette, writeChapterPalette, } from '@/scene/world/palette'; import { createRoad } from '@/scene/world/road'; import { createTerrain } from '@/scene/world/terrain'; describe('procedural world models', () => { it('scales geometry and atmosphere work monotonically by live quality tier', () => { const high = renderQualityFor('high', 3, false); const medium = renderQualityFor('medium', 3, false); const low = renderQualityFor('low', 3, false); expect(high.roadSegments).toBeGreaterThan(medium.roadSegments); expect(medium.roadSegments).toBeGreaterThan(low.roadSegments); expect(high.terrainSegments).toBeGreaterThan(medium.terrainSegments); expect(medium.terrainSegments).toBeGreaterThan(low.terrainSegments); expect(high.streakCount).toBeGreaterThan(low.streakCount); expect(low.postEffects).toBe(false); }); it('builds a shader road whose motion is entirely uniform-driven', () => { const resources = new SceneResourceBag(); const road = createRoad(resources, renderQualityFor('high', 1, false)); const surface = road.group.getObjectByName('road-surface') as ShaderRenderable; expect(surface).toBeInstanceOf(THREE.Mesh); expect(surface.geometry.index).not.toBeNull(); expect(surface.geometry.getAttribute('position').count).toBeGreaterThan(1_000); expect(surface.material.uniforms).toMatchObject({ uDistance: { value: 0 }, uSpeed: { value: 0 }, uAcceleration: { value: 0 }, }); expect(surface.material.fragmentShader).toContain('centerLane'); expect(surface.material.fragmentShader).toContain('outerEdge'); const positionArray = surface.geometry.getAttribute('position').array; const before = resources.snapshot(); for (let index = 0; index < 300; index += 1) { road.update(index * 0.5, 0.65, 0.4); } expect(resources.snapshot()).toEqual(before); expect(surface.geometry.getAttribute('position').array).toBe(positionArray); expect(surface.material.uniforms.uDistance?.value).toBe(149.5); expect(surface.material.uniforms.uSpeed?.value).toBe(0.65); expect(surface.material.uniforms.uAcceleration?.value).toBe(0.4); resources.dispose(); }); it('builds one wide indexed line terrain with a flat road-center mask', () => { const resources = new SceneResourceBag(); const terrain = createTerrain(resources, renderQualityFor('medium', 1, false)); const lines = terrain.group.getObjectByName('terrain-lines') as ShaderRenderable; expect(lines).toBeInstanceOf(THREE.LineSegments); expect(lines.geometry.index).not.toBeNull(); lines.geometry.computeBoundingBox(); expect(lines.geometry.boundingBox).not.toBeNull(); expect( (lines.geometry.boundingBox?.max.x ?? 0) - (lines.geometry.boundingBox?.min.x ?? 0), ).toBeGreaterThan(120); const roadMask = lines.geometry.getAttribute('roadMask'); expect(roadMask).toBeDefined(); const maskValues = Array.from(roadMask.array as ArrayLike); expect(Math.min(...maskValues)).toBe(0); expect(Math.max(...maskValues)).toBeGreaterThan(0.99); expect(lines.material.uniforms).toMatchObject({ uDistance: { value: 0 }, uSpeed: { value: 0 }, uRouteProgress: { value: 0 }, }); expect(lines.material.vertexShader).toContain('multiOctaveNoise'); const indexArray = lines.geometry.index?.array; const positionArray = lines.geometry.getAttribute('position').array; const lowColor = lines.material.uniforms.uLowColor?.value; const highColor = lines.material.uniforms.uHighColor?.value; const before = resources.snapshot(); for (let index = 0; index < 300; index += 1) { terrain.update(index * 0.4, 0.7, 'builds', index / 299); } expect(resources.snapshot()).toEqual(before); expect(lines.geometry.index?.array).toBe(indexArray); expect(lines.geometry.getAttribute('position').array).toBe(positionArray); expect(lines.material.uniforms.uLowColor?.value).toBe(lowColor); expect(lines.material.uniforms.uHighColor?.value).toBe(highColor); expect(lines.material.uniforms.uRouteProgress?.value).toBe(1); resources.dispose(); }); it('writes deterministic chapter blends into stable color allocations', () => { const target = createMutableChapterPalette('deep-night'); const colorReferences = [ target.sky, target.fog, target.terrainLow, target.terrainHigh, target.sun, target.haze, ]; expect(Object.keys(CHAPTER_PALETTES)).toEqual([ 'deep-night', 'systems', 'infrastructure', 'builds', 'pre-dawn', 'destination', ] satisfies EnvironmentChapter[]); expect(writeChapterPalette(target, 'deep-night', 'destination', 0.5)).toBe(target); const first = paletteSnapshot(target); expect(writeChapterPalette(target, 'deep-night', 'destination', 0.5)).toBe(target); expect(paletteSnapshot(target)).toEqual(first); expect([ target.sky, target.fog, target.terrainLow, target.terrainHigh, target.sun, target.haze, ]).toEqual(colorReferences); expect(target.warmth).toBeGreaterThan(CHAPTER_PALETTES['deep-night'].warmth); expect(target.warmth).toBeLessThan(CHAPTER_PALETTES.destination.warmth); }); }); function paletteSnapshot(palette: ReturnType) { return { sky: palette.sky.getHex(), fog: palette.fog.getHex(), terrainLow: palette.terrainLow.getHex(), terrainHigh: palette.terrainHigh.getHex(), sun: palette.sun.getHex(), haze: palette.haze.getHex(), warmth: palette.warmth, terrainEnergy: palette.terrainEnergy, starVisibility: palette.starVisibility, streakEnergy: palette.streakEnergy, }; } interface ShaderRenderable { geometry: { index: { array: ArrayLike } | null; boundingBox: { min: { x: number }; max: { x: number } } | null; computeBoundingBox(): void; getAttribute(name: string): { count: number; array: ArrayLike }; }; material: { uniforms: Record; vertexShader: string; fragmentShader: string; }; }