Files
Portfolio/tests/unit/sun-arrival.test.ts

160 lines
7.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { SceneResourceBag } from '@/scene/resources';
import { createSun, DAWN_LIFT } 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<string, { value: number }> };
};
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('lifts the disc, widens the halo and fades the meteors as the dawn rises, without allocating', () => {
const resources = new SceneResourceBag();
const sun = createSun(resources);
const palette = createMutableChapterPalette('destination');
const disc = sun.group.getObjectByName('horizon-sun') as unknown as {
position: { y: number };
material: { uniforms: Record<string, { value: number }> };
};
const limb = sun.group.getObjectByName('sun-limb-glow') as unknown as { position: { y: number } };
const halo = sun.group.getObjectByName('horizon-halo') as unknown as Halo & { position: { y: number } };
const meteors = sun.group.getObjectByName('arrival-meteors') as unknown as {
material: { uniforms: { uFade: { value: number } } };
};
sun.update(palette, 1, 10, 0, true, 0);
sun.update(palette, 1, 20, 0, true, 0);
const night = { y: disc.position.y, width: halo.scale.x };
expect(meteors.material.uniforms.uFade.value).toBe(1);
const allocation = resources.snapshot();
sun.update(palette, 1, 20, 0, true, 0.7);
expect(meteors.material.uniforms.uFade.value).toBe(1);
sun.update(palette, 1, 20, 0, true, 1);
expect(disc.position.y).toBeCloseTo(night.y + DAWN_LIFT);
expect(DAWN_LIFT).toBe(58);
expect(limb.position.y).toBe(disc.position.y);
expect(halo.position.y).toBe(disc.position.y);
expect(halo.scale.x / night.width).toBeCloseTo(1.18);
expect(disc.material.uniforms.uDawn!.value).toBe(1);
expect(meteors.material.uniforms.uFade.value).toBe(0);
expect(resources.snapshot()).toEqual(allocation);
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();
});
});