Canyon Run
Gemini 3.7 Flash · full
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. 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 66,957 bytes · SHA-256 7bf216751213
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Solar Velocity - High-Speed Canyon Racer</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body, html {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #0d0614;
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
color: #fff;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* UI Overlay */
#ui-layer {
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 32px;
box-sizing: border-box;
}
/* Top HUD */
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.stat-box {
background: rgba(18, 9, 32, 0.45);
border: 1px solid rgba(255, 140, 50, 0.25);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5), inset 0 0 15px rgba(255,100,20,0.05);
letter-spacing: 1px;
}
.stat-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
color: #ff9d5c;
letter-spacing: 2px;
margin-bottom: 2px;
}
.stat-value {
font-size: 28px;
font-weight: 900;
font-family: 'Courier New', monospace;
color: #ffffff;
text-shadow: 0 0 12px rgba(255, 160, 60, 0.6);
}
.multiplier-wrap {
display: flex;
flex-direction: column;
align-items: center;
min-width: 130px;
transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.multiplier-value {
font-size: 38px;
font-weight: 900;
font-family: 'Courier New', monospace;
color: #ffde59;
text-shadow: 0 0 16px rgba(255, 220, 80, 0.8), 0 0 30px rgba(255, 100, 20, 0.5);
}
.multiplier-pulse {
transform: scale(1.35);
filter: brightness(1.5);
}
/* Energy & Speed Center Bottom HUD */
.hud-bottom {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
width: 100%;
max-width: 640px;
margin: 0 auto;
}
.near-miss-banner {
font-size: 20px;
font-weight: 900;
letter-spacing: 4px;
text-transform: uppercase;
color: #fff;
text-shadow: 0 0 15px #ff5e3a, 0 0 30px #ff2a85;
opacity: 0;
transform: translateY(10px) scale(0.9);
transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
}
.near-miss-banner.active {
opacity: 1;
transform: translateY(0) scale(1.15);
}
.gauge-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(13, 6, 24, 0.6);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 16px 24px;
border-radius: 8px;
border: 1px solid rgba(255, 120, 60, 0.3);
box-shadow: 0 10px 30px rgba(0,0,0,0.6);
}
.bar-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.bar-title {
font-size: 11px;
font-weight: 800;
letter-spacing: 2px;
text-transform: uppercase;
width: 70px;
color: #ff9d5c;
}
.progress-track {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.progress-fill {
height: 100%;
width: 100%;
border-radius: 4px;
transition: width 0.08s linear;
}
#energy-fill {
background: linear-gradient(90deg, #ff2a6d, #ff9500, #ffea00);
box-shadow: 0 0 12px rgba(255, 149, 0, 0.8);
}
#turbo-fill {
background: linear-gradient(90deg, #05d9e8, #0056ff, #d600ff);
box-shadow: 0 0 12px rgba(5, 217, 232, 0.8);
}
.turbo-ready {
animation: turboGlow 0.8s infinite alternate;
}
@keyframes turboGlow {
from { box-shadow: 0 0 8px #05d9e8; }
to { box-shadow: 0 0 20px #05d9e8, 0 0 35px #d600ff; }
}
.speedo-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
}
.speedo-marks {
display: flex;
gap: 3px;
flex: 1;
height: 4px;
margin: 0 16px;
}
.speed-tick {
flex: 1;
background: rgba(255,255,255,0.12);
border-radius: 1px;
transition: background 0.05s ease;
}
.speed-tick.lit {
background: #ffaa40;
box-shadow: 0 0 6px #ff8800;
}
.speed-tick.lit-boost {
background: #00f0ff;
box-shadow: 0 0 8px #00f0ff;
}
.speed-digits {
font-family: 'Courier New', monospace;
font-size: 14px;
font-weight: 800;
color: #ffc480;
min-width: 65px;
text-align: right;
}
/* Screen Tint / Boost Overlay */
#speed-vignette {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 5;
background: radial-gradient(circle, transparent 40%, rgba(255, 60, 0, 0) 80%);
opacity: 0;
transition: opacity 0.3s ease;
}
#speed-lines-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 6;
opacity: 0;
transition: opacity 0.2s ease;
}
/* Screen Modals (Menu / Game Over) */
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
background: rgba(8, 3, 15, 0.78);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
transition: opacity 0.4s ease, visibility 0.4s;
}
.modal-backdrop.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.modal-card {
background: linear-gradient(135deg, rgba(30, 12, 45, 0.9) 0%, rgba(15, 6, 25, 0.95) 100%);
border: 2px solid rgba(255, 120, 50, 0.4);
box-shadow: 0 0 50px rgba(255, 60, 0, 0.3), inset 0 0 30px rgba(255, 100, 20, 0.1);
border-radius: 12px;
padding: 40px 48px;
max-width: 480px;
width: 90%;
text-align: center;
color: #fff;
}
.game-title {
font-size: 38px;
font-weight: 900;
letter-spacing: 6px;
text-transform: uppercase;
margin-bottom: 6px;
background: linear-gradient(90deg, #ffea00, #ff5e3a, #ff2a85);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 25px rgba(255, 94, 58, 0.4);
}
.game-subtitle {
font-size: 13px;
letter-spacing: 4px;
text-transform: uppercase;
color: #ff9d5c;
margin-bottom: 28px;
}
.instructions-list {
text-align: left;
font-size: 13px;
line-height: 1.8;
color: #d8c8e8;
margin-bottom: 30px;
background: rgba(255, 255, 255, 0.04);
padding: 18px 22px;
border-radius: 6px;
border-left: 3px solid #ff7a28;
}
.key-badge {
display: inline-block;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 3px;
padding: 1px 7px;
font-size: 11px;
font-weight: 700;
color: #fff;
margin: 0 2px;
font-family: monospace;
}
.action-btn {
background: linear-gradient(90deg, #ff5e3a, #ff2a85);
color: #ffffff;
border: none;
padding: 16px 36px;
font-size: 16px;
font-weight: 800;
letter-spacing: 3px;
text-transform: uppercase;
border-radius: 30px;
cursor: pointer;
box-shadow: 0 0 25px rgba(255, 94, 58, 0.6);
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
outline: none;
}
.action-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 35px rgba(255, 94, 58, 0.9), 0 0 60px rgba(255, 42, 133, 0.5);
}
.action-btn:active {
transform: scale(0.97);
}
/* Score Table in Game Over */
.score-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin: 24px 0 32px 0;
}
.summary-item {
background: rgba(255,255,255,0.04);
padding: 12px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.07);
}
.summary-item .label {
font-size: 10px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: #ff9d5c;
margin-bottom: 4px;
}
.summary-item .val {
font-size: 22px;
font-weight: 900;
font-family: 'Courier New', monospace;
color: #fff;
}
.highlight-val {
color: #ffea00 !important;
text-shadow: 0 0 10px rgba(255,234,0,0.5);
}
/* Mobile Controls Overlay */
#mobile-controls {
position: absolute;
bottom: 120px;
left: 0;
width: 100%;
display: none;
justify-content: space-between;
padding: 0 30px;
box-sizing: border-box;
pointer-events: auto;
z-index: 20;
}
.dpad-btn, .boost-touch-btn {
width: 65px;
height: 65px;
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 50%;
color: white;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
touch-action: manipulation;
}
.dpad-btn:active, .boost-touch-btn:active {
background: rgba(255, 94, 58, 0.6);
transform: scale(0.92);
}
@media (max-width: 768px) {
#mobile-controls {
display: flex;
}
.hud-top {
padding: 0 4px;
}
.stat-box {
padding: 8px 12px;
}
.stat-value {
font-size: 20px;
}
.multiplier-value {
font-size: 26px;
}
}
</style>
<!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<div id="canvas-container"></div>
<div id="speed-vignette"></div>
<canvas id="speed-lines-canvas"></canvas>
<div id="ui-layer">
<!-- Top HUD -->
<div class="hud-top">
<div class="stat-box">
<div class="stat-label">Score</div>
<div class="stat-value" id="score-display">000000</div>
</div>
<div class="stat-box multiplier-wrap" id="multiplier-box">
<div class="stat-label">Multiplier</div>
<div class="multiplier-value" id="multiplier-display">x1.0</div>
</div>
<div class="stat-box" style="text-align: right;">
<div class="stat-label">Distance</div>
<div class="stat-value" id="dist-display">0 m</div>
</div>
</div>
<!-- Center Near Miss Announcement -->
<div class="near-miss-banner" id="near-miss-banner">⚡ NEAR MISS +250 ⚡</div>
<!-- Bottom HUD (Energy & Speedometer) -->
<div class="hud-bottom">
<div class="gauge-container">
<!-- Energy Bar -->
<div class="bar-row">
<div class="bar-title">Solar Core</div>
<div class="progress-track">
<div class="progress-fill" id="energy-fill" style="width: 100%;"></div>
</div>
</div>
<!-- Turbo Bar -->
<div class="bar-row">
<div class="bar-title">Turbo</div>
<div class="progress-track">
<div class="progress-fill" id="turbo-fill" style="width: 0%;"></div>
</div>
</div>
<!-- Speedometer -->
<div class="speedo-row">
<div class="bar-title">Velocity</div>
<div class="speedo-marks" id="speed-ticks">
<!-- Dynamically populated ticks -->
</div>
<div class="speed-digits" id="speed-digits">180 KM/H</div>
</div>
</div>
</div>
</div>
<!-- Mobile Touch Controls -->
<div id="mobile-controls">
<div style="display: flex; gap: 15px;">
<button class="dpad-btn" id="btn-left">◀</button>
<button class="dpad-btn" id="btn-right">▶</button>
</div>
<div style="display: flex; gap: 15px;">
<button class="dpad-btn" id="btn-pitch">▲</button>
<button class="boost-touch-btn" id="btn-boost">🚀</button>
</div>
</div>
<!-- Start Menu Modal -->
<div class="modal-backdrop" id="start-menu">
<div class="modal-card">
<div class="game-title">Solar Velocity</div>
<div class="game-subtitle">High-Speed Canyon Flight</div>
<div class="instructions-list">
<div>• <b>Steer:</b> <span class="key-badge">A</span><span class="key-badge">D</span> or <span class="key-badge">←</span><span class="key-badge">→</span> (Banking flight physics)</div>
<div>• <b>Pitch:</b> <span class="key-badge">W</span><span class="key-badge">S</span> or <span class="key-badge">↑</span><span class="key-badge">↓</span> (Glide altitude control)</div>
<div>• <b>Turbo Boost:</b> <span class="key-badge">Space</span> or <span class="key-badge">Shift</span></div>
<div>• <b>Solar Energy:</b> Decays constantly — collect <span style="color:#ffea00">Amber Orbs</span> to recharge!</div>
<div>• <b>Proximity:</b> Skim close to canyon walls for high <b>Near-Miss Multipliers</b>!</div>
</div>
<button class="action-btn" id="start-btn">Engage Engines</button>
</div>
</div>
<!-- Game Over Modal -->
<div class="modal-backdrop hidden" id="game-over-menu">
<div class="modal-card">
<div class="game-title" id="game-over-title" style="font-size: 32px;">Solar Core Depleted</div>
<div class="game-subtitle" id="game-over-reason">Canyon Flight Terminated</div>
<div class="score-summary">
<div class="summary-item">
<div class="label">Total Score</div>
<div class="val highlight-val" id="final-score">0</div>
</div>
<div class="summary-item">
<div class="label">Distance Flown</div>
<div class="val" id="final-dist">0 m</div>
</div>
<div class="summary-item">
<div class="label">Max Multiplier</div>
<div class="val" id="final-multi">x1.0</div>
</div>
<div class="summary-item">
<div class="label">Near Misses</div>
<div class="val" id="final-nearmiss">0</div>
</div>
</div>
<button class="action-btn" id="restart-btn">Fly Again (R)</button>
</div>
</div>
<script>
/* =========================================================================
1. WEB AUDIO SYNTHESIS SYSTEM
========================================================================= */
class AudioSynthesizer {
constructor() {
this.ctx = null;
this.isMuted = false;
this.engineOsc1 = null;
this.engineOsc2 = null;
this.engineFilter = null;
this.engineGain = null;
this.windGain = null;
this.windFilter = null;
this.initialized = false;
}
init() {
if (this.initialized) return;
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
// Master Gain
this.masterGain = this.ctx.createGain();
this.masterGain.gain.setValueAtTime(0.7, this.ctx.currentTime);
this.masterGain.connect(this.ctx.destination);
// 1. Engine Drone (Dual saw/triangle + Lowpass)
this.engineGain = this.ctx.createGain();
this.engineGain.gain.setValueAtTime(0.001, this.ctx.currentTime);
this.engineFilter = this.ctx.createBiquadFilter();
this.engineFilter.type = 'lowpass';
this.engineFilter.frequency.setValueAtTime(180, this.ctx.currentTime);
this.engineFilter.Q.setValueAtTime(3.5, this.ctx.currentTime);
this.engineOsc1 = this.ctx.createOscillator();
this.engineOsc1.type = 'sawtooth';
this.engineOsc1.frequency.setValueAtTime(55, this.ctx.currentTime); // A1
this.engineOsc2 = this.ctx.createOscillator();
this.engineOsc2.type = 'triangle';
this.engineOsc2.frequency.setValueAtTime(110, this.ctx.currentTime); // A2
this.engineOsc1.connect(this.engineFilter);
this.engineOsc2.connect(this.engineFilter);
this.engineFilter.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc1.start();
this.engineOsc2.start();
// 2. High-speed Wind Noise Generator
const bufferSize = this.ctx.sampleRate * 2;
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const whiteNoise = this.ctx.createBufferSource();
whiteNoise.buffer = noiseBuffer;
whiteNoise.loop = true;
this.windFilter = this.ctx.createBiquadFilter();
this.windFilter.type = 'bandpass';
this.windFilter.frequency.setValueAtTime(400, this.ctx.currentTime);
this.windFilter.Q.setValueAtTime(1.2, this.ctx.currentTime);
this.windGain = this.ctx.createGain();
this.windGain.gain.setValueAtTime(0.02, this.ctx.currentTime);
whiteNoise.connect(this.windFilter);
this.windFilter.connect(this.windGain);
this.windGain.connect(this.masterGain);
whiteNoise.start();
this.initialized = true;
} catch (e) {
console.warn('Web Audio API not supported or blocked:', e);
}
}
updateEngine(speedRatio, isBoosting, proximityDist) {
if (!this.initialized || !this.ctx || this.ctx.state !== 'running') return;
const t = this.ctx.currentTime;
// Base frequency scales with velocity
const baseFreq = 50 + speedRatio * 90 + (isBoosting ? 65 : 0);
this.engineOsc1.frequency.setTargetAtTime(baseFreq, t, 0.08);
this.engineOsc2.frequency.setTargetAtTime(baseFreq * 2.01, t, 0.08);
const filterFreq = 160 + speedRatio * 900 + (isBoosting ? 1200 : 0);
this.engineFilter.frequency.setTargetAtTime(filterFreq, t, 0.06);
const targetVol = 0.12 + speedRatio * 0.18 + (isBoosting ? 0.15 : 0);
this.engineGain.gain.setTargetAtTime(targetVol, t, 0.05);
// Wind noise modulation
const windFreq = 200 + speedRatio * 1800 + (proximityDist < 12 ? 800 : 0);
const windVol = 0.03 + speedRatio * 0.22 + (isBoosting ? 0.15 : 0);
this.windFilter.frequency.setTargetAtTime(windFreq, t, 0.1);
this.windGain.gain.setTargetAtTime(windVol, t, 0.1);
}
playOrbCollect(multiplier) {
if (!this.initialized || !this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const basePitch = 523.25; // C5
const scale = [1, 1.25, 1.333, 1.5, 1.875, 2.0];
const pitchIdx = Math.min(Math.floor(multiplier), scale.length - 1);
const freq = basePitch * scale[pitchIdx];
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, t);
osc.frequency.exponentialRampToValueAtTime(freq * 1.5, t + 0.18);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.35);
}
playNearMiss() {
if (!this.initialized || !this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const filter = this.ctx.createBiquadFilter();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, t);
osc.frequency.exponentialRampToValueAtTime(660, t + 0.12);
osc.frequency.exponentialRampToValueAtTime(110, t + 0.3);
filter.type = 'bandpass';
filter.frequency.setValueAtTime(400, t);
filter.frequency.exponentialRampToValueAtTime(2400, t + 0.12);
filter.frequency.exponentialRampToValueAtTime(300, t + 0.3);
filter.Q.setValueAtTime(5.0, t);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.3);
}
playBoostIgnite() {
if (!this.initialized || !this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(120, t);
osc.frequency.exponentialRampToValueAtTime(480, t + 0.25);
gain.gain.setValueAtTime(0.35, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.4);
}
playCrash() {
if (!this.initialized || !this.ctx) return;
const t = this.ctx.currentTime;
// Low boom + noise burst
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(130, t);
osc.frequency.exponentialRampToValueAtTime(25, t + 0.8);
gain.gain.setValueAtTime(0.6, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.85);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.85);
// Mute engine
if (this.engineGain) {
this.engineGain.gain.setTargetAtTime(0, t, 0.1);
}
}
}
const audio = new AudioSynthesizer();
/* =========================================================================
2. THREE.JS SCENE SETUP & CYBER SUNSET AESTHETICS
========================================================================= */
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x190826, 0.0065); // Cyber Sunset purple/burgundy fog
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 900);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// Speed Lines Canvas 2D
const speedLinesCanvas = document.getElementById('speed-lines-canvas');
const speedLinesCtx = speedLinesCanvas.getContext('2d');
const speedVignette = document.getElementById('speed-vignette');
function resizeSpeedLines() {
speedLinesCanvas.width = window.innerWidth;
speedLinesCanvas.height = window.innerHeight;
}
resizeSpeedLines();
// Lighting setup for dramatic golden hour / cyber sunset long shadows
const ambientLight = new THREE.AmbientLight(0x421c59, 1.4);
scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0xff7b29, 0x1f0b3b, 1.2);
scene.add(hemiLight);
const sunLight = new THREE.DirectionalLight(0xffa238, 2.8);
sunLight.position.set(0, 45, -350);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 600;
sunLight.shadow.camera.left = -120;
sunLight.shadow.camera.right = 120;
sunLight.shadow.camera.top = 100;
sunLight.shadow.camera.bottom = -40;
sunLight.shadow.bias = -0.0005;
scene.add(sunLight);
// Glowing Giant Sun Disk on Horizon
const sunGeo = new THREE.CircleGeometry(160, 48);
const sunMat = new THREE.MeshBasicMaterial({
color: 0xff4b1f,
side: THREE.DoubleSide
});
const sunMesh = new THREE.Mesh(sunGeo, sunMat);
sunMesh.position.set(0, 45, -750);
scene.add(sunMesh);
// Sun Inner Core
const sunInnerGeo = new THREE.CircleGeometry(130, 48);
const sunInnerMat = new THREE.MeshBasicMaterial({
color: 0xffd200,
side: THREE.DoubleSide
});
const sunInner = new THREE.Mesh(sunInnerGeo, sunInnerMat);
sunInner.position.set(0, 45, -748);
scene.add(sunInner);
// Horizon Grid / Mountain Backdrops
const bgMountainsGroup = new THREE.Group();
const mountainMat = new THREE.MeshLambertMaterial({
color: 0x240e3b
});
for (let i = -12; i <= 12; i++) {
const h = 40 + Math.random() * 70;
const w = 60 + Math.random() * 50;
const mGeo = new THREE.ConeGeometry(w, h, 4);
const mMesh = new THREE.Mesh(mGeo, mountainMat);
mMesh.position.set(i * 50 + (Math.random() - 0.5) * 20, h * 0.4 - 10, -680 + (Math.random() - 0.5) * 60);
mMesh.rotation.y = Math.random() * Math.PI;
bgMountainsGroup.add(mMesh);
}
scene.add(bgMountainsGroup);
/* =========================================================================
3. PLAYER CRAFT MODEL & WING DYNAMICS
========================================================================= */
const playerGroup = new THREE.Group();
scene.add(playerGroup);
// Sleek geometric low-poly interceptor craft
const shipBodyGroup = new THREE.Group();
playerGroup.add(shipBodyGroup);
// Main fuselage (sharp wedge)
const fuselageGeo = new THREE.BufferGeometry();
const vertices = new Float32Array([
// Top central nose to wingroots
0, 0.4, -2.4, -1.4, 0.1, 1.2, 1.4, 0.1, 1.2,
0, 0.4, -2.4, 1.4, 0.1, 1.2, 0, 0.6, 1.0,
0, 0.4, -2.4, 0, 0.6, 1.0, -1.4, 0.1, 1.2,
// Cockpit / Canopy
0, 0.7, -0.6, -0.5, 0.35, 0.4, 0.5, 0.35, 0.4,
0, 0.7, -0.6, 0, 0.3, -1.5, -0.5, 0.35, 0.4,
0, 0.7, -0.6, 0.5, 0.35, 0.4, 0, 0.3, -1.5,
// Wings Left / Right (swept forward-angled poly)
-1.4, 0.1, 1.2, -3.2, 0.2, 0.4, -2.2, -0.05, 1.6,
1.4, 0.1, 1.2, 2.2, -0.05, 1.6, 3.2, 0.2, 0.4,
-1.4, 0.1, 1.2, 0, 0.6, 1.0, -2.2, -0.05, 1.6,
1.4, 0.1, 1.2, 2.2, -0.05, 1.6, 0, 0.6, 1.0,
// Bottom Belly
0, -0.3, -2.2, 1.4, 0.1, 1.2, -1.4, 0.1, 1.2,
0, -0.3, -2.2, -1.4, 0.1, 1.2, 0, -0.2, 1.4,
0, -0.3, -2.2, 0, -0.2, 1.4, 1.4, 0.1, 1.2,
]);
fuselageGeo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
fuselageGeo.computeVertexNormals();
const shipMaterial = new THREE.MeshStandardMaterial({
color: 0xe8e8f2,
roughness: 0.25,
metalness: 0.85,
flatShading: true
});
const shipMesh = new THREE.Mesh(fuselageGeo, shipMaterial);
shipMesh.castShadow = true;
shipMesh.receiveShadow = true;
shipBodyGroup.add(shipMesh);
// Dark trim / cockpit canopy
const canopyGeo = new THREE.ConeGeometry(0.55, 1.6, 4);
canopyGeo.rotateX(Math.PI / 2);
const canopyMat = new THREE.MeshStandardMaterial({
color: 0x05d9e8,
roughness: 0.1,
metalness: 0.9,
emissive: 0x012b33,
flatShading: true
});
const canopyMesh = new THREE.Mesh(canopyGeo, canopyMat);
canopyMesh.position.set(0, 0.4, -0.1);
canopyMesh.scale.set(0.8, 0.5, 1.1);
shipBodyGroup.add(canopyMesh);
// Glowing Thruster Engine Cores
const thrusterGeo = new THREE.CylinderGeometry(0.2, 0.32, 0.5, 6);
thrusterGeo.rotateX(Math.PI / 2);
const thrusterMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
const thrusterL = new THREE.Mesh(thrusterGeo, thrusterMat);
thrusterL.position.set(-0.6, 0.15, 1.4);
shipBodyGroup.add(thrusterL);
const thrusterR = new THREE.Mesh(thrusterGeo, thrusterMat);
thrusterR.position.set(0.6, 0.15, 1.4);
shipBodyGroup.add(thrusterR);
// Thruster Jet Glow Sprites/Plumes
const plumeGeo = new THREE.ConeGeometry(0.22, 1.8, 6);
plumeGeo.rotateX(-Math.PI / 2);
const plumeMat = new THREE.MeshBasicMaterial({
color: 0xffb700,
transparent: true,
opacity: 0.85
});
const plumeL = new THREE.Mesh(plumeGeo, plumeMat);
plumeL.position.set(-0.6, 0.15, 2.3);
shipBodyGroup.add(plumeL);
const plumeR = new THREE.Mesh(plumeGeo, plumeMat);
plumeR.position.set(0.6, 0.15, 2.3);
shipBodyGroup.add(plumeR);
// Shadow blob under craft
const shadowGeo = new THREE.CircleGeometry(1.6, 16);
shadowGeo.rotateX(-Math.PI / 2);
const shadowMat = new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
opacity: 0.4
});
const playerShadow = new THREE.Mesh(shadowGeo, shadowMat);
scene.add(playerShadow);
/* =========================================================================
4. PROCEDURAL CHUNK & CANYON GENERATION SYSTEM
========================================================================= */
const CHUNK_LENGTH = 180;
const TOTAL_CHUNKS = 9;
const CANYON_WIDTH_BASE = 48;
// Reusable Materials for Low-Poly Canyon
const canyonMaterial = new THREE.MeshStandardMaterial({
color: 0x5a2347, // Sunset canyon burgundy/terracotta
roughness: 0.85,
metalness: 0.15,
flatShading: true
});
const canyonFloorMaterial = new THREE.MeshStandardMaterial({
color: 0x2a1030,
roughness: 0.9,
metalness: 0.1,
flatShading: true
});
const monolithMaterial = new THREE.MeshStandardMaterial({
color: 0x8b3a4a,
roughness: 0.5,
metalness: 0.4,
flatShading: true
});
const archMaterial = new THREE.MeshStandardMaterial({
color: 0xd95a3f,
roughness: 0.6,
metalness: 0.3,
flatShading: true
});
// Glowing Energy Orb Material & Geometry
const orbCoreGeo = new THREE.OctahedronGeometry(1.2, 0);
const orbCoreMat = new THREE.MeshBasicMaterial({ color: 0xffea00 });
const orbRingGeo = new THREE.TorusGeometry(1.8, 0.15, 6, 16);
const orbRingMat = new THREE.MeshBasicMaterial({ color: 0xff5e00, wireframe: true });
class CanyonChunk {
constructor(index) {
this.group = new THREE.Group();
this.obstacles = []; // Array of { collider: Box3/Sphere, mesh, type }
this.orbs = []; // Array of { mesh, collected, ring }
this.index = index;
this.startZ = 0;
this.endZ = 0;
scene.add(this.group);
}
generate(startZ, difficultyFactor, trackCurvature) {
this.startZ = startZ;
this.endZ = startZ - CHUNK_LENGTH;
this.group.position.set(0, 0, 0);
this.clear();
const segmentsZ = 12;
const segmentStep = CHUNK_LENGTH / segmentsZ;
const width = Math.max(26, CANYON_WIDTH_BASE - difficultyFactor * 8);
// 1. Canyon Floor & Wall Geometry
const floorGeo = new THREE.PlaneGeometry(width * 2.2, CHUNK_LENGTH, 8, segmentsZ);
floorGeo.rotateX(-Math.PI / 2);
// Perturb floor vertices for terrain feel
const pos = floorGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const zRel = pos.getZ(i);
const globalZ = startZ - (zRel + CHUNK_LENGTH / 2);
const curveOffset = Math.sin(globalZ * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
pos.setX(i, pos.getX(i) + curveOffset);
pos.setY(i, (Math.random() - 0.5) * 1.5);
}
floorGeo.computeVertexNormals();
const floorMesh = new THREE.Mesh(floorGeo, canyonFloorMaterial);
floorMesh.position.set(0, 0, startZ - CHUNK_LENGTH / 2);
floorMesh.receiveShadow = true;
this.group.add(floorMesh);
// 2. Left and Right Canyon Walls
for (let side of [-1, 1]) {
const wallGeo = new THREE.BufferGeometry();
const wallVerts = [];
const wallHeight = 45 + Math.random() * 15;
const wallSteps = segmentsZ;
for (let s = 0; s < wallSteps; s++) {
const z1 = -s * segmentStep;
const z2 = -(s + 1) * segmentStep;
const globalZ1 = startZ + z1;
const globalZ2 = startZ + z2;
const curve1 = Math.sin(globalZ1 * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
const curve2 = Math.sin(globalZ2 * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
const xBase1 = curve1 + side * (width * 0.5);
const xBase2 = curve2 + side * (width * 0.5);
const xTop1 = curve1 + side * (width * 0.5 + 18 + Math.sin(s) * 6);
const xTop2 = curve2 + side * (width * 0.5 + 18 + Math.sin(s + 1) * 6);
// 2 triangles per wall segment
wallVerts.push(xBase1, 0, z1);
wallVerts.push(xTop1, wallHeight, z1);
wallVerts.push(xBase2, 0, z2);
wallVerts.push(xBase2, 0, z2);
wallVerts.push(xTop1, wallHeight, z1);
wallVerts.push(xTop2, wallHeight, z2);
}
wallGeo.setAttribute('position', new THREE.Float32BufferAttribute(wallVerts, 3));
wallGeo.computeVertexNormals();
const wallMesh = new THREE.Mesh(wallGeo, canyonMaterial);
wallMesh.position.set(0, 0, startZ);
wallMesh.castShadow = true;
wallMesh.receiveShadow = true;
this.group.add(wallMesh);
// Wall collision proxy boxes along the sides
for (let s = 0; s < wallSteps; s += 2) {
const zCenter = startZ - s * segmentStep - segmentStep;
const curve = Math.sin(zCenter * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
const xWallCenter = curve + side * (width * 0.5 + 8);
const wallCollider = new THREE.Box3(
new THREE.Vector3(side === -1 ? xWallCenter - 25 : xWallCenter - 5, -2, zCenter - segmentStep),
new THREE.Vector3(side === -1 ? xWallCenter + 5 : xWallCenter + 25, wallHeight, zCenter + segmentStep)
);
this.obstacles.push({
box: wallCollider,
isWall: true,
center: new THREE.Vector3(xWallCenter, wallHeight * 0.5, zCenter)
});
}
}
// 3. Obstacle Types in Chunk:
// A) Floating Monoliths, B) Tight Squeeze Arches, C) Needle Spikes
const obstacleCount = 2 + Math.floor(difficultyFactor * 3.5);
for (let o = 0; o < obstacleCount; o++) {
const zLocal = -15 - Math.random() * (CHUNK_LENGTH - 30);
const zGlobal = startZ + zLocal;
const curve = Math.sin(zGlobal * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
const rType = Math.random();
if (rType < 0.45) {
// Monolith Pillar (requires banking left/right)
const xOffset = (Math.random() - 0.5) * (width * 0.55);
const pHeight = 25 + Math.random() * 25;
const pRadius = 2.4 + Math.random() * 2.2;
const pillarGeo = new THREE.CylinderGeometry(pRadius * 0.6, pRadius, pHeight, 5);
const pillar = new THREE.Mesh(pillarGeo, monolithMaterial);
pillar.position.set(curve + xOffset, pHeight * 0.5, zGlobal);
pillar.rotation.y = Math.random() * Math.PI;
pillar.rotation.z = (Math.random() - 0.5) * 0.25;
pillar.castShadow = true;
pillar.receiveShadow = true;
this.group.add(pillar);
const box = new THREE.Box3().setFromObject(pillar);
this.obstacles.push({
box: box,
mesh: pillar,
isWall: false,
center: pillar.position.clone()
});
} else if (rType < 0.75) {
// Tight Squeeze Overhang Arch (requires precision altitude / roll)
const archGroup = new THREE.Group();
const archHeight = 8 + Math.random() * 8;
const archW = 12 + Math.random() * 8;
const topBarGeo = new THREE.BoxGeometry(archW, 4, 5);
const topBar = new THREE.Mesh(topBarGeo, archMaterial);
topBar.position.set(0, archHeight + 3, 0);
archGroup.add(topBar);
const pillarGeoL = new THREE.BoxGeometry(3.5, archHeight + 6, 4);
const pL = new THREE.Mesh(pillarGeoL, archMaterial);
pL.position.set(-archW * 0.5, (archHeight + 6) * 0.5, 0);
archGroup.add(pL);
const pR = new THREE.Mesh(pillarGeoL, archMaterial);
pR.position.set(archW * 0.5, (archHeight + 6) * 0.5, 0);
archGroup.add(pR);
archGroup.position.set(curve + (Math.random() - 0.5) * 6, 0, zGlobal);
archGroup.rotation.y = (Math.random() - 0.5) * 0.4;
archGroup.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
this.group.add(archGroup);
// Add colliders for top bar and side pillars
const topBox = new THREE.Box3().setFromObject(topBar);
const lBox = new THREE.Box3().setFromObject(pL);
const rBox = new THREE.Box3().setFromObject(pR);
this.obstacles.push({ box: topBox, isWall: false, center: topBar.position.clone().add(archGroup.position) });
this.obstacles.push({ box: lBox, isWall: false, center: pL.position.clone().add(archGroup.position) });
this.obstacles.push({ box: rBox, isWall: false, center: pR.position.clone().add(archGroup.position) });
} else {
// Slanted Needle Spires
const spireH = 30 + Math.random() * 20;
const spireGeo = new THREE.ConeGeometry(3, spireH, 4);
const spire = new THREE.Mesh(spireGeo, monolithMaterial);
const slantSide = Math.random() > 0.5 ? 1 : -1;
const xPos = curve + slantSide * (width * 0.28);
spire.position.set(xPos, spireH * 0.4, zGlobal);
spire.rotation.z = -slantSide * (0.35 + Math.random() * 0.3);
spire.castShadow = true;
spire.receiveShadow = true;
this.group.add(spire);
const box = new THREE.Box3().setFromObject(spire);
this.obstacles.push({ box: box, mesh: spire, isWall: false, center: spire.position.clone() });
}
}
// 4. Energy Orbs Generation (Placed strategically in gaps & flight paths)
const orbCount = 3 + Math.floor(Math.random() * 3);
for (let i = 0; i < orbCount; i++) {
const zLocal = -10 - (i / orbCount) * (CHUNK_LENGTH - 20) + (Math.random() - 0.5) * 8;
const zGlobal = startZ + zLocal;
const curve = Math.sin(zGlobal * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
const orbGroup = new THREE.Group();
const core = new THREE.Mesh(orbCoreGeo, orbCoreMat);
const ring = new THREE.Mesh(orbRingGeo, orbRingMat);
orbGroup.add(core);
orbGroup.add(ring);
const orbY = 2.5 + Math.random() * 8.5;
const orbX = curve + (Math.random() - 0.5) * (width * 0.6);
orbGroup.position.set(orbX, orbY, zGlobal);
this.group.add(orbGroup);
this.orbs.push({
mesh: orbGroup,
core: core,
ring: ring,
pos: orbGroup.position,
collected: false,
radius: 2.2
});
}
}
clear() {
while (this.group.children.length > 0) {
const obj = this.group.children[0];
this.group.remove(obj);
if (obj.geometry) obj.geometry.dispose();
}
this.obstacles = [];
this.orbs = [];
}
}
// Initialize Chunks
const chunks = [];
let farthestZ = 0;
let trackCurvatureSeed = 0.5;
function initChunks() {
farthestZ = 20;
for (let i = 0; i < TOTAL_CHUNKS; i++) {
const chunk = new CanyonChunk(i);
chunk.generate(farthestZ, 0.1, trackCurvatureSeed);
chunks.push(chunk);
farthestZ -= CHUNK_LENGTH;
}
}
function updateChunks(playerZ, difficulty) {
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
// If chunk is behind player by more than 1 chunk length, recycle to front
if (chunk.startZ > playerZ + CHUNK_LENGTH) {
chunk.generate(farthestZ, difficulty, trackCurvatureSeed);
farthestZ -= CHUNK_LENGTH;
}
}
}
/* =========================================================================
5. PARTICLE SYSTEMS (Trail Particles & Speed Dust & Crash Debris)
========================================================================= */
// Dust Particles Flying Past Craft
const DUST_COUNT = 350;
const dustGeo = new THREE.BufferGeometry();
const dustPositions = new Float32Array(DUST_COUNT * 3);
const dustColors = new Float32Array(DUST_COUNT * 3);
for (let i = 0; i < DUST_COUNT; i++) {
dustPositions[i * 3] = (Math.random() - 0.5) * 80;
dustPositions[i * 3 + 1] = Math.random() * 30;
dustPositions[i * 3 + 2] = (Math.random() - 0.5) * 200;
dustColors[i * 3] = 1.0;
dustColors[i * 3 + 1] = 0.6 + Math.random() * 0.4;
dustColors[i * 3 + 2] = 0.2 + Math.random() * 0.6;
}
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPositions, 3));
dustGeo.setAttribute('color', new THREE.BufferAttribute(dustColors, 3));
const dustMat = new THREE.PointsMaterial({
size: 0.65,
vertexColors: true,
transparent: true,
opacity: 0.75,
blending: THREE.AdditiveBlending
});
const dustParticles = new THREE.Points(dustGeo, dustMat);
scene.add(dustParticles);
// Trail ribbon particle ring buffer
const TRAIL_MAX = 60;
const trailPositionsL = [];
const trailPositionsR = [];
for (let i = 0; i < TRAIL_MAX; i++) {
trailPositionsL.push(new THREE.Vector3());
trailPositionsR.push(new THREE.Vector3());
}
const trailGeoL = new THREE.BufferGeometry().setFromPoints(trailPositionsL);
const trailGeoR = new THREE.BufferGeometry().setFromPoints(trailPositionsR);
const trailMat = new THREE.LineBasicMaterial({
color: 0x00f0ff,
linewidth: 2,
transparent: true,
opacity: 0.8
});
const trailLineL = new THREE.Line(trailGeoL, trailMat);
const trailLineR = new THREE.Line(trailGeoR, trailMat);
scene.add(trailLineL);
scene.add(trailLineR);
// Crash Debris Fracture System
const debrisGroup = new THREE.Group();
scene.add(debrisGroup);
const debrisList = [];
function triggerCrashExplosion(position) {
const fragCount = 35;
const shardGeo = new THREE.TetrahedronGeometry(0.7, 0);
const shardMat = new THREE.MeshBasicMaterial({ color: 0xff7700 });
for (let i = 0; i < fragCount; i++) {
const mesh = new THREE.Mesh(shardGeo, shardMat);
mesh.position.copy(position);
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * 35,
Math.random() * 25 + 5,
(Math.random() - 0.5) * 35 - 15
);
debrisGroup.add(mesh);
debrisList.push({
mesh: mesh,
vel: velocity,
rotVel: new THREE.Vector3(Math.random() * 10, Math.random() * 10, Math.random() * 10),
life: 1.0
});
}
}
/* =========================================================================
6. GAME STATE & FLIGHT PHYSICS ENGINE
========================================================================= */
const STATE_MENU = 0;
const STATE_PLAYING = 1;
const STATE_GAMEOVER = 2;
const game = {
state: STATE_MENU,
score: 0,
distance: 0,
multiplier: 1.0,
multiplierTimer: 0,
maxMultiplier: 1.0,
nearMissCount: 0,
// Energy & Turbo
energy: 100,
maxEnergy: 100,
energyDecayRate: 6.8, // % per second
turboCharge: 0,
isBoosting: false,
// Physics State
pos: new THREE.Vector3(0, 3.5, 0),
vel: new THREE.Vector3(0, 0, 0),
baseSpeed: 75,
currentSpeed: 75,
maxSpeed: 75,
boostSpeedBonus: 55,
// Rotation & Flight Dynamics
pitch: 0,
roll: 0,
yaw: 0,
targetRoll: 0,
targetPitch: 0,
// Camera Controls
camOffset: new THREE.Vector3(0, 3.8, 8.5),
camLookOffset: new THREE.Vector3(0, 1.2, -18),
camShake: 0,
// Difficulty
difficulty: 0.1
};
// Keyboard & Input Listener
const keys = {
left: false,
right: false,
up: false,
down: false,
boost: false
};
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' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') {
keys.boost = true;
if (game.state === STATE_PLAYING && game.turboCharge >= 20 && !game.isBoosting) {
audio.playBoostIgnite();
}
}
if (e.code === 'KeyR' && game.state === STATE_GAMEOVER) {
restartGame();
}
});
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' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.boost = false;
});
// Touch Controls setup
function setupTouchControls() {
const btnL = document.getElementById('btn-left');
const btnR = document.getElementById('btn-right');
const btnP = document.getElementById('btn-pitch');
const btnB = document.getElementById('btn-boost');
const bindTouch = (elem, pressFn, releaseFn) => {
elem.addEventListener('touchstart', (e) => { e.preventDefault(); pressFn(); }, { passive: false });
elem.addEventListener('touchend', (e) => { e.preventDefault(); releaseFn(); }, { passive: false });
};
bindTouch(btnL, () => keys.left = true, () => keys.left = false);
bindTouch(btnR, () => keys.right = true, () => keys.right = false);
bindTouch(btnP, () => keys.up = true, () => keys.up = false);
bindTouch(btnB, () => keys.boost = true, () => keys.boost = false);
}
setupTouchControls();
// Speedometer UI Ticks Generation
const speedTicksContainer = document.getElementById('speed-ticks');
const TOTAL_TICKS = 24;
for (let i = 0; i < TOTAL_TICKS; i++) {
const tick = document.createElement('div');
tick.className = 'speed-tick';
speedTicksContainer.appendChild(tick);
}
const tickElements = document.querySelectorAll('.speed-tick');
/* =========================================================================
7. CORE SIMULATION LOOP & FLIGHT MECHANICS
========================================================================= */
let lastTime = performance.now();
function updateFlightPhysics(dt) {
// 1. Difficulty Scaling based on score & distance
game.difficulty = Math.min(1.0, 0.1 + (game.distance / 4500) * 0.9);
game.baseSpeed = 75 + game.difficulty * 45;
// 2. Turbo Boost Triggering & Consumption
if (keys.boost && game.turboCharge > 2) {
game.isBoosting = true;
game.turboCharge = Math.max(0, game.turboCharge - dt * 28);
} else {
game.isBoosting = false;
// Passive slow turbo build on straight flying
if (Math.abs(game.roll) < 0.15 && game.state === STATE_PLAYING) {
game.turboCharge = Math.min(100, game.turboCharge + dt * 4);
}
}
// 3. Target Speed & Acceleration
const targetSpeed = game.baseSpeed + (game.isBoosting ? game.boostSpeedBonus : 0);
game.currentSpeed += (targetSpeed - game.currentSpeed) * Math.min(1.0, dt * 4.5);
if (game.currentSpeed > game.maxSpeed) game.maxSpeed = game.currentSpeed;
// 4. Banking (Roll) & Turning Mechanics
const rollSpeed = 3.2;
const returnSpeed = 4.5;
const maxRoll = 1.15; // ~66 degrees banking
if (keys.left) {
game.targetRoll = Math.min(maxRoll, game.targetRoll + dt * rollSpeed);
} else if (keys.right) {
game.targetRoll = Math.max(-maxRoll, game.targetRoll - dt * rollSpeed);
} else {
game.targetRoll += (0 - game.targetRoll) * Math.min(1.0, dt * returnSpeed);
}
// Smooth Roll Interpolation
game.roll += (game.targetRoll - game.roll) * Math.min(1.0, dt * 10);
// Pitch (Dive / Climb) Controls
const pitchSpeed = 2.2;
if (keys.up) {
game.targetPitch = Math.min(0.6, game.targetPitch + dt * pitchSpeed); // Climb
} else if (keys.down) {
game.targetPitch = Math.max(-0.6, game.targetPitch - dt * pitchSpeed); // Dive
} else {
game.targetPitch += (0 - game.targetPitch) * Math.min(1.0, dt * 6.0);
}
game.pitch += (game.targetPitch - game.pitch) * Math.min(1.0, dt * 8);
// 5. Calculate Flight Momentum Velocities
// Lateral velocity is driven directly by the bank angle (Flight physics feel)
const lateralForce = -Math.sin(game.roll) * 38;
game.vel.x += (lateralForce - game.vel.x) * Math.min(1.0, dt * 6);
// Vertical climb / dive
const verticalForce = Math.sin(game.pitch) * 22;
game.vel.y += (verticalForce - game.vel.y) * Math.min(1.0, dt * 6);
// Forward velocity along -Z
game.vel.z = -game.currentSpeed;
// Apply displacement
game.pos.x += game.vel.x * dt;
game.pos.y += game.vel.y * dt;
game.pos.z += game.vel.z * dt;
// Flight boundary clamps
game.pos.y = Math.max(1.2, Math.min(26.0, game.pos.y));
// Update Craft 3D Group Transform
playerGroup.position.copy(game.pos);
shipBodyGroup.rotation.z = game.roll;
shipBodyGroup.rotation.x = game.pitch * 0.7;
shipBodyGroup.rotation.y = -game.roll * 0.25;
// Dynamic wingtip plume stretch & colors
const plumeScale = 0.8 + (game.currentSpeed / game.baseSpeed) * 0.6 + (game.isBoosting ? 0.8 : 0);
plumeL.scale.set(1, plumeScale, 1);
plumeR.scale.set(1, plumeScale, 1);
plumeMat.color.setHex(game.isBoosting ? 0x00f0ff : 0xffaa00);
// Shadow projection directly on ground
playerShadow.position.set(game.pos.x, 0.08, game.pos.z);
const heightRatio = Math.max(0.1, 1 - (game.pos.y / 25));
playerShadow.scale.setScalar(heightRatio * 1.3);
shadowMat.opacity = heightRatio * 0.45;
// 6. Camera Follow Physics with Smooth Damping & Dynamic FOV
const targetCamX = game.pos.x + (-game.roll * 1.8);
const targetCamY = game.pos.y + game.camOffset.y + (game.pitch * 1.5);
const targetCamZ = game.pos.z + game.camOffset.z + (game.isBoosting ? 1.5 : 0);
camera.position.x += (targetCamX - camera.position.x) * Math.min(1.0, dt * 10);
camera.position.y += (targetCamY - camera.position.y) * Math.min(1.0, dt * 10);
camera.position.z += (targetCamZ - camera.position.z) * Math.min(1.0, dt * 12);
// Camera Shake
if (game.camShake > 0) {
camera.position.x += (Math.random() - 0.5) * game.camShake;
camera.position.y += (Math.random() - 0.5) * game.camShake;
game.camShake = Math.max(0, game.camShake - dt * 3.5);
}
const lookTarget = new THREE.Vector3(
game.pos.x + game.camLookOffset.x - (game.roll * 4.0),
game.pos.y + game.camLookOffset.y,
game.pos.z + game.camLookOffset.z
);
camera.lookAt(lookTarget);
// Camera FOV Expansion during high speed / boost
const targetFov = game.isBoosting ? 82 : (65 + (game.currentSpeed - 75) * 0.15);
camera.fov += (targetFov - camera.fov) * Math.min(1.0, dt * 5);
camera.updateProjectionMatrix();
// Dynamic Sun follows player along Z for infinite sunset
sunMesh.position.z = game.pos.z - 750;
sunInner.position.z = game.pos.z - 748;
sunLight.position.z = game.pos.z - 350;
sunLight.target.position.set(game.pos.x, 0, game.pos.z);
sunLight.target.updateMatrixWorld();
bgMountainsGroup.position.z = game.pos.z * 0.85; // Parallax
}
/* =========================================================================
8. PROXIMITY SCORING (NEAR-MISS) & COLLISION CHECKS
========================================================================= */
const playerSphere = new THREE.Sphere(new THREE.Vector3(), 1.3);
const nearMissSphere = new THREE.Sphere(new THREE.Vector3(), 4.8);
const nearMissBanner = document.getElementById('near-miss-banner');
let bannerTimeout = null;
function triggerNearMissEffect(bonusScore) {
game.nearMissCount++;
game.multiplier = Math.min(10.0, game.multiplier + 0.5);
if (game.multiplier > game.maxMultiplier) game.maxMultiplier = game.multiplier;
game.multiplierTimer = 4.0; // Multiplier decay reset
game.score += Math.round(bonusScore * game.multiplier);
game.camShake = Math.min(0.6, game.camShake + 0.25);
game.turboCharge = Math.min(100, game.turboCharge + 12);
audio.playNearMiss();
// UI Punch
const multiBox = document.getElementById('multiplier-box');
multiBox.classList.add('multiplier-pulse');
setTimeout(() => multiBox.classList.remove('multiplier-pulse'), 200);
nearMissBanner.textContent = `⚡ NEAR MISS +${Math.round(bonusScore * game.multiplier)} ⚡`;
nearMissBanner.classList.add('active');
clearTimeout(bannerTimeout);
bannerTimeout = setTimeout(() => nearMissBanner.classList.remove('active'), 800);
}
function checkCollisionsAndNearMisses(dt) {
playerSphere.center.copy(game.pos);
nearMissSphere.center.copy(game.pos);
let closestObstacleDist = 999;
for (let c = 0; c < chunks.length; c++) {
const chunk = chunks[c];
// Only test chunks near the player
if (chunk.startZ < game.pos.z - CHUNK_LENGTH || chunk.endZ > game.pos.z + 40) continue;
// 1. Check Obstacle Collisions & Near-Misses
for (let i = 0; i < chunk.obstacles.length; i++) {
const obs = chunk.obstacles[i];
const distToCenter = obs.center.distanceTo(game.pos);
if (distToCenter < closestObstacleDist) {
closestObstacleDist = distToCenter;
}
// Exact Box vs Player Sphere Collision Check
if (obs.box.intersectsSphere(playerSphere)) {
// CRASH!
gameOver("COLLISION DETECTED");
return;
}
// Near Miss Check
if (!obs.nearMissed && obs.box.intersectsSphere(nearMissSphere)) {
obs.nearMissed = true;
triggerNearMissEffect(250);
}
}
// 2. Check Energy Orb Collectibles
for (let i = 0; i < chunk.orbs.length; i++) {
const orb = chunk.orbs[i];
if (orb.collected) continue;
// Rotate orb meshes
orb.core.rotation.y += dt * 3;
orb.core.rotation.x += dt * 2;
orb.ring.rotation.z += dt * 4;
const dist = orb.pos.distanceTo(game.pos);
if (dist < orb.radius + 1.4) {
// Collected!
orb.collected = true;
orb.mesh.visible = false;
game.energy = Math.min(game.maxEnergy, game.energy + 28);
game.turboCharge = Math.min(100, game.turboCharge + 18);
game.score += Math.round(150 * game.multiplier);
audio.playOrbCollect(game.multiplier);
// Floating feedback
game.camShake = Math.min(0.35, game.camShake + 0.15);
}
}
}
// Audio update
const speedRatio = (game.currentSpeed - 70) / (game.baseSpeed + game.boostSpeedBonus - 70);
audio.updateEngine(Math.max(0, Math.min(1.5, speedRatio)), game.isBoosting, closestObstacleDist);
}
/* =========================================================================
9. GAME LOOP, SCORE TRACKING & UI UPDATES
========================================================================= */
const scoreDisplay = document.getElementById('score-display');
const distDisplay = document.getElementById('dist-display');
const multiplierDisplay = document.getElementById('multiplier-display');
const energyFill = document.getElementById('energy-fill');
const turboFill = document.getElementById('turbo-fill');
const speedDigits = document.getElementById('speed-digits');
function updateParticlesAndTrails(dt) {
// 1. Dust stream
const posAttr = dustGeo.attributes.position;
for (let i = 0; i < DUST_COUNT; i++) {
let pz = posAttr.getZ(i);
pz += (game.currentSpeed * 1.5) * dt;
if (pz > 20) {
pz = -180 - Math.random() * 50;
posAttr.setX(i, game.pos.x + (Math.random() - 0.5) * 80);
posAttr.setY(i, Math.random() * 30);
}
posAttr.setZ(i, pz);
}
dustGeo.attributes.position.needsUpdate = true;
dustParticles.position.z = game.pos.z;
// 2. Wingtip Trails
const leftWingPos = new THREE.Vector3(-2.8, 0.1, 0.8).applyMatrix4(playerGroup.matrixWorld);
const rightWingPos = new THREE.Vector3(2.8, 0.1, 0.8).applyMatrix4(playerGroup.matrixWorld);
trailPositionsL.unshift(leftWingPos);
trailPositionsL.pop();
trailPositionsR.unshift(rightWingPos);
trailPositionsR.pop();
trailGeoL.setFromPoints(trailPositionsL);
trailGeoR.setFromPoints(trailPositionsR);
// 3. Crash Debris Update
for (let i = debrisList.length - 1; i >= 0; i--) {
const d = debrisList[i];
d.mesh.position.addScaledVector(d.vel, dt);
d.vel.y -= 35 * dt; // gravity
d.mesh.rotation.x += d.rotVel.x * dt;
d.mesh.rotation.y += d.rotVel.y * dt;
d.life -= dt * 0.8;
if (d.life <= 0) {
debrisGroup.remove(d.mesh);
debrisList.splice(i, 1);
}
}
}
function drawSpeedLines(dt) {
if (game.isBoosting || game.currentSpeed > 105) {
speedLinesCanvas.style.opacity = '1';
speedVignette.style.opacity = game.isBoosting ? '0.7' : '0.35';
speedLinesCtx.clearRect(0, 0, speedLinesCanvas.width, speedLinesCanvas.height);
const cx = speedLinesCanvas.width / 2;
const cy = speedLinesCanvas.height / 2;
const lineCount = game.isBoosting ? 28 : 14;
speedLinesCtx.strokeStyle = game.isBoosting ? 'rgba(5, 217, 232, 0.65)' : 'rgba(255, 170, 60, 0.4)';
speedLinesCtx.lineWidth = 1.5;
for (let i = 0; i < lineCount; i++) {
const angle = Math.random() * Math.PI * 2;
const r1 = Math.random() * 180 + 120;
const r2 = r1 + Math.random() * 120 + 80;
const x1 = cx + Math.cos(angle) * r1;
const y1 = cy + Math.sin(angle) * r1;
const x2 = cx + Math.cos(angle) * r2;
const y2 = cy + Math.sin(angle) * r2;
speedLinesCtx.beginPath();
speedLinesCtx.moveTo(x1, y1);
speedLinesCtx.lineTo(x2, y2);
speedLinesCtx.stroke();
}
} else {
speedLinesCanvas.style.opacity = '0';
speedVignette.style.opacity = '0';
}
}
function updateHUD(dt) {
// Score & Distance
scoreDisplay.textContent = String(Math.floor(game.score)).padStart(6, '0');
distDisplay.textContent = `${Math.floor(game.distance)} m`;
multiplierDisplay.textContent = `x${game.multiplier.toFixed(1)}`;
// Energy Bar
energyFill.style.width = `${Math.max(0, game.energy)}%`;
// Turbo Bar
turboFill.style.width = `${Math.max(0, game.turboCharge)}%`;
if (game.turboCharge >= 20) {
turboFill.classList.add('turbo-ready');
} else {
turboFill.classList.remove('turbo-ready');
}
// Speedometer
const kmh = Math.floor(game.currentSpeed * 2.2);
speedDigits.textContent = `${kmh} KM/H`;
const maxKmh = 290;
const minKmh = 140;
const litCount = Math.floor(((kmh - minKmh) / (maxKmh - minKmh)) * TOTAL_TICKS);
tickElements.forEach((t, idx) => {
t.className = 'speed-tick';
if (idx < litCount) {
t.classList.add(game.isBoosting ? 'lit-boost' : 'lit');
}
});
}
function animate(currentTime) {
requestAnimationFrame(animate);
const dt = Math.min(0.1, (currentTime - lastTime) / 1000);
lastTime = currentTime;
if (game.state === STATE_PLAYING) {
// Flight physics & world displacement
updateFlightPhysics(dt);
updateChunks(game.pos.z, game.difficulty);
checkCollisionsAndNearMisses(dt);
updateParticlesAndTrails(dt);
drawSpeedLines(dt);
// Continuous scoring & distance progression
game.distance += game.currentSpeed * dt;
game.score += game.currentSpeed * dt * 0.6 * game.multiplier;
// Energy decay mechanic (forces collecting glowing amber orbs)
game.energy -= game.energyDecayRate * dt;
if (game.energy <= 0) {
game.energy = 0;
gameOver("SOLAR CORE DEPLETED");
}
// Multiplier gradual timer decay back to 1.0
if (game.multiplierTimer > 0) {
game.multiplierTimer -= dt;
} else if (game.multiplier > 1.0) {
game.multiplier = Math.max(1.0, game.multiplier - dt * 0.45);
}
updateHUD(dt);
} else if (game.state === STATE_MENU) {
// Idle camera sweep in canyon
const time = currentTime * 0.0006;
playerGroup.position.set(Math.sin(time) * 4, 3.5 + Math.sin(time * 2) * 0.4, -20);
playerGroup.rotation.z = Math.sin(time) * 0.2;
camera.position.set(Math.sin(time) * 3, 5, -12);
camera.lookAt(playerGroup.position);
updateParticlesAndTrails(dt);
} else if (game.state === STATE_GAMEOVER) {
updateParticlesAndTrails(dt);
}
renderer.render(scene, camera);
}
/* =========================================================================
10. GAME STATE TRANSITIONS (START, GAME OVER, RESTART)
========================================================================= */
const startMenu = document.getElementById('start-menu');
const gameOverMenu = document.getElementById('game-over-menu');
const startBtn = document.getElementById('start-btn');
const restartBtn = document.getElementById('restart-btn');
function startGame() {
audio.init();
startMenu.classList.add('hidden');
gameOverMenu.classList.add('hidden');
game.state = STATE_PLAYING;
game.score = 0;
game.distance = 0;
game.multiplier = 1.0;
game.multiplierTimer = 0;
game.maxMultiplier = 1.0;
game.nearMissCount = 0;
game.energy = 100;
game.turboCharge = 0;
game.currentSpeed = 75;
game.maxSpeed = 75;
game.pos.set(0, 3.5, 0);
game.vel.set(0, 0, 0);
game.roll = 0;
game.pitch = 0;
game.targetRoll = 0;
game.targetPitch = 0;
game.camShake = 0;
playerGroup.visible = true;
// Re-init chunks
chunks.forEach(c => c.clear());
chunks.length = 0;
initChunks();
}
function gameOver(reason) {
if (game.state === STATE_GAMEOVER) return;
game.state = STATE_GAMEOVER;
audio.playCrash();
triggerCrashExplosion(game.pos.clone());
playerGroup.visible = false;
// Populate game over stats
document.getElementById('game-over-title').textContent = reason;
document.getElementById('final-score').textContent = Math.floor(game.score).toLocaleString();
document.getElementById('final-dist').textContent = `${Math.floor(game.distance)} m`;
document.getElementById('final-multi').textContent = `x${game.maxMultiplier.toFixed(1)}`;
document.getElementById('final-nearmiss').textContent = game.nearMissCount;
setTimeout(() => {
gameOverMenu.classList.remove('hidden');
}, 700);
}
function restartGame() {
startGame();
}
startBtn.addEventListener('click', startGame);
restartBtn.addEventListener('click', restartGame);
// Window Resize Handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
resizeSpeedLines();
});
// Initialize world
initChunks();
requestAnimationFrame(animate);
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.7-flash","provider":"openrouter","persona":"full","sessionId":"f7e5b2c7-393d-4420-9289-e82d58289a28","tokensIn":309400,"tokensOut":26051,"tokensTotal":335451,"cost":0.1426462875,"turns":10,"toolCalls":9,"failedToolCalls":0,"timestamp":"2026-08-15T22:59:40.498Z"} -->