diff --git a/index.html b/index.html
index cd8ca7b..dfc1e46 100644
--- a/index.html
+++ b/index.html
@@ -82,6 +82,25 @@
font-size: 13px;
color: var(--muted);
}
+ .pause-btn {
+ background: rgba(34, 211, 238, 0.14);
+ color: #67e8f9;
+ border: 1px solid rgba(34, 211, 238, 0.4);
+ border-radius: 10px;
+ padding: 8px 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition:
+ transform 120ms ease,
+ filter 120ms ease;
+ }
+ .pause-btn:hover {
+ filter: brightness(1.05);
+ transform: translateY(-1px);
+ }
+ .pause-btn:active {
+ transform: translateY(0);
+ }
header .pill {
padding: 6px 10px;
border-radius: 8px;
@@ -159,6 +178,25 @@
border: 1px solid rgba(255, 255, 255, 0.12);
display: inline-block;
}
+ .pause-overlay {
+ position: absolute;
+ top: 12px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ color: #e2e8f0;
+ padding: 8px 14px;
+ border-radius: 12px;
+ font-weight: 800;
+ letter-spacing: 0.5px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 160ms ease;
+ }
+ .pause-overlay.visible {
+ opacity: 1;
+ }
.game-over {
position: absolute;
inset: 0;
@@ -256,9 +294,11 @@
Physics
Link same-colored balls to clear them.
+ Pause
+
Paused
Game Over
@@ -295,7 +335,7 @@
than the minimum vanish; chains with enough balls clear all
linked balls (score: 10 × length²). If the entry gets blocked
and a new ball cannot drop in, the run ends—restart to try
- again.
+ again. Pause/resume with the Pause button or Escape key.
diff --git a/main.js b/main.js
index 67e796b..a3e33f3 100644
--- a/main.js
+++ b/main.js
@@ -29,6 +29,8 @@
const gameOverEl = document.getElementById("game-over");
const finalScoreEl = document.getElementById("final-score");
const restartBtn = document.getElementById("restart-btn");
+ const pauseBtn = document.getElementById("pause-btn");
+ const pauseOverlay = document.getElementById("pause-overlay");
let width = sceneEl.clientWidth;
let height = sceneEl.clientHeight;
@@ -86,6 +88,7 @@
let spawnTimer = null;
let score = 0;
let gameOver = false;
+ let isPaused = false;
const chain = {
active: false,
@@ -131,6 +134,13 @@
spawnTimer = setInterval(spawnBall, config.spawnIntervalMs);
};
+ const stopSpawner = () => {
+ if (spawnTimer) {
+ clearInterval(spawnTimer);
+ spawnTimer = null;
+ }
+ };
+
const cleanupBall = (ball) => {
if (ball.plugin && ball.plugin.entryCheckId) {
clearTimeout(ball.plugin.entryCheckId);
@@ -141,14 +151,19 @@
const triggerGameOver = () => {
if (gameOver) return;
gameOver = true;
+ isPaused = false;
resetChainVisuals();
- if (spawnTimer) clearInterval(spawnTimer);
+ stopSpawner();
+ Runner.stop(runner);
+ pauseOverlay.classList.remove("visible");
+ pauseBtn.textContent = "Pause";
finalScoreEl.textContent = score;
gameOverEl.classList.add("visible");
};
const restartGame = () => {
gameOver = false;
+ isPaused = false;
score = 0;
resetChainVisuals();
balls.forEach((ball) => {
@@ -157,7 +172,10 @@
});
balls.length = 0;
gameOverEl.classList.remove("visible");
+ pauseOverlay.classList.remove("visible");
+ pauseBtn.textContent = "Pause";
updateHud();
+ Runner.run(runner, engine);
startSpawner();
};
@@ -166,6 +184,22 @@
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
};
+ const setPaused = (state) => {
+ if (gameOver) return;
+ if (state === isPaused) return;
+ isPaused = state;
+ pauseBtn.textContent = isPaused ? "Resume" : "Pause";
+ pauseOverlay.classList.toggle("visible", isPaused);
+ if (isPaused) {
+ resetChainVisuals();
+ stopSpawner();
+ Runner.stop(runner);
+ } else {
+ startSpawner();
+ Runner.run(runner, engine);
+ }
+ };
+
const resetChainVisuals = () => {
chain.bodies.forEach((b) => setHighlight(b, false));
chain.constraints.forEach((c) => World.remove(world, c));
@@ -220,7 +254,7 @@
};
const finishChain = (releasePoint) => {
- if (!chain.active || gameOver) return;
+ if (!chain.active || gameOver || isPaused) return;
if (chain.bodies.length >= config.minChain) {
const gain = 10 * Math.pow(chain.bodies.length, 2);
score += gain;
@@ -264,7 +298,7 @@
};
const handlePointerDown = (evt) => {
- if (gameOver) return;
+ if (gameOver || isPaused) return;
const point = getPointerPosition(evt);
const body = pickBody(point);
if (!body) return;
@@ -279,7 +313,7 @@
const handlePointerMove = (evt) => {
if (!chain.active) return;
- if (gameOver) return;
+ if (gameOver || isPaused) return;
const point = getPointerPosition(evt);
chain.pointer = point;
const body = pickBody(point);
@@ -407,6 +441,12 @@
window.addEventListener("resize", handleResize);
restartBtn.addEventListener("click", restartGame);
+ pauseBtn.addEventListener("click", () => setPaused(!isPaused));
+ window.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ setPaused(!isPaused);
+ }
+ });
buildLegend();
updateHud();
startSpawner();