// tests/engine/params.test.ts
import { describe, expect, it } from 'vitest';
import {
type PostUniformInput,
type UniformInput,
packParams,
packPostParams,
} from '~/engine/params';
import type { SpeciesParams } from '~/engine/types';
const sp = (over: Partial<SpeciesParams> = {}): SpeciesParams => ({
color: [1, 1, 1],
populationShare: 1,
sensorAngle: 0.5,
sensorDistance: 24,
turnSpeed: 0.4,
moveSpeed: 1,
depositAmount: 20,
...over,
});
const input: UniformInput = {
fieldWidth: 1920,
fieldHeight: 1080,
agentCount: 123456,
frame: 77,
time: 9.5,
decayRate: 0.97,
fadeStrength: 0.1,
trailSaturation: 255,
diffusionFreq: 3,
chaosInterval: 90,
chaosStrength: 0.5,
rhythmAmplitude: 0.05,
exposure: 2.5,
spawnPattern: 'ring',
headIntensity: 1.6,
backgroundColor: [0.1, 0.2, 0.3],
species: [sp({ moveSpeed: 1 }), sp({ moveSpeed: 2 }), sp({ moveSpeed: 3 })],
matrix: [
[1, -0.6, -0.6],
[-0.6, 1, -0.6],
[-0.6, -0.6, 1],
],
audio: {
moveSpeedFactor: 1.4,
turnSpeedFactor: 0.8,
depositFactor: 2,
sensorDistanceFactor: 1.3,
rhythmAmplitudeFactor: 1.1,
chaosStrengthFactor: 1.5,
},
};
describe('packParams', () => {
it('produces a 304-byte buffer', () => {
expect(packParams(input).byteLength).toBe(304);
});
it('packs the u32 header fields', () => {
const u = new Uint32Array(packParams(input));
expect(u[0]).toBe(1920);
expect(u[1]).toBe(1080);
expect(u[2]).toBe(123456);
expect(u[3]).toBe(77); // frame
expect(u[8]).toBe(3); // diffusionFreq
expect(u[11]).toBe(90); // chaosInterval
expect(u[15]).toBe(3); // 'ring' → 3
});
it('packs global f32 fields', () => {
const f = new Float32Array(packParams(input));
expect(f[4]).toBeCloseTo(9.5); // time
expect(f[5]).toBeCloseTo(0.97); // decayRate
expect(f[6]).toBeCloseTo(0.1); // fadeStrength
expect(f[7]).toBeCloseTo(255); // trailSaturation
expect(f[12]).toBeCloseTo(0.5); // chaosStrength
expect(f[13]).toBeCloseTo(0.05); // rhythmAmplitude
expect(f[14]).toBeCloseTo(2.5); // exposure
});
it('zeros the pre-1.2 legacy attraction/repulsion strength slots', () => {
const f = new Float32Array(packParams(input));
expect(f[9]).toBe(0);
expect(f[10]).toBe(0);
});
it('packs audio modulation factors + background fields', () => {
const f = new Float32Array(packParams(input));
expect(f[16]).toBeCloseTo(1.4); // moveSpeedFactor
expect(f[17]).toBeCloseTo(0.8); // turnSpeedFactor
expect(f[18]).toBeCloseTo(2); // depositFactor
expect(f[19]).toBeCloseTo(1.3); // sensorDistanceFactor
expect(f[20]).toBeCloseTo(1.1); // rhythmAmplitudeFactor
expect(f[21]).toBeCloseTo(1.5); // chaosStrengthFactor
expect(f[22]).toBeCloseTo(1.6); // headIntensity
expect(f[24]).toBeCloseTo(0.1); // backgroundColor.r
expect(f[25]).toBeCloseTo(0.2); // backgroundColor.g
expect(f[26]).toBeCloseTo(0.3); // backgroundColor.b
});
it('packs per-species moveSpeed at the right offset', () => {
const f = new Float32Array(packParams(input));
// species block starts at float 28; moveSpeed at +7 floats, stride 12
expect(f[28 + 7]).toBeCloseTo(1); // species 0
expect(f[28 + 12 + 7]).toBeCloseTo(2); // species 1
expect(f[28 + 24 + 7]).toBeCloseTo(3); // species 2
});
it('packs matrix rows at float index 64', () => {
const f = new Float32Array(packParams(input));
expect(f[64]).toBeCloseTo(1); // row0.x
expect(f[65]).toBeCloseTo(-0.6); // row0.y
expect(f[66]).toBeCloseTo(-0.6); // row0.z
expect(f[68]).toBeCloseTo(-0.6); // row1.x
expect(f[69]).toBeCloseTo(1); // row1.y
expect(f[72]).toBeCloseTo(-0.6); // row2.x
expect(f[74]).toBeCloseTo(1); // row2.z
});
});
const postInput: PostUniformInput = {
resolutionX: 1920,
resolutionY: 1080,
exposure: 1.5,
time: 12.5,
feedbackAmount: 0.92,
bloomThreshold: 1.1,
bloomIntensity: 0.8,
vignette: 0.4,
saturation: 1.2,
contrast: 1.1,
backgroundColor: [0.7, 0.1, 0.2],
blendMode: 'paint',
kaleidoscope: 4,
exposureFactor: 1.6,
bloomIntensityFactor: 1.8,
saturationFactor: 1.2,
contrastFactor: 1.3,
};
describe('packPostParams', () => {
it('produces an 80-byte buffer (bg + audio factors)', () => {
expect(packPostParams(postInput).byteLength).toBe(80);
});
it('packs scalar post fields at their indices', () => {
const f = new Float32Array(packPostParams(postInput));
expect(f[0]).toBeCloseTo(1920);
expect(f[1]).toBeCloseTo(1080);
expect(f[2]).toBeCloseTo(1.5); // exposure
expect(f[3]).toBeCloseTo(12.5); // time
expect(f[4]).toBeCloseTo(0.92); // feedbackAmount
expect(f[5]).toBeCloseTo(1.1); // bloomThreshold
expect(f[6]).toBeCloseTo(0.8); // bloomIntensity
expect(f[7]).toBeCloseTo(0.4); // vignette
expect(f[8]).toBeCloseTo(1.2); // saturation
expect(f[9]).toBeCloseTo(1.1); // contrast
});
it('packs backgroundColor at vec3-aligned offset (byte 48 / float 12)', () => {
const f = new Float32Array(packPostParams(postInput));
expect(f[12]).toBeCloseTo(0.7); // bg.r
expect(f[13]).toBeCloseTo(0.1); // bg.g
expect(f[14]).toBeCloseTo(0.2); // bg.b
});
it('packs post-side audio factors at f[16..19]', () => {
const f = new Float32Array(packPostParams(postInput));
expect(f[16]).toBeCloseTo(1.6); // exposureFactor
expect(f[17]).toBeCloseTo(1.8); // bloomIntensityFactor
expect(f[18]).toBeCloseTo(1.2); // saturationFactor
expect(f[19]).toBeCloseTo(1.3); // contrastFactor
});
it('packs blendMode as u32 at the post-bg pad slot (u[15])', () => {
const u = new Uint32Array(packPostParams(postInput));
expect(u[15]).toBe(1); // 'paint' = index 1 in BLEND_MODES
const additive = packPostParams({ ...postInput, blendMode: 'additive' });
expect(new Uint32Array(additive)[15]).toBe(0);
});
it('packs kaleidoscope as u32 at the pre-bg pad slot (u[10])', () => {
const u = new Uint32Array(packPostParams(postInput));
expect(u[10]).toBe(4);
const off = packPostParams({ ...postInput, kaleidoscope: 0 });
expect(new Uint32Array(off)[10]).toBe(0);
});
it('clamps kaleidoscope to 0..6 even if a caller passes a wild value', () => {
const high = packPostParams({ ...postInput, kaleidoscope: 99 });
expect(new Uint32Array(high)[10]).toBe(6);
const neg = packPostParams({ ...postInput, kaleidoscope: -3 });
expect(new Uint32Array(neg)[10]).toBe(0);
});
});