Phase 3: AR hunt /hunt + launch page /play - shared-visual ghosts, colour lure, hauntometer

This commit is contained in:
2026-06-23 11:19:47 +10:00
parent 6a5fb9d8c6
commit a6c45437c3
2 changed files with 15 additions and 12 deletions
+11 -10
View File
@@ -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);
+4 -2
View File
@@ -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 });