Files
hidden-spectre/js/ghost.js
T

150 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as THREE from "three";
// Palette of ghost tints (matching the supernatural neon aesthetic)
const GHOST_TINTS = [
{ core: 0x9fe6ff, glow: 0x2ffbe6, name: "WISP" },
{ core: 0xff9fd0, glow: 0xff2d8f, name: "POLTERGEIST" },
{ core: 0xd4ff9f, glow: 0xb6ff3c, name: "PHANTOM" },
{ core: 0xc9b3ff, glow: 0x9b6dff, name: "SHADE" },
];
export class Ghost {
constructor(scene, position) {
this.scene = scene;
this.tint = GHOST_TINTS[Math.floor(Math.random() * GHOST_TINTS.length)];
this.group = new THREE.Group();
this.group.position.copy(position);
this.basePos = position.clone();
this.captured = false;
this.escaped = false;
this.captureProgress = 0;
this.spawnTime = performance.now();
this.lifespan = 14000 + Math.random() * 8000; // 1422s before it flees
this.phase = Math.random() * Math.PI * 2;
this.bobSpeed = 0.6 + Math.random() * 0.5;
this.drift = new THREE.Vector3(
(Math.random() - 0.5) * 0.0006,
(Math.random() - 0.5) * 0.0003,
(Math.random() - 0.5) * 0.0006
);
this._build();
scene.add(this.group);
}
_build() {
const t = this.tint;
// Body — a softened rounded blob (sphere squished + tail)
const bodyGeo = new THREE.SphereGeometry(0.16, 24, 24);
bodyGeo.scale(1, 1.15, 1);
const bodyMat = new THREE.MeshBasicMaterial({
color: t.core, transparent: true, opacity: 0.55,
});
this.body = new THREE.Mesh(bodyGeo, bodyMat);
this.group.add(this.body);
// Inner glow core
const coreMat = new THREE.MeshBasicMaterial({
color: t.glow, transparent: true, opacity: 0.35,
});
const core = new THREE.Mesh(new THREE.SphereGeometry(0.1, 16, 16), coreMat);
core.position.y = 0.02;
this.group.add(core);
// Outer halo (sprite-like additive shell)
const haloMat = new THREE.MeshBasicMaterial({
color: t.glow, transparent: true, opacity: 0.12,
blending: THREE.AdditiveBlending, side: THREE.BackSide,
});
this.halo = new THREE.Mesh(new THREE.SphereGeometry(0.26, 20, 20), haloMat);
this.group.add(this.halo);
// Wispy tail tendrils
this.tendrils = [];
for (let i = 0; i < 3; i++) {
const tendril = new THREE.Mesh(
new THREE.ConeGeometry(0.05, 0.22, 8),
new THREE.MeshBasicMaterial({ color: t.core, transparent: true, opacity: 0.4 })
);
tendril.position.set((i - 1) * 0.06, -0.18, 0);
tendril.rotation.x = Math.PI;
this.group.add(tendril);
this.tendrils.push(tendril);
}
// Eyes (two dark voids)
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x05070f });
const eyeGeo = new THREE.SphereGeometry(0.022, 10, 10);
const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
const eyeR = new THREE.Mesh(eyeGeo, eyeMat);
eyeL.position.set(-0.05, 0.03, 0.14);
eyeR.position.set(0.05, 0.03, 0.14);
this.group.add(eyeL, eyeR);
}
update(dt, camera) {
if (this.captured || this.escaped) return;
const now = performance.now();
const age = now - this.spawnTime;
// Bob + sway
const tt = now * 0.001;
this.group.position.x = this.basePos.x + Math.sin(tt * this.bobSpeed + this.phase) * 0.05;
this.group.position.y = this.basePos.y + Math.sin(tt * this.bobSpeed * 1.3 + this.phase) * 0.06;
this.group.position.z = this.basePos.z + Math.cos(tt * this.bobSpeed + this.phase) * 0.05;
// Slow wandering drift
this.basePos.add(this.drift);
// Tendril wiggle
this.tendrils.forEach((td, i) => {
td.rotation.z = Math.sin(tt * 3 + i) * 0.3;
});
// Halo pulse
const pulse = 1 + Math.sin(tt * 2) * 0.08;
this.halo.scale.setScalar(pulse);
// Always face the camera-ish (billboard the eyes)
if (camera) this.group.lookAt(camera.position.x, this.group.position.y, camera.position.z);
// If being captured, shrink & spin
if (this.captureProgress > 0) {
const s = 1 - this.captureProgress * 0.5;
this.group.scale.setScalar(s);
this.group.rotation.y += dt * 6;
}
// Flee when lifespan exceeded
if (age > this.lifespan && this.captureProgress < 0.05) {
this.escaped = true;
}
}
// Returns the ghost's world position
getWorldPosition(target) {
return this.group.getWorldPosition(target);
}
setCaptureProgress(p) {
this.captureProgress = Math.min(1, Math.max(0, p));
}
capture() {
this.captured = true;
return this.tint.name;
}
dispose() {
this.scene.remove(this.group);
this.group.traverse((o) => {
if (o.geometry) o.geometry.dispose();
if (o.material) o.material.dispose();
});
}
}
export { GHOST_TINTS };