Fix gravity reset and add spawn cap for swirl arena
This commit is contained in:
@@ -100,6 +100,7 @@
|
|||||||
<script src="./src/scenes/scene-fastdrop.js"></script>
|
<script src="./src/scenes/scene-fastdrop.js"></script>
|
||||||
<script src="./src/scenes/scene-lavalamp.js"></script>
|
<script src="./src/scenes/scene-lavalamp.js"></script>
|
||||||
<script src="./src/scenes/scene-relax.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/scenes/index.js"></script>
|
||||||
<script src="./src/ui.js"></script>
|
<script src="./src/ui.js"></script>
|
||||||
<script src="./src/main.js"></script>
|
<script src="./src/main.js"></script>
|
||||||
|
|||||||
98
src/main.js
98
src/main.js
@@ -64,6 +64,8 @@
|
|||||||
const BALL_BASELINE = 680; // reference height used for relative ball sizing
|
const BALL_BASELINE = 680; // reference height used for relative ball sizing
|
||||||
|
|
||||||
const engine = Engine.create();
|
const engine = Engine.create();
|
||||||
|
const defaultGravityScale = engine.gravity.scale;
|
||||||
|
const defaultTimeScale = engine.timing.timeScale || 1;
|
||||||
engine.gravity.y = config.gravity;
|
engine.gravity.y = config.gravity;
|
||||||
const world = engine.world;
|
const world = engine.world;
|
||||||
|
|
||||||
@@ -125,6 +127,7 @@
|
|||||||
const balls = [];
|
const balls = [];
|
||||||
const blobConstraints = new Map();
|
const blobConstraints = new Map();
|
||||||
let spawnTimer = null;
|
let spawnTimer = null;
|
||||||
|
let spawnCount = 0;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let highScore = 0;
|
let highScore = 0;
|
||||||
let clearedCount = 0;
|
let clearedCount = 0;
|
||||||
@@ -165,7 +168,16 @@
|
|||||||
const prevRadius = config.ballRadius;
|
const prevRadius = config.ballRadius;
|
||||||
Object.assign(config, next.config);
|
Object.assign(config, next.config);
|
||||||
config.link = { ...next.config.link };
|
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);
|
updateBallRadius(prevRadius);
|
||||||
|
engine.gravity.x = 0;
|
||||||
engine.gravity.y = config.gravity;
|
engine.gravity.y = config.gravity;
|
||||||
clearedCount = 0;
|
clearedCount = 0;
|
||||||
levelWon = false;
|
levelWon = false;
|
||||||
@@ -221,16 +233,39 @@
|
|||||||
|
|
||||||
const spawnBall = () => {
|
const spawnBall = () => {
|
||||||
if (gameOver) return;
|
if (gameOver) return;
|
||||||
|
const spawnLimit = currentScene?.config?.spawnLimit;
|
||||||
|
if (Number.isFinite(spawnLimit) && spawnCount >= spawnLimit) {
|
||||||
|
stopSpawner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const color =
|
const color =
|
||||||
config.palette[Math.floor(Math.random() * config.palette.length)];
|
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,
|
config.ballRadius + 10,
|
||||||
Math.min(width - config.ballRadius - 10, Math.random() * width),
|
Math.min(width - config.ballRadius - 10, Math.random() * width),
|
||||||
);
|
);
|
||||||
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
|
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
|
||||||
const y = spawnFromBottom
|
const y =
|
||||||
|
centerSpawn?.y ??
|
||||||
|
(spawnFromBottom
|
||||||
? height + config.ballRadius * 2
|
? height + config.ballRadius * 2
|
||||||
: -config.ballRadius * 2;
|
: -config.ballRadius * 2);
|
||||||
const batchMin = currentScene?.config?.spawnBatchMin ?? 1;
|
const batchMin = currentScene?.config?.spawnBatchMin ?? 1;
|
||||||
const batchMax = currentScene?.config?.spawnBatchMax ?? 1;
|
const batchMax = currentScene?.config?.spawnBatchMax ?? 1;
|
||||||
const batchCount =
|
const batchCount =
|
||||||
@@ -276,6 +311,7 @@
|
|||||||
World.add(world, blob.constraints);
|
World.add(world, blob.constraints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
spawnCount += batchCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
const startSpawner = () => {
|
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) => {
|
const cleanupBall = (ball) => {
|
||||||
if (ball.plugin && ball.plugin.entryCheckId) {
|
if (ball.plugin && ball.plugin.entryCheckId) {
|
||||||
clearTimeout(ball.plugin.entryCheckId);
|
clearTimeout(ball.plugin.entryCheckId);
|
||||||
@@ -371,6 +415,7 @@
|
|||||||
gameOver = false;
|
gameOver = false;
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
levelWon = false;
|
levelWon = false;
|
||||||
|
spawnCount = 0;
|
||||||
score = 0;
|
score = 0;
|
||||||
clearedCount = 0;
|
clearedCount = 0;
|
||||||
clearedByColor = {};
|
clearedByColor = {};
|
||||||
@@ -393,6 +438,7 @@
|
|||||||
ui.hideGameOver();
|
ui.hideGameOver();
|
||||||
ui.hideWin();
|
ui.hideWin();
|
||||||
ui.setPauseState(false);
|
ui.setPauseState(false);
|
||||||
|
engine.gravity.x = 0;
|
||||||
engine.gravity.y = config.gravity;
|
engine.gravity.y = config.gravity;
|
||||||
engine.timing.timeScale = 1;
|
engine.timing.timeScale = 1;
|
||||||
startRunner();
|
startRunner();
|
||||||
@@ -400,6 +446,7 @@
|
|||||||
if (isGridScene()) {
|
if (isGridScene()) {
|
||||||
spawnGridBalls();
|
spawnGridBalls();
|
||||||
} else {
|
} else {
|
||||||
|
spawnInitialBurst();
|
||||||
startSpawner();
|
startSpawner();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -534,7 +581,20 @@
|
|||||||
const finishChain = (releasePoint) => {
|
const finishChain = (releasePoint) => {
|
||||||
if (!chain.active || gameOver || isPaused) return;
|
if (!chain.active || gameOver || isPaused) return;
|
||||||
if (chain.bodies.length >= config.minChain) {
|
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;
|
score += gain;
|
||||||
clearedCount += chain.bodies.length;
|
clearedCount += chain.bodies.length;
|
||||||
if (score > highScore) {
|
if (score > highScore) {
|
||||||
@@ -576,6 +636,16 @@
|
|||||||
balls.splice(i, 1);
|
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 {
|
} else {
|
||||||
chain.constraints.forEach((c) => World.remove(world, c));
|
chain.constraints.forEach((c) => World.remove(world, c));
|
||||||
chain.bodies.forEach((b) => setHighlight(b, false));
|
chain.bodies.forEach((b) => setHighlight(b, false));
|
||||||
@@ -886,10 +956,11 @@
|
|||||||
const totalTarget = targets.reduce((sum, t) => sum + t.count, 0);
|
const totalTarget = targets.reduce((sum, t) => sum + t.count, 0);
|
||||||
let totalAchieved = 0;
|
let totalAchieved = 0;
|
||||||
const parts = targets.map((t) => {
|
const parts = targets.map((t) => {
|
||||||
const got = Math.min(
|
const achieved = Math.max(
|
||||||
t.count,
|
0,
|
||||||
clearedByColor[normalizeColor(t.color)] || 0,
|
clearedByColor[normalizeColor(t.color)] || 0,
|
||||||
);
|
);
|
||||||
|
const got = Math.min(t.count, achieved);
|
||||||
totalAchieved += got;
|
totalAchieved += got;
|
||||||
const remaining = Math.max(0, t.count - got);
|
const remaining = Math.max(0, t.count - got);
|
||||||
return `${got}/${t.count} (${remaining} left)`;
|
return `${got}/${t.count} (${remaining} left)`;
|
||||||
@@ -972,8 +1043,16 @@
|
|||||||
ball.plugin.hasEntered = true;
|
ball.plugin.hasEntered = true;
|
||||||
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
|
const spawnFromBottom = currentScene?.config?.spawnFrom === "bottom";
|
||||||
Matter.Body.setPosition(ball, {
|
Matter.Body.setPosition(ball, {
|
||||||
x: Math.random() * width,
|
x:
|
||||||
y: spawnFromBottom ? height + 40 : -40,
|
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 });
|
Matter.Body.setVelocity(ball, { x: 0, y: 0 });
|
||||||
}
|
}
|
||||||
@@ -982,6 +1061,9 @@
|
|||||||
|
|
||||||
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 (typeof currentScene?.config?.onBeforeUpdate === "function") {
|
||||||
|
currentScene.config.onBeforeUpdate({ engine, width, height });
|
||||||
|
}
|
||||||
chain.constraints.forEach((c) => {
|
chain.constraints.forEach((c) => {
|
||||||
if (!c.plugin || !c.plugin.rope) return;
|
if (!c.plugin || !c.plugin.rope) return;
|
||||||
const current = Vector.magnitude(
|
const current = Vector.magnitude(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"fast-drop-maze",
|
"fast-drop-maze",
|
||||||
"balanced",
|
"balanced",
|
||||||
"scene-lava",
|
"scene-lava",
|
||||||
|
"swirl-arena",
|
||||||
"relax",
|
"relax",
|
||||||
];
|
];
|
||||||
const orderedScenes = desiredOrder
|
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;
|
if (!point || !sceneEl) return;
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "floating-score";
|
el.className = "floating-score";
|
||||||
el.textContent = `+${amount}`;
|
const sign = amount > 0 ? "+" : "";
|
||||||
|
el.textContent = `${sign}${amount}`;
|
||||||
el.style.left = `${point.x}px`;
|
el.style.left = `${point.x}px`;
|
||||||
el.style.top = `${point.y}px`;
|
el.style.top = `${point.y}px`;
|
||||||
el.style.color = color || "#e0f2fe";
|
el.style.color = color || "#e0f2fe";
|
||||||
|
|||||||
Reference in New Issue
Block a user