diff --git a/public/js/ghosts/behavior.js b/public/js/ghosts/behavior.js index 2256323..35b6e9e 100644 --- a/public/js/ghosts/behavior.js +++ b/public/js/ghosts/behavior.js @@ -12,6 +12,15 @@ const BOB_AMP_CM = 0.3; // 3mm const BOB_HZ = 0.4; const SHIMMER_CM = 0.3; // secondary float used by wander + path modes +/* Fallbacks for records that arrive without explicit behaviour params. The + * playlist normally supplies these (wanderRadius / wanderVertical / pathSpeed), + * so these only fire for hand-made or legacy records — but they still need to + * suit a ~25cm table, or an unconfigured ghost drifts clean off the display. */ +const WANDER_RADIUS_CM = 2.5; +const WANDER_VERTICAL_CM = 0.4; +const PATH_SPEED_CMS = 1.5; +const PATH_LOOKAHEAD_CM = 1.5; // distance sampled ahead to face travel direction + function hashNoise(seed, k) { // cheap deterministic pseudo-noise in [-1, 1] const x = Math.sin(seed * 127.1 + k * 311.7) * 43758.5453; @@ -27,14 +36,14 @@ export function ghostTransform(rec, nowMs, out) { pathTransform(b, t, out); } else if (b.type === 'wander') { const s = b.seed || 1; - const R = b.radius ?? 60; // cm + const R = b.radius ?? WANDER_RADIUS_CM; const v = b.speed ?? 0.15; // smooth pseudo-random orbit-drift: two incommensurate sines per axis const ph = t * v; out.position.set( base.x + R * 0.9 * Math.sin(ph * 1.0 + hashNoise(s, 1) * 6.28) * 0.7 + R * 0.4 * Math.sin(ph * 2.3 + hashNoise(s, 2) * 6.28) * 0.3, - base.y + (b.vertical ?? 10) * Math.sin(t * v * 1.3 + hashNoise(s, 3) * 6.28) + base.y + (b.vertical ?? WANDER_VERTICAL_CM) * Math.sin(t * v * 1.3 + hashNoise(s, 3) * 6.28) + SHIMMER_CM * Math.sin(t * 1.7 + hashNoise(s, 6) * 6.28), base.z + R * 0.9 * Math.cos(ph * 0.8 + hashNoise(s, 4) * 6.28) * 0.7 + R * 0.4 * Math.cos(ph * 1.9 + hashNoise(s, 5) * 6.28) * 0.3 @@ -92,10 +101,10 @@ function pathTransform(b, t, out) { return v; }; - const d = wrap(t * (b.speed ?? 8) + (b.phase || 0)); + const d = wrap(t * (b.speed ?? PATH_SPEED_CMS) + (b.phase || 0)); sample(d, out.position); - // face where we're heading: sample a few cm ahead so corners turn smoothly - const ahead = sample(wrap(t * (b.speed ?? 8) + (b.phase || 0) + 6), _look); + // face where we're heading: sample a short distance ahead so corners turn smoothly + const ahead = sample(wrap(t * (b.speed ?? PATH_SPEED_CMS) + (b.phase || 0) + PATH_LOOKAHEAD_CM), _look); const dx = ahead.x - out.position.x, dz = ahead.z - out.position.z; if (dx * dx + dz * dz > 1e-4) out.rotationY = Math.atan2(dx, dz); // gentle float so walking still reads ghostly