import { describe, expect, it } from 'vitest'; import { SceneResourceBag } from '@/scene/resources'; import { createSun } from '@/scene/world/sun'; import { createMutableChapterPalette } from '@/scene/world/palette'; interface Halo { scale: { x: number }; material: { opacity: number; color: { getHex(): number } }; } describe('destination sun', () => { it('keeps a distinct destination glow after the one-shot finale, without allocating on updates', () => { const resources = new SceneResourceBag(); const sun = createSun(resources); const palette = createMutableChapterPalette(); const halo = sun.group.getObjectByName('horizon-halo') as Halo; sun.update(palette, 1, 10, 0, false); const initial = { width: halo.scale.x, opacity: halo.material.opacity, color: halo.material.color.getHex() }; const allocation = resources.snapshot(); sun.update(palette, 1, 10, 0, true); sun.update(palette, 1, 11, 0, true); expect(halo.scale.x).toBeGreaterThan(initial.width); expect(halo.material.opacity).toBeGreaterThan(initial.opacity); expect(halo.material.color.getHex()).not.toBe(initial.color); sun.update(palette, 1, 15, 0, true); const settled = { width: halo.scale.x, opacity: halo.material.opacity, color: halo.material.color.getHex() }; expect(settled.color).not.toBe(initial.color); expect(settled.opacity).toBeGreaterThan(initial.opacity); for (const time of [20, 100]) { sun.update(palette, 1, time, 0, true); expect(halo.scale.x).toBe(settled.width); expect(halo.material.opacity).toBe(settled.opacity); expect(halo.material.color.getHex()).toBe(settled.color); } sun.update(palette, 0.8, 101, 0, false); sun.update(palette, 1, 102, 0, true); sun.update(palette, 1, 103, 0, true); expect(halo.scale.x).toBeGreaterThan(initial.width); expect(resources.snapshot()).toEqual(allocation); resources.dispose(); }); it('draws three batched meteor trails only during arrival and re-arms after leaving', () => { const resources = new SceneResourceBag(); const sun = createSun(resources); const palette = createMutableChapterPalette(); const meteors = sun.group.getObjectByName('arrival-meteors') as { visible: boolean; geometry: { getAttribute(name: string): { count: number; array: Float32Array } }; material: { uniforms: { uTime: { value: number } } }; } | undefined; expect(meteors).toBeDefined(); sun.update(palette, 1, 0, 0, false); expect(meteors!.visible).toBe(true); expect(meteors!.material.uniforms.uTime.value).toBe(-1); sun.update(palette, 1, 0.1, 0, false); expect(meteors!.visible).toBe(false); const allocation = resources.snapshot(); const positions = meteors!.geometry.getAttribute('position'); expect(positions.count).toBeLessThanOrEqual(18); sun.update(palette, 1, 1, 0, true); sun.update(palette, 1, 2, 0, true); expect(meteors!.visible).toBe(true); expect(meteors!.material.uniforms.uTime.value).toBe(1); sun.update(palette, 1, 5.1, 0, true); expect(meteors!.visible).toBe(false); sun.update(palette, 1, 50, 0, true); expect(meteors!.visible).toBe(false); sun.update(palette, 0.8, 51, 0, false); sun.update(palette, 1, 52, 0, true); sun.update(palette, 1, 53, 0, true); expect(meteors!.visible).toBe(true); sun.update(palette, 0.8, 53.1, 0, false); expect(meteors!.visible).toBe(false); expect(meteors!.geometry.getAttribute('position').array).toBe(positions.array); expect(resources.snapshot()).toEqual(allocation); resources.dispose(); expect(resources.snapshot().owned.geometry).toBe(0); expect(resources.snapshot().owned.material).toBe(0); }); it('reuses the sun rim for an expanding ring that repeats every three seconds only at the destination', () => { const resources = new SceneResourceBag(); const sun = createSun(resources); const palette = createMutableChapterPalette(); const limb = sun.group.getObjectByName('sun-limb-glow') as { material: { uniforms: Record }; }; const uniforms = limb.material.uniforms; expect(uniforms.uRingRadius).toBeDefined(); sun.update(palette, 1, 0, 0, true); sun.update(palette, 1, 1, 0, true); const firstRadius = uniforms.uRingRadius!.value; const firstOpacity = uniforms.uRingOpacity!.value; expect(uniforms.uRingOpacity!.value).toBeGreaterThan(0); sun.update(palette, 1, 2, 0, true); expect(uniforms.uRingRadius!.value).toBeGreaterThan(firstRadius); sun.update(palette, 1, 3.5, 0, true); expect(uniforms.uRingOpacity!.value).toBe(0); for (const time of [4, 7, 40]) { sun.update(palette, 1, time, 0, true); expect(uniforms.uRingRadius!.value).toBeCloseTo(firstRadius); expect(uniforms.uRingOpacity!.value).toBeCloseTo(firstOpacity); } sun.update(palette, 0.8, 40.1, 0, false); expect(uniforms.uRingOpacity!.value).toBe(0); resources.dispose(); }); it('does not pulse from route progress alone and cancels when leaving Contact', () => { const resources = new SceneResourceBag(); const sun = createSun(resources); const palette = createMutableChapterPalette(); const halo = sun.group.getObjectByName('horizon-halo') as Halo; sun.update(palette, 1, 0); const width = halo.scale.x; sun.update(palette, 1, 1); expect(halo.scale.x).toBe(width); sun.update(palette, 1, 2, 0, true); sun.update(palette, 1, 3, 0, true); expect(halo.scale.x).toBeGreaterThan(width); sun.update(palette, 1, 3.1, 0, false); expect(halo.scale.x).toBe(width); resources.dispose(); }); });