tab-shooter/popup.js

868 lines
23 KiB
JavaScript

/* ============================================================
TAB SHOOTER — Game Engine + Chrome API Integration
============================================================ */
const CANVAS_W = 420;
const CANVAS_H = 600;
// ---------- DOM ----------
const canvas = document.getElementById("game-canvas");
const ctx = canvas.getContext("2d");
const screens = {
start: document.getElementById("start-screen"),
game: document.getElementById("game-screen"),
gameover: document.getElementById("gameover-screen"),
empty: document.getElementById("empty-screen"),
};
const els = {
hsValue: document.getElementById("hs-value"),
hudScore: document.getElementById("hud-score"),
hudLives: document.getElementById("hud-lives"),
hudTabs: document.getElementById("hud-tabs"),
hudPowerups: document.getElementById("hud-powerups"),
resultTabs: document.getElementById("result-tabs"),
resultScore: document.getElementById("result-score"),
resultHighscore: document.getElementById("result-highscore"),
newRecord: document.getElementById("new-record"),
};
// ---------- GAME STATE ----------
let game = null;
let highscore = 0;
let selectedScope = "current"; // "current" | "all"
// ---------- UTILITIES ----------
function showScreen(name) {
Object.values(screens).forEach((s) => s.classList.remove("visible"));
screens[name].classList.add("visible");
}
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// Load a favicon for a given URL as an Image (Chrome MV3 favicon API).
// Returns a Promise that resolves to an Image or null on error.
function loadFavicon(pageUrl, size = 32) {
return new Promise((resolve) => {
// Chrome "_favicon" API route. Requires "favicon" permission in manifest.
const favUrl = `/_favicon/?pageUrl=${encodeURIComponent(pageUrl)}&size=${size}`;
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
// Slight delay safety: if it fails we just render the placeholder letter.
img.src = favUrl;
});
}
// ---------- HIGHSCORE ----------
async function loadHighscore() {
const data = await chrome.storage.local.get("highscore");
highscore = data.highscore || 0;
els.hsValue.textContent = highscore;
}
async function saveHighscore(value) {
await chrome.storage.local.set({ highscore: value });
highscore = value;
}
// ---------- TAB LOADING ----------
async function loadTabs(scope) {
let tabs = [];
if (scope === "all") {
tabs = await chrome.tabs.query({});
} else {
const win = await chrome.windows.getLastFocused();
tabs = await chrome.tabs.query({ windowId: win.id });
}
// Filter out the popup itself (this extension page), chrome:// pages, and
// devtools. We keep everything else as a valid target.
const playable = tabs.filter((t) => {
if (!t.id) return false;
if (t.url && t.url.startsWith("chrome-extension://")) return false;
if (t.url && t.url.startsWith("chrome://")) return false;
if (t.url && t.url.startsWith("devtools://")) return false;
return true;
});
return playable;
}
// ============================================================
// GAME CLASS
// ============================================================
class TabShooterGame {
constructor(tabs) {
this.tabs = tabs;
this.enemies = [];
this.bullets = [];
this.enemyBullets = [];
this.particles = [];
this.powerups = [];
this.floatingTexts = [];
this.player = {
x: CANVAS_W / 2,
y: CANVAS_H - 60,
w: 40,
h: 36,
speed: 5,
cooldown: 0,
lives: 3,
invincible: 0,
};
this.score = 0;
this.tabsDestroyed = 0;
this.totalTabs = tabs.length;
this.keys = {};
this.mouseX = null;
this.firing = false;
// Power-up timers
this.tripleShot = 0;
this.rapidFire = 0;
this.shield = 0;
this.running = false;
this.lastTime = 0;
this.spawnTimer = 0;
this.spawnIndex = 0;
// Bind handlers
this._onKeyDown = this.onKeyDown.bind(this);
this._onKeyUp = this.onKeyUp.bind(this);
this._onMouseMove = this.onMouseMove.bind(this);
this._onMouseDown = this.onMouseDown.bind(this);
this._onMouseUp = this.onMouseUp.bind(this);
this._onMouseLeave = this.onMouseLeave.bind(this);
}
async init() {
// Preload favicons for all tabs, then build enemy spawn queue.
const loaded = await Promise.all(
this.tabs.map(async (t) => {
const fav = await loadFavicon(t.url || "", 32);
return {
tabId: t.id,
title: (t.title || "Tab").slice(0, 22),
url: t.url,
favicon: fav,
};
})
);
// Shuffle the spawn order for variety
this.spawnQueue = loaded.sort(() => Math.random() - 0.5);
}
start() {
this.running = true;
this.attachInput();
this.lastTime = performance.now();
requestAnimationFrame(this.loop.bind(this));
}
stop() {
this.running = false;
this.detachInput();
}
// ---------- INPUT ----------
attachInput() {
window.addEventListener("keydown", this._onKeyDown);
window.addEventListener("keyup", this._onKeyUp);
canvas.addEventListener("mousemove", this._onMouseMove);
canvas.addEventListener("mousedown", this._onMouseDown);
canvas.addEventListener("mouseup", this._onMouseUp);
canvas.addEventListener("mouseleave", this._onMouseLeave);
}
detachInput() {
window.removeEventListener("keydown", this._onKeyDown);
window.removeEventListener("keyup", this._onKeyUp);
canvas.removeEventListener("mousemove", this._onMouseMove);
canvas.removeEventListener("mousedown", this._onMouseDown);
canvas.removeEventListener("mouseup", this._onMouseUp);
canvas.removeEventListener("mouseleave", this._onMouseLeave);
}
onKeyDown(e) {
this.keys[e.key.toLowerCase()] = true;
if ([" ", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
e.preventDefault();
}
}
onKeyUp(e) {
this.keys[e.key.toLowerCase()] = false;
}
onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
this.mouseX = e.clientX - rect.left;
}
onMouseDown() {
this.firing = true;
}
onMouseUp() {
this.firing = false;
}
onMouseLeave() {
this.mouseX = null;
this.firing = false;
}
// ---------- SPAWNING ----------
spawnEnemy() {
if (this.spawnIndex >= this.spawnQueue.length) return;
const data = this.spawnQueue[this.spawnIndex];
this.spawnIndex++;
const w = 60;
const h = 50;
const x = rand(30, CANVAS_W - 30 - w);
// ~25% of enemies are "shooters"
const isShooter = Math.random() < 0.25;
this.enemies.push({
x,
y: -h - 10,
w,
h,
vx: rand(-1.2, 1.2),
vy: rand(0.5, 1.1),
hp: 1,
tabId: data.tabId,
title: data.title,
favicon: data.favicon,
shooter: isShooter,
shootCooldown: rand(60, 180),
swayPhase: rand(0, Math.PI * 2),
hit: 0,
closing: false, // true once we've issued tabs.remove for it
});
}
// ---------- SHOOTING ----------
fireBullet() {
if (this.player.cooldown > 0) return;
const baseCd = this.rapidFire > 0 ? 6 : 14;
this.player.cooldown = baseCd;
const px = this.player.x;
const py = this.player.y - this.player.h / 2;
if (this.tripleShot > 0) {
this.bullets.push({ x: px, y: py, vx: 0, vy: -9 });
this.bullets.push({ x: px - 4, y: py + 4, vx: -2.5, vy: -8.5 });
this.bullets.push({ x: px + 4, y: py + 4, vx: 2.5, vy: -8.5 });
} else {
this.bullets.push({ x: px, y: py, vx: 0, vy: -9 });
}
}
// ---------- POWER-UPS ----------
maybeDropPowerup(x, y) {
if (Math.random() < 0.18) {
const types = ["triple", "rapid", "shield"];
this.powerups.push({
x,
y,
vy: 1.6,
type: pick(types),
rot: 0,
});
}
}
applyPowerup(type) {
if (type === "triple") {
this.tripleShot = 360; // ~6s at 60fps
this.pushFloatingText("TRIPLE SHOT!", "#00f0ff");
} else if (type === "rapid") {
this.rapidFire = 360;
this.pushFloatingText("RAPID FIRE!", "#ffaa00");
} else if (type === "shield") {
this.shield = 1; // absorbs 1 hit
this.pushFloatingText("SHIELD UP!", "#00ff88");
}
updatePowerupHUD(this);
}
pushFloatingText(text, color) {
this.floatingTexts.push({
text,
color,
x: this.player.x,
y: this.player.y - 30,
life: 60,
});
}
// ---------- DAMAGE ----------
damagePlayer() {
if (this.player.invincible > 0) return;
if (this.shield > 0) {
this.shield = 0;
this.pushFloatingText("SHIELD GONE", "#ff4444");
updatePowerupHUD(this);
this.player.invincible = 40;
return;
}
this.player.lives--;
this.player.invincible = 90;
updateLivesHUD(this.player.lives);
// Screen-shake-ish particle burst
this.spawnExplosion(this.player.x, this.player.y, "#ff4444", 20);
if (this.player.lives <= 0) {
this.gameOver();
}
}
// ---------- PARTICLES ----------
spawnExplosion(x, y, color, count = 15) {
for (let i = 0; i < count; i++) {
const angle = rand(0, Math.PI * 2);
const speed = rand(1, 5);
this.particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: rand(20, 45),
maxLife: 45,
size: rand(2, 4),
color,
});
}
}
// ---------- TAB CLOSING ----------
closeTab(tabId) {
// Don't re-issue close for the same enemy.
if (chrome.tabs && typeof chrome.tabs.remove === "function") {
chrome.tabs.remove(tabId).catch(() => {
// Tab may already be gone — ignore silently.
});
}
}
// ---------- MAIN LOOP ----------
loop(now) {
if (!this.running) return;
const dt = Math.min(40, now - this.lastTime);
this.lastTime = now;
this.update(dt);
this.render();
requestAnimationFrame(this.loop.bind(this));
}
update(dt) {
const p = this.player;
// Movement: keyboard
let dx = 0;
if (this.keys["arrowleft"] || this.keys["a"]) dx -= 1;
if (this.keys["arrowright"] || this.keys["d"]) dx += 1;
p.x += dx * p.speed;
// Movement: mouse (overrides keyboard if present)
if (this.mouseX !== null) {
p.x += (this.mouseX - p.x) * 0.25;
}
p.x = clamp(p.x, p.w / 2, CANVAS_W - p.w / 2);
// Firing
if (this.keys[" "] || this.firing) {
this.fireBullet();
}
if (p.cooldown > 0) p.cooldown--;
if (p.invincible > 0) p.invincible--;
if (this.tripleShot > 0) this.tripleShot--;
if (this.rapidFire > 0) this.rapidFire--;
if (this.tripleShot === 0 || this.rapidFire === 0 || this.shield === 0) {
updatePowerupHUD(this);
}
// Spawn enemies gradually — keep a steady stream.
this.spawnTimer--;
if (this.spawnTimer <= 0 && this.spawnIndex < this.spawnQueue.length) {
this.spawnEnemy();
this.spawnTimer = rand(35, 80);
}
// Update bullets
for (let i = this.bullets.length - 1; i >= 0; i--) {
const b = this.bullets[i];
b.x += b.vx;
b.y += b.vy;
if (b.y < -10 || b.x < -10 || b.x > CANVAS_W + 10) {
this.bullets.splice(i, 1);
}
}
// Update enemy bullets
for (let i = this.enemyBullets.length - 1; i >= 0; i--) {
const b = this.enemyBullets[i];
b.y += b.vy;
b.x += b.vx;
// Hit player?
if (
Math.abs(b.x - p.x) < p.w / 2 &&
Math.abs(b.y - p.y) < p.h / 2
) {
this.enemyBullets.splice(i, 1);
this.damagePlayer();
continue;
}
if (b.y > CANVAS_H + 10) this.enemyBullets.splice(i, 1);
}
// Update enemies
for (let i = this.enemies.length - 1; i >= 0; i--) {
const e = this.enemies[i];
e.swayPhase += 0.04;
e.x += e.vx + Math.sin(e.swayPhase) * 0.4;
e.y += e.vy;
if (e.hit > 0) e.hit--;
// Bounce off side walls
if (e.x < 5 || e.x + e.w > CANVAS_W - 5) {
e.vx *= -1;
e.x = clamp(e.x, 5, CANVAS_W - 5 - e.w);
}
// Shooter fires
if (e.shooter && !e.closing) {
e.shootCooldown--;
if (e.shootCooldown <= 0) {
this.enemyBullets.push({
x: e.x + e.w / 2,
y: e.y + e.h,
vx: 0,
vy: 3.2,
});
e.shootCooldown = rand(90, 220);
}
}
// Enemy reaches bottom → player takes damage, enemy loops off
if (e.y > CANVAS_H) {
this.enemies.splice(i, 1);
continue;
}
// Bullet collisions
for (let j = this.bullets.length - 1; j >= 0; j--) {
const b = this.bullets[j];
if (
b.x > e.x &&
b.x < e.x + e.w &&
b.y > e.y &&
b.y < e.y + e.h
) {
this.bullets.splice(j, 1);
e.hp--;
e.hit = 6;
if (e.hp <= 0) {
// Destroyed! Close the real tab.
this.closeTab(e.tabId);
e.closing = true;
this.spawnExplosion(e.x + e.w / 2, e.y + e.h / 2, "#ff8844", 18);
this.maybeDropPowerup(e.x + e.w / 2, e.y + e.h / 2);
this.score += 100;
this.tabsDestroyed++;
updateScoreHUD(this.score);
updateTabsHUD(Math.max(0, this.totalTabs - this.tabsDestroyed));
this.enemies.splice(i, 1);
}
break;
}
}
}
// Collide enemy directly with player
for (let i = this.enemies.length - 1; i >= 0; i--) {
const e = this.enemies[i];
if (
Math.abs(e.x + e.w / 2 - p.x) < p.w / 2 + e.w / 2 - 8 &&
Math.abs(e.y + e.h / 2 - p.y) < p.h / 2 + e.h / 2 - 8
) {
if (!e.closing) {
this.closeTab(e.tabId);
e.closing = true;
}
this.spawnExplosion(e.x + e.w / 2, e.y + e.h / 2, "#ff4444", 14);
this.enemies.splice(i, 1);
this.damagePlayer();
}
}
// Power-ups falling
for (let i = this.powerups.length - 1; i >= 0; i--) {
const pu = this.powerups[i];
pu.y += pu.vy;
pu.rot += 0.08;
if (
Math.abs(pu.x - p.x) < p.w / 2 + 12 &&
Math.abs(pu.y - p.y) < p.h / 2 + 12
) {
this.applyPowerup(pu.type);
this.powerups.splice(i, 1);
continue;
}
if (pu.y > CANVAS_H + 20) this.powerups.splice(i, 1);
}
// Particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const pt = this.particles[i];
pt.x += pt.vx;
pt.y += pt.vy;
pt.vx *= 0.96;
pt.vy *= 0.96;
pt.life--;
if (pt.life <= 0) this.particles.splice(i, 1);
}
// Floating texts
for (let i = this.floatingTexts.length - 1; i >= 0; i--) {
const ft = this.floatingTexts[i];
ft.y -= 0.6;
ft.life--;
if (ft.life <= 0) this.floatingTexts.splice(i, 1);
}
// Win condition: all tabs destroyed & no enemies left & queue empty
if (
this.spawnIndex >= this.spawnQueue.length &&
this.enemies.length === 0
) {
this.gameOver(true);
}
}
// ---------- RENDER ----------
render() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
// Subtle background grid / nebula
this.renderBackground();
// Bullets (player)
for (const b of this.bullets) {
ctx.fillStyle = "#00f0ff";
ctx.shadowColor = "#00f0ff";
ctx.shadowBlur = 10;
ctx.fillRect(b.x - 2, b.y - 8, 4, 12);
}
ctx.shadowBlur = 0;
// Enemy bullets
for (const b of this.enemyBullets) {
ctx.fillStyle = "#ff3366";
ctx.shadowColor = "#ff3366";
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(b.x, b.y, 4, 0, Math.PI * 2);
ctx.fill();
}
ctx.shadowBlur = 0;
// Enemies (tabs)
for (const e of this.enemies) {
this.renderEnemy(e);
}
// Power-ups
for (const pu of this.powerups) {
this.renderPowerup(pu);
}
// Player ship
this.renderPlayer();
// Particles
for (const pt of this.particles) {
const alpha = pt.life / pt.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = pt.color;
ctx.fillRect(pt.x - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size);
}
ctx.globalAlpha = 1;
// Floating texts
for (const ft of this.floatingTexts) {
ctx.globalAlpha = clamp(ft.life / 60, 0, 1);
ctx.fillStyle = ft.color;
ctx.font = "bold 13px Segoe UI, Arial";
ctx.textAlign = "center";
ctx.fillText(ft.text, ft.x, ft.y);
}
ctx.globalAlpha = 1;
}
renderBackground() {
// Moving starfield
if (!this._stars) {
this._stars = [];
for (let i = 0; i < 50; i++) {
this._stars.push({
x: Math.random() * CANVAS_W,
y: Math.random() * CANVAS_H,
s: Math.random() * 1.5 + 0.3,
v: Math.random() * 0.8 + 0.2,
});
}
}
ctx.fillStyle = "#ffffff";
for (const st of this._stars) {
st.y += st.v;
if (st.y > CANVAS_H) st.y = 0;
ctx.globalAlpha = 0.3 + st.s * 0.3;
ctx.fillRect(st.x, st.y, st.s, st.s);
}
ctx.globalAlpha = 1;
}
renderEnemy(e) {
// Body
ctx.save();
ctx.translate(e.x, e.y);
// Glow when recently hit
if (e.hit > 0) {
ctx.shadowColor = "#ffffff";
ctx.shadowBlur = 15;
}
// Rounded rect "tab" body
const r = 8;
ctx.fillStyle = e.shooter ? "#3a1530" : "#1a1a3a";
ctx.strokeStyle = e.shooter ? "#ff3366" : "#444466";
ctx.lineWidth = 1.5;
roundRect(ctx, 0, 0, e.w, e.h, r);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Favicon
if (e.favicon) {
try {
ctx.drawImage(e.favicon, 6, 4, 16, 16);
} catch (_) {
drawLetterAvatar(ctx, e.title, 6, 4, 16);
}
} else {
drawLetterAvatar(ctx, e.title, 6, 4, 16);
}
// Title text (clipped)
ctx.fillStyle = "#ccccdd";
ctx.font = "9px Segoe UI, Arial";
ctx.textAlign = "left";
ctx.textBaseline = "top";
const title = e.title.length > 14 ? e.title.slice(0, 13) + "…" : e.title;
// Two-line layout: first line of title
ctx.fillText(title, 26, 6, e.w - 30);
// Shooter indicator
if (e.shooter) {
ctx.fillStyle = "#ff3366";
ctx.font = "8px Segoe UI, Arial";
ctx.fillText("● feindlich", 6, 26);
}
ctx.restore();
}
renderPlayer() {
const p = this.player;
ctx.save();
ctx.translate(p.x, p.y);
// Shield aura
if (this.shield > 0) {
ctx.strokeStyle = "rgba(0,255,136,0.6)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, p.w / 2 + 8, 0, Math.PI * 2);
ctx.stroke();
}
// Invincibility flicker
if (p.invincible > 0 && Math.floor(p.invincible / 4) % 2 === 0) {
ctx.globalAlpha = 0.4;
}
// Ship body — triangle with details
ctx.fillStyle = "#00f0ff";
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, -p.h / 2); // nose
ctx.lineTo(-p.w / 2, p.h / 2); // bottom-left
ctx.lineTo(-p.w / 4, p.h / 3);
ctx.lineTo(p.w / 4, p.h / 3);
ctx.lineTo(p.w / 2, p.h / 2); // bottom-right
ctx.closePath();
ctx.fill();
ctx.stroke();
// Cockpit
ctx.fillStyle = "#ff00e5";
ctx.beginPath();
ctx.arc(0, -2, 5, 0, Math.PI * 2);
ctx.fill();
// Engine glow
ctx.fillStyle = "#ffaa00";
ctx.globalAlpha = 0.7 * ctx.globalAlpha;
ctx.fillRect(-4, p.h / 3, 8, 6);
ctx.restore();
ctx.globalAlpha = 1;
}
renderPowerup(pu) {
ctx.save();
ctx.translate(pu.x, pu.y);
ctx.rotate(pu.rot);
const colors = {
triple: "#00f0ff",
rapid: "#ffaa00",
shield: "#00ff88",
};
const icons = { triple: "T", rapid: "R", shield: "S" };
const c = colors[pu.type];
ctx.fillStyle = c;
ctx.shadowColor = c;
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "#05060f";
ctx.font = "bold 12px Segoe UI, Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.rotate(-pu.rot); // keep icon upright
ctx.fillText(icons[pu.type], 0, 1);
ctx.restore();
}
// ---------- END ----------
gameOver(victory = false) {
this.stop();
const isNewRecord = this.score > highscore;
if (isNewRecord) {
saveHighscore(this.score);
}
els.resultTabs.textContent = this.tabsDestroyed;
els.resultScore.textContent = this.score;
els.resultHighscore.textContent = Math.max(highscore, this.score);
els.newRecord.classList.toggle("hidden", !isNewRecord);
showScreen("gameover");
}
}
// ---------- HELPERS ----------
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawLetterAvatar(ctx, title, x, y, size) {
const colors = ["#ff6b6b", "#4ecdc4", "#ffe66d", "#a8e6cf", "#c7b3ff", "#ffb3d9"];
const letter = (title || "?").trim().charAt(0).toUpperCase() || "?";
const color = colors[(title || "").length % colors.length];
ctx.fillStyle = color;
ctx.fillRect(x, y, size, size);
ctx.fillStyle = "#ffffff";
ctx.font = "bold 10px Segoe UI, Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(letter, x + size / 2, y + size / 2 + 1);
}
// ---------- HUD UPDATES ----------
function updateScoreHUD(score) {
els.hudScore.textContent = score;
}
function updateLivesHUD(lives) {
els.hudLives.textContent = "❤️".repeat(Math.max(0, lives));
}
function updateTabsHUD(remaining) {
els.hudTabs.textContent = remaining;
}
function updatePowerupHUD(game) {
let html = "";
if (game.shield > 0) html += "🛡️";
if (game.tripleShot > 0) html += "🔫";
if (game.rapidFire > 0) html += "⚡";
els.hudPowerups.innerHTML = html;
}
// ============================================================
// BOOTSTRAP / SCREEN WIRING
// ============================================================
async function startNewGame() {
const tabs = await loadTabs(selectedScope);
if (tabs.length === 0) {
showScreen("empty");
return;
}
showScreen("game");
// Reset HUD
updateScoreHUD(0);
updateLivesHUD(3);
updateTabsHUD(tabs.length);
els.hudPowerups.innerHTML = "";
game = new TabShooterGame(tabs);
await game.init();
game.start();
}
// Scope buttons
document.querySelectorAll(".scope-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".scope-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
selectedScope = btn.dataset.scope;
});
});
// Start button
document.getElementById("start-btn").addEventListener("click", startNewGame);
// Replay
document.getElementById("replay-btn").addEventListener("click", startNewGame);
// Back to menu
document.getElementById("menu-btn").addEventListener("click", () => {
if (game) game.stop();
loadHighscore();
showScreen("start");
});
document.getElementById("empty-menu-btn").addEventListener("click", () => {
loadHighscore();
showScreen("start");
});
// Init on load
loadHighscore();