Add pause/resume controls and score popup
This commit is contained in:
42
index.html
42
index.html
@@ -82,6 +82,25 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--muted);
|
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 {
|
header .pill {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -159,6 +178,25 @@
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
display: inline-block;
|
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 {
|
.game-over {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -256,9 +294,11 @@
|
|||||||
<span class="pill">Physics</span>
|
<span class="pill">Physics</span>
|
||||||
<span>Link same-colored balls to clear them.</span>
|
<span>Link same-colored balls to clear them.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="pause-btn" id="pause-btn">Pause</button>
|
||||||
</header>
|
</header>
|
||||||
<div id="scene-wrapper">
|
<div id="scene-wrapper">
|
||||||
<div class="legend" id="palette-legend"></div>
|
<div class="legend" id="palette-legend"></div>
|
||||||
|
<div class="pause-overlay" id="pause-overlay">Paused</div>
|
||||||
<div class="game-over" id="game-over">
|
<div class="game-over" id="game-over">
|
||||||
<div class="game-over__card">
|
<div class="game-over__card">
|
||||||
<div class="title">Game Over</div>
|
<div class="title">Game Over</div>
|
||||||
@@ -295,7 +335,7 @@
|
|||||||
than the minimum vanish; chains with enough balls clear all
|
than the minimum vanish; chains with enough balls clear all
|
||||||
linked balls (score: 10 × length²). If the entry gets blocked
|
linked balls (score: 10 × length²). If the entry gets blocked
|
||||||
and a new ball cannot drop in, the run ends—restart to try
|
and a new ball cannot drop in, the run ends—restart to try
|
||||||
again.
|
again. Pause/resume with the Pause button or Escape key.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
48
main.js
48
main.js
@@ -29,6 +29,8 @@
|
|||||||
const gameOverEl = document.getElementById("game-over");
|
const gameOverEl = document.getElementById("game-over");
|
||||||
const finalScoreEl = document.getElementById("final-score");
|
const finalScoreEl = document.getElementById("final-score");
|
||||||
const restartBtn = document.getElementById("restart-btn");
|
const restartBtn = document.getElementById("restart-btn");
|
||||||
|
const pauseBtn = document.getElementById("pause-btn");
|
||||||
|
const pauseOverlay = document.getElementById("pause-overlay");
|
||||||
|
|
||||||
let width = sceneEl.clientWidth;
|
let width = sceneEl.clientWidth;
|
||||||
let height = sceneEl.clientHeight;
|
let height = sceneEl.clientHeight;
|
||||||
@@ -86,6 +88,7 @@
|
|||||||
let spawnTimer = null;
|
let spawnTimer = null;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let gameOver = false;
|
let gameOver = false;
|
||||||
|
let isPaused = false;
|
||||||
|
|
||||||
const chain = {
|
const chain = {
|
||||||
active: false,
|
active: false,
|
||||||
@@ -131,6 +134,13 @@
|
|||||||
spawnTimer = setInterval(spawnBall, config.spawnIntervalMs);
|
spawnTimer = setInterval(spawnBall, config.spawnIntervalMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopSpawner = () => {
|
||||||
|
if (spawnTimer) {
|
||||||
|
clearInterval(spawnTimer);
|
||||||
|
spawnTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cleanupBall = (ball) => {
|
const cleanupBall = (ball) => {
|
||||||
if (ball.plugin && ball.plugin.entryCheckId) {
|
if (ball.plugin && ball.plugin.entryCheckId) {
|
||||||
clearTimeout(ball.plugin.entryCheckId);
|
clearTimeout(ball.plugin.entryCheckId);
|
||||||
@@ -141,14 +151,19 @@
|
|||||||
const triggerGameOver = () => {
|
const triggerGameOver = () => {
|
||||||
if (gameOver) return;
|
if (gameOver) return;
|
||||||
gameOver = true;
|
gameOver = true;
|
||||||
|
isPaused = false;
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
if (spawnTimer) clearInterval(spawnTimer);
|
stopSpawner();
|
||||||
|
Runner.stop(runner);
|
||||||
|
pauseOverlay.classList.remove("visible");
|
||||||
|
pauseBtn.textContent = "Pause";
|
||||||
finalScoreEl.textContent = score;
|
finalScoreEl.textContent = score;
|
||||||
gameOverEl.classList.add("visible");
|
gameOverEl.classList.add("visible");
|
||||||
};
|
};
|
||||||
|
|
||||||
const restartGame = () => {
|
const restartGame = () => {
|
||||||
gameOver = false;
|
gameOver = false;
|
||||||
|
isPaused = false;
|
||||||
score = 0;
|
score = 0;
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
balls.forEach((ball) => {
|
balls.forEach((ball) => {
|
||||||
@@ -157,7 +172,10 @@
|
|||||||
});
|
});
|
||||||
balls.length = 0;
|
balls.length = 0;
|
||||||
gameOverEl.classList.remove("visible");
|
gameOverEl.classList.remove("visible");
|
||||||
|
pauseOverlay.classList.remove("visible");
|
||||||
|
pauseBtn.textContent = "Pause";
|
||||||
updateHud();
|
updateHud();
|
||||||
|
Runner.run(runner, engine);
|
||||||
startSpawner();
|
startSpawner();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,6 +184,22 @@
|
|||||||
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
|
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 = () => {
|
const resetChainVisuals = () => {
|
||||||
chain.bodies.forEach((b) => setHighlight(b, false));
|
chain.bodies.forEach((b) => setHighlight(b, false));
|
||||||
chain.constraints.forEach((c) => World.remove(world, c));
|
chain.constraints.forEach((c) => World.remove(world, c));
|
||||||
@@ -220,7 +254,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const finishChain = (releasePoint) => {
|
const finishChain = (releasePoint) => {
|
||||||
if (!chain.active || gameOver) return;
|
if (!chain.active || gameOver || isPaused) return;
|
||||||
if (chain.bodies.length >= config.minChain) {
|
if (chain.bodies.length >= config.minChain) {
|
||||||
const gain = 10 * Math.pow(chain.bodies.length, 2);
|
const gain = 10 * Math.pow(chain.bodies.length, 2);
|
||||||
score += gain;
|
score += gain;
|
||||||
@@ -264,7 +298,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = (evt) => {
|
const handlePointerDown = (evt) => {
|
||||||
if (gameOver) return;
|
if (gameOver || isPaused) return;
|
||||||
const point = getPointerPosition(evt);
|
const point = getPointerPosition(evt);
|
||||||
const body = pickBody(point);
|
const body = pickBody(point);
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
@@ -279,7 +313,7 @@
|
|||||||
|
|
||||||
const handlePointerMove = (evt) => {
|
const handlePointerMove = (evt) => {
|
||||||
if (!chain.active) return;
|
if (!chain.active) return;
|
||||||
if (gameOver) return;
|
if (gameOver || isPaused) return;
|
||||||
const point = getPointerPosition(evt);
|
const point = getPointerPosition(evt);
|
||||||
chain.pointer = point;
|
chain.pointer = point;
|
||||||
const body = pickBody(point);
|
const body = pickBody(point);
|
||||||
@@ -407,6 +441,12 @@
|
|||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
restartBtn.addEventListener("click", restartGame);
|
restartBtn.addEventListener("click", restartGame);
|
||||||
|
pauseBtn.addEventListener("click", () => setPaused(!isPaused));
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setPaused(!isPaused);
|
||||||
|
}
|
||||||
|
});
|
||||||
buildLegend();
|
buildLegend();
|
||||||
updateHud();
|
updateHud();
|
||||||
startSpawner();
|
startSpawner();
|
||||||
|
|||||||
Reference in New Issue
Block a user