Centralize mutable game state
This commit is contained in:
264
src/main.js
264
src/main.js
@@ -77,16 +77,34 @@
|
|||||||
const ui = createUI();
|
const ui = createUI();
|
||||||
const { sceneEl } = ui;
|
const { sceneEl } = ui;
|
||||||
|
|
||||||
let width = sceneEl.clientWidth;
|
const state = {
|
||||||
let height = sceneEl.clientHeight;
|
width: sceneEl.clientWidth,
|
||||||
|
height: sceneEl.clientHeight,
|
||||||
|
boundaries: [],
|
||||||
|
rotators: [],
|
||||||
|
oscillators: [],
|
||||||
|
balls: [],
|
||||||
|
blobConstraints: new Map(),
|
||||||
|
score: 0,
|
||||||
|
highScore: 0,
|
||||||
|
longestChainRecord: 0,
|
||||||
|
clearedCount: 0,
|
||||||
|
clearedByColor: {},
|
||||||
|
gameOver: false,
|
||||||
|
paused: false,
|
||||||
|
levelWon: false,
|
||||||
|
timerEndMs: null,
|
||||||
|
lastTimerDisplay: null,
|
||||||
|
};
|
||||||
|
|
||||||
const BALL_BASELINE = 680; // reference height used for relative ball sizing
|
const BALL_BASELINE = 680; // reference height used for relative ball sizing
|
||||||
|
|
||||||
const createEngine = getFactory("PhysilinksEngine");
|
const createEngine = getFactory("PhysilinksEngine");
|
||||||
const { engine, render, runner, startRunner, stopRunner, setRenderSize } =
|
const { engine, render, runner, startRunner, stopRunner, setRenderSize } =
|
||||||
createEngine({
|
createEngine({
|
||||||
sceneEl,
|
sceneEl,
|
||||||
width,
|
width: state.width,
|
||||||
height,
|
height: state.height,
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
wireframes: false,
|
wireframes: false,
|
||||||
showAngleIndicator: false,
|
showAngleIndicator: false,
|
||||||
@@ -96,10 +114,6 @@
|
|||||||
engine.gravity.y = config.gravity;
|
engine.gravity.y = config.gravity;
|
||||||
const world = engine.world;
|
const world = engine.world;
|
||||||
|
|
||||||
// Static boundaries and scene-specific obstacles.
|
|
||||||
let boundaries = [];
|
|
||||||
let rotators = [];
|
|
||||||
let oscillators = [];
|
|
||||||
const initialSceneId =
|
const initialSceneId =
|
||||||
(getSceneIdFromUrl && getSceneIdFromUrl(scenes)) ||
|
(getSceneIdFromUrl && getSceneIdFromUrl(scenes)) ||
|
||||||
(getSceneById && getSceneById(scenes, defaultSceneId)
|
(getSceneById && getSceneById(scenes, defaultSceneId)
|
||||||
@@ -114,27 +128,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rebuildSceneBodies = () => {
|
const rebuildSceneBodies = () => {
|
||||||
boundaries.forEach((b) => World.remove(world, b));
|
state.boundaries.forEach((b) => World.remove(world, b));
|
||||||
const nextBoundaries = currentScene.createBodies(width, height);
|
const nextBoundaries = currentScene.createBodies(state.width, state.height);
|
||||||
boundaries.length = 0;
|
state.boundaries.length = 0;
|
||||||
boundaries.push(...nextBoundaries);
|
state.boundaries.push(...nextBoundaries);
|
||||||
rotators = boundaries.filter((b) => b.plugin && b.plugin.rotSpeed);
|
state.rotators = state.boundaries.filter(
|
||||||
oscillators = boundaries.filter((b) => b.plugin && b.plugin.oscillate);
|
(b) => b.plugin && b.plugin.rotSpeed,
|
||||||
World.add(world, boundaries);
|
);
|
||||||
|
state.oscillators = state.boundaries.filter(
|
||||||
|
(b) => b.plugin && b.plugin.oscillate,
|
||||||
|
);
|
||||||
|
World.add(world, state.boundaries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const balls = [];
|
|
||||||
const blobConstraints = new Map();
|
|
||||||
let score = 0;
|
|
||||||
let highScore = 0;
|
|
||||||
let longestChainRecord = 0;
|
|
||||||
let clearedCount = 0;
|
|
||||||
let clearedByColor = {};
|
|
||||||
let gameOver = false;
|
|
||||||
let isPaused = false;
|
|
||||||
let levelWon = false;
|
|
||||||
let timerEndMs = null;
|
|
||||||
let lastTimerDisplay = null;
|
|
||||||
let spawnSystem = null;
|
let spawnSystem = null;
|
||||||
let input = null;
|
let input = null;
|
||||||
|
|
||||||
@@ -184,11 +189,11 @@
|
|||||||
setConfigForScene(next.config);
|
setConfigForScene(next.config);
|
||||||
ui.setMessageDefaults(config.messages);
|
ui.setMessageDefaults(config.messages);
|
||||||
resetEngineForScene(next.config, { prevRadius });
|
resetEngineForScene(next.config, { prevRadius });
|
||||||
clearedCount = 0;
|
state.clearedCount = 0;
|
||||||
levelWon = false;
|
state.levelWon = false;
|
||||||
clearedByColor = {};
|
state.clearedByColor = {};
|
||||||
highScore = loadHighScore(next.id);
|
state.highScore = loadHighScore(next.id);
|
||||||
longestChainRecord = loadLongestChain(next.id);
|
state.longestChainRecord = loadLongestChain(next.id);
|
||||||
rebuildSceneBodies();
|
rebuildSceneBodies();
|
||||||
buildLegend();
|
buildLegend();
|
||||||
restartGame();
|
restartGame();
|
||||||
@@ -217,68 +222,68 @@
|
|||||||
|
|
||||||
const triggerGameOver = () => {
|
const triggerGameOver = () => {
|
||||||
if (currentScene?.config?.noGameOver) return;
|
if (currentScene?.config?.noGameOver) return;
|
||||||
if (gameOver) return;
|
if (state.gameOver) return;
|
||||||
gameOver = true;
|
state.gameOver = true;
|
||||||
isPaused = false;
|
state.paused = false;
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
spawnSystem?.stopSpawner();
|
spawnSystem?.stopSpawner();
|
||||||
stopRunner();
|
stopRunner();
|
||||||
engine.timing.timeScale = 0;
|
engine.timing.timeScale = 0;
|
||||||
ui.setPauseState(false);
|
ui.setPauseState(false);
|
||||||
ui.showGameOver(score);
|
ui.showGameOver(state.score);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSpawn = getFactory("PhysilinksSpawn");
|
const createSpawn = getFactory("PhysilinksSpawn");
|
||||||
spawnSystem = createSpawn({
|
spawnSystem = createSpawn({
|
||||||
config,
|
config,
|
||||||
world,
|
world,
|
||||||
balls,
|
balls: state.balls,
|
||||||
blobConstraints,
|
blobConstraints: state.blobConstraints,
|
||||||
getCurrentScene: () => currentScene,
|
getCurrentScene: () => currentScene,
|
||||||
getDimensions: () => ({ width, height }),
|
getDimensions: () => ({ width: state.width, height: state.height }),
|
||||||
isGridScene,
|
isGridScene,
|
||||||
triggerGameOver,
|
triggerGameOver,
|
||||||
isGameOver: () => gameOver,
|
isGameOver: () => state.gameOver,
|
||||||
ballBaseline: BALL_BASELINE,
|
ballBaseline: BALL_BASELINE,
|
||||||
});
|
});
|
||||||
const createGoals = getFactory("PhysilinksGoals");
|
const createGoals = getFactory("PhysilinksGoals");
|
||||||
const goals = createGoals({
|
const goals = createGoals({
|
||||||
config,
|
config,
|
||||||
getCurrentScene: () => currentScene,
|
getCurrentScene: () => currentScene,
|
||||||
getScore: () => score,
|
getScore: () => state.score,
|
||||||
getClearedCount: () => clearedCount,
|
getClearedCount: () => state.clearedCount,
|
||||||
getClearedByColor: () => clearedByColor,
|
getClearedByColor: () => state.clearedByColor,
|
||||||
getTimerEndMs: () => timerEndMs,
|
getTimerEndMs: () => state.timerEndMs,
|
||||||
ui,
|
ui,
|
||||||
});
|
});
|
||||||
const normalizeColor = goals.normalizeColor;
|
const normalizeColor = goals.normalizeColor;
|
||||||
|
|
||||||
const restartGame = () => {
|
const restartGame = () => {
|
||||||
spawnSystem.stopSpawner();
|
spawnSystem.stopSpawner();
|
||||||
gameOver = false;
|
state.gameOver = false;
|
||||||
isPaused = false;
|
state.paused = false;
|
||||||
levelWon = false;
|
state.levelWon = false;
|
||||||
spawnSystem.resetSpawnState();
|
spawnSystem.resetSpawnState();
|
||||||
score = 0;
|
state.score = 0;
|
||||||
clearedCount = 0;
|
state.clearedCount = 0;
|
||||||
clearedByColor = {};
|
state.clearedByColor = {};
|
||||||
goals.resetMilestones();
|
goals.resetMilestones();
|
||||||
input?.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;
|
||||||
timerEndMs = Date.now() + duration * 1000;
|
state.timerEndMs = Date.now() + duration * 1000;
|
||||||
lastTimerDisplay = null;
|
state.lastTimerDisplay = null;
|
||||||
} else {
|
} else {
|
||||||
timerEndMs = null;
|
state.timerEndMs = null;
|
||||||
lastTimerDisplay = null;
|
state.lastTimerDisplay = null;
|
||||||
}
|
}
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
balls.forEach((ball) => {
|
state.balls.forEach((ball) => {
|
||||||
spawnSystem.cleanupBall(ball);
|
spawnSystem.cleanupBall(ball);
|
||||||
World.remove(world, ball);
|
World.remove(world, ball);
|
||||||
});
|
});
|
||||||
balls.length = 0;
|
state.balls.length = 0;
|
||||||
ui.hideGameOver();
|
ui.hideGameOver();
|
||||||
ui.hideWin();
|
ui.hideWin();
|
||||||
ui.setPauseState(false);
|
ui.setPauseState(false);
|
||||||
@@ -305,12 +310,12 @@
|
|||||||
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
|
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPaused = (state) => {
|
const setPaused = (nextState) => {
|
||||||
if (gameOver || levelWon) return;
|
if (state.gameOver || state.levelWon) return;
|
||||||
if (state === isPaused) return;
|
if (nextState === state.paused) return;
|
||||||
isPaused = state;
|
state.paused = nextState;
|
||||||
ui.setPauseState(isPaused);
|
ui.setPauseState(state.paused);
|
||||||
if (isPaused) {
|
if (state.paused) {
|
||||||
resetChainVisuals();
|
resetChainVisuals();
|
||||||
spawnSystem.stopSpawner();
|
spawnSystem.stopSpawner();
|
||||||
stopRunner();
|
stopRunner();
|
||||||
@@ -345,7 +350,7 @@
|
|||||||
engine.gravity.y = winCond.onWin.setGravity;
|
engine.gravity.y = winCond.onWin.setGravity;
|
||||||
}
|
}
|
||||||
if (winCond.onWin.shoveBalls) {
|
if (winCond.onWin.shoveBalls) {
|
||||||
balls.forEach((ball) => {
|
state.balls.forEach((ball) => {
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
const magnitude = 12 + Math.random() * 10;
|
const magnitude = 12 + Math.random() * 10;
|
||||||
const force = {
|
const force = {
|
||||||
@@ -356,7 +361,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (winCond.onWin.swirlBalls) {
|
if (winCond.onWin.swirlBalls) {
|
||||||
balls.forEach((ball) => {
|
state.balls.forEach((ball) => {
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
const mag = 0.06;
|
const mag = 0.06;
|
||||||
Body.applyForce(ball, ball.position, {
|
Body.applyForce(ball, ball.position, {
|
||||||
@@ -368,27 +373,31 @@
|
|||||||
}
|
}
|
||||||
if (winCond.onWin.removeCurves) {
|
if (winCond.onWin.removeCurves) {
|
||||||
const remaining = [];
|
const remaining = [];
|
||||||
boundaries.forEach((b) => {
|
state.boundaries.forEach((b) => {
|
||||||
if (b.plugin && b.plugin.curve) {
|
if (b.plugin && b.plugin.curve) {
|
||||||
World.remove(world, b);
|
World.remove(world, b);
|
||||||
} else {
|
} else {
|
||||||
remaining.push(b);
|
remaining.push(b);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
boundaries.length = 0;
|
state.boundaries.length = 0;
|
||||||
boundaries.push(...remaining);
|
state.boundaries.push(...remaining);
|
||||||
rotators = rotators.filter((b) => boundaries.includes(b));
|
state.rotators = state.rotators.filter((b) =>
|
||||||
oscillators = oscillators.filter((b) => boundaries.includes(b));
|
state.boundaries.includes(b),
|
||||||
|
);
|
||||||
|
state.oscillators = state.oscillators.filter((b) =>
|
||||||
|
state.boundaries.includes(b),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkWinCondition = () => {
|
const checkWinCondition = () => {
|
||||||
if (levelWon) return;
|
if (state.levelWon) return;
|
||||||
const goal = goals.getGoalState();
|
const goal = goals.getGoalState();
|
||||||
ui.setGoal(goal || { label: "—", progress: 0 });
|
ui.setGoal(goal || { label: "—", progress: 0 });
|
||||||
if (!goal || !goal.met) return;
|
if (!goal || !goal.met) return;
|
||||||
applyWinEffects();
|
applyWinEffects();
|
||||||
levelWon = true;
|
state.levelWon = true;
|
||||||
spawnSystem.stopSpawner();
|
spawnSystem.stopSpawner();
|
||||||
engine.timing.timeScale = 1;
|
engine.timing.timeScale = 1;
|
||||||
startRunner();
|
startRunner();
|
||||||
@@ -436,9 +445,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateLongestChain = (chainLength) => {
|
const updateLongestChain = (chainLength) => {
|
||||||
if (chainLength <= longestChainRecord) return;
|
if (chainLength <= state.longestChainRecord) return;
|
||||||
longestChainRecord = chainLength;
|
state.longestChainRecord = chainLength;
|
||||||
saveLongestChain(currentScene.id, longestChainRecord);
|
saveLongestChain(currentScene.id, state.longestChainRecord);
|
||||||
console.log(
|
console.log(
|
||||||
"New longest chain record",
|
"New longest chain record",
|
||||||
chainLength,
|
chainLength,
|
||||||
@@ -482,29 +491,30 @@
|
|||||||
} else {
|
} else {
|
||||||
if (body.plugin?.color) {
|
if (body.plugin?.color) {
|
||||||
const key = normalizeColor(body.plugin.color);
|
const key = normalizeColor(body.plugin.color);
|
||||||
clearedByColor[key] = (clearedByColor[key] || 0) + 1;
|
state.clearedByColor[key] = (state.clearedByColor[key] || 0) + 1;
|
||||||
}
|
}
|
||||||
spawnSystem.cleanupBall(body);
|
spawnSystem.cleanupBall(body);
|
||||||
World.remove(world, body);
|
World.remove(world, body);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
blobIds.forEach((id) => {
|
blobIds.forEach((id) => {
|
||||||
balls
|
state.balls
|
||||||
.filter((b) => b.plugin?.blobId === id)
|
.filter((b) => b.plugin?.blobId === id)
|
||||||
.forEach((b) => {
|
.forEach((b) => {
|
||||||
if (b.plugin?.color) {
|
if (b.plugin?.color) {
|
||||||
const key = normalizeColor(b.plugin.color);
|
const key = normalizeColor(b.plugin.color);
|
||||||
clearedByColor[key] = (clearedByColor[key] || 0) + 1;
|
state.clearedByColor[key] = (state.clearedByColor[key] || 0) + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
spawnSystem.removeBlob(id);
|
spawnSystem.removeBlob(id);
|
||||||
});
|
});
|
||||||
for (let i = balls.length - 1; i >= 0; i -= 1) {
|
for (let i = state.balls.length - 1; i >= 0; i -= 1) {
|
||||||
if (
|
if (
|
||||||
chain.bodies.includes(balls[i]) ||
|
chain.bodies.includes(state.balls[i]) ||
|
||||||
(balls[i].plugin?.blobId && blobIds.has(balls[i].plugin?.blobId))
|
(state.balls[i].plugin?.blobId &&
|
||||||
|
blobIds.has(state.balls[i].plugin?.blobId))
|
||||||
) {
|
) {
|
||||||
balls.splice(i, 1);
|
state.balls.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -516,8 +526,8 @@
|
|||||||
}
|
}
|
||||||
winCond.targets.forEach((target) => {
|
winCond.targets.forEach((target) => {
|
||||||
const key = normalizeColor(target.color);
|
const key = normalizeColor(target.color);
|
||||||
const current = clearedByColor[key] || 0;
|
const current = state.clearedByColor[key] || 0;
|
||||||
clearedByColor[key] = Math.max(0, current - chainLength);
|
state.clearedByColor[key] = Math.max(0, current - chainLength);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -532,16 +542,16 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const finishChain = (releasePoint) => {
|
const finishChain = (releasePoint) => {
|
||||||
if (!chain.active || gameOver || isPaused) return;
|
if (!chain.active || state.gameOver || state.paused) return;
|
||||||
const chainLength = chain.bodies.length;
|
const chainLength = chain.bodies.length;
|
||||||
if (chainLength >= config.minChain) {
|
if (chainLength >= config.minChain) {
|
||||||
updateLongestChain(chainLength);
|
updateLongestChain(chainLength);
|
||||||
const { gain, isNegativeProgress } = getChainScoreState();
|
const { gain, isNegativeProgress } = getChainScoreState();
|
||||||
score += gain;
|
state.score += gain;
|
||||||
clearedCount += chainLength;
|
state.clearedCount += chainLength;
|
||||||
if (score > highScore) {
|
if (state.score > state.highScore) {
|
||||||
highScore = score;
|
state.highScore = state.score;
|
||||||
saveHighScore(currentScene.id, highScore);
|
saveHighScore(currentScene.id, state.highScore);
|
||||||
}
|
}
|
||||||
ui.spawnScorePopup(releasePoint || chain.pointer, gain, chain.color);
|
ui.spawnScorePopup(releasePoint || chain.pointer, gain, chain.color);
|
||||||
removeChainConstraints();
|
removeChainConstraints();
|
||||||
@@ -561,8 +571,8 @@
|
|||||||
spawnIntervalMs: config.spawnIntervalMs,
|
spawnIntervalMs: config.spawnIntervalMs,
|
||||||
minChain: config.minChain,
|
minChain: config.minChain,
|
||||||
chainLength: chain.bodies.length,
|
chainLength: chain.bodies.length,
|
||||||
score,
|
score: state.score,
|
||||||
highScore,
|
highScore: state.highScore,
|
||||||
activeColor: chain.color,
|
activeColor: chain.color,
|
||||||
});
|
});
|
||||||
const goal = goals.getGoalState();
|
const goal = goals.getGoalState();
|
||||||
@@ -574,14 +584,14 @@
|
|||||||
input = createInput({
|
input = createInput({
|
||||||
render,
|
render,
|
||||||
world,
|
world,
|
||||||
balls,
|
balls: state.balls,
|
||||||
boundaries,
|
boundaries: state.boundaries,
|
||||||
chain,
|
chain,
|
||||||
config,
|
config,
|
||||||
getCurrentScene: () => currentScene,
|
getCurrentScene: () => currentScene,
|
||||||
isPaused: () => isPaused,
|
isPaused: () => state.paused,
|
||||||
isLevelWon: () => levelWon,
|
isLevelWon: () => state.levelWon,
|
||||||
isGameOver: () => gameOver,
|
isGameOver: () => state.gameOver,
|
||||||
getMaxLinkDistance,
|
getMaxLinkDistance,
|
||||||
setHighlight,
|
setHighlight,
|
||||||
removeLastFromChain,
|
removeLastFromChain,
|
||||||
@@ -595,17 +605,17 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clampBodiesIntoView = (prevWidth, prevHeight) => {
|
const clampBodiesIntoView = (prevWidth, prevHeight) => {
|
||||||
const scaleX = width / (prevWidth || width);
|
const scaleX = state.width / (prevWidth || state.width);
|
||||||
const scaleY = height / (prevHeight || height);
|
const scaleY = state.height / (prevHeight || state.height);
|
||||||
const margin = config.ballRadius * 1.2;
|
const margin = config.ballRadius * 1.2;
|
||||||
balls.forEach((ball) => {
|
state.balls.forEach((ball) => {
|
||||||
const nextX = Math.min(
|
const nextX = Math.min(
|
||||||
Math.max(ball.position.x * scaleX, margin),
|
Math.max(ball.position.x * scaleX, margin),
|
||||||
Math.max(margin, width - margin),
|
Math.max(margin, state.width - margin),
|
||||||
);
|
);
|
||||||
const nextY = Math.min(
|
const nextY = Math.min(
|
||||||
ball.position.y * scaleY,
|
ball.position.y * scaleY,
|
||||||
Math.max(height - margin, margin),
|
Math.max(state.height - margin, margin),
|
||||||
);
|
);
|
||||||
Body.setPosition(ball, { x: nextX, y: nextY });
|
Body.setPosition(ball, { x: nextX, y: nextY });
|
||||||
Body.setVelocity(ball, { x: 0, y: 0 });
|
Body.setVelocity(ball, { x: 0, y: 0 });
|
||||||
@@ -615,12 +625,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const prevWidth = width;
|
const prevWidth = state.width;
|
||||||
const prevHeight = height;
|
const prevHeight = state.height;
|
||||||
const prevRadius = config.ballRadius;
|
const prevRadius = config.ballRadius;
|
||||||
width = sceneEl.clientWidth;
|
state.width = sceneEl.clientWidth;
|
||||||
height = sceneEl.clientHeight;
|
state.height = sceneEl.clientHeight;
|
||||||
setRenderSize(width, height);
|
setRenderSize(state.width, state.height);
|
||||||
spawnSystem.updateBallRadius(prevRadius);
|
spawnSystem.updateBallRadius(prevRadius);
|
||||||
clampBodiesIntoView(prevWidth, prevHeight);
|
clampBodiesIntoView(prevWidth, prevHeight);
|
||||||
rebuildSceneBodies();
|
rebuildSceneBodies();
|
||||||
@@ -628,7 +638,7 @@
|
|||||||
|
|
||||||
Events.on(engine, "afterUpdate", () => {
|
Events.on(engine, "afterUpdate", () => {
|
||||||
// Keep stray balls within the play area horizontally.
|
// Keep stray balls within the play area horizontally.
|
||||||
balls.forEach((ball) => {
|
state.balls.forEach((ball) => {
|
||||||
if (
|
if (
|
||||||
!ball.plugin.hasEntered &&
|
!ball.plugin.hasEntered &&
|
||||||
ball.position.y > config.ballRadius * 1.5
|
ball.position.y > config.ballRadius * 1.5
|
||||||
@@ -637,10 +647,10 @@
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
ball.position.x < -100 ||
|
ball.position.x < -100 ||
|
||||||
ball.position.x > width + 100 ||
|
ball.position.x > state.width + 100 ||
|
||||||
(currentScene?.config?.spawnFrom === "bottom"
|
(currentScene?.config?.spawnFrom === "bottom"
|
||||||
? ball.position.y < -500
|
? ball.position.y < -500
|
||||||
: ball.position.y > height + 500)
|
: ball.position.y > state.height + 500)
|
||||||
) {
|
) {
|
||||||
spawnSystem.cleanupBall(ball);
|
spawnSystem.cleanupBall(ball);
|
||||||
ball.plugin.hasEntered = true;
|
ball.plugin.hasEntered = true;
|
||||||
@@ -648,13 +658,13 @@
|
|||||||
Matter.Body.setPosition(ball, {
|
Matter.Body.setPosition(ball, {
|
||||||
x:
|
x:
|
||||||
currentScene?.config?.spawnOrigin === "center"
|
currentScene?.config?.spawnOrigin === "center"
|
||||||
? width / 2
|
? state.width / 2
|
||||||
: Math.random() * width,
|
: Math.random() * state.width,
|
||||||
y:
|
y:
|
||||||
currentScene?.config?.spawnOrigin === "center"
|
currentScene?.config?.spawnOrigin === "center"
|
||||||
? height / 2
|
? state.height / 2
|
||||||
: spawnFromBottom
|
: spawnFromBottom
|
||||||
? height + 40
|
? state.height + 40
|
||||||
: -40,
|
: -40,
|
||||||
});
|
});
|
||||||
Matter.Body.setVelocity(ball, { x: 0, y: 0 });
|
Matter.Body.setVelocity(ball, { x: 0, y: 0 });
|
||||||
@@ -665,10 +675,14 @@
|
|||||||
Events.on(engine, "beforeUpdate", () => {
|
Events.on(engine, "beforeUpdate", () => {
|
||||||
// Rope-like constraint handling: allow shortening without push-back, tension when stretched.
|
// Rope-like constraint handling: allow shortening without push-back, tension when stretched.
|
||||||
if (
|
if (
|
||||||
!levelWon &&
|
!state.levelWon &&
|
||||||
typeof currentScene?.config?.onBeforeUpdate === "function"
|
typeof currentScene?.config?.onBeforeUpdate === "function"
|
||||||
) {
|
) {
|
||||||
currentScene.config.onBeforeUpdate({ engine, width, height });
|
currentScene.config.onBeforeUpdate({
|
||||||
|
engine,
|
||||||
|
width: state.width,
|
||||||
|
height: state.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
chain.constraints.forEach((c) => {
|
chain.constraints.forEach((c) => {
|
||||||
if (!c.plugin || !c.plugin.rope) return;
|
if (!c.plugin || !c.plugin.rope) return;
|
||||||
@@ -687,14 +701,14 @@
|
|||||||
// Rotate any scene rotators slowly.
|
// Rotate any scene rotators slowly.
|
||||||
const dt = (engine.timing && engine.timing.delta) || 16;
|
const dt = (engine.timing && engine.timing.delta) || 16;
|
||||||
const timeScale = engine.timing?.timeScale ?? 1;
|
const timeScale = engine.timing?.timeScale ?? 1;
|
||||||
if (isPaused || gameOver || timeScale === 0) return;
|
if (state.paused || state.gameOver || timeScale === 0) return;
|
||||||
rotators.forEach((b) => {
|
state.rotators.forEach((b) => {
|
||||||
const speed = b.plugin.rotSpeed || 0;
|
const speed = b.plugin.rotSpeed || 0;
|
||||||
if (speed !== 0) {
|
if (speed !== 0) {
|
||||||
Body.rotate(b, speed * ((dt * timeScale) / 1000));
|
Body.rotate(b, speed * ((dt * timeScale) / 1000));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
oscillators.forEach((b) => {
|
state.oscillators.forEach((b) => {
|
||||||
const osc = b.plugin.oscillate;
|
const osc = b.plugin.oscillate;
|
||||||
if (!osc) return;
|
if (!osc) return;
|
||||||
if (!osc.base) {
|
if (!osc.base) {
|
||||||
@@ -712,17 +726,17 @@
|
|||||||
Body.setPosition(b, target);
|
Body.setPosition(b, target);
|
||||||
Body.setVelocity(b, { x: 0, y: 0 });
|
Body.setVelocity(b, { x: 0, y: 0 });
|
||||||
});
|
});
|
||||||
if (timerEndMs) {
|
if (state.timerEndMs) {
|
||||||
const winCond = currentScene?.config?.winCondition;
|
const winCond = currentScene?.config?.winCondition;
|
||||||
const duration = winCond?.durationSec ?? 120;
|
const duration = winCond?.durationSec ?? 120;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const remainingMs = Math.max(0, timerEndMs - now);
|
const remainingMs = Math.max(0, state.timerEndMs - now);
|
||||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||||
if (lastTimerDisplay !== remainingSec) {
|
if (state.lastTimerDisplay !== remainingSec) {
|
||||||
lastTimerDisplay = remainingSec;
|
state.lastTimerDisplay = remainingSec;
|
||||||
updateHud();
|
updateHud();
|
||||||
}
|
}
|
||||||
if (remainingMs <= 0 && !levelWon) {
|
if (remainingMs <= 0 && !state.levelWon) {
|
||||||
checkWinCondition();
|
checkWinCondition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -761,7 +775,7 @@
|
|||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
ui.setHandlers({
|
ui.setHandlers({
|
||||||
onPauseToggle: () => setPaused(!isPaused),
|
onPauseToggle: () => setPaused(!state.paused),
|
||||||
onRestart: restartGame,
|
onRestart: restartGame,
|
||||||
onSceneChange: (id) => applyScene(id),
|
onSceneChange: (id) => applyScene(id),
|
||||||
onWinNext: () =>
|
onWinNext: () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user