(() => { const { Bodies } = Matter; const scenes = (window.PhysilinksSceneDefs = window.PhysilinksSceneDefs || []); const makeSquareBodies = (w, h, offset = 0, wallThickness = 24) => { const squareSize = Math.min(w, h) * 0.82; const left = (w - squareSize) / 2 + offset; const top = (h - squareSize) / 2 + offset; const render = { fillStyle: "#0b1222", strokeStyle: "#334155", lineWidth: 2, }; const floorHeight = Math.max(30, squareSize * 0.06); return [ Bodies.rectangle( left - wallThickness / 2, h / 2, wallThickness, h + wallThickness * 2, { isStatic: true, render }, ), Bodies.rectangle( left + squareSize + wallThickness / 2, h / 2, wallThickness, h + wallThickness * 2, { isStatic: true, render }, ), Bodies.rectangle( w / 2, h + floorHeight / 2, w + wallThickness * 2, floorHeight, { isStatic: true, restitution: 0.1, render }, ), ]; }; scenes.push({ id: "storm-grid", name: "Storm Grid Shift", config: { gravity: 0.9, spawnIntervalMs: 520, autoSpawn: true, minChain: 3, palette: ["#38bdf8", "#f97316", "#facc15", "#22c55e"], ballRadius: 18, ballShape: "rect", spawnColumns: 10, sizeFromColumns: true, initialRows: 3, rowGapMultiplier: 1, squarePlayArea: true, requireClearSpawn: true, spawnInset: 0, spawnIntervals: [ { seconds: 0, gravityX: 0, gravityY: 0.9, label: "Calm start" }, { seconds: 20, gravityX: 0.2, gravityY: 0.88, label: "Gust: East pull", }, { seconds: 40, gravityX: -0.25, gravityY: 0.85, label: "Gust: West pull", }, { seconds: 60, gravityX: 0, gravityY: 0.7, label: "Updraft" }, ], winCondition: { type: "timer", durationSec: 90, onWin: { shoveBalls: true }, }, link: { stiffness: 0.82, lengthScale: 1.05, damping: 0.08, lineWidth: 3, rope: true, renderType: "line", maxLengthMultiplier: 3.3, }, onBeforeUpdate: ({ engine, width, height }) => { const state = engine.plugin.stormState || {}; const now = (engine.timing?.timestamp || 0) / 1000; if (!state.startTime) { state.startTime = now; state.nextIdx = 0; engine.plugin.stormState = state; } const elapsed = now - state.startTime; const steps = engine.world.plugin?.stormSteps || []; const upcoming = steps[state.nextIdx]; if (upcoming && elapsed >= upcoming.seconds) { engine.gravity.x = upcoming.gravityX; engine.gravity.y = upcoming.gravityY; // Nudge play area offset to keep things lively. const offset = ((state.nextIdx % 2 === 0 ? 1 : -1) * Math.min(width, height)) / 50; engine.world.plugin.squareOffset = offset; state.nextIdx += 1; if (window?.PhysilinksUI?.instance?.showFloatingMessage) { window.PhysilinksUI.instance.showFloatingMessage( { text: upcoming.label || "Gust incoming" }, { durationMs: 2200 }, ); } } }, spawnInsets: ({ width, height, world }) => { const wallThickness = Math.max(20, Math.min(width, height) * 0.02); const offset = world?.plugin?.squareOffset || 0; const squareSize = Math.min(width, height) * 0.82; const left = (width - squareSize) / 2 + offset; return { left, right: width - (left + squareSize) }; }, }, createBodies: (w, h) => { const offset = 0; const walls = makeSquareBodies( w, h, offset, Math.max(20, Math.min(w, h) * 0.02), ); walls.forEach((b) => { b.plugin = b.plugin || {}; b.plugin.stormWall = true; }); return walls; }, }); })();