From a8e21a3bc685b2f13ec341b5ddfca92de617bee5 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 14 Jun 2026 12:55:14 -0600 Subject: [PATCH] refactor: overhaul 2048 merge animation and swap sound effects - Replace PIECE_CLICK with alternating MASTERMIND_GLITCH sounds - Refactor merge animation: redraw all tiles before merge phase instead of skipping merged positions - Hide spawn tile early to prevent flash before spawn animation - Simplify merge destination setup by removing redundant alpha reset - Extract runMergePhase callback for cleaner merge/no-merge flow --- public/src/games/2048/2048Game.js | 51 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/public/src/games/2048/2048Game.js b/public/src/games/2048/2048Game.js index 12d0b98..852a7a0 100644 --- a/public/src/games/2048/2048Game.js +++ b/public/src/games/2048/2048Game.js @@ -35,11 +35,12 @@ export default class Game2048 extends Phaser.Scene { this.tileGraphics = []; this.tileTexts = []; - this.busy = false; - this.gameOver = false; - this.wonAlready = false; - this.score = 0; - this.best = 0; + this.busy = false; + this.gameOver = false; + this.wonAlready = false; + this.score = 0; + this.best = 0; + this._sfxToggle = 0; this.scoreValueText = null; this.bestValueText = null; @@ -416,7 +417,8 @@ export default class Game2048 extends Phaser.Scene { if (!newState) return; this.busy = true; - playSound(this, SFX.PIECE_CLICK); + playSound(this, this._sfxToggle ? SFX.MASTERMIND_GLITCH_2 : SFX.MASTERMIND_GLITCH_1); + this._sfxToggle ^= 1; this._animateMove(this.state, newState, () => { this.state = newState; @@ -500,7 +502,7 @@ export default class Game2048 extends Phaser.Scene { return; } - // Build map: merge destination index → moving source containers currently at that position + // Build map: merge destination → moving source containers sitting at that position const mergeSources = new Map(); for (const mv of movingEntries) { if (!mergeSet.has(mv.toIdx)) continue; @@ -509,33 +511,35 @@ export default class Game2048 extends Phaser.Scene { mergeSources.get(mv.toIdx).push({ cont: this.tileContainers[fr][fc], fromIdx: mv.fromIdx }); } - // Immediately update all tiles that aren't part of a merge transition - const skipIndices = new Set(newState.mergedAt); - for (const srcs of mergeSources.values()) { - for (const { fromIdx } of srcs) skipIndices.add(fromIdx); - } - this._redrawTilesExcept(newState, skipIndices); - - // Count total source containers across all merges let totalSrcs = 0; for (const srcs of mergeSources.values()) totalSrcs += srcs.length; - if (totalSrcs === 0) { + // Restore every tile to its canonical position/value, then hand off to + // _showMergedTiles which overrides scale=0 on the merge destinations so + // the grow animation runs from nothing. Hiding the spawn tile here + // prevents it flashing visible before spawnPhase animates it in. + const runMergePhase = () => { + this._redrawAllTiles(newState); + if (newState.spawned !== null) { + const sr = Math.floor(newState.spawned / 4), sc = newState.spawned % 4; + this.tileContainers[sr][sc].setAlpha(0); + } this._showMergedTiles(newState, spawnPhase); - return; - } + }; - // Scale down all source containers simultaneously (they're at the merge position) + if (totalSrcs === 0) { runMergePhase(); return; } + + // Scale down all merge-source containers simultaneously (they're at the merge position) let scaledDown = 0; const onAllScaledDown = () => { - // Reset every source to its home cell (invisible) before growing merged tiles + // Reset source containers to their home cells before the full redraw for (const [, srcs] of mergeSources) { for (const { cont, fromIdx } of srcs) { const { cx, cy } = this._cellCenter(Math.floor(fromIdx / 4), fromIdx % 4); cont.setAlpha(0).setScale(1).setPosition(cx, cy); } } - this._showMergedTiles(newState, spawnPhase); + runMergePhase(); }; for (const [, srcs] of mergeSources) { @@ -577,9 +581,10 @@ export default class Game2048 extends Phaser.Scene { for (const idx of newState.mergedAt) { const tr = Math.floor(idx / 4), tc = idx % 4; const destCont = this.tileContainers[tr][tc]; - this._paintTile(tr, tc, newState.grid[idx]); + // _redrawAllTiles already painted this tile at alpha=1 scale=1; + // collapse scale to 0 so the grow tween starts from nothing. this._scorePopup(destCont.x, destCont.y - 40, newState.grid[idx]); - destCont.setAlpha(1).setScale(0); + destCont.setScale(0); this.tweens.add({ targets: destCont, scaleX: 1.2, scaleY: 1.2, duration: 200, ease: 'Back.easeOut',