Upload star-voyager.html
Browse files- 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>
|