/** * LANE-RENDER — the relic. `snapshot.relics` (v6): optical fossils buried in the crust. * * The brief is a Mario hidden pipe: findable by a player who wanders and looks, invisible * to one who doesn't. So an UNFOUND relic is only a faint static-patch decal that breathes * — no marker, no minimap ping, no arrow. Its discoverability budget is curiosity. The * restraint is the feature: if you can see it from across the map it isn't hidden. * * FOUND (excavated) → the patch becomes a small monument: a fossil slab on a plinth, * lifting out of the ground, lit. §1: the optical soundtrack fossils *sing* when * excavated — the singing is LANE-SCREEN's audio band; this is the thing it sings from. */ import * as THREE from 'three'; import type { RelicState } from '../contracts'; import { disposeObject } from './coords'; import type { Juice } from './juice'; const SHIMMER_VERT = /* glsl */ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const SHIMMER_FRAG = /* glsl */ ` precision highp float; uniform float uTime; varying vec2 vUv; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); } void main() { vec2 p = vUv * 2.0 - 1.0; float rr = dot(p, p); if (rr > 1.0) discard; // static crawl: a hidden VHS dropout, quantised so it flickers rather than smears. vec2 g = floor(vUv * 22.0) + floor(vec2(uTime * 9.0)); float s = hash(g); // breathe the whole patch so a still eye catches motion, but faintly. float breathe = 0.5 + 0.5 * sin(uTime * 1.3); float edge = 1.0 - smoothstep(0.4, 1.0, rr); float a = s * edge * (0.05 + 0.06 * breathe); // deliberately tiny gl_FragColor = vec4(vec3(0.7, 0.75, 0.85) * s, a); } `; interface Rec { found: boolean; obj: THREE.Object3D; } const PLANE = new THREE.PlaneGeometry(1, 1); export class RelicLayer { readonly group = new THREE.Group(); private live = new Map(); private seen = new Set(); private shimmerUniforms = { uTime: { value: 0 } }; private shimmerMat: THREE.ShaderMaterial; /** False until the first sync: a save loaded with a relic already found must not fire * the excavation geyser, exactly as EntityLayer primes before firing placement juice. */ private primed = false; constructor(private juice?: Juice) { this.group.name = 'relics'; this.shimmerMat = new THREE.ShaderMaterial({ uniforms: this.shimmerUniforms, vertexShader: SHIMMER_VERT, fragmentShader: SHIMMER_FRAG, transparent: true, depthWrite: false, }); } sync(relics: ReadonlyArray | undefined, timeSec: number): void { this.shimmerUniforms.uTime.value = timeSec; this.seen.clear(); for (const r of relics ?? []) { this.seen.add(r.id); let rec = this.live.get(r.id); const justExcavated = !!rec && !rec.found && r.found; if (rec && rec.found !== r.found) { this.drop(r.id, rec); // excavated: swap shimmer → monument rec = undefined; } if (!rec) { const obj = r.found ? this.buildMonument() : this.buildShimmer(); obj.position.set(r.pos.x + 0.5, 0, r.pos.y + 0.5); this.group.add(obj); rec = { found: r.found, obj }; this.live.set(r.id, rec); } // The fossil surfaces: a golden geyser that "sings" up out of the crust. Gated on a // real unfound→found transition seen live (not a save that loads already-excavated). if (justExcavated && this.primed) this.juice?.excavate(r.pos.x + 0.5, r.pos.y + 0.5); if (r.found) { // gentle lift + rotate so the excavated fossil reads as an exhibit rec.obj.rotation.y = timeSec * 0.5; rec.obj.position.y = 0.05 + Math.sin(timeSec * 1.5) * 0.03; } } for (const [id, rec] of this.live) if (!this.seen.has(id)) this.drop(id, rec); this.primed = true; } private buildShimmer(): THREE.Object3D { const m = new THREE.Mesh(PLANE, this.shimmerMat); m.rotation.x = -Math.PI / 2; m.scale.set(2.4, 2.4, 1); m.position.y = 0.02; m.renderOrder = 2; return m; } private buildMonument(): THREE.Object3D { const g = new THREE.Group(); const plinth = new THREE.Mesh( new THREE.CylinderGeometry(0.5, 0.6, 0.4, 12), new THREE.MeshStandardMaterial({ color: 0x14121d, roughness: 0.9, emissive: 0x1a2740, emissiveIntensity: 0.4 }), ); plinth.position.y = 0.2; plinth.castShadow = plinth.receiveShadow = true; // The fossil: a wavy photographic strip that "sings" — an emissive slab, gamut-lit. const strip = new THREE.Mesh( new THREE.BoxGeometry(0.18, 0.8, 0.5), new THREE.MeshStandardMaterial({ color: 0xffe0b0, emissive: 0xffcaa0, emissiveIntensity: 1.3, roughness: 0.3, metalness: 0.4, }), ); strip.position.y = 0.8; strip.castShadow = true; g.add(plinth, strip); g.userData.owns = true; return g; } private drop(id: number, rec: Rec): void { this.group.remove(rec.obj); if (rec.obj.userData.owns) disposeObject(rec.obj); // monuments own their geo/mats // shimmers share PLANE + shimmerMat — nothing to dispose this.live.delete(id); } }