Azivandor commited on
Commit
b2e6936
·
verified ·
1 Parent(s): a8ba57b

Upload star-voyager.html

Browse files
Files changed (1) hide show
  1. star-voyager.html +601 -0
star-voyager.html ADDED
@@ -0,0 +1,601 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Star Voyager</title>
6
+ <style>
7
+ * { -webkit-tap-highlight-color: transparent; user-select: none; touch-action: none; }
8
+ html, body {
9
+ margin: 0; padding: 0; min-height: 100%;
10
+ background: radial-gradient(circle at 50% 10%, #0f1a2a, #04070c 70%);
11
+ display: flex; align-items: center; justify-content: center;
12
+ font-family: 'Segoe UI', Arial, sans-serif;
13
+ }
14
+ #wrap { text-align: center; padding: 12px 0; }
15
+ #logo {
16
+ font-size: 28px; font-weight: 900; letter-spacing: 4px;
17
+ color: #4ecbff;
18
+ text-shadow: 0 0 10px #2a8fd8, 0 0 22px #4ecbff;
19
+ margin-bottom: 6px;
20
+ }
21
+ #logo span { color: #ff8a3d; text-shadow: 0 0 10px #b35a1a, 0 0 22px #ff8a3d; }
22
+ #tableFrame {
23
+ display: inline-block; position: relative;
24
+ padding: 10px; border-radius: 16px;
25
+ background: linear-gradient(160deg, #16243a, #08101c 60%);
26
+ box-shadow: 0 0 0 2px #000, 0 0 30px #4ecbff44, 0 10px 24px #000a;
27
+ }
28
+ canvas {
29
+ background: #000;
30
+ border: 3px solid #4ecbff;
31
+ box-shadow: 0 0 30px #4ecbff55, inset 0 0 40px #000a;
32
+ border-radius: 10px;
33
+ display: block;
34
+ }
35
+ #msg {
36
+ position: absolute; color: #fff; font-size: 26px; font-weight: bold;
37
+ text-shadow: 0 0 20px #4ecbff, 0 0 40px #ff5a5a;
38
+ display: none; text-align: center; pointer-events: none;
39
+ left: 50%; top: 45%; transform: translate(-50%,-50%); width: 90%;
40
+ }
41
+ #msg span { display:block; font-size: 14px; font-weight: normal; margin-top: 10px; color: #cbe6ff; }
42
+ #pad {
43
+ display: grid; grid-template-columns: 70px 70px 70px 70px 100px;
44
+ gap: 6px; justify-content: center; margin-top: 10px;
45
+ }
46
+ .pbtn {
47
+ background: linear-gradient(160deg, #16243a, #0a121e);
48
+ border: 1px solid #4ecbff55; border-radius: 8px; color: #d9f0ff;
49
+ font-size: 13px; font-weight: bold; display: flex; align-items: center; justify-content: center;
50
+ cursor: pointer; padding: 12px 0;
51
+ }
52
+ .pbtn:active { background: linear-gradient(160deg, #4ecbff44, #0a121e); }
53
+ #controls { color: #7f9ec9; font-size: 11px; margin-top: 6px; }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <a href="index.html" style="position:fixed; top:10px; left:10px; z-index:999; display:inline-flex; align-items:center; gap:5px; padding:7px 12px; border-radius:20px; background:rgba(10,10,20,0.75); color:#fff; font-family:'Segoe UI', Arial, sans-serif; font-size:13px; font-weight:bold; text-decoration:none; border:1px solid rgba(255,255,255,0.25); backdrop-filter: blur(3px);">← Home</a>
58
+ <div id="wrap">
59
+ <div id="logo">STAR <span>VOYAGER</span></div>
60
+ <div id="tableFrame">
61
+ <canvas id="game" width="800" height="560"></canvas>
62
+ <div id="msg"></div>
63
+ </div>
64
+ <div id="pad">
65
+ <div class="pbtn" id="btnLeft">◄</div>
66
+ <div class="pbtn" id="btnUp">▲</div>
67
+ <div class="pbtn" id="btnDown">▼</div>
68
+ <div class="pbtn" id="btnRight">►</div>
69
+ <div class="pbtn" id="btnFire">FIRE</div>
70
+ </div>
71
+ <div id="controls">WASD / Arrow keys to steer · Space to fire · dodge or destroy asteroids before they hit the cockpit</div>
72
+ </div>
73
+
74
+ <script>
75
+ const canvas = document.getElementById('game');
76
+ const ctx = canvas.getContext('2d');
77
+ const W = canvas.width, H = canvas.height;
78
+ const CX = W/2, CY = H*0.46;
79
+ const msgEl = document.getElementById('msg');
80
+
81
+ const PROJ_SCALE = 260;
82
+ const Z_NEAR = 0.35;
83
+ const Z_FAR = 40;
84
+ const FORWARD_SPEED = 5.2;
85
+ const MAX_SHIELD = 100;
86
+
87
+ let state = 'playing'; // playing | gameover
88
+ let score = 0, best = 0;
89
+ let shield = MAX_SHIELD;
90
+ let shootCooldown = 0;
91
+ let hitFlashT = 0;
92
+ let shakeT = 0;
93
+
94
+ let cam = { x: 0, y: 0, vx: 0, vy: 0, bank: 0 };
95
+ let keys = { left:false, right:false, up:false, down:false, fire:false };
96
+
97
+ let stars = [];
98
+ let asteroids = [];
99
+ let lasers = [];
100
+ let particles = [];
101
+
102
+ function rand(min, max){ return Math.random()*(max-min)+min; }
103
+
104
+ function makeStar(){
105
+ return { x: rand(-14,14), y: rand(-10,10), z: rand(2, Z_FAR) };
106
+ }
107
+ function resetStar(s){ s.x = rand(-14,14); s.y = rand(-10,10); s.z = Z_FAR; }
108
+
109
+ function makeAsteroid(farSpawn){
110
+ return {
111
+ x: rand(-6,6), y: rand(-4,4), z: farSpawn ? rand(20, Z_FAR) : rand(10, Z_FAR),
112
+ size: rand(0.5, 1.3),
113
+ rot: rand(0, Math.PI*2), rotSpeed: rand(-1,1),
114
+ seed: Math.random()*1000,
115
+ alive: true
116
+ };
117
+ }
118
+ function resetAsteroid(a){
119
+ a.x = rand(-6,6); a.y = rand(-4,4); a.z = rand(24, Z_FAR);
120
+ a.size = rand(0.5, 1.3);
121
+ a.seed = Math.random()*1000;
122
+ }
123
+
124
+ function initField(){
125
+ stars = []; for (let i=0;i<220;i++) stars.push(makeStar());
126
+ asteroids = []; for (let i=0;i<12;i++) asteroids.push(makeAsteroid(true));
127
+ lasers = []; particles = [];
128
+ }
129
+ initField();
130
+
131
+ function project(x, y, z){
132
+ const scale = PROJ_SCALE / z;
133
+ return {
134
+ sx: CX + (x - cam.x) * scale,
135
+ sy: CY + (y - cam.y) * scale,
136
+ scale
137
+ };
138
+ }
139
+
140
+ function spawnBurst(sx, sy, color, count, spread){
141
+ spread = spread || 3;
142
+ for (let i=0;i<count;i++){
143
+ const a = Math.random()*Math.PI*2, sp = 0.5+Math.random()*spread;
144
+ particles.push({ x: sx, y: sy, vx: Math.cos(a)*sp, vy: Math.sin(a)*sp, life: 1, color });
145
+ }
146
+ }
147
+
148
+ function newGame(){
149
+ score = 0; shield = MAX_SHIELD; shootCooldown = 0;
150
+ hitFlashT = 0; shakeT = 0;
151
+ cam = { x:0, y:0, vx:0, vy:0, bank:0 };
152
+ initField();
153
+ state = 'playing';
154
+ msgEl.style.display = 'none';
155
+ }
156
+
157
+ function gameOver(){
158
+ state = 'gameover';
159
+ if (score > best) best = score;
160
+ msgEl.style.display = 'block';
161
+ msgEl.innerHTML = `HULL BREACH<span>Score: ${score} — best: ${best} — click/tap to fly again</span>`;
162
+ }
163
+
164
+ // ---- Input ----
165
+ document.addEventListener('keydown', e => {
166
+ if (state === 'gameover'){ if (e.key === ' ' || e.key === 'Enter') newGame(); return; }
167
+ if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = true;
168
+ if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = true;
169
+ if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') keys.up = true;
170
+ if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') keys.down = true;
171
+ if (e.key === ' '){ e.preventDefault(); keys.fire = true; }
172
+ });
173
+ document.addEventListener('keyup', e => {
174
+ if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = false;
175
+ if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = false;
176
+ if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') keys.up = false;
177
+ if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') keys.down = false;
178
+ if (e.key === ' ') keys.fire = false;
179
+ });
180
+ function bindHold(el, onDown, onUp){
181
+ el.addEventListener('pointerdown', e => { e.preventDefault(); onDown(); });
182
+ el.addEventListener('pointerup', () => onUp());
183
+ el.addEventListener('pointerleave', () => onUp());
184
+ el.addEventListener('pointercancel', () => onUp());
185
+ }
186
+ bindHold(document.getElementById('btnLeft'), () => keys.left=true, () => keys.left=false);
187
+ bindHold(document.getElementById('btnRight'), () => keys.right=true, () => keys.right=false);
188
+ bindHold(document.getElementById('btnUp'), () => keys.up=true, () => keys.up=false);
189
+ bindHold(document.getElementById('btnDown'), () => keys.down=true, () => keys.down=false);
190
+ bindHold(document.getElementById('btnFire'), () => keys.fire=true, () => keys.fire=false);
191
+ canvas.addEventListener('pointerdown', () => { if (state === 'gameover') newGame(); });
192
+
193
+ let muzzleFlashT = 0;
194
+ function fireLaser(){
195
+ lasers.push({ x: cam.x, y: cam.y, z: Z_NEAR, alive: true });
196
+ spawnBurst(CX, H*0.86, '#4ecbff', 4, 1.5);
197
+ muzzleFlashT = 0.08;
198
+ }
199
+
200
+ // ---- Update ----
201
+ function updateCamera(dt){
202
+ const accel = 3.4, damp = 2.4, maxV = 2.6;
203
+ if (keys.left) cam.vx -= accel*dt;
204
+ if (keys.right) cam.vx += accel*dt;
205
+ if (keys.up) cam.vy -= accel*dt;
206
+ if (keys.down) cam.vy += accel*dt;
207
+ cam.vx *= Math.max(0, 1 - damp*dt);
208
+ cam.vy *= Math.max(0, 1 - damp*dt);
209
+ cam.vx = Math.max(-maxV, Math.min(maxV, cam.vx));
210
+ cam.vy = Math.max(-maxV, Math.min(maxV, cam.vy));
211
+ cam.x += cam.vx*dt;
212
+ cam.y += cam.vy*dt;
213
+ cam.x = Math.max(-5, Math.min(5, cam.x));
214
+ cam.y = Math.max(-3.5, Math.min(3.5, cam.y));
215
+
216
+ const targetBank = -cam.vx * 0.32;
217
+ cam.bank += (targetBank - cam.bank) * Math.min(1, dt*6);
218
+
219
+ if (keys.fire && shootCooldown <= 0){
220
+ fireLaser();
221
+ shootCooldown = 0.22;
222
+ }
223
+ if (shootCooldown > 0) shootCooldown -= dt;
224
+ }
225
+
226
+ function updateStars(dt){
227
+ stars.forEach(s => {
228
+ s.z -= FORWARD_SPEED*dt;
229
+ if (s.z <= 0.1) resetStar(s);
230
+ });
231
+ }
232
+
233
+ function updateAsteroids(dt){
234
+ asteroids.forEach(a => {
235
+ if (!a.alive) return;
236
+ a.z -= FORWARD_SPEED*dt;
237
+ a.rot += a.rotSpeed*dt;
238
+
239
+ // danger check: close to camera AND near the center of the screen = hull hit
240
+ if (a.z < 1.4 && a.z > 0.05){
241
+ const p = project(a.x, a.y, Math.max(a.z, 0.2));
242
+ const dist = Math.hypot(p.sx - CX, p.sy - CY);
243
+ const dangerRadius = 90;
244
+ if (dist < dangerRadius){
245
+ takeDamage(14);
246
+ spawnBurst(p.sx, p.sy, '#ff8a3d', 16, 5);
247
+ resetAsteroid(a);
248
+ return;
249
+ }
250
+ }
251
+ if (a.z <= 0.05) resetAsteroid(a);
252
+ });
253
+ }
254
+
255
+ function updateLasers(dt){
256
+ const LASER_SPEED = 16;
257
+ lasers.forEach(l => { l.z += LASER_SPEED*dt; if (l.z > Z_FAR) l.alive = false; });
258
+ lasers = lasers.filter(l => l.alive);
259
+
260
+ for (const l of lasers){
261
+ if (!l.alive) continue;
262
+ for (const a of asteroids){
263
+ if (!a.alive) continue;
264
+ if (Math.abs(a.z - l.z) < 1.1){
265
+ const dx = a.x - l.x, dy = a.y - l.y;
266
+ const hitR = a.size * 1.1 + 0.15;
267
+ if (dx*dx + dy*dy < hitR*hitR){
268
+ l.alive = false;
269
+ const p = project(a.x, a.y, Math.max(a.z, 0.2));
270
+ spawnBurst(p.sx, p.sy, '#ffe14e', 18, 4.5);
271
+ score += Math.round(50 / Math.max(0.4, a.size));
272
+ resetAsteroid(a);
273
+ shakeT = Math.max(shakeT, 0.08);
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ lasers = lasers.filter(l => l.alive);
280
+ }
281
+
282
+ function takeDamage(amt){
283
+ shield = Math.max(0, shield - amt);
284
+ hitFlashT = 0.35;
285
+ shakeT = 0.3;
286
+ if (shield <= 0) gameOver();
287
+ }
288
+
289
+ function update(dt){
290
+ if (state !== 'playing') return;
291
+ updateCamera(dt);
292
+ updateStars(dt);
293
+ updateAsteroids(dt);
294
+ updateLasers(dt);
295
+ particles.forEach(p => { p.x+=p.vx; p.y+=p.vy; p.vx*=0.94; p.vy*=0.94; p.life-=0.03; });
296
+ particles = particles.filter(p => p.life > 0);
297
+ if (hitFlashT > 0) hitFlashT -= dt;
298
+ if (muzzleFlashT > 0) muzzleFlashT -= dt;
299
+ if (shakeT > 0) shakeT -= dt;
300
+ }
301
+
302
+ // ---- Draw ----
303
+ function drawStars(){
304
+ ctx.save();
305
+ stars.forEach(s => {
306
+ const p = project(s.x, s.y, s.z);
307
+ if (p.sx < -20 || p.sx > W+20 || p.sy < -20 || p.sy > H+20) return;
308
+ const b = Math.max(0.35, Math.min(1, 1 - s.z/Z_FAR));
309
+ const r = Math.max(1, 2.6*b);
310
+ ctx.fillStyle = `rgba(220,235,255,${b})`;
311
+ ctx.shadowColor = '#cfe6ff'; ctx.shadowBlur = r*1.5;
312
+ ctx.fillRect(p.sx, p.sy, r, r);
313
+ ctx.shadowBlur = 0;
314
+ });
315
+ ctx.restore();
316
+ }
317
+
318
+ function drawHorizonGrid(){
319
+ // a floor-style wireframe grid below the ship — a strong geometric reference
320
+ // that makes steering and banking dramatically easier to perceive than stars alone
321
+ ctx.save();
322
+ ctx.strokeStyle = 'rgba(78,203,255,0.35)';
323
+ ctx.lineWidth = 1.5;
324
+ const gridY = 5.5;
325
+ for (let gz = 2; gz <= 34; gz += 4){
326
+ ctx.beginPath();
327
+ let started = false;
328
+ for (let gx = -12; gx <= 12; gx += 1){
329
+ const p = project(gx, gridY, gz);
330
+ if (started) ctx.lineTo(p.sx, p.sy); else { ctx.moveTo(p.sx, p.sy); started = true; }
331
+ }
332
+ ctx.stroke();
333
+ }
334
+ for (let gx = -12; gx <= 12; gx += 3){
335
+ ctx.beginPath();
336
+ const p1 = project(gx, gridY, 2);
337
+ const p2 = project(gx, gridY, 34);
338
+ ctx.moveTo(p1.sx, p1.sy);
339
+ ctx.lineTo(p2.sx, p2.sy);
340
+ ctx.stroke();
341
+ }
342
+ ctx.restore();
343
+ }
344
+
345
+ function drawAsteroidShape(sx, sy, r, seed, rot){
346
+ ctx.save();
347
+ ctx.translate(sx, sy);
348
+ ctx.rotate(rot);
349
+ const points = 9;
350
+ ctx.beginPath();
351
+ for (let i = 0; i <= points; i++){
352
+ const ang = (i/points) * Math.PI*2;
353
+ const jitter = 0.7 + 0.3*Math.sin(seed + i*2.1);
354
+ const rr = r * jitter;
355
+ const px = Math.cos(ang)*rr, py = Math.sin(ang)*rr;
356
+ if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
357
+ }
358
+ ctx.closePath();
359
+ const grad = ctx.createRadialGradient(-r*0.3,-r*0.3,r*0.1,0,0,r);
360
+ grad.addColorStop(0, '#8a7a6a');
361
+ grad.addColorStop(1, '#3a3028');
362
+ ctx.fillStyle = grad;
363
+ ctx.fill();
364
+ ctx.strokeStyle = 'rgba(255,200,150,0.3)';
365
+ ctx.lineWidth = 1.5;
366
+ ctx.stroke();
367
+ ctx.restore();
368
+ }
369
+
370
+ function drawAsteroids(){
371
+ const drawList = asteroids.filter(a => a.alive && a.z > 0.05).sort((a,b) => b.z - a.z);
372
+ drawList.forEach(a => {
373
+ const p = project(a.x, a.y, a.z);
374
+ const r = a.size * p.scale * 0.6;
375
+ if (p.sx < -r-20 || p.sx > W+r+20 || p.sy < -r-20 || p.sy > H+r+20) return;
376
+ drawAsteroidShape(p.sx, p.sy, r, a.seed, a.rot);
377
+ });
378
+ }
379
+
380
+ function drawLasers(){
381
+ ctx.save();
382
+ lasers.forEach(l => {
383
+ const p = project(l.x, l.y, l.z);
384
+ const prevP = project(l.x, l.y, Math.max(0.05, l.z - 1.6));
385
+ ctx.strokeStyle = '#4ecbff';
386
+ ctx.shadowColor = '#4ecbff'; ctx.shadowBlur = 10;
387
+ ctx.lineWidth = Math.max(1, 6*p.scale);
388
+ ctx.beginPath();
389
+ ctx.moveTo(prevP.sx, prevP.sy);
390
+ ctx.lineTo(p.sx, p.sy);
391
+ ctx.stroke();
392
+ ctx.shadowBlur = 0;
393
+ });
394
+ ctx.restore();
395
+ }
396
+
397
+ function drawParticles(){
398
+ particles.forEach(p => {
399
+ ctx.save();
400
+ ctx.globalAlpha = Math.max(0, p.life);
401
+ ctx.fillStyle = p.color;
402
+ ctx.fillRect(p.x-2, p.y-2, 4, 4);
403
+ ctx.restore();
404
+ });
405
+ }
406
+
407
+ function drawReticle(){
408
+ ctx.save();
409
+ ctx.strokeStyle = '#4ecbff';
410
+ ctx.shadowColor = '#4ecbff'; ctx.shadowBlur = 6;
411
+ ctx.lineWidth = 2.5;
412
+ ctx.beginPath();
413
+ ctx.arc(CX, CY, 22, 0, Math.PI*2);
414
+ ctx.stroke();
415
+ ctx.beginPath();
416
+ ctx.moveTo(CX-32, CY); ctx.lineTo(CX-14, CY);
417
+ ctx.moveTo(CX+14, CY); ctx.lineTo(CX+32, CY);
418
+ ctx.moveTo(CX, CY-32); ctx.lineTo(CX, CY-14);
419
+ ctx.moveTo(CX, CY+14); ctx.lineTo(CX, CY+32);
420
+ ctx.stroke();
421
+ ctx.shadowBlur = 0;
422
+ ctx.restore();
423
+ }
424
+
425
+ function drawCockpit(){
426
+ const sideW = 74;
427
+ const dashY = H*0.82, dashH = H - dashY;
428
+
429
+ // side panels — solid, opaque metal, not a fade-to-transparent gradient
430
+ [0, W-sideW].forEach((sx, i) => {
431
+ const grad = ctx.createLinearGradient(sx, 0, sx + (i===0?sideW:0), 0);
432
+ if (i === 0){ grad.addColorStop(0, '#1c2a3a'); grad.addColorStop(1, '#0d1520'); }
433
+ else { grad.addColorStop(0, '#0d1520'); grad.addColorStop(1, '#1c2a3a'); }
434
+ ctx.fillStyle = grad;
435
+ ctx.fillRect(sx, 0, sideW, dashY);
436
+ // rivet details
437
+ ctx.fillStyle = 'rgba(150,200,230,0.35)';
438
+ for (let y = 20; y < dashY; y += 46){
439
+ ctx.beginPath(); ctx.arc(sx + sideW/2, y, 3, 0, Math.PI*2); ctx.fill();
440
+ }
441
+ // small warning light
442
+ const lit = Math.floor(performance.now()/600)%2===0;
443
+ ctx.fillStyle = lit ? '#63ff8f' : '#1a3a28';
444
+ ctx.shadowColor = '#63ff8f'; ctx.shadowBlur = lit ? 8 : 0;
445
+ ctx.beginPath(); ctx.arc(sx + sideW/2, dashY - 30, 5, 0, Math.PI*2); ctx.fill();
446
+ ctx.shadowBlur = 0;
447
+ });
448
+ ctx.strokeStyle = '#4ecbff';
449
+ ctx.lineWidth = 3;
450
+ ctx.shadowColor = '#4ecbff'; ctx.shadowBlur = 8;
451
+ ctx.beginPath(); ctx.moveTo(sideW,0); ctx.lineTo(sideW,dashY); ctx.stroke();
452
+ ctx.beginPath(); ctx.moveTo(W-sideW,0); ctx.lineTo(W-sideW,dashY); ctx.stroke();
453
+ ctx.shadowBlur = 0;
454
+
455
+ // top strip — solid bar with a glowing edge
456
+ ctx.fillStyle = '#0f1a26';
457
+ ctx.fillRect(0,0,W,30);
458
+ ctx.strokeStyle = '#4ecbff';
459
+ ctx.lineWidth = 2;
460
+ ctx.beginPath(); ctx.moveTo(0,30); ctx.lineTo(W,30); ctx.stroke();
461
+ ctx.fillStyle = '#8fc4e8';
462
+ ctx.font = 'bold 12px Segoe UI';
463
+ ctx.textAlign = 'center';
464
+ ctx.fillText('S T A R V O Y A G E R — N A V S Y S T E M O N L I N E', W/2, 20);
465
+
466
+ // bottom dashboard — solid opaque panel with a bright top bezel
467
+ const dashGrad = ctx.createLinearGradient(0, dashY, 0, H);
468
+ dashGrad.addColorStop(0, '#152234');
469
+ dashGrad.addColorStop(1, '#0a1420');
470
+ ctx.fillStyle = dashGrad;
471
+ ctx.fillRect(0, dashY, W, dashH);
472
+ ctx.strokeStyle = '#4ecbff';
473
+ ctx.lineWidth = 3;
474
+ ctx.shadowColor = '#4ecbff'; ctx.shadowBlur = 10;
475
+ ctx.beginPath(); ctx.moveTo(0, dashY); ctx.lineTo(W, dashY); ctx.stroke();
476
+ ctx.shadowBlur = 0;
477
+
478
+ // score
479
+ ctx.textAlign = 'left';
480
+ ctx.font = 'bold 13px Segoe UI';
481
+ ctx.fillStyle = '#8fc4e8';
482
+ ctx.fillText('SCORE', 90, dashY+22);
483
+ ctx.font = 'bold 24px Courier New';
484
+ ctx.fillStyle = '#ffe14e';
485
+ ctx.shadowColor = '#ffb300'; ctx.shadowBlur = 8;
486
+ ctx.fillText(String(score).padStart(6,'0'), 90, dashY+48);
487
+ ctx.shadowBlur = 0;
488
+
489
+ // shield
490
+ ctx.font = 'bold 13px Segoe UI';
491
+ ctx.fillStyle = '#8fc4e8';
492
+ ctx.fillText('SHIELD', 90, dashY+66);
493
+ const barW = 150, barX = 90, barY = dashY+72;
494
+ ctx.fillStyle = '#2a1418'; ctx.fillRect(barX, barY, barW, 12);
495
+ const shieldFrac = shield/MAX_SHIELD;
496
+ ctx.fillStyle = shieldFrac > 0.4 ? '#4ecbff' : '#ff5a5a';
497
+ ctx.fillRect(barX, barY, barW*shieldFrac, 12);
498
+ ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
499
+ ctx.strokeRect(barX, barY, barW, 12);
500
+
501
+ drawRadar(W - 200, dashY + dashH/2);
502
+ }
503
+
504
+ function drawRadar(rx, ry){
505
+ const R = 46;
506
+ ctx.save();
507
+ ctx.beginPath();
508
+ ctx.arc(rx, ry, R, 0, Math.PI*2);
509
+ ctx.fillStyle = 'rgba(10,20,15,0.8)';
510
+ ctx.fill();
511
+ ctx.strokeStyle = 'rgba(99,255,143,0.6)'; ctx.lineWidth = 2;
512
+ ctx.stroke();
513
+ ctx.beginPath(); ctx.moveTo(rx-R,ry); ctx.lineTo(rx+R,ry); ctx.stroke();
514
+ ctx.beginPath(); ctx.moveTo(rx,ry-R); ctx.lineTo(rx,ry+R); ctx.stroke();
515
+
516
+ ctx.fillStyle = '#63ff8f';
517
+ asteroids.forEach(a => {
518
+ if (!a.alive || a.z > 18) return;
519
+ const rxp = rx + Math.max(-1,Math.min(1,(a.x-cam.x)/8)) * (R-4);
520
+ const ryp = ry + Math.max(-1,Math.min(1,(a.y-cam.y)/6)) * (R-4);
521
+ const alpha = Math.max(0.25, 1 - a.z/18);
522
+ ctx.globalAlpha = alpha;
523
+ ctx.beginPath();
524
+ ctx.arc(rxp, ryp, 3, 0, Math.PI*2);
525
+ ctx.fill();
526
+ });
527
+ ctx.globalAlpha = 1;
528
+
529
+ ctx.fillStyle = '#fff';
530
+ ctx.beginPath(); ctx.arc(rx, ry, 3, 0, Math.PI*2); ctx.fill();
531
+
532
+ ctx.font = 'bold 10px Segoe UI';
533
+ ctx.fillStyle = '#8fc4e8';
534
+ ctx.textAlign = 'center';
535
+ ctx.fillText('RADAR', rx, ry-R-8);
536
+ ctx.restore();
537
+ }
538
+
539
+ function drawHitFlash(){
540
+ if (hitFlashT <= 0) return;
541
+ ctx.save();
542
+ ctx.globalAlpha = Math.min(0.5, hitFlashT);
543
+ ctx.fillStyle = '#ff2a2a';
544
+ ctx.fillRect(0,0,W,H);
545
+ ctx.restore();
546
+ }
547
+
548
+ function drawGameOverBackdrop(){
549
+ if (state !== 'gameover') return;
550
+ ctx.save();
551
+ ctx.fillStyle = 'rgba(0,0,0,0.35)';
552
+ ctx.fillRect(0,0,W,H);
553
+ ctx.restore();
554
+ }
555
+
556
+ let lastTime = performance.now();
557
+ function loop(){
558
+ const now = performance.now();
559
+ const dt = Math.min(0.05, (now-lastTime)/1000);
560
+ lastTime = now;
561
+
562
+ update(dt);
563
+
564
+ ctx.clearRect(0,0,W,H);
565
+ ctx.save();
566
+ if (shakeT > 0){
567
+ const mag = shakeT * 10;
568
+ ctx.translate((Math.random()-0.5)*mag, (Math.random()-0.5)*mag);
569
+ }
570
+ ctx.save();
571
+ ctx.translate(CX, CY);
572
+ ctx.rotate(cam.bank);
573
+ ctx.translate(-CX, -CY);
574
+ drawHorizonGrid();
575
+ drawStars();
576
+ drawAsteroids();
577
+ drawLasers();
578
+ ctx.restore();
579
+ drawParticles();
580
+ drawReticle();
581
+ drawCockpit();
582
+ drawHitFlash();
583
+ if (muzzleFlashT > 0){
584
+ ctx.save();
585
+ ctx.globalAlpha = muzzleFlashT / 0.08;
586
+ ctx.fillStyle = '#cfe8ff';
587
+ ctx.shadowColor = '#4ecbff'; ctx.shadowBlur = 20;
588
+ ctx.beginPath();
589
+ ctx.arc(CX, H*0.86, 14, 0, Math.PI*2);
590
+ ctx.fill();
591
+ ctx.restore();
592
+ }
593
+ drawGameOverBackdrop();
594
+ ctx.restore();
595
+
596
+ requestAnimationFrame(loop);
597
+ }
598
+ loop();
599
+ </script>
600
+ </body>
601
+ </html>