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

This commit is contained in:
2026-06-23 11:12:44 +10:00
parent efadc85195
commit 6a5fb9d8c6
54 changed files with 9630 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
/**
* ghost-visual.js — Newbury Nights shared ghost visual (Phase 2)
*
* Replicates the original's architecture: ONE shared base body, recoloured by a
* per-colour gradient (recovered from GhostType_* assets). No per-ghost models.
*
* Tier 0 (always): procedural wisp mesh + vertical gradient material.
* Tier 1 (progressive enhancement): FX billboards (trail / glow / smoke)
* layered on top when textures + GPU budget allow.
* Special: two roster ghosts can use real meshes (Ghost_Baseball/Bat) via
* the `meshLoader` hook.
*
* Engine-agnostic about your spawn/capture logic — this only produces the
* Object3D you place in the scene. Drop into your Three.js r160 app.
*
* Usage:
* import { createGhostVisual, GHOST_GRADIENTS } from './ghost-visual.js';
* const g = createGhostVisual(THREE, { color: 'Red', fx: true });
* scene.add(g.object3d);
* // each frame:
* g.update(dt, elapsed);
*/
export const GHOST_GRADIENTS = {
Red: { top: '#F65151', bottom: '#FF2678' }, // GhostType_Angry
Yellow: { top: '#B57F0B', bottom: '#FFF35D' }, // GhostType_Crazy
Blue: { top: '#529EFF', bottom: '#51EAF1' }, // GhostType_Sad
};
const WISP_VERT = `
varying float vY;
varying vec2 vUv;
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;
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
`;
const WISP_FRAG = `
precision mediump float;
varying float vY;
varying vec2 vUv;
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
gl_FragColor = vec4(col, a);
}
`;
function hexToRGB(THREE, hex) { return new THREE.Color(hex); }
/**
* Build the shared wisp body (a tapered, double-sided plane-ish blob).
* Kept cheap: a subdivided cone/teardrop so the gradient + billow read well.
*/
function buildWispGeometry(THREE) {
// teardrop: radius shrinks toward the tail (top in UV space = head)
const geo = new THREE.CylinderGeometry(0.0, 0.5, 1.2, 18, 8, true);
geo.translate(0, 0.1, 0);
return geo;
}
export function createGhostVisual(THREE, opts = {}) {
const color = opts.color && GHOST_GRADIENTS[opts.color] ? opts.color : 'Blue';
const grad = GHOST_GRADIENTS[color];
const wantFx = opts.fx !== false;
const group = new THREE.Group();
group.name = `ghost-${color}`;
// --- Tier 0: wisp body + gradient material ---
const uniforms = {
uTime: { value: 0 },
uTop: { value: hexToRGB(THREE, grad.top) },
uBottom: { value: hexToRGB(THREE, grad.bottom) },
uAlpha: { value: 1.0 },
};
const mat = new THREE.ShaderMaterial({
vertexShader: WISP_VERT,
fragmentShader: WISP_FRAG,
uniforms,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
blending: THREE.NormalBlending,
});
const body = new THREE.Mesh(buildWispGeometry(THREE), mat);
group.add(body);
// --- Tier 1: FX billboards (progressive enhancement) ---
const fxSprites = [];
function addFxBillboard(texture, scale, yOffset, opacity, blend) {
const m = new THREE.SpriteMaterial({
map: texture,
color: new THREE.Color(grad.top),
transparent: true,
opacity,
depthWrite: false,
blending: blend ?? THREE.AdditiveBlending,
});
const s = new THREE.Sprite(m);
s.scale.setScalar(scale);
s.position.y = yOffset;
group.add(s);
fxSprites.push(s);
return s;
}
// caller passes a loaded-texture map when fx enabled; see attachFx()
let fxAttached = false;
function attachFx(textures) {
if (!wantFx || fxAttached || !textures) return;
if (textures.glow) addFxBillboard(textures.glow, 1.6, 0.1, 0.5);
if (textures.trail) addFxBillboard(textures.trail, 1.2, -0.4, 0.35);
fxAttached = true;
}
// --- optional special mesh override (Ghost_Baseball / Ghost_Bat) ---
function useSpecialMesh(mesh) {
if (!mesh) return;
body.visible = false;
mesh.traverse?.(o => {
if (o.isMesh && o.material) {
o.material.color = new THREE.Color(grad.top);
}
});
group.add(mesh);
}
// --- spawn / capture animation helpers ---
let state = 'idle';
let stateT = 0;
function setState(s) { state = s; stateT = 0; }
function update(dt, elapsed) {
uniforms.uTime.value = elapsed;
stateT += dt;
// bob
group.position.y += Math.sin(elapsed * 1.8) * dt * 0.15;
// billboard FX face camera handled by Sprite automatically
if (state === 'spawn') {
const k = Math.min(stateT / 0.6, 1);
group.scale.setScalar(k);
uniforms.uAlpha.value = k;
if (k >= 1) setState('idle');
} else if (state === 'capture') {
const k = Math.min(stateT / 0.5, 1);
group.scale.setScalar(1 - k);
uniforms.uAlpha.value = 1 - k;
}
}
// start hidden, play spawn
group.scale.setScalar(0);
setState('spawn');
return {
object3d: group,
color,
update,
setState, // 'idle' | 'spawn' | 'capture'
attachFx, // call with {glow, trail, smoke} THREE.Textures
useSpecialMesh, // call with a loaded Ghost_Baseball/Bat mesh
setColor(next) {
const g2 = GHOST_GRADIENTS[next]; if (!g2) return;
uniforms.uTop.value.set(g2.top);
uniforms.uBottom.value.set(g2.bottom);
fxSprites.forEach(s => s.material.color.set(g2.top));
},
dispose() {
body.geometry?.dispose?.(); mat.dispose?.();
fxSprites.forEach(s => s.material?.map?.dispose?.());
},
};
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because it is too large Load Diff
+767
View File
@@ -0,0 +1,767 @@
g Ghost_Bat
v 0.23093598 1.6888598 9.536743E-09
v 0.16428131 1.7850729 9.536743E-09
v 0.14227325 1.7850726 -0.08214095
v 0.19999602 1.6888598 -0.115468964
v 0.23055358 0.080041505 9.536743E-09
v 0.0821405 1.7850729 -0.14227232
v 0.19966583 0.080041505 -0.11527636
v 0.21243744 -0.14647491 9.536743E-09
v 0.11546661 1.6888598 -0.19999824
v -0 1.7850729 -0.16428192
v 0.18397796 -0.14647491 -0.10621962
v 0.14700073 -0.6485732 9.536743E-09
v 3.0517577E-07 1.6888598 -0.23093818
v -0.0821405 1.7850729 -0.14227232
v 0.11527374 0.080041505 -0.19966449
v 0.12730224 -0.6485732 -0.07349921
v 0.116545714 -1.1276284 9.536743E-09
v -0.115467526 1.6888598 -0.19999824
v -0.14227447 1.7850726 -0.08214095
v 0.106221005 -0.14647491 -0.18397777
v 3.0517577E-07 0.080041505 -0.2305527
v 0.100928344 -1.1276284 -0.058271825
v 0.138067 -1.6819524 9.536743E-09
v 0.11956604 -1.6819524 -0.06903267
v -0.19999634 1.6888598 -0.115468964
v -0.16428192 1.7850729 9.536743E-09
v -0.23093536 1.6888598 9.536743E-09
v 0.073499754 -0.64857316 -0.12730438
v 0.058272704 -1.1276284 -0.100929715
v 0.06903198 -1.6819524 -0.119568124
v 3.0517577E-07 -0.14647491 -0.21243922
v -0.11527435 0.080041505 -0.19966449
v -0.19966644 0.080041505 -0.11527636
v -0.23055267 0.080041505 9.536743E-09
v 3.0517577E-07 -1.1276284 -0.11654369
v 3.0517577E-07 -1.6819524 -0.13806534
v 3.0517577E-07 -0.6485732 -0.14699839
v -0.10622131 -0.14647491 -0.18397777
v -0.18397857 -0.14647491 -0.10621962
v -0.21243712 -0.14647491 9.536743E-09
v -0.073500365 -0.64857316 -0.12730438
v -0.058272704 -1.1276284 -0.100929715
v -0.06903137 -1.6819524 -0.119568124
v -0.12730254 -0.6485732 -0.07349921
v -0.14700073 -0.6485732 9.536743E-09
v -0.100928344 -1.1276284 -0.058271825
v -0.11956573 -1.6819524 -0.06903267
v -0.116545714 -1.1276284 9.536743E-09
v -0.13806671 -1.6819524 9.536743E-09
v -0.13806671 -1.6819524 9.536743E-09
v -0.23737335 -1.7254586 9.536743E-09
v -0.20557372 -1.7254586 -0.11868828
v -0.11956573 -1.6819524 -0.06903267
v -0.1186911 -1.7254586 -0.20557404
v -0.06903137 -1.6819524 -0.119568124
v 3.0517577E-07 -1.7254586 -0.23737656
v 3.0517577E-07 -1.6819524 -0.13806534
v 0.11868805 -1.7254586 -0.20557404
v 0.06903198 -1.6819524 -0.119568124
v 0.20557372 -1.7254586 -0.118688464
v 0.11956604 -1.6819524 -0.06903267
v 0.23737335 -1.7254586 9.536743E-09
v 0.138067 -1.6819524 9.536743E-09
v -0.23737335 -1.7254586 9.536743E-09
v -0.23737335 -1.8253057 9.536743E-09
v -0.20557372 -1.8253057 -0.11868828
v -0.20557372 -1.7254586 -0.11868828
v -0.1186911 -1.8253057 -0.20557404
v -0.1186911 -1.7254586 -0.20557404
v 3.0517577E-07 -1.8253057 -0.23737656
v 3.0517577E-07 -1.7254586 -0.23737656
v 0.11868805 -1.8253057 -0.20557404
v 0.11868805 -1.7254586 -0.20557404
v 0.20557372 -1.8253057 -0.118688464
v 0.20557372 -1.7254586 -0.118688464
v 0.23737335 -1.8253057 9.536743E-09
v 0.23737335 -1.7254586 9.536743E-09
v -0.14227447 1.7850726 -0.08214095
v 3.0517577E-07 1.825306 9.536743E-09
v -0.16428192 1.7850729 9.536743E-09
v -0.0821405 1.7850729 -0.14227232
v -0 1.7850729 -0.16428192
v 0.0821405 1.7850729 -0.14227232
v 0.14227325 1.7850726 -0.08214095
v 0.16428131 1.7850729 9.536743E-09
v 0.14227325 1.7850729 0.08214097
v 0.16428131 1.7850729 9.536743E-09
v 0.23093598 1.6888598 9.536743E-09
v 0.19999602 1.6888598 0.11546906
v 0.0821405 1.7850726 0.14227243
v 0.23055358 0.080041505 9.536743E-09
v 0.11546661 1.6888598 0.19999824
v -0 1.7850726 0.164282
v 0.19966583 0.080041505 0.115276344
v 0.21243744 -0.14647491 9.536743E-09
v 3.0517577E-07 1.6888598 0.2309381
v -0.0821405 1.7850726 0.14227243
v 0.18397796 -0.14647476 0.10621964
v 0.14700073 -0.6485732 9.536743E-09
v 0.11527374 0.080041505 0.19966446
v -0.115467526 1.6888598 0.19999824
v -0.14227447 1.7850729 0.08214097
v 0.12730224 -0.6485732 0.07349923
v 0.116545714 -1.1276284 9.536743E-09
v 0.106221005 -0.14647491 0.18397777
v 3.0517577E-07 0.080041505 0.23055267
v -0.19999634 1.6888598 0.11546906
v -0.16428192 1.7850729 9.536743E-09
v -0.23093536 1.6888598 9.536743E-09
v 0.100928344 -1.1276284 0.05827185
v 0.138067 -1.6819524 9.536743E-09
v 0.11956604 -1.6819524 0.069032684
v -0.11527435 0.080041505 0.19966446
v -0.19966644 0.080041505 0.115276344
v -0.23055267 0.080041505 9.536743E-09
v 3.0517577E-07 -0.14647491 0.21243927
v 0.073499754 -0.6485732 0.12730438
v 0.058272704 -1.1276284 0.10092972
v 0.06903198 -1.6819524 0.11956815
v -0.10622131 -0.14647491 0.18397777
v -0.18397857 -0.14647476 0.10621964
v -0.21243712 -0.14647491 9.536743E-09
v 3.0517577E-07 -0.6485732 0.14699844
v 3.0517577E-07 -1.1276283 0.11654372
v 3.0517577E-07 -1.6819524 0.13806537
v -0.073500365 -0.6485732 0.12730438
v -0.12730254 -0.6485732 0.07349923
v -0.14700073 -0.6485732 9.536743E-09
v -0.058272704 -1.1276284 0.10092972
v -0.06903137 -1.6819524 0.11956815
v -0.100928344 -1.1276284 0.05827185
v -0.116545714 -1.1276284 9.536743E-09
v -0.11956573 -1.6819524 0.069032684
v -0.13806671 -1.6819524 9.536743E-09
v -0.20557372 -1.7254586 0.11868829
v -0.23737335 -1.7254586 9.536743E-09
v -0.13806671 -1.6819524 9.536743E-09
v -0.11956573 -1.6819524 0.069032684
v -0.1186911 -1.7254586 0.20557404
v -0.06903137 -1.6819524 0.11956815
v 3.0517577E-07 -1.7254586 0.23737656
v 3.0517577E-07 -1.6819524 0.13806537
v 0.11868805 -1.7254586 0.20557404
v 0.06903198 -1.6819524 0.11956815
v 0.20557372 -1.7254586 0.11868849
v 0.11956604 -1.6819524 0.069032684
v 0.23737335 -1.7254586 9.536743E-09
v 0.138067 -1.6819524 9.536743E-09
v -0.20557372 -1.8253057 0.118688285
v -0.23737335 -1.8253057 9.536743E-09
v -0.23737335 -1.7254586 9.536743E-09
v -0.20557372 -1.7254586 0.11868829
v -0.1186911 -1.8253057 0.20557404
v -0.1186911 -1.7254586 0.20557404
v 3.0517577E-07 -1.8253057 0.23737656
v 3.0517577E-07 -1.7254586 0.23737656
v 0.11868805 -1.8253057 0.20557404
v 0.11868805 -1.7254586 0.20557404
v 0.20557372 -1.8253057 0.118688494
v 0.20557372 -1.7254586 0.11868849
v 0.23737335 -1.8253057 9.536743E-09
v 0.23737335 -1.7254586 9.536743E-09
v 3.0517577E-07 1.825306 9.536743E-09
v -0.14227447 1.7850729 0.08214097
v -0.16428192 1.7850729 9.536743E-09
v -0.0821405 1.7850726 0.14227243
v -0 1.7850726 0.164282
v 0.0821405 1.7850726 0.14227243
v 0.14227325 1.7850729 0.08214097
v 0.16428131 1.7850729 9.536743E-09
v -9.155273E-07 -1.825306 9.536743E-09
v 0.20557372 -1.8253057 0.118688494
v 0.23737335 -1.8253057 9.536743E-09
v 0.20557372 -1.8253057 -0.118688464
v 0.11868805 -1.8253057 0.20557404
v 0.11868805 -1.8253057 -0.20557404
v 3.0517577E-07 -1.8253057 0.23737656
v 3.0517577E-07 -1.8253057 -0.23737656
v -0.1186911 -1.8253057 0.20557404
v -0.1186911 -1.8253057 -0.20557404
v -0.20557372 -1.8253057 0.118688285
v -0.20557372 -1.8253057 -0.11868828
v -0.23737335 -1.8253057 9.536743E-09
vt 0.033712 0.545644
vt 0.020241 0.536712
vt 0.020241 0.521679
vt 0.033712 0.526721
vt 0.292339 0.546247
vt 0.020241 0.505521
vt 0.292339 0.527201
vt 0.331103 0.541357
vt 0.033712 0.507875
vt 0.020241 0.489027
vt 0.331103 0.52395
vt 0.41101 0.518695
vt 0.033712 0.489051
vt 0.020241 0.472557
vt 0.292339 0.508151
vt 0.41101 0.508839
vt 0.48663 0.510622
vt 0.033712 0.470215
vt 0.020241 0.456432
vt 0.331103 0.506483
vt 0.292339 0.489101
vt 0.48663 0.503249
vt 0.577956 0.515712
vt 0.577956 0.506448
vt 0.033712 0.451342
vt 0.020241 0.440988
vt 0.033712 0.432426
vt 0.41101 0.498874
vt 0.48663 0.495866
vt 0.577956 0.49719
vt 0.331103 0.489005
vt 0.292339 0.47005
vt 0.292339 0.450989
vt 0.292339 0.431904
vt 0.48663 0.488476
vt 0.577956 0.487934
vt 0.41101 0.488858
vt 0.331103 0.471548
vt 0.331103 0.454152
vt 0.331103 0.436904
vt 0.41101 0.478845
vt 0.48663 0.481084
vt 0.577956 0.478676
vt 0.41101 0.468891
vt 0.41101 0.459045
vt 0.48663 0.473692
vt 0.577956 0.469409
vt 0.48663 0.466304
vt 0.577956 0.460131
vt 0.321383 0.559963
vt 0.32882 0.574769
vt 0.312915 0.574769
vt 0.309785 0.559963
vt 0.297487 0.574769
vt 0.296334 0.559963
vt 0.282349 0.574769
vt 0.282115 0.559963
vt 0.267131 0.574769
vt 0.267854 0.559963
vt 0.251542 0.574769
vt 0.254327 0.559962
vt 0.235757 0.574769
vt 0.243116 0.559963
vt 0.32882 0.574769
vt 0.32882 0.589843
vt 0.312671 0.589843
vt 0.312915 0.574769
vt 0.297284 0.589843
vt 0.297487 0.574769
vt 0.282726 0.589843
vt 0.282349 0.574769
vt 0.26791 0.589843
vt 0.267131 0.574769
vt 0.252166 0.589843
vt 0.251542 0.574769
vt 0.235757 0.589843
vt 0.235757 0.574769
vt 0.037127 0.260495
vt 0.011751 0.297588
vt 0.011751 0.253695
vt 0.05252 0.277233
vt 0.057481 0.299426
vt 0.050682 0.321125
vt 0.033943 0.336518
vt 0.011751 0.34148
vt 0.020241 0.521679
vt 0.020241 0.536712
vt 0.033712 0.545644
vt 0.033712 0.526721
vt 0.020241 0.505521
vt 0.292339 0.546247
vt 0.033712 0.507875
vt 0.020241 0.489027
vt 0.292339 0.527201
vt 0.331103 0.541357
vt 0.033712 0.489051
vt 0.020241 0.472557
vt 0.331103 0.52395
vt 0.41101 0.518695
vt 0.292339 0.508151
vt 0.033712 0.470215
vt 0.020241 0.456432
vt 0.41101 0.508839
vt 0.48663 0.510622
vt 0.331103 0.506483
vt 0.292339 0.489101
vt 0.033712 0.451342
vt 0.020241 0.440988
vt 0.033712 0.432426
vt 0.48663 0.503249
vt 0.577956 0.515712
vt 0.577956 0.506448
vt 0.292339 0.47005
vt 0.292339 0.450989
vt 0.292339 0.431904
vt 0.331103 0.489005
vt 0.41101 0.498874
vt 0.48663 0.495866
vt 0.577956 0.49719
vt 0.331103 0.471548
vt 0.331103 0.454152
vt 0.331103 0.436904
vt 0.41101 0.488858
vt 0.48663 0.488476
vt 0.577956 0.487934
vt 0.41101 0.478845
vt 0.41101 0.468891
vt 0.41101 0.459045
vt 0.48663 0.481084
vt 0.577956 0.478676
vt 0.48663 0.473692
vt 0.48663 0.466304
vt 0.577956 0.469409
vt 0.577956 0.460131
vt 0.312913 0.574766
vt 0.328818 0.574766
vt 0.321381 0.559959
vt 0.309783 0.559959
vt 0.297486 0.574766
vt 0.296332 0.559959
vt 0.282347 0.574766
vt 0.282113 0.559959
vt 0.267129 0.574766
vt 0.267852 0.559959
vt 0.25154 0.574766
vt 0.254325 0.559959
vt 0.235755 0.574766
vt 0.243114 0.559959
vt 0.312669 0.589839
vt 0.328818 0.589839
vt 0.328818 0.574766
vt 0.312913 0.574766
vt 0.297282 0.589839
vt 0.297486 0.574766
vt 0.282724 0.589839
vt 0.282347 0.574766
vt 0.267908 0.589839
vt 0.267129 0.574766
vt 0.252164 0.58984
vt 0.25154 0.574766
vt 0.235755 0.589839
vt 0.235755 0.574766
vt 0.011751 0.297588
vt 0.037127 0.260495
vt 0.011751 0.253695
vt 0.05252 0.277233
vt 0.057481 0.299426
vt 0.050682 0.321125
vt 0.033943 0.336518
vt 0.011751 0.34148
vt 0.649191 0.419149
vt 0.665469 0.390954
vt 0.649191 0.386592
vt 0.632912 0.390954
vt 0.677386 0.402871
vt 0.620995 0.402871
vt 0.681748 0.419149
vt 0.616634 0.419149
vt 0.677386 0.435428
vt 0.620995 0.435428
vt 0.665469 0.447344
vt 0.632912 0.447344
vt 0.649191 0.451706
vn 0.9585068 0.28506985 0
vn 0.6041495 0.79687095 0
vn 0.5232121 0.79686904 -0.30207404
vn 0.8300891 0.28507143 -0.479256
vn 0.99920964 -0.0397502 0
vn 0.30207363 0.79687333 -0.52320594
vn 0.8653401 -0.039747465 -0.49960646
vn 0.9945681 -0.10408779 0
vn 0.4792509 0.2850737 -0.83009124
vn 4.3355183E-07 0.79687506 -0.6041442
vn 0.8613218 -0.10408532 -0.4972835
vn 0.99529195 -0.09692217 0
vn 1.2513581E-06 0.2850765 -0.95850474
vn -0.30207178 0.7968739 -0.5232061
vn 0.49960172 -0.039745323 -0.865343
vn 0.8619444 -0.09692138 -0.4976526
vn 0.9999225 -0.012446101 0
vn -0.47925013 0.28507364 -0.8300918
vn -0.5232138 0.7968682 -0.30207342
vn 0.49728736 -0.1040834 -0.8613199
vn 1.2316204E-06 -0.03974624 -0.99920976
vn 0.86595005 -0.012445819 -0.49997568
vn 0.9992472 0.038795333 0
vn 0.86536473 0.038794216 -0.4996387
vn -0.83009106 0.28507048 -0.47925323
vn -0.6041511 0.79686975 0
vn -0.95850754 0.2850673 0
vn 0.49765417 -0.09692319 -0.8619433
vn 0.49997118 -0.012445979 -0.86595255
vn 0.49963182 0.03879506 -0.86536866
vn 1.2029865E-06 -0.10408348 -0.9945686
vn -0.49960047 -0.03974562 -0.8653437
vn -0.86534244 -0.039747626 -0.49960238
vn -0.99920964 -0.039749898 0
vn 1.1310963E-06 -0.012445888 -0.9999225
vn 5.5348755E-07 0.03879548 -0.9992472
vn 1.514343E-06 -0.09692247 -0.99529195
vn -0.4972856 -0.10408365 -0.86132085
vn -0.8613239 -0.104085565 -0.49727997
vn -0.9945682 -0.10408735 0
vn -0.49765322 -0.09692343 -0.86194384
vn -0.4999705 -0.012446378 -0.865953
vn -0.49963075 0.038794797 -0.8653693
vn -0.86194605 -0.09692179 -0.4976497
vn -0.99529195 -0.096922286 0
vn -0.8659506 -0.012446293 -0.49997464
vn -0.8653645 0.03879403 -0.4996392
vn -0.9999225 -0.012446395 0
vn -0.9992472 0.038795333 0
vn -0.40126997 0.9159598 0
vn -0.40126997 0.9159598 0
vn -0.34750712 0.915962 -0.20063005
vn -0.3475068 0.915962 -0.20063078
vn -0.20063345 0.91596407 -0.3474998
vn -0.2006302 0.91596407 -0.34750158
vn 8.6672554E-07 0.9159622 -0.40126446
vn 1.1384262E-06 0.9159622 -0.4012645
vn 0.20063318 0.9159625 -0.34750405
vn 0.2006331 0.9159625 -0.34750408
vn 0.34750792 0.91596097 -0.20063308
vn 0.3475075 0.91596097 -0.20063396
vn 0.4012704 0.9159596 0
vn 0.4012704 0.9159596 0
vn -1 0 0
vn -1 0 0
vn -0.8660355 0 -0.49998257
vn -0.8660355 0 -0.49998257
vn -0.5000049 0 -0.86602265
vn -0.5000049 0 -0.86602265
vn 3.6730592E-06 0 -1
vn 3.6730592E-06 0 -1
vn 0.5000007 0 -0.866025
vn 0.5000007 0 -0.866025
vn 0.86603063 0 -0.49999094
vn 0.86603063 0 -0.49999094
vn 1 0 0
vn 1 0 0
vn -0.5232138 0.7968682 -0.30207342
vn 9.2884443E-07 1 0
vn -0.6041511 0.79686975 0
vn -0.30207178 0.7968739 -0.5232061
vn 4.3355183E-07 0.79687506 -0.6041442
vn 0.30207363 0.79687333 -0.52320594
vn 0.5232121 0.79686904 -0.30207404
vn 0.6041495 0.79687095 0
vn 0.52321213 0.79686916 0.30207407
vn 0.6041495 0.79687095 0
vn 0.9585068 0.28506985 0
vn 0.8300891 0.28507143 0.47925606
vn 0.3020736 0.7968734 0.52320594
vn 0.99920964 -0.0397502 0
vn 0.4792509 0.28507373 0.8300913
vn 4.3222195E-07 0.79687506 0.6041442
vn 0.8653401 -0.03974747 0.49960646
vn 0.9945681 -0.10408779 0
vn 1.2406169E-06 0.2850765 0.95850474
vn -0.30207178 0.7968739 0.5232061
vn 0.8613218 -0.10408533 0.4972835
vn 0.99529195 -0.09692217 0
vn 0.49960172 -0.039745323 0.865343
vn -0.47925013 0.28507364 0.8300918
vn -0.5232138 0.7968682 0.30207345
vn 0.8619444 -0.09692139 0.49765265
vn 0.9999225 -0.012446101 0
vn 0.49728736 -0.1040834 0.8613199
vn 1.2217279E-06 -0.03974624 0.99920976
vn -0.83009106 0.28507048 0.47925323
vn -0.6041511 0.79686975 0
vn -0.95850754 0.2850673 0
vn 0.86595005 -0.012445819 0.49997568
vn 0.9992472 0.038795333 0
vn 0.8653647 0.038794212 0.49963874
vn -0.49960047 -0.03974562 0.8653437
vn -0.86534244 -0.039747626 0.49960238
vn -0.99920964 -0.039749898 0
vn 1.2029865E-06 -0.10408348 0.9945686
vn 0.49765417 -0.09692319 0.8619433
vn 0.49997118 -0.012445979 0.86595255
vn 0.49963182 0.03879506 0.86536866
vn -0.4972856 -0.10408365 0.86132085
vn -0.8613239 -0.104085565 0.49727997
vn -0.9945682 -0.10408735 0
vn 1.5045731E-06 -0.09692247 0.99529195
vn 1.1213455E-06 -0.012445888 0.9999225
vn 5.5348755E-07 0.03879548 0.9992472
vn -0.49765322 -0.09692343 0.86194384
vn -0.86194605 -0.09692179 0.4976497
vn -0.99529195 -0.096922286 0
vn -0.4999705 -0.0124463765 0.865953
vn -0.49963075 0.038794797 0.8653693
vn -0.8659506 -0.012446293 0.4999747
vn -0.9999225 -0.012446395 0
vn -0.8653645 0.038794033 0.4996392
vn -0.9992472 0.038795333 0
vn -0.34750712 0.915962 0.20063005
vn -0.40126997 0.9159598 0
vn -0.40126997 0.9159598 0
vn -0.3475068 0.915962 0.20063078
vn -0.20063345 0.91596407 0.3474998
vn -0.2006302 0.91596407 0.34750158
vn 8.7235355E-07 0.9159622 0.4012645
vn 1.1425659E-06 0.9159622 0.4012645
vn 0.20063315 0.9159625 0.34750405
vn 0.2006331 0.9159625 0.34750414
vn 0.34750792 0.91596097 0.20063308
vn 0.34750748 0.9159611 0.20063396
vn 0.4012704 0.9159596 0
vn 0.4012704 0.9159596 0
vn -0.8660355 0 0.49998257
vn -1 0 0
vn -1 0 0
vn -0.8660355 0 0.49998257
vn -0.5000049 0 0.86602265
vn -0.5000049 0 0.86602265
vn 3.6730592E-06 0 1
vn 3.6730592E-06 0 1
vn 0.5000007 0 0.866025
vn 0.5000007 0 0.866025
vn 0.86603063 0 0.49999094
vn 0.86603063 0 0.49999094
vn 1 0 0
vn 1 0 0
vn 9.2884443E-07 1 0
vn -0.5232138 0.7968682 0.30207345
vn -0.6041511 0.79686975 0
vn -0.30207178 0.7968739 0.5232061
vn 4.3222195E-07 0.79687506 0.6041442
vn 0.3020736 0.7968734 0.52320594
vn 0.52321213 0.79686916 0.30207407
vn 0.6041495 0.79687095 0
vn -0 -1 0
vn 1.2372466E-06 -1 5.2292353E-07
vn 1.4286578E-06 -1 0
vn 1.2372466E-06 -1 -5.2292353E-07
vn 5.2291966E-07 -1 1.2372409E-06
vn 5.2291966E-07 -1 -1.2372409E-06
vn -0 -1 1.4286439E-06
vn -0 -1 -1.4286439E-06
vn -6.1863193E-07 -1 1.0714709E-06
vn -6.1863193E-07 -1 -1.0714709E-06
vn -1.2372628E-06 -1 7.142983E-07
vn -1.2372628E-06 -1 -7.142983E-07
vn -1.2372628E-06 -1 0
g Ghost_Bat_0
f 3/3/3 2/2/2 1/1/1
f 4/4/4 3/3/3 1/1/1
f 4/4/4 1/1/1 5/5/5
f 6/6/6 3/3/3 4/4/4
f 7/7/7 4/4/4 5/5/5
f 7/7/7 5/5/5 8/8/8
f 9/9/9 6/6/6 4/4/4
f 9/9/9 4/4/4 7/7/7
f 10/10/10 6/6/6 9/9/9
f 11/11/11 7/7/7 8/8/8
f 11/11/11 8/8/8 12/12/12
f 13/13/13 10/10/10 9/9/9
f 14/14/14 10/10/10 13/13/13
f 15/15/15 9/9/9 7/7/7
f 15/15/15 7/7/7 11/11/11
f 13/13/13 9/9/9 15/15/15
f 16/16/16 11/11/11 12/12/12
f 16/16/16 12/12/12 17/17/17
f 18/18/18 14/14/14 13/13/13
f 19/19/19 14/14/14 18/18/18
f 20/20/20 15/15/15 11/11/11
f 20/20/20 11/11/11 16/16/16
f 21/21/21 13/13/13 15/15/15
f 18/18/18 13/13/13 21/21/21
f 21/21/21 15/15/15 20/20/20
f 22/22/22 16/16/16 17/17/17
f 22/22/22 17/17/17 23/23/23
f 24/24/24 22/22/22 23/23/23
f 25/25/25 19/19/19 18/18/18
f 26/26/26 19/19/19 25/25/25
f 27/27/27 26/26/26 25/25/25
f 28/28/28 20/20/20 16/16/16
f 28/28/28 16/16/16 22/22/22
f 29/29/29 22/22/22 24/24/24
f 29/29/29 28/28/28 22/22/22
f 30/30/30 29/29/29 24/24/24
f 31/31/31 21/21/21 20/20/20
f 31/31/31 20/20/20 28/28/28
f 32/32/32 18/18/18 21/21/21
f 25/25/25 18/18/18 32/32/32
f 32/32/32 21/21/21 31/31/31
f 27/27/27 25/25/25 33/33/33
f 33/33/33 25/25/25 32/32/32
f 34/34/34 27/27/27 33/33/33
f 35/35/35 29/29/29 30/30/30
f 36/36/36 35/35/35 30/30/30
f 37/37/37 28/28/28 29/29/29
f 37/37/37 31/31/31 28/28/28
f 35/35/35 37/37/37 29/29/29
f 38/38/38 32/32/32 31/31/31
f 33/33/33 32/32/32 38/38/38
f 38/38/38 31/31/31 37/37/37
f 34/34/34 33/33/33 39/39/39
f 39/39/39 33/33/33 38/38/38
f 40/40/40 34/34/34 39/39/39
f 41/41/41 38/38/38 37/37/37
f 41/41/41 37/37/37 35/35/35
f 39/39/39 38/38/38 41/41/41
f 42/42/42 35/35/35 36/36/36
f 42/42/42 41/41/41 35/35/35
f 43/43/43 42/42/42 36/36/36
f 40/40/40 39/39/39 44/44/44
f 44/44/44 39/39/39 41/41/41
f 44/44/44 41/41/41 42/42/42
f 45/45/45 40/40/40 44/44/44
f 46/46/46 42/42/42 43/43/43
f 46/46/46 44/44/44 42/42/42
f 45/45/45 44/44/44 46/46/46
f 47/47/47 46/46/46 43/43/43
f 48/48/48 45/45/45 46/46/46
f 48/48/48 46/46/46 47/47/47
f 49/49/49 48/48/48 47/47/47
f 52/52/52 51/51/51 50/50/50
f 53/53/53 52/52/52 50/50/50
f 54/54/54 52/52/52 53/53/53
f 55/55/55 54/54/54 53/53/53
f 56/56/56 54/54/54 55/55/55
f 57/57/57 56/56/56 55/55/55
f 58/58/58 56/56/56 57/57/57
f 59/59/59 58/58/58 57/57/57
f 60/60/60 58/58/58 59/59/59
f 61/61/61 60/60/60 59/59/59
f 62/62/62 60/60/60 61/61/61
f 63/63/63 62/62/62 61/61/61
f 66/66/66 65/65/65 64/64/64
f 67/67/67 66/66/66 64/64/64
f 68/68/68 66/66/66 67/67/67
f 69/69/69 68/68/68 67/67/67
f 70/70/70 68/68/68 69/69/69
f 71/71/71 70/70/70 69/69/69
f 72/72/72 70/70/70 71/71/71
f 73/73/73 72/72/72 71/71/71
f 74/74/74 72/72/72 73/73/73
f 75/75/75 74/74/74 73/73/73
f 76/76/76 74/74/74 75/75/75
f 77/77/77 76/76/76 75/75/75
f 80/80/80 79/79/79 78/78/78
f 79/79/79 81/81/81 78/78/78
f 81/81/81 79/79/79 82/82/82
f 79/79/79 83/83/83 82/82/82
f 83/83/83 79/79/79 84/84/84
f 79/79/79 85/85/85 84/84/84
f 88/88/88 87/87/87 86/86/86
f 89/89/89 88/88/88 86/86/86
f 89/89/89 86/86/86 90/90/90
f 91/91/91 88/88/88 89/89/89
f 92/92/92 89/89/89 90/90/90
f 92/92/92 90/90/90 93/93/93
f 94/94/94 91/91/91 89/89/89
f 94/94/94 89/89/89 92/92/92
f 95/95/95 91/91/91 94/94/94
f 96/96/96 92/92/92 93/93/93
f 96/96/96 93/93/93 97/97/97
f 98/98/98 95/95/95 94/94/94
f 99/99/99 95/95/95 98/98/98
f 100/100/100 94/94/94 92/92/92
f 100/100/100 92/92/92 96/96/96
f 98/98/98 94/94/94 100/100/100
f 101/101/101 96/96/96 97/97/97
f 101/101/101 97/97/97 102/102/102
f 103/103/103 99/99/99 98/98/98
f 104/104/104 99/99/99 103/103/103
f 105/105/105 98/98/98 100/100/100
f 103/103/103 98/98/98 105/105/105
f 106/106/106 100/100/100 96/96/96
f 106/106/106 96/96/96 101/101/101
f 105/105/105 100/100/100 106/106/106
f 107/107/107 101/101/101 102/102/102
f 107/107/107 102/102/102 108/108/108
f 109/109/109 107/107/107 108/108/108
f 110/110/110 104/104/104 103/103/103
f 111/111/111 104/104/104 110/110/110
f 112/112/112 111/111/111 110/110/110
f 113/113/113 106/106/106 101/101/101
f 113/113/113 101/101/101 107/107/107
f 114/114/114 107/107/107 109/109/109
f 114/114/114 113/113/113 107/107/107
f 115/115/115 114/114/114 109/109/109
f 116/116/116 105/105/105 106/106/106
f 116/116/116 106/106/106 113/113/113
f 117/117/117 103/103/103 105/105/105
f 110/110/110 103/103/103 117/117/117
f 117/117/117 105/105/105 116/116/116
f 112/112/112 110/110/110 118/118/118
f 118/118/118 110/110/110 117/117/117
f 119/119/119 112/112/112 118/118/118
f 120/120/120 113/113/113 114/114/114
f 120/120/120 116/116/116 113/113/113
f 121/121/121 114/114/114 115/115/115
f 121/121/121 120/120/120 114/114/114
f 122/122/122 121/121/121 115/115/115
f 123/123/123 117/117/117 116/116/116
f 118/118/118 117/117/117 123/123/123
f 123/123/123 116/116/116 120/120/120
f 119/119/119 118/118/118 124/124/124
f 124/124/124 118/118/118 123/123/123
f 125/125/125 119/119/119 124/124/124
f 126/126/126 123/123/123 120/120/120
f 126/126/126 120/120/120 121/121/121
f 124/124/124 123/123/123 126/126/126
f 127/127/127 121/121/121 122/122/122
f 127/127/127 126/126/126 121/121/121
f 128/128/128 127/127/127 122/122/122
f 125/125/125 124/124/124 129/129/129
f 129/129/129 124/124/124 126/126/126
f 129/129/129 126/126/126 127/127/127
f 130/130/130 125/125/125 129/129/129
f 131/131/131 127/127/127 128/128/128
f 131/131/131 129/129/129 127/127/127
f 130/130/130 129/129/129 131/131/131
f 132/132/132 131/131/131 128/128/128
f 133/133/133 130/130/130 131/131/131
f 133/133/133 131/131/131 132/132/132
f 134/134/134 133/133/133 132/132/132
f 137/137/137 136/136/136 135/135/135
f 138/138/138 137/137/137 135/135/135
f 138/138/138 135/135/135 139/139/139
f 140/140/140 138/138/138 139/139/139
f 140/140/140 139/139/139 141/141/141
f 142/142/142 140/140/140 141/141/141
f 142/142/142 141/141/141 143/143/143
f 144/144/144 142/142/142 143/143/143
f 144/144/144 143/143/143 145/145/145
f 146/146/146 144/144/144 145/145/145
f 146/146/146 145/145/145 147/147/147
f 148/148/148 146/146/146 147/147/147
f 151/151/151 150/150/150 149/149/149
f 152/152/152 151/151/151 149/149/149
f 152/152/152 149/149/149 153/153/153
f 154/154/154 152/152/152 153/153/153
f 154/154/154 153/153/153 155/155/155
f 156/156/156 154/154/154 155/155/155
f 156/156/156 155/155/155 157/157/157
f 158/158/158 156/156/156 157/157/157
f 158/158/158 157/157/157 159/159/159
f 160/160/160 158/158/158 159/159/159
f 160/160/160 159/159/159 161/161/161
f 162/162/162 160/160/160 161/161/161
f 165/165/165 164/164/164 163/163/163
f 164/164/164 166/166/166 163/163/163
f 166/166/166 167/167/167 163/163/163
f 167/167/167 168/168/168 163/163/163
f 168/168/168 169/169/169 163/163/163
f 169/169/169 170/170/170 163/163/163
f 173/173/173 172/172/172 171/171/171
f 174/174/174 173/173/173 171/171/171
f 175/175/175 171/171/171 172/172/172
f 171/171/171 176/176/176 174/174/174
f 171/171/171 175/175/175 177/177/177
f 178/178/178 176/176/176 171/171/171
f 179/179/179 171/171/171 177/177/177
f 180/180/180 178/178/178 171/171/171
f 171/171/171 179/179/179 181/181/181
f 182/182/182 180/180/180 171/171/171
f 183/183/183 171/171/171 181/181/181
f 183/183/183 182/182/182 171/171/171
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+332
View File
@@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Newbury Nights - Hunt</title>
<style>
:root {
--ink: #0a0c14;
--ink-2: #11131f;
--mist: #c7ccd9;
--mist-dim: #6b7184;
--red: #FF2678;
--red-top: #F65151;
--yellow: #FFF35D;
--yellow-top: #B57F0B;
--blue: #51EAF1;
--blue-top: #529EFF;
--haze: rgba(199,204,217,0.08);
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
margin: 0; height: 100%; overflow: hidden;
background: var(--ink); color: var(--mist);
font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
user-select: none;
}
#scene, #cam { position: fixed; inset: 0; width: 100%; height: 100%; }
#cam { object-fit: cover; z-index: 0; background: #000; }
#scene { z-index: 1; }
canvas { display: block; }
#hud { position: fixed; inset: 0; z-index: 3; pointer-events: none; }
.pointer { pointer-events: auto; }
#haunt {
position: fixed; left: 0; top: 0; bottom: 0; width: 8px; z-index: 4;
}
#haunt-fill {
position: absolute; left: 0; right: 0; bottom: 0; height: 0%;
background: linear-gradient(0deg, var(--blue), #fff2);
box-shadow: 0 0 18px var(--blue); transition: height 0.4s ease, background 0.6s, box-shadow 0.6s;
}
#haunt-label {
position: fixed; left: 14px; top: 14px; z-index: 5;
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--mist-dim);
}
#haunt-pct { color: var(--mist); font-size: 13px; }
#readout {
position: fixed; right: 14px; top: 14px; z-index: 5; text-align: right;
font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mist-dim);
}
#readout b { color: var(--mist); font-size: 15px; letter-spacing: 0; }
#reticle {
position: fixed; left: 50%; top: 50%; width: 64px; height: 64px;
transform: translate(-50%, -50%); z-index: 4; pointer-events: none;
}
#reticle .ring {
position: absolute; inset: 0; border: 1.5px solid var(--haze); border-radius: 50%;
}
#reticle .charge {
position: absolute; inset: 4px; border-radius: 50%;
background: conic-gradient(var(--blue) 0deg, transparent 0deg);
opacity: 0.85;
}
#reticle .dot { position: absolute; left: 50%; top: 50%; width: 4px; height: 4px;
transform: translate(-50%,-50%); background: var(--mist); border-radius: 50%; }
#reticle.locked .ring { border-color: var(--mist); }
#wheel {
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
z-index: 5; display: flex; gap: 14px; pointer-events: auto;
}
.lure {
width: 52px; height: 52px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.14);
background: var(--c); position: relative; cursor: pointer;
transition: transform 0.15s, box-shadow 0.2s, border-color 0.2s; opacity: 0.55;
}
.lure[data-on="1"] { opacity: 1; transform: scale(1.12);
border-color: #fff; box-shadow: 0 0 22px var(--c); }
.lure.red { --c: var(--red); }
.lure.blue { --c: var(--blue); }
.lure.yellow { --c: var(--yellow); }
#wheel-label { position: fixed; left: 50%; bottom: 88px; transform: translateX(-50%);
z-index: 5; font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;
color: var(--mist-dim); }
#fire {
position: fixed; right: 22px; bottom: 30px; z-index: 6;
width: 84px; height: 84px; border-radius: 50%;
background: radial-gradient(circle at 50% 40%, var(--ink-2), #05060c);
border: 2px solid rgba(255,255,255,0.16); color: var(--mist);
font: inherit; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
pointer-events: auto; cursor: pointer; touch-action: none;
}
#fire:active { border-color: #fff; }
#toasts { position: fixed; left: 50%; top: 64px; transform: translateX(-50%);
z-index: 6; display: flex; flex-direction: column; gap: 6px; align-items: center; }
.toast { font-size: 12px; letter-spacing: 0.06em; padding: 6px 12px;
background: rgba(10,12,20,0.7); border: 1px solid var(--haze); border-radius: 2px;
color: var(--mist); animation: rise 2.4s forwards; }
.toast .nm { color: #fff; }
@keyframes rise { 0% { opacity: 0; transform: translateY(6px);} 12%{opacity:1;transform:none;}
80%{opacity:1;} 100%{opacity:0;} }
#start { position: fixed; inset: 0; z-index: 10; background:
radial-gradient(120% 80% at 50% 0%, #14182a 0%, var(--ink) 60%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
text-align: center; padding: 24px; }
#start h1 { font-size: clamp(30px, 9vw, 52px); margin: 0; letter-spacing: -0.01em;
font-weight: 600; color: #fff; line-height: 0.98; }
#start h1 .n2 { display: block; color: var(--blue); font-style: italic; }
#start p { max-width: 30ch; color: var(--mist-dim); font-size: 13px; line-height: 1.6;
letter-spacing: 0.02em; }
#start .sub { font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--mist-dim); margin-bottom: 18px; }
.btn { margin-top: 22px; pointer-events: auto; cursor: pointer; font: inherit;
font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase;
padding: 14px 28px; color: var(--ink); background: var(--mist);
border: none; border-radius: 2px; }
.btn.ghost { background: transparent; color: var(--mist); border: 1px solid var(--haze);
margin-top: 10px; }
#note { position: fixed; bottom: 8px; left: 0; right: 0; text-align: center; z-index: 10;
font-size: 9px; letter-spacing: 0.14em; color: #3a3f4f; }
@media (prefers-reduced-motion: reduce) { .toast { animation-duration: 0.1s; } }
</style>
</head>
<body>
<video id="cam" autoplay muted playsinline></video>
<div id="scene"></div>
<div id="hud">
<div id="haunt"><div id="haunt-fill"></div></div>
<div id="haunt-label">Haunt <span id="haunt-pct">0%</span></div>
<div id="readout"><b id="caught">0</b><br>caught &middot; <span id="score">0</span> pts</div>
<div id="reticle"><div class="ring"></div><div class="charge"></div><div class="dot"></div></div>
<div id="wheel-label">Lure</div>
<div id="wheel" class="pointer">
<div class="lure red" data-color="Red"></div>
<div class="lure blue" data-color="Blue"></div>
<div class="lure yellow" data-color="Yellow"></div>
</div>
<button id="fire">Hold<br>to charge</button>
<div id="toasts"></div>
</div>
<div id="start">
<div class="sub">A spiritual successor - fan tribute</div>
<h1>Newbury<span class="n2">Nights</span></h1>
<p>Raise your phone to the room. Spirits gather in the dark - pick a lure colour,
aim, and charge your shot to catch them before the haunt overtakes the night.</p>
<button class="btn" id="go-ar">Begin hunt (AR)</button>
<button class="btn ghost" id="go-gyro">No AR - use motion</button>
</div>
<div id="note">Fan-made tribute. Not affiliated with or endorsed by the LEGO Group.</div>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}}
</script>
<script type="module">
import * as THREE from 'three';
import { createGhostVisual } from '/ghost-visual.js';
import { createHunt } from '/hunt.js';
const $ = s => document.querySelector(s);
let roster = [];
try {
roster = await (await fetch('/ghosts.enriched.json')).json();
} catch (e) {
roster = [
{ id:'basic-r', name:'Wisp', color:'Red', rarity:'Common', rarityTier:1, chargePips:2, healthMax:263 },
{ id:'basic-b', name:'Wisp', color:'Blue', rarity:'Common', rarityTier:1, chargePips:2, healthMax:263 },
{ id:'basic-y', name:'Wisp', color:'Yellow', rarity:'Common', rarityTier:1, chargePips:2, healthMax:263 },
];
}
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
$('#scene').appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, innerWidth/innerHeight, 0.01, 50);
scene.add(new THREE.HemisphereLight(0x8899ff, 0x111122, 1.1));
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth/innerHeight; camera.updateProjectionMatrix();
});
const hunt = createHunt(THREE, scene, {
roster, fx: true,
onEvent: (e) => {
if (e.type === 'capture') toast(`Caught <span class="nm">${e.ghost.name}</span> +${e.score}`);
if (e.type === 'flee') toast(`<span class="nm">${e.ghost.name}</span> slipped away`);
if (e.type === 'capture' || e.type === 'spawn' || e.type === 'flee') {
score = scoreFromCaptures();
$('#caught').textContent = hunt.captured.length;
$('#score').textContent = score;
}
}
});
const RARITY_SCORE = { Common:10, Rare:25, Epic:60, Legendary:150 };
let score = 0;
function scoreFromCaptures() {
const byId = Object.fromEntries(roster.map(g => [g.id, g]));
return hunt.captured.reduce((s,id)=> s + (RARITY_SCORE[byId[id]?.rarity]||10), 0);
}
document.querySelectorAll('.lure').forEach(el => {
el.addEventListener('click', () => {
const on = el.dataset.on === '1';
document.querySelectorAll('.lure').forEach(x => x.dataset.on='0');
if (!on) { el.dataset.on='1'; hunt.setLure(el.dataset.color); reticleColor(el.dataset.color); }
else { hunt.setLure(null); reticleColor(null); }
});
});
function reticleColor(c){
const map = { Red:'#FF2678', Blue:'#51EAF1', Yellow:'#FFF35D' };
document.documentElement.style.setProperty('--blue', map[c] || '#51EAF1');
}
function currentTarget() {
const list = hunt.listActive();
let best=null, bestD=Infinity;
for (const a of list) {
const p = a.object3d.position.clone().project(camera);
if (p.z > 1) continue;
const d = Math.hypot(p.x, p.y);
if (d < bestD) { bestD = d; best = a; }
}
return bestD < 0.35 ? best : null;
}
let charging=false, charge=0;
const fireBtn = $('#fire');
const chargeEl = $('#reticle .charge');
function setCharge(v){
charge = Math.max(0, Math.min(1, v));
chargeEl.style.background = `conic-gradient(var(--blue) ${charge*360}deg, transparent ${charge*360}deg)`;
}
const press = (e)=>{ e.preventDefault(); charging=true; };
const release = (e)=>{
e.preventDefault();
if (!charging) return; charging=false;
const t = currentTarget();
if (t) hunt.fireAt(t.id, charge);
setCharge(0);
};
fireBtn.addEventListener('pointerdown', press);
fireBtn.addEventListener('pointerup', release);
fireBtn.addEventListener('pointercancel', release);
function toast(html){
const d = document.createElement('div'); d.className='toast'; d.innerHTML=html;
$('#toasts').appendChild(d); setTimeout(()=>d.remove(), 2400);
}
function paintHaunt(){
const h = hunt.haunt;
$('#haunt-fill').style.height = h + '%';
$('#haunt-pct').textContent = Math.round(h) + '%';
const col = h < 50 ? 'var(--blue)' : h < 80 ? 'var(--yellow)' : 'var(--red)';
$('#haunt-fill').style.background = `linear-gradient(0deg, ${col}, #fff2)`;
}
let usingXR = false, xrSession = null, gyroQuat = null;
async function startAR(){
if (navigator.xr && await navigator.xr.isSessionSupported?.('immersive-ar')) {
try {
xrSession = await navigator.xr.requestSession('immersive-ar', { optionalFeatures:['local-floor','hit-test'] });
renderer.xr.enabled = true;
await renderer.xr.setSession(xrSession);
usingXR = true; beginLoop(); hideStart(); return;
} catch(e){ /* fall through to gyro */ }
}
startGyro();
}
async function startGyro(){
hideStart();
try {
const stream = await navigator.mediaDevices.getUserMedia({ video:{ facingMode:'environment' }, audio:false });
$('#cam').srcObject = stream;
} catch(e){ /* no camera - black bg is fine */ }
if (typeof DeviceOrientationEvent !== 'undefined' && DeviceOrientationEvent.requestPermission) {
try { await DeviceOrientationEvent.requestPermission(); } catch(e){}
}
gyroQuat = new THREE.Quaternion();
const euler = new THREE.Euler();
const zee = new THREE.Vector3(0,0,1);
const q0 = new THREE.Quaternion();
const q1 = new THREE.Quaternion(-Math.sqrt(0.5),0,0,Math.sqrt(0.5));
addEventListener('deviceorientation', (ev)=>{
const a = THREE.MathUtils.degToRad(ev.alpha||0);
const b = THREE.MathUtils.degToRad(ev.beta||0);
const g = THREE.MathUtils.degToRad(ev.gamma||0);
const orient = THREE.MathUtils.degToRad(screen.orientation?.angle||0);
euler.set(b, a, -g, 'YXZ');
gyroQuat.setFromEuler(euler);
gyroQuat.multiply(q1);
gyroQuat.multiply(q0.setFromAxisAngle(zee, -orient));
}, true);
beginLoop();
}
function hideStart(){ $('#start').style.display='none'; }
$('#go-ar').addEventListener('click', startAR);
$('#go-gyro').addEventListener('click', startGyro);
const clock = new THREE.Clock();
function frame(){
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
if (charging) setCharge(charge + dt * 0.9);
if (!usingXR && gyroQuat) camera.quaternion.copy(gyroQuat);
hunt.update(dt, t);
$('#reticle').classList.toggle('locked', !!currentTarget());
paintHaunt();
renderer.render(scene, camera);
}
function beginLoop(){ renderer.setAnimationLoop(frame); }
</script>
</body>
</html>
+155
View File
@@ -0,0 +1,155 @@
/**
* hunt.js — Newbury Nights hunt loop (Phase 3, standalone demo)
*
* Core v1 mechanics:
* - Spawn: rarity-weighted draw from the roster, placed in a forward arc.
* - Colour lure: the wheel sets the "lured" colour; matching ghosts spawn more
* often and are worth more, mirroring the original's GhostColor mechanic.
* - Capture: aim the reticle, hold to charge, release to fire. Charge time
* scales with the ghost's chargePips; misses let it flee (escape timing).
* - Hauntometer: every active/escaped ghost raises the meter; captures lower
* it. As the meter climbs, spawn rate and rarity bias escalate — the session
* gets harder the longer the haunt goes unchecked.
*
* Engine-agnostic: takes a THREE instance + a scene + the createGhostVisual
* factory. No AR/session code here — the page wires camera/anchor.
*/
import { createGhostVisual } from './ghost-visual.js';
const RARITY_BASE_WEIGHT = { Common: 50, Rare: 30, Epic: 15, Legendary: 5 };
const RARITY_SCORE = { Common: 10, Rare: 25, Epic: 60, Legendary: 150 };
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 onEvent = opts.onEvent || (() => {});
let luredColor = null; // set by the colour wheel
let haunt = 0; // 0..100 hauntometer
let captured = []; // captured ghost ids this session
const active = new Map(); // id -> { ghost, visual, hp, state, fleeAt }
let nextId = 1;
let spawnTimer = 0;
// --- weighted roster draw, biased by lure + haunt escalation ---
function pickGhost() {
// haunt escalation: higher haunt tilts the draw toward rarer ghosts
const escal = 1 + (haunt / 100) * 1.5; // up to 2.5x rare bias
const pool = [];
for (const g of roster) {
let w = RARITY_BASE_WEIGHT[g.rarity] || 1;
// rarer ghosts get heavier as haunt climbs
if (g.rarity !== 'Common') w *= escal;
// lure: matching colour spawns ~2x more often
if (luredColor && g.color === luredColor) w *= 2;
pool.push([g, w]);
}
const total = pool.reduce((s, [, w]) => s + w, 0);
let r = Math.random() * total;
for (const [g, w] of pool) { r -= w; if (r <= 0) return g; }
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 spawn() {
if (active.size >= maxActive) return;
const g = pickGhost();
const visual = createGhostVisual(THREE, { color: g.color, fx: opts.fx !== false });
placeInArc(visual.object3d);
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, {
ghost: g, visual, hp, maxHp: hp,
state: 'idle',
fleeAt: performance.now() + (6000 + Math.random() * 4000), // escapes if ignored
});
haunt = Math.min(100, haunt + 4); // presence raises the meter
onEvent({ type: 'spawn', id, ghost: g, haunt });
}
// --- capture attempt: called by the page with a charge level 0..1 ---
function fireAt(id, charge) {
const a = active.get(id);
if (!a) return { hit: false };
// damage = charge, but the ghost needs enough charge relative to chargePips
const needed = 0.4 + (a.ghost.chargePips || 2) * 0.12; // 0.5-1.0
const dmg = charge >= needed ? charge * 1.6 : charge * 0.4;
a.hp -= dmg;
if (a.hp <= 0) return capture(id);
a.state = 'hit';
onEvent({ type: 'hit', id, ghost: a.ghost, hpFrac: Math.max(0, a.hp / a.maxHp) });
return { hit: true, captured: false };
}
function capture(id) {
const a = active.get(id);
if (!a) return { hit: false };
a.visual.setState('capture');
const colorBonus = luredColor && a.ghost.color === luredColor ? 1.5 : 1;
const score = Math.round((RARITY_SCORE[a.ghost.rarity] || 10) * colorBonus);
captured.push(a.ghost.id);
haunt = Math.max(0, haunt - 10); // captures calm the haunt
setTimeout(() => { scene.remove(a.visual.object3d); a.visual.dispose(); }, 500);
active.delete(id);
onEvent({ type: 'capture', id, ghost: a.ghost, score, captured: captured.length, haunt });
return { hit: true, captured: true, score };
}
function flee(id) {
const a = active.get(id);
if (!a) return;
a.visual.setState('capture'); // reuse shrink-out
setTimeout(() => { scene.remove(a.visual.object3d); a.visual.dispose(); }, 400);
active.delete(id);
haunt = Math.min(100, haunt + 8); // an escape feeds the haunt
onEvent({ type: 'flee', id, ghost: a.ghost, haunt });
}
function setLure(color) {
luredColor = COLORS.includes(color) ? color : null;
onEvent({ type: 'lure', color: luredColor });
}
function update(dt, elapsed) {
// spawn cadence accelerates with haunt
const interval = Math.max(1.2, 4.5 - (haunt / 100) * 3.0);
spawnTimer += dt;
if (spawnTimer >= interval) { spawnTimer = 0; spawn(); }
const now = performance.now();
for (const [id, a] of active) {
a.visual.update(dt, elapsed);
if (now >= a.fleeAt) flee(id);
}
}
function listActive() {
return [...active.entries()].map(([id, a]) => ({
id, ghost: a.ghost, object3d: a.visual.object3d,
hpFrac: Math.max(0, a.hp / a.maxHp),
}));
}
return {
update, spawn, fireAt, capture, flee, setLure, listActive,
get haunt() { return haunt; },
get captured() { return captured.slice(); },
get luredColor() { return luredColor; },
};
}
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Newbury Nights</title>
<style>
:root{ --ink:#0a0c14; --mist:#c7ccd9; --mist-dim:#6b7184; --blue:#51EAF1;
--red:#FF2678; --yellow:#FFF35D; --haze:rgba(199,204,217,0.08); }
*{ box-sizing:border-box; }
html,body{ margin:0; min-height:100%; background:var(--ink); color:var(--mist);
font-family:ui-monospace,"SF Mono","Cascadia Mono",Menlo,monospace; }
body{ display:flex; flex-direction:column; align-items:center; justify-content:center;
min-height:100vh; text-align:center; padding:24px;
background:radial-gradient(120% 80% at 50% 0%, #14182a 0%, var(--ink) 60%); }
.sub{ font-size:10px; letter-spacing:0.24em; text-transform:uppercase; color:var(--mist-dim);
margin-bottom:18px; }
h1{ font-size:clamp(34px,11vw,68px); margin:0; font-weight:600; color:#fff; line-height:0.95;
letter-spacing:-0.01em; }
h1 .n2{ display:block; font-style:italic; color:var(--blue); }
.lures{ display:flex; gap:10px; justify-content:center; margin:22px 0 6px; }
.lures i{ width:14px; height:14px; border-radius:50%; display:block; }
.lures .r{ background:var(--red); box-shadow:0 0 12px var(--red); }
.lures .b{ background:var(--blue); box-shadow:0 0 12px var(--blue); }
.lures .y{ background:var(--yellow); box-shadow:0 0 12px var(--yellow); }
p{ max-width:34ch; color:var(--mist-dim); font-size:13px; line-height:1.6; }
.actions{ display:flex; flex-direction:column; gap:10px; margin-top:24px; width:min(280px,80vw); }
a.btn{ text-decoration:none; font-size:13px; letter-spacing:0.16em; text-transform:uppercase;
padding:15px 24px; border-radius:2px; }
a.primary{ background:var(--mist); color:var(--ink); }
a.ghost{ background:transparent; color:var(--mist); border:1px solid var(--haze); }
footer{ margin-top:36px; font-size:9px; letter-spacing:0.14em; color:#3a3f4f; }
</style>
</head>
<body>
<div class="sub">A spiritual successor &middot; fan tribute</div>
<h1>Newbury<span class="n2">Nights</span></h1>
<div class="lures"><i class="r"></i><i class="b"></i><i class="y"></i></div>
<p>The town doesn't sleep after dark. Raise your phone, lure the spirits by colour,
and catch them before the haunt takes hold.</p>
<div class="actions">
<a class="btn primary" href="/hunt">Begin the hunt</a>
<a class="btn ghost" href="/#about">About this tribute</a>
</div>
<footer>Fan-made tribute. Not affiliated with or endorsed by the LEGO Group.</footer>
</body>
</html>
+11
View File
@@ -41,6 +41,17 @@ app.get('/preview', (_req, res) => {
});
// JSON error handler (multer + thrown errors).
// Hunt page (Phase 3 AR ghost hunt; standalone, no auth).
app.get('/hunt', (_req, res) => {
res.sendFile(join(__dirname, 'public', 'hunt.html'));
});
// Landing / launch page.
app.get('/play', (_req, res) => {
res.sendFile(join(__dirname, 'public', 'play.html'));
});
app.use((err, _req, res, _next) => {
console.error(err);
res.status(err.status || 500).json({ error: err.message || 'server error' });