diff --git a/assets/images/background-menu.png b/assets/images/background-menu.png index 14f0ea1..d4b62a2 100644 Binary files a/assets/images/background-menu.png and b/assets/images/background-menu.png differ diff --git a/assets/images/background-room.png b/assets/images/background-room.png index 4fea47d..0e0f524 100644 Binary files a/assets/images/background-room.png and b/assets/images/background-room.png differ diff --git a/assets/images/game-icons.png b/assets/images/game-icons.png index df9d068..c12cab3 100644 Binary files a/assets/images/game-icons.png and b/assets/images/game-icons.png differ diff --git a/assets/images/game-icons.psd b/assets/images/game-icons.psd index b56a942..3818930 100644 Binary files a/assets/images/game-icons.psd and b/assets/images/game-icons.psd differ diff --git a/assets/images/main-title.png b/assets/images/main-title.png index e4760da..60b986a 100644 Binary files a/assets/images/main-title.png and b/assets/images/main-title.png differ diff --git a/src/games/tetrisattack/TetrisAttackGame.js b/src/games/tetrisattack/TetrisAttackGame.js index ff54450..f81a621 100644 --- a/src/games/tetrisattack/TetrisAttackGame.js +++ b/src/games/tetrisattack/TetrisAttackGame.js @@ -343,7 +343,14 @@ export default class TetrisAttackGame extends Phaser.Scene { if (spr.dying) continue; const pos = this.cellCenter(r, c); spr.x = pos.x; - spr.y = pos.y + (p.state === 'falling' ? fp * CELL : 0); + if (p.state === 'falling' && p._prevRow != null) { + // interpolate from previous row to current row for smooth fall animation + const fp = st.fallTick / TUNING.FALL_INTERVAL; + const prevPos = this.cellCenter(p._prevRow, c); + spr.y = prevPos.y + (pos.y - prevPos.y) * fp; + } else { + spr.y = pos.y + (p.state === 'falling' ? fp * CELL : 0); + } spr.setDepth(D.panels); } } diff --git a/src/games/tetrisattack/TetrisAttackLogic.js b/src/games/tetrisattack/TetrisAttackLogic.js index 918f4c6..59ccadd 100644 --- a/src/games/tetrisattack/TetrisAttackLogic.js +++ b/src/games/tetrisattack/TetrisAttackLogic.js @@ -320,6 +320,7 @@ function updateFallStates(board) { const p = board[r][c]; if (p && p.state === 'falling' && supported(board, r, c)) { p.state = 'idle'; + delete p._prevRow; // landed — clear animation origin landed.push({ r, c, id: p.id }); } } @@ -334,6 +335,7 @@ function applyFallStep(board) { for (let r = board.length - 2; r >= 0; r--) { const p = board[r][c]; if (p && p.state === 'falling' && board[r + 1][c] === null) { + p._prevRow = r; // remember old row for fall animation board[r + 1][c] = p; board[r][c] = null; moves.push({ col: c, fromR: r, toR: r + 1, id: p.id }); @@ -407,13 +409,20 @@ export function step(state, rng = Math.random) { // re-evaluate landings after the move const landed2 = updateFallStates(board); for (const l of landed2) events.push({ type: 'land', ...l }); + if (landed2.length) state._justLanded = true; } + } else if (state._justLanded) { + // one settling frame after last panels land: show final animation frame + // before match detection fires, so the player sees panels arrive in place. + state.fallTick = TUNING.FALL_INTERVAL; + state._justLanded = false; } else { state.fallTick = 0; } - // 3. Match detection over settled idle panels. - const groups = findMatchGroups(board); + // 3. Match detection over settled idle panels (skip during settling frame). + if (!state._justLanded) { + const groups = findMatchGroups(board); if (groups.length) { let hadChainMember = false; for (const g of groups) { @@ -450,16 +459,20 @@ export function step(state, rng = Math.random) { state.score += tickScore; events.push({ type: 'clear', cells: allCells, groups: groups.map((g) => g.size), combo: biggestCombo, chain: state.chain, score: tickScore }); } + } // end: if (!state._justLanded) // 4. Wave end: board settled with no clears / falls / new matches → reset chain. - const settled = state.clears.length === 0 && !anyFalling(board); - if (settled) { - if (state.chain > 0) { - if (state.chain > 1) events.push({ type: 'chainEnd', chain: state.chain }); - state.chain = 0; - state.combo = 0; - // clear lingering chain flags - for (const row of board) for (const p of row) if (p) p.chain = false; + // (skip during settling frame — chain may still continue) + if (!state._justLanded) { + const settled = state.clears.length === 0 && !anyFalling(board); + if (settled) { + if (state.chain > 0) { + if (state.chain > 1) events.push({ type: 'chainEnd', chain: state.chain }); + state.chain = 0; + state.combo = 0; + // clear lingering chain flags + for (const row of board) for (const p of row) if (p) p.chain = false; + } } } @@ -496,7 +509,7 @@ function finishStep(state, events) { if (!inDanger && state.danger) { state.danger = false; events.push({ type: 'danger', on: false }); } if (!state.over && !state.won) { - const idleSettled = state.clears.length === 0 && !anyFalling(board); + const idleSettled = state.clears.length === 0 && !anyFalling(board) && !state._justLanded; if (state.mode === 'puzzle') { if (isBoardEmpty(board)) { state.won = true; events.push({ type: 'win' }); } else if (idleSettled && state.movesLeft <= 0) { state.over = true; events.push({ type: 'gameOver' }); } @@ -509,7 +522,7 @@ function finishStep(state, events) { // ── Query helpers (renderer / verifier) ───────────────────────────────────── export function isSettled(state) { - return state.clears.length === 0 && !anyFalling(state.board); + return state.clears.length === 0 && !anyFalling(state.board) && !state._justLanded; } export function remainingTargets(state) {