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();