import React, { useEffect, useRef, useState } from "react"; import * as THREE from "three"; /** * Superflight (clean-room) — Downhill Wingsuit (WebGL) * --------------------------------------------------- * Update: * - Keyboard-only + Arrow keys (W/S/A/D + ↑/↓/←/→) and Q/E barrel roll * - New blocky, layered canyon/mountain generator with carved tunnels & caves * - Stronger haze/fog for depth; portal at the bottom to roll a new seed */ // -------------------- Utility -------------------- function mulberry32(seed) { let t = seed >>> 0; return function () { t += 0x6D2B79F5; let r = Math.imul(t ^ (t >>> 15), 1 | t); r ^= r + Math.imul(r ^ (r >>> 7), 61 | r); return ((r ^ (r >>> 14)) >>> 0) / 4294967296; }; } const randRange = (rng, min, max) => min + (max - min) * rng(); const choose = (rng, arr) => arr[Math.floor(rng() * arr.length)]; // -------------------- Color Palettes -------------------- // [terrainBase, accent/portal, skyTop, skyBottom] const THEMES = [ [0x8e5a33, 0xbf5af2, 0x2e253f, 0xdcd7c9], // warm canyon + purple accents [0x6c5b7b, 0xf67280, 0x2a2b4a, 0x9fb3c8], [0x3d405b, 0xe07a5f, 0x1a1e26, 0x5b728a], ]; // Simple per-instance color banding by height (strata look) function strataColor(y, topY, botY, baseHex) { const base = new THREE.Color(baseHex); const band = Math.floor((topY - y) / 10) % 2; // 10u thick bands const shade = band ? 0.8 : 1.05; // alternate slightly base.multiplyScalar(shade); return base; } class SphereCollider { constructor(center, radius, tag = "rock") { this.center = center; this.radius = radius; this.tag = tag; } } export default function SuperflightCleanRoom() { const mountRef = useRef(null); const sceneRef = useRef(null); const cameraRef = useRef(null); const rendererRef = useRef(null); const rafRef = useRef(0); const gliderRef = useRef(null); const worldGroupRef = useRef(null); const portalMeshRef = useRef(null); const collidersRef = useRef([]); const [ui, setUI] = useState({ score: 0, multiplier: 1, speed: 0, crashed: false, paused: false, worldSeed: 1, highScore: 0, difficulty: 1 }); const inputsRef = useRef({ keyW: false, keyS: false, keyA: false, keyD: false, keyQ: false, keyE: false }); const stateRef = useRef({ playerPos: new THREE.Vector3(0, 180, 0), playerQuat: new THREE.Quaternion(), playerRadius: 1.0, vel: new THREE.Vector3(0, -22, -6), gravity: 22, glideAccel: 44, drag: 0.62, maxSpeed: 130, yawRate: THREE.MathUtils.degToRad(85), pitchRate: THREE.MathUtils.degToRad(70), rollRate: THREE.MathUtils.degToRad(140), score: 0, multiplier: 1, maxMultiplier: 12, proxThreshold: 6.0, crashed: false, paused: false, worldSeed: Math.floor(Math.random() * 1e9), difficulty: 1, mountainTopY: 140, mountainBottomY: -1400, }); // -------------------- Scene Setup -------------------- useEffect(() => { const container = mountRef.current; const scene = new THREE.Scene(); sceneRef.current = scene; const theme = THEMES[0]; scene.fog = new THREE.Fog(new THREE.Color(theme[3]), 50, 1700); const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 5000); camera.position.set(0, 185, 16); cameraRef.current = camera; const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 1.05); hemi.position.set(0, 200, 0); scene.add(hemi); const dir = new THREE.DirectionalLight(0xffffff, 0.85); dir.position.set(-70, 140, 30); scene.add(dir); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(container.clientWidth, container.clientHeight); rendererRef.current = renderer; container.appendChild(renderer.domElement); const glider = buildGliderMesh(); gliderRef.current = glider; scene.add(glider); const world = new THREE.Group(); worldGroupRef.current = world; scene.add(world); generateMountainWorld(stateRef.current.worldSeed, scene, world, collidersRef, portalMeshRef, stateRef.current); const onResize = () => { if (!container || !rendererRef.current || !cameraRef.current) return; const w = container.clientWidth, h = container.clientHeight; rendererRef.current.setSize(w, h); cameraRef.current.aspect = w / h; cameraRef.current.updateProjectionMatrix(); }; window.addEventListener("resize", onResize); const onKey = (e, down) => { const i = inputsRef.current; switch (e.code) { // WASD case "KeyW": i.keyW = down; break; case "KeyS": i.keyS = down; break; case "KeyA": i.keyA = down; break; case "KeyD": i.keyD = down; break; // Arrows (mirror WASD) case "ArrowUp": i.keyW = down; break; case "ArrowDown": i.keyS = down; break; case "ArrowLeft": i.keyA = down; break; case "ArrowRight": i.keyD = down; break; // Roll case "KeyQ": i.keyQ = down; break; case "KeyE": i.keyE = down; break; // System case "KeyR": if (down) restart(); break; case "KeyP": if (down) togglePause(); break; default: break; } }; const onKeyDown = (e) => onKey(e, true); const onKeyUp = (e) => onKey(e, false); window.addEventListener("keydown", onKeyDown); window.addEventListener("keyup", onKeyUp); const clock = new THREE.Clock(); const loop = () => { rafRef.current = requestAnimationFrame(loop); const dt = Math.min(0.05, clock.getDelta()); tick(dt); renderer.render(scene, camera); }; loop(); return () => { cancelAnimationFrame(rafRef.current); window.removeEventListener("resize", onResize); window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keyup", onKeyUp); disposeWorld(worldGroupRef.current); if (gliderRef.current) { gliderRef.current.traverse((o)=>{ if(o.isMesh){ o.geometry?.dispose?.(); o.material?.dispose?.(); } }); } renderer.dispose(); container.removeChild(renderer.domElement); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // -------------------- Core Tick -------------------- const tick = (dt) => { const st = stateRef.current; const cam = cameraRef.current; const glider = gliderRef.current; if (!cam || !glider) return; if (st.paused || st.crashed) return; // Orientation: keyboard only (W/S pitch, A/D yaw, Q/E roll) const inp = inputsRef.current; const yaw = ((inp.keyD ? 1 : 0) - (inp.keyA ? 1 : 0)) * st.yawRate * dt; const pitch = ((inp.keyS ? 1 : 0) - (inp.keyW ? 1 : 0)) * st.pitchRate * dt; const roll = ((inp.keyE ? 1 : 0) - (inp.keyQ ? 1 : 0)) * st.rollRate * dt; const qYaw = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), yaw); const qPitch = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), pitch); const qRoll = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), -roll); st.playerQuat.multiply(qYaw).multiply(qPitch).multiply(qRoll).normalize(); // Forward vector & physics const fwd = new THREE.Vector3(0,0,-1).applyQuaternion(st.playerQuat).normalize(); const pitchDown = Math.max(0, -fwd.y); const thrust = (0.25 + pitchDown) * st.glideAccel; st.vel.addScaledVector(new THREE.Vector3(0,-1,0), st.gravity * dt); st.vel.addScaledVector(fwd, thrust * dt); // Drag const drag = 1 - Math.exp(-st.drag * dt); st.vel.multiplyScalar(1 - drag); // Clamp const spd = st.vel.length(); if (spd > st.maxSpeed) st.vel.multiplyScalar(st.maxSpeed / spd); // Keep slight horizontal component const horiz = new THREE.Vector3(st.vel.x, 0, st.vel.z); const hlen = horiz.length(); if (hlen < 3) { const fH = new THREE.Vector3(fwd.x, 0, fwd.z).normalize(); st.vel.addScaledVector(fH, (3 - hlen) * 0.6); } // Integrate st.playerPos.addScaledVector(st.vel, dt); // Camera chase const camOffLocal = new THREE.Vector3(0, 1.9, 8.0); const camPos = camOffLocal.clone().applyQuaternion(st.playerQuat).add(st.playerPos); cam.position.lerp(camPos, 0.2); cam.lookAt(st.playerPos.clone().add(fwd.clone().multiplyScalar(16))); // Glider transform glider.position.copy(st.playerPos); glider.quaternion.copy(st.playerQuat); // Collisions & proximity const { nearestDist } = nearestCollider(st.playerPos, st.playerRadius, collidersRef.current); if (nearestDist < 0) { doCrash(); return; } const proxActive = nearestDist < st.proxThreshold; if (proxActive) { const closeness = 1 - THREE.MathUtils.clamp(nearestDist / st.proxThreshold, 0, 1); const gain = (5 + spd * 0.18) * st.multiplier * (0.25 + closeness * 1.3); st.score += gain * dt; if (st.multiplier < st.maxMultiplier) st.multiplier = Math.min(st.maxMultiplier, st.multiplier + dt * (0.15 + closeness * 0.45)); } else { st.multiplier = Math.max(1, st.multiplier - dt * 1.5); } // Portal if (portalMeshRef.current) { const portal = portalMeshRef.current; const c = portal.userData.center; const ringR = portal.userData.ringR; const thick = portal.userData.thickness; const dx = st.playerPos.x - c.x; const dz = st.playerPos.z - c.z; const dy = Math.abs(st.playerPos.y - c.y); const radial = Math.sqrt(dx*dx + dz*dz); if (Math.abs(radial - ringR) < thick * 0.7 && dy < thick * 0.9) nextWorld(); } // UI setUI((u) => ({ ...u, score: st.score, multiplier: st.multiplier, speed: spd, crashed: st.crashed, paused: st.paused, worldSeed: st.worldSeed, difficulty: st.difficulty, highScore: Math.max(u.highScore, st.score | 0) })); }; function nearestCollider(playerPos, playerRadius, colliders) { let min = Infinity; let tag = null; for (let i = 0; i < colliders.length; i++) { const c = colliders[i]; const d = playerPos.distanceTo(c.center) - (c.radius + playerRadius); if (d < min) { min = d; tag = c.tag; } } if (!isFinite(min)) min = 9999; return { nearestDist: min, nearestTag: tag }; } // -------------------- Crash / Restart / Pause -------------------- function doCrash() { const st = stateRef.current; st.crashed = true; setUI((u) => ({ ...u, crashed: true, highScore: Math.max(u.highScore, st.score | 0) })); } function restart() { const st = stateRef.current; st.playerPos.set(0, st.mountainTopY + 40, 0); st.playerQuat.identity(); st.playerQuat.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), THREE.MathUtils.degToRad(20))); st.vel.set(0, -22, -6); st.score = 0; st.multiplier = 1; st.crashed = false; } function togglePause() { const st = stateRef.current; st.paused = !st.paused; setUI((u) => ({ ...u, paused: st.paused })); } function nextWorld() { const st = stateRef.current; st.worldSeed = (st.worldSeed * 1664525 + 1013904223) >>> 0; st.difficulty += 1; const scene = sceneRef.current; disposeWorld(worldGroupRef.current); generateMountainWorld(st.worldSeed, scene, worldGroupRef.current, collidersRef, portalMeshRef, st); st.playerPos.set(0, st.mountainTopY + 40, 0); st.playerQuat.identity(); st.playerQuat.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), THREE.MathUtils.degToRad(20))); st.vel.set(0, -22, -6); } // -------------------- World Generation: Blocky Canyon Mountain -------------------- function generateMountainWorld(seed, scene, worldGroup, collidersRef, portalMeshRef, st) { const rng = mulberry32(seed >>> 0); const theme = choose(rng, THEMES); worldGroup.clear(); collidersRef.current = []; if (scene && scene.fog) scene.fog.color.setHex(theme[3]); // Materials const rockMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.95, metalness: 0.02, vertexColors: true }); const portalMat = new THREE.MeshStandardMaterial({ color: theme[1], emissive: new THREE.Color(theme[1]).multiplyScalar(0.5), roughness: 0.25 }); const topY = st.mountainTopY, botY = st.mountainBottomY; const height = topY - botY; // Base radius curve (mountain silhouette) const baseR = 42 + randRange(rng, -6, 8); const minR = 18 + randRange(rng, -3, 3); function radiusAtY(y) { const t = (y - botY) / height; // 0..1 bottom->top const r = THREE.MathUtils.lerp(minR, baseR, Math.pow(t, 0.55)); // add soft wobble to create alcoves return r + Math.sin(y * 0.06) * 2.2 + Math.cos(y * 0.017 + 1.7) * 1.8; } // Tunnels (3–5 weaving shafts) const tunnelCount = 3 + Math.floor(rng() * 3); const tunnels = new Array(tunnelCount).fill(0).map(() => ({ offX: randRange(rng, -8, 8), offZ: randRange(rng, -8, 8), ax: randRange(rng, 6, 12), az: randRange(rng, 6, 12), fx: randRange(rng, 0.006, 0.012), fz: randRange(rng, 0.006, 0.012), phx: randRange(rng, 0, Math.PI * 2), phz: randRange(rng, 0, Math.PI * 2), radius: randRange(rng, 3.5, 6.2), })); const tunnelCenterAtY = (y, t) => new THREE.Vector3(t.offX + Math.sin(y * t.fx + t.phx) * t.ax, y, t.offZ + Math.cos(y * t.fz + t.phz) * t.az); // Random spherical caves (nibbles) const nibbles = Array.from({ length: 20 }, () => ({ c: new THREE.Vector3(randRange(rng, -20, 20), randRange(rng, botY + 80, topY - 20), randRange(rng, -20, 20)), r: randRange(rng, 3, 10) })); // Build layered ring slabs using InstancedMesh of boxes const boxGeo = new THREE.BoxGeometry(1, 1, 1); const instances = []; const layerStep = 12; // vertical distance between strata (bigger => fewer layers) const shellStep = 2.6; // radial shell spacing const tangentialSpacing = 9; // arc length per box target for (let y = topY; y >= botY; y -= layerStep) { const rProfile = Math.max(5, radiusAtY(y)); const ringLayers = 1 + Math.floor(randRange(rng, 0, 1.2)); // 1..2 shells occasionally for (let rl = 0; rl < ringLayers; rl++) { const r = Math.max(4, rProfile - rl * shellStep); const circumference = 2 * Math.PI * r; const count = THREE.MathUtils.clamp(Math.floor(circumference / tangentialSpacing), 22, 60); for (let i = 0; i < count; i++) { // Skip with some randomness to form gaps + carving checks if (rng() < 0.1) continue; const ang = (i / count) * Math.PI * 2 + randRange(rng, -0.05, 0.05); const radJitter = randRange(rng, -0.9, 0.9); const x = Math.cos(ang) * (r + radJitter); const z = Math.sin(ang) * (r + radJitter); const p = new THREE.Vector3(x, y + randRange(rng, -1.0, 1.0), z); // Carve if in tunnels or caves let carve = false; for (let k = 0; k < tunnels.length && !carve; k++) { const c = tunnelCenterAtY(y, tunnels[k]); const d = Math.hypot(p.x - c.x, p.z - c.z); if (d < tunnels[k].radius + 1.6) carve = true; } if (!carve) { for (let k = 0; k < nibbles.length; k++) { const nb = nibbles[k]; if (p.distanceTo(nb.c) < nb.r) { carve = true; break; } } } if (carve) continue; // Box dimensions (tangent width, vertical height, radial depth) const width = randRange(rng, 2.2, 5.4); const heightBox = randRange(rng, 1.2, 2.8); const depth = randRange(rng, 1.2, 3.0); const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), ang); const s = new THREE.Vector3(width, heightBox, depth); const m4 = new THREE.Matrix4().compose(p, q, s); const color = strataColor(y, topY, botY, theme[0]); instances.push({ m4, color, radiusApprox: Math.max(width, heightBox, depth) * 0.55, p }); } } } // Add blocky towers/arches here and there for dramatic silhouettes const towerCount = 90 + Math.floor(st.difficulty * 20); for (let t = 0; t < towerCount; t++) { const yBase = randRange(rng, botY + 120, topY - 20); const r = radiusAtY(yBase) * randRange(rng, 0.55, 0.95); const ang = randRange(rng, 0, Math.PI * 2); const x = Math.cos(ang) * r, z = Math.sin(ang) * r; const levels = 3 + Math.floor(rng() * 5); let y = yBase; for (let lv = 0; lv < levels; lv++) { const p = new THREE.Vector3(x + randRange(rng, -1.2, 1.2), y, z + randRange(rng, -1.2, 1.2)); const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), ang + randRange(rng, -0.2, 0.2)); const s = new THREE.Vector3(randRange(rng, 2.0, 4.6), randRange(rng, 1.6, 3.2), randRange(rng, 1.6, 3.6)); const m4 = new THREE.Matrix4().compose(p, q, s); const color = strataColor(y, topY, botY, theme[0]); instances.push({ m4, color, radiusApprox: Math.max(s.x, s.y, s.z) * 0.55, p }); y += s.y + randRange(rng, 0.6, 1.4); } } // Build InstancedMesh const mesh = new THREE.InstancedMesh(boxGeo, rockMat, instances.length); const colorAttr = new THREE.InstancedBufferAttribute(new Float32Array(instances.length * 3), 3); const dummy = new THREE.Object3D(); const tmpColor = new THREE.Color(); instances.forEach((it, i) => { mesh.setMatrixAt(i, it.m4); tmpColor.copy(it.color); colorAttr.setXYZ(i, tmpColor.r, tmpColor.g, tmpColor.b); // Use sparser colliders to keep CPU light if (i % 4 === 0) collidersRef.current.push(new SphereCollider(it.p.clone(), it.radiusApprox, "rock")); }); mesh.instanceMatrix.needsUpdate = true; mesh.geometry.setAttribute('instanceColor', colorAttr); worldGroup.add(mesh); // Subtle dust const dust = buildDustField(theme[1], 1400); worldGroup.add(dust); // Portal (horizontal ring at bottom) const portalY = botY + 45; const portalCenter = new THREE.Vector3(0, portalY, 0); const ringR = 10.0, ringThickness = 1.8; const portalGeo = new THREE.TorusGeometry(ringR, ringThickness, 16, 44); const portalMesh = new THREE.Mesh(portalGeo, portalMat); portalMesh.position.copy(portalCenter); portalMesh.rotation.x = 0; portalMesh.userData.center = portalCenter; portalMesh.userData.ringR = ringR; portalMesh.userData.thickness = ringThickness; worldGroup.add(portalMesh); portalMeshRef.current = portalMesh; // A few pearls for portal collision collidersRef.current.push(new SphereCollider(portalCenter.clone().add(new THREE.Vector3(ringR, 0, 0)), ringThickness * 0.95, "portal")); collidersRef.current.push(new SphereCollider(portalCenter.clone().add(new THREE.Vector3(-ringR, 0, 0)), ringThickness * 0.95, "portal")); collidersRef.current.push(new SphereCollider(portalCenter.clone().add(new THREE.Vector3(0, 0, ringR)), ringThickness * 0.95, "portal")); collidersRef.current.push(new SphereCollider(portalCenter.clone().add(new THREE.Vector3(0, 0, -ringR)), ringThickness * 0.95, "portal")); // Background stars const stars = buildStars(theme[2], theme[3]); worldGroup.add(stars); } function disposeWorld(group) { if (!group) return; group.traverse((o)=>{ if (o.isMesh) { o.geometry?.dispose?.(); o.material?.dispose?.(); } }); group.clear(); } // -------------------- Mesh Builders -------------------- function buildGliderMesh() { const g = new THREE.Group(); const bodyGeo = new THREE.ConeGeometry(0.5, 1.8, 6); const wingGeo = new THREE.BoxGeometry(3.2, 0.08, 0.6); const tailGeo = new THREE.BoxGeometry(0.9, 0.06, 0.5); const matBody = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.4, metalness: 0.1 }); const matWing = new THREE.MeshStandardMaterial({ color: 0x87ceeb, roughness: 0.6, metalness: 0.0 }); const body = new THREE.Mesh(bodyGeo, matBody); body.rotation.x = Math.PI / 2; g.add(body); const wing = new THREE.Mesh(wingGeo, matWing); wing.position.set(0, 0, -0.1); g.add(wing); const tail = new THREE.Mesh(tailGeo, matWing); tail.position.set(0, -0.2, 0.4); tail.rotation.z = THREE.MathUtils.degToRad(15); g.add(tail); return g; } function buildDustField(colorHex, count = 1000) { const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); for (let i = 0; i < count; i++) { positions[i*3+0] = (Math.random()-0.5)*120; positions[i*3+1] = (Math.random()-0.5)*160; positions[i*3+2] = (Math.random()-0.5)*120; } geo.setAttribute("position", new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color: colorHex, size: 0.06, transparent: true, opacity: 0.45 }); const pts = new THREE.Points(geo, mat); pts.position.set(0, (stateRef.current.mountainTopY + stateRef.current.mountainBottomY)/2, 0); return pts; } function buildStars(colorTop, colorBottom) { const count = 900; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); for (let i = 0; i < count; i++) { const r = 900 + Math.random() * 700; const th = Math.random() * Math.PI * 2; const ph = Math.random() * Math.PI; positions[i*3+0] = r * Math.sin(ph) * Math.cos(th); positions[i*3+1] = r * Math.cos(ph); positions[i*3+2] = r * Math.sin(ph) * Math.sin(th); } geo.setAttribute("position", new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color: colorTop, size: 0.8, transparent: true, opacity: 0.35 }); return new THREE.Points(geo, mat); } // -------------------- Render -------------------- return (
{/* HUD */}
Score: {ui.score.toFixed(0)}
× Mult: {ui.multiplier.toFixed(1)}
Speed: {ui.speed.toFixed(1)}
Seed: {ui.worldSeed}
Diff: {ui.difficulty}
{/* Reticle */}
{/* Help */}
Controls
W/S or ↑/↓: pitch
A/D or ←/→: yaw
Q/E: roll (barrel)
P: pause · R: restart
{/* Crash Overlay */} {ui.crashed && (
CRASHED
Press R to restart
High Score: {ui.highScore.toFixed(0)}
)} {/* Pause Overlay */} {ui.paused && !ui.crashed && (
PAUSED
Press P to resume
)}
Downhill gliding · Layered canyon generator · Keyboard & Arrow keys
); }