Extract UI handling to dedicated module
This commit is contained in:
@@ -76,6 +76,7 @@
|
|||||||
|
|
||||||
<script src="https://unpkg.com/matter-js@0.19.0/build/matter.min.js"></script>
|
<script src="https://unpkg.com/matter-js@0.19.0/build/matter.min.js"></script>
|
||||||
<script src="./scenes.js"></script>
|
<script src="./scenes.js"></script>
|
||||||
|
<script src="./ui.js"></script>
|
||||||
<script src="./main.js"></script>
|
<script src="./main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
107
main.js
107
main.js
@@ -31,20 +31,8 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sceneEl = document.getElementById("scene-wrapper");
|
const ui = window.PhysilinksUI.create();
|
||||||
const activeColorEl = document.getElementById("active-color");
|
const { sceneEl } = ui;
|
||||||
const chainLenEl = document.getElementById("chain-length");
|
|
||||||
const spawnRateEl = document.getElementById("spawn-rate");
|
|
||||||
const minLinkEl = document.getElementById("min-link");
|
|
||||||
const paletteLegendEl = document.getElementById("palette-legend");
|
|
||||||
const scoreEl = document.getElementById("score");
|
|
||||||
const highScoreEl = document.getElementById("high-score");
|
|
||||||
const sceneSelectEl = document.getElementById("scene-select");
|
|
||||||
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 width = sceneEl.clientWidth;
|
||||||
let height = sceneEl.clientHeight;
|
let height = sceneEl.clientHeight;
|
||||||
@@ -130,21 +118,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateSceneSelect = () => {
|
|
||||||
if (!sceneSelectEl) return;
|
|
||||||
sceneSelectEl.innerHTML = "";
|
|
||||||
scenes.forEach((scene) => {
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = scene.id;
|
|
||||||
opt.textContent = scene.name;
|
|
||||||
sceneSelectEl.appendChild(opt);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyScene = (sceneId) => {
|
const applyScene = (sceneId) => {
|
||||||
const next = scenes.find((s) => s.id === sceneId) || scenes[0];
|
const next = scenes.find((s) => s.id === sceneId) || scenes[0];
|
||||||
currentScene = next;
|
currentScene = next;
|
||||||
if (sceneSelectEl) sceneSelectEl.value = next.id;
|
ui.setSceneSelection(next.id);
|
||||||
Object.assign(config, next.config);
|
Object.assign(config, next.config);
|
||||||
config.link = { ...next.config.link };
|
config.link = { ...next.config.link };
|
||||||
engine.gravity.y = config.gravity;
|
engine.gravity.y = config.gravity;
|
||||||
@@ -230,10 +207,8 @@
|
|||||||
stopSpawner();
|
stopSpawner();
|
||||||
stopRunner();
|
stopRunner();
|
||||||
engine.timing.timeScale = 0;
|
engine.timing.timeScale = 0;
|
||||||
pauseOverlay.classList.remove("visible");
|
ui.setPauseState(false);
|
||||||
pauseBtn.textContent = "Pause";
|
ui.showGameOver(score);
|
||||||
finalScoreEl.textContent = score;
|
|
||||||
gameOverEl.classList.add("visible");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const restartGame = () => {
|
const restartGame = () => {
|
||||||
@@ -246,9 +221,8 @@
|
|||||||
World.remove(world, ball);
|
World.remove(world, ball);
|
||||||
});
|
});
|
||||||
balls.length = 0;
|
balls.length = 0;
|
||||||
gameOverEl.classList.remove("visible");
|
ui.hideGameOver();
|
||||||
pauseOverlay.classList.remove("visible");
|
ui.setPauseState(false);
|
||||||
pauseBtn.textContent = "Pause";
|
|
||||||
engine.timing.timeScale = 1;
|
engine.timing.timeScale = 1;
|
||||||
startRunner();
|
startRunner();
|
||||||
updateHud();
|
updateHud();
|
||||||
@@ -264,8 +238,7 @@
|
|||||||
if (gameOver) return;
|
if (gameOver) return;
|
||||||
if (state === isPaused) return;
|
if (state === isPaused) return;
|
||||||
isPaused = state;
|
isPaused = state;
|
||||||
pauseBtn.textContent = isPaused ? "Resume" : "Pause";
|
ui.setPauseState(isPaused);
|
||||||
pauseOverlay.classList.toggle("visible", isPaused);
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
stopSpawner();
|
stopSpawner();
|
||||||
@@ -328,18 +301,6 @@
|
|||||||
updateHud();
|
updateHud();
|
||||||
};
|
};
|
||||||
|
|
||||||
const spawnScorePopup = (point, amount, color) => {
|
|
||||||
if (!point) return;
|
|
||||||
const el = document.createElement("div");
|
|
||||||
el.className = "floating-score";
|
|
||||||
el.textContent = `+${amount}`;
|
|
||||||
el.style.left = `${point.x}px`;
|
|
||||||
el.style.top = `${point.y}px`;
|
|
||||||
el.style.color = color || "#e0f2fe";
|
|
||||||
sceneEl.appendChild(el);
|
|
||||||
setTimeout(() => el.remove(), 950);
|
|
||||||
};
|
|
||||||
|
|
||||||
const finishChain = (releasePoint) => {
|
const finishChain = (releasePoint) => {
|
||||||
if (!chain.active || gameOver || isPaused) return;
|
if (!chain.active || gameOver || isPaused) return;
|
||||||
if (chain.bodies.length >= config.minChain) {
|
if (chain.bodies.length >= config.minChain) {
|
||||||
@@ -349,7 +310,7 @@
|
|||||||
highScore = score;
|
highScore = score;
|
||||||
saveHighScore();
|
saveHighScore();
|
||||||
}
|
}
|
||||||
spawnScorePopup(releasePoint || chain.pointer, gain, chain.color);
|
ui.spawnScorePopup(releasePoint || chain.pointer, gain, chain.color);
|
||||||
chain.constraints.forEach((c) => World.remove(world, c));
|
chain.constraints.forEach((c) => World.remove(world, c));
|
||||||
chain.bodies.forEach((body) => {
|
chain.bodies.forEach((body) => {
|
||||||
cleanupBall(body);
|
cleanupBall(body);
|
||||||
@@ -457,32 +418,18 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateHud = () => {
|
const updateHud = () => {
|
||||||
spawnRateEl.textContent = `${config.spawnIntervalMs} ms`;
|
ui.updateHud({
|
||||||
minLinkEl.textContent = config.minChain;
|
spawnIntervalMs: config.spawnIntervalMs,
|
||||||
chainLenEl.textContent = chain.bodies.length;
|
minChain: config.minChain,
|
||||||
scoreEl.textContent = score;
|
chainLength: chain.bodies.length,
|
||||||
highScoreEl.textContent = highScore;
|
score,
|
||||||
if (chain.color) {
|
highScore,
|
||||||
activeColorEl.textContent = "";
|
activeColor: chain.color,
|
||||||
activeColorEl.style.display = "inline-block";
|
});
|
||||||
activeColorEl.style.width = "14px";
|
|
||||||
activeColorEl.style.height = "14px";
|
|
||||||
activeColorEl.style.borderRadius = "50%";
|
|
||||||
activeColorEl.style.background = chain.color;
|
|
||||||
activeColorEl.style.border = "1px solid rgba(255,255,255,0.3)";
|
|
||||||
} else {
|
|
||||||
activeColorEl.removeAttribute("style");
|
|
||||||
activeColorEl.textContent = "—";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildLegend = () => {
|
const buildLegend = () => {
|
||||||
paletteLegendEl.innerHTML = "";
|
ui.buildLegend(config.palette);
|
||||||
config.palette.forEach((color) => {
|
|
||||||
const swatch = document.createElement("span");
|
|
||||||
swatch.style.background = color;
|
|
||||||
paletteLegendEl.appendChild(swatch);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -581,16 +528,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
restartBtn.addEventListener("click", restartGame);
|
ui.setHandlers({
|
||||||
pauseBtn.addEventListener("click", () => setPaused(!isPaused));
|
onPauseToggle: () => setPaused(!isPaused),
|
||||||
window.addEventListener("keydown", (e) => {
|
onRestart: restartGame,
|
||||||
if (e.key === "Escape") {
|
onSceneChange: (id) => applyScene(id),
|
||||||
setPaused(!isPaused);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (sceneSelectEl) {
|
ui.setSceneOptions(
|
||||||
sceneSelectEl.addEventListener("change", (e) => applyScene(e.target.value));
|
scenes,
|
||||||
}
|
(currentScene && currentScene.id) || defaultSceneId,
|
||||||
populateSceneSelect();
|
);
|
||||||
applyScene((currentScene && currentScene.id) || defaultSceneId);
|
applyScene((currentScene && currentScene.id) || defaultSceneId);
|
||||||
})();
|
})();
|
||||||
|
|||||||
156
ui.js
Normal file
156
ui.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
(() => {
|
||||||
|
const create = () => {
|
||||||
|
const sceneEl = document.getElementById("scene-wrapper");
|
||||||
|
const activeColorEl = document.getElementById("active-color");
|
||||||
|
const chainLenEl = document.getElementById("chain-length");
|
||||||
|
const spawnRateEl = document.getElementById("spawn-rate");
|
||||||
|
const minLinkEl = document.getElementById("min-link");
|
||||||
|
const paletteLegendEl = document.getElementById("palette-legend");
|
||||||
|
const scoreEl = document.getElementById("score");
|
||||||
|
const highScoreEl = document.getElementById("high-score");
|
||||||
|
const sceneSelectEl = document.getElementById("scene-select");
|
||||||
|
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");
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
onPauseToggle: null,
|
||||||
|
onRestart: null,
|
||||||
|
onSceneChange: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pauseBtn) {
|
||||||
|
pauseBtn.addEventListener("click", () => {
|
||||||
|
if (handlers.onPauseToggle) handlers.onPauseToggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restartBtn) {
|
||||||
|
restartBtn.addEventListener("click", () => {
|
||||||
|
if (handlers.onRestart) handlers.onRestart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneSelectEl) {
|
||||||
|
sceneSelectEl.addEventListener("change", (e) => {
|
||||||
|
if (handlers.onSceneChange) handlers.onSceneChange(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && handlers.onPauseToggle) {
|
||||||
|
handlers.onPauseToggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setHandlers = (nextHandlers = {}) => {
|
||||||
|
Object.assign(handlers, nextHandlers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSceneOptions = (scenes, activeId) => {
|
||||||
|
if (!sceneSelectEl) return;
|
||||||
|
sceneSelectEl.innerHTML = "";
|
||||||
|
scenes.forEach((scene) => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = scene.id;
|
||||||
|
opt.textContent = scene.name;
|
||||||
|
sceneSelectEl.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (activeId) {
|
||||||
|
sceneSelectEl.value = activeId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSceneSelection = (sceneId) => {
|
||||||
|
if (sceneSelectEl) {
|
||||||
|
sceneSelectEl.value = sceneId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPauseState = (paused) => {
|
||||||
|
if (pauseOverlay) {
|
||||||
|
pauseOverlay.classList.toggle("visible", paused);
|
||||||
|
}
|
||||||
|
if (pauseBtn) {
|
||||||
|
pauseBtn.textContent = paused ? "Resume" : "Pause";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showGameOver = (score) => {
|
||||||
|
if (finalScoreEl) finalScoreEl.textContent = score;
|
||||||
|
if (gameOverEl) gameOverEl.classList.add("visible");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideGameOver = () => {
|
||||||
|
if (gameOverEl) gameOverEl.classList.remove("visible");
|
||||||
|
};
|
||||||
|
|
||||||
|
const spawnScorePopup = (point, amount, color) => {
|
||||||
|
if (!point || !sceneEl) return;
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "floating-score";
|
||||||
|
el.textContent = `+${amount}`;
|
||||||
|
el.style.left = `${point.x}px`;
|
||||||
|
el.style.top = `${point.y}px`;
|
||||||
|
el.style.color = color || "#e0f2fe";
|
||||||
|
sceneEl.appendChild(el);
|
||||||
|
setTimeout(() => el.remove(), 950);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHud = ({
|
||||||
|
spawnIntervalMs,
|
||||||
|
minChain,
|
||||||
|
chainLength,
|
||||||
|
score,
|
||||||
|
highScore,
|
||||||
|
activeColor,
|
||||||
|
}) => {
|
||||||
|
if (spawnRateEl) spawnRateEl.textContent = `${spawnIntervalMs} ms`;
|
||||||
|
if (minLinkEl) minLinkEl.textContent = minChain;
|
||||||
|
if (chainLenEl) chainLenEl.textContent = chainLength;
|
||||||
|
if (scoreEl) scoreEl.textContent = score;
|
||||||
|
if (highScoreEl) highScoreEl.textContent = highScore;
|
||||||
|
if (activeColorEl) {
|
||||||
|
if (activeColor) {
|
||||||
|
activeColorEl.textContent = "";
|
||||||
|
activeColorEl.style.display = "inline-block";
|
||||||
|
activeColorEl.style.width = "14px";
|
||||||
|
activeColorEl.style.height = "14px";
|
||||||
|
activeColorEl.style.borderRadius = "50%";
|
||||||
|
activeColorEl.style.background = activeColor;
|
||||||
|
activeColorEl.style.border = "1px solid rgba(255,255,255,0.3)";
|
||||||
|
} else {
|
||||||
|
activeColorEl.removeAttribute("style");
|
||||||
|
activeColorEl.textContent = "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLegend = (palette) => {
|
||||||
|
if (!paletteLegendEl) return;
|
||||||
|
paletteLegendEl.innerHTML = "";
|
||||||
|
palette.forEach((color) => {
|
||||||
|
const swatch = document.createElement("span");
|
||||||
|
swatch.style.background = color;
|
||||||
|
paletteLegendEl.appendChild(swatch);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sceneEl,
|
||||||
|
updateHud,
|
||||||
|
buildLegend,
|
||||||
|
spawnScorePopup,
|
||||||
|
setPauseState,
|
||||||
|
showGameOver,
|
||||||
|
hideGameOver,
|
||||||
|
setSceneOptions,
|
||||||
|
setSceneSelection,
|
||||||
|
setHandlers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.PhysilinksUI = { create };
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user