From 95420f1acff78f4ee64f2712a46f88675d354786 Mon Sep 17 00:00:00 2001 From: Daddy32 Date: Sun, 14 Dec 2025 22:06:52 +0100 Subject: [PATCH] Clamp square spawns for stack blocks --- src/main.js | 137 +++++++++++++++++++++++++------ src/scenes/scene-stack-blocks.js | 36 +++++--- 2 files changed, 139 insertions(+), 34 deletions(-) diff --git a/src/main.js b/src/main.js index d697930..db01b59 100644 --- a/src/main.js +++ b/src/main.js @@ -322,23 +322,32 @@ } : null; const columnCount = currentScene?.config?.spawnColumns; + const insets = getSpawnInsets(); + const squareArea = getSquarePlayArea(); + const playWidth = squareArea ? squareArea.size : width; + const playLeft = squareArea ? squareArea.left : 0; + const usableWidth = Math.max(0, playWidth - insets.left - insets.right); + const minX = playLeft + insets.left + config.ballRadius + 2; + const maxX = playLeft + playWidth - insets.right - config.ballRadius - 2; let x = centerSpawn?.x ?? Math.max( - config.ballRadius + 10, - Math.min(width - config.ballRadius - 10, Math.random() * width), + minX, + Math.min(maxX, playLeft + insets.left + Math.random() * usableWidth), ); if (Number.isFinite(columnCount) && columnCount > 0) { - const columnWidth = width / columnCount; + const columnWidth = usableWidth / columnCount; const colIndex = Math.floor(Math.random() * columnCount); - x = columnWidth * (colIndex + 0.5); + x = playLeft + insets.left + columnWidth * (colIndex + 0.5); + x = Math.max(minX, Math.min(maxX, x)); } const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom"; + const defaultTop = squareArea + ? squareArea.top - config.ballRadius * 2 + : -config.ballRadius * 2; const y = centerSpawn?.y ?? - (spawnFromBottom - ? height + config.ballRadius * 2 - : -config.ballRadius * 2); + (spawnFromBottom ? height + config.ballRadius * 2 : defaultTop); const batchMin = currentScene?.config?.spawnBatchMin ?? 1; const batchMax = currentScene?.config?.spawnBatchMax ?? 1; const batchCount = @@ -362,15 +371,17 @@ (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; + if (currentScene?.config?.requireClearSpawn) { + 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) { @@ -418,18 +429,58 @@ } }; + const getSquarePlayArea = () => { + if (!currentScene?.config?.squarePlayArea) return null; + const size = Math.min(width, height); + const left = (width - size) / 2; + const top = (height - size) / 2; + return { size, left, top }; + }; + + const getSpawnInsets = () => { + let left = 0; + let right = 0; + const insetVal = currentScene?.config?.spawnInset; + if (Number.isFinite(insetVal)) { + left = insetVal; + right = insetVal; + } + if (typeof currentScene?.config?.spawnInsets === "function") { + try { + const res = currentScene.config.spawnInsets({ width, height }); + if (res && Number.isFinite(res.left)) left = res.left; + if (res && Number.isFinite(res.right)) right = res.right; + } catch (err) { + console.error("spawnInsets function failed", err); + } + } + return { left, right }; + }; + 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 insets = getSpawnInsets(); + const squareArea = getSquarePlayArea(); + const usableWidth = Math.max( + 0, + (squareArea ? squareArea.size : width) - insets.left - insets.right, + ); + const columnWidth = usableWidth / columns; const side = config.ballRadius * 2; - const rowGap = side * 1.05; + const rowGap = side * (currentScene?.config?.rowGapMultiplier ?? 1.05); + const startY = squareArea + ? squareArea.top + config.ballRadius + : config.ballRadius * 1.5; for (let r = 0; r < rows; r += 1) { - const y = config.ballRadius * 1.5 + r * rowGap; + const y = startY + r * rowGap; for (let c = 0; c < columns; c += 1) { - const x = columnWidth * (c + 0.5); + const x = + (squareArea ? squareArea.left : 0) + + insets.left + + columnWidth * (c + 0.5); const color = config.palette[Math.floor(Math.random() * config.palette.length)]; const blob = createBallBodies(x, y, color); @@ -969,6 +1020,21 @@ const scaled = cellSize * scale; return Math.round(Math.max(10, Math.min(60, scaled))); } + if ( + currentScene?.config?.sizeFromColumns && + Number.isFinite(currentScene?.config?.spawnColumns) && + currentScene.config.spawnColumns > 0 + ) { + const insets = getSpawnInsets(); + const squareArea = getSquarePlayArea(); + const usableWidth = Math.max( + 0, + (squareArea ? squareArea.size : width) - insets.left - insets.right, + ); + const colWidth = usableWidth / currentScene.config.spawnColumns; + // Fit 10 per side; halve to get radius. Cap to a reasonable range. + return Math.round(Math.max(8, Math.min(60, colWidth / 2))); + } const baseRadius = (currentScene && currentScene.config && currentScene.config.ballRadius) || config.ballRadius || @@ -988,7 +1054,15 @@ const scale = nextRadius / prevRadius; balls.forEach((ball) => { Body.scale(ball, scale, scale); - ball.circleRadius = nextRadius; + if (ball.plugin?.shape === "rect") return; + if (ball.plugin?.shape === "jagged") { + // Recompute center to keep shape consistent; vertices are scaled by Body.scale + Body.setPosition(ball, ball.position); + return; + } + if (typeof ball.circleRadius === "number") { + ball.circleRadius = nextRadius; + } }); } config.ballRadius = nextRadius; @@ -1049,7 +1123,12 @@ }); } const body = Bodies.fromVertices(x, y, [points], commonOpts, true); - body.plugin = { color, hasEntered: false, entryCheckId: null }; + body.plugin = { + color, + hasEntered: false, + entryCheckId: null, + shape: "jagged", + }; return { bodies: [body], constraints: [], blobId: null }; } if (currentScene?.config?.ballShape === "rect") { @@ -1058,11 +1137,21 @@ ...commonOpts, chamfer: 4, }); - body.plugin = { color, hasEntered: false, entryCheckId: null }; + body.plugin = { + color, + hasEntered: false, + entryCheckId: null, + shape: "rect", + }; return { bodies: [body], constraints: [], blobId: null }; } const body = Bodies.circle(x, y, config.ballRadius, commonOpts); - body.plugin = { color, hasEntered: false, entryCheckId: null }; + body.plugin = { + color, + hasEntered: false, + entryCheckId: null, + shape: "circle", + }; return { bodies: [body], constraints: [], blobId: null }; }; diff --git a/src/scenes/scene-stack-blocks.js b/src/scenes/scene-stack-blocks.js index 30b0bc9..b70ff84 100644 --- a/src/scenes/scene-stack-blocks.js +++ b/src/scenes/scene-stack-blocks.js @@ -7,7 +7,7 @@ id: "stack-blocks", name: "Stack Blocks", config: { - gravity: 1.05, + gravity: 1.85, spawnIntervalMs: 900, autoSpawn: true, minChain: 3, @@ -15,7 +15,15 @@ ballRadius: 18, ballShape: "rect", spawnColumns: 10, + sizeFromColumns: true, initialRows: 3, + requireClearSpawn: true, + squarePlayArea: true, + rowGapMultiplier: 1, + spawnInsets: ({ width }) => { + const wallThickness = Math.max(20, width * 0.02); + return { left: 0, right: 0 }; + }, winCondition: { type: "clearCount", target: 100, @@ -27,31 +35,39 @@ lineWidth: 3, rope: true, renderType: "line", - maxLengthMultiplier: 3.1, + maxLengthMultiplier: 2.5, }, }, createBodies: (w, h) => { + const squareSize = Math.min(w, h); + const left = (w - squareSize) / 2; + const top = (h - squareSize) / 2; const wallThickness = Math.max(20, w * 0.02); - const floorHeight = Math.max(30, h * 0.06); + const floorHeight = Math.max(30, squareSize * 0.06); + const wallRender = { + fillStyle: "#1e293b", + strokeStyle: "#94a3b8", + lineWidth: 2, + }; return [ Bodies.rectangle( - -wallThickness / 2, + left - wallThickness / 2, h / 2, wallThickness, - h * 2, + h + wallThickness * 2, { isStatic: true, - render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" }, + render: wallRender, }, ), Bodies.rectangle( - w + wallThickness / 2, + left + squareSize + wallThickness / 2, h / 2, wallThickness, - h * 2, + h + wallThickness * 2, { isStatic: true, - render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" }, + render: wallRender, }, ), Bodies.rectangle( @@ -62,7 +78,7 @@ { isStatic: true, restitution: 0.2, - render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" }, + render: wallRender, }, ), ];