Store high scores per scene and add README
This commit is contained in:
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Physilinks
|
||||||
|
|
||||||
|
Physilinks is a browser-based physics linking game built with Matter.js. Match and chain same-colored falling balls; link enough to clear them and rack up points.
|
||||||
|
|
||||||
|
## Play instructions
|
||||||
|
- Open `index.html` in a modern browser (desktop or mobile touch).
|
||||||
|
- Choose a scene from the selector (changes gravity, obstacles, spawn rate, palette, ball size). Switching scenes restarts the run.
|
||||||
|
- Click/touch a ball to start a chain; drag through balls of the same color to add them. Drag back to the previous ball to undo the last link.
|
||||||
|
- Release: if the chain length is below the minimum, links vanish; if it meets/exceeds the minimum, linked balls are cleared and you score `10 × length²` (popup shows the gain).
|
||||||
|
- The top is open; if a new ball cannot enter because the entry is blocked, the run ends. The overlay shows final score with a restart button.
|
||||||
|
- Pause/resume with the button or `Esc`. HUD shows spawn rate, min link, chain length, score, per-scene high score, and palette legend.
|
||||||
|
|
||||||
|
## Tech notes
|
||||||
|
- **Engine**: Matter.js (via CDN). Canvas rendering with custom overlays for HUD, pause, game over, and score popups.
|
||||||
|
- **Scenes**: Defined in `main.js` as an array of presets (config + `createBodies` factory). Applying a scene updates gravity, spawn rate, ball radius, palette, and static bodies, then restarts the game.
|
||||||
|
- **Physics entities**: Falling balls (`Bodies.circle`) with gentle restitution/friction; static boundaries/obstacles per scene. The top is open; sides and floor are static bodies.
|
||||||
|
- **Input**: Pointer/touch events mapped to scene coords; chain state tracks bodies and a dashed preview line to the pointer. Undo by dragging back to the previous node.
|
||||||
|
- **Scoring**: `10 × length²` per cleared chain. Score popup rendered as DOM element near release point.
|
||||||
|
- **Persistence**: Per-scene high score stored in `localStorage` under `physilinks-highscore-<sceneId>`; loaded on scene change; HUD shows current scene's best.
|
||||||
|
- **Game loop**: Single Matter runner started once. Pause sets `engine.timing.timeScale = 0` and stops spawning; resume resets time scale and spawner. Game over stops spawner, freezes physics, and shows overlay.
|
||||||
|
- **Lose detection**: Spawned balls monitor entry; if they remain near the spawn zone with negligible velocity after a short delay, the run is over.
|
||||||
|
|
||||||
|
## Development quick start
|
||||||
|
- No build step. Open `index.html` directly in the browser.
|
||||||
|
- Key files: `index.html` (layout/styles), `main.js` (game logic).
|
||||||
|
- Adjust or add scenes in `main.js` by extending the `scenes` array with config and a `createBodies(width, height)` function.
|
||||||
12
main.js
12
main.js
@@ -24,7 +24,7 @@
|
|||||||
id: "scene1",
|
id: "scene1",
|
||||||
name: "Balanced (default)",
|
name: "Balanced (default)",
|
||||||
config: {
|
config: {
|
||||||
gravity: 0.78,
|
gravity: 0.88,
|
||||||
spawnIntervalMs: 720,
|
spawnIntervalMs: 720,
|
||||||
minChain: 3,
|
minChain: 3,
|
||||||
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
|
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
|
||||||
@@ -175,11 +175,11 @@
|
|||||||
let gameOver = false;
|
let gameOver = false;
|
||||||
let isPaused = false;
|
let isPaused = false;
|
||||||
|
|
||||||
const STORAGE_KEY = "physilinks-highscore";
|
const makeStorageKey = (sceneId) => `physilinks-highscore-${sceneId}`;
|
||||||
|
|
||||||
const loadHighScore = () => {
|
const loadHighScore = (sceneId) => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(makeStorageKey(sceneId));
|
||||||
const parsed = parseInt(raw, 10);
|
const parsed = parseInt(raw, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : 0;
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
|
|
||||||
const saveHighScore = () => {
|
const saveHighScore = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, String(highScore));
|
localStorage.setItem(makeStorageKey(currentScene.id), String(highScore));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore write failures (private mode or blocked storage)
|
// ignore write failures (private mode or blocked storage)
|
||||||
}
|
}
|
||||||
@@ -212,6 +212,7 @@
|
|||||||
if (sceneSelectEl) sceneSelectEl.value = next.id;
|
if (sceneSelectEl) sceneSelectEl.value = next.id;
|
||||||
Object.assign(config, next.config);
|
Object.assign(config, next.config);
|
||||||
engine.gravity.y = config.gravity;
|
engine.gravity.y = config.gravity;
|
||||||
|
highScore = loadHighScore(next.id);
|
||||||
rebuildSceneBodies();
|
rebuildSceneBodies();
|
||||||
buildLegend();
|
buildLegend();
|
||||||
restartGame();
|
restartGame();
|
||||||
@@ -583,7 +584,6 @@
|
|||||||
if (sceneSelectEl) {
|
if (sceneSelectEl) {
|
||||||
sceneSelectEl.addEventListener("change", (e) => applyScene(e.target.value));
|
sceneSelectEl.addEventListener("change", (e) => applyScene(e.target.value));
|
||||||
}
|
}
|
||||||
highScore = loadHighScore();
|
|
||||||
populateSceneSelect();
|
populateSceneSelect();
|
||||||
applyScene(currentScene.id);
|
applyScene(currentScene.id);
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user