diff --git a/index.html b/index.html
index 2b4da7d..3968c12 100644
--- a/index.html
+++ b/index.html
@@ -157,6 +157,14 @@
color: var(--text);
font-size: 13px;
}
+ .selector {
+ background: rgba(0, 0, 0, 0.25);
+ color: var(--text);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ border-radius: 8px;
+ padding: 6px 8px;
+ font-size: 13px;
+ }
.legend {
position: absolute;
top: 16px;
@@ -330,6 +338,10 @@
High score 0
+
+ Scene
+
+
Click or touch a ball to start a chain. Drag through balls of
@@ -338,7 +350,9 @@
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. Pause/resume with the Pause button or Escape key.
+ again. Pause/resume with the Pause button or Escape key. Switch
+ scenes with the selector to try different layouts/configs
+ (resets the run).
diff --git a/main.js b/main.js
index bb789d0..22231f7 100644
--- a/main.js
+++ b/main.js
@@ -12,13 +12,113 @@
} = Matter;
const config = {
- gravity: 0.78,
- spawnIntervalMs: 720,
+ gravity: 1,
+ spawnIntervalMs: 520,
minChain: 3,
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
- ballRadius: 48,
+ ballRadius: 18,
};
+ const scenes = [
+ {
+ id: "scene1",
+ name: "Balanced (default)",
+ config: {
+ gravity: 1,
+ spawnIntervalMs: 520,
+ minChain: 3,
+ palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
+ ballRadius: 18,
+ },
+ createBodies: (w, h) => [
+ Bodies.rectangle(w / 2, h + 40, w, 80, {
+ isStatic: true,
+ restitution: 0.8,
+ }),
+ Bodies.rectangle(-40, h / 2, 80, h * 2, { isStatic: true }),
+ Bodies.rectangle(w + 40, h / 2, 80, h * 2, { isStatic: true }),
+ Bodies.rectangle(w * 0.25, h * 0.55, 160, 20, {
+ isStatic: true,
+ angle: -0.3,
+ }),
+ Bodies.rectangle(w * 0.7, h * 0.4, 220, 24, {
+ isStatic: true,
+ angle: 0.26,
+ }),
+ ],
+ },
+ {
+ id: "scene2",
+ name: "Low-G terraces",
+ config: {
+ gravity: 0.65,
+ spawnIntervalMs: 600,
+ minChain: 3,
+ palette: ["#fb7185", "#fbbf24", "#34d399", "#38bdf8"],
+ ballRadius: 22,
+ },
+ createBodies: (w, h) => [
+ Bodies.rectangle(w / 2, h + 50, w, 100, {
+ isStatic: true,
+ restitution: 0.9,
+ }),
+ Bodies.rectangle(-50, h / 2, 100, h * 2, { isStatic: true }),
+ Bodies.rectangle(w + 50, h / 2, 100, h * 2, { isStatic: true }),
+ Bodies.rectangle(w * 0.2, h * 0.45, 200, 18, {
+ isStatic: true,
+ angle: 0.08,
+ }),
+ Bodies.rectangle(w * 0.5, h * 0.6, 260, 18, {
+ isStatic: true,
+ angle: -0.04,
+ }),
+ Bodies.rectangle(w * 0.8, h * 0.42, 180, 18, {
+ isStatic: true,
+ angle: 0.14,
+ }),
+ ],
+ },
+ {
+ id: "scene3",
+ name: "Fast drop maze",
+ config: {
+ gravity: 1.25,
+ spawnIntervalMs: 420,
+ minChain: 3,
+ palette: ["#e879f9", "#38bdf8", "#f97316", "#22c55e"],
+ ballRadius: 16,
+ },
+ createBodies: (w, h) => {
+ const bodies = [
+ Bodies.rectangle(w / 2, h + 40, w, 80, {
+ isStatic: true,
+ restitution: 0.75,
+ }),
+ Bodies.rectangle(-40, h / 2, 80, h * 2, { isStatic: true }),
+ Bodies.rectangle(w + 40, h / 2, 80, h * 2, { isStatic: true }),
+ ];
+ for (let i = 0; i < 5; i += 1) {
+ const x = (w * (i + 1)) / 6;
+ const y = h * 0.35 + (i % 2 === 0 ? 40 : -30);
+ bodies.push(
+ Bodies.circle(x, y, 18, { isStatic: true, restitution: 0.9 }),
+ );
+ }
+ bodies.push(
+ Bodies.rectangle(w * 0.3, h * 0.55, 140, 16, {
+ isStatic: true,
+ angle: -0.3,
+ }),
+ Bodies.rectangle(w * 0.7, h * 0.58, 160, 16, {
+ isStatic: true,
+ angle: 0.28,
+ }),
+ );
+ return bodies;
+ },
+ },
+ ];
+
const sceneEl = document.getElementById("scene-wrapper");
const activeColorEl = document.getElementById("active-color");
const chainLenEl = document.getElementById("chain-length");
@@ -57,33 +157,16 @@
const runner = Runner.create();
Runner.run(runner, engine);
- // Static boundaries and some obstacles to bounce around.
+ // Static boundaries and scene-specific obstacles.
let boundaries = [];
- const createBounds = () => {
+ let currentScene = scenes[0];
+
+ const rebuildSceneBodies = () => {
boundaries.forEach((b) => World.remove(world, b));
- boundaries = [
- Bodies.rectangle(width / 2, height + 40, width, 80, {
- isStatic: true,
- restitution: 0.8,
- }),
- Bodies.rectangle(-40, height / 2, 80, height * 2, { isStatic: true }),
- Bodies.rectangle(width + 40, height / 2, 80, height * 2, {
- isStatic: true,
- }),
- ];
- boundaries.push(
- Bodies.rectangle(width * 0.25, height * 0.55, 160, 20, {
- isStatic: true,
- angle: -0.3,
- }),
- Bodies.rectangle(width * 0.7, height * 0.4, 220, 24, {
- isStatic: true,
- angle: 0.26,
- }),
- );
+ boundaries = currentScene.createBodies(width, height);
World.add(world, boundaries);
};
- createBounds();
+ rebuildSceneBodies();
const balls = [];
let spawnTimer = null;
@@ -112,6 +195,28 @@
}
};
+ const populateSceneSelect = () => {
+ 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 next = scenes.find((s) => s.id === sceneId) || scenes[0];
+ currentScene = next;
+ sceneSelectEl.value = next.id;
+ Object.assign(config, next.config);
+ engine.gravity.y = config.gravity;
+ rebuildSceneBodies();
+ buildLegend();
+ restartGame();
+ updateHud();
+ };
+
const chain = {
active: false,
color: null,
@@ -426,7 +531,7 @@
min: { x: 0, y: 0 },
max: { x: width, y: height },
});
- createBounds();
+ rebuildSceneBodies();
};
Events.on(engine, "afterUpdate", () => {
@@ -474,6 +579,9 @@
setPaused(!isPaused);
}
});
+ sceneSelectEl.addEventListener("change", (e) => applyScene(e.target.value));
+ populateSceneSelect();
+ sceneSelectEl.value = currentScene.id;
highScore = loadHighScore();
buildLegend();
updateHud();