diff --git a/assets/fx/click.mp3 b/assets/fx/click.mp3 new file mode 100644 index 0000000..4aa85c8 Binary files /dev/null and b/assets/fx/click.mp3 differ diff --git a/assets/images/advancewars/advancewars-units.png b/assets/images/advancewars/advancewars-units.png index 1ad95f4..9574925 100644 Binary files a/assets/images/advancewars/advancewars-units.png and b/assets/images/advancewars/advancewars-units.png differ diff --git a/assets/images/advancewars/advancewars-units.psd b/assets/images/advancewars/advancewars-units.psd index 2742938..f0fd5bf 100644 Binary files a/assets/images/advancewars/advancewars-units.psd and b/assets/images/advancewars/advancewars-units.psd differ diff --git a/assets/images/tetrisattack/tetrisattack-panels.png b/assets/images/tetrisattack/tetrisattack-panels.png index 3e53e90..4c00662 100644 Binary files a/assets/images/tetrisattack/tetrisattack-panels.png and b/assets/images/tetrisattack/tetrisattack-panels.png differ diff --git a/assets/images/tetrisattack/tetrisattack-panels.psd b/assets/images/tetrisattack/tetrisattack-panels.psd index b4e3f59..ebda345 100644 Binary files a/assets/images/tetrisattack/tetrisattack-panels.psd and b/assets/images/tetrisattack/tetrisattack-panels.psd differ diff --git a/data/tetrisattack.json b/data/tetrisattack.json index 4824fca..d1388f3 100644 --- a/data/tetrisattack.json +++ b/data/tetrisattack.json @@ -24,7 +24,7 @@ "characterId": "jerry", "spriteIndex": 10, "name": "Jerry", - "speedLevel": 2, + "speedLevel": 1, "startRows": 6, "clearLineRows": 3, "intro": "Well now, y'all know I came to play some'a them games!", @@ -35,7 +35,7 @@ "characterId": "michael", "spriteIndex": 15, "name": "Michael", - "speedLevel": 3, + "speedLevel": 2, "startRows": 7, "clearLineRows": 3, "intro": "Hey mon! I been waitin' ta play on da Fertig Games — let's go mon!", @@ -46,7 +46,7 @@ "characterId": "steve", "spriteIndex": 17, "name": "Steve", - "speedLevel": 4, + "speedLevel": 2, "startRows": 7, "clearLineRows": 2, "intro": "I traveled WAY too far across the galaxy for this. Don't waste my time.", @@ -57,7 +57,7 @@ "characterId": "victor", "spriteIndex": 2, "name": "Victor", - "speedLevel": 5, + "speedLevel": 3, "startRows": 8, "clearLineRows": 2, "intro": "Patience. Every panel falls exactly where the ancients intended.", @@ -68,7 +68,7 @@ "characterId": "klaxon", "spriteIndex": 29, "name": "Klaxon", - "speedLevel": 6, + "speedLevel": 4, "startRows": 8, "clearLineRows": 2, "intro": "MISSION OBJECTIVE: victory. All other priorities rescinded.", diff --git a/src/games/tetrisattack/TetrisAttackGame.js b/src/games/tetrisattack/TetrisAttackGame.js index 217b148..4ab20a0 100644 --- a/src/games/tetrisattack/TetrisAttackGame.js +++ b/src/games/tetrisattack/TetrisAttackGame.js @@ -22,6 +22,16 @@ const BOARD_TOP = 116; const STEP_MS = 1000 / 60; // logic tick rate const POSE_HOLD_MS = 5000; // how long a reaction pose holds before resting +// Clear-burst animation (Panel de Pon style): tiles pop one-by-one; a face +// flashes over each, then the tile+face shatter into four quadrants that fly +// diagonally outward while shrinking + fading. Whole burst reads at ~1s and +// fits inside the engine's clear freeze window (CLEAR_TICKS ≈ 767ms). +const FACE_FRAME = 6; // frame 6 of tetrisattack-panels = the pop face +const STAGGER_MS = 70; // gap between consecutive tile pops +const FACE_MS = 150; // face overlay appears + holds before the split +const SPLIT_MS = 300; // quadrants fly out / shrink / fade +const TRAVEL = 0.45 * CELL; // how far each quadrant flies from tile center + export const D = { bg: -10, boardBg: 0, panels: 5, incoming: 4, cursor: 12, fx: 16, hud: 24, portrait: 20, overlay: 60, overlayUI: 62 }; // Bright 16-bit panel palette: fill, light facet, dark rim. @@ -247,6 +257,7 @@ export default class TetrisAttackGame extends Phaser.Scene { handleEvents(events) { for (const e of events) { if (e.type === 'clear') this.onClear(e); + else if (e.type === 'pop') this.onPop(e); else if (e.type === 'rowShift') playSound(this, SFX.EIGHTBIT_MOVE); else if (e.type === 'clearLineAppear') this.onClearLineAppear(); else if (e.type === 'danger') this.onDanger(e.on); @@ -257,30 +268,31 @@ export default class TetrisAttackGame extends Phaser.Scene { } onClear(e) { - // flash + pop the cleared sprites (positions carried on the event's cells) - const dur = TUNING.CLEAR_TICKS * STEP_MS; + // Panel de Pon pop: tiles burst one-by-one in reading order (staggered), each + // flashing a face then shattering into four fly-away quadrants (see popTile). + const ordered = [...(e.cells ?? [])] + .sort((a, b) => (a.r - b.r) || (a.c - b.c)) + .map((cell) => ({ cell, spr: this.sprites.get(cell.id) })) + .filter(({ spr }) => spr); + const biggest = e.combo ?? 3; + // 3-tile matches only get the per-tile 8bit-card pop. 4-tile matches add a + // single 8bit-action once every tile has finished exploding; 5+ tile matches + // add 8bit-win instead, once the whole 8bit-card set has played out. + let popped = 0; + const total = ordered.length; + const onTileExplode = () => { + popped++; + if (popped < total) return; + if (biggest === 4) playSound(this, SFX.EIGHTBIT_ACTION); + else if (biggest >= 5) playSound(this, SFX.EIGHTBIT_WIN); + }; + let cx = 0, cy = 0, n = 0; - for (const cell of (e.cells ?? [])) { - const spr = this.sprites.get(cell.id); - if (!spr) continue; - // Keep it in the map but flag it dying: the panel stays on the board (state - // 'clearing') until it pops, and renderBoard skips dying sprites — so this - // prevents a duplicate sprite being spawned for the same cell. - spr.dying = true; + ordered.forEach(({ cell, spr }, i) => { const pos = this.cellCenter(cell.r, cell.c, false); cx += pos.x; cy += pos.y; n++; - this.tweens.add({ targets: spr, alpha: 0.35, duration: 90, yoyo: true, repeat: 2 }); - this.tweens.add({ - targets: spr, scale: 0, angle: 40, delay: dur * 0.55, duration: dur * 0.45, - ease: 'Back.easeIn', onComplete: () => { spr.destroy(); this.sprites.delete(cell.id); }, - }); - } - // sfx scales with size - const biggest = e.combo ?? 3; - if (e.chain >= 2) playSound(this, SFX.GEM_CHAIN); - else if (biggest >= 5) playSound(this, SFX.GEM_MATCH_5); - else if (biggest >= 4) playSound(this, SFX.GEM_MATCH_4); - else playSound(this, SFX.GEM_MATCH_1); + this.popTile(spr, cell, i * STAGGER_MS, onTileExplode); + }); if (n && (e.chain >= 2 || biggest >= 4)) { const label = e.chain >= 2 ? `CHAIN ×${e.chain}` : `COMBO ×${biggest}`; @@ -301,6 +313,85 @@ export default class TetrisAttackGame extends Phaser.Scene { } } + // Pop one matched tile: after `delay`, flash the frame-6 face over it (Phase A), + // then swap it for four quadrant fragments that fly diagonally out from the tile + // center while shrinking + fading (Phase B). Fragments live in boardLayer so the + // playfield mask clips any overshoot. `spr` is flagged dying so renderBoard leaves + // it be until we destroy it at the Phase A→B handoff. + popTile(spr, cell, delay, onExplode) { + spr.dying = true; + const { x: cx, y: cy } = this.cellCenter(cell.r, cell.c, false); + const idx = PANEL_COLORS.indexOf(cell.color); + + // Phase A — face overlay (spritesheet only; fallback just holds the tile). + let face = null; + if (this.usePanelSheet) { + face = this.add.image(cx, cy, 'tetrisattack-panels', FACE_FRAME).setAlpha(0); + const fs = (CELL - 4) / Math.max(face.width, 1); + face.setScale(fs).setDepth(D.panels + 1); + this.boardLayer.add(face); + this.tweens.add({ targets: face, alpha: 1, scale: fs * 1.1, delay, duration: 80, ease: 'Quad.easeOut' }); + } + + // Phase B — shatter into four fly-away quadrants. + this.time.delayedCall(delay + FACE_MS, () => { + if (!this.state) return; + // The panel stays on the board (state 'clearing') until the engine pops it, + // so hide (don't destroy) the intact sprite: keeping it in the map, flagged + // dying, stops renderBoard respawning it. onPop destroys it for real. + spr.setVisible(false); + if (face) face.destroy(); + playSound(this, SFX.EIGHTBIT_CARD); + onExplode?.(); + + // Build one cropped corner of the given frame (or the fallback texture), + // centered on its own quadrant center at the container origin. + const mkPiece = (frameArg, sx, sy) => { + let img; + if (this.usePanelSheet) { + img = this.make.image({ x: 0, y: 0, key: 'tetrisattack-panels', frame: frameArg, add: false }); + img.setScale((CELL - 4) / Math.max(img.width, 1)); + } else { + img = this.make.image({ x: 0, y: 0, key: `ta-panel-${cell.color}`, add: false }); + } + const F = img.width; + const qx = sx > 0 ? 1 : 0, qy = sy > 0 ? 1 : 0; + img.setCrop(qx * F / 2, qy * F / 2, F / 2, F / 2); + const S = img.displayWidth; + img.setPosition(-sx * S / 4, -sy * S / 4); + return img; + }; + + for (const [sx, sy] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) { + const frag = this.add.container(0, 0); + this.boardLayer.add(frag); + frag.setDepth(D.panels + 1); + const tile = mkPiece(idx, sx, sy); + frag.add(tile); + if (this.usePanelSheet) frag.add(mkPiece(FACE_FRAME, sx, sy)); + const S = tile.displayWidth; + const startX = cx + sx * S / 4, startY = cy + sy * S / 4; + frag.setPosition(startX, startY); + this.tweens.add({ + targets: frag, + x: startX + sx * TRAVEL, y: startY + sy * TRAVEL, + scaleX: 0, scaleY: 0, alpha: 0, + duration: SPLIT_MS, ease: 'Quad.easeIn', + onComplete: () => frag.destroy(), + }); + } + }); + } + + // Engine has removed the popped panels from the board — destroy the hidden + // placeholder sprites the pop animation left behind (see popTile Phase B). + onPop(e) { + for (const cell of (e.cells ?? [])) { + const spr = this.sprites.get(cell.id); + if (spr) { spr.destroy(); this.sprites.delete(cell.id); } + } + } + // The boundary line has slid in under the stack: everything above it is now // the objective. Announce it over the board. onClearLineAppear() { @@ -667,7 +758,7 @@ export default class TetrisAttackGame extends Phaser.Scene { kb.on('keydown', (e) => { if (!this.playing || !this.state) return; const dir = dirMap[e.code]; - if (dir) { e.preventDefault?.(); moveCursor(this.state, dir); playSound(this, SFX.EIGHTBIT_MOVE); this.renderBoard(); return; } + if (dir) { e.preventDefault?.(); moveCursor(this.state, dir); playSound(this, SFX.CLICK); this.renderBoard(); return; } if (e.code === 'Space' || e.code === 'KeyZ' || e.code === 'Enter' || e.code === 'KeyX') { e.preventDefault?.(); const ev = trySwap(this.state); diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index 1959134..bf65c8e 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -138,6 +138,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.audio('sfx-8bit-activate', 'assets/fx/8bit-activate.mp3'); this.load.audio('sfx-8bit-action', 'assets/fx/8bit-action.mp3'); this.load.audio('sfx-8bit-move', 'assets/fx/8bit-move.mp3'); + this.load.audio('sfx-click', 'assets/fx/click.mp3'); this.load.audio('sfx-8bit-jump', 'assets/fx/8bit-jump.mp3'); this.load.audio('sfx-8bit-explode', 'assets/fx/8bit-explode.mp3'); this.load.audio('sfx-8bit-explode2', 'assets/fx/8bit-explode2.mp3'); diff --git a/src/ui/Sounds.js b/src/ui/Sounds.js index 1744a43..a8a2897 100644 --- a/src/ui/Sounds.js +++ b/src/ui/Sounds.js @@ -44,11 +44,13 @@ export const SFX = { EIGHTBIT_ACTIVATE: 'sfx-8bit-activate', EIGHTBIT_ACTION: 'sfx-8bit-action', EIGHTBIT_MOVE: 'sfx-8bit-move', + CLICK: 'sfx-click', EIGHTBIT_JUMP: 'sfx-8bit-jump', EIGHTBIT_CARD: 'sfx-card-deal-8bit', EIGHTBIT_EXPLODE: 'sfx-8bit-explode', EIGHTBIT_EXPLODE_2: 'sfx-8bit-explode2', EIGHTBIT_COUNT: 'sfx-8bit-count', + EIGHTBIT_WIN: 'sfx-casino-win-8bit', SQUISH: 'sfx-squish', SQUASH: 'sfx-squash', WOOSH: 'sfx-woosh',