Fix gravity reset and add spawn cap for swirl arena

This commit is contained in:
Daddy32
2025-12-13 23:21:27 +01:00
parent 91ed6c6202
commit ea2446e36b
5 changed files with 186 additions and 13 deletions

View File

@@ -100,6 +100,7 @@
<script src="./src/scenes/scene-fastdrop.js"></script>
<script src="./src/scenes/scene-lavalamp.js"></script>
<script src="./src/scenes/scene-relax.js"></script>
<script src="./src/scenes/scene-swirl-arena.js"></script>
<script src="./src/scenes/index.js"></script>
<script src="./src/ui.js"></script>
<script src="./src/main.js"></script>

View File

@@ -64,6 +64,8 @@
const BALL_BASELINE = 680; // reference height used for relative ball sizing
const engine = Engine.create();
const defaultGravityScale = engine.gravity.scale;
const defaultTimeScale = engine.timing.timeScale || 1;
engine.gravity.y = config.gravity;
const world = engine.world;
@@ -125,6 +127,7 @@
const balls = [];
const blobConstraints = new Map();
let spawnTimer = null;
let spawnCount = 0;
let score = 0;
let highScore = 0;
let clearedCount = 0;
@@ -165,7 +168,16 @@
const prevRadius = config.ballRadius;
Object.assign(config, next.config);
config.link = { ...next.config.link };
engine.gravity.scale =
typeof next.config.gravityScale === "number"
? next.config.gravityScale
: defaultGravityScale;
engine.timing.timeScale =
typeof next.config.timeScale === "number"
? next.config.timeScale
: defaultTimeScale;
updateBallRadius(prevRadius);
engine.gravity.x = 0;
engine.gravity.y = config.gravity;
clearedCount = 0;
levelWon = false;
@@ -221,16 +233,39 @@
const spawnBall = () => {
if (gameOver) return;
const spawnLimit = currentScene?.config?.spawnLimit;
if (Number.isFinite(spawnLimit) && spawnCount >= spawnLimit) {
stopSpawner();
return;
}
const color =
config.palette[Math.floor(Math.random() * config.palette.length)];
const x = Math.max(
const spawnOrigin = currentScene?.config?.spawnOrigin || "edge";
const spawnJitter =
currentScene?.config?.spawnJitter ?? config.ballRadius * 3;
const centerSpawn =
spawnOrigin === "center"
? {
x:
width / 2 +
(Math.random() - 0.5) * Math.max(spawnJitter, config.ballRadius),
y:
height / 2 +
(Math.random() - 0.5) * Math.max(spawnJitter, config.ballRadius),
}
: null;
const x =
centerSpawn?.x ??
Math.max(
config.ballRadius + 10,
Math.min(width - config.ballRadius - 10, Math.random() * width),
);
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
const y = spawnFromBottom
const y =
centerSpawn?.y ??
(spawnFromBottom
? height + config.ballRadius * 2
: -config.ballRadius * 2;
: -config.ballRadius * 2);
const batchMin = currentScene?.config?.spawnBatchMin ?? 1;
const batchMax = currentScene?.config?.spawnBatchMax ?? 1;
const batchCount =
@@ -276,6 +311,7 @@
World.add(world, blob.constraints);
}
}
spawnCount += batchCount;
};
const startSpawner = () => {
@@ -292,6 +328,14 @@
}
};
const spawnInitialBurst = () => {
const initialCount = currentScene?.config?.initialSpawnCount || 0;
if (!initialCount || initialCount <= 0) return;
for (let i = 0; i < initialCount; i += 1) {
spawnBall();
}
};
const cleanupBall = (ball) => {
if (ball.plugin && ball.plugin.entryCheckId) {
clearTimeout(ball.plugin.entryCheckId);
@@ -371,6 +415,7 @@
gameOver = false;
isPaused = false;
levelWon = false;
spawnCount = 0;
score = 0;
clearedCount = 0;
clearedByColor = {};
@@ -393,6 +438,7 @@
ui.hideGameOver();
ui.hideWin();
ui.setPauseState(false);
engine.gravity.x = 0;
engine.gravity.y = config.gravity;
engine.timing.timeScale = 1;
startRunner();
@@ -400,6 +446,7 @@
if (isGridScene()) {
spawnGridBalls();
} else {
spawnInitialBurst();
startSpawner();
}
};
@@ -534,7 +581,20 @@
const finishChain = (releasePoint) => {
if (!chain.active || gameOver || isPaused) return;
if (chain.bodies.length >= config.minChain) {
const gain = 10 * Math.pow(chain.bodies.length, 2);
const baseGain = 10 * Math.pow(chain.bodies.length, 2);
const negativeColors = (
currentScene?.config?.negativeScoreColors || []
).map(normalizeColor);
const negativeProgressColors = (
currentScene?.config?.negativeProgressColors || []
).map(normalizeColor);
const isNegative =
chain.color &&
negativeColors.includes(normalizeColor(chain.color || ""));
const isNegativeProgress =
chain.color &&
negativeProgressColors.includes(normalizeColor(chain.color || ""));
const gain = isNegative ? -baseGain : baseGain;
score += gain;
clearedCount += chain.bodies.length;
if (score > highScore) {
@@ -576,6 +636,16 @@
balls.splice(i, 1);
}
}
if (isNegativeProgress) {
const winCond = currentScene?.config?.winCondition;
if (winCond?.type === "colorClear" && Array.isArray(winCond.targets)) {
winCond.targets.forEach((target) => {
const key = normalizeColor(target.color);
const current = clearedByColor[key] || 0;
clearedByColor[key] = Math.max(0, current - chain.bodies.length);
});
}
}
} else {
chain.constraints.forEach((c) => World.remove(world, c));
chain.bodies.forEach((b) => setHighlight(b, false));
@@ -886,10 +956,11 @@
const totalTarget = targets.reduce((sum, t) => sum + t.count, 0);
let totalAchieved = 0;
const parts = targets.map((t) => {
const got = Math.min(
t.count,
const achieved = Math.max(
0,
clearedByColor[normalizeColor(t.color)] || 0,
);
const got = Math.min(t.count, achieved);
totalAchieved += got;
const remaining = Math.max(0, t.count - got);
return `${got}/${t.count} (${remaining} left)`;
@@ -972,8 +1043,16 @@
ball.plugin.hasEntered = true;
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
Matter.Body.setPosition(ball, {
x: Math.random() * width,
y: spawnFromBottom ? height + 40 : -40,
x:
currentScene?.config?.spawnOrigin === "center"
? width / 2
: Math.random() * width,
y:
currentScene?.config?.spawnOrigin === "center"
? height / 2
: spawnFromBottom
? height + 40
: -40,
});
Matter.Body.setVelocity(ball, { x: 0, y: 0 });
}
@@ -982,6 +1061,9 @@
Events.on(engine, "beforeUpdate", () => {
// Rope-like constraint handling: allow shortening without push-back, tension when stretched.
if (typeof currentScene?.config?.onBeforeUpdate === "function") {
currentScene.config.onBeforeUpdate({ engine, width, height });
}
chain.constraints.forEach((c) => {
if (!c.plugin || !c.plugin.rope) return;
const current = Vector.magnitude(

View File

@@ -6,6 +6,7 @@
"fast-drop-maze",
"balanced",
"scene-lava",
"swirl-arena",
"relax",
];
const orderedScenes = desiredOrder

View File

@@ -0,0 +1,88 @@
(() => {
const { Bodies } = Matter;
const scenes = (window.PhysilinksSceneDefs =
window.PhysilinksSceneDefs || []);
scenes.push({
id: "swirl-arena",
name: "Swirl Arena",
config: {
gravity: 0,
gravityScale: 0.0007,
timeScale: 0.9,
spawnIntervalMs: 520,
initialSpawnCount: 90,
spawnLimit: 500,
autoSpawn: true,
minChain: 3,
palette: ["#f472b6", "#22c55e"],
ballRadius: 20,
spawnOrigin: "center",
spawnJitter: 120,
blobBalls: false,
noGameOver: true,
winCondition: {
type: "colorClear",
targets: [{ color: "#22c55e", count: 100 }],
},
negativeScoreColors: ["#f472b6"],
negativeProgressColors: ["#f472b6"],
link: {
stiffness: 0.82,
lengthScale: 1.08,
damping: 0.06,
lineWidth: 3,
rope: true,
renderType: "line",
maxLengthMultiplier: 3.6,
},
onBeforeUpdate: ({ engine }) => {
const t = (engine.timing?.timestamp || 0) * 0.0005;
engine.gravity.x = Math.cos(t);
engine.gravity.y = Math.sin(t);
},
},
createBodies: (w, h) => {
const wallThickness = Math.max(36, Math.min(w, h) * 0.05);
const innerW = w - wallThickness * 2;
const innerH = h - wallThickness * 2;
const cx = w / 2;
const cy = h / 2;
const wallOptions = {
isStatic: true,
restitution: 0.3,
render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" },
};
return [
Bodies.rectangle(
cx,
cy - innerH / 2 - wallThickness / 2,
innerW + wallThickness * 2,
wallThickness,
wallOptions,
),
Bodies.rectangle(
cx,
cy + innerH / 2 + wallThickness / 2,
innerW + wallThickness * 2,
wallThickness,
wallOptions,
),
Bodies.rectangle(
cx - innerW / 2 - wallThickness / 2,
cy,
wallThickness,
innerH + wallThickness * 2,
wallOptions,
),
Bodies.rectangle(
cx + innerW / 2 + wallThickness / 2,
cy,
wallThickness,
innerH + wallThickness * 2,
wallOptions,
),
];
},
});
})();

View File

@@ -148,7 +148,8 @@
if (!point || !sceneEl) return;
const el = document.createElement("div");
el.className = "floating-score";
el.textContent = `+${amount}`;
const sign = amount > 0 ? "+" : "";
el.textContent = `${sign}${amount}`;
el.style.left = `${point.x}px`;
el.style.top = `${point.y}px`;
el.style.color = color || "#e0f2fe";