diff --git a/public/assets/fx/gem-01.mp3 b/public/assets/fx/gem-01.mp3 index 58f1975..ff29726 100644 Binary files a/public/assets/fx/gem-01.mp3 and b/public/assets/fx/gem-01.mp3 differ diff --git a/public/assets/fx/gem-02.mp3 b/public/assets/fx/gem-02.mp3 index c2daf17..6aef8e7 100644 Binary files a/public/assets/fx/gem-02.mp3 and b/public/assets/fx/gem-02.mp3 differ diff --git a/public/assets/fx/gem-04.mp3 b/public/assets/fx/gem-04.mp3 new file mode 100644 index 0000000..677cc31 Binary files /dev/null and b/public/assets/fx/gem-04.mp3 differ diff --git a/public/assets/fx/gem-05.mp3 b/public/assets/fx/gem-05.mp3 new file mode 100644 index 0000000..cffdc78 Binary files /dev/null and b/public/assets/fx/gem-05.mp3 differ diff --git a/public/assets/fx/gem-big-drop.mp3 b/public/assets/fx/gem-big-drop.mp3 new file mode 100644 index 0000000..36d4e56 Binary files /dev/null and b/public/assets/fx/gem-big-drop.mp3 differ diff --git a/public/assets/fx/gem-chain.mp3 b/public/assets/fx/gem-chain.mp3 new file mode 100644 index 0000000..0289f94 Binary files /dev/null and b/public/assets/fx/gem-chain.mp3 differ diff --git a/public/assets/fx/gem-drop.mp3 b/public/assets/fx/gem-drop.mp3 new file mode 100644 index 0000000..69e40b6 Binary files /dev/null and b/public/assets/fx/gem-drop.mp3 differ diff --git a/public/src/games/bejeweled/BejeweledGame.js b/public/src/games/bejeweled/BejeweledGame.js index 6b7e490..7469527 100644 --- a/public/src/games/bejeweled/BejeweledGame.js +++ b/public/src/games/bejeweled/BejeweledGame.js @@ -81,6 +81,7 @@ export default class BejeweledGame extends Phaser.Scene { this.lastAction = 0; this.maxCascade = 0; this.peakMultiplier = 1; + this.gemToggle = false; } create() { @@ -387,6 +388,7 @@ export default class BejeweledGame extends Phaser.Scene { this.gemLayer.setMask(this.boardMask.createGeometryMask()); this.buildGems(true); + this.time.delayedCall(900, () => playSound(this, SFX.GEM_BIG_DROP)); this.time.delayedCall(950, () => { this.busy = false; this.lastAction = this.time.now; @@ -735,7 +737,6 @@ export default class BejeweledGame extends Phaser.Scene { if (!phases) { this.invalidSwap(a, b); return; } this.busy = true; - playSound(this, SFX.SCIFI_WOOSH); const sa = this.grid[a.r][a.c]; const sb = this.grid[b.r][b.c]; const pa = this.cellXY(a.c, a.r); @@ -761,7 +762,6 @@ export default class BejeweledGame extends Phaser.Scene { invalidSwap(a, b) { this.busy = true; - playSound(this, SFX.MASTERMIND_DENIED); const sa = this.grid[a.r][a.c]; const sb = this.grid[b.r][b.c]; const pa = this.cellXY(a.c, a.r); @@ -827,8 +827,19 @@ export default class BejeweledGame extends Phaser.Scene { this.redrawTimer(); } - // Clear matched gems with a burst. - if (phase.cleared.length) playSound(this, SFX.MASTERMIND_MATCH); + // Clear matched gems with a burst — sound depends on match size. + if (phase.cleared.length) { + const hasHyper = phase.spawns.some((s) => s.special === SPECIAL.HYPER); + const hasFlame = phase.spawns.some((s) => s.special === SPECIAL.FLAME); + if (hasHyper) { + playSound(this, SFX.GEM_MATCH_5); + } else if (hasFlame) { + playSound(this, SFX.GEM_MATCH_4); + } else { + playSound(this, this.gemToggle ? SFX.GEM_MATCH_2 : SFX.GEM_MATCH_1); + this.gemToggle = !this.gemToggle; + } + } const sparse = phase.cleared.length > 14; phase.cleared.forEach((cell, i) => { const sprite = this.grid[cell.r][cell.c]; @@ -897,6 +908,10 @@ export default class BejeweledGame extends Phaser.Scene { for (const f of phase.falls) maxFall = Math.max(maxFall, 110 + 58 * (f.toR - f.fromR)); for (const f of phase.refills) maxFall = Math.max(maxFall, 110 + 58 * (f.r - f.fromR)); + if (maxFall > 0 && (phase.falls.length || phase.refills.length)) { + this.time.delayedCall(FALL_AT + maxFall, () => playSound(this, SFX.GEM_DROP)); + } + this.time.delayedCall(FALL_AT + maxFall + 70, done); } @@ -910,6 +925,7 @@ export default class BejeweledGame extends Phaser.Scene { } const { x, y } = this.cellXY(e.c, e.r); if (e.type === 'flame') { + playSound(this, SFX.GEM_CHAIN); playSound(this, SFX.SCIFI_EXPLODE); this.cameras.main.shake(110, 0.0045); const flash = this.add.image(x, y, 'bj-glow').setScale(1).setTint(0xff7a1a) @@ -928,7 +944,7 @@ export default class BejeweledGame extends Phaser.Scene { this.time.delayedCall(800, () => em.destroy()); if (e.timeBonus > 0) this.time.delayedCall(500, () => this.timeBonusBanner(e.timeBonus)); } else if (e.type === 'star') { - playSound(this, SFX.SCIFI_LAUNCH); + playSound(this, SFX.GEM_CHAIN); this.cameras.main.shake(90, 0.003); const h = this.add.image(BOARD_X + BOARD_W / 2, y, 'bj-beam') .setDepth(D.fx).setBlendMode(Phaser.BlendModes.ADD).setDisplaySize(BOARD_W + 60, 110); @@ -945,6 +961,7 @@ export default class BejeweledGame extends Phaser.Scene { this.time.delayedCall(700, () => em.destroy()); if (e.timeBonus > 0) this.time.delayedCall(400, () => this.timeBonusBanner(e.timeBonus)); } else if (e.type === 'hyper') { + playSound(this, SFX.GEM_CHAIN); playSound(this, SFX.SCIFI_REVEAL); this.cameras.main.shake(170, 0.006); const tint = e.color ? GEMS[e.color].base : 0xffffff; diff --git a/public/src/games/yatzi/YatziGame.js b/public/src/games/yatzi/YatziGame.js index 9942fa2..58c6dcd 100644 --- a/public/src/games/yatzi/YatziGame.js +++ b/public/src/games/yatzi/YatziGame.js @@ -304,12 +304,14 @@ export default class YatziGame extends Phaser.Scene { for (let i = 0; i < 5; i++) { const x = DICE_LEFT + i * (DICE_SIZE + DICE_GAP) + DICE_SIZE / 2; const y = DICE_Y; - const frame = this.add.graphics().setDepth(DEPTH.dieBox); - const face = this.add.graphics().setDepth(DEPTH.diePip); + const container = this.add.container(x, y).setDepth(DEPTH.dieBox); + const frame = this.add.graphics(); + const face = this.add.graphics(); + container.add([frame, face]); const hit = this.add.zone(x, y, DICE_SIZE, DICE_SIZE).setOrigin(0.5).setDepth(DEPTH.dieHit); hit.setInteractive({ useHandCursor: true }); hit.on('pointerdown', () => this.onDieClick(i)); - this.dieEls.push({ frame, face, hit, cx: x, cy: y, currentFace: 1 }); + this.dieEls.push({ frame, face, hit, container, cx: x, cy: y, currentFace: 1 }); } } @@ -322,26 +324,25 @@ export default class YatziGame extends Phaser.Scene { renderDieFace(idx, face, held) { const el = this.dieEls[idx]; el.currentFace = face; - const x = el.cx, y = el.cy; const half = DICE_SIZE / 2; const r = 14; - // Frame + // Frame (drawn at container-local coords, center = 0,0) el.frame.clear(); const borderColor = held ? COLORS.accent : COLORS.muted; const borderWidth = held ? 5 : 2; - el.frame.fillStyle(0xf2ead8, 1); // cream face - el.frame.fillRoundedRect(x - half, y - half, DICE_SIZE, DICE_SIZE, r); + el.frame.fillStyle(0xf2ead8, 1); + el.frame.fillRoundedRect(-half, -half, DICE_SIZE, DICE_SIZE, r); el.frame.lineStyle(borderWidth, borderColor, 1); - el.frame.strokeRoundedRect(x - half, y - half, DICE_SIZE, DICE_SIZE, r); + el.frame.strokeRoundedRect(-half, -half, DICE_SIZE, DICE_SIZE, r); // Pips el.face.clear(); el.face.fillStyle(0x1a1208, 1); const pipR = 9; const off = DICE_SIZE * 0.28; - for (const [cx, cy] of PIP_POS[face]) { - el.face.fillCircle(x + cx * off, y + cy * off, pipR); + for (const [px, py] of PIP_POS[face]) { + el.face.fillCircle(px * off, py * off, pipR); } } @@ -377,25 +378,66 @@ export default class YatziGame extends Phaser.Scene { async animateRoll(targetDice) { playSound(this, SFX.DICE_ROLL); this.animating = true; - return new Promise((resolve) => { - this.tweens.addCounter({ - from: 0, to: 1, duration: 550, ease: 'Quad.Out', - onUpdate: () => { - for (let i = 0; i < 5; i++) { - if (this.gs.held[i]) continue; - const fake = Math.ceil(Math.random() * 6); - this.renderDieFace(i, fake, false); - } - }, - onComplete: () => { - for (let i = 0; i < 5; i++) { - this.renderDieFace(i, targetDice[i], this.gs.held[i]); - } - this.animating = false; - resolve(); - }, + + const promises = []; + for (let i = 0; i < 5; i++) { + if (this.gs.held[i]) continue; + + const el = this.dieEls[i]; + const finalX = el.cx; + const finalY = el.cy; + + const startX = finalX + Phaser.Math.Between(-120, 120); + const startY = 1280; + const landX = finalX + Phaser.Math.Between(-14, 14); + const landY = finalY + Phaser.Math.Between(-14, 14); + const P1x = (startX + landX) / 2 + Phaser.Math.Between(-80, 80); + const P1y = landY - Phaser.Math.Between(380, 520); + + const startRot = Phaser.Math.FloatBetween(-Math.PI * 3, Math.PI * 3); + const endRot = Phaser.Math.FloatBetween(-0.12, 0.12); + + el.container.x = startX; + el.container.y = startY; + el.container.rotation = startRot; + + const progress = { t: 0 }; + const p = new Promise((resolve) => { + this.tweens.add({ + targets: progress, + t: 1, + duration: 700, + delay: i * 70, + ease: 'Cubic.Out', + onUpdate: () => { + const t = progress.t; + const u = 1 - t; + el.container.x = u * u * startX + 2 * u * t * P1x + t * t * landX; + el.container.y = u * u * startY + 2 * u * t * P1y + t * t * landY; + el.container.rotation = startRot + (endRot - startRot) * t; + this.renderDieFace(i, Math.ceil(Math.random() * 6), false); + }, + onComplete: () => { + el.container.x = landX; + el.container.y = landY; + el.container.rotation = endRot; + this.renderDieFace(i, targetDice[i], false); + this.tweens.add({ + targets: el.container, + scaleX: { from: 1.12, to: 1 }, + scaleY: { from: 1.12, to: 1 }, + duration: 110, + ease: 'Quad.Out', + onComplete: resolve, + }); + }, + }); }); - }); + promises.push(p); + } + + await Promise.all(promises); + this.animating = false; } // ─── Portraits ───────────────────────────────────────────────────────── diff --git a/public/src/scenes/PreloadScene.js b/public/src/scenes/PreloadScene.js index e8d5889..ae56af4 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -132,6 +132,13 @@ export default class PreloadScene extends Phaser.Scene { this.load.audio('sfx-ui-flip', '/assets/fx/ui-flip.mp3'); this.load.audio('sfx-ui-place', '/assets/fx/ui-place.mp3'); this.load.audio('sfx-ui-chime', '/assets/fx/ui-chime.mp3'); + this.load.audio('sfx-gem-match-1', '/assets/fx/gem-01.mp3'); + this.load.audio('sfx-gem-match-2', '/assets/fx/gem-02.mp3'); + this.load.audio('sfx-gem-match-4', '/assets/fx/gem-04.mp3'); + this.load.audio('sfx-gem-match-5', '/assets/fx/gem-05.mp3'); + this.load.audio('sfx-gem-chain', '/assets/fx/gem-chain.mp3'); + this.load.audio('sfx-gem-drop', '/assets/fx/gem-drop.mp3'); + this.load.audio('sfx-gem-big-drop','/assets/fx/gem-big-drop.mp3'); this.load.spritesheet('catan-special-cards', '/assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 }); diff --git a/public/src/ui/Sounds.js b/public/src/ui/Sounds.js index 12d6b1f..3e10d24 100644 --- a/public/src/ui/Sounds.js +++ b/public/src/ui/Sounds.js @@ -55,6 +55,13 @@ export const SFX = { UI_FLIP: 'sfx-ui-flip', UI_PLACE: 'sfx-ui-place', UI_CHIME: 'sfx-ui-chime', + GEM_MATCH_1: 'sfx-gem-match-1', + GEM_MATCH_2: 'sfx-gem-match-2', + GEM_MATCH_4: 'sfx-gem-match-4', + GEM_MATCH_5: 'sfx-gem-match-5', + GEM_CHAIN: 'sfx-gem-chain', + GEM_DROP: 'sfx-gem-drop', + GEM_BIG_DROP: 'sfx-gem-big-drop', }; export function playSound(scene, key) {