diff --git a/index.html b/index.html
index 69bdee6..3255d03 100644
--- a/index.html
+++ b/index.html
@@ -102,6 +102,7 @@
+
diff --git a/src/main.js b/src/main.js
index a6fdd0f..d697930 100644
--- a/src/main.js
+++ b/src/main.js
@@ -52,6 +52,7 @@
minChain: 3,
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
ballRadius: 18,
+ ballShape: "circle",
link: {
stiffness: 0.85,
lengthScale: 1.05, // max stretch factor; slack below this
@@ -320,12 +321,18 @@
(Math.random() - 0.5) * Math.max(spawnJitter, config.ballRadius),
}
: null;
- const x =
+ const columnCount = currentScene?.config?.spawnColumns;
+ let x =
centerSpawn?.x ??
Math.max(
config.ballRadius + 10,
Math.min(width - config.ballRadius - 10, Math.random() * width),
);
+ if (Number.isFinite(columnCount) && columnCount > 0) {
+ const columnWidth = width / columnCount;
+ const colIndex = Math.floor(Math.random() * columnCount);
+ x = columnWidth * (colIndex + 0.5);
+ }
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
const y =
centerSpawn?.y ??
@@ -342,21 +349,30 @@
Math.floor(Math.random() * (batchMax - batchMin + 1)) + batchMin,
);
for (let i = 0; i < batchCount; i += 1) {
- const blob = createBallBodies(
- Math.min(
- Math.max(
- config.ballRadius + 10,
- x + (i - batchCount / 2) * config.ballRadius * 1.5,
- ),
- width - config.ballRadius - 10,
+ const targetX = Math.min(
+ Math.max(
+ config.ballRadius + 10,
+ x + (i - batchCount / 2) * config.ballRadius * 1.5,
),
- y +
- i *
- (spawnFromBottom
- ? -config.ballRadius * 0.5
- : config.ballRadius * 0.5),
- color,
+ width - config.ballRadius - 10,
);
+ const targetY =
+ y +
+ i *
+ (spawnFromBottom
+ ? -config.ballRadius * 0.5
+ : config.ballRadius * 0.5);
+ const halfSize = config.ballRadius * 1.05;
+ const region = {
+ min: { x: targetX - halfSize, y: targetY - halfSize },
+ max: { x: targetX + halfSize, y: targetY + halfSize },
+ };
+ const hits = Query.region(balls, region);
+ if (hits && hits.length > 0) {
+ triggerGameOver();
+ return;
+ }
+ const blob = createBallBodies(targetX, targetY, color);
if (blob.constraints.length > 0 && blob.blobId) {
blobConstraints.set(blob.blobId, blob.constraints);
}
@@ -402,6 +418,39 @@
}
};
+ const spawnInitialColumns = () => {
+ const rows = currentScene?.config?.initialRows;
+ const columns = currentScene?.config?.spawnColumns;
+ if (!Number.isFinite(rows) || rows <= 0) return false;
+ if (!Number.isFinite(columns) || columns <= 0) return false;
+ const columnWidth = width / columns;
+ const side = config.ballRadius * 2;
+ const rowGap = side * 1.05;
+ for (let r = 0; r < rows; r += 1) {
+ const y = config.ballRadius * 1.5 + r * rowGap;
+ for (let c = 0; c < columns; c += 1) {
+ const x = columnWidth * (c + 0.5);
+ const color =
+ config.palette[Math.floor(Math.random() * config.palette.length)];
+ const blob = createBallBodies(x, y, color);
+ blob.bodies.forEach((body) => {
+ body.plugin = body.plugin || {};
+ body.plugin.hasEntered = true;
+ balls.push(body);
+ World.add(world, body);
+ });
+ if (blob.constraints.length > 0) {
+ World.add(world, blob.constraints);
+ }
+ if (blob.constraints.length > 0 && blob.blobId) {
+ blobConstraints.set(blob.blobId, blob.constraints);
+ }
+ }
+ }
+ spawnCount += rows * columns;
+ return true;
+ };
+
const cleanupBall = (ball) => {
if (ball.plugin && ball.plugin.entryCheckId) {
clearTimeout(ball.plugin.entryCheckId);
@@ -517,7 +566,10 @@
if (isGridScene()) {
spawnGridBalls();
} else {
- spawnInitialBurst();
+ const spawnedGrid = spawnInitialColumns();
+ if (!spawnedGrid) {
+ spawnInitialBurst();
+ }
startSpawner();
}
showGoalIntro();
@@ -1000,6 +1052,15 @@
body.plugin = { color, hasEntered: false, entryCheckId: null };
return { bodies: [body], constraints: [], blobId: null };
}
+ if (currentScene?.config?.ballShape === "rect") {
+ const side = config.ballRadius * 2;
+ const body = Bodies.rectangle(x, y, side, side, {
+ ...commonOpts,
+ chamfer: 4,
+ });
+ body.plugin = { color, hasEntered: false, entryCheckId: null };
+ return { bodies: [body], constraints: [], blobId: null };
+ }
const body = Bodies.circle(x, y, config.ballRadius, commonOpts);
body.plugin = { color, hasEntered: false, entryCheckId: null };
return { bodies: [body], constraints: [], blobId: null };
diff --git a/src/scenes/index.js b/src/scenes/index.js
index aba2356..f9c004a 100644
--- a/src/scenes/index.js
+++ b/src/scenes/index.js
@@ -8,6 +8,7 @@
"scene-lava",
"swirl-arena",
"relax",
+ "stack-blocks",
];
const orderedScenes = desiredOrder
.map((id) => scenes.find((s) => s.id === id))