Extract input module
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
<script src="./src/storage.js"></script>
|
||||
<script src="./src/spawn.js"></script>
|
||||
<script src="./src/goals.js"></script>
|
||||
<script src="./src/input.js"></script>
|
||||
<script src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
167
src/input.js
Normal file
167
src/input.js
Normal file
@@ -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 };
|
||||
})();
|
||||
175
src/main.js
175
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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user