GAME OVER
Score: ${score}
High Score: ${highScore}
Press Enter / A / Tap to Restart
`;
overlay.style.display = "flex";
resetGameState();
}
/* =========================
INPUT — KEYBOARD
========================= */
window.addEventListener("keydown", e => {
if (!started) startGame();
if (e.key >= "0" && e.key <= "9") appendDigit(e.key);
if (e.key === "Backspace") backspace();
if (e.key === "Enter") submitAnswer();
});
/* =========================
INPUT — TOUCH / MOBILE
========================= */
overlay.addEventListener("click", startGame);
input.addEventListener("input", () => {
buffer = input.value.replace(/\D/g, "");
});
/* =========================
INPUT — GAMEPAD
========================= */
function pollGamepad() {
const pads = navigator.getGamepads();
if (!pads) return;
for (const p of pads) {
if (!p) continue;
const press = (i) => {
const v = p.buttons[i]?.pressed;
const w = prevButtons[i] || false;
prevButtons[i] = v;
return v && !w;
};
if (press(1)) backspace(); // B
if (press(9)) submitAnswer(); // Start
if (press(12)) padIndex = (padIndex + 9) % 12; // up
if (press(13)) padIndex = (padIndex + 3) % 12; // down
if (press(14)) padIndex = (padIndex + 11) % 12; // left
if (press(15)) padIndex = (padIndex + 1) % 12; // right
if (press(0)) handlePadInput(numpadLayout[padIndex]);
// X = submit answer (arcade-friendly)
if (press(2)) {
submitAnswer();
}
// B = backspace
if (press(1)) {
backspace();
}
updateNumpadHighlight();
}
}
/* =========================
UPDATE
========================= */
function update(dt) {
pollGamepad();
lastSpawn += dt;
if (lastSpawn > Math.max(1600 - level * 80, 700)) {
spawnBomb();
lastSpawn = 0;
}
bombs.forEach(b => b.x -= dt * b.speed);
bombs = bombs.filter(b => {
if (b.x < 20) {
leaked++;
if (leaked >= 3) gameOver();
return false;
}
return true;
});
}
/* =========================
DRAW
========================= */
function draw() {
ctx.clearRect(0,0,LOGICAL_WIDTH,LOGICAL_HEIGHT);
ctx.fillStyle = "#0f0";
ctx.font = "14px monospace";
ctx.textAlign = "center";
const hudY = 16;
ctx.fillText(`Level ${level}`, LOGICAL_WIDTH * 0.25, hudY);
ctx.fillText(`Score ${score}`, LOGICAL_WIDTH * 0.50, hudY);
ctx.fillText(`Leaks ${leaked}/3`, LOGICAL_WIDTH * 0.75, hudY);
ctx.textAlign = "center"; // reset for rest of draw
ctx.font = "16px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
bombs.forEach(b => {
const danger = 1 - (b.x / LOGICAL_WIDTH);
const r = Math.floor(100 + 155 * danger);
ctx.fillStyle = `rgb(${r}, 0, 0)`;
ctx.strokeStyle = "#ff0000";
ctx.beginPath();
ctx.arc(b.x, b.y, BOMB_RADIUS, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#04ff00";
ctx.font = "16px monospace";
ctx.textBaseline = "middle";
/*
Right-aligned math column
*/
const colX = b.x + 12;
ctx.textAlign = "right";
ctx.fillText(String(b.top), colX, b.y - 10);
ctx.fillText(String(b.bottom), colX, b.y + 10);
/*
Operator sits to the left of the bottom number
*/
ctx.textAlign = "center";
ctx.fillText(b.op, colX - 18, b.y + 10);
/*
Underline
*/
ctx.beginPath();
ctx.moveTo(colX - 22, b.y + 18);
ctx.lineTo(colX + 2, b.y + 18);
ctx.strokeStyle = "#04ff00";
ctx.stroke();
});
}
/* =========================
LOOP
========================= */
function loop(t) {
if (!started) return;
const dt = t - lastTime;
lastTime = t;
update(dt);
draw();
requestAnimationFrame(loop);
}