Fix gravity reset and add spawn cap for swirl arena
This commit is contained in:
106
src/main.js
106
src/main.js
@@ -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(
|
||||
config.ballRadius + 10,
|
||||
Math.min(width - config.ballRadius - 10, Math.random() * width),
|
||||
);
|
||||
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
|
||||
? height + config.ballRadius * 2
|
||||
: -config.ballRadius * 2;
|
||||
const y =
|
||||
centerSpawn?.y ??
|
||||
(spawnFromBottom
|
||||
? height + 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(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"fast-drop-maze",
|
||||
"balanced",
|
||||
"scene-lava",
|
||||
"swirl-arena",
|
||||
"relax",
|
||||
];
|
||||
const orderedScenes = desiredOrder
|
||||
|
||||
88
src/scenes/scene-swirl-arena.js
Normal file
88
src/scenes/scene-swirl-arena.js
Normal 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,
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user