feat: enhance Bejeweled sound effects and Yatzi dice animations
- Replace generic sci-fi sounds with gem-themed audio (match, chain, drop, big drop) - Add context-aware match sounds based on special gems (hyper, flame) and match size - Improve Yatzi dice roll with bezier curve trajectories, rotation, and bounce - Restructure dice rendering to use containers for consistent transforms - Add new gem sound assets and SFX constants
This commit is contained in:
parent
d22f6ff902
commit
5632dbae9b
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue