diff --git a/main.js b/main.js index 16c32a0..de3e0ea 100644 --- a/main.js +++ b/main.js @@ -202,23 +202,51 @@ const y = spawnFromBottom ? height + config.ballRadius * 2 : -config.ballRadius * 2; - const ball = createBallBody(x, y, color); - ball.plugin = { - color, - hasEntered: false, - entryCheckId: null, - squishX: 1, - squishY: 1, - }; - balls.push(ball); - World.add(world, ball); - ball.plugin.entryCheckId = setTimeout(() => { - ball.plugin.entryCheckId = null; - if (gameOver) return; - if (!ball.plugin.hasEntered && Math.abs(ball.velocity.y) < 0.2) { - triggerGameOver(); + const batchMin = currentScene?.config?.spawnBatchMin ?? 1; + const batchMax = currentScene?.config?.spawnBatchMax ?? 1; + const batchCount = + batchMin === batchMax + ? batchMin + : Math.max( + batchMin, + 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, + ), + y + + i * + (spawnFromBottom + ? -config.ballRadius * 0.5 + : config.ballRadius * 0.5), + color, + ); + if (blob.constraints.length > 0 && blob.blobId) { + blobConstraints.set(blob.blobId, blob.constraints); } - }, 1500); + blob.bodies.forEach((body) => { + balls.push(body); + World.add(world, body); + if (!currentScene?.config?.noGameOver) { + body.plugin.entryCheckId = setTimeout(() => { + body.plugin.entryCheckId = null; + if (gameOver) return; + if (!body.plugin.hasEntered && Math.abs(body.velocity.y) < 0.2) { + triggerGameOver(); + } + }, 1500); + } + }); + if (blob.constraints.length > 0) { + World.add(world, blob.constraints); + } + } }; const startSpawner = () => { diff --git a/scenes/scene-relax.js b/scenes/scene-relax.js index 3ddd56f..25b743e 100644 --- a/scenes/scene-relax.js +++ b/scenes/scene-relax.js @@ -1,5 +1,5 @@ (() => { - const { Bodies } = Matter; + const { Bodies, Composites } = Matter; const scenes = (window.PhysilinksSceneDefs = window.PhysilinksSceneDefs || []); @@ -16,7 +16,7 @@ minChain: 2, palette: ["#38bdf8", "#f472b6", "#fbbf24", "#22c55e", "#a855f7"], ballRadius: 20, - blobBalls: true, + blobBalls: false, noGameOver: true, winCondition: { type: "timer", @@ -38,6 +38,46 @@ const wallHeight = h + wallThickness * 2; const floorHeight = Math.max(40, h * 0.08); const bumperRadius = Math.max(30, Math.min(w, h) * 0.04); + const makeSoft = (cx, cy, cols, rows, radius, color) => { + const particleOpts = { + friction: 0.02, + frictionStatic: 0.04, + restitution: 0.02, + render: { fillStyle: color, strokeStyle: color }, + plugin: { draggable: true, nonLinkable: true }, + }; + const constraintOpts = { + stiffness: 0.08, + damping: 0.35, + render: { visible: false, type: "line", anchors: false }, + }; + const comp = Composites.softBody( + cx - cols * radius * 1.1, + cy - rows * radius * 1.1, + cols, + rows, + 0, + 0, + true, + radius, + particleOpts, + constraintOpts, + ); + comp.bodies.forEach((b) => { + b.plugin = b.plugin || {}; + b.plugin.draggable = true; + b.plugin.nonLinkable = true; + }); + comp.constraints.forEach((c) => { + c.plugin = { soft: true }; + }); + return comp; + }; + + const softRadius = Math.max(18, w * 0.025); + const softA = makeSoft(w * 0.35, h * 0.4, 3, 3, softRadius, "#38bdf8"); + const softB = makeSoft(w * 0.65, h * 0.55, 3, 3, softRadius, "#f472b6"); + return [ Bodies.rectangle( w / 2, @@ -84,6 +124,10 @@ restitution: 1.05, render: { fillStyle: "#22c55e", strokeStyle: "#22c55e" }, }), + ...softA.bodies, + ...softA.constraints, + ...softB.bodies, + ...softB.constraints, ]; }, });