Canyon Run
muse spark 1.2 · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 40,226 bytes · SHA-256 113d6d4c0ed4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANYON RUSH — High-Speed Procedural Racer</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--orange:#ff6b2c;--pink:#ff2e88;--purple:#7a2cff;--cyan:#2cfff8;--gold:#ffb020;--bg:#0a0a14}
body{background:var(--bg);color:#fff;font-family:'Space Grotesk',sans-serif;overflow:hidden;height:100vh;width:100vw}
#c{position:fixed;inset:0;width:100%;height:100%;display:block}
#hud{position:fixed;inset:0;pointer-events:none;z-index:10}
.top-bar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:18px 24px;align-items:flex-start}
.hud-group{display:flex;flex-direction:column;gap:6px}
.hud-label{font-size:9px;letter-spacing:0.28em;color:rgba(255,255,255,0.45);font-weight:600;text-transform:uppercase}
.hud-value{font-family:'JetBrains Mono',monospace;font-weight:700;letter-spacing:-0.02em}
#scoreVal{font-size:34px;color:#fff;line-height:1}
#scoreVal span{font-size:16px;color:var(--gold)}
#speedWrap{width:220px}
.speed-track{height:2px;background:rgba(255,255,255,0.12);position:relative;overflow:hidden;margin-top:6px}
#speedFill{height:100%;width:30%;background:linear-gradient(90deg,var(--orange),var(--gold),var(--cyan));box-shadow:0 0 10px var(--orange);transition:width 0.1s}
#speedNum{font-family:'JetBrains Mono',monospace;font-size:11px;margin-top:4px;color:rgba(255,255,255,0.7);display:flex;justify-content:space-between}
.mult-display{position:absolute;left:50%;top:42%;transform:translate(-50%,-50%);text-align:center;opacity:0;transition:all 0.2s}
.mult-display.active{opacity:1;transform:translate(-50%,-55%) scale(1.05)}
#multNum{font-size:72px;font-weight:800;font-style:italic;letter-spacing:-0.06em;line-height:0.9;color:#fff;text-shadow:0 0 30px var(--pink), 0 0 60px var(--pink)}
#multLabel{font-size:10px;letter-spacing:0.4em;color:var(--gold);margin-top:2px}
.near-flash{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:11px;letter-spacing:0.5em;color:var(--cyan);opacity:0;font-weight:700;pointer-events:none}
.near-flash.show{animation:flash 0.6s ease}
@keyframes flash{0%{opacity:0;transform:translate(-50%,-40%)}20%{opacity:1}100%{opacity:0;transform:translate(-50%,-60%)}}
.bottom-bar{position:absolute;bottom:0;left:0;right:0;padding:20px 24px;display:flex;align-items:flex-end;justify-content:space-between;gap:24px}
.energy-wrap{flex:1;max-width:520px}
.energy-track{height:8px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.1);position:relative;overflow:hidden;backdrop-filter:blur(10px)}
#energyFill{height:100%;width:65%;background:linear-gradient(90deg,var(--orange) 0%, var(--pink) 50%, var(--cyan) 100%);box-shadow:0 0 18px var(--pink);transition:width 0.2s}
#energyFill.low{background:linear-gradient(90deg,#ff1a1a,#ff6b2c);animation:pulse 0.4s infinite alternate}
@keyframes pulse{from{opacity:1}to{opacity:0.6}}
.turbo-wrap{width:180px;text-align:right}
.turbo-track{height:4px;background:rgba(255,255,255,0.08);margin-top:6px;position:relative}
#turboFill{height:100%;width:0%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);transition:width 0.15s}
#turboBtn{margin-top:8px;font-size:9px;letter-spacing:0.22em;color:rgba(255,255,255,0.6);border:1px solid rgba(44,255,248,0.3);padding:4px 10px;display:inline-block;background:rgba(44,255,248,0.08)}
#turboBtn.ready{color:var(--cyan);border-color:var(--cyan);background:rgba(44,255,248,0.18);box-shadow:0 0 12px rgba(44,255,248,0.5)}
.controls-hint{position:absolute;bottom:84px;left:50%;transform:translateX(-50%);display:flex;gap:8px;align-items:center;font-size:9px;letter-spacing:0.18em;color:rgba(255,255,255,0.35)}
.kbd{border:1px solid rgba(255,255,255,0.18);padding:3px 6px;font-family:'JetBrains Mono',monospace;font-size:9px;background:rgba(255,255,255,0.06)}
.overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;background:radial-gradient(1200px 600px at 50% 0%, rgba(255,107,44,0.22), transparent 70%), linear-gradient(180deg, rgba(10,6,20,0.92) 0%, rgba(8,8,18,0.96) 100%);backdrop-filter:blur(2px)}
.card{width:min(560px,92vw);border:1px solid rgba(255,255,255,0.08);background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));padding:32px 28px;position:relative;overflow:hidden}
.card::before{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg, transparent, var(--orange), var(--pink), transparent)}
.eyebrow{font-size:10px;letter-spacing:0.42em;color:var(--gold);font-weight:700}
h1{font-size:44px;line-height:0.9;letter-spacing:-0.05em;margin:10px 0 10px;font-weight:700}
h1 i{font-style:italic;color:var(--orange);font-weight:300}
.desc{font-size:13px;line-height:1.6;color:rgba(255,255,255,0.62);max-width:46ch}
.stats-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin:18px 0}
.stat{border:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.03);padding:12px;text-align:center}
.stat b{font-family:'JetBrains Mono',monospace;font-size:18px;display:block;color:#fff}
.stat span{font-size:9px;letter-spacing:0.18em;color:rgba(255,255,255,0.45)}
.btn{width:100%;padding:16px;background:#fff;color:#0a0a14;border:none;font-weight:800;letter-spacing:0.18em;font-size:13px;cursor:pointer;transition:all 0.15s;pointer-events:auto}
.btn:hover{background:var(--orange);color:#fff;box-shadow:0 0 30px rgba(255,107,44,0.5)}
.btn:active{transform:scale(0.99)}
.btn-alt{background:transparent;color:#fff;border:1px solid rgba(255,255,255,0.14);margin-top:10px}
.btn-alt:hover{background:rgba(255,255,255,0.08);box-shadow:none}
.legend{display:flex;gap:16px;margin-top:16px;flex-wrap:wrap}
.legend div{font-size:10px;color:rgba(255,255,255,0.5);display:flex;align-items:center;gap:6px;letter-spacing:0.08em}
.dot{width:8px;height:8px;border-radius:50%}
.hidden{display:none !important}
#shake{position:fixed;inset:0;pointer-events:none}
@media(max-width:700px){
h1{font-size:34px}
.top-bar{padding:12px}
#scoreVal{font-size:26px}
.bottom-bar{flex-direction:column;align-items:stretch}
}
</style>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<canvas id="c"></canvas>
<div id="shake"></div>
<div id="hud">
<div class="top-bar">
<div class="hud-group">
<div class="hud-label">Distance • Score</div>
<div id="scoreVal" class="hud-value">00000 <span>+0</span></div>
<div style="display:flex;gap:14px;margin-top:4px">
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.5)">MAX <b id="maxSpeed" style="color:#fff">0</b> KM/H</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.5)">BEST <b id="bestScore" style="color:var(--gold)">0</b></div>
</div>
</div>
<div class="hud-group" style="align-items:flex-end">
<div class="hud-label">Velocity</div>
<div id="speedWrap">
<div class="speed-track"><div id="speedFill"></div></div>
<div id="speedNum"><span id="speedKmh">0</span><span>KM/H</span></div>
</div>
</div>
</div>
<div class="mult-display" id="multDisplay">
<div id="multNum">×1.0</div>
<div id="multLabel">MULTIPLIER</div>
</div>
<div class="near-flash" id="nearFlash">NEAR MISS +250</div>
<div class="controls-hint">
<span class="kbd">A D</span> <span class="kbd">W S</span> BANK & PITCH • <span class="kbd">SPACE</span> TURBO
</div>
<div class="bottom-bar">
<div class="energy-wrap">
<div class="hud-label" style="display:flex;justify-content:space-between"><span>Energy Core</span><span id="energyPct" style="color:var(--gold)">65%</span></div>
<div class="energy-track"><div id="energyFill"></div></div>
<div style="font-size:9px;letter-spacing:0.12em;color:rgba(255,255,255,0.35);margin-top:4px">COLLECT ORBS TO REFUEL • GRAZE WALLS FOR MULTIPLIER</div>
</div>
<div class="turbo-wrap">
<div class="hud-label">Turbo Charge</div>
<div class="turbo-track"><div id="turboFill"></div></div>
<div id="turboBtn">CHARGING</div>
</div>
</div>
</div>
<div id="startOverlay" class="overlay">
<div class="card">
<div class="eyebrow">PROCEDURAL CANYON RACER • VER 1.4</div>
<h1>CANYON <i>RUSH</i></h1>
<p class="desc">Skim the golden-hour canyon at Mach 1. Grazing walls builds your multiplier, but the core bleeds fast. Thread the needle, harvest orbs, and ride the turbo — the canyon never ends.</p>
<div class="stats-row">
<div class="stat"><b>∞</b><span>ENDLESS CHUNKS</span></div>
<div class="stat"><b>×99</b><span>NEAR MISS</span></div>
<div class="stat"><b>60</b><span>FPS TARGET</span></div>
</div>
<button class="btn" id="playBtn">IGNITE ENGINES — PLAY</button>
<button class="btn btn-alt" id="howBtn">HOW TO FLY</button>
<div class="legend">
<div><span class="dot" style="background:var(--gold)"></span> ORB = +25 ENERGY</div>
<div><span class="dot" style="background:var(--cyan)"></span> GRAZE = MULTIPLIER</div>
<div><span class="dot" style="background:var(--pink)"></span> MONOLITH = DEATH</div>
</div>
<div id="howText" class="hidden" style="margin-top:14px;font-size:12px;line-height:1.6;color:rgba(255,255,255,0.6);border-top:1px solid rgba(255,255,255,0.06);padding-top:12px">
<b style="color:#fff">Flight model:</b> Momentum-based. Roll (A/D) banks and yaws, Pitch (W/S) climbs/dives. Hold straight for passive turbo charge. Grazing within 1.5m of walls triggers <b style="color:var(--cyan)">NEAR MISS</b>. Collect amber orbs for energy. SPACE when turbo full.
</div>
</div>
</div>
<div id="gameOverOverlay" class="overlay hidden">
<div class="card" style="text-align:center">
<div class="eyebrow" style="color:var(--pink)">SIGNAL LOST • CORE DEPLETED</div>
<h1 style="font-size:38px">CRASH <i style="color:var(--pink)">LANDING</i></h1>
<div class="stats-row">
<div class="stat"><b id="finalScore">0</b><span>FINAL SCORE</span></div>
<div class="stat"><b id="finalDist">0m</b><span>DISTANCE</span></div>
<div class="stat"><b id="finalSpeed">0</b><span>TOP SPEED</span></div>
</div>
<div style="display:flex;gap:10px">
<button class="btn" id="restartBtn" style="flex:1">RETRY RUN</button>
<button class="btn btn-alt" id="menuBtn" style="flex:0.6">MENU</button>
</div>
<div style="margin-top:12px;font-size:10px;letter-spacing:0.2em;color:rgba(255,255,255,0.3)">NEAR MISSES CANYON WALLS BUILD LEGEND</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
const canvas = document.getElementById('c');
const scoreVal = document.getElementById('scoreVal');
const speedFill = document.getElementById('speedFill');
const speedKmh = document.getElementById('speedKmh');
const maxSpeedEl = document.getElementById('maxSpeed');
const bestScoreEl = document.getElementById('bestScore');
const energyFill = document.getElementById('energyFill');
const energyPct = document.getElementById('energyPct');
const turboFill = document.getElementById('turboFill');
const turboBtn = document.getElementById('turboBtn');
const multDisplay = document.getElementById('multDisplay');
const multNum = document.getElementById('multNum');
const nearFlash = document.getElementById('nearFlash');
const startOverlay = document.getElementById('startOverlay');
const gameOverOverlay = document.getElementById('gameOverOverlay');
let scene, camera, renderer, clock;
let player, playerGroup;
let dustSystem, trailGroup;
let chunks = [];
let orbs = [];
let obstacles = [];
let audioCtx;
const CHUNK_LEN = 50;
const NUM_CHUNKS = 7;
const CANYON_HALF = 16;
let state = 'menu'; // menu, playing, dead
let score = 0, distance = 0, multiplier = 1, nearMissCooldown = 0;
let energy = 72, turbo = 0, isBoosting = false, boostTime = 0;
let baseSpeed = 42, curSpeed = 42, maxSpeed = 0, difficulty = 0;
let keys = {};
let roll = 0, pitch = 0, targetRoll = 0, targetPitch = 0;
let shake = 0;
let bestScore = parseInt(localStorage.getItem('canyonBest')||'0');
bestScoreEl.textContent = bestScore.toLocaleString();
// audio
function initAudio(){
if(audioCtx) return;
audioCtx = new (window.AudioContext||window.webkitAudioContext)();
}
function beep(freq, dur, type='sine', gain=0.15){
if(!audioCtx) return;
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type; o.frequency.value = freq;
g.gain.value = gain;
o.connect(g); g.connect(audioCtx.destination);
o.start();
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+dur);
o.stop(audioCtx.currentTime+dur);
}
function playCollect(){ beep(880,0.12,'sine',0.25); setTimeout(()=>beep(1320,0.18,'sine',0.18),70); }
function playNear(){ beep(220,0.15,'sawtooth',0.12); beep(440,0.25,'square',0.08); }
function playHit(){ beep(80,0.4,'square',0.3); beep(40,0.6,'sawtooth',0.25); }
function playBoost(){ beep(120,0.8,'sawtooth',0.18); }
// three setup
function init(){
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x1a0a2e, 60, 260);
scene.background = new THREE.Color(0x0f0a1a);
camera = new THREE.PerspectiveCamera(78, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(0,7,14);
renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
clock = new THREE.Clock();
// lights - golden hour
const hemi = new THREE.HemisphereLight(0xff8a4a, 0x1a0a3a, 0.9);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffb87a, 2.2);
sun.position.set(-80, 60, -40);
sun.castShadow = true;
sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.near=1; sun.shadow.camera.far=300;
sun.shadow.camera.left=-80; sun.shadow.camera.right=80; sun.shadow.camera.top=80; sun.shadow.camera.bottom=-80;
sun.shadow.bias=-0.0005;
scene.add(sun);
const rim = new THREE.DirectionalLight(0x7a2cff, 0.9);
rim.position.set(60,20,60);
scene.add(rim);
// player craft
playerGroup = new THREE.Group();
const bodyGeo = new THREE.ConeGeometry(0.9, 2.8, 3);
bodyGeo.rotateX(Math.PI/2);
const bodyMat = new THREE.MeshStandardMaterial({color:0xffffff, flatShading:true, metalness:0.6, roughness:0.25});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.castShadow = true;
playerGroup.add(body);
// wings
const wingGeo = new THREE.BoxGeometry(2.6, 0.08, 0.9);
const wingMat = new THREE.MeshStandardMaterial({color:0xe8e8ee, flatShading:true});
const wing = new THREE.Mesh(wingGeo, wingMat);
wing.position.set(0, -0.15, -0.35);
wing.castShadow = true;
playerGroup.add(wing);
// cockpit
const cock = new THREE.Mesh(new THREE.SphereGeometry(0.45,8,8), new THREE.MeshStandardMaterial({color:0x2cfff8, emissive:0x2cfff8, emissiveIntensity:1.2}));
cock.position.set(0,0.25,0.2); cock.scale.set(1,0.7,1.2);
playerGroup.add(cock);
// engine glow
const engine = new THREE.Mesh(new THREE.ConeGeometry(0.5,0.9,6), new THREE.MeshBasicMaterial({color:0xff6b2c}));
engine.rotation.x = -Math.PI/2; engine.position.set(0,-0.1,-1.5);
playerGroup.add(engine);
engine.name='engine';
// trail light
const pLight = new THREE.PointLight(0xff6b2c, 8, 12);
pLight.position.set(0,0,-1.6);
playerGroup.add(pLight);
player = new THREE.Group();
player.add(playerGroup);
player.position.set(0,5,0);
scene.add(player);
// ground plane subtle
const groundGeo = new THREE.PlaneGeometry(200, 2000);
const groundMat = new THREE.MeshStandardMaterial({color:0x1e102e, roughness:0.9});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI/2; ground.position.y = -1.2; ground.position.z = -500;
ground.receiveShadow = true;
scene.add(ground);
// dust particles
const dustCount = 600;
const dustGeo = new THREE.BufferGeometry();
const pos = new Float32Array(dustCount*3);
const vel = new Float32Array(dustCount);
for(let i=0;i<dustCount;i++){
pos[i*3]=(Math.random()-0.5)*80;
pos[i*3+1]=Math.random()*30;
pos[i*3+2]= (Math.random()-0.5)*400 - 100;
vel[i]= 0.5+Math.random()*1.5;
}
dustGeo.setAttribute('position', new THREE.BufferAttribute(pos,3));
dustGeo.setAttribute('vel', new THREE.BufferAttribute(vel,1));
const dustMat = new THREE.PointsMaterial({color:0xffc99a, size:0.18, transparent:true, opacity:0.55, sizeAttenuation:true});
dustSystem = new THREE.Points(dustGeo, dustMat);
scene.add(dustSystem);
// trail particles behind ship
trailGroup = new THREE.Group();
scene.add(trailGroup);
// chunks
for(let i=0;i<NUM_CHUNKS;i++){
const z = -i*CHUNK_LEN;
createChunk(z, i===0?0:null);
}
window.addEventListener('resize', onResize);
window.addEventListener('keydown', e=>{
keys[e.code]=true;
if(e.code==='Space'){
if(turbo>=98 && state==='playing'){ startBoost(); }
e.preventDefault();
}
});
window.addEventListener('keyup', e=> keys[e.code]=false);
// touch / pointer steering
let dragging=false, startX=0;
canvas.addEventListener('pointerdown', e=>{dragging=true; startX=e.clientX; initAudio();});
canvas.addEventListener('pointerup', ()=>dragging=false);
canvas.addEventListener('pointermove', e=>{
if(!dragging||state!=='playing') return;
const dx = (e.clientX - startX)/innerWidth;
targetRoll = -dx*1.6;
});
canvas.addEventListener('pointerleave', ()=>dragging=false);
animate();
}
function createChunk(z, forcedType){
const group = new THREE.Group();
group.position.z = z;
scene.add(group);
chunks.push(group);
const typeRoll = forcedType!==null? forcedType : Math.random();
// canyon walls - left/right cliffs
const wallH = 14 + Math.random()*10;
const wallMat = new THREE.MeshStandardMaterial({color:0x3a1a4a, flatShading:true, roughness:0.85});
const wallMat2 = new THREE.MeshStandardMaterial({color:0x4a2a5a, flatShading:true, roughness:0.85});
// left wall
const leftW = 18 + Math.random()*8;
const leftGeo = new THREE.BoxGeometry(leftW, wallH, CHUNK_LEN+2);
const left = new THREE.Mesh(leftGeo, wallMat);
left.position.set(-CANYON_HALF - leftW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
left.castShadow=true; left.receiveShadow=true;
group.add(left);
// right wall
const rightW = 18 + Math.random()*8;
const rightGeo = new THREE.BoxGeometry(rightW, wallH, CHUNK_LEN+2);
const right = new THREE.Mesh(rightGeo, wallMat2);
right.position.set(CANYON_HALF + rightW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
right.castShadow=true; right.receiveShadow=true;
group.add(right);
// top shadow cap illusion (dark boxes inside walls emulated)
// decide obstacles
const diff = difficulty;
if(typeRoll < 0.28){
// monolith field
const count = 1 + Math.floor(Math.random()*2 + diff*0.6);
for(let i=0;i<count;i++){
const w = 2.2 + Math.random()*1.8;
const h = 6 + Math.random()*9;
const d = 2.2 + Math.random()*1.5;
const mon = new THREE.Mesh(new THREE.BoxGeometry(w,h,d), new THREE.MeshStandardMaterial({color:0x2a2a36, flatShading:true, metalness:0.2, roughness:0.7}));
mon.position.set((Math.random()-0.5)*(CANYON_HALF*1.3), h/2-1.2, (Math.random()-0.5)*(CHUNK_LEN*0.6));
mon.castShadow=true; mon.receiveShadow=true;
mon.userData.isObstacle=true;
mon.userData.radius = Math.max(w,d)*0.7;
group.add(mon);
obstacles.push(mon);
}
} else if(typeRoll < 0.55){
// tight squeeze gate
const gap = Math.max(5.5 - diff*0.7, 3.2);
const gateX = (Math.random()-0.5)*6;
const h = 9 + Math.random()*4;
const makePillar = (x) =>{
const pil = new THREE.Mesh(new THREE.BoxGeometry(2.4, h, 2.4), new THREE.MeshStandardMaterial({color:0x5a3a6a, flatShading:true}));
pil.position.set(x, h/2-1.2, (Math.random()-0.5)*6);
pil.castShadow=true; pil.receiveShadow=true;
pil.userData.isObstacle=true; pil.userData.radius=1.8;
group.add(pil); obstacles.push(pil);
// top beam if gap small
if(gap<4.5){
const beam = new THREE.Mesh(new THREE.BoxGeometry(gap+4.8,0.7,1.2), new THREE.MeshStandardMaterial({color:0x8a5a3a}));
beam.position.set(gateX, h-0.6, pil.position.z);
group.add(beam);
beam.userData.isObstacle=true; beam.userData.radius=1.2;
obstacles.push(beam);
}
};
makePillar(gateX - gap/2 -1.2);
makePillar(gateX + gap/2 +1.2);
} else if(typeRoll < 0.72){
// floating arch
const arch = new THREE.Group();
const aH = 7;
const aW = 10 + Math.random()*4;
const leg1 = new THREE.Mesh(new THREE.BoxGeometry(1.2,aH,1.2), new THREE.MeshStandardMaterial({color:0x4a2e5a}));
leg1.position.set(-aW/2, aH/2-1.2,0); arch.add(leg1);
const leg2 = leg1.clone(); leg2.position.x = aW/2; arch.add(leg2);
const top = new THREE.Mesh(new THREE.BoxGeometry(aW+1.2,0.9,1.6), new THREE.MeshStandardMaterial({color:0xff6b2c, emissive:0xff6b2c, emissiveIntensity:0.15}));
top.position.set(0,aH-0.8,0); arch.add(top);
top.userData.isObstacle=true; top.userData.radius=1.0;
leg1.userData.isObstacle=true; leg1.userData.radius=1.0; leg2.userData.isObstacle=true; leg2.userData.radius=1.0;
arch.position.x = (Math.random()-0.5)*4;
group.add(arch);
obstacles.push(leg1,leg2,top);
}
// orbs - place in risky spots
const orbCount = Math.random()<0.7 ? (Math.random()<0.5?2:1) : 0;
for(let i=0;i<orbCount;i++){
const orbGeo = new THREE.IcosahedronGeometry(0.7,1);
const orbMat = new THREE.MeshStandardMaterial({color:0xffd27a, emissive:0xff9a1a, emissiveIntensity:1.6, flatShading:true});
const orb = new THREE.Mesh(orbGeo, orbMat);
// place near gap or wall for risk
let rx, rz;
if(obstacles.length>0 && Math.random()<0.75){
const ref = obstacles[Math.floor(Math.random()*obstacles.length)];
const wp = new THREE.Vector3(); ref.getWorldPosition(wp);
rx = wp.x + (Math.random()-0.5)*3;
rz = ref.position.z + (Math.random()-0.5)*4;
} else {
rx=(Math.random()-0.5)*(CANYON_HALF*1.1); rz=(Math.random()-0.5)*(CHUNK_LEN*0.7);
}
rx = THREE.MathUtils.clamp(rx, -CANYON_HALF+2, CANYON_HALF-2);
orb.position.set(rx, 3+Math.random()*4, rz);
orb.userData.isOrb=true;
orb.userData.baseY = orb.position.y;
orb.userData.phase = Math.random()*Math.PI*2;
const light = new THREE.PointLight(0xffb020, 6, 10);
light.position.copy(orb.position);
group.add(light);
orb.userData.light = light;
// glow sprite
const glow = new THREE.Mesh(new THREE.SphereGeometry(1.15,8,8), new THREE.MeshBasicMaterial({color:0xffb020, transparent:true, opacity:0.18}));
orb.add(glow);
group.add(orb);
orbs.push(orb);
}
// ground deco rocks
for(let i=0;i<3;i++){
const r = new THREE.Mesh(new THREE.DodecahedronGeometry(0.6+Math.random()*0.6), new THREE.MeshStandardMaterial({color:0x2a1838, flatShading:true}));
r.position.set((Math.random()-0.5)*22, 0, (Math.random()-0.5)*CHUNK_LEN);
r.scale.y=0.6; r.rotation.set(Math.random()*Math.PI,Math.random()*Math.PI,0);
group.add(r);
}
}
function recycleChunks(){
// player moves negative Z visually, but we keep player at 0 and move world positive
// Instead move chunks forward (positive Z). When chunk z > 30 (behind player), move to front
for(const g of chunks){
if(g.position.z > 30){
// find most negative
let minZ = Math.min(...chunks.map(c=>c.position.z));
g.position.z = minZ - CHUNK_LEN;
// clear old orbs/obstacles associated
// remove orbs/obstacles that belong to this group
const toRemoveOrb = [];
for(const o of orbs){ if(o.parent===g) toRemoveOrb.push(o); }
// but we will just remove group children that are orbs/obstacles from arrays after recycling, better recreate
// clear group
// remove from arrays
const children = [...g.children];
for(const ch of children){
if(ch.userData.isOrb || ch.userData.isObstacle){
// remove from arrays
const idxO = orbs.indexOf(ch); if(idxO>=0) orbs.splice(idxO,1);
const idxB = obstacles.indexOf(ch); if(idxB>=0) obstacles.splice(idxB,1);
}
// keep walls? we will keep but re-randomize wall heights via scaling? simpler remove all and regenerate
}
// remove all children
while(g.children.length) g.remove(g.children[0]);
// obstacles for this group already removed, now regenerate content for new position
// need to reconstruct similar to createChunk but reuse group
// inline generation without pushing new chunk
(function regen(group){
const wallH = 14 + Math.random()*10;
const leftW = 18 + Math.random()*8;
const rightW = 18 + Math.random()*8;
const wallMat = new THREE.MeshStandardMaterial({color:0x3a1a4a, flatShading:true});
const wallMat2 = new THREE.MeshStandardMaterial({color:0x4a2a5a, flatShading:true});
const left = new THREE.Mesh(new THREE.BoxGeometry(leftW, wallH, CHUNK_LEN+2), wallMat);
left.position.set(-CANYON_HALF - leftW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
left.castShadow=true; group.add(left);
const right = new THREE.Mesh(new THREE.BoxGeometry(rightW, wallH, CHUNK_LEN+2), wallMat2);
right.position.set(CANYON_HALF + rightW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
right.castShadow=true; group.add(right);
const typeRoll=Math.random();
if(typeRoll < 0.30){
const count = 1+ Math.floor(Math.random()*2 + difficulty*0.6);
for(let i=0;i<count;i++){
const w=2.2+Math.random()*1.8, h=6+Math.random()*9, d=2.2+Math.random()*1.5;
const mon=new THREE.Mesh(new THREE.BoxGeometry(w,h,d), new THREE.MeshStandardMaterial({color:0x2a2a36, flatShading:true}));
mon.position.set((Math.random()-0.5)*(CANYON_HALF*1.3), h/2-1.2, (Math.random()-0.5)*(CHUNK_LEN*0.6));
mon.castShadow=true; mon.userData.isObstacle=true; mon.userData.radius=Math.max(w,d)*0.7;
group.add(mon); obstacles.push(mon);
}
} else if(typeRoll<0.58){
const gap=Math.max(5.5 - difficulty*0.7, 3.2);
const gateX=(Math.random()-0.5)*6;
const h=9+Math.random()*4;
const make=(x)=>{
const pil=new THREE.Mesh(new THREE.BoxGeometry(2.4,h,2.4), new THREE.MeshStandardMaterial({color:0x5a3a6a}));
pil.position.set(x,h/2-1.2,(Math.random()-0.5)*6); pil.castShadow=true; pil.userData.isObstacle=true; pil.userData.radius=1.8;
group.add(pil); obstacles.push(pil);
if(gap<4.5){
const beam=new THREE.Mesh(new THREE.BoxGeometry(gap+4.8,0.7,1.2), new THREE.MeshStandardMaterial({color:0x8a5a3a}));
beam.position.set(gateX,h-0.6,pil.position.z); beam.userData.isObstacle=true; beam.userData.radius=1.2; group.add(beam); obstacles.push(beam);
}
};
make(gateX -gap/2-1.2); make(gateX+gap/2+1.2);
}
const orbCount=Math.random()<0.65? (Math.random()<0.55?1:2):0;
for(let i=0;i<orbCount;i++){
const orb=new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,1), new THREE.MeshStandardMaterial({color:0xffd27a, emissive:0xff9a1a, emissiveIntensity:1.6}));
let rx=(Math.random()-0.5)*(CANYON_HALF*1.2), rz=(Math.random()-0.5)*(CHUNK_LEN*0.7);
rx=THREE.MathUtils.clamp(rx,-CANYON_HALF+2,CANYON_HALF-2);
orb.position.set(rx, 3+Math.random()*4, rz); orb.userData.isOrb=true; orb.userData.baseY=orb.position.y; orb.userData.phase=Math.random()*Math.PI*2;
const glow=new THREE.Mesh(new THREE.SphereGeometry(1.15,8,8), new THREE.MeshBasicMaterial({color:0xffb020, transparent:true, opacity:0.18})); orb.add(glow);
const light=new THREE.PointLight(0xffb020,6,10); light.position.copy(orb.position); group.add(light); orb.userData.light=light;
group.add(orb); orbs.push(orb);
}
for(let i=0;i<3;i++){
const r=new THREE.Mesh(new THREE.DodecahedronGeometry(0.6+Math.random()*0.6), new THREE.MeshStandardMaterial({color:0x2a1838, flatShading:true}));
r.position.set((Math.random()-0.5)*22,0,(Math.random()-0.5)*CHUNK_LEN); r.scale.y=0.6; group.add(r);
}
})(g);
}
}
}
function startBoost(){
if(isBoosting) return;
isBoosting=true; boostTime=3.2; turbo=0; playBoost();
shake=0.8;
}
function onResize(){
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
}
function updateHUD(){
const kmh = Math.round(curSpeed*3.2);
speedKmh.textContent = kmh.toString().padStart(3,'0');
maxSpeed = Math.max(maxSpeed, kmh);
maxSpeedEl.textContent = maxSpeed;
const pct = THREE.MathUtils.clamp(curSpeed/110,0,1);
speedFill.style.width = (pct*100)+'%';
energyFill.style.width = Math.max(0,energy)+'%';
energyPct.textContent = Math.round(energy)+'%';
if(energy<22) energyFill.classList.add('low'); else energyFill.classList.remove('low');
turboFill.style.width = turbo+'%';
if(turbo>=99){ turboBtn.textContent='SPACE — BOOST!'; turboBtn.classList.add('ready'); }
else { turboBtn.textContent = turbo>60?'CHARGING '+Math.round(turbo)+'%':'CHARGING'; turboBtn.classList.remove('ready'); }
scoreVal.innerHTML = Math.floor(score).toString().padStart(5,'0') + ` <span>×${multiplier.toFixed(1)}</span>`;
multNum.textContent = '×'+multiplier.toFixed(1);
multDisplay.classList.toggle('active', multiplier>1.05);
if(multiplier>1.05) multNum.style.color = multiplier>3?'#2cfff8':'#fff';
}
let lastTrail=0;
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.033);
const t = clock.elapsedTime;
if(state==='playing'){
// input
if(keys['KeyA']||keys['ArrowLeft']) targetRoll = 0.85;
else if(keys['KeyD']||keys['ArrowRight']) targetRoll = -0.85;
else {
// decay to 0 but allow pointer influence if dragging handled
if(!keys['KeyA']&&!keys['KeyD']&&!keys['ArrowLeft']&&!keys['ArrowRight']) targetRoll *= 0.88;
if(Math.abs(targetRoll)<0.02) targetRoll=0;
}
if(keys['KeyW']||keys['ArrowUp']) targetPitch = -0.55;
else if(keys['KeyS']||keys['ArrowDown']) targetPitch = 0.55;
else targetPitch *= 0.88;
roll = THREE.MathUtils.lerp(roll, targetRoll, 0.09);
pitch = THREE.MathUtils.lerp(pitch, targetPitch, 0.09);
playerGroup.rotation.z = roll;
playerGroup.rotation.x = pitch*0.55;
playerGroup.rotation.y = -roll*0.22;
// speed logic
baseSpeed = 42 + difficulty*14 + (multiplier-1)*4;
if(isBoosting){
curSpeed = baseSpeed + 58;
boostTime -= dt;
if(boostTime<=0) isBoosting=false;
} else {
curSpeed = THREE.MathUtils.lerp(curSpeed, baseSpeed, 0.04);
// passive turbo charge when flying straight and fast
if(Math.abs(roll)<0.15 && curSpeed>45) turbo = Math.min(100, turbo + dt*9);
}
// move world
const move = curSpeed * dt;
distance += move;
score += move * 10 * (0.7 + multiplier*0.45);
difficulty = Math.min(5, distance/320);
// move chunks
for(const g of chunks) g.position.z += move;
// dust
const posAttr = dustSystem.geometry.attributes.position;
for(let i=0;i<posAttr.count;i++){
let z = posAttr.getZ(i);
z += move* (0.8 + posAttr.getX(i)*0);
// actually move with speed
posAttr.setZ(i, z);
if(z>18){ posAttr.setZ(i, -380 + Math.random()*20); posAttr.setX(i,(Math.random()-0.5)*80); posAttr.setY(i, Math.random()*30); }
}
posAttr.needsUpdate=true;
// player lateral/vertical movement constrained in canyon
player.position.x += -roll * curSpeed * 0.045;
player.position.y += -pitch * curSpeed * 0.028;
player.position.x = THREE.MathUtils.clamp(player.position.x, -CANYON_HALF+1.6, CANYON_HALF-1.6);
player.position.y = THREE.MathUtils.clamp(player.position.y, 1.2, 9.5);
if(Math.abs(player.position.x) > CANYON_HALF-2) {
// wall scrape
energy -= dt*10;
shake = 0.4;
if(nearMissCooldown<=0){
multiplier = Math.min(9.9, multiplier+0.15);
nearMissCooldown=0.35;
nearFlash.textContent='WALL GRAZE +'+Math.round(60*multiplier);
nearFlash.classList.remove('show'); void nearFlash.offsetWidth; nearFlash.classList.add('show');
}
}
// energy decay
const decay = (6.2 + difficulty*1.6 + (isBoosting?5:0)) * dt;
energy -= decay;
if(energy<=0){ energy=0; triggerGameOver(); }
// trail engine scale with speed/boost
const eng = playerGroup.getObjectByName('engine');
if(eng){ eng.scale.setScalar(1 + (curSpeed-40)*0.01 + (isBoosting?0.7:0)); eng.material.color.setHSL(isBoosting?0.52:0.07,1,0.55); }
// orbs
for(let i=orbs.length-1;i>=0;i--){
const orb = orbs[i];
orb.rotation.y += dt*1.6;
orb.position.y = orb.userData.baseY + Math.sin(t*2 + orb.userData.phase)*0.35;
if(orb.userData.light) orb.userData.light.position.copy(orb.position); // local? need world but approximate
// world pos
const wp = new THREE.Vector3(); orb.getWorldPosition(wp);
const dist = wp.distanceTo(player.position);
if(dist < 1.9){
// collect
energy = Math.min(100, energy+26);
score += 420 * multiplier;
turbo = Math.min(100, turbo+18);
multiplier = Math.min(9.9, multiplier+0.22);
playCollect();
// pop effect
orb.userData.light.intensity=0;
orb.parent.remove(orb);
orbs.splice(i,1);
// small particle burst
// flash
shake=0.25;
nearFlash.textContent='ORB +26 ENERGY';
nearFlash.classList.remove('show'); void nearFlash.offsetWidth; nearFlash.classList.add('show');
continue;
}
// cull behind
if(wp.z > 18){ // behind player, already handled by recycle but remove if stray
// no
}
}
// obstacles collision / near miss
let closestDist = Infinity;
for(const obs of obstacles){
const wp = new THREE.Vector3(); obs.getWorldPosition(wp);
const d = wp.distanceTo(player.position);
const rad = obs.userData.radius || 1.5;
// near miss band 1.6 - 3.7
if(d < rad + 1.1){
// collision
triggerGameOver(true);
break;
}
if(d < rad + 3.4 && d > rad+0.9){
closestDist = Math.min(closestDist, d - rad);
}
}
if(closestDist < 2.2){
shake = Math.max(shake, (2.2-closestDist)*0.35);
camera.fov = 78 + (2.2-closestDist)*4 + (isBoosting?12:0);
camera.updateProjectionMatrix();
if(nearMissCooldown<=0){
multiplier = Math.min(9.9, multiplier+0.035);
turbo = Math.min(100, turbo+0.9);
score += 38;
nearMissCooldown = 0.28;
if(Math.random()<0.3){
nearFlash.textContent='NEAR MISS +'+Math.round(120*multiplier);
nearFlash.classList.remove('show'); void nearFlash.offsetWidth; nearFlash.classList.add('show');
playNear();
}
}
} else {
camera.fov = THREE.MathUtils.lerp(camera.fov, 78 + (isBoosting?14:0), 0.08);
camera.updateProjectionMatrix();
// slowly decay multiplier if not grazing
if(!isBoosting) multiplier = Math.max(1, multiplier - dt*0.18);
}
if(nearMissCooldown>0) nearMissCooldown-=dt;
recycleChunks();
// subtle auto camera follow
camera.position.x = THREE.MathUtils.lerp(camera.position.x, player.position.x*0.35, 0.07);
camera.position.y = THREE.MathUtils.lerp(camera.position.y, player.position.y+2.2, 0.07);
camera.lookAt(player.position.x*0.5, player.position.y-0.4, player.position.z - 14);
// shake
if(shake>0){
camera.position.x += (Math.random()-0.5)*shake;
camera.position.y += (Math.random()-0.5)*shake*0.6;
shake = Math.max(0, shake - dt*2.2);
}
updateHUD();
} else {
// idle camera drift in menu/dead
const idle = t*0.12;
camera.position.x = Math.sin(idle)*1.2;
camera.position.y = 6.5 + Math.sin(idle*0.7)*0.6;
camera.lookAt(0,3,-14);
// rotate orbs gently even when not playing
for(const o of orbs){ o.rotation.y+=0.01; }
}
// engine trail particles occasional
if(state==='playing' && t - lastTrail > 0.025){
lastTrail=t;
// simple expanding ring behind
const p = new THREE.Mesh(new THREE.SphereGeometry(0.12,6,6), new THREE.MeshBasicMaterial({color: isBoosting?0x2cfff8:0xff8a3a, transparent:true, opacity:0.9}));
p.position.copy(player.position); p.position.z -=1.2; p.position.y -=0.15;
p.userData.life=0.45; p.userData.vel = new THREE.Vector3((Math.random()-0.5)*1.2, (Math.random()-0.5)*0.8, 1.5+Math.random()*2);
trailGroup.add(p);
}
for(let i=trailGroup.children.length-1;i>=0;i--){
const p=trailGroup.children[i];
p.position.addScaledVector(p.userData.vel, dt*10);
p.userData.life-=dt;
p.material.opacity = p.userData.life*1.6;
p.scale.multiplyScalar(1.015);
if(p.userData.life<=0) trailGroup.remove(p);
}
renderer.render(scene, camera);
}
function triggerGameOver(crashed=false){
if(state==='dead') return;
state='dead';
if(crashed) { playHit(); shake=1.2; } else { beep(60,1,'sawtooth',0.2); }
document.getElementById('finalScore').textContent = Math.floor(score).toLocaleString();
document.getElementById('finalDist').textContent = Math.floor(distance*3)+'m';
document.getElementById('finalSpeed').textContent = maxSpeed+' KM/H';
if(score>bestScore){ bestScore=Math.floor(score); localStorage.setItem('canyonBest',bestScore); bestScoreEl.textContent=bestScore.toLocaleString(); }
gameOverOverlay.classList.remove('hidden');
// dim engine
const eng=playerGroup.getObjectByName('engine'); if(eng) eng.material.color.set(0x330000);
}
function resetGame(){
score=0; distance=0; multiplier=1; energy=78; turbo=0; isBoosting=false; boostTime=0; difficulty=0; curSpeed=42; baseSpeed=42; maxSpeed=0; roll=0; pitch=0; targetRoll=0; targetPitch=0;
player.position.set(0,5,0);
playerGroup.rotation.set(0,0,0);
nearMissCooldown=0;
// clear chunks and rebuild
for(const g of chunks) scene.remove(g);
chunks=[]; orbs=[]; obstacles=[]; trailGroup.clear();
for(let i=0;i<NUM_CHUNKS;i++) createChunk(-i*CHUNK_LEN, i===0?0:null);
energyFill.classList.remove('low');
gameOverOverlay.classList.add('hidden');
startOverlay.classList.add('hidden');
camera.fov=78; camera.updateProjectionMatrix();
state='playing';
initAudio();
if(audioCtx.state==='suspended') audioCtx.resume();
// hum
beep(90,0.3,'sine',0.08);
}
document.getElementById('playBtn').addEventListener('click', ()=>{ resetGame(); });
document.getElementById('restartBtn').addEventListener('click', ()=>{ resetGame(); });
document.getElementById('menuBtn').addEventListener('click', ()=>{
gameOverOverlay.classList.add('hidden');
startOverlay.classList.remove('hidden');
state='menu';
});
document.getElementById('howBtn').addEventListener('click', ()=>{
document.getElementById('howText').classList.toggle('hidden');
});
init();
updateHUD();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"6fb19f24-e810-47c2-ba68-043fb7670626","tokensIn":924,"tokensOut":13627,"tokensTotal":14551,"cost":0.05906975,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:12:10.745Z"} -->