Phase 3: AR hunt /hunt + launch page /play - shared-visual ghosts, colour lure, hauntometer
@@ -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?.());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 56 KiB |
@@ -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 · <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>
|
||||
@@ -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; },
|
||||
};
|
||||
}
|
||||
@@ -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 · 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>
|
||||
@@ -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' });
|
||||
|
||||