Extract chain and loop controllers
This commit is contained in:
209
src/chain-controller.js
Normal file
209
src/chain-controller.js
Normal file
@@ -0,0 +1,209 @@
|
||||
(() => {
|
||||
const { Constraint, Vector, World } = Matter;
|
||||
|
||||
const create = ({
|
||||
config,
|
||||
state,
|
||||
chain,
|
||||
world,
|
||||
spawnSystem,
|
||||
getCurrentScene,
|
||||
normalizeColor,
|
||||
saveHighScore,
|
||||
saveLongestChain,
|
||||
updateHud,
|
||||
checkWinCondition,
|
||||
ui,
|
||||
}) => {
|
||||
const setHighlight = (body, on) => {
|
||||
body.render.lineWidth = on ? 4 : 2;
|
||||
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
|
||||
};
|
||||
|
||||
const resetChainVisuals = () => {
|
||||
chain.bodies.forEach((b) => setHighlight(b, false));
|
||||
chain.constraints.forEach((c) => World.remove(world, c));
|
||||
chain.active = false;
|
||||
chain.color = null;
|
||||
chain.bodies = [];
|
||||
chain.constraints = [];
|
||||
chain.pointer = null;
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const removeLastFromChain = () => {
|
||||
const removedConstraint = chain.constraints.pop();
|
||||
if (removedConstraint) {
|
||||
World.remove(world, removedConstraint);
|
||||
}
|
||||
const removedBody = chain.bodies.pop();
|
||||
if (removedBody) setHighlight(removedBody, false);
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const addToChain = (body) => {
|
||||
const last = chain.bodies[chain.bodies.length - 1];
|
||||
const dist = Vector.magnitude(Vector.sub(last.position, body.position));
|
||||
const linkCfg = config.link || {};
|
||||
const constraint = Constraint.create({
|
||||
bodyA: last,
|
||||
bodyB: body,
|
||||
length: dist * (linkCfg.lengthScale ?? 1),
|
||||
stiffness: linkCfg.stiffness ?? 0.9,
|
||||
damping: linkCfg.damping ?? 0,
|
||||
render: {
|
||||
strokeStyle: chain.color,
|
||||
lineWidth: linkCfg.lineWidth ?? 3,
|
||||
type: linkCfg.renderType || "line",
|
||||
},
|
||||
});
|
||||
constraint.plugin = {
|
||||
restLength: dist * (linkCfg.lengthScale ?? 1),
|
||||
rope: linkCfg.rope ?? false,
|
||||
baseStiffness: linkCfg.stiffness ?? 0.9,
|
||||
maxLength: dist * (linkCfg.lengthScale ?? 1),
|
||||
};
|
||||
chain.constraints.push(constraint);
|
||||
chain.bodies.push(body);
|
||||
setHighlight(body, true);
|
||||
World.add(world, constraint);
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const getChainScoreState = () => {
|
||||
const baseGain = 10 * Math.pow(chain.bodies.length, 2);
|
||||
const currentScene = getCurrentScene();
|
||||
const negativeColors = (
|
||||
currentScene?.config?.negativeScoreColors || []
|
||||
).map(normalizeColor);
|
||||
const negativeProgressColors = (
|
||||
currentScene?.config?.negativeProgressColors || []
|
||||
).map(normalizeColor);
|
||||
const normalizedColor = chain.color
|
||||
? normalizeColor(chain.color || "")
|
||||
: null;
|
||||
const isNegative =
|
||||
normalizedColor && negativeColors.includes(normalizedColor);
|
||||
const isNegativeProgress =
|
||||
normalizedColor && negativeProgressColors.includes(normalizedColor);
|
||||
const gain = isNegative ? -baseGain : baseGain;
|
||||
return { gain, isNegativeProgress };
|
||||
};
|
||||
|
||||
const removeChainConstraints = () => {
|
||||
chain.constraints.forEach((c) => World.remove(world, c));
|
||||
};
|
||||
|
||||
const removeChainBodies = () => {
|
||||
const blobIds = new Set();
|
||||
chain.bodies.forEach((body) => {
|
||||
if (body.plugin?.blobId) {
|
||||
blobIds.add(body.plugin.blobId);
|
||||
} else {
|
||||
if (body.plugin?.color) {
|
||||
const key = normalizeColor(body.plugin.color);
|
||||
state.clearedByColor[key] = (state.clearedByColor[key] || 0) + 1;
|
||||
}
|
||||
spawnSystem.cleanupBall(body);
|
||||
World.remove(world, body);
|
||||
}
|
||||
});
|
||||
blobIds.forEach((id) => {
|
||||
state.balls
|
||||
.filter((b) => b.plugin?.blobId === id)
|
||||
.forEach((b) => {
|
||||
if (b.plugin?.color) {
|
||||
const key = normalizeColor(b.plugin.color);
|
||||
state.clearedByColor[key] = (state.clearedByColor[key] || 0) + 1;
|
||||
}
|
||||
});
|
||||
spawnSystem.removeBlob(id);
|
||||
});
|
||||
for (let i = state.balls.length - 1; i >= 0; i -= 1) {
|
||||
if (
|
||||
chain.bodies.includes(state.balls[i]) ||
|
||||
(state.balls[i].plugin?.blobId &&
|
||||
blobIds.has(state.balls[i].plugin?.blobId))
|
||||
) {
|
||||
state.balls.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyNegativeProgressPenalty = (chainLength) => {
|
||||
const currentScene = getCurrentScene();
|
||||
const winCond = currentScene?.config?.winCondition;
|
||||
if (winCond?.type !== "colorClear" || !Array.isArray(winCond.targets)) {
|
||||
return;
|
||||
}
|
||||
winCond.targets.forEach((target) => {
|
||||
const key = normalizeColor(target.color);
|
||||
const current = state.clearedByColor[key] || 0;
|
||||
state.clearedByColor[key] = Math.max(0, current - chainLength);
|
||||
});
|
||||
};
|
||||
|
||||
const updateLongestChain = (chainLength) => {
|
||||
if (chainLength <= state.longestChainRecord) return;
|
||||
const currentScene = getCurrentScene();
|
||||
state.longestChainRecord = chainLength;
|
||||
saveLongestChain(currentScene?.id, state.longestChainRecord);
|
||||
console.log(
|
||||
"New longest chain record",
|
||||
chainLength,
|
||||
"scene",
|
||||
currentScene?.id,
|
||||
);
|
||||
ui.showFloatingMessage(`New chain record: ${chainLength}`, {
|
||||
durationMs: 3600,
|
||||
position: config.messages.position,
|
||||
});
|
||||
};
|
||||
|
||||
const resetChainState = () => {
|
||||
chain.active = false;
|
||||
chain.color = null;
|
||||
chain.bodies = [];
|
||||
chain.constraints = [];
|
||||
chain.pointer = null;
|
||||
updateHud();
|
||||
checkWinCondition();
|
||||
};
|
||||
|
||||
const finishChain = (releasePoint) => {
|
||||
if (!chain.active || state.gameOver || state.paused) return;
|
||||
const chainLength = chain.bodies.length;
|
||||
const currentScene = getCurrentScene();
|
||||
if (chainLength >= config.minChain) {
|
||||
updateLongestChain(chainLength);
|
||||
const { gain, isNegativeProgress } = getChainScoreState();
|
||||
state.score += gain;
|
||||
state.clearedCount += chainLength;
|
||||
if (state.score > state.highScore) {
|
||||
state.highScore = state.score;
|
||||
saveHighScore(currentScene?.id, state.highScore);
|
||||
}
|
||||
ui.spawnScorePopup(releasePoint || chain.pointer, gain, chain.color);
|
||||
removeChainConstraints();
|
||||
removeChainBodies();
|
||||
if (isNegativeProgress) {
|
||||
applyNegativeProgressPenalty(chainLength);
|
||||
}
|
||||
} else {
|
||||
removeChainConstraints();
|
||||
chain.bodies.forEach((b) => setHighlight(b, false));
|
||||
}
|
||||
resetChainState();
|
||||
};
|
||||
|
||||
return {
|
||||
addToChain,
|
||||
finishChain,
|
||||
removeLastFromChain,
|
||||
resetChainVisuals,
|
||||
setHighlight,
|
||||
};
|
||||
};
|
||||
|
||||
window.PhysilinksChainController = { create };
|
||||
})();
|
||||
Reference in New Issue
Block a user