Create scropt.js
Browse files
scropt.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ZEN Glider — Canyon Run
|
| 2 |
+
No audio, single-file Three.js gameplay.
|
| 3 |
+
Author: ZEN AI Co.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// ---------- Scene & Renderer ----------
|
| 7 |
+
const scene = new THREE.Scene();
|
| 8 |
+
|
| 9 |
+
// Gradient sky (procedural)
|
| 10 |
+
const skyCanvas = document.createElement('canvas');
|
| 11 |
+
skyCanvas.width = 1; skyCanvas.height = 256;
|
| 12 |
+
const skyCtx = skyCanvas.getContext('2d');
|
| 13 |
+
const grad = skyCtx.createLinearGradient(0,256,0,0);
|
| 14 |
+
grad.addColorStop(0.00, '#0b1220'); // deep blue
|
| 15 |
+
grad.addColorStop(0.45, '#0f2240'); // indigo
|
| 16 |
+
grad.addColorStop(1.00, '#0a0a0f'); // near-black
|
| 17 |
+
skyCtx.fillStyle = grad; skyCtx.fillRect(0,0,1,256);
|
| 18 |
+
const skyTex = new THREE.CanvasTexture(skyCanvas);
|
| 19 |
+
scene.background = skyTex;
|
| 20 |
+
|
| 21 |
+
// Subtle stars
|
| 22 |
+
{
|
| 23 |
+
const N = 900;
|
| 24 |
+
const pos = new Float32Array(N*3);
|
| 25 |
+
for (let i=0;i<N;i++){
|
| 26 |
+
const r = 600, th = Math.random()*Math.PI*2, ph = Math.random()*Math.PI*0.5;
|
| 27 |
+
pos[3*i+0] = r*Math.sin(ph)*Math.cos(th);
|
| 28 |
+
pos[3*i+1] = r*Math.cos(ph)+80;
|
| 29 |
+
pos[3*i+2] = r*Math.sin(ph)*Math.sin(th);
|
| 30 |
+
}
|
| 31 |
+
const g = new THREE.BufferGeometry();
|
| 32 |
+
g.setAttribute('position', new THREE.BufferAttribute(pos,3));
|
| 33 |
+
const m = new THREE.PointsMaterial({size:1, sizeAttenuation:false, color:0xffffff, opacity:0.8, transparent:true});
|
| 34 |
+
const stars = new THREE.Points(g,m);
|
| 35 |
+
scene.add(stars);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 2000);
|
| 39 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 40 |
+
renderer.setSize(innerWidth, innerHeight);
|
| 41 |
+
renderer.shadowMap.enabled = true;
|
| 42 |
+
document.body.appendChild(renderer.domElement);
|
| 43 |
+
|
| 44 |
+
// Lights
|
| 45 |
+
const hemi = new THREE.HemisphereLight(0x6ba7ff, 0x0a0a0a, 0.6);
|
| 46 |
+
scene.add(hemi);
|
| 47 |
+
const sun = new THREE.DirectionalLight(0xfff2cc, 1.15);
|
| 48 |
+
sun.position.set(-120,180,-60);
|
| 49 |
+
sun.castShadow = true;
|
| 50 |
+
sun.shadow.mapSize.set(1024,1024);
|
| 51 |
+
scene.add(sun);
|
| 52 |
+
|
| 53 |
+
// ---------- Canyon Terrain (procedural) ----------
|
| 54 |
+
const terrain = new THREE.Mesh(
|
| 55 |
+
new THREE.PlaneGeometry(2000, 2000, 400, 400),
|
| 56 |
+
new THREE.MeshStandardMaterial({
|
| 57 |
+
color: 0x1b2433,
|
| 58 |
+
roughness: 0.95,
|
| 59 |
+
metalness: 0.0
|
| 60 |
+
})
|
| 61 |
+
);
|
| 62 |
+
terrain.rotation.x = -Math.PI/2;
|
| 63 |
+
terrain.receiveShadow = true;
|
| 64 |
+
scene.add(terrain);
|
| 65 |
+
|
| 66 |
+
// Height function for canyon: valley width varies with z; walls rise smoothly outside valley
|
| 67 |
+
function canyonHalfWidth(z){
|
| 68 |
+
return 8 + 4*Math.sin(0.02*z) + 2*Math.sin(0.004*z+1.2);
|
| 69 |
+
}
|
| 70 |
+
function wallFactor(x, z, w){
|
| 71 |
+
const d = Math.max(0, Math.abs(x) - w);
|
| 72 |
+
return 1 - Math.exp(-0.75*d);
|
| 73 |
+
}
|
| 74 |
+
function baseNoise(x,z){
|
| 75 |
+
return -1.0 + 0.45*Math.sin(0.04*z) + 0.25*Math.sin(0.17*x + 0.31*z);
|
| 76 |
+
}
|
| 77 |
+
function heightAt(x,z){
|
| 78 |
+
const w = canyonHalfWidth(z);
|
| 79 |
+
const walls = wallFactor(x,z,w) * (6 + 2*Math.sin(0.05*z));
|
| 80 |
+
const ripples = 0.25*Math.sin(0.9*Math.sin(0.02*z)*x);
|
| 81 |
+
return baseNoise(x,z) + walls + ripples;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Deform terrain geometry
|
| 85 |
+
{
|
| 86 |
+
const pos = terrain.geometry.attributes.position;
|
| 87 |
+
for (let i=0;i<pos.count;i++){
|
| 88 |
+
const x = pos.getX(i);
|
| 89 |
+
const z = pos.getZ(i);
|
| 90 |
+
const h = heightAt(x,z);
|
| 91 |
+
pos.setY(i, h);
|
| 92 |
+
}
|
| 93 |
+
terrain.geometry.computeVertexNormals();
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// ---------- Rings & Thermals ----------
|
| 97 |
+
const rings = [];
|
| 98 |
+
const thermals = [];
|
| 99 |
+
|
| 100 |
+
function makeRing(x,y,z){
|
| 101 |
+
const t = new THREE.Mesh(
|
| 102 |
+
new THREE.TorusGeometry(1.6, 0.12, 8, 24),
|
| 103 |
+
new THREE.MeshBasicMaterial({ color: 0x00e5ff, transparent:true, opacity:0.9 })
|
| 104 |
+
);
|
| 105 |
+
t.position.set(x,y,z);
|
| 106 |
+
scene.add(t);
|
| 107 |
+
rings.push(t);
|
| 108 |
+
return t;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function makeThermal(x,y,z,height=6){
|
| 112 |
+
const cyl = new THREE.Mesh(
|
| 113 |
+
new THREE.CylinderGeometry(1.2, 1.2, height, 24, 1, true),
|
| 114 |
+
new THREE.MeshBasicMaterial({ color: 0x22d39a, transparent:true, opacity:0.18, side:THREE.DoubleSide })
|
| 115 |
+
);
|
| 116 |
+
cyl.position.set(x, y + height/2, z);
|
| 117 |
+
scene.add(cyl);
|
| 118 |
+
thermals.push({mesh:cyl, top: y+height});
|
| 119 |
+
return cyl;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Populate forward corridor (z from 20 to 1500)
|
| 123 |
+
function populateEnvironment(){
|
| 124 |
+
// clear previous if any
|
| 125 |
+
rings.splice(0, rings.length);
|
| 126 |
+
thermals.splice(0, thermals.length);
|
| 127 |
+
// place every ~30–40 units
|
| 128 |
+
for (let z=40; z<=1500; ){
|
| 129 |
+
const w = canyonHalfWidth(z)*0.7;
|
| 130 |
+
const x = (Math.random()*2-1) * w;
|
| 131 |
+
const ground = heightAt(x, z);
|
| 132 |
+
const y = ground + 3.2 + Math.random()*2.5;
|
| 133 |
+
makeRing(x, y, z);
|
| 134 |
+
// sometimes pair a thermal after ring
|
| 135 |
+
if (Math.random() < 0.45){
|
| 136 |
+
const tz = z + 6 + Math.random()*8;
|
| 137 |
+
const tw = canyonHalfWidth(tz)*0.6;
|
| 138 |
+
const tx = THREE.MathUtils.clamp(x + (Math.random()*2-1)*2.5, -tw, tw);
|
| 139 |
+
const ty = heightAt(tx, tz) + 1.0;
|
| 140 |
+
makeThermal(tx, ty, tz, 7+Math.random()*3);
|
| 141 |
+
}
|
| 142 |
+
z += 30 + Math.floor(Math.random()*14);
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
populateEnvironment();
|
| 146 |
+
|
| 147 |
+
// ---------- Paper Glider ----------
|
| 148 |
+
function makeGlider(){
|
| 149 |
+
const group = new THREE.Group();
|
| 150 |
+
const paper = new THREE.MeshLambertMaterial({ color: 0xf4f6f8, side: THREE.DoubleSide });
|
| 151 |
+
// Center body (slender wedge)
|
| 152 |
+
const bodyShape = new THREE.Shape();
|
| 153 |
+
bodyShape.moveTo(0, 0);
|
| 154 |
+
bodyShape.lineTo(-0.22, 0.55);
|
| 155 |
+
bodyShape.lineTo(-0.42, 1.05);
|
| 156 |
+
bodyShape.lineTo(0.42, 1.05);
|
| 157 |
+
bodyShape.lineTo(0.22, 0.55);
|
| 158 |
+
bodyShape.lineTo(0, 0);
|
| 159 |
+
const body = new THREE.Mesh(new THREE.ExtrudeGeometry(bodyShape, { depth: 0.03, bevelEnabled:false }), paper);
|
| 160 |
+
body.castShadow = true; body.receiveShadow = true;
|
| 161 |
+
|
| 162 |
+
// Wings
|
| 163 |
+
const wingShape = (dir)=> {
|
| 164 |
+
const s = new THREE.Shape();
|
| 165 |
+
s.moveTo(0,0.2); s.lineTo(dir*0.32, 0.85); s.lineTo(dir*0.9, 0.5); s.lineTo(0,0.2);
|
| 166 |
+
return s;
|
| 167 |
+
};
|
| 168 |
+
const leftWing = new THREE.Mesh(new THREE.ExtrudeGeometry(wingShape(-1), {depth:0.02, bevelEnabled:false}), paper);
|
| 169 |
+
const rightWing= new THREE.Mesh(new THREE.ExtrudeGeometry(wingShape( 1), {depth:0.02, bevelEnabled:false}), paper);
|
| 170 |
+
leftWing.position.y = 0.01; rightWing.position.y = 0.015;
|
| 171 |
+
leftWing.rotation.x = 0.18; rightWing.rotation.x = 0.18;
|
| 172 |
+
leftWing.castShadow = rightWing.castShadow = true;
|
| 173 |
+
|
| 174 |
+
// Center crease
|
| 175 |
+
const creaseGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0.03,0), new THREE.Vector3(0,0.03,1.05)]);
|
| 176 |
+
const crease = new THREE.Line(creaseGeo, new THREE.LineBasicMaterial({ color: 0xdedede }));
|
| 177 |
+
|
| 178 |
+
group.add(body, leftWing, rightWing, crease);
|
| 179 |
+
group.rotation.order = "ZXY";
|
| 180 |
+
group.rotation.x = -Math.PI/2;
|
| 181 |
+
return group;
|
| 182 |
+
}
|
| 183 |
+
const glider = makeGlider();
|
| 184 |
+
scene.add(glider);
|
| 185 |
+
|
| 186 |
+
// ---------- Game State & Physics ----------
|
| 187 |
+
let game = "aim"; // "aim" | "fly" | "end"
|
| 188 |
+
let power = 0, maxPower = 14, charging = false;
|
| 189 |
+
let stamina = 100, maxStamina = 100;
|
| 190 |
+
let combo = 1, comboTimer = 0, comboWindow = 2.5; // seconds to chain rings
|
| 191 |
+
let distance = 0, score = 0;
|
| 192 |
+
|
| 193 |
+
const vel = new THREE.Vector3(0,0,0);
|
| 194 |
+
const forwardSpeed = { base: 14, current: 0 }; // m/s along +z
|
| 195 |
+
const gravity = 5.0;
|
| 196 |
+
const drag = 0.015; // fractional per second
|
| 197 |
+
let pitch = 0; // radians, positive pitches nose up (we'll invert to rotation)
|
| 198 |
+
let bank = 0; // radians for left/right roll visual
|
| 199 |
+
|
| 200 |
+
// Controls
|
| 201 |
+
let key = { left:false, right:false, up:false, down:false, burst:false };
|
| 202 |
+
let touchLeftActive=false, touchRightActive=false;
|
| 203 |
+
|
| 204 |
+
function reset(){
|
| 205 |
+
game = "aim";
|
| 206 |
+
power = 0; charging = false;
|
| 207 |
+
stamina = maxStamina;
|
| 208 |
+
combo = 1; comboTimer = 0;
|
| 209 |
+
distance = 0; score = 0;
|
| 210 |
+
forwardSpeed.current = 0;
|
| 211 |
+
vel.set(0,0,0);
|
| 212 |
+
pitch = 0; bank = 0;
|
| 213 |
+
glider.position.set(0, heightAt(0,0)+2.5, 0);
|
| 214 |
+
glider.rotation.set(-Math.PI/2, 0, 0);
|
| 215 |
+
camera.position.set(0, glider.position.y+4, glider.position.z-10);
|
| 216 |
+
populateEnvironment();
|
| 217 |
+
document.getElementById('centerMsg').style.display = 'none';
|
| 218 |
+
}
|
| 219 |
+
reset();
|
| 220 |
+
|
| 221 |
+
function launch(){
|
| 222 |
+
forwardSpeed.current = THREE.MathUtils.mapLinear(power, 0, maxPower, 8, 26);
|
| 223 |
+
vel.y = Math.sin(Math.PI/4) * (power*0.65); // initial toss angle ~45°
|
| 224 |
+
game = "fly"; power = 0; charging = false;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// ---------- UI helpers ----------
|
| 228 |
+
const elDistance = document.getElementById('distance');
|
| 229 |
+
const elScore = document.getElementById('score');
|
| 230 |
+
const elPower = document.getElementById('powerBar');
|
| 231 |
+
const elStamina = document.getElementById('staminaBar');
|
| 232 |
+
const elCenter = document.getElementById('centerMsg');
|
| 233 |
+
const elResult = document.getElementById('resultLine');
|
| 234 |
+
const elDetail = document.getElementById('resultDetail');
|
| 235 |
+
document.getElementById('btnRestart').addEventListener('click', reset);
|
| 236 |
+
|
| 237 |
+
function setBar(el, v){ el.style.width = `${THREE.MathUtils.clamp(v,0,100)}%`; }
|
| 238 |
+
|
| 239 |
+
// ---------- Input ----------
|
| 240 |
+
addEventListener('keydown', (e)=>{
|
| 241 |
+
if (e.code === 'Space'){ if (game==='aim'){ charging = true; e.preventDefault(); } else if (game==='end'){ reset(); } }
|
| 242 |
+
if (e.code === 'ArrowLeft') key.left = true;
|
| 243 |
+
if (e.code === 'ArrowRight') key.right = true;
|
| 244 |
+
if (e.code === 'ArrowUp') key.up = true;
|
| 245 |
+
if (e.code === 'ArrowDown') key.down = true;
|
| 246 |
+
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') key.burst = true;
|
| 247 |
+
if (e.code === 'Escape') reset();
|
| 248 |
+
});
|
| 249 |
+
addEventListener('keyup', (e)=>{
|
| 250 |
+
if (e.code === 'Space' && game==='aim'){ launch(); e.preventDefault(); }
|
| 251 |
+
if (e.code === 'ArrowLeft') key.left = false;
|
| 252 |
+
if (e.code === 'ArrowRight') key.right = false;
|
| 253 |
+
if (e.code === 'ArrowUp') key.up = false;
|
| 254 |
+
if (e.code === 'ArrowDown') key.down = false;
|
| 255 |
+
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') key.burst = false;
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
const touchLeft = document.getElementById('touchLeft');
|
| 259 |
+
const touchRight = document.getElementById('touchRight');
|
| 260 |
+
touchLeft.addEventListener('touchstart', ()=>{ touchLeftActive=true; }, {passive:true});
|
| 261 |
+
touchLeft.addEventListener('touchend', ()=>{ touchLeftActive=false; }, {passive:true});
|
| 262 |
+
touchRight.addEventListener('touchstart',()=>{ touchRightActive=true; }, {passive:true});
|
| 263 |
+
touchRight.addEventListener('touchend', ()=>{ touchRightActive=false; }, {passive:true});
|
| 264 |
+
|
| 265 |
+
addEventListener('mousedown', ()=>{ if (game==='aim'){ charging = true; } });
|
| 266 |
+
addEventListener('mouseup', ()=>{ if (game==='aim' && charging){ launch(); } });
|
| 267 |
+
|
| 268 |
+
// ---------- Helpers ----------
|
| 269 |
+
const clock = new THREE.Clock();
|
| 270 |
+
|
| 271 |
+
function sampleGroundY(x,z){
|
| 272 |
+
return heightAt(x,z);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
function endRun(reason){
|
| 276 |
+
game = 'end';
|
| 277 |
+
elResult.textContent = reason || 'Crash!';
|
| 278 |
+
elDetail.textContent = `Distance: ${distance.toFixed(1)} m • Score: ${Math.floor(score)} • Best combo ×${combo}`;
|
| 279 |
+
elCenter.style.display = 'grid';
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function ringCollected(t){
|
| 283 |
+
// remove visually
|
| 284 |
+
scene.remove(t);
|
| 285 |
+
rings.splice(rings.indexOf(t),1);
|
| 286 |
+
// reward
|
| 287 |
+
stamina = Math.min(maxStamina, stamina + 35);
|
| 288 |
+
combo = Math.min(10, combo + 1);
|
| 289 |
+
comboTimer = comboWindow;
|
| 290 |
+
score += 150 * combo;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
function inThermalBoost(p){
|
| 294 |
+
// check if inside any thermal cylinder footprint
|
| 295 |
+
for (const th of thermals){
|
| 296 |
+
const dx = p.x - th.mesh.position.x;
|
| 297 |
+
const dz = p.z - th.mesh.position.z;
|
| 298 |
+
const r = 1.2;
|
| 299 |
+
const insideXZ = (dx*dx + dz*dz) <= r*r;
|
| 300 |
+
const y = p.y;
|
| 301 |
+
if (insideXZ && y >= th.mesh.position.y && y <= th.top){
|
| 302 |
+
return true;
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
return false;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// ---------- Main Loop ----------
|
| 309 |
+
function animate(){
|
| 310 |
+
requestAnimationFrame(animate);
|
| 311 |
+
const dt = Math.min(0.033, clock.getDelta());
|
| 312 |
+
|
| 313 |
+
// background gentle shift with distance
|
| 314 |
+
const ratio = Math.min(1, distance / 1500);
|
| 315 |
+
skyTex.rotation = ratio * Math.PI * 0.4; skyTex.center.set(0.5,0.5); skyTex.needsUpdate = true;
|
| 316 |
+
|
| 317 |
+
// UI update
|
| 318 |
+
setBar(elPower, game==='aim' ? (power/maxPower)*100 : 0);
|
| 319 |
+
setBar(elStamina, (stamina/maxStamina)*100);
|
| 320 |
+
elDistance.textContent = `Distance: ${distance.toFixed(1)} m`;
|
| 321 |
+
elScore.textContent = `Score: ${Math.floor(score)} | Combo ×${combo}`;
|
| 322 |
+
|
| 323 |
+
if (game==='aim'){
|
| 324 |
+
if (charging){
|
| 325 |
+
power += 24*dt; // charge rate
|
| 326 |
+
power = Math.min(power, maxPower);
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
if (game==='fly'){
|
| 331 |
+
// Controls -> bank/pitch targets
|
| 332 |
+
const steerLeft = key.left || touchLeftActive;
|
| 333 |
+
const steerRight = key.right || touchRightActive;
|
| 334 |
+
|
| 335 |
+
const bankTarget = (steerLeft?-1:0) + (steerRight?1:0); // -1 left, +1 right
|
| 336 |
+
bank = THREE.MathUtils.lerp(bank, bankTarget * Math.PI/5, 0.1);
|
| 337 |
+
|
| 338 |
+
const pitchTarget = (key.up?0.35:0) + (key.down?-0.35:0);
|
| 339 |
+
pitch = THREE.MathUtils.lerp(pitch, pitchTarget, 0.12);
|
| 340 |
+
|
| 341 |
+
// Forward speed, drag & burst
|
| 342 |
+
const baseAcc = 0.0;
|
| 343 |
+
forwardSpeed.current += baseAcc*dt;
|
| 344 |
+
forwardSpeed.current *= (1 - drag); // simple drag
|
| 345 |
+
|
| 346 |
+
if ((key.burst || key.down) && stamina>0){
|
| 347 |
+
forwardSpeed.current += 8*dt;
|
| 348 |
+
vel.y += 5.5*dt; // upward impulse during burst
|
| 349 |
+
stamina -= 18*dt;
|
| 350 |
+
} else {
|
| 351 |
+
stamina += 4*dt; // slow regen
|
| 352 |
+
}
|
| 353 |
+
stamina = THREE.MathUtils.clamp(stamina, 0, maxStamina);
|
| 354 |
+
|
| 355 |
+
// Lift from pitch and forward motion
|
| 356 |
+
const lift = (Math.sin(pitch) * (forwardSpeed.current+6)) * 0.9;
|
| 357 |
+
vel.y += (lift - gravity) * dt;
|
| 358 |
+
|
| 359 |
+
// Gentle random gusts
|
| 360 |
+
const gust = (Math.random()-0.5) * 0.3;
|
| 361 |
+
const lateralControl = (steerRight?-1:0) + (steerLeft?1:0);
|
| 362 |
+
glider.position.x += (lateralControl*3.8 + gust) * dt;
|
| 363 |
+
|
| 364 |
+
// Move forward
|
| 365 |
+
glider.position.z += (forwardSpeed.current + 10) * dt;
|
| 366 |
+
// Vertical
|
| 367 |
+
glider.position.y += vel.y * dt;
|
| 368 |
+
|
| 369 |
+
// Visual orientation (x = pitch, z = bank)
|
| 370 |
+
glider.rotation.x = -Math.PI/2 + ( -pitch * 0.6 );
|
| 371 |
+
glider.rotation.z = bank * 0.8;
|
| 372 |
+
|
| 373 |
+
// Thermals
|
| 374 |
+
if (inThermalBoost(glider.position)){
|
| 375 |
+
vel.y += 6.5*dt;
|
| 376 |
+
score += 5 * combo; // tiny drip while in column
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// Check rings
|
| 380 |
+
for (let i=rings.length-1;i>=0;i--){
|
| 381 |
+
const t = rings[i];
|
| 382 |
+
const d = glider.position.distanceTo(t.position);
|
| 383 |
+
if (d < 1.6){
|
| 384 |
+
ringCollected(t);
|
| 385 |
+
} else {
|
| 386 |
+
// animate ring pulse
|
| 387 |
+
t.rotation.x += 0.6*dt;
|
| 388 |
+
t.rotation.z += 0.4*dt;
|
| 389 |
+
const s = 1 + 0.08*Math.sin(performance.now()/260 + i);
|
| 390 |
+
t.scale.set(s,s,s);
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
// Combo decay
|
| 395 |
+
comboTimer -= dt;
|
| 396 |
+
if (comboTimer <= 0 && combo>1){
|
| 397 |
+
combo = Math.max(1, combo-1);
|
| 398 |
+
comboTimer = 0.35; // prevent rapid drop
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
// Distance & score
|
| 402 |
+
const dz = (forwardSpeed.current + 10) * dt;
|
| 403 |
+
distance += dz;
|
| 404 |
+
score += dz * (0.8*combo);
|
| 405 |
+
|
| 406 |
+
// Collision with ground/ walls
|
| 407 |
+
const groundY = sampleGroundY(glider.position.x, glider.position.z);
|
| 408 |
+
if (glider.position.y <= groundY + 0.05){
|
| 409 |
+
endRun('You skimmed the canyon floor!');
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Fail-safe: out of bounds (too high or too far sideways)
|
| 413 |
+
if (glider.position.y > 40 || Math.abs(glider.position.x) > 50){
|
| 414 |
+
endRun('You drifted off course!');
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// Camera chase rig
|
| 419 |
+
const camTarget = new THREE.Vector3(glider.position.x, glider.position.y + 3.0, glider.position.z - 10);
|
| 420 |
+
camera.position.lerp(camTarget, 0.12);
|
| 421 |
+
camera.lookAt(glider.position);
|
| 422 |
+
|
| 423 |
+
renderer.render(scene, camera);
|
| 424 |
+
}
|
| 425 |
+
animate();
|
| 426 |
+
|
| 427 |
+
// ---------- Resize ----------
|
| 428 |
+
addEventListener('resize', ()=>{
|
| 429 |
+
camera.aspect = innerWidth/innerHeight;
|
| 430 |
+
camera.updateProjectionMatrix();
|
| 431 |
+
renderer.setSize(innerWidth, innerHeight);
|
| 432 |
+
});
|