From fb7b6b2e6d22446019d37667f0f0136418fb205c Mon Sep 17 00:00:00 2001 From: Daddy32 Date: Mon, 15 Dec 2025 18:30:42 +0100 Subject: [PATCH] Extract input module --- README.md | 3 +- index.html | 1 + src/input.js | 167 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.js | 175 +++++++-------------------------------------------- 4 files changed, 194 insertions(+), 152 deletions(-) create mode 100644 src/input.js diff --git a/README.md b/README.md index 1049764..e8ea673 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ Physilinks is a browser-based physics linking game built with Matter.js. Match a - `src/ui.js`: DOM access, HUD updates, overlays, popups, and control/selector wiring. - `src/spawn.js`: Spawner utilities (intervals, batch/column/grid spawns), ball creation (shapes/blobs), radius scaling, and blob cleanup. - `src/goals.js`: Goal computation and messaging (timer/score/clear/color goals, milestone announcements, intro message). -- `src/main.js`: Physics setup, state machine, chain interaction, scene application, and pause/restart logic; delegates spawn duties to `src/spawn.js` and goal handling to `src/goals.js`. +- `src/input.js`: Pointer/touch handling, drag constraints, chain linking/undo flow, and input event wiring. +- `src/main.js`: Physics setup, state machine, chain interaction, scene application, and pause/restart logic; delegates spawn duties to `src/spawn.js`, goal handling to `src/goals.js`, and input/chain interactions to `src/input.js`. ## Development quick start - No build step. Open `index.html` directly in the browser. diff --git a/index.html b/index.html index 7e6b777..5e07223 100644 --- a/index.html +++ b/index.html @@ -109,6 +109,7 @@ + diff --git a/src/input.js b/src/input.js new file mode 100644 index 0000000..4c74e8d --- /dev/null +++ b/src/input.js @@ -0,0 +1,167 @@ +(() => { + const { Query, Constraint, World, Vector } = Matter; + + const create = ({ + render, + world, + balls, + boundaries, + chain, + config, + getCurrentScene, + isPaused, + isLevelWon, + isGameOver, + getMaxLinkDistance, + setHighlight, + removeLastFromChain, + addToChain, + finishChain, + updateHud, + }) => { + let dragConstraint = null; + + const getPointerPosition = (evt) => { + const rect = render.canvas.getBoundingClientRect(); + const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX; + const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY; + return { + x: clientX - rect.left, + y: clientY - rect.top, + }; + }; + + const getDraggableBody = (point) => { + const draggables = [ + ...boundaries.filter((b) => b.plugin?.draggable), + ...balls.filter((b) => b.plugin?.draggable), + ]; + const hits = Query.point(draggables, point); + return hits[0]; + }; + + const pickBody = (point) => { + const hits = Query.point(balls, point); + return hits[0]; + }; + + const startDrag = (body, point) => { + endDrag(); + dragConstraint = Constraint.create({ + pointA: point, + bodyB: body, + stiffness: 0.2, + damping: 0.3, + render: { visible: false }, + }); + World.add(world, dragConstraint); + }; + + const updateDrag = (point) => { + if (!dragConstraint) return; + dragConstraint.pointA = point; + }; + + const endDrag = () => { + if (dragConstraint) { + World.remove(world, dragConstraint); + dragConstraint = null; + } + }; + + const handlePointerDown = (evt) => { + if (isGameOver() || isPaused() || isLevelWon()) return; + const point = getPointerPosition(evt); + const dragTarget = getDraggableBody(point); + if (dragTarget) { + startDrag(dragTarget, point); + return; + } + const body = pickBody(point); + if (!body) return; + const scene = getCurrentScene(); + if (!scene?.config?.relaxMode) { + chain.color = body.plugin.color; + } else { + chain.color = body.plugin.color; + } + chain.active = true; + chain.bodies = [body]; + chain.constraints = []; + chain.pointer = point; + setHighlight(body, true); + updateHud(); + }; + + const handlePointerMove = (evt) => { + if (dragConstraint) { + updateDrag(getPointerPosition(evt)); + return; + } + if (!chain.active) return; + if (isGameOver() || isPaused() || isLevelWon()) return; + const point = getPointerPosition(evt); + chain.pointer = point; + const body = pickBody(point); + if (!body) return; + const alreadyInChain = chain.bodies.includes(body); + if (alreadyInChain) { + const targetIndex = chain.bodies.indexOf(body); + if (chain.bodies.length > 1 && targetIndex === chain.bodies.length - 2) { + removeLastFromChain(); + } + return; + } + const scene = getCurrentScene(); + if (!scene?.config?.relaxMode && body.plugin.color !== chain.color) return; + const maxLinkDist = getMaxLinkDistance(); + const dist = Vector.magnitude( + Vector.sub(chain.bodies[chain.bodies.length - 1].position, body.position), + ); + if (dist > maxLinkDist) return; + addToChain(body); + }; + + const handlePointerUp = () => { + if (dragConstraint) { + endDrag(); + return; + } + finishChain(chain.pointer); + }; + + render.canvas.addEventListener("mousedown", handlePointerDown); + render.canvas.addEventListener("mousemove", handlePointerMove); + window.addEventListener("mouseup", handlePointerUp); + render.canvas.addEventListener( + "touchstart", + (e) => { + e.preventDefault(); + handlePointerDown(e); + }, + { passive: false }, + ); + render.canvas.addEventListener( + "touchmove", + (e) => { + e.preventDefault(); + handlePointerMove(e); + }, + { passive: false }, + ); + render.canvas.addEventListener( + "touchend", + (e) => { + e.preventDefault(); + handlePointerUp(e); + }, + { passive: false }, + ); + + return { + endDrag, + }; + }; + + window.PhysilinksInput = { create }; +})(); diff --git a/src/main.js b/src/main.js index 8f41dc5..74497a4 100644 --- a/src/main.js +++ b/src/main.js @@ -1,15 +1,6 @@ (() => { - const { - Engine, - Render, - Runner, - World, - Body, - Constraint, - Events, - Query, - Vector, - } = Matter; + const { Engine, Render, Runner, World, Body, Constraint, Events, Vector } = + Matter; const { scenes = [], defaultSceneId } = window.PhysilinksScenes || {}; const getSceneById = (sceneId) => @@ -143,8 +134,8 @@ let levelWon = false; let timerEndMs = null; let lastTimerDisplay = null; - let dragConstraint = null; let spawnSystem = null; + let input = null; const { loadHighScore = () => 0, @@ -303,7 +294,7 @@ clearedCount = 0; clearedByColor = {}; goals.resetMilestones(); - endDrag(); + input?.endDrag(); const winCond = currentScene?.config?.winCondition; if (winCond?.type === "timer") { const duration = winCond.durationSec ?? 120; @@ -571,143 +562,6 @@ checkWinCondition(); }; - const pickBody = (point) => { - const hits = Query.point(balls, point); - return hits[0]; - }; - - const getDraggableBody = (point) => { - const draggables = [ - ...boundaries.filter((b) => b.plugin?.draggable), - ...balls.filter((b) => b.plugin?.draggable), - ]; - const hits = Query.point(draggables, point); - return hits[0]; - }; - - const startDrag = (body, point) => { - endDrag(); - dragConstraint = Constraint.create({ - pointA: point, - bodyB: body, - stiffness: 0.2, - damping: 0.3, - render: { visible: false }, - }); - World.add(world, dragConstraint); - }; - - const updateDrag = (point) => { - if (!dragConstraint) return; - dragConstraint.pointA = point; - }; - - const endDrag = () => { - if (dragConstraint) { - World.remove(world, dragConstraint); - dragConstraint = null; - } - }; - - const getPointerPosition = (evt) => { - const rect = render.canvas.getBoundingClientRect(); - const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX; - const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY; - return { - x: clientX - rect.left, - y: clientY - rect.top, - }; - }; - - const handlePointerDown = (evt) => { - if (gameOver || isPaused || levelWon) return; - const point = getPointerPosition(evt); - const dragTarget = getDraggableBody(point); - if (dragTarget) { - startDrag(dragTarget, point); - return; - } - const body = pickBody(point); - if (!body) return; - if (!currentScene?.config?.relaxMode) { - // Only allow linking same colors unless relax mode explicitly opts out. - chain.color = body.plugin.color; - } else { - chain.color = body.plugin.color; - } - chain.active = true; - chain.bodies = [body]; - chain.constraints = []; - chain.pointer = point; - setHighlight(body, true); - updateHud(); - }; - - const handlePointerMove = (evt) => { - if (dragConstraint) { - updateDrag(getPointerPosition(evt)); - return; - } - if (!chain.active) return; - if (gameOver || isPaused || levelWon) return; - const point = getPointerPosition(evt); - chain.pointer = point; - const body = pickBody(point); - if (!body) return; - const alreadyInChain = chain.bodies.includes(body); - if (alreadyInChain) { - const targetIndex = chain.bodies.indexOf(body); - if (chain.bodies.length > 1 && targetIndex === chain.bodies.length - 2) { - removeLastFromChain(); - } - return; - } - if (!currentScene?.config?.relaxMode && body.plugin.color !== chain.color) - return; - const maxLinkDist = getMaxLinkDistance(); - const dist = Vector.magnitude( - Vector.sub(chain.bodies[chain.bodies.length - 1].position, body.position), - ); - if (dist > maxLinkDist) return; - addToChain(body); - }; - - const handlePointerUp = () => { - if (dragConstraint) { - endDrag(); - return; - } - finishChain(chain.pointer); - }; - - render.canvas.addEventListener("mousedown", handlePointerDown); - render.canvas.addEventListener("mousemove", handlePointerMove); - window.addEventListener("mouseup", handlePointerUp); - render.canvas.addEventListener( - "touchstart", - (e) => { - e.preventDefault(); - handlePointerDown(e); - }, - { passive: false }, - ); - render.canvas.addEventListener( - "touchmove", - (e) => { - e.preventDefault(); - handlePointerMove(e); - }, - { passive: false }, - ); - render.canvas.addEventListener( - "touchend", - (e) => { - e.preventDefault(); - handlePointerUp(e); - }, - { passive: false }, - ); - const updateHud = () => { ui.updateHud({ spawnIntervalMs: config.spawnIntervalMs, @@ -722,6 +576,25 @@ goals.maybeAnnounceGoalProgress(goal); }; + input = window.PhysilinksInput.create({ + render, + world, + balls, + boundaries, + chain, + config, + getCurrentScene: () => currentScene, + isPaused: () => isPaused, + isLevelWon: () => levelWon, + isGameOver: () => gameOver, + getMaxLinkDistance, + setHighlight, + removeLastFromChain, + addToChain, + finishChain, + updateHud, + }); + const buildLegend = () => { ui.buildLegend(config.palette); }; @@ -743,7 +616,7 @@ Body.setVelocity(ball, { x: 0, y: 0 }); }); resetChainVisuals(); - endDrag(); + input?.endDrag(); }; const handleResize = () => {