Phase 3 update: orbit ghosts, parallax, FX, shader fix [build v8]

This commit is contained in:
2026-06-23 14:53:36 +10:00
parent 70dc130647
commit 802ae7fab9
3 changed files with 343 additions and 26 deletions
+31 -12
View File
@@ -27,6 +27,7 @@ export function createHunt(THREE, scene, opts = {}) {
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 fxTextures = opts.fxTextures || null; // {glow, trail, smoke} THREE.Textures
const onEvent = opts.onEvent || (() => {});
let luredColor = null; // set by the colour wheel
@@ -55,15 +56,29 @@ export function createHunt(THREE, scene, opts = {}) {
return pool[pool.length - 1][0];
}
function placeInArc(obj) {
const half = (spawnArcDeg * Math.PI / 180) / 2;
const yaw = (Math.random() * 2 - 1) * half; // forward-facing arc
const pitch = (Math.random() * 0.5 - 0.15); // slightly varied height
const dist = 2.2 + Math.random() * 1.8; // 2.2-4m out
obj.position.set(
Math.sin(yaw) * dist,
0.2 + Math.sin(pitch) * 1.2,
-Math.cos(yaw) * dist
function placeOnRing(a) {
// Full 360° ring around the stationary player at roughly eye level.
// Each ghost gets orbit params so it slowly circles + bobs; the player
// stands still and pans to track them (works on iOS, no walking needed).
a.azimuth = Math.random() * Math.PI * 2; // start anywhere around
a.radius = 2.6 + Math.random() * 1.6; // 2.64.2m out
a.height = -0.3 + Math.random() * 1.0; // around eye level
a.orbitSpeed = (Math.random() < 0.5 ? -1 : 1) * (0.05 + Math.random() * 0.12); // rad/s, either direction
a.bobPhase = Math.random() * Math.PI * 2;
a.bobAmp = 0.1 + Math.random() * 0.2;
a.radiusPhase = Math.random() * Math.PI * 2;
a.radiusAmp = 0.2 + Math.random() * 0.4; // gentle in/out drift
positionFromOrbit(a, 0);
}
function positionFromOrbit(a, elapsed) {
const az = a.azimuth + a.orbitSpeed * elapsed;
const r = a.radius + Math.sin(elapsed * 0.5 + a.radiusPhase) * a.radiusAmp;
const y = a.height + Math.sin(elapsed * 1.2 + a.bobPhase) * a.bobAmp;
a.visual.object3d.position.set(
Math.sin(az) * r,
y,
-Math.cos(az) * r
);
}
@@ -71,16 +86,18 @@ export function createHunt(THREE, scene, opts = {}) {
if (active.size >= maxActive) return;
const g = pickGhost();
const visual = createGhostVisual(THREE, { color: g.color, fx: opts.fx !== false });
placeInArc(visual.object3d);
if (fxTextures) visual.attachFx(fxTextures);
scene.add(visual.object3d);
const id = nextId++;
// hp derived from health pips/stat; charge to capture scales with chargePips
const hp = (g.healthMax || 300) / 100; // ~2.6-4.5
active.set(id, {
const a = {
ghost: g, visual, hp, maxHp: hp,
state: 'idle',
fleeAt: performance.now() + (fleeMinMs + Math.random() * (fleeMaxMs - fleeMinMs)), // escapes if ignored
});
};
placeOnRing(a); // assign orbit + initial position
active.set(id, a);
haunt = Math.min(100, haunt + 4); // presence raises the meter
const p = visual.object3d.position;
onEvent({ type: 'spawn', id, ghost: g, haunt, pos: { x: +p.x.toFixed(2), y: +p.y.toFixed(2), z: +p.z.toFixed(2) } });
@@ -137,6 +154,8 @@ export function createHunt(THREE, scene, opts = {}) {
const now = performance.now();
for (const [id, a] of active) {
// orbit-and-drift around the stationary player
if (a.state !== 'capture') positionFromOrbit(a, elapsed);
a.visual.update(dt, elapsed);
if (now >= a.fleeAt) flee(id);
}