ZENLLC commited on
Commit
05ba0ac
·
verified ·
1 Parent(s): 3d887be

Create scropt.js

Browse files
Files changed (1) hide show
  1. scropt.js +432 -0
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
+ });