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.lastAction = 0;
|
||||||
this.maxCascade = 0;
|
this.maxCascade = 0;
|
||||||
this.peakMultiplier = 1;
|
this.peakMultiplier = 1;
|
||||||
|
this.gemToggle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -387,6 +388,7 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
this.gemLayer.setMask(this.boardMask.createGeometryMask());
|
this.gemLayer.setMask(this.boardMask.createGeometryMask());
|
||||||
|
|
||||||
this.buildGems(true);
|
this.buildGems(true);
|
||||||
|
this.time.delayedCall(900, () => playSound(this, SFX.GEM_BIG_DROP));
|
||||||
this.time.delayedCall(950, () => {
|
this.time.delayedCall(950, () => {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.lastAction = this.time.now;
|
this.lastAction = this.time.now;
|
||||||
|
|
@ -735,7 +737,6 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
if (!phases) { this.invalidSwap(a, b); return; }
|
if (!phases) { this.invalidSwap(a, b); return; }
|
||||||
|
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
playSound(this, SFX.SCIFI_WOOSH);
|
|
||||||
const sa = this.grid[a.r][a.c];
|
const sa = this.grid[a.r][a.c];
|
||||||
const sb = this.grid[b.r][b.c];
|
const sb = this.grid[b.r][b.c];
|
||||||
const pa = this.cellXY(a.c, a.r);
|
const pa = this.cellXY(a.c, a.r);
|
||||||
|
|
@ -761,7 +762,6 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
|
|
||||||
invalidSwap(a, b) {
|
invalidSwap(a, b) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
playSound(this, SFX.MASTERMIND_DENIED);
|
|
||||||
const sa = this.grid[a.r][a.c];
|
const sa = this.grid[a.r][a.c];
|
||||||
const sb = this.grid[b.r][b.c];
|
const sb = this.grid[b.r][b.c];
|
||||||
const pa = this.cellXY(a.c, a.r);
|
const pa = this.cellXY(a.c, a.r);
|
||||||
|
|
@ -827,8 +827,19 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
this.redrawTimer();
|
this.redrawTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear matched gems with a burst.
|
// Clear matched gems with a burst — sound depends on match size.
|
||||||
if (phase.cleared.length) playSound(this, SFX.MASTERMIND_MATCH);
|
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;
|
const sparse = phase.cleared.length > 14;
|
||||||
phase.cleared.forEach((cell, i) => {
|
phase.cleared.forEach((cell, i) => {
|
||||||
const sprite = this.grid[cell.r][cell.c];
|
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.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));
|
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);
|
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);
|
const { x, y } = this.cellXY(e.c, e.r);
|
||||||
if (e.type === 'flame') {
|
if (e.type === 'flame') {
|
||||||
|
playSound(this, SFX.GEM_CHAIN);
|
||||||
playSound(this, SFX.SCIFI_EXPLODE);
|
playSound(this, SFX.SCIFI_EXPLODE);
|
||||||
this.cameras.main.shake(110, 0.0045);
|
this.cameras.main.shake(110, 0.0045);
|
||||||
const flash = this.add.image(x, y, 'bj-glow').setScale(1).setTint(0xff7a1a)
|
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());
|
this.time.delayedCall(800, () => em.destroy());
|
||||||
if (e.timeBonus > 0) this.time.delayedCall(500, () => this.timeBonusBanner(e.timeBonus));
|
if (e.timeBonus > 0) this.time.delayedCall(500, () => this.timeBonusBanner(e.timeBonus));
|
||||||
} else if (e.type === 'star') {
|
} else if (e.type === 'star') {
|
||||||
playSound(this, SFX.SCIFI_LAUNCH);
|
playSound(this, SFX.GEM_CHAIN);
|
||||||
this.cameras.main.shake(90, 0.003);
|
this.cameras.main.shake(90, 0.003);
|
||||||
const h = this.add.image(BOARD_X + BOARD_W / 2, y, 'bj-beam')
|
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);
|
.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());
|
this.time.delayedCall(700, () => em.destroy());
|
||||||
if (e.timeBonus > 0) this.time.delayedCall(400, () => this.timeBonusBanner(e.timeBonus));
|
if (e.timeBonus > 0) this.time.delayedCall(400, () => this.timeBonusBanner(e.timeBonus));
|
||||||
} else if (e.type === 'hyper') {
|
} else if (e.type === 'hyper') {
|
||||||
|
playSound(this, SFX.GEM_CHAIN);
|
||||||
playSound(this, SFX.SCIFI_REVEAL);
|
playSound(this, SFX.SCIFI_REVEAL);
|
||||||
this.cameras.main.shake(170, 0.006);
|
this.cameras.main.shake(170, 0.006);
|
||||||
const tint = e.color ? GEMS[e.color].base : 0xffffff;
|
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++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const x = DICE_LEFT + i * (DICE_SIZE + DICE_GAP) + DICE_SIZE / 2;
|
const x = DICE_LEFT + i * (DICE_SIZE + DICE_GAP) + DICE_SIZE / 2;
|
||||||
const y = DICE_Y;
|
const y = DICE_Y;
|
||||||
const frame = this.add.graphics().setDepth(DEPTH.dieBox);
|
const container = this.add.container(x, y).setDepth(DEPTH.dieBox);
|
||||||
const face = this.add.graphics().setDepth(DEPTH.diePip);
|
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);
|
const hit = this.add.zone(x, y, DICE_SIZE, DICE_SIZE).setOrigin(0.5).setDepth(DEPTH.dieHit);
|
||||||
hit.setInteractive({ useHandCursor: true });
|
hit.setInteractive({ useHandCursor: true });
|
||||||
hit.on('pointerdown', () => this.onDieClick(i));
|
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) {
|
renderDieFace(idx, face, held) {
|
||||||
const el = this.dieEls[idx];
|
const el = this.dieEls[idx];
|
||||||
el.currentFace = face;
|
el.currentFace = face;
|
||||||
const x = el.cx, y = el.cy;
|
|
||||||
const half = DICE_SIZE / 2;
|
const half = DICE_SIZE / 2;
|
||||||
const r = 14;
|
const r = 14;
|
||||||
|
|
||||||
// Frame
|
// Frame (drawn at container-local coords, center = 0,0)
|
||||||
el.frame.clear();
|
el.frame.clear();
|
||||||
const borderColor = held ? COLORS.accent : COLORS.muted;
|
const borderColor = held ? COLORS.accent : COLORS.muted;
|
||||||
const borderWidth = held ? 5 : 2;
|
const borderWidth = held ? 5 : 2;
|
||||||
el.frame.fillStyle(0xf2ead8, 1); // cream face
|
el.frame.fillStyle(0xf2ead8, 1);
|
||||||
el.frame.fillRoundedRect(x - half, y - half, DICE_SIZE, DICE_SIZE, r);
|
el.frame.fillRoundedRect(-half, -half, DICE_SIZE, DICE_SIZE, r);
|
||||||
el.frame.lineStyle(borderWidth, borderColor, 1);
|
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
|
// Pips
|
||||||
el.face.clear();
|
el.face.clear();
|
||||||
el.face.fillStyle(0x1a1208, 1);
|
el.face.fillStyle(0x1a1208, 1);
|
||||||
const pipR = 9;
|
const pipR = 9;
|
||||||
const off = DICE_SIZE * 0.28;
|
const off = DICE_SIZE * 0.28;
|
||||||
for (const [cx, cy] of PIP_POS[face]) {
|
for (const [px, py] of PIP_POS[face]) {
|
||||||
el.face.fillCircle(x + cx * off, y + cy * off, pipR);
|
el.face.fillCircle(px * off, py * off, pipR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -377,25 +378,66 @@ export default class YatziGame extends Phaser.Scene {
|
||||||
async animateRoll(targetDice) {
|
async animateRoll(targetDice) {
|
||||||
playSound(this, SFX.DICE_ROLL);
|
playSound(this, SFX.DICE_ROLL);
|
||||||
this.animating = true;
|
this.animating = true;
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.tweens.addCounter({
|
const promises = [];
|
||||||
from: 0, to: 1, duration: 550, ease: 'Quad.Out',
|
for (let i = 0; i < 5; i++) {
|
||||||
onUpdate: () => {
|
if (this.gs.held[i]) continue;
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
if (this.gs.held[i]) continue;
|
const el = this.dieEls[i];
|
||||||
const fake = Math.ceil(Math.random() * 6);
|
const finalX = el.cx;
|
||||||
this.renderDieFace(i, fake, false);
|
const finalY = el.cy;
|
||||||
}
|
|
||||||
},
|
const startX = finalX + Phaser.Math.Between(-120, 120);
|
||||||
onComplete: () => {
|
const startY = 1280;
|
||||||
for (let i = 0; i < 5; i++) {
|
const landX = finalX + Phaser.Math.Between(-14, 14);
|
||||||
this.renderDieFace(i, targetDice[i], this.gs.held[i]);
|
const landY = finalY + Phaser.Math.Between(-14, 14);
|
||||||
}
|
const P1x = (startX + landX) / 2 + Phaser.Math.Between(-80, 80);
|
||||||
this.animating = false;
|
const P1y = landY - Phaser.Math.Between(380, 520);
|
||||||
resolve();
|
|
||||||
},
|
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 ─────────────────────────────────────────────────────────
|
// ─── 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-flip', '/assets/fx/ui-flip.mp3');
|
||||||
this.load.audio('sfx-ui-place', '/assets/fx/ui-place.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-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 });
|
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_FLIP: 'sfx-ui-flip',
|
||||||
UI_PLACE: 'sfx-ui-place',
|
UI_PLACE: 'sfx-ui-place',
|
||||||
UI_CHIME: 'sfx-ui-chime',
|
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) {
|
export function playSound(scene, key) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue