Add packed stack blocks and chain timer loss
This commit is contained in:
@@ -243,6 +243,18 @@
|
||||
updateLongestChain(chainLength);
|
||||
const { gain, isNegativeProgress } = getChainScoreState();
|
||||
state.score += gain;
|
||||
const chainLose = currentScene?.config?.chainLose;
|
||||
if (chainLose && Number.isFinite(chainLose.idleSec)) {
|
||||
const now = Date.now();
|
||||
const maxMs = chainLose.idleSec * 1000;
|
||||
const bumpMs = maxMs / 3;
|
||||
state.chainTimerDurationSec = chainLose.idleSec;
|
||||
const currentEnd = state.chainTimerEndMs || now + maxMs;
|
||||
const nextEnd = Math.min(now + maxMs, currentEnd + bumpMs);
|
||||
state.chainTimerEndMs = nextEnd;
|
||||
state.chainTimerLastDisplay = null;
|
||||
state.chainTimerLastWarnSec = null;
|
||||
}
|
||||
const clearTargets = collectClearTargets();
|
||||
state.clearedCount += chainLength;
|
||||
if (state.score > state.highScore) {
|
||||
|
||||
56
src/loop.js
56
src/loop.js
@@ -12,6 +12,7 @@
|
||||
getMaxLinkDistance,
|
||||
updateHud,
|
||||
checkWinCondition,
|
||||
triggerGameOver,
|
||||
ui,
|
||||
}) => {
|
||||
const runSceneBeforeUpdateHook = () => {
|
||||
@@ -95,6 +96,60 @@
|
||||
}
|
||||
};
|
||||
|
||||
const stepChainLoseTimer = () => {
|
||||
const chainLose = getCurrentScene()?.config?.chainLose;
|
||||
if (!chainLose || !Number.isFinite(chainLose.idleSec)) return;
|
||||
if (!state.chainTimerEndMs) return;
|
||||
if (state.chainTimerFrozen) return;
|
||||
const duration = state.chainTimerDurationSec ?? chainLose.idleSec;
|
||||
if (!Number.isFinite(duration) || duration <= 0) return;
|
||||
const now = Date.now();
|
||||
const remainingMs = Math.max(0, state.chainTimerEndMs - now);
|
||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||
const progress = (100 * remainingSec) / duration;
|
||||
const warnAt = chainLose.warnAtSec ?? 3;
|
||||
const urgent = remainingSec <= warnAt;
|
||||
if (state.chainTimerLastDisplay !== remainingSec) {
|
||||
state.chainTimerLastDisplay = remainingSec;
|
||||
if (ui.setChainTimer) {
|
||||
ui.setChainTimer({
|
||||
label: `${remainingSec}s left`,
|
||||
progress,
|
||||
urgent,
|
||||
});
|
||||
}
|
||||
if (
|
||||
urgent &&
|
||||
remainingSec > 0 &&
|
||||
state.chainTimerLastWarnSec !== remainingSec
|
||||
) {
|
||||
state.chainTimerLastWarnSec = remainingSec;
|
||||
const text =
|
||||
chainLose.countdownMessage ||
|
||||
(remainingSec <= 1 ? "Chain now!" : `${remainingSec}`);
|
||||
ui.showFloatingMessage(
|
||||
{ text },
|
||||
{
|
||||
durationMs: chainLose.countdownDurationMs ?? 700,
|
||||
position: chainLose.countdownPosition || {
|
||||
xPercent: 50,
|
||||
yPercent: 18,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (ui.setChainTimer) {
|
||||
ui.setChainTimer({
|
||||
label: `${remainingSec}s left`,
|
||||
progress,
|
||||
urgent,
|
||||
});
|
||||
}
|
||||
if (remainingMs <= 0) {
|
||||
triggerGameOver?.({ force: true });
|
||||
}
|
||||
};
|
||||
|
||||
const keepBallsInBounds = () => {
|
||||
const currentScene = getCurrentScene();
|
||||
state.balls.forEach((ball) => {
|
||||
@@ -140,6 +195,7 @@
|
||||
stepRotators(dt, timeScale);
|
||||
stepOscillators();
|
||||
stepTimer();
|
||||
stepChainLoseTimer();
|
||||
};
|
||||
|
||||
const linkSparkles = [];
|
||||
|
||||
140
src/main.js
140
src/main.js
@@ -142,6 +142,16 @@
|
||||
levelWon: false,
|
||||
timerEndMs: null,
|
||||
lastTimerDisplay: null,
|
||||
chainTimerEndMs: null,
|
||||
chainTimerDurationSec: null,
|
||||
chainTimerLastDisplay: null,
|
||||
chainTimerLastWarnSec: null,
|
||||
chainTimerIntroTimeoutId: null,
|
||||
chainTimerIntroAtMs: null,
|
||||
chainTimerIntroRemainingMs: null,
|
||||
chainTimerIntroText: null,
|
||||
chainTimerFrozen: false,
|
||||
pauseStartMs: null,
|
||||
};
|
||||
|
||||
const BALL_BASELINE = 680; // reference height used for relative ball sizing
|
||||
@@ -271,11 +281,19 @@
|
||||
const isGridScene = () =>
|
||||
currentScene && currentScene.config && currentScene.config.gridLayouts;
|
||||
|
||||
const triggerGameOver = () => {
|
||||
if (currentScene?.config?.noGameOver) return;
|
||||
const triggerGameOver = (options = {}) => {
|
||||
const force = options.force === true;
|
||||
if (currentScene?.config?.noGameOver && !force) return;
|
||||
if (state.gameOver) return;
|
||||
state.gameOver = true;
|
||||
state.paused = false;
|
||||
if (state.chainTimerIntroTimeoutId) {
|
||||
clearTimeout(state.chainTimerIntroTimeoutId);
|
||||
state.chainTimerIntroTimeoutId = null;
|
||||
state.chainTimerIntroAtMs = null;
|
||||
state.chainTimerIntroRemainingMs = null;
|
||||
state.chainTimerIntroText = null;
|
||||
}
|
||||
resetChainVisuals();
|
||||
spawnSystem?.stopSpawner();
|
||||
stopRunner();
|
||||
@@ -314,10 +332,23 @@
|
||||
const createLoop = load("PhysilinksLoop", { create: "create" });
|
||||
|
||||
const restartGame = () => {
|
||||
if (state.chainTimerIntroTimeoutId) {
|
||||
clearTimeout(state.chainTimerIntroTimeoutId);
|
||||
state.chainTimerIntroTimeoutId = null;
|
||||
state.chainTimerIntroAtMs = null;
|
||||
state.chainTimerIntroRemainingMs = null;
|
||||
state.chainTimerIntroText = null;
|
||||
}
|
||||
spawnSystem.stopSpawner();
|
||||
state.gameOver = false;
|
||||
state.paused = false;
|
||||
state.pauseStartMs = null;
|
||||
state.levelWon = false;
|
||||
state.chainTimerFrozen = false;
|
||||
if (currentScene?.id === "stack-blocks-packed") {
|
||||
engine.plugin ??= {};
|
||||
engine.plugin.stackBlocksPacked = null;
|
||||
}
|
||||
spawnSystem.resetSpawnState();
|
||||
state.score = 0;
|
||||
state.clearedCount = 0;
|
||||
@@ -333,6 +364,59 @@
|
||||
state.timerEndMs = null;
|
||||
state.lastTimerDisplay = null;
|
||||
}
|
||||
const chainLose = currentScene?.config?.chainLose;
|
||||
if (
|
||||
chainLose &&
|
||||
Number.isFinite(chainLose.idleSec) &&
|
||||
chainLose.idleSec > 0
|
||||
) {
|
||||
state.chainTimerDurationSec = chainLose.idleSec;
|
||||
state.chainTimerEndMs = Date.now() + chainLose.idleSec * 1000;
|
||||
state.chainTimerLastDisplay = null;
|
||||
state.chainTimerLastWarnSec = null;
|
||||
state.chainTimerFrozen = false;
|
||||
if (ui.setChainTimerVisibility) {
|
||||
ui.setChainTimerVisibility(true);
|
||||
}
|
||||
if (ui.setChainTimer) {
|
||||
ui.setChainTimer({
|
||||
label: `${chainLose.idleSec}s left`,
|
||||
progress: 100,
|
||||
urgent: false,
|
||||
});
|
||||
}
|
||||
const introDelay = chainLose.introDelayMs ?? 1200;
|
||||
const introText =
|
||||
chainLose.introMessage || `Make a chain in ${chainLose.idleSec}s`;
|
||||
if (introDelay >= 0) {
|
||||
state.chainTimerIntroText = introText;
|
||||
state.chainTimerIntroAtMs = Date.now() + introDelay;
|
||||
state.chainTimerIntroTimeoutId = setTimeout(() => {
|
||||
state.chainTimerIntroAtMs = null;
|
||||
state.chainTimerIntroText = null;
|
||||
ui.showFloatingMessage(
|
||||
{ text: introText },
|
||||
{
|
||||
durationMs: chainLose.introDurationMs ?? 2600,
|
||||
position: chainLose.introPosition || {
|
||||
xPercent: 50,
|
||||
yPercent: 12,
|
||||
},
|
||||
},
|
||||
);
|
||||
}, introDelay);
|
||||
}
|
||||
} else {
|
||||
state.chainTimerDurationSec = null;
|
||||
state.chainTimerEndMs = null;
|
||||
state.chainTimerLastDisplay = null;
|
||||
state.chainTimerLastWarnSec = null;
|
||||
state.chainTimerIntroText = null;
|
||||
state.chainTimerFrozen = false;
|
||||
if (ui.setChainTimerVisibility) {
|
||||
ui.setChainTimerVisibility(false);
|
||||
}
|
||||
}
|
||||
resetChainVisuals();
|
||||
ui.clearMessages();
|
||||
state.balls.forEach((ball) => {
|
||||
@@ -367,11 +451,54 @@
|
||||
state.paused = nextState;
|
||||
ui.setPauseState(state.paused);
|
||||
if (state.paused) {
|
||||
state.pauseStartMs = Date.now();
|
||||
if (state.chainTimerIntroTimeoutId && state.chainTimerIntroAtMs) {
|
||||
state.chainTimerIntroRemainingMs = Math.max(
|
||||
0,
|
||||
state.chainTimerIntroAtMs - Date.now(),
|
||||
);
|
||||
clearTimeout(state.chainTimerIntroTimeoutId);
|
||||
state.chainTimerIntroTimeoutId = null;
|
||||
state.chainTimerIntroAtMs = null;
|
||||
}
|
||||
resetChainVisuals();
|
||||
spawnSystem.stopSpawner();
|
||||
stopRunner();
|
||||
engine.timing.timeScale = 0;
|
||||
} else {
|
||||
if (state.pauseStartMs) {
|
||||
const pausedFor = Date.now() - state.pauseStartMs;
|
||||
state.pauseStartMs = null;
|
||||
if (state.timerEndMs) state.timerEndMs += pausedFor;
|
||||
if (state.chainTimerEndMs) state.chainTimerEndMs += pausedFor;
|
||||
}
|
||||
if (
|
||||
state.chainTimerIntroRemainingMs &&
|
||||
state.chainTimerIntroRemainingMs > 0
|
||||
) {
|
||||
const introText = state.chainTimerIntroText;
|
||||
const chainLose = currentScene?.config?.chainLose;
|
||||
state.chainTimerIntroAtMs =
|
||||
Date.now() + state.chainTimerIntroRemainingMs;
|
||||
state.chainTimerIntroTimeoutId = setTimeout(() => {
|
||||
state.chainTimerIntroAtMs = null;
|
||||
state.chainTimerIntroRemainingMs = null;
|
||||
if (introText) {
|
||||
ui.showFloatingMessage(
|
||||
{ text: introText },
|
||||
{
|
||||
durationMs: chainLose?.introDurationMs ?? 2600,
|
||||
position: chainLose?.introPosition || {
|
||||
xPercent: 50,
|
||||
yPercent: 12,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
state.chainTimerIntroText = null;
|
||||
}, state.chainTimerIntroRemainingMs);
|
||||
state.chainTimerIntroRemainingMs = null;
|
||||
}
|
||||
startRunner();
|
||||
spawnSystem.startSpawner();
|
||||
engine.timing.timeScale = 1;
|
||||
@@ -436,6 +563,14 @@
|
||||
const goal = goals.getGoalState();
|
||||
ui.setGoal(goal || { label: "—", progress: 0 }, config.goalEffects);
|
||||
if (!goal || !goal.met) return;
|
||||
if (state.chainTimerIntroTimeoutId) {
|
||||
clearTimeout(state.chainTimerIntroTimeoutId);
|
||||
state.chainTimerIntroTimeoutId = null;
|
||||
state.chainTimerIntroAtMs = null;
|
||||
state.chainTimerIntroRemainingMs = null;
|
||||
state.chainTimerIntroText = null;
|
||||
}
|
||||
state.chainTimerFrozen = true;
|
||||
applyWinEffects();
|
||||
state.levelWon = true;
|
||||
spawnSystem.stopSpawner();
|
||||
@@ -547,6 +682,7 @@
|
||||
getMaxLinkDistance,
|
||||
updateHud,
|
||||
checkWinCondition,
|
||||
triggerGameOver,
|
||||
ui,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"scene-grid",
|
||||
"balanced",
|
||||
"stack-blocks",
|
||||
"stack-blocks-packed",
|
||||
"scene-lava",
|
||||
"swirl-arena",
|
||||
"relax",
|
||||
|
||||
237
src/scenes/scene-stack-blocks-packed.js
Normal file
237
src/scenes/scene-stack-blocks-packed.js
Normal file
@@ -0,0 +1,237 @@
|
||||
(() => {
|
||||
const { Bodies, Query } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
const getSquareArea = ({ width, height, world }) => {
|
||||
const size = Math.min(width, height);
|
||||
const offset = world?.plugin?.squareOffset || 0;
|
||||
return {
|
||||
size,
|
||||
left: (width - size) / 2 + offset,
|
||||
top: (height - size) / 2 + offset,
|
||||
};
|
||||
};
|
||||
|
||||
const getSpawnInsets = ({ config, width, height, world }) => {
|
||||
let left = 0;
|
||||
let right = 0;
|
||||
const insetVal = config.spawnInset;
|
||||
if (Number.isFinite(insetVal)) {
|
||||
left = insetVal;
|
||||
right = insetVal;
|
||||
}
|
||||
if (typeof config.spawnInsets === "function") {
|
||||
try {
|
||||
const res = config.spawnInsets({ width, height, world });
|
||||
if (res && Number.isFinite(res.left)) left = res.left;
|
||||
if (res && Number.isFinite(res.right)) right = res.right;
|
||||
} catch (err) {
|
||||
console.error("spawnInsets function failed", err);
|
||||
}
|
||||
}
|
||||
return { left, right };
|
||||
};
|
||||
|
||||
const getColumnCenters = ({ config, width, height, world }) => {
|
||||
const columns = config.spawnColumns || 0;
|
||||
if (!Number.isFinite(columns) || columns <= 0) return [];
|
||||
const squareArea = getSquareArea({ width, height, world });
|
||||
const insets = getSpawnInsets({ config, width, height, world });
|
||||
const usableWidth = Math.max(
|
||||
0,
|
||||
squareArea.size - insets.left - insets.right,
|
||||
);
|
||||
const columnWidth = usableWidth / columns;
|
||||
const minX = squareArea.left + insets.left + config.ballRadius + 2;
|
||||
const maxX =
|
||||
squareArea.left +
|
||||
squareArea.size -
|
||||
insets.right -
|
||||
config.ballRadius -
|
||||
2;
|
||||
return Array.from({ length: columns }, (_, index) => {
|
||||
const x =
|
||||
squareArea.left +
|
||||
insets.left +
|
||||
columnWidth * (index + 0.5);
|
||||
return Math.max(minX, Math.min(maxX, x));
|
||||
});
|
||||
};
|
||||
|
||||
const getRowCount = ({ config, width, height, world }) => {
|
||||
const squareArea = getSquareArea({ width, height, world });
|
||||
const side = config.ballRadius * 2;
|
||||
const rowGap = side * (config.rowGapMultiplier ?? 1);
|
||||
const usableHeight = squareArea.size - config.ballRadius * 2;
|
||||
return Math.max(1, Math.floor(usableHeight / rowGap) + 1);
|
||||
};
|
||||
|
||||
const findOpenTopSlot = ({ config, width, height, world, balls }) => {
|
||||
const squareArea = getSquareArea({ width, height, world });
|
||||
const centers = getColumnCenters({ config, width, height, world });
|
||||
const y = squareArea.top + config.ballRadius;
|
||||
const halfSize = config.ballRadius * 1.05;
|
||||
for (let i = 0; i < centers.length; i += 1) {
|
||||
const x = centers[i];
|
||||
const region = {
|
||||
min: { x: x - halfSize, y: y - halfSize },
|
||||
max: { x: x + halfSize, y: y + halfSize },
|
||||
};
|
||||
const hits = Query.region(balls, region);
|
||||
if (!hits || hits.length === 0) {
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
scenes.push({
|
||||
id: "stack-blocks-packed",
|
||||
name: "Stack Blocks Packed",
|
||||
config: {
|
||||
gravity: 1,
|
||||
spawnIntervalMs: 900,
|
||||
autoSpawn: false,
|
||||
minChain: 3,
|
||||
palette: ["#38bdf8", "#f97316", "#facc15", "#22c55e"],
|
||||
ballRadius: 18,
|
||||
ballShape: "rect",
|
||||
spawnColumns: 10,
|
||||
sizeFromColumns: true,
|
||||
initialRows: null,
|
||||
requireClearSpawn: false,
|
||||
squarePlayArea: true,
|
||||
rowGapMultiplier: 1,
|
||||
noGameOver: true,
|
||||
chainLose: {
|
||||
idleSec: 15,
|
||||
warnAtSec: 3,
|
||||
introDelayMs: 1200,
|
||||
},
|
||||
ballPhysics: {
|
||||
restitution: 0.35,
|
||||
friction: 0.18,
|
||||
frictionAir: 0.02,
|
||||
frictionStatic: 0.45,
|
||||
density: 0.003,
|
||||
},
|
||||
spawnInsets: ({ width }) => {
|
||||
const wallThickness = Math.max(20, width * 0.02);
|
||||
return { left: 0, right: 0 };
|
||||
},
|
||||
winCondition: {
|
||||
type: "score",
|
||||
target: 30000,
|
||||
},
|
||||
link: {
|
||||
stiffness: 0.85,
|
||||
lengthScale: 1.15,
|
||||
damping: 0.08,
|
||||
lineWidth: 3,
|
||||
rope: true,
|
||||
renderType: "line",
|
||||
maxLengthMultiplier: 2.5,
|
||||
clearAnimation: {
|
||||
type: "shatter",
|
||||
durationMs: 380,
|
||||
sizeScale: 2.2,
|
||||
},
|
||||
},
|
||||
onBeforeUpdate: ({
|
||||
engine,
|
||||
width,
|
||||
height,
|
||||
state,
|
||||
spawnSystem,
|
||||
config,
|
||||
}) => {
|
||||
if (!state || !spawnSystem || state.paused || state.levelWon) return;
|
||||
engine.plugin ??= {};
|
||||
const packedState = engine.plugin.stackBlocksPacked || {};
|
||||
if (packedState.sceneId !== "stack-blocks-packed") {
|
||||
packedState.sceneId = "stack-blocks-packed";
|
||||
packedState.seeded = false;
|
||||
}
|
||||
if (!packedState.seeded) {
|
||||
const rows = getRowCount({
|
||||
config,
|
||||
width,
|
||||
height,
|
||||
world: engine.world,
|
||||
});
|
||||
const squareArea = getSquareArea({
|
||||
width,
|
||||
height,
|
||||
world: engine.world,
|
||||
});
|
||||
const rowGap = config.ballRadius * 2 * (config.rowGapMultiplier ?? 1);
|
||||
for (let r = 0; r < rows; r += 1) {
|
||||
const y = squareArea.top + config.ballRadius + r * rowGap;
|
||||
spawnSystem.spawnRowAtY({ y, markEntered: true });
|
||||
}
|
||||
packedState.seeded = true;
|
||||
engine.plugin.stackBlocksPacked = packedState;
|
||||
}
|
||||
if (state.gameOver) return;
|
||||
const openSlot = findOpenTopSlot({
|
||||
config,
|
||||
width,
|
||||
height,
|
||||
world: engine.world,
|
||||
balls: state.balls,
|
||||
});
|
||||
if (openSlot) {
|
||||
spawnSystem.spawnAtPosition({
|
||||
x: openSlot.x,
|
||||
y: openSlot.y,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const squareSize = Math.min(w, h);
|
||||
const left = (w - squareSize) / 2;
|
||||
const wallThickness = Math.max(20, w * 0.02);
|
||||
const floorHeight = Math.max(30, squareSize * 0.06);
|
||||
const wallRender = {
|
||||
fillStyle: "#1e293b",
|
||||
strokeStyle: "#94a3b8",
|
||||
lineWidth: 2,
|
||||
};
|
||||
return [
|
||||
Bodies.rectangle(
|
||||
left - wallThickness / 2,
|
||||
h / 2,
|
||||
wallThickness,
|
||||
h + wallThickness * 2,
|
||||
{
|
||||
isStatic: true,
|
||||
render: wallRender,
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
left + squareSize + wallThickness / 2,
|
||||
h / 2,
|
||||
wallThickness,
|
||||
h + wallThickness * 2,
|
||||
{
|
||||
isStatic: true,
|
||||
render: wallRender,
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
w / 2,
|
||||
h + floorHeight / 2,
|
||||
w + wallThickness * 2,
|
||||
floorHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
restitution: 0.2,
|
||||
render: wallRender,
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
})();
|
||||
61
src/spawn.js
61
src/spawn.js
@@ -119,10 +119,13 @@
|
||||
|
||||
const createBallBodies = (x, y, color) => {
|
||||
const scene = getCurrentScene();
|
||||
const ballPhysics = scene?.config?.ballPhysics || {};
|
||||
const commonOpts = {
|
||||
restitution: 0.72,
|
||||
friction: 0.01,
|
||||
frictionAir: 0.012,
|
||||
restitution: ballPhysics.restitution ?? 0.72,
|
||||
friction: ballPhysics.friction ?? 0.01,
|
||||
frictionAir: ballPhysics.frictionAir ?? 0.012,
|
||||
frictionStatic: ballPhysics.frictionStatic ?? 0,
|
||||
density: ballPhysics.density ?? 0.001,
|
||||
render: {
|
||||
fillStyle: color,
|
||||
strokeStyle: "#0b1222",
|
||||
@@ -320,6 +323,57 @@
|
||||
spawnCount += batchCount;
|
||||
};
|
||||
|
||||
const spawnAtPosition = ({
|
||||
x,
|
||||
y,
|
||||
color,
|
||||
markEntered = false,
|
||||
enforceSpawnLimit = true,
|
||||
} = {}) => {
|
||||
if (isGameOver()) return 0;
|
||||
const scene = getCurrentScene();
|
||||
const sceneConfig = scene?.config || {};
|
||||
const spawnLimit = sceneConfig.spawnLimit;
|
||||
if (
|
||||
enforceSpawnLimit &&
|
||||
Number.isFinite(spawnLimit) &&
|
||||
spawnCount >= spawnLimit
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
const spawnColor =
|
||||
color ??
|
||||
config.palette[Math.floor(Math.random() * config.palette.length)];
|
||||
const blob = createBallBodies(x, y, spawnColor);
|
||||
if (blob.constraints.length > 0 && blob.blobId) {
|
||||
blobConstraints.set(blob.blobId, blob.constraints);
|
||||
}
|
||||
blob.bodies.forEach((body) => {
|
||||
body.plugin = body.plugin || {};
|
||||
body.plugin.hasEntered = !!markEntered;
|
||||
if (body.plugin.entryCheckId) {
|
||||
clearTimeout(body.plugin.entryCheckId);
|
||||
body.plugin.entryCheckId = null;
|
||||
}
|
||||
balls.push(body);
|
||||
World.add(world, body);
|
||||
if (!sceneConfig.noGameOver && !markEntered) {
|
||||
body.plugin.entryCheckId = setTimeout(() => {
|
||||
body.plugin.entryCheckId = null;
|
||||
if (isGameOver()) return;
|
||||
if (!body.plugin.hasEntered && Math.abs(body.velocity.y) < 0.2) {
|
||||
triggerGameOver();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
if (blob.constraints.length > 0) {
|
||||
World.add(world, blob.constraints);
|
||||
}
|
||||
spawnCount += blob.bodies.length;
|
||||
return blob.bodies.length;
|
||||
};
|
||||
|
||||
const startSpawner = () => {
|
||||
if (isGridScene()) return;
|
||||
const scene = getCurrentScene();
|
||||
@@ -551,6 +605,7 @@
|
||||
removeBlob,
|
||||
resetSpawnState,
|
||||
spawnRowAtY,
|
||||
spawnAtPosition,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
23
src/ui.js
23
src/ui.js
@@ -17,6 +17,11 @@
|
||||
const pauseOverlay = document.getElementById("pause-overlay");
|
||||
const goalLabelEl = document.getElementById("goal-label");
|
||||
const goalProgressEl = document.getElementById("goal-progress");
|
||||
const chainTimerCard = document.getElementById("chain-timer-card");
|
||||
const chainTimerLabelEl = document.getElementById("chain-timer-label");
|
||||
const chainTimerProgressEl = document.getElementById(
|
||||
"chain-timer-progress",
|
||||
);
|
||||
const winEl = document.getElementById("win-overlay");
|
||||
const winMessageEl = document.getElementById("win-message");
|
||||
const winNextBtn = document.getElementById("win-next");
|
||||
@@ -178,6 +183,22 @@
|
||||
}
|
||||
};
|
||||
|
||||
const setChainTimerVisibility = (visible) => {
|
||||
if (!chainTimerCard) return;
|
||||
chainTimerCard.classList.toggle("hidden", !visible);
|
||||
};
|
||||
|
||||
const setChainTimer = ({ label, progress, urgent } = {}) => {
|
||||
if (chainTimerLabelEl) {
|
||||
chainTimerLabelEl.textContent = label || "—";
|
||||
}
|
||||
if (chainTimerProgressEl) {
|
||||
const clamped = Math.max(0, Math.min(100, progress ?? 0));
|
||||
chainTimerProgressEl.style.width = `${clamped}%`;
|
||||
chainTimerProgressEl.classList.toggle("timer-urgent", !!urgent);
|
||||
}
|
||||
};
|
||||
|
||||
const showWin = (message) => {
|
||||
if (winMessageEl) winMessageEl.textContent = message || "You win!";
|
||||
if (winEl) winEl.classList.add("visible");
|
||||
@@ -541,6 +562,8 @@
|
||||
setSceneSelection,
|
||||
setHandlers,
|
||||
setGoal,
|
||||
setChainTimer,
|
||||
setChainTimerVisibility,
|
||||
showFloatingMessage,
|
||||
setMessageDefaults,
|
||||
clearMessages,
|
||||
|
||||
Reference in New Issue
Block a user