115 lines
4.8 KiB
TypeScript
115 lines
4.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { SceneResourceBag } from '@/scene/resources';
|
|
import { createYard, railYaw } from '@/scene/world/yard';
|
|
import { STACK_TIERS, STACK_ZS, YARD_STOPS, yardStopsPassed } from '@/journey/yard-progress';
|
|
|
|
interface Node {
|
|
name: string;
|
|
count?: number;
|
|
isInstancedMesh?: boolean;
|
|
geometry?: Disposable;
|
|
material?: Disposable;
|
|
}
|
|
interface Disposable {
|
|
addEventListener(type: string, listener: () => void): void;
|
|
}
|
|
const nodes = (group: unknown): Node[] => {
|
|
const found: Node[] = [];
|
|
(group as { traverse(visit: (child: Node) => void): void }).traverse((child) => found.push(child));
|
|
return found;
|
|
};
|
|
const instancedMeshes = (group: unknown) => nodes(group).filter((child) => child.isInstancedMesh === true);
|
|
|
|
describe('night freight yard', () => {
|
|
it('draws the stacks as one InstancedMesh of 4 + 3 + 2 + 1 containers', () => {
|
|
const yard = createYard(new SceneResourceBag(), true);
|
|
const stacks = instancedMeshes(yard.group).filter((mesh) => mesh.name === 'yard-stacks');
|
|
expect(stacks).toHaveLength(1);
|
|
expect(stacks[0]!.count).toBe(STACK_TIERS.reduce((sum, tiers) => sum + tiers, 0));
|
|
});
|
|
|
|
it('is visible only in Additional work, and fades at the chapter edges', () => {
|
|
const yard = createYard(new SceneResourceBag(), true);
|
|
yard.update('toolkit', 0.5, 0, 1 / 60);
|
|
expect(yard.group.visible).toBe(false);
|
|
yard.update('additional-work', null, 0, 1 / 60);
|
|
expect(yard.group.visible).toBe(false);
|
|
yard.update('additional-work', 0, 0, 1 / 60);
|
|
expect(yard.group.visible).toBe(false);
|
|
yard.update('additional-work', 0.5, 0, 1 / 60);
|
|
expect(yard.group.visible).toBe(true);
|
|
expect(yard.inspect().presence).toBe(1);
|
|
});
|
|
|
|
it('lights one plate per stop passed, forward and back', () => {
|
|
const yard = createYard(new SceneResourceBag(), true);
|
|
const ps = Array.from({ length: 101 }, (_, i) => i / 100);
|
|
for (const p of [...ps, ...[...ps].reverse()]) {
|
|
yard.update('additional-work', p, 0, 1 / 60);
|
|
expect(yard.inspect().litPlates).toBe(yardStopsPassed(p));
|
|
}
|
|
});
|
|
|
|
it('lowers the spreader and strikes the work lamps at a stop, lifts and dims them after', () => {
|
|
const yard = createYard(new SceneResourceBag(), true);
|
|
for (let frame = 0; frame < 60; frame += 1) yard.update('additional-work', YARD_STOPS[1], 0, 1 / 60);
|
|
const working = yard.inspect();
|
|
expect(working.spreader).toBe(1);
|
|
expect(working.craneZ).toBe(STACK_ZS[1]);
|
|
expect(working.lamp).toBe(1);
|
|
expect(working.strikes).toBe(1);
|
|
expect(working.flood).toBeCloseTo(1 / 1.5, 2);
|
|
for (let frame = 0; frame < 60; frame += 1) yard.update('additional-work', 0.2, 0, 1 / 60);
|
|
expect(yard.inspect().spreader).toBe(0);
|
|
expect(yard.inspect().lamp).toBe(0);
|
|
});
|
|
|
|
it('hides the crane (not the yard) while an underpass deck is near it', () => {
|
|
const yard = createYard(new SceneResourceBag(), true);
|
|
yard.update('additional-work', 0.9, 0, 1 / 60, null);
|
|
expect(yard.inspect().cranePresence).toBe(1);
|
|
const z = yard.inspect().craneWorldZ;
|
|
yard.update('additional-work', 0.9, 0, 1 / 60, z + 4);
|
|
expect(yard.inspect().cranePresence).toBe(0);
|
|
expect(yard.inspect().presence).toBe(1);
|
|
});
|
|
|
|
it('lite: crane, stacks and plates; no lamps, floods or pools', () => {
|
|
const yard = createYard(new SceneResourceBag(), false);
|
|
expect(instancedMeshes(yard.group)).toHaveLength(1);
|
|
for (let frame = 0; frame < 120; frame += 1) yard.update('additional-work', YARD_STOPS[0], 1, 1 / 60);
|
|
expect(yard.inspect()).toMatchObject({ litPlates: 1, lamp: 0, flood: 0, spreader: 1 });
|
|
});
|
|
|
|
it('turns the rails to the viewport aspect so the yard keeps its place in the left margin', () => {
|
|
const yard = createYard(new SceneResourceBag(), true);
|
|
const yawAt = (aspect: number) => {
|
|
yard.update('additional-work', 0.5, 0, 1 / 60, null, aspect);
|
|
return (yard.group as { rotation: { y: number } }).rotation.y;
|
|
};
|
|
const wide = yawAt(1920 / 1080);
|
|
const design = yawAt(1440 / 900);
|
|
const laptop = yawAt(1024 / 700);
|
|
expect(wide).toBeGreaterThan(design);
|
|
expect(design).toBeGreaterThan(laptop);
|
|
expect(design).toBeCloseTo(railYaw(1.6), 6);
|
|
expect(yawAt(390 / 844)).toBeGreaterThan(0);
|
|
expect(yawAt(Number.NaN)).toBeCloseTo(railYaw(), 6);
|
|
});
|
|
|
|
it('frees every geometry and material on dispose', () => {
|
|
const resources = new SceneResourceBag();
|
|
const yard = createYard(resources, true);
|
|
const owned = new Set<Disposable>();
|
|
for (const child of nodes(yard.group)) {
|
|
if (child.geometry) owned.add(child.geometry);
|
|
if (child.material) owned.add(child.material);
|
|
}
|
|
expect(owned.size).toBeGreaterThan(6);
|
|
let disposed = 0;
|
|
owned.forEach((resource) => resource.addEventListener('dispose', () => { disposed += 1; }));
|
|
resources.dispose();
|
|
expect(disposed).toBe(owned.size);
|
|
});
|
|
});
|