← back to Games Agentabrams
games/asteroids/index.html
829 lines
<!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>Abrams Asteroids</title>
<style>
:root {
--neon: #00f0ff;
--neon2: #ff2bd6;
--accent: #b6ff3a;
--bg0: #04060f;
--bg1: #0a0f24;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
overflow: hidden;
background: var(--bg0);
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
-webkit-user-select: none; user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: none;
}
#wrap {
position: fixed; inset: 0;
background:
radial-gradient(ellipse at 50% 30%, #0d1636 0%, var(--bg0) 70%),
var(--bg0);
}
canvas { position: absolute; inset: 0; display: block; }
/* HUD */
#hud {
position: absolute; top: 0; left: 0; right: 0;
display: flex; justify-content: space-between; align-items: flex-start;
padding: 14px 18px;
pointer-events: none;
font-variant-numeric: tabular-nums;
text-shadow: 0 0 8px currentColor;
z-index: 5;
}
#hud .col { display: flex; flex-direction: column; gap: 4px; }
#hud .col.right { align-items: flex-end; text-align: right; }
.label { font-size: 11px; letter-spacing: 3px; color: #6f7bb5; text-shadow: none; }
.val { font-size: 22px; font-weight: 700; letter-spacing: 1px; color: var(--neon); }
.val.best { color: var(--accent); font-size: 16px; }
#lives { display: flex; gap: 6px; margin-top: 2px; }
#lives svg { filter: drop-shadow(0 0 4px var(--neon)); }
#wave { color: var(--neon2); font-size: 16px; }
/* Overlay */
#overlay {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
background: radial-gradient(ellipse at 50% 45%, rgba(6,10,28,.72) 0%, rgba(2,4,12,.94) 75%);
z-index: 10;
padding: 24px;
backdrop-filter: blur(2px);
}
#overlay.hidden { display: none; }
#title {
font-size: clamp(34px, 9vw, 84px);
font-weight: 800;
letter-spacing: clamp(2px, 1.5vw, 12px);
color: #eafcff;
text-shadow: 0 0 18px var(--neon), 0 0 40px var(--neon), 0 0 4px #fff;
line-height: 1;
}
#title .sub { display:block; font-size: clamp(13px,3.5vw,22px); letter-spacing: clamp(3px,2vw,16px); color: var(--neon2); text-shadow: 0 0 12px var(--neon2); margin-top: 10px; }
#ovMsg { margin-top: 18px; font-size: clamp(14px,4vw,20px); color: #b9c6ff; min-height: 1.2em; text-shadow: 0 0 8px rgba(120,150,255,.4);}
#ovScore { margin-top: 6px; font-size: clamp(13px,3.6vw,18px); color: var(--accent); }
#playBtn {
margin-top: 26px;
pointer-events: auto; cursor: pointer;
background: linear-gradient(180deg, rgba(0,240,255,.16), rgba(0,240,255,.04));
border: 2px solid var(--neon);
color: #eafcff;
font-size: clamp(16px,4.5vw,22px); font-weight: 700; letter-spacing: 4px;
padding: 14px 40px; border-radius: 40px;
text-shadow: 0 0 10px var(--neon);
box-shadow: 0 0 22px rgba(0,240,255,.35), inset 0 0 18px rgba(0,240,255,.18);
transition: transform .08s ease, box-shadow .2s ease;
}
#playBtn:hover { box-shadow: 0 0 34px rgba(0,240,255,.6), inset 0 0 24px rgba(0,240,255,.28); }
#playBtn:active { transform: scale(.96); }
#hint {
margin-top: 26px; font-size: clamp(11px,3vw,14px); color: #6f7bb5;
letter-spacing: 1px; line-height: 1.9; max-width: 560px;
}
#hint b { color: var(--neon); font-weight: 700; }
/* Toast / status pills */
#statusPills {
position: absolute; top: 56px; left: 50%; transform: translateX(-50%);
display: flex; gap: 10px; z-index: 6; pointer-events: none;
}
.pill {
font-size: 11px; letter-spacing: 2px; padding: 4px 10px; border-radius: 20px;
border: 1px solid rgba(120,150,255,.4); color: #9fb0f0; background: rgba(10,16,40,.6);
opacity: 0; transition: opacity .3s;
}
.pill.on { opacity: 1; }
.pill.paused { color: var(--accent); border-color: var(--accent); }
.pill.muted { color: var(--neon2); border-color: var(--neon2); }
#centerToast {
position: absolute; top: 42%; left: 50%; transform: translate(-50%,-50%);
font-size: clamp(20px,7vw,48px); font-weight: 800; letter-spacing: 6px;
color: #eafcff; text-shadow: 0 0 20px var(--neon2), 0 0 40px var(--neon2);
z-index: 7; pointer-events: none; opacity: 0; transition: opacity .4s;
}
#centerToast.on { opacity: 1; }
/* Touch controls */
#touch { position: absolute; inset: 0; z-index: 8; display: none; pointer-events: none; }
#touch.show { display: block; }
.tbtn {
position: absolute; pointer-events: auto;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
border: 2px solid rgba(0,240,255,.55);
background: rgba(8,14,36,.42);
color: var(--neon); font-weight: 700; font-size: 13px; letter-spacing: 1px;
box-shadow: 0 0 14px rgba(0,240,255,.22), inset 0 0 12px rgba(0,240,255,.12);
backdrop-filter: blur(2px);
-webkit-user-select: none; user-select: none;
transition: background .08s, transform .08s;
}
.tbtn:active, .tbtn.active { background: rgba(0,240,255,.28); transform: scale(.94); }
.tbtn svg { width: 44%; height: 44%; fill: var(--neon); filter: drop-shadow(0 0 4px var(--neon)); }
#tLeft { left: 20px; bottom: 96px; width: 74px; height: 74px; }
#tRight { left: 110px; bottom: 30px; width: 74px; height: 74px; }
#tThrust{ right: 110px; bottom: 96px; width: 82px; height: 82px; }
#tFire { right: 20px; bottom: 30px; width: 82px; height: 82px; border-color: var(--neon2); color: var(--neon2); box-shadow: 0 0 14px rgba(255,43,214,.3), inset 0 0 12px rgba(255,43,214,.14); }
#tFire svg { fill: var(--neon2); filter: drop-shadow(0 0 4px var(--neon2)); }
#tHyper { right: 24px; top: 84px; width: 56px; height: 56px; font-size: 11px; border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
<div id="hud">
<div class="col">
<span class="label">SCORE</span>
<span class="val" id="score">0</span>
<span class="val best" id="best">BEST 0</span>
</div>
<div class="col right">
<span class="label">WAVE</span>
<span class="val" id="wave">1</span>
<span class="label" style="margin-top:6px;">SHIPS</span>
<div id="lives"></div>
</div>
</div>
<div id="statusPills">
<span class="pill paused" id="pillPaused">PAUSED</span>
<span class="pill muted" id="pillMuted">MUTED</span>
</div>
<div id="centerToast"></div>
<!-- Touch controls -->
<div id="touch">
<div class="tbtn" id="tLeft"><svg viewBox="0 0 100 100"><path d="M65 15 L30 50 L65 85 Z"/></svg></div>
<div class="tbtn" id="tRight"><svg viewBox="0 0 100 100"><path d="M35 15 L70 50 L35 85 Z"/></svg></div>
<div class="tbtn" id="tThrust"><svg viewBox="0 0 100 100"><path d="M50 12 L80 78 L50 62 L20 78 Z"/></svg></div>
<div class="tbtn" id="tFire"><svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="26"/></svg></div>
<div class="tbtn" id="tHyper">HYP</div>
</div>
<div id="overlay">
<div id="title">ABRAMS<span class="sub">ASTEROIDS</span></div>
<div id="ovMsg">Clear the field. Survive the swarm.</div>
<div id="ovScore"></div>
<button id="playBtn">LAUNCH</button>
<div id="hint">
<b>▲ / W</b> Thrust <b>◀ ▶ / A D</b> Rotate <b>SPACE</b> Fire<br>
<b>SHIFT</b> Hyperspace <b>P</b> Pause <b>M</b> Mute <b>R</b> Restart<br>
<span style="opacity:.75">Touch devices: use the on-screen pads.</span>
</div>
</div>
</div>
<script>
(function () {
"use strict";
// ---------- Setup ----------
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var wrap = document.getElementById("wrap");
var W = 0, H = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
var r = wrap.getBoundingClientRect();
W = Math.max(320, r.width);
H = Math.max(240, r.height);
canvas.width = Math.floor(W * DPR);
canvas.height = Math.floor(H * DPR);
canvas.style.width = W + "px";
canvas.style.height = H + "px";
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener("resize", resize);
resize();
// ---------- Audio (Web Audio, procedural) ----------
var Audio = (function () {
var actx = null, master = null;
var muted = false;
function ensure() {
if (actx) return;
try {
var AC = window.AudioContext || window.webkitAudioContext;
actx = new AC();
master = actx.createGain();
master.gain.value = 0.35;
master.connect(actx.destination);
} catch (e) { actx = null; }
}
function resume() { if (actx && actx.state === "suspended") actx.resume(); }
function tone(freq, dur, type, vol, slideTo) {
if (muted || !actx) return;
var t = actx.currentTime;
var o = actx.createOscillator();
var g = actx.createGain();
o.type = type || "square";
o.frequency.setValueAtTime(freq, t);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(vol || 0.3, t + 0.008);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(master);
o.start(t); o.stop(t + dur + 0.02);
}
function noise(dur, vol, freq) {
if (muted || !actx) return;
var t = actx.currentTime;
var n = Math.floor(actx.sampleRate * dur);
var buf = actx.createBuffer(1, n, actx.sampleRate);
var d = buf.getChannelData(0);
for (var i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / n);
var src = actx.createBufferSource(); src.buffer = buf;
var flt = actx.createBiquadFilter();
flt.type = "lowpass"; flt.frequency.value = freq || 1200;
var g = actx.createGain();
g.gain.setValueAtTime(vol || 0.4, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
src.connect(flt); flt.connect(g); g.connect(master);
src.start(t);
}
return {
init: function () { ensure(); resume(); },
resume: resume,
isMuted: function () { return muted; },
toggleMute: function () { muted = !muted; return muted; },
shoot: function () { tone(880, 0.09, "square", 0.14, 300); },
thrust: function () { noise(0.05, 0.06, 500); },
bangBig: function () { noise(0.32, 0.5, 700); tone(70, 0.3, "sawtooth", 0.2, 40); },
bangMed: function () { noise(0.22, 0.4, 1000); tone(120, 0.2, "sawtooth", 0.15, 60); },
bangSmall: function () { noise(0.14, 0.3, 1600); tone(200, 0.12, "square", 0.12, 90); },
hyper: function () { tone(200, 0.35, "sine", 0.25, 1400); },
death: function () { noise(0.6, 0.55, 500); tone(160, 0.6, "sawtooth", 0.3, 30); },
wave: function () { tone(523, 0.12, "triangle", 0.2); setTimeout(function(){tone(784,0.16,"triangle",0.2);}, 120); },
extraLife: function () { tone(660, 0.1, "triangle", 0.2); setTimeout(function(){tone(990,0.14,"triangle",0.22);}, 100); }
};
})();
// ---------- Input ----------
var keys = {};
var touch = { left: false, right: false, thrust: false, fire: false };
function down(code) {
if (code === "KeyM") { toggleMute(); return; }
if (code === "KeyP") { togglePause(); return; }
if (code === "KeyR") { if (state === "play" || state === "over") restart(); return; }
keys[code] = true;
}
function up(code) { keys[code] = false; }
window.addEventListener("keydown", function (e) {
var c = e.code;
if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space"].indexOf(c) >= 0) e.preventDefault();
if (state !== "play") {
if (c === "Space" || c === "Enter") {
if (state === "start" || state === "over") { startGame(); return; }
}
}
Audio.init();
down(c);
});
window.addEventListener("keyup", function (e) { up(e.code); });
// Touch buttons
function bindTouch(id, prop) {
var el = document.getElementById(id);
function on(e) { e.preventDefault(); Audio.init(); touch[prop] = true; el.classList.add("active"); }
function off(e) { e.preventDefault(); touch[prop] = false; el.classList.remove("active"); }
el.addEventListener("touchstart", on, { passive: false });
el.addEventListener("touchend", off, { passive: false });
el.addEventListener("touchcancel", off, { passive: false });
el.addEventListener("mousedown", on);
el.addEventListener("mouseup", off);
el.addEventListener("mouseleave", off);
}
bindTouch("tLeft", "left");
bindTouch("tRight", "right");
bindTouch("tThrust", "thrust");
bindTouch("tFire", "fire");
(function () {
var el = document.getElementById("tHyper");
function on(e) { e.preventDefault(); Audio.init(); doHyperspace(); el.classList.add("active"); }
function off(e) { e.preventDefault(); el.classList.remove("active"); }
el.addEventListener("touchstart", on, { passive: false });
el.addEventListener("touchend", off, { passive: false });
el.addEventListener("mousedown", on);
el.addEventListener("mouseup", off);
})();
var isTouch = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
if (isTouch) document.getElementById("touch").classList.add("show");
// ---------- Game constants ----------
var SHIP_R = 14;
var TURN = 4.2; // rad/sec
var THRUST = 340; // px/sec^2
var FRICTION = 0.72; // per sec (velocity retained ~)
var MAX_SPEED = 460;
var BULLET_SPEED = 560;
var BULLET_LIFE = 0.85;
var FIRE_COOLDOWN = 0.16;
var INVULN_TIME = 2.4;
var START_LIVES = 3;
var EXTRA_LIFE_EVERY = 10000;
var AST_SIZES = { big: 46, med: 26, small: 15 };
var AST_SCORE = { big: 20, med: 50, small: 100 };
var AST_SPEED = { big: 42, med: 66, small: 92 };
// ---------- State ----------
var state = "start"; // start | play | paused | over
var ship, bullets, asteroids, particles;
var score, best, lives, wave, fireTimer, hyperCooldown, nextExtra;
var thrustSoundTimer = 0;
best = parseInt(localStorage.getItem("abrams-asteroids-best") || "0", 10) || 0;
function rand(a, b) { return a + Math.random() * (b - a); }
function wrapPos(o) {
if (o.x < 0) o.x += W; else if (o.x >= W) o.x -= W;
if (o.y < 0) o.y += H; else if (o.y >= H) o.y -= H;
}
function makeShip() {
return { x: W / 2, y: H / 2, a: -Math.PI / 2, vx: 0, vy: 0, r: SHIP_R, invuln: INVULN_TIME, thrusting: false, alive: true };
}
function makeAsteroid(x, y, size) {
var speed = AST_SPEED[size] * rand(0.7, 1.35);
var ang = rand(0, Math.PI * 2);
var r = AST_SIZES[size];
var verts = [], n = Math.floor(rand(9, 14));
for (var i = 0; i < n; i++) {
verts.push(rand(0.72, 1.15));
}
return {
x: x, y: y, size: size, r: r,
vx: Math.cos(ang) * speed, vy: Math.sin(ang) * speed,
rot: rand(-1, 1), spin: rand(-0.9, 0.9),
verts: verts, hue: rand(180, 300)
};
}
function spawnWave(n) {
asteroids = [];
for (var i = 0; i < n; i++) {
// spawn away from ship center
var x, y, tries = 0;
do {
x = rand(0, W); y = rand(0, H);
tries++;
} while (ship && Math.hypot(x - ship.x, y - ship.y) < 180 && tries < 30);
asteroids.push(makeAsteroid(x, y, "big"));
}
}
function resetRound() {
ship = makeShip();
bullets = [];
particles = [];
fireTimer = 0;
hyperCooldown = 0;
}
function startGame() {
Audio.init();
score = 0; lives = START_LIVES; wave = 1; nextExtra = EXTRA_LIFE_EVERY;
resetRound();
spawnWave(4);
state = "play";
hideOverlay();
Audio.wave();
lastT = performance.now();
showToast("WAVE 1", 900);
}
function restart() {
startGame();
}
function nextWave() {
wave++;
spawnWave(Math.min(4 + wave, 12));
ship.invuln = Math.max(ship.invuln, 1.2);
Audio.wave();
showToast("WAVE " + wave, 900);
}
function gameOver() {
state = "over";
if (score > best) {
best = score;
localStorage.setItem("abrams-asteroids-best", String(best));
}
updateHud();
showOverlay(
"GAME OVER",
"The field claimed your fleet.",
"SCORE " + score + " • BEST " + best,
"PLAY AGAIN"
);
}
// ---------- Actions ----------
function fire() {
if (fireTimer > 0 || !ship || !ship.alive) return;
if (bullets.length > 8) return;
var nx = Math.cos(ship.a), ny = Math.sin(ship.a);
bullets.push({
x: ship.x + nx * ship.r, y: ship.y + ny * ship.r,
vx: ship.vx + nx * BULLET_SPEED, vy: ship.vy + ny * BULLET_SPEED,
life: BULLET_LIFE
});
fireTimer = FIRE_COOLDOWN;
Audio.shoot();
}
function doHyperspace() {
if (state !== "play" || !ship || !ship.alive || hyperCooldown > 0) return;
hyperCooldown = 1.2;
// small risk: 12% chance of misjump explosion
ship.x = rand(40, W - 40);
ship.y = rand(40, H - 40);
ship.vx = 0; ship.vy = 0;
ship.invuln = Math.max(ship.invuln, 0.8);
Audio.hyper();
for (var i = 0; i < 14; i++) spawnParticle(ship.x, ship.y, 200, "#00f0ff");
if (Math.random() < 0.12) {
killShip();
}
}
function killShip() {
if (!ship.alive) return;
ship.alive = false;
Audio.death();
for (var i = 0; i < 26; i++) spawnParticle(ship.x, ship.y, 240, "#ff2bd6");
lives--;
updateHud();
if (lives <= 0) {
setTimeout(gameOver, 900);
} else {
setTimeout(function () {
if (state !== "play") return;
ship = makeShip();
}, 900);
}
}
function splitAsteroid(a, idx) {
// score
addScore(AST_SCORE[a.size]);
// particles
var col = "hsl(" + a.hue + ",90%,65%)";
var pc = a.size === "big" ? 16 : a.size === "med" ? 11 : 7;
for (var i = 0; i < pc; i++) spawnParticle(a.x, a.y, a.size === "big" ? 220 : 160, col);
if (a.size === "big") Audio.bangBig();
else if (a.size === "med") Audio.bangMed();
else Audio.bangSmall();
asteroids.splice(idx, 1);
if (a.size === "big") {
asteroids.push(makeAsteroid(a.x, a.y, "med"));
asteroids.push(makeAsteroid(a.x, a.y, "med"));
} else if (a.size === "med") {
asteroids.push(makeAsteroid(a.x, a.y, "small"));
asteroids.push(makeAsteroid(a.x, a.y, "small"));
}
}
function addScore(n) {
score += n;
if (score >= nextExtra) {
lives++;
nextExtra += EXTRA_LIFE_EVERY;
Audio.extraLife();
showToast("EXTRA SHIP", 800);
}
updateHud();
}
function spawnParticle(x, y, spd, color) {
var a = rand(0, Math.PI * 2);
var s = rand(spd * 0.25, spd);
particles.push({
x: x, y: y, vx: Math.cos(a) * s, vy: Math.sin(a) * s,
life: rand(0.4, 0.9), max: 0.9, color: color
});
}
// ---------- Update ----------
function update(dt) {
if (state !== "play") return;
// ship controls
if (ship && ship.alive) {
var left = keys["ArrowLeft"] || keys["KeyA"] || touch.left;
var right = keys["ArrowRight"] || keys["KeyD"] || touch.right;
var thr = keys["ArrowUp"] || keys["KeyW"] || touch.thrust;
var shift = keys["ShiftLeft"] || keys["ShiftRight"];
if (left) ship.a -= TURN * dt;
if (right) ship.a += TURN * dt;
ship.thrusting = !!thr;
if (thr) {
ship.vx += Math.cos(ship.a) * THRUST * dt;
ship.vy += Math.sin(ship.a) * THRUST * dt;
thrustSoundTimer -= dt;
if (thrustSoundTimer <= 0) { Audio.thrust(); thrustSoundTimer = 0.06; }
}
// clamp speed
var sp = Math.hypot(ship.vx, ship.vy);
if (sp > MAX_SPEED) { ship.vx *= MAX_SPEED / sp; ship.vy *= MAX_SPEED / sp; }
// friction
var f = Math.pow(FRICTION, dt);
ship.vx *= f; ship.vy *= f;
ship.x += ship.vx * dt; ship.y += ship.vy * dt;
wrapPos(ship);
if (shift && hyperCooldown <= 0) doHyperspace();
if (keys["Space"] || touch.fire) fire();
if (ship.invuln > 0) ship.invuln -= dt;
}
if (fireTimer > 0) fireTimer -= dt;
if (hyperCooldown > 0) hyperCooldown -= dt;
// bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.x += b.vx * dt; b.y += b.vy * dt;
wrapPos(b);
b.life -= dt;
if (b.life <= 0) { bullets.splice(i, 1); continue; }
}
// asteroids
for (var j = 0; j < asteroids.length; j++) {
var a = asteroids[j];
a.x += a.vx * dt; a.y += a.vy * dt;
a.rot += a.spin * dt;
wrapPos(a);
}
// bullet vs asteroid
for (var bi = bullets.length - 1; bi >= 0; bi--) {
var bu = bullets[bi];
for (var ai = asteroids.length - 1; ai >= 0; ai--) {
var as = asteroids[ai];
if ((bu.x - as.x) * (bu.x - as.x) + (bu.y - as.y) * (bu.y - as.y) < as.r * as.r) {
bullets.splice(bi, 1);
splitAsteroid(as, ai);
break; // this bullet is gone; move to next bullet
}
}
}
// ship vs asteroid
if (ship && ship.alive && ship.invuln <= 0) {
for (var k = 0; k < asteroids.length; k++) {
var ak = asteroids[k];
var rr = ak.r + ship.r * 0.7;
if ((ship.x - ak.x) * (ship.x - ak.x) + (ship.y - ak.y) * (ship.y - ak.y) < rr * rr) {
killShip();
break;
}
}
}
// particles
for (var p = particles.length - 1; p >= 0; p--) {
var pt = particles[p];
pt.x += pt.vx * dt; pt.y += pt.vy * dt;
pt.vx *= Math.pow(0.4, dt); pt.vy *= Math.pow(0.4, dt);
pt.life -= dt;
if (pt.life <= 0) particles.splice(p, 1);
}
// wave clear
if (asteroids.length === 0) nextWave();
}
// ---------- Render ----------
var starField = null;
function buildStars() {
starField = [];
var n = Math.floor((W * H) / 6500);
for (var i = 0; i < n; i++) {
starField.push({ x: Math.random() * W, y: Math.random() * H, r: Math.random() * 1.4 + 0.2, a: Math.random() * 0.6 + 0.2 });
}
}
buildStars();
var lastStarKey = W + "x" + H;
function drawStars() {
var key = W + "x" + H;
if (key !== lastStarKey || !starField) { buildStars(); lastStarKey = key; }
ctx.save();
for (var i = 0; i < starField.length; i++) {
var s = starField[i];
ctx.globalAlpha = s.a;
ctx.fillStyle = "#9fc7ff";
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawShip() {
if (!ship || !ship.alive) return;
if (ship.invuln > 0 && Math.floor(ship.invuln * 12) % 2 === 0) return; // blink
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.a);
// thrust flame
if (ship.thrusting && Math.random() > 0.35) {
ctx.beginPath();
ctx.moveTo(-ship.r * 0.6, -5);
ctx.lineTo(-ship.r * 1.5 - Math.random() * 8, 0);
ctx.lineTo(-ship.r * 0.6, 5);
ctx.strokeStyle = "#ff9b2b";
ctx.lineWidth = 2;
ctx.shadowBlur = 14; ctx.shadowColor = "#ff6a00";
ctx.stroke();
}
// hull
ctx.beginPath();
ctx.moveTo(ship.r, 0);
ctx.lineTo(-ship.r * 0.7, -ship.r * 0.75);
ctx.lineTo(-ship.r * 0.4, 0);
ctx.lineTo(-ship.r * 0.7, ship.r * 0.75);
ctx.closePath();
ctx.strokeStyle = "#eafcff";
ctx.lineWidth = 2;
ctx.shadowBlur = 12; ctx.shadowColor = "#00f0ff";
ctx.stroke();
ctx.restore();
}
function drawAsteroid(a) {
ctx.save();
ctx.translate(a.x, a.y);
ctx.rotate(a.rot);
ctx.beginPath();
var n = a.verts.length;
for (var i = 0; i < n; i++) {
var ang = (i / n) * Math.PI * 2;
var rr = a.r * a.verts[i];
var px = Math.cos(ang) * rr, py = Math.sin(ang) * rr;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.strokeStyle = "hsl(" + a.hue + ",85%,68%)";
ctx.lineWidth = 2;
ctx.shadowBlur = 12; ctx.shadowColor = "hsl(" + a.hue + ",90%,55%)";
ctx.stroke();
ctx.restore();
}
function drawBullets() {
ctx.save();
ctx.fillStyle = "#eafcff";
ctx.shadowBlur = 10; ctx.shadowColor = "#00f0ff";
for (var i = 0; i < bullets.length; i++) {
var b = bullets[i];
ctx.beginPath();
ctx.arc(b.x, b.y, 2.4, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawParticles() {
ctx.save();
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
ctx.globalAlpha = Math.max(0, p.life / p.max);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function render() {
ctx.clearRect(0, 0, W, H);
drawStars();
if (state === "play" || state === "paused" || state === "over") {
drawParticles();
for (var i = 0; i < (asteroids ? asteroids.length : 0); i++) drawAsteroid(asteroids[i]);
drawBullets();
drawShip();
}
}
// ---------- HUD / overlay ----------
var elScore = document.getElementById("score");
var elBest = document.getElementById("best");
var elWave = document.getElementById("wave");
var elLives = document.getElementById("lives");
var overlay = document.getElementById("overlay");
var ovTitle = document.getElementById("title");
var ovMsg = document.getElementById("ovMsg");
var ovScore = document.getElementById("ovScore");
var playBtn = document.getElementById("playBtn");
var pillPaused = document.getElementById("pillPaused");
var pillMuted = document.getElementById("pillMuted");
var centerToast = document.getElementById("centerToast");
function shipIcon() {
return '<svg width="16" height="16" viewBox="0 0 100 100"><path d="M85 50 L20 20 L38 50 L20 80 Z" fill="none" stroke="#00f0ff" stroke-width="8"/></svg>';
}
function updateHud() {
elScore.textContent = score != null ? score : 0;
elBest.textContent = "BEST " + best;
elWave.textContent = wave != null ? wave : 1;
var html = "";
var n = Math.max(0, lives != null ? lives : 0);
for (var i = 0; i < Math.min(n, 6); i++) html += shipIcon();
if (n > 6) html += '<span style="color:#00f0ff;font-size:14px;align-self:center;">x' + n + '</span>';
elLives.innerHTML = html;
}
function showOverlay(title, msg, sc, btn) {
ovTitle.innerHTML = title === "ABRAMS" ? 'ABRAMS<span class="sub">ASTEROIDS</span>' :
(title + '<span class="sub" style="visibility:hidden">.</span>');
if (title === "GAME OVER") {
ovTitle.innerHTML = 'GAME<span class="sub">OVER</span>';
}
ovMsg.textContent = msg || "";
ovScore.textContent = sc || "";
playBtn.textContent = btn || "LAUNCH";
overlay.classList.remove("hidden");
}
function hideOverlay() { overlay.classList.add("hidden"); }
playBtn.addEventListener("click", function () { Audio.init(); startGame(); });
var toastTimer = null;
function showToast(text, ms) {
centerToast.textContent = text;
centerToast.classList.add("on");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () { centerToast.classList.remove("on"); }, ms || 800);
}
function togglePause() {
if (state === "play") {
state = "paused";
pillPaused.classList.add("on");
} else if (state === "paused") {
state = "play";
pillPaused.classList.remove("on");
lastT = performance.now();
}
}
function toggleMute() {
var m = Audio.toggleMute();
if (m) pillMuted.classList.add("on"); else pillMuted.classList.remove("on");
}
// ---------- Main loop ----------
var lastT = performance.now();
function loop(now) {
var dt = (now - lastT) / 1000;
lastT = now;
if (dt > 0.05) dt = 0.05; // clamp to avoid huge steps
if (dt < 0) dt = 0;
update(dt);
render();
requestAnimationFrame(loop);
}
// init HUD + overlay
score = 0; lives = START_LIVES; wave = 1;
updateHud();
showOverlay("ABRAMS", "Clear the field. Survive the swarm.", "", "LAUNCH");
requestAnimationFrame(loop);
})();
</script>
</body>
</html>