Zum Inhalt

tests/engine/params.test.ts

// 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);
  });
});