- bump project/runtime version markers to v2.3.0 (pyproject + TetriNode UI version) - complete behavior-preserving refactor from monoliths into modular Python and JS architecture (tetrinode/ + js/live/) - add parity + standard regression harnesses under qa/ - fix queue panel layout so Queue bottom stays aligned to matrix bottom regardless of Controls visibility - fix music playback fallback when selected track is none, including modal Play behavior and runtime playback path
144 lines
4.5 KiB
JavaScript
144 lines
4.5 KiB
JavaScript
export function createRuntimeLoopHelpers(deps) {
|
|
const {
|
|
applyPendingClear,
|
|
getConfig,
|
|
getBackgroundSource,
|
|
getLockMode,
|
|
updateAutoRepeat,
|
|
settlePiece,
|
|
stepDown,
|
|
updateBackendState,
|
|
isNodeSelected,
|
|
} = deps;
|
|
|
|
function ensureTimer(node) {
|
|
const live = node.__tetrisLive;
|
|
if (!live || live.state.timer) return;
|
|
live.state.timer = setInterval(() => {
|
|
if (live.state.running && live.state.started && !live.state.gameOver && !isNodeSelected(node)) {
|
|
live.state.running = false;
|
|
updateBackendState(node);
|
|
node.setDirtyCanvas(true, true);
|
|
return;
|
|
}
|
|
if (!live.state.running || live.state.gameOver) return;
|
|
if (live.state.lockFlash) {
|
|
if (getConfig(node).anim_lock_flash === false) {
|
|
live.state.lockFlash = null;
|
|
live.state.boardDirty = true;
|
|
} else {
|
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
const start = live.state.lockFlash.start ?? now;
|
|
if (live.state.lockFlash.start == null) {
|
|
live.state.lockFlash.start = start;
|
|
}
|
|
live.state.lockFlash.elapsed = Math.max(0, now - start);
|
|
if (now >= live.state.lockFlash.until || live.state.lockFlash.elapsed >= live.state.lockFlash.duration) {
|
|
live.state.lockFlash = null;
|
|
}
|
|
live.state.boardDirty = true;
|
|
}
|
|
}
|
|
if (live.state.hardDropTrail) {
|
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
if (now - live.state.hardDropTrail.start >= live.state.hardDropTrail.duration) {
|
|
live.state.hardDropTrail = null;
|
|
} else {
|
|
live.state.boardDirty = true;
|
|
}
|
|
}
|
|
if (live.state.actionToast) {
|
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
if (now >= live.state.actionToast.until) {
|
|
live.state.actionToast = null;
|
|
} else {
|
|
live.state.boardDirty = true;
|
|
}
|
|
}
|
|
if (live.state.clearing) {
|
|
const { lineClear } = live.state;
|
|
if (lineClear) {
|
|
lineClear.elapsed += 50;
|
|
live.state.boardDirty = true;
|
|
if (lineClear.elapsed >= lineClear.duration) {
|
|
const pending = live.state.pendingClear;
|
|
if (pending) {
|
|
applyPendingClear(live.state, pending);
|
|
}
|
|
live.state.pendingClear = null;
|
|
live.state.clearing = false;
|
|
live.state.lineClear = null;
|
|
live.state.boardDirty = true;
|
|
} else {
|
|
live.state.boardDirty = true;
|
|
}
|
|
}
|
|
updateBackendState(node);
|
|
node.setDirtyCanvas(true, true);
|
|
return;
|
|
}
|
|
const lockMode = getLockMode(node);
|
|
updateAutoRepeat(live.state, node, 50);
|
|
live.state.elapsed += 50;
|
|
live.state.timeMs += 50;
|
|
if (live.state.locking) {
|
|
if (lockMode === "extended" && live.state.lockMoves >= 15) {
|
|
settlePiece(live.state);
|
|
updateBackendState(node);
|
|
node.setDirtyCanvas(true, true);
|
|
return;
|
|
}
|
|
live.state.lockElapsed += 50;
|
|
if (live.state.lockElapsed >= live.state.lockDelayMs) {
|
|
settlePiece(live.state);
|
|
updateBackendState(node);
|
|
node.setDirtyCanvas(true, true);
|
|
return;
|
|
}
|
|
}
|
|
if (live.state.elapsed >= live.state.dropMs) {
|
|
live.state.elapsed = 0;
|
|
stepDown(live.state);
|
|
if (lockMode === "extended" && live.state.lockMoves >= 15 && live.state.locking) {
|
|
settlePiece(live.state);
|
|
updateBackendState(node);
|
|
node.setDirtyCanvas(true, true);
|
|
return;
|
|
}
|
|
updateBackendState(node);
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function stopTimer(node) {
|
|
const live = node.__tetrisLive;
|
|
if (live?.state?.timer) {
|
|
clearInterval(live.state.timer);
|
|
live.state.timer = null;
|
|
}
|
|
if (live?.bgTimer) {
|
|
clearInterval(live.bgTimer);
|
|
live.bgTimer = null;
|
|
}
|
|
}
|
|
|
|
function ensureBackgroundUpdater(node) {
|
|
const live = node.__tetrisLive;
|
|
if (!live || live.bgTimer) return;
|
|
live.bgTimer = setInterval(() => {
|
|
const bg = getBackgroundSource(node);
|
|
if (bg && live.bgSource !== bg) {
|
|
live.bgSource = bg;
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
}, 250);
|
|
}
|
|
|
|
return {
|
|
ensureTimer,
|
|
stopTimer,
|
|
ensureBackgroundUpdater,
|
|
};
|
|
}
|