Add scene selection with multiple level configs
This commit is contained in:
16
index.html
16
index.html
@@ -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
162
main.js
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user