diff --git a/src/loop.js b/src/loop.js index c7782cd..b513c87 100644 --- a/src/loop.js +++ b/src/loop.js @@ -25,6 +25,9 @@ engine, width: state.width, height: state.height, + state, + spawnSystem, + config, }); }; diff --git a/src/scenes/scene-stack-blocks-chaos.js b/src/scenes/scene-stack-blocks-chaos.js index 5dca736..6e07513 100644 --- a/src/scenes/scene-stack-blocks-chaos.js +++ b/src/scenes/scene-stack-blocks-chaos.js @@ -1,5 +1,5 @@ (() => { - const { Bodies } = Matter; + const { Bodies, Body } = Matter; const scenes = (window.PhysilinksSceneDefs = window.PhysilinksSceneDefs || []); @@ -7,14 +7,14 @@ id: "stack-blocks-chaos", name: "Stack Blocks Chaos", config: { - gravity: 1.85, + gravity: 2.85, spawnIntervalMs: 750, autoSpawn: true, minChain: 3, - palette: ["#fb7185", "#8b5cf6", "#22d3ee", "#fbbf24", "#24fbbf"], + palette: ["#fb7185", "#8b5cf6", "#22d3ee"], ballRadius: 18, ballShape: "rect", - spawnColumns: 12, + spawnColumns: 10, sizeFromColumns: true, initialRows: 3, requireClearSpawn: true, @@ -25,8 +25,8 @@ return { left: 0, right: 0 }; }, winCondition: { - type: "clearCount", - target: 100, + type: "score", + target: 20000, }, link: { stiffness: 0.85, @@ -37,6 +37,72 @@ renderType: "line", maxLengthMultiplier: 2.5, }, + chaosSurge: { + intervalMs: 19000, + jitterMs: 2500, + pushRows: 1, + columnJitter: 10, + }, + onBeforeUpdate: ({ + engine, + width, + height, + state, + spawnSystem, + config, + }) => { + if ( + !state || + !spawnSystem || + state.paused || + state.gameOver || + state.levelWon + ) { + return; + } + const surgeCfg = config.chaosSurge || {}; + const interval = surgeCfg.intervalMs ?? 9000; + const jitter = surgeCfg.jitterMs ?? 0; + const now = engine.timing?.timestamp ?? Date.now(); + engine.plugin ??= {}; + const chaosState = engine.plugin.stackBlocksChaos || {}; + if (chaosState.sceneId !== "stack-blocks-chaos") { + chaosState.sceneId = "stack-blocks-chaos"; + chaosState.nextSurgeMs = now + interval + Math.random() * jitter; + } + if (!chaosState.nextSurgeMs) { + chaosState.nextSurgeMs = now + interval + Math.random() * jitter; + } + if (now < chaosState.nextSurgeMs) { + engine.plugin.stackBlocksChaos = chaosState; + return; + } + chaosState.nextSurgeMs = now + interval + Math.random() * jitter; + engine.plugin.stackBlocksChaos = chaosState; + + const rowGap = config.ballRadius * 2 * (config.rowGapMultiplier ?? 1); + const pushDistance = Math.min( + rowGap * (surgeCfg.pushRows ?? 1), + height * 0.35, + ); + const dampen = 0.6; + state.balls.forEach((ball) => { + Body.translate(ball, { x: 0, y: -pushDistance }); + Body.setVelocity(ball, { + x: ball.velocity.x * dampen, + y: Math.min(ball.velocity.y, 0), + }); + }); + + const squareSize = Math.min(width, height); + const playTop = (height - squareSize) / 2; + const baseY = playTop + squareSize - config.ballRadius * 1.2; + spawnSystem.spawnRowAtY({ + y: baseY, + jitter: surgeCfg.columnJitter ?? config.ballRadius * 0.4, + markEntered: true, + }); + }, }, createBodies: (w, h) => { const squareSize = Math.min(w, h); diff --git a/src/spawn.js b/src/spawn.js index 6c894e5..d835c76 100644 --- a/src/spawn.js +++ b/src/spawn.js @@ -471,6 +471,74 @@ spawnCount = 0; }; + const spawnRowAtY = ({ + y, + columns: columnsOverride, + jitter = 0, + markEntered = false, + } = {}) => { + const scene = getCurrentScene(); + const sceneConfig = scene?.config || {}; + const columns = + Number.isFinite(columnsOverride) && columnsOverride > 0 + ? columnsOverride + : sceneConfig.spawnColumns; + if (!Number.isFinite(columns) || columns <= 0) return 0; + const { width, height } = getDimensions(); + const insets = getSpawnInsets(); + const squareArea = getSquarePlayArea(); + const usableWidth = Math.max( + 0, + (squareArea ? squareArea.size : width) - insets.left - insets.right, + ); + const columnWidth = usableWidth / columns; + const minX = + (squareArea ? squareArea.left : 0) + + insets.left + + config.ballRadius + + 2; + const maxX = + (squareArea ? squareArea.left + squareArea.size : width) - + insets.right - + config.ballRadius - + 2; + const baseY = + typeof y === "number" + ? y + : (squareArea ? squareArea.top + squareArea.size : height) - + config.ballRadius * 1.1; + for (let c = 0; c < columns; c += 1) { + const jitterX = (Math.random() - 0.5) * jitter; + const x = + (squareArea ? squareArea.left : 0) + + insets.left + + columnWidth * (c + 0.5) + + jitterX; + const safeX = Math.max(minX, Math.min(maxX, x)); + const color = + config.palette[Math.floor(Math.random() * config.palette.length)]; + const blob = createBallBodies(safeX, baseY, color); + blob.bodies.forEach((body) => { + body.plugin = body.plugin || {}; + body.plugin.hasEntered = !!markEntered; + if (body.plugin.entryCheckId) { + clearTimeout(body.plugin.entryCheckId); + body.plugin.entryCheckId = null; + } + 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 += columns; + return columns; + }; + return { startSpawner, stopSpawner, @@ -482,6 +550,7 @@ cleanupBall, removeBlob, resetSpawnState, + spawnRowAtY, }; };