// src/engine/seeding.ts
// Pure agent-buffer seeding for the 4 spawn patterns.
// Layout per agent: 24 bytes / 6 float slots =
// pos.x:f32, pos.y:f32, angle:f32, species:u32, rhythmPhase:f32, pad.
import type { SpawnPattern } from './types';
type Shares = readonly [number, number, number];
/** Bytes per agent in the GPU buffer (WGSL struct stride, 8-byte aligned). */
export const AGENT_BYTE_STRIDE = 24;
/** Builds a flat array that maps agent index -> species, weighted by shares. */
function speciesPlan(count: number, shares: Shares): Uint8Array {
const total = shares[0] + shares[1] + shares[2];
const plan = new Uint8Array(count);
if (total <= 0) return plan; // all-zero → every agent is species 0
let written = 0;
for (let s = 0; s < 3; s++) {
const n = s === 2 ? count - written : Math.round(((shares[s] ?? 0) / total) * count);
for (let i = 0; i < n && written < count; i++) plan[written++] = s;
}
return plan;
}
export function seedAgents(
pattern: SpawnPattern,
agentCount: number,
shares: Shares,
fieldWidth: number,
fieldHeight: number,
): ArrayBuffer {
const buf = new ArrayBuffer(agentCount * AGENT_BYTE_STRIDE);
const f = new Float32Array(buf);
const u = new Uint32Array(buf);
const plan = speciesPlan(agentCount, shares);
const cx = fieldWidth / 2;
const cy = fieldHeight / 2;
const minDim = Math.min(fieldWidth, fieldHeight);
// clusters: one centre per species, arranged on an equilateral triangle
const clusterR = minDim * 0.3;
const clusterSpread = minDim * 0.12;
const clusterCentres = [0, 1, 2].map((s) => {
const a = -Math.PI / 2 + (s * 2 * Math.PI) / 3;
return { x: cx + Math.cos(a) * clusterR, y: cy + Math.sin(a) * clusterR };
});
const ringR = minDim * 0.35;
const centerR = minDim * 0.1;
const stride = AGENT_BYTE_STRIDE / 4; // 6 float slots
for (let i = 0; i < agentCount; i++) {
const o = i * stride;
const species = plan[i] ?? 0;
let x: number;
let y: number;
let angle = Math.random() * Math.PI * 2;
switch (pattern) {
case 'clusters': {
const c = clusterCentres[species] ?? { x: cx, y: cy };
x = c.x + (Math.random() - 0.5) * 2 * clusterSpread;
y = c.y + (Math.random() - 0.5) * 2 * clusterSpread;
break;
}
case 'center': {
const a = Math.random() * Math.PI * 2;
const r = Math.random() * centerR;
x = cx + Math.cos(a) * r;
y = cy + Math.sin(a) * r;
break;
}
case 'ring': {
const a = Math.random() * Math.PI * 2;
x = cx + Math.cos(a) * ringR;
y = cy + Math.sin(a) * ringR;
angle = a; // heading outward
break;
}
default: {
// 'random'
x = Math.random() * fieldWidth;
y = Math.random() * fieldHeight;
break;
}
}
f[o + 0] = Math.max(0, Math.min(fieldWidth, x));
f[o + 1] = Math.max(0, Math.min(fieldHeight, y));
f[o + 2] = angle;
u[o + 3] = species;
f[o + 4] = Math.random() * Math.PI * 2; // rhythmPhase
f[o + 5] = 0; // pad
}
return buf;
}