Files
Erik W. a62da8926d release: v2.3.0 refactor and UI/audio fixes
- 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
2026-02-08 17:14:25 -05:00

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,
};
}