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/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/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/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
|
## Development quick start
|
||||||
- No build step. Open `index.html` directly in the browser.
|
- No build step. Open `index.html` directly in the browser.
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
<script src="./src/storage.js"></script>
|
<script src="./src/storage.js"></script>
|
||||||
<script src="./src/spawn.js"></script>
|
<script src="./src/spawn.js"></script>
|
||||||
<script src="./src/goals.js"></script>
|
<script src="./src/goals.js"></script>
|
||||||
|
<script src="./src/input.js"></script>
|
||||||
<script src="./src/main.js"></script>
|
<script src="./src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 {
|
const { Engine, Render, Runner, World, Body, Constraint, Events, Vector } =
|
||||||
Engine,
|
Matter;
|
||||||
Render,
|
|
||||||
Runner,
|
|
||||||
World,
|
|
||||||
Body,
|
|
||||||
Constraint,
|
|
||||||
Events,
|
|
||||||
Query,
|
|
||||||
Vector,
|
|
||||||
} = Matter;
|
|
||||||
|
|
||||||
const { scenes = [], defaultSceneId } = window.PhysilinksScenes || {};
|
const { scenes = [], defaultSceneId } = window.PhysilinksScenes || {};
|
||||||
const getSceneById = (sceneId) =>
|
const getSceneById = (sceneId) =>
|
||||||
@@ -143,8 +134,8 @@
|
|||||||
let levelWon = false;
|
let levelWon = false;
|
||||||
let timerEndMs = null;
|
let timerEndMs = null;
|
||||||
let lastTimerDisplay = null;
|
let lastTimerDisplay = null;
|
||||||
let dragConstraint = null;
|
|
||||||
let spawnSystem = null;
|
let spawnSystem = null;
|
||||||
|
let input = null;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loadHighScore = () => 0,
|
loadHighScore = () => 0,
|
||||||
@@ -303,7 +294,7 @@
|
|||||||
clearedCount = 0;
|
clearedCount = 0;
|
||||||
clearedByColor = {};
|
clearedByColor = {};
|
||||||
goals.resetMilestones();
|
goals.resetMilestones();
|
||||||
endDrag();
|
input?.endDrag();
|
||||||
const winCond = currentScene?.config?.winCondition;
|
const winCond = currentScene?.config?.winCondition;
|
||||||
if (winCond?.type === "timer") {
|
if (winCond?.type === "timer") {
|
||||||
const duration = winCond.durationSec ?? 120;
|
const duration = winCond.durationSec ?? 120;
|
||||||
@@ -571,143 +562,6 @@
|
|||||||
checkWinCondition();
|
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 = () => {
|
const updateHud = () => {
|
||||||
ui.updateHud({
|
ui.updateHud({
|
||||||
spawnIntervalMs: config.spawnIntervalMs,
|
spawnIntervalMs: config.spawnIntervalMs,
|
||||||
@@ -722,6 +576,25 @@
|
|||||||
goals.maybeAnnounceGoalProgress(goal);
|
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 = () => {
|
const buildLegend = () => {
|
||||||
ui.buildLegend(config.palette);
|
ui.buildLegend(config.palette);
|
||||||
};
|
};
|
||||||
@@ -743,7 +616,7 @@
|
|||||||
Body.setVelocity(ball, { x: 0, y: 0 });
|
Body.setVelocity(ball, { x: 0, y: 0 });
|
||||||
});
|
});
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
endDrag();
|
input?.endDrag();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user