feat: add fullscreen mode and extract shared game engine

- Add new fullscreen game mode playable in a dedicated browser tab
- Extract shared game logic into `engine.js` to be used by both popup and fullscreen modes
- Implement self-protection to prevent closing the game tab or popup tab
- Update documentation and bump version to 1.1.0
This commit is contained in:
Janik Dietz 2026-08-04 08:02:53 +02:00
parent 0f566b02ac
commit 25fc6fb345
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
8 changed files with 1064 additions and 798 deletions

127
game.js Normal file
View file

@ -0,0 +1,127 @@
/* ============================================================
TAB SHOOTER Vollbild-Controller (game.html)
Läuft als eigener Tab. Dieser Tab wird über seine ID
von der Zielliste ausgeschlossen -> er lässt sich NICHT abschießen.
============================================================ */
import { TabShooterGame, loadTabs } from "./engine.js";
// ---------- DOM ----------
const canvas = document.getElementById("game-canvas");
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"),
};
// ---------- STATE ----------
let game = null;
let highscore = 0;
let selectedScope = "current";
let myTabId = null;
// Eigene Tab-ID herausfinden damit wir sie aus der Zielliste streichen können.
(async () => {
try {
const [tab] = await chrome.tabs.getCurrent();
if (tab) myTabId = tab.id;
} catch (_) {
// getCurrent() schlägt fehlt, wenn wir gar kein Tab sind (sollte nicht passieren)
}
})();
// ---------- SCREEN HELPERS ----------
function showScreen(name) {
Object.values(screens).forEach((s) => s.classList.remove("visible"));
screens[name].classList.add("visible");
}
// ---------- 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;
}
// ---------- HUD CALLBACKS ----------
const hudCallbacks = {
onScore: (s) => (els.hudScore.textContent = s),
onLives: (l) => (els.hudLives.textContent = "❤️".repeat(Math.max(0, l))),
onTabs: (t) => (els.hudTabs.textContent = t),
onPowerups: (g) => {
let html = "";
if (g.shield > 0) html += "🛡️";
if (g.tripleShot > 0) html += "🔫";
if (g.rapidFire > 0) html += "⚡";
els.hudPowerups.innerHTML = html;
},
onGameOver: ({ score }) => {
const isNewRecord = score > highscore;
if (isNewRecord) saveHighscore(score);
els.resultTabs.textContent = game.tabsDestroyed;
els.resultScore.textContent = score;
els.resultHighscore.textContent = Math.max(highscore, score);
els.newRecord.classList.toggle("hidden", !isNewRecord);
showScreen("gameover");
},
};
// ---------- GAME START ----------
async function startNewGame() {
// WICHTIG: myTabId wird ausgeschlossen, damit dieser Tab kein Ziel ist.
const tabs = await loadTabs(selectedScope, myTabId);
if (tabs.length === 0) {
showScreen("empty");
return;
}
showScreen("game");
els.hudScore.textContent = "0";
els.hudLives.textContent = "❤️❤️❤️";
els.hudTabs.textContent = tabs.length;
els.hudPowerups.innerHTML = "";
game = new TabShooterGame(canvas, {}, hudCallbacks);
await game.init(tabs);
game.start();
}
// ---------- EVENT WIRING ----------
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;
});
});
document.getElementById("start-btn").addEventListener("click", startNewGame);
document.getElementById("replay-btn").addEventListener("click", startNewGame);
document.getElementById("menu-btn").addEventListener("click", () => {
if (game) game.stop();
loadHighscore();
showScreen("start");
});
document.getElementById("empty-menu-btn").addEventListener("click", () => {
loadHighscore();
showScreen("start");
});
// ---------- INIT ----------
loadHighscore();