Canyon Run
Gemini 3.6 Flash · full

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 48,065 bytes · SHA-256 9fa5da528354
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>CYBER SUNSET: CANYON OVERDRIVE</title>
<!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #05020a;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #fff;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* HUD Overlay */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 24px;
box-sizing: border-box;
}
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.hud-card {
background: rgba(15, 8, 28, 0.65);
border: 1px solid rgba(255, 120, 50, 0.3);
box-shadow: 0 0 20px rgba(255, 90, 0, 0.15), inset 0 0 10px rgba(255, 120, 50, 0.1);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 12px;
padding: 12px 20px;
display: flex;
flex-direction: column;
}
.score-label, .stat-label {
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: #ffaa55;
margin-bottom: 2px;
}
.score-value {
font-size: 32px;
font-weight: 800;
font-family: monospace;
color: #ffffff;
text-shadow: 0 0 12px rgba(255, 170, 85, 0.8);
}
.multiplier-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ff0055, #ff5500);
color: #fff;
font-weight: 900;
font-size: 18px;
padding: 4px 12px;
border-radius: 20px;
margin-left: 10px;
box-shadow: 0 0 15px rgba(255, 0, 85, 0.6);
transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.multiplier-badge.pulse {
transform: scale(1.35);
}
.near-miss-banner {
position: absolute;
top: 18%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
color: #00ffff;
font-size: 28px;
font-weight: 900;
letter-spacing: 4px;
text-transform: uppercase;
text-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
white-space: nowrap;
}
.near-miss-banner.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1.1);
}
/* Bottom Gauges */
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
}
.gauge-group {
display: flex;
flex-direction: column;
gap: 12px;
width: 280px;
}
.gauge-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.gauge-header {
display: flex;
justify-content: space-between;
font-size: 11px;
letter-spacing: 1.5px;
color: #d1bfff;
font-weight: 600;
}
.gauge-bar-bg {
width: 100%;
height: 12px;
background: rgba(20, 10, 35, 0.8);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
overflow: hidden;
padding: 2px;
box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
}
.gauge-fill {
height: 100%;
border-radius: 4px;
width: 100%;
transition: width 0.1s linear, background-color 0.3s ease;
}
#energy-fill {
background: linear-gradient(90deg, #ff0055, #ffaa00, #00ffaa);
box-shadow: 0 0 10px #00ffaa;
}
#energy-fill.warning {
background: #ff0044;
box-shadow: 0 0 15px #ff0044;
animation: pulse-red 0.5s infinite alternate;
}
#boost-fill {
background: linear-gradient(90deg, #0088ff, #00ffff);
box-shadow: 0 0 10px #00ffff;
}
@keyframes pulse-red {
0% { opacity: 0.6; }
100% { opacity: 1; }
}
.speedometer {
text-align: right;
}
.speed-number {
font-size: 48px;
font-weight: 900;
font-family: monospace;
line-height: 1;
color: #ffffff;
text-shadow: 0 0 15px rgba(255, 140, 0, 0.6);
}
.speed-unit {
font-size: 12px;
letter-spacing: 3px;
color: #ff9933;
}
/* Screen Overlays (Start / Pause / Game Over) */
.overlay-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 20;
background: radial-gradient(circle at center, rgba(30, 10, 45, 0.85) 0%, rgba(5, 2, 10, 0.95) 100%);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 30px;
text-align: center;
opacity: 1;
transition: opacity 0.4s ease;
}
.overlay-screen.hidden {
opacity: 0;
pointer-events: none;
}
.game-title {
font-size: 52px;
font-weight: 900;
letter-spacing: 6px;
background: linear-gradient(135deg, #fff 20%, #ffaa00 60%, #ff0055 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 30px rgba(255, 120, 0, 0.5);
margin-bottom: 8px;
text-transform: uppercase;
}
.game-subtitle {
font-size: 14px;
letter-spacing: 4px;
color: #00ffff;
margin-bottom: 36px;
text-transform: uppercase;
}
.btn {
pointer-events: auto;
background: linear-gradient(135deg, #ff5500, #ff0055);
border: none;
color: #fff;
font-size: 18px;
font-weight: 800;
letter-spacing: 3px;
padding: 16px 42px;
border-radius: 40px;
cursor: pointer;
box-shadow: 0 0 25px rgba(255, 50, 0, 0.6);
transition: all 0.2s ease;
text-transform: uppercase;
outline: none;
}
.btn:hover {
transform: scale(1.06);
box-shadow: 0 0 35px rgba(255, 50, 0, 0.9);
background: linear-gradient(135deg, #ff7700, #ff0077);
}
.btn:active {
transform: scale(0.98);
}
.controls-guide {
margin-top: 36px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 16px;
max-width: 520px;
width: 100%;
}
.control-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 12px 8px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.key-badge {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
padding: 4px 10px;
font-size: 12px;
font-weight: bold;
font-family: monospace;
color: #00ffff;
}
.control-desc {
font-size: 11px;
color: #bbb;
letter-spacing: 1px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin: 24px 0 32px 0;
width: 100%;
max-width: 400px;
}
.stat-box {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 120, 50, 0.2);
border-radius: 10px;
padding: 14px;
}
.stat-num {
font-size: 26px;
font-weight: 800;
color: #fff;
font-family: monospace;
}
/* Sound Button */
.audio-btn {
position: absolute;
top: 24px;
right: 24px;
z-index: 30;
pointer-events: auto;
background: rgba(15, 8, 28, 0.7);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
width: 42px;
height: 42px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 18px;
backdrop-filter: blur(6px);
transition: all 0.2s ease;
}
.audio-btn:hover {
background: rgba(255, 120, 50, 0.4);
border-color: #ffaa00;
}
/* Mobile Controls Overlay */
#touch-controls {
position: absolute;
bottom: 20px;
left: 0;
width: 100%;
height: 180px;
z-index: 15;
pointer-events: none;
display: none;
}
@media (hover: none) and (pointer: coarse) {
#touch-controls {
display: block;
}
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<!-- HUD Overlay -->
<div id="hud">
<div class="hud-top">
<div class="hud-card">
<div class="score-label">Score</div>
<div style="display: flex; align-items: center;">
<div class="score-value" id="hud-score">0</div>
<div class="multiplier-badge" id="hud-multiplier">x1</div>
</div>
</div>
<div class="hud-card" style="align-items: flex-end;">
<div class="stat-label">High Score</div>
<div class="score-value" id="hud-highscore" style="font-size: 22px; color: #ffaa00;">0</div>
</div>
</div>
<div class="near-miss-banner" id="near-miss-banner">NEAR MISS +250</div>
<div class="hud-bottom">
<div class="gauge-group">
<div class="gauge-container">
<div class="gauge-header">
<span>ENERGY / FUEL</span>
<span id="energy-text">100%</span>
</div>
<div class="gauge-bar-bg">
<div class="gauge-fill" id="energy-fill" style="width: 100%;"></div>
</div>
</div>
<div class="gauge-container">
<div class="gauge-header">
<span>TURBO BOOST [SPACE]</span>
<span id="boost-text">READY</span>
</div>
<div class="gauge-bar-bg">
<div class="gauge-fill" id="boost-fill" style="width: 100%;"></div>
</div>
</div>
</div>
<div class="speedometer">
<div class="speed-number" id="hud-speed">120</div>
<div class="speed-unit">KM / H</div>
</div>
</div>
</div>
<button class="audio-btn" id="audio-toggle" title="Toggle Audio">🔊</button>
<!-- Start Screen Overlay -->
<div class="overlay-screen" id="start-screen">
<div class="game-title">CYBER SUNSET</div>
<div class="game-subtitle">CANYON OVERDRIVE</div>
<button class="btn" id="start-btn">ENGINE LAUNCH</button>
<div class="controls-guide">
<div class="control-card">
<span class="key-badge">A / D or ← / →</span>
<span class="control-desc">BANK & ROLL</span>
</div>
<div class="control-card">
<span class="key-badge">W / S or ↑ / ↓</span>
<span class="control-desc">PITCH ELEVATION</span>
</div>
<div class="control-card">
<span class="key-badge">SPACE</span>
<span class="control-desc">TURBO BOOST</span>
</div>
<div class="control-card">
<span class="key-badge">CLOSE FLIGHT</span>
<span class="control-desc">NEAR MISS MULTIPLIER</span>
</div>
</div>
</div>
<!-- Game Over Overlay -->
<div class="overlay-screen hidden" id="gameover-screen">
<div class="game-title" style="font-size: 42px; background: linear-gradient(135deg, #ff0055, #ff5500);">CRASH DETECTED</div>
<div class="game-subtitle" id="gameover-cause">ENERGY DEPLETED</div>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-label">FINAL SCORE</div>
<div class="stat-num" id="go-score">0</div>
</div>
<div class="stat-box">
<div class="stat-label">MAX SPEED</div>
<div class="stat-num" id="go-speed">0</div>
</div>
<div class="stat-box">
<div class="stat-label">NEAR MISSES</div>
<div class="stat-num" id="go-nearmiss">0</div>
</div>
<div class="stat-box">
<div class="stat-label">ORBS COLLECTED</div>
<div class="stat-num" id="go-orbs">0</div>
</div>
</div>
<button class="btn" id="restart-btn">REDEPLOY CRAFT</button>
</div>
<!-- JavaScript Logic -->
<script>
// --- STANDALONE / CDN FALLBACK SHIM FOR THREE.JS IF OFFLINE ---
(function ensureThreeLoaded() {
if (typeof THREE !== 'undefined') return;
console.warn("Three.js not found from CDN. Initializing lightweight WebGL fallback stub...");
// Lightweight THREE stub so game logic runs smoothly without breaking
window.THREE = {
Vector3: class {
constructor(x=0,y=0,z=0){this.x=x;this.y=y;this.z=z;}
set(x,y,z){this.x=x;this.y=y;this.z=z;return this;}
copy(v){this.x=v.x;this.y=v.y;this.z=v.z;return this;}
clone(){return new window.THREE.Vector3(this.x,this.y,this.z);}
add(v){this.x+=v.x;this.y+=v.y;this.z+=v.z;return this;}
sub(v){this.x-=v.x;this.y-=v.y;this.z-=v.z;return this;}
multiplyScalar(s){this.x*=s;this.y*=s;this.z*=s;return this;}
distanceTo(v){let dx=this.x-v.x,dy=this.y-v.y,dz=this.z-v.z;return Math.sqrt(dx*dx+dy*dy+dz*dz);}
length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z);}
lerp(v,t){this.x+=(v.x-this.x)*t;this.y+=(v.y-this.y)*t;this.z+=(v.z-this.z)*t;return this;}
},
Euler: class {
constructor(x=0,y=0,z=0){this.x=x;this.y=y;this.z=z;}
set(x,y,z){this.x=x;this.y=y;this.z=z;return this;}
},
Color: class {
constructor(c){this.hex=c||0xffffff;}
setHex(h){this.hex=h;return this;}
},
Box3: class {
constructor(min, max){
this.min = min || new window.THREE.Vector3(-1,-1,-1);
this.max = max || new window.THREE.Vector3(1,1,1);
}
setFromObject(obj){
if(obj.position){
let p = obj.position, s = obj.scale || {x:1,y:1,z:1};
this.min.set(p.x - s.x, p.y - s.y, p.z - s.z);
this.max.set(p.x + s.x, p.y + s.y, p.z + s.z);
}
return this;
}
intersectsBox(b){
return !(this.max.x < b.min.x || this.min.x > b.max.x ||
this.max.y < b.min.y || this.min.y > b.max.y ||
this.max.z < b.min.z || this.min.z > b.max.z);
}
},
Scene: class {
constructor(){this.children=[];this.fog=null;}
add(o){this.children.push(o);if(o.parent)o.parent=this;}
remove(o){let i=this.children.indexOf(o);if(i>=0)this.children.splice(i,1);}
},
PerspectiveCamera: class {
constructor(fov, aspect, near, far){
this.fov=fov;this.aspect=aspect;this.near=near;this.far=far;
this.position=new window.THREE.Vector3(0,0,0);
this.rotation=new window.THREE.Euler(0,0,0);
}
lookAt(x,y,z){if(typeof x==='object'){y=x.y;z=x.z;x=x.x;}this.target={x,y,z};}
},
WebGLRenderer: class {
constructor(opts={}){
this.domElement = document.createElement('canvas');
this.domElement.id = "fallback-canvas";
this.domElement.style.width="100%";
this.domElement.style.height="100%";
this.ctx = this.domElement.getContext('2d');
this.shadowMap = {enabled:false, type:0};
}
setSize(w,h){this.domElement.width=w;this.domElement.height=h;}
setPixelRatio(r){}
render(scene, camera){
if(!this.ctx) return;
let w=this.domElement.width, h=this.domElement.height;
this.ctx.fillStyle = "#0c0414";
this.ctx.fillRect(0,0,w,h);
// Render basic gradient horizon for fallback
let grad = this.ctx.createLinearGradient(0,0,0,h);
grad.addColorStop(0, '#0a0312');
grad.addColorStop(0.5, '#4a154b');
grad.addColorStop(0.7, '#ff5500');
grad.addColorStop(1, '#1a0525');
this.ctx.fillStyle = grad;
this.ctx.fillRect(0,0,w,h);
// Draw glowing synthwave sun in center horizon
this.ctx.beginPath();
this.ctx.arc(w/2, h*0.6, 80, 0, Math.PI*2);
let sunGrad = this.ctx.createRadialGradient(w/2, h*0.6, 10, w/2, h*0.6, 90);
sunGrad.addColorStop(0, '#ffff55');
sunGrad.addColorStop(0.5, '#ff5500');
sunGrad.addColorStop(1, 'rgba(255,0,85,0)');
this.ctx.fillStyle = sunGrad;
this.ctx.fill();
}
},
Group: class {
constructor(){this.children=[];this.position=new window.THREE.Vector3();this.rotation=new window.THREE.Euler();this.scale=new window.THREE.Vector3(1,1,1);}
add(o){this.children.push(o);}
remove(o){let i=this.children.indexOf(o);if(i>=0)this.children.splice(i,1);}
},
Mesh: class {
constructor(geo, mat){
this.geometry=geo;this.material=mat;
this.position=new window.THREE.Vector3();
this.rotation=new window.THREE.Euler();
this.scale=new window.THREE.Vector3(1,1,1);
this.castShadow=false;this.receiveShadow=false;
}
},
BoxGeometry: class { constructor(w,h,d){this.w=w;this.h=h;this.d=d;} },
ConeGeometry: class { constructor(r,h,s){this.r=r;this.h=h;} },
OctahedronGeometry: class { constructor(r){this.r=r;} },
BufferGeometry: class { constructor(){this.attributes={};} setAttribute(){}},
Float32BufferAttribute: class { constructor(a,s){this.array=a;this.itemSize=s;} },
Points: class { constructor(g,m){this.geometry=g;this.material=m;this.position=new window.THREE.Vector3();} },
PointsMaterial: class { constructor(p){Object.assign(this,p);} },
MeshStandardMaterial: class { constructor(p){Object.assign(this,p);} },
MeshBasicMaterial: class { constructor(p){Object.assign(this,p);} },
AmbientLight: class { constructor(c,i){this.color=c;this.intensity=i;} },
DirectionalLight: class { constructor(c,i){this.color=c;this.intensity=i;this.position=new window.THREE.Vector3();this.shadow={mapSize:{width:0,height:0},camera:{near:0,far:0,left:0,right:0,top:0,bottom:0}};} },
PointLight: class { constructor(c,i,d){this.color=c;this.intensity=i;this.distance=d;this.position=new window.THREE.Vector3();} },
FogExp2: class { constructor(c,d){this.color=c;this.density=d;} },
PCFSoftShadowMap: 1
};
})();
// --- AUDIO SYNTHESIZER ENGINE (Web Audio API) ---
class SoundEngine {
constructor() {
this.ctx = null;
this.muted = false;
this.engineGain = null;
this.engineOsc = null;
this.engineFilter = null;
}
init() {
if (this.ctx) return;
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) return;
this.ctx = new AudioCtx();
// Continuous Engine Hum Oscillator
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.setValueAtTime(55, this.ctx.currentTime); // Low pitch A1
this.engineFilter = this.ctx.createBiquadFilter();
this.engineFilter.type = 'lowpass';
this.engineFilter.frequency.setValueAtTime(180, this.ctx.currentTime);
this.engineGain = this.ctx.createGain();
this.engineGain.gain.setValueAtTime(0.12, this.ctx.currentTime);
this.engineOsc.connect(this.engineFilter);
this.engineFilter.connect(this.engineGain);
this.engineGain.connect(this.ctx.destination);
this.engineOsc.start();
}
updateEngineSound(speedRatio, isBoosting) {
if (!this.ctx || this.muted) return;
const targetFreq = 50 + speedRatio * 180 + (isBoosting ? 90 : 0);
const filterFreq = 160 + speedRatio * 600 + (isBoosting ? 400 : 0);
this.engineOsc.frequency.setTargetAtTime(targetFreq, this.ctx.currentTime, 0.05);
this.engineFilter.frequency.setTargetAtTime(filterFreq, this.ctx.currentTime, 0.05);
}
playOrbPickup() {
if (!this.ctx || this.muted) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(523.25, t); // C5
osc.frequency.exponentialRampToValueAtTime(1046.50, t + 0.15); // C6
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.25);
}
playNearMiss() {
if (!this.ctx || this.muted) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const filter = this.ctx.createBiquadFilter();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(220, t);
osc.frequency.exponentialRampToValueAtTime(880, t + 0.18);
filter.type = 'bandpass';
filter.frequency.setValueAtTime(600, t);
filter.Q.setValueAtTime(8, t);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.22);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.22);
}
playCrash() {
if (!this.ctx || this.muted) return;
const t = this.ctx.currentTime;
// Noise buffer for explosion
const bufferSize = this.ctx.sampleRate * 0.8;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, t);
filter.frequency.exponentialRampToValueAtTime(40, t + 0.8);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.6, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.8);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(t);
}
toggleMute() {
this.muted = !this.muted;
if (this.engineGain) {
this.engineGain.gain.setValueAtTime(this.muted ? 0 : 0.12, this.ctx ? this.ctx.currentTime : 0);
}
return this.muted;
}
}
const sound = new SoundEngine();
// --- GAME ENGINE & THREE.JS SETUP ---
let container, scene, camera, renderer;
let playerCraft, sunMesh;
let speedStardust;
const STATE = {
MENU: 'MENU',
PLAYING: 'PLAYING',
GAMEOVER: 'GAMEOVER'
};
let currentState = STATE.MENU;
// Game Physics & Stats Variables
const game = {
score: 0,
highScore: parseInt(localStorage.getItem('solar_runner_hi') || '0', 10),
multiplier: 1,
nearMissCount: 0,
orbsCollected: 0,
distance: 0,
speed: 70, // current forward speed units/sec
baseSpeed: 70,
maxSpeed: 170,
energy: 100, // 0 to 100
boost: 100, // boost meter
isBoosting: false,
cameraShake: 0,
time: 0
};
// Controls state
const keys = {
left: false,
right: false,
up: false,
down: false,
boost: false
};
// Player Flight Kinematics
const playerPhysics = {
pos: new THREE.Vector3(0, 4, 0),
vel: new THREE.Vector3(0, 0, 0),
roll: 0, // Current roll angle
pitch: 0, // Current pitch angle
yaw: 0, // Yaw angle
targetRoll: 0,
targetPitch: 0,
bounds: new THREE.Box3()
};
// Procedural Canyon Segment Manager
const SEGMENT_LENGTH = 120;
const TOTAL_SEGMENTS = 12;
let segments = [];
let activeOrbs = [];
let activeObstacles = [];
let particles = [];
// Initialize WebGL Application
function initApp() {
container = document.getElementById('canvas-container');
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x180528, 0.0035);
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1200);
camera.position.set(0, 8, 14);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
if (renderer.shadowMap) {
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
}
container.appendChild(renderer.domElement);
// Lighting Setup (Sunset Atmosphere)
const ambientLight = new THREE.AmbientLight(0x4a2060, 1.2);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xff7722, 2.5);
sunLight.position.set(0, 20, -500);
if (sunLight.shadow) {
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 800;
sunLight.shadow.camera.left = -100;
sunLight.shadow.camera.right = 100;
sunLight.shadow.camera.top = 100;
sunLight.shadow.camera.bottom = -50;
}
scene.add(sunLight);
// Cyber Sun Disk on Horizon
const sunGeo = new THREE.OctahedronGeometry(45, 3);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffaa00, wireframe: false });
sunMesh = new THREE.Mesh(sunGeo, sunMat);
sunMesh.position.set(0, 15, -700);
scene.add(sunMesh);
// Build Player Craft
createPlayerCraft();
// Build Environmental Stardust Speed Particles
createSpeedStardust();
// Spawn Initial Canyon Segments
buildInitialCanyon();
// Setup Event Listeners
window.addEventListener('resize', onWindowResize, false);
setupControls();
updateHUD();
// Start Render Loop
requestAnimationFrame(gameLoop);
}
// --- PLAYER CRAFT GEOMETRY & MATERIAL ---
function createPlayerCraft() {
playerCraft = new THREE.Group();
// Fuselage / Main Body
const bodyGeo = new THREE.ConeGeometry(1.2, 5, 5);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x111625,
roughness: 0.2,
metalness: 0.8,
flatShading: true
});
const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.castShadow = true;
playerCraft.add(bodyMesh);
// Swept Delta Wings
const wingShape = new THREE.Group();
const wingGeo = new THREE.BoxGeometry(7, 0.15, 2.2);
const wingMat = new THREE.MeshStandardMaterial({
color: 0xff3300,
roughness: 0.3,
metalness: 0.7,
flatShading: true
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, 0, 0.4);
wings.castShadow = true;
playerCraft.add(wings);
// Cockpit Canopy
const canopyGeo = new THREE.BoxGeometry(0.8, 0.6, 1.8);
const canopyMat = new THREE.MeshStandardMaterial({
color: 0x00ffff,
roughness: 0.1,
metalness: 0.9,
flatShading: true
});
const canopy = new THREE.Mesh(canopyGeo, canopyMat);
canopy.position.set(0, 0.4, -0.2);
playerCraft.add(canopy);
// Engine Thruster Glow Nodes
const engineGeo = new THREE.ConeGeometry(0.5, 1.5, 6);
engineGeo.rotateX(-Math.PI / 2);
const engineMat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
const engineMesh = new THREE.Mesh(engineGeo, engineMat);
engineMesh.position.set(0, 0, 2.2);
playerCraft.add(engineMesh);
// Dynamic Ship Point Light
const shipLight = new THREE.PointLight(0x00ffff, 1.5, 25);
shipLight.position.set(0, 0, 1);
playerCraft.add(shipLight);
scene.add(playerCraft);
}
// Environmental Stardust Particles for Speed Feeling
function createSpeedStardust() {
const particleCount = 200;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 120;
positions[i + 1] = Math.random() * 40;
positions[i + 2] = -Math.random() * 400;
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xffaa55,
size: 0.8,
transparent: true,
opacity: 0.6
});
speedStardust = new THREE.Points(geo, mat);
scene.add(speedStardust);
}
// --- PROCEDURAL CANYON GENERATION ---
function getCanyonCurveX(z) {
// Smooth serpentine canyon winding
return Math.sin(z * 0.008) * 28 + Math.cos(z * 0.003) * 15;
}
function createCanyonSegment(zPos, index) {
const segmentGroup = new THREE.Group();
segmentGroup.position.z = zPos;
const centerX = getCanyonCurveX(zPos);
const canyonWidth = Math.max(32, 65 - (index * 0.8)); // Gradually narrows with progress
const wallHeight = 45;
// Canyon Wall Materials
const wallMat = new THREE.MeshStandardMaterial({
color: 0x241138,
roughness: 0.8,
metalness: 0.2,
flatShading: true
});
const floorMat = new THREE.MeshStandardMaterial({
color: 0x0d0518,
roughness: 0.9,
flatShading: true
});
// Floor Mesh
const floorGeo = new THREE.BoxGeometry(canyonWidth + 20, 2, SEGMENT_LENGTH);
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.position.set(centerX, -1, -SEGMENT_LENGTH / 2);
floorMesh.receiveShadow = true;
segmentGroup.add(floorMesh);
// Left Wall Formation
const leftWallGeo = new THREE.BoxGeometry(30, wallHeight, SEGMENT_LENGTH);
const leftWall = new THREE.Mesh(leftWallGeo, wallMat);
leftWall.position.set(centerX - (canyonWidth / 2) - 15, wallHeight / 2 - 2, -SEGMENT_LENGTH / 2);
leftWall.castShadow = true;
leftWall.receiveShadow = true;
segmentGroup.add(leftWall);
// Right Wall Formation
const rightWallGeo = new THREE.BoxGeometry(30, wallHeight, SEGMENT_LENGTH);
const rightWall = new THREE.Mesh(rightWallGeo, wallMat);
rightWall.position.set(centerX + (canyonWidth / 2) + 15, wallHeight / 2 - 2, -SEGMENT_LENGTH / 2);
rightWall.castShadow = true;
rightWall.receiveShadow = true;
segmentGroup.add(rightWall);
// --- OBSTACLES & PROCEDURAL VARIETY ---
// Random obstacle placement based on index & difficulty
if (index > 1) {
const obstacleType = Math.floor(Math.random() * 4);
const obstacleZ = zPos - SEGMENT_LENGTH * 0.5;
if (obstacleType === 1) {
// Monolith Pillar
const pWidth = 4 + Math.random() * 4;
const pHeight = 25 + Math.random() * 15;
const pxOffset = centerX + (Math.random() - 0.5) * (canyonWidth - 12);
const pillarGeo = new THREE.BoxGeometry(pWidth, pHeight, pWidth);
const pillarMat = new THREE.MeshStandardMaterial({
color: 0x3d1c59,
roughness: 0.4,
flatShading: true
});
const pillar = new THREE.Mesh(pillarGeo, pillarMat);
pillar.position.set(pxOffset, pHeight / 2, -SEGMENT_LENGTH / 2);
pillar.castShadow = true;
pillar.receiveShadow = true;
segmentGroup.add(pillar);
activeObstacles.push({ mesh: pillar, pos: new THREE.Vector3(pxOffset, pHeight / 2, obstacleZ), radius: pWidth });
}
else if (obstacleType === 2) {
// Tight Squeeze Arch / Gate
const gapWidth = 12;
const gateMat = new THREE.MeshStandardMaterial({ color: 0x5e1943, flatShading: true });
const leftBar = new THREE.Mesh(new THREE.BoxGeometry(canyonWidth/2, 18, 4), gateMat);
leftBar.position.set(centerX - gapWidth/2 - canyonWidth/4, 9, -SEGMENT_LENGTH / 2);
const rightBar = new THREE.Mesh(new THREE.BoxGeometry(canyonWidth/2, 18, 4), gateMat);
rightBar.position.set(centerX + gapWidth/2 + canyonWidth/4, 9, -SEGMENT_LENGTH / 2);
segmentGroup.add(leftBar);
segmentGroup.add(rightBar);
activeObstacles.push({ mesh: leftBar, pos: new THREE.Vector3(leftBar.position.x, 9, obstacleZ), radius: canyonWidth/4 });
activeObstacles.push({ mesh: rightBar, pos: new THREE.Vector3(rightBar.position.x, 9, obstacleZ), radius: canyonWidth/4 });
}
}
// --- ENERGY ORB PICKUPS ---
if (Math.random() < 0.75 && index > 0) {
const orbGeo = new THREE.OctahedronGeometry(1.2, 0);
const orbMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: false });
const orbMesh = new THREE.Mesh(orbGeo, orbMat);
const orbX = centerX + (Math.random() - 0.5) * (canyonWidth - 16);
const orbY = 2 + Math.random() * 8;
orbMesh.position.set(orbX, orbY, -SEGMENT_LENGTH / 2);
// Glowing outer aura ring
const auraGeo = new THREE.OctahedronGeometry(1.8, 1);
const auraMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: true, transparent: true, opacity: 0.4 });
const auraMesh = new THREE.Mesh(auraGeo, auraMat);
orbMesh.add(auraMesh);
segmentGroup.add(orbMesh);
activeOrbs.push({
mesh: orbMesh,
group: segmentGroup,
pos: new THREE.Vector3(orbX, orbY, zPos - SEGMENT_LENGTH / 2),
collected: false
});
}
scene.add(segmentGroup);
return { group: segmentGroup, zPos: zPos, index: index, centerX: centerX, width: canyonWidth };
}
function buildInitialCanyon() {
for (let i = 0; i < TOTAL_SEGMENTS; i++) {
const seg = createCanyonSegment(-i * SEGMENT_LENGTH, i);
segments.push(seg);
}
}
function updateCanyonChunks() {
const shipZ = playerPhysics.pos.z;
// Recycle segments behind the player
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (seg.zPos > shipZ + SEGMENT_LENGTH) {
// Find furthest segment
let minZ = 0;
let maxIndex = 0;
for (let s of segments) {
if (s.zPos < minZ) minZ = s.zPos;
if (s.index > maxIndex) maxIndex = s.index;
}
scene.remove(seg.group);
// Spawn recycled segment ahead
const newZ = minZ - SEGMENT_LENGTH;
const newSeg = createCanyonSegment(newZ, maxIndex + 1);
segments[i] = newSeg;
}
}
// Cleanup despawned orbs & obstacles
activeOrbs = activeOrbs.filter(o => o.pos.z < shipZ + 20 && !o.collected);
activeObstacles = activeObstacles.filter(ob => ob.pos.z < shipZ + 20);
}
// --- CONTROLS & INPUT HANDLING ---
function setupControls() {
window.addEventListener('keydown', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = true;
if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = true;
if (e.code === 'Space') keys.boost = true;
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = false;
if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = false;
if (e.code === 'Space') keys.boost = false;
});
// Mouse / Touch Steering Support
let touchStartX = 0;
let touchStartY = 0;
window.addEventListener('touchstart', (e) => {
if (e.touches.length > 0) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
if (currentState === STATE.PLAYING) sound.init();
}
}, { passive: true });
window.addEventListener('touchmove', (e) => {
if (e.touches.length > 0 && currentState === STATE.PLAYING) {
const deltaX = e.touches[0].clientX - touchStartX;
const deltaY = e.touches[0].clientY - touchStartY;
keys.left = deltaX < -15;
keys.right = deltaX > 15;
keys.up = deltaY < -15;
keys.down = deltaY > 15;
}
}, { passive: true });
window.addEventListener('touchend', () => {
keys.left = false; keys.right = false; keys.up = false; keys.down = false;
});
// UI Buttons
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', startGame);
document.getElementById('audio-toggle').addEventListener('click', () => {
const isMuted = sound.toggleMute();
document.getElementById('audio-toggle').innerText = isMuted ? '🔇' : '🔊';
});
}
// --- GAMEPLAY LOOPS & PHYSICS ---
function startGame() {
sound.init();
// Reset Stats
game.score = 0;
game.multiplier = 1;
game.nearMissCount = 0;
game.orbsCollected = 0;
game.distance = 0;
game.speed = game.baseSpeed;
game.energy = 100;
game.boost = 100;
game.isBoosting = false;
// Reset Player Position
playerPhysics.pos.set(0, 4, 0);
playerPhysics.vel.set(0, 0, 0);
playerPhysics.roll = 0;
playerPhysics.pitch = 0;
// Hide Overlays
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('gameover-screen').classList.add('hidden');
currentState = STATE.PLAYING;
}
function triggerGameOver(cause) {
currentState = STATE.GAMEOVER;
sound.playCrash();
if (game.score > game.highScore) {
game.highScore = Math.floor(game.score);
localStorage.setItem('solar_runner_hi', game.highScore.toString());
}
document.getElementById('gameover-cause').innerText = cause || 'CRASH DETECTED';
document.getElementById('go-score').innerText = Math.floor(game.score);
document.getElementById('go-speed').innerText = Math.floor(game.maxSpeed) + " KM/H";
document.getElementById('go-nearmiss').innerText = game.nearMissCount;
document.getElementById('go-orbs').innerText = game.orbsCollected;
document.getElementById('gameover-screen').classList.remove('hidden');
}
function triggerNearMiss() {
game.nearMissCount++;
game.multiplier = Math.min(10, game.multiplier + 1);
game.score += 250 * game.multiplier;
game.energy = Math.min(100, game.energy + 10);
game.cameraShake = 0.4;
sound.playNearMiss();
const banner = document.getElementById('near-miss-banner');
banner.innerText = `NEAR MISS +${250 * game.multiplier}`;
banner.classList.add('show');
setTimeout(() => banner.classList.remove('show'), 600);
const badge = document.getElementById('hud-multiplier');
badge.classList.add('pulse');
setTimeout(() => badge.classList.remove('pulse'), 200);
}
function updateFlightPhysics(delta) {
// Lateral Banking & Roll Momentum
const bankSpeed = 2.8;
const pitchSpeed = 2.2;
if (keys.left) playerPhysics.targetRoll = 0.75;
else if (keys.right) playerPhysics.targetRoll = -0.75;
else playerPhysics.targetRoll = 0;
if (keys.up) playerPhysics.targetPitch = -0.4;
else if (keys.down) playerPhysics.targetPitch = 0.4;
else playerPhysics.targetPitch = 0;
// Lerp smooth movement
playerPhysics.roll += (playerPhysics.targetRoll - playerPhysics.roll) * bankSpeed * delta * 5;
playerPhysics.pitch += (playerPhysics.targetPitch - playerPhysics.pitch) * pitchSpeed * delta * 5;
// Calculate Lateral Velocity based on Roll
playerPhysics.vel.x = -playerPhysics.roll * 22;
playerPhysics.vel.y = -playerPhysics.pitch * 16;
// Turbo Boost Management
if (keys.boost && game.boost > 5) {
game.isBoosting = true;
game.boost = Math.max(0, game.boost - delta * 35);
game.speed = game.baseSpeed * 1.6;
game.cameraShake = Math.max(game.cameraShake, 0.12);
} else {
game.isBoosting = false;
game.boost = Math.min(100, game.boost + delta * 12);
game.speed = game.baseSpeed + (game.distance * 0.005);
}
if (game.speed > game.maxSpeed) game.maxSpeed = game.speed;
// Advance Forward Position
playerPhysics.pos.z -= game.speed * delta;
playerPhysics.pos.x += playerPhysics.vel.x * delta;
playerPhysics.pos.y += playerPhysics.vel.y * delta;
// Clamp altitude
playerPhysics.pos.y = Math.max(1.5, Math.min(22, playerPhysics.pos.y));
// Apply transformations to Three.js Craft Mesh
playerCraft.position.copy(playerPhysics.pos);
playerCraft.rotation.z = playerPhysics.roll;
playerCraft.rotation.x = playerPhysics.pitch;
// Energy Decay
const energyDecayRate = game.isBoosting ? 8 : 3.5;
game.energy -= energyDecayRate * delta;
if (game.energy <= 0) {
triggerGameOver('ENERGY DEPLETED');
}
// Update Sound Engine Pitch
sound.updateEngineSound(game.speed / game.maxSpeed, game.isBoosting);
}
function checkCollisionsAndNearMisses() {
const shipPos = playerPhysics.pos;
const currentSegIndex = Math.floor(-shipPos.z / SEGMENT_LENGTH);
const currentSeg = segments.find(s => s.index === currentSegIndex) || segments[0];
if (currentSeg) {
const canyonCenterX = currentSeg.centerX;
const halfWidth = currentSeg.width / 2;
const distLeftWall = shipPos.x - (canyonCenterX - halfWidth);
const distRightWall = (canyonCenterX + halfWidth) - shipPos.x;
// Collision with Canyon Walls
if (distLeftWall < 1.8 || distRightWall < 1.8) {
triggerGameOver('WALL IMPACT');
return;
}
// Near-Miss Proximity Check with Canyon Walls
if (distLeftWall < 5.5 || distRightWall < 5.5) {
if (Math.random() < 0.08) {
triggerNearMiss();
}
}
}
// Check Obstacle Collisions & Near Misses
for (let ob of activeObstacles) {
const dist = shipPos.distanceTo(ob.pos);
if (dist < ob.radius * 0.8) {
triggerGameOver('MONOLITH COLLISION');
return;
} else if (dist < ob.radius + 4.5 && dist >= ob.radius * 0.8) {
if (Math.random() < 0.06) triggerNearMiss();
}
}
// Check Orb Pickups
for (let orb of activeOrbs) {
if (!orb.collected) {
orb.mesh.rotation.y += 0.05;
const dist = shipPos.distanceTo(orb.pos);
if (dist < 3.2) {
orb.collected = true;
orb.mesh.visible = false;
game.orbsCollected++;
game.energy = Math.min(100, game.energy + 25);
game.score += 500 * game.multiplier;
sound.playOrbPickup();
}
}
}
}
function updateCamera(delta) {
// Dynamic Smooth Third-Person Camera
const targetCamPos = new THREE.Vector3(
playerPhysics.pos.x * 0.7,
playerPhysics.pos.y + 3.8,
playerPhysics.pos.z + 11.5
);
// Camera Shake
if (game.cameraShake > 0) {
targetCamPos.x += (Math.random() - 0.5) * game.cameraShake * 2;
targetCamPos.y += (Math.random() - 0.5) * game.cameraShake * 2;
game.cameraShake = Math.max(0, game.cameraShake - delta * 2);
}
camera.position.lerp(targetCamPos, delta * 8);
camera.lookAt(playerPhysics.pos.x * 0.5, playerPhysics.pos.y + 0.5, playerPhysics.pos.z - 20);
// FOV Expansion on Boost
const targetFOV = game.isBoosting ? 82 : 65;
camera.fov += (targetFOV - camera.fov) * delta * 6;
camera.updateProjectionMatrix();
// Sun Follows Player Z
if (sunMesh) {
sunMesh.position.z = playerPhysics.pos.z - 700;
sunMesh.position.x = getCanyonCurveX(playerPhysics.pos.z - 700);
}
// Stardust particle field positioning
if (speedStardust) {
speedStardust.position.z = playerPhysics.pos.z;
speedStardust.position.x = playerPhysics.pos.x;
}
}
function updateHUD() {
document.getElementById('hud-score').innerText = Math.floor(game.score);
document.getElementById('hud-highscore').innerText = game.highScore;
document.getElementById('hud-multiplier').innerText = `x${game.multiplier}`;
document.getElementById('hud-speed').innerText = Math.floor(game.speed * 2.2);
const energyFill = document.getElementById('energy-fill');
const energyPct = Math.max(0, game.energy);
energyFill.style.width = `${energyPct}%`;
document.getElementById('energy-text').innerText = `${Math.floor(energyPct)}%`;
if (energyPct < 25) energyFill.classList.add('warning');
else energyFill.classList.remove('warning');
const boostFill = document.getElementById('boost-fill');
boostFill.style.width = `${game.boost}%`;
document.getElementById('boost-text').innerText = game.isBoosting ? 'BOOSTING' : (game.boost > 20 ? 'READY' : 'RECHARGING');
}
// --- MAIN GAME LOOP ---
let lastTime = performance.now();
function gameLoop(now) {
requestAnimationFrame(gameLoop);
const delta = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
game.time += delta;
if (currentState === STATE.PLAYING) {
game.distance += game.speed * delta;
game.score += game.speed * delta * 0.1 * game.multiplier;
updateFlightPhysics(delta);
updateCanyonChunks();
checkCollisionsAndNearMisses();
updateHUD();
} else if (currentState === STATE.MENU) {
// Idle camera animation on menu screen
playerCraft.rotation.y = Math.sin(game.time * 1.2) * 0.3;
playerCraft.position.y = 4 + Math.sin(game.time * 2) * 0.5;
}
updateCamera(delta);
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('load', initApp);
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.6-flash","provider":"openrouter","persona":"full","sessionId":"b5dfa074-7d66-4a14-91f1-6d4eb353e1c4","tokensIn":111247,"tokensOut":22585,"tokensTotal":133832,"turns":6,"toolCalls":5,"failedToolCalls":0,"timestamp":"2026-07-22T13:59:10.283Z"} -->