// stub/world_stub.js (Lane F) — executable spec of THE WORLD CONTRACT (docs/TECH.md). // A straight-ish pulsing esophagus tube, analytic everywhere. Lane A replaces this with // js/world/index.js; until then every lane builds against this. Keep contract-identical. import * as THREE from 'three'; // Sample level — kept in sync with LEVEL SCHEMA v1 by Lane C (round 1, authorised by // ROUND1_INSTRUCTIONS §Lane C.1). Schema of record: docs/TECH.md §Level data schema v1. // This is the schema's shop window: if you are implementing against level data, this is the // smallest complete example of it. // // Two deliberate restraints, both so this stays a stub and not a level: // - It keeps the BARE archetype ids ('floater'/'seeker'/'turret') rather than the fiction // ids that real levels use ('bolus_chunk'...). Lane B's pools are keyed by archetype and // B's dev harness (flight/dev.js) reads STUB_LEVEL directly, so renaming here would break // a live lane for nothing. levels/enemies.js resolves both (passthrough entries). // - Turrets are authored one-per-event with a scalar `theta`. Real levels may use a // `theta: [a, b]` array for count > 1; that form needs one line in B's spawn loop first // (snippet in LANE_C_NOTES.md §→ Lane B), so it is not demonstrated here yet. export const STUB_LEVEL = { schema: 1, id: 'STUB', name: 'Stub Esophagus', seed: 20260716, par: { time: 30, score: 900, samples: 0 }, segments: [{ biome: 'esophagus', name: 'Stub Esophagus', length: 400, radius: { base: 10, wobble: 0.15 }, curviness: 0.3, flow: 14, }], arenas: [], events: [ { s: 60, type: 'spawn', enemy: 'floater', count: 4, spread: 60 }, { s: 140, type: 'spawn', enemy: 'seeker', count: 3, spread: 40 }, { s: 200, type: 'checkpoint', name: 'Stub Checkpoint' }, { s: 260, type: 'spawn', enemy: 'turret', count: 1, theta: 0 }, { s: 280, type: 'spawn', enemy: 'turret', count: 1, theta: 3.1416 }, { s: 320, type: 'pickup', kind: 'nutrient_orb', count: 3, spread: 40 }, ], }; const BIOME = { // mirrors ART_BIBLE esophagus row; A's biomes.js is the real registry id: 'esophagus', flow: 14, coatDrain: 0.5, palette: { tint: 0x1e6e64, rim: 0x5affd2, void: 0x02100d }, fog: 0.028, }; const WAVE_A = 0.9; // peristalsis displacement amplitude (units, inward) const WAVE_K = 0.22; // spatial frequency (rad/unit along s) const CREST = 1.6; // crest phase speed = CREST × flow. Round-2 ruling: must beat // throttleMax (1.4) or surfing loses to throttle-mashing (Lane B, // round 1). ω = k · CREST · flow, so at flow 14: 22.4 u/s. const SKIN = 0.6; // collision safety margin export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) { const seg = levelData.segments[0]; const LEN = seg.length, R = seg.radius.base; const AX = seg.curviness * 8, AY = seg.curviness * 5; // gentle analytic centreline const KX = 0.020, KY = 0.031; // --- centreline & frames (analytic; s ≈ arclength for these gentle amplitudes) --- const centre = (s) => new THREE.Vector3(AX * Math.sin(s * KX), AY * Math.sin(s * KY), -s); const tangent = (s) => new THREE.Vector3(AX * KX * Math.cos(s * KX), AY * KY * Math.cos(s * KY), -1).normalize(); function sample(s) { const tan = tangent(s); const up = new THREE.Vector3(0, 1, 0); const nor = up.clone().addScaledVector(tan, -up.dot(tan)).normalize(); // Gram-Schmidt (stable: tube never goes vertical) const bin = new THREE.Vector3().crossVectors(tan, nor); return { pos: centre(s), tan, nor, bin, radius: radiusAt(s) }; } const radiusAt = (s) => R * (1 + seg.radius.wobble * Math.sin(s * 0.05) * Math.sin(s * 0.013 + 2.1)); const waveW = WAVE_K * CREST * seg.flow; // crest phase speed ω/k = CREST × flow const flowPulse = (s, t) => Math.pow(Math.max(0, Math.sin(WAVE_K * s - waveW * t)), 3); // --- geometry: rings along s, uv = (theta/2pi, s), inward-facing --- // seam ring vertex is duplicated (RADIAL+1 columns) so uv.x runs 0..1 without wrapping // backwards across the last quad (visible streak otherwise). const RADIAL = 64, COLS = RADIAL + 1, STEP = 0.5; const rings = Math.floor(LEN / STEP) + 1; const pos = new Float32Array(rings * COLS * 3); const inward = new Float32Array(rings * COLS * 3); const uv = new Float32Array(rings * COLS * 2); let p = 0, q = 0; for (let i = 0; i < rings; i++) { const s = i * STEP, f = sample(s); for (let j = 0; j < COLS; j++) { const th = (j / RADIAL) * Math.PI * 2; const dir = f.nor.clone().multiplyScalar(Math.cos(th)).addScaledVector(f.bin, Math.sin(th)); const v = f.pos.clone().addScaledVector(dir, f.radius); pos[p] = v.x; pos[p + 1] = v.y; pos[p + 2] = v.z; inward[p] = -dir.x; inward[p + 1] = -dir.y; inward[p + 2] = -dir.z; p += 3; uv[q++] = j / RADIAL; uv[q++] = s; } } const idx = []; for (let i = 0; i < rings - 1; i++) for (let j = 0; j < RADIAL; j++) { const a = i * COLS + j, b = i * COLS + j + 1, c = a + COLS, d = b + COLS; idx.push(a, c, b, b, c, d); // wound to face inward } const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setAttribute('aInward', new THREE.BufferAttribute(inward, 3)); geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2)); geo.setIndex(idx); const mat = new THREE.ShaderMaterial({ side: THREE.FrontSide, uniforms: { uTime: { value: 0 }, uTint: { value: new THREE.Color(BIOME.palette.tint) }, uRim: { value: new THREE.Color(BIOME.palette.rim) }, uVoid: { value: new THREE.Color(BIOME.palette.void) }, uFog: { value: BIOME.fog }, }, vertexShader: /* glsl */` attribute vec3 aInward; varying vec2 vUv; varying vec3 vN; varying vec3 vView; uniform float uTime; void main() { vUv = uv; float pulse = pow(max(0.0, sin(${WAVE_K.toFixed(3)} * uv.y - ${waveW.toFixed(3)} * uTime)), 3.0); float breathe = 0.15 * sin(uv.y * 0.7 + uTime * 0.8) * sin(uv.x * 6.2831 * 3.0); vec3 disp = position + aInward * (${WAVE_A.toFixed(2)} * pulse + breathe); vec4 mv = modelViewMatrix * vec4(disp, 1.0); vN = normalize(normalMatrix * aInward); vView = -mv.xyz; gl_Position = projectionMatrix * mv; }`, fragmentShader: /* glsl */` varying vec2 vUv; varying vec3 vN; varying vec3 vView; uniform vec3 uTint; uniform vec3 uRim; uniform vec3 uVoid; uniform float uFog; uniform float uTime; void main() { // fake SEM detail until Lane D textures land: ridged folds along theta + s striation float folds = 0.55 + 0.45 * sin(vUv.x * 6.2831 * 9.0 + sin(vUv.y * 0.9) * 2.0); float stria = 0.85 + 0.15 * sin(vUv.y * 2.2); vec3 base = uTint * folds * stria * 0.8; float fres = pow(1.0 - abs(dot(normalize(vN), normalize(vView))), 2.2); vec3 col = base + uRim * fres * 0.9; float d = length(vView); col = mix(col, uVoid, 1.0 - exp(-uFog * d * 0.55)); gl_FragColor = vec4(col, 1.0); #include }`, }); const mesh = new THREE.Mesh(geo, mat); mesh.frustumCulled = false; // one chunk; A's real world streams + culls properly const group = new THREE.Group(); group.name = 'world'; group.add(mesh); let time = 0; const world = { level: levelData, length: LEN, group, sample, flowPulse: (s, t = time) => flowPulse(s, t), crestSpeed: (_s) => CREST * seg.flow, // u/s the crest travels at s (contract v1.1) project(v) { const s = THREE.MathUtils.clamp(-v.z, 0, LEN); const f = sample(s); const rel = v.clone().sub(f.pos); return { s, theta: Math.atan2(rel.dot(f.bin), rel.dot(f.nor)), rho: rel.length() }; }, wallRho: (s, _theta) => radiusAt(s) - WAVE_A - SKIN, collide(v, r = 0) { const { s, theta, rho } = world.project(v); const max = world.wallRho(s, theta) - r; if (rho <= max) return null; const f = sample(s); const dir = f.nor.clone().multiplyScalar(Math.cos(theta)).addScaledVector(f.bin, Math.sin(theta)); return { push: dir.multiplyScalar(max - rho), kind: 'wall', biome: BIOME.id }; }, biomeAt: (_s) => BIOME, modeAt: (_s) => 'tube', arenaAt: (_s) => null, update(dt, _playerS) { time += dt; mat.uniforms.uTime.value = time; }, hash() { let h = 2166136261 >>> 0; for (let s = 0; s <= LEN; s += 10) { const v = sample(s).pos; for (const x of [v.x, v.y, v.z, radiusAt(s)]) { h ^= Math.round(x * 1000) & 0xffff; h = Math.imul(h, 16777619); } } return (h >>> 0).toString(16); }, dispose() { geo.dispose(); mat.dispose(); }, }; return world; }