diff --git a/public/ghost-visual.js b/public/ghost-visual.js index 3cfd9bd..a1daddb 100644 --- a/public/ghost-visual.js +++ b/public/ghost-visual.js @@ -29,15 +29,15 @@ export const GHOST_GRADIENTS = { const WISP_VERT = ` varying float vY; - varying vec2 vUv; + varying vec3 vPos; uniform float uTime; void main() { - vUv = uv; vec3 p = position; // gentle billow so the wisp breathes float w = sin(uTime * 1.6 + position.y * 3.0) * 0.04; p.x += w * (0.5 + uv.y); vY = uv.y; + vPos = position; // object-space, for radial falloff in frag gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); } `; @@ -45,17 +45,16 @@ const WISP_VERT = ` const WISP_FRAG = ` precision mediump float; varying float vY; - varying vec2 vUv; + varying vec3 vPos; uniform vec3 uTop; uniform vec3 uBottom; uniform float uAlpha; uniform float uTime; void main() { - vec3 col = mix(uBottom, uTop, vY); // vertical gradient - // soft radial falloff around the body centreline for a wispy edge - float edge = smoothstep(0.5, 0.05, abs(vUv.x - 0.5)); - float fade = edge * (0.65 + 0.35 * sin(uTime * 2.0 + vY * 6.2831)); - float a = uAlpha * mix(0.45, 1.0, vY) * fade; // thinner at the tail + vec3 col = mix(uBottom, uTop, vY); // vertical gradient (head bright) + // breathing shimmer, never lets the body vanish + float shimmer = 0.78 + 0.22 * sin(uTime * 2.0 + vY * 6.2831); + float a = uAlpha * mix(0.55, 1.0, vY) * shimmer; gl_FragColor = vec4(col, a); } `; @@ -148,8 +147,10 @@ export function createGhostVisual(THREE, opts = {}) { function update(dt, elapsed) { uniforms.uTime.value = elapsed; stateT += dt; - // bob - group.position.y += Math.sin(elapsed * 1.8) * dt * 0.15; + // bob: offset the BODY around the group, so the group's spawn position + // stays fixed (previously this accumulated onto group.y and the ghost + // drifted up out of view every frame). + body.position.y = Math.sin(elapsed * 1.8) * 0.08; // billboard FX face camera handled by Sprite automatically if (state === 'spawn') { const k = Math.min(stateT / 0.6, 1); diff --git a/public/hunt.js b/public/hunt.js index cb92354..990ffb9 100644 --- a/public/hunt.js +++ b/public/hunt.js @@ -24,7 +24,9 @@ const COLORS = ['Red', 'Blue', 'Yellow']; export function createHunt(THREE, scene, opts = {}) { const roster = opts.roster || []; const maxActive = opts.maxActive ?? 4; - const spawnArcDeg = opts.spawnArcDeg ?? 120; // ±60° + const spawnArcDeg = opts.spawnArcDeg ?? 90; // ±45° — tighter so ghosts land in front + const fleeMinMs = opts.fleeMinMs ?? 12000; // longer discovery window + const fleeMaxMs = opts.fleeMaxMs ?? 18000; const onEvent = opts.onEvent || (() => {}); let luredColor = null; // set by the colour wheel @@ -77,7 +79,7 @@ export function createHunt(THREE, scene, opts = {}) { active.set(id, { ghost: g, visual, hp, maxHp: hp, state: 'idle', - fleeAt: performance.now() + (6000 + Math.random() * 4000), // escapes if ignored + fleeAt: performance.now() + (fleeMinMs + Math.random() * (fleeMaxMs - fleeMinMs)), // escapes if ignored }); haunt = Math.min(100, haunt + 4); // presence raises the meter onEvent({ type: 'spawn', id, ghost: g, haunt });