Add scene selection with multiple level configs

This commit is contained in:
Daddy32
2025-12-12 12:32:13 +01:00
parent a3be004c12
commit ccba28d646
2 changed files with 150 additions and 28 deletions

View File

@@ -157,6 +157,14 @@
color: var(--text); color: var(--text);
font-size: 13px; 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 { .legend {
position: absolute; position: absolute;
top: 16px; top: 16px;
@@ -330,6 +338,10 @@
<div class="card"> <div class="card">
<strong>High score</strong> <span id="high-score">0</span> <strong>High score</strong> <span id="high-score">0</span>
</div> </div>
<div class="card">
<strong>Scene</strong>
<select id="scene-select" class="selector"></select>
</div>
</div> </div>
<div class="instructions"> <div class="instructions">
Click or touch a ball to start a chain. Drag through balls of 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 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. 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).
</div> </div>
</div> </div>

162
main.js
View File

@@ -12,13 +12,113 @@
} = Matter; } = Matter;
const config = { const config = {
gravity: 0.78, gravity: 1,
spawnIntervalMs: 720, spawnIntervalMs: 520,
minChain: 3, minChain: 3,
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"], 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 sceneEl = document.getElementById("scene-wrapper");
const activeColorEl = document.getElementById("active-color"); const activeColorEl = document.getElementById("active-color");
const chainLenEl = document.getElementById("chain-length"); const chainLenEl = document.getElementById("chain-length");
@@ -57,33 +157,16 @@
const runner = Runner.create(); const runner = Runner.create();
Runner.run(runner, engine); Runner.run(runner, engine);
// Static boundaries and some obstacles to bounce around. // Static boundaries and scene-specific obstacles.
let boundaries = []; let boundaries = [];
const createBounds = () => { let currentScene = scenes[0];
const rebuildSceneBodies = () => {
boundaries.forEach((b) => World.remove(world, b)); boundaries.forEach((b) => World.remove(world, b));
boundaries = [ boundaries = currentScene.createBodies(width, height);
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,
}),
);
World.add(world, boundaries); World.add(world, boundaries);
}; };
createBounds(); rebuildSceneBodies();
const balls = []; const balls = [];
let spawnTimer = null; 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 = { const chain = {
active: false, active: false,
color: null, color: null,
@@ -426,7 +531,7 @@
min: { x: 0, y: 0 }, min: { x: 0, y: 0 },
max: { x: width, y: height }, max: { x: width, y: height },
}); });
createBounds(); rebuildSceneBodies();
}; };
Events.on(engine, "afterUpdate", () => { Events.on(engine, "afterUpdate", () => {
@@ -474,6 +579,9 @@
setPaused(!isPaused); setPaused(!isPaused);
} }
}); });
sceneSelectEl.addEventListener("change", (e) => applyScene(e.target.value));
populateSceneSelect();
sceneSelectEl.value = currentScene.id;
highScore = loadHighScore(); highScore = loadHighScore();
buildLegend(); buildLegend();
updateHud(); updateHud();