(() => { const { Bodies, Query } = Matter; const scenes = (window.PhysilinksSceneDefs = window.PhysilinksSceneDefs || []); const getSquareArea = ({ width, height, world }) => { const size = Math.min(width, height); const offset = world?.plugin?.squareOffset || 0; return { size, left: (width - size) / 2 + offset, top: (height - size) / 2 + offset, }; }; const getSpawnInsets = ({ config, width, height, world }) => { let left = 0; let right = 0; const insetVal = config.spawnInset; if (Number.isFinite(insetVal)) { left = insetVal; right = insetVal; } if (typeof config.spawnInsets === "function") { try { const res = config.spawnInsets({ width, height, world }); 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 getColumnCenters = ({ config, width, height, world }) => { const columns = config.spawnColumns || 0; if (!Number.isFinite(columns) || columns <= 0) return []; const squareArea = getSquareArea({ width, height, world }); const insets = getSpawnInsets({ config, width, height, world }); const usableWidth = Math.max( 0, squareArea.size - insets.left - insets.right, ); const columnWidth = usableWidth / columns; const minX = squareArea.left + insets.left + config.ballRadius + 2; const maxX = squareArea.left + squareArea.size - insets.right - config.ballRadius - 2; return Array.from({ length: columns }, (_, index) => { const x = squareArea.left + insets.left + columnWidth * (index + 0.5); return Math.max(minX, Math.min(maxX, x)); }); }; const getRowCount = ({ config, width, height, world }) => { const squareArea = getSquareArea({ width, height, world }); const side = config.ballRadius * 2; const rowGap = side * (config.rowGapMultiplier ?? 1); const usableHeight = squareArea.size - config.ballRadius * 2; return Math.max(1, Math.floor(usableHeight / rowGap) + 1); }; const findOpenTopSlot = ({ config, width, height, world, balls }) => { const squareArea = getSquareArea({ width, height, world }); const centers = getColumnCenters({ config, width, height, world }); const y = squareArea.top + config.ballRadius; const halfSize = config.ballRadius * 1.05; for (let i = 0; i < centers.length; i += 1) { const x = centers[i]; const region = { min: { x: x - halfSize, y: y - halfSize }, max: { x: x + halfSize, y: y + halfSize }, }; const hits = Query.region(balls, region); if (!hits || hits.length === 0) { return { x, y }; } } return null; }; scenes.push({ id: "stack-blocks-packed", name: "Stack Blocks Packed", config: { gravity: 1, spawnIntervalMs: 900, autoSpawn: false, minChain: 3, palette: ["#38bdf8", "#f97316", "#facc15", "#22c55e"], ballRadius: 18, ballShape: "rect", spawnColumns: 10, sizeFromColumns: true, initialRows: null, requireClearSpawn: false, squarePlayArea: true, rowGapMultiplier: 1, noGameOver: true, chainLose: { idleSec: 15, warnAtSec: 3, introDelayMs: 1200, }, ballPhysics: { restitution: 0.35, friction: 0.18, frictionAir: 0.02, frictionStatic: 0.45, density: 0.003, }, spawnInsets: ({ width }) => { const wallThickness = Math.max(20, width * 0.02); return { left: 0, right: 0 }; }, winCondition: { type: "score", target: 30000, }, link: { stiffness: 0.85, lengthScale: 1.15, damping: 0.08, lineWidth: 3, rope: true, renderType: "line", maxLengthMultiplier: 2.5, clearAnimation: { type: "shatter", durationMs: 380, sizeScale: 2.2, }, }, onBeforeUpdate: ({ engine, width, height, state, spawnSystem, config, }) => { if (!state || !spawnSystem || state.paused || state.levelWon) return; engine.plugin ??= {}; const packedState = engine.plugin.stackBlocksPacked || {}; if (packedState.sceneId !== "stack-blocks-packed") { packedState.sceneId = "stack-blocks-packed"; packedState.seeded = false; } if (!packedState.seeded) { const rows = getRowCount({ config, width, height, world: engine.world, }); const squareArea = getSquareArea({ width, height, world: engine.world, }); const rowGap = config.ballRadius * 2 * (config.rowGapMultiplier ?? 1); for (let r = 0; r < rows; r += 1) { const y = squareArea.top + config.ballRadius + r * rowGap; spawnSystem.spawnRowAtY({ y, markEntered: true }); } packedState.seeded = true; engine.plugin.stackBlocksPacked = packedState; } if (state.gameOver) return; const openSlot = findOpenTopSlot({ config, width, height, world: engine.world, balls: state.balls, }); if (openSlot) { spawnSystem.spawnAtPosition({ x: openSlot.x, y: openSlot.y, }); } }, }, createBodies: (w, h) => { const squareSize = Math.min(w, h); const left = (w - squareSize) / 2; const wallThickness = Math.max(20, w * 0.02); const floorHeight = Math.max(30, squareSize * 0.06); const wallRender = { fillStyle: "#1e293b", strokeStyle: "#94a3b8", lineWidth: 2, }; return [ Bodies.rectangle( left - wallThickness / 2, h / 2, wallThickness, h + wallThickness * 2, { isStatic: true, render: wallRender, }, ), Bodies.rectangle( left + squareSize + wallThickness / 2, h / 2, wallThickness, h + wallThickness * 2, { isStatic: true, render: wallRender, }, ), Bodies.rectangle( w / 2, h + floorHeight / 2, w + wallThickness * 2, floorHeight, { isStatic: true, restitution: 0.2, render: wallRender, }, ), ]; }, }); })();