(() => { const { Body, Events, Vector } = Matter; const create = ({ engine, render, config, state, chain, spawnSystem, getCurrentScene, getMaxLinkDistance, updateHud, checkWinCondition, }) => { const runSceneBeforeUpdateHook = () => { const currentScene = getCurrentScene(); if ( state.levelWon || typeof currentScene?.config?.onBeforeUpdate !== "function" ) { return; } currentScene.config.onBeforeUpdate({ engine, width: state.width, height: state.height, state, spawnSystem, config, }); }; const stepRopeConstraints = () => { chain.constraints.forEach((c) => { if (!c.plugin || !c.plugin.rope) return; const current = Vector.magnitude( Vector.sub(c.bodyA.position, c.bodyB.position), ); const maxLen = c.plugin.maxLength ?? c.length; if (current <= maxLen) { c.length = current; c.stiffness = 0; } else { c.length = maxLen; c.stiffness = c.plugin.baseStiffness ?? c.stiffness; } }); }; const stepRotators = (dt, timeScale) => { state.rotators.forEach((b) => { const speed = b.plugin.rotSpeed || 0; if (speed !== 0) { Body.rotate(b, speed * ((dt * timeScale) / 1000)); } }); }; const stepOscillators = () => { state.oscillators.forEach((b) => { const osc = b.plugin.oscillate; if (!osc) return; if (!osc.base) { osc.base = { x: b.position.x, y: b.position.y }; } const now = (engine.timing.timestamp || 0) / 1000; const amplitude = osc.amplitude ?? 0; const speed = osc.speed ?? 1; const phase = osc.phase ?? 0; const offset = Math.sin(now * speed + phase) * amplitude; const target = osc.axis === "x" ? { x: osc.base.x + offset, y: osc.base.y } : { x: osc.base.x, y: osc.base.y + offset }; Body.setPosition(b, target); Body.setVelocity(b, { x: 0, y: 0 }); }); }; const stepTimer = () => { if (!state.timerEndMs) return; const winCond = getCurrentScene()?.config?.winCondition; const duration = winCond?.durationSec ?? 120; const now = Date.now(); const remainingMs = Math.max(0, state.timerEndMs - now); const remainingSec = Math.ceil(remainingMs / 1000); if (state.lastTimerDisplay !== remainingSec) { state.lastTimerDisplay = remainingSec; updateHud(); } if (remainingMs <= 0 && !state.levelWon) { checkWinCondition(); } }; const keepBallsInBounds = () => { const currentScene = getCurrentScene(); state.balls.forEach((ball) => { if ( !ball.plugin.hasEntered && ball.position.y > config.ballRadius * 1.5 ) { ball.plugin.hasEntered = true; } if ( ball.position.x < -100 || ball.position.x > state.width + 100 || (currentScene?.config?.spawnFrom === "bottom" ? ball.position.y < -500 : ball.position.y > state.height + 500) ) { spawnSystem.cleanupBall(ball); ball.plugin.hasEntered = true; const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom"; Body.setPosition(ball, { x: currentScene?.config?.spawnOrigin === "center" ? state.width / 2 : Math.random() * state.width, y: currentScene?.config?.spawnOrigin === "center" ? state.height / 2 : spawnFromBottom ? state.height + 40 : -40, }); Body.setVelocity(ball, { x: 0, y: 0 }); } }); }; const beforeUpdateStep = () => { runSceneBeforeUpdateHook(); stepRopeConstraints(); const dt = (engine.timing && engine.timing.delta) || 16; const timeScale = engine.timing?.timeScale ?? 1; if (state.paused || state.gameOver || timeScale === 0) return; stepRotators(dt, timeScale); stepOscillators(); stepTimer(); }; const drawChainPreview = () => { if (!chain.active || !chain.pointer || chain.bodies.length === 0) return; const last = chain.bodies[chain.bodies.length - 1]; const ctx = render.context; const maxLinkDist = getMaxLinkDistance(); const delta = Vector.sub(chain.pointer, last.position); const dist = Vector.magnitude(delta); const previewMargin = config.ballRadius; const cappedDist = Math.max( 0, Math.min(dist, Math.max(0, maxLinkDist - previewMargin)), ); const safeTarget = dist > 0 ? Vector.add( last.position, Vector.mult(Vector.normalise(delta), cappedDist), ) : chain.pointer; ctx.save(); ctx.strokeStyle = chain.color || "#fff"; ctx.lineWidth = 3; ctx.setLineDash([6, 6]); ctx.beginPath(); ctx.moveTo(last.position.x, last.position.y); ctx.lineTo(safeTarget.x, safeTarget.y); ctx.stroke(); ctx.restore(); }; Events.on(engine, "afterUpdate", keepBallsInBounds); Events.on(engine, "beforeUpdate", beforeUpdateStep); Events.on(render, "afterRender", drawChainPreview); }; window.PhysilinksLoop = { create }; })();