Extract chain and loop controllers

This commit is contained in:
Daddy32
2025-12-15 22:11:14 +01:00
parent 2a00974d44
commit 0ddab23808
4 changed files with 438 additions and 349 deletions

209
src/chain-controller.js Normal file
View 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 };
})();