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
5632dbae9b
commit
e4c12e91bd
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -79,6 +79,16 @@
|
|||
"file": "track16.mp3",
|
||||
"artist": "Surf Sundown",
|
||||
"title": "Khruangbin"
|
||||
},
|
||||
{
|
||||
"file": "track17.mp3",
|
||||
"artist": "All Night Long",
|
||||
"title": "Becky Hill"
|
||||
},
|
||||
{
|
||||
"file": "track18.mp3",
|
||||
"artist": "How You Feel It",
|
||||
"title": "Mutemath"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -493,10 +493,10 @@ export default class BlockFighterGame extends Phaser.Scene {
|
|||
let locked = false;
|
||||
let events = null;
|
||||
switch (action) {
|
||||
case 'left': if (moveLeft(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break;
|
||||
case 'right': if (moveRight(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break;
|
||||
case 'rotateCW': if (rotateCW(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break;
|
||||
case 'rotateCCW': if (rotateCCW(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break;
|
||||
case 'left': if (moveLeft(m, idx) && idx === 0) playSound(this, SFX.UI_PICK); break;
|
||||
case 'right': if (moveRight(m, idx) && idx === 0) playSound(this, SFX.UI_PICK); break;
|
||||
case 'rotateCW': if (rotateCW(m, idx) && idx === 0) playSound(this, SFX.UI_PICK); break;
|
||||
case 'rotateCCW': if (rotateCCW(m, idx) && idx === 0) playSound(this, SFX.UI_PICK); break;
|
||||
case 'softDrop': {
|
||||
const r = stepDown(m, idx);
|
||||
locked = r.locked; events = r.events;
|
||||
|
|
@ -525,7 +525,7 @@ export default class BlockFighterGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
onLocked(idx, events) {
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
playSound(this, SFX.UI_PLACE);
|
||||
this.updatePieceSprites(idx); // hides the piece (now null)
|
||||
this.enqueue(idx, events);
|
||||
}
|
||||
|
|
@ -542,6 +542,7 @@ export default class BlockFighterGame extends Phaser.Scene {
|
|||
case 'spawn':
|
||||
this.updatePieceSprites(idx);
|
||||
this.updatePreviews();
|
||||
if (idx === 0) playSound(this, SFX.UI_CHIME);
|
||||
break;
|
||||
case 'place':
|
||||
case 'settle':
|
||||
|
|
@ -550,12 +551,12 @@ export default class BlockFighterGame extends Phaser.Scene {
|
|||
break;
|
||||
case 'garbage':
|
||||
this.renderBoard(idx, e);
|
||||
if (e.cells?.length) playSound(this, SFX.DICE_ROLL);
|
||||
if (e.cells?.length) playSound(this, SFX.GEM_CHAIN);
|
||||
break;
|
||||
case 'clear':
|
||||
case 'diamond': {
|
||||
this.renderBoard(idx, e);
|
||||
playSound(this, SFX.MASTERMIND_MATCH ?? SFX.CARD_SHOW);
|
||||
playSound(this, SFX.GEM_MATCH_4);
|
||||
for (const cell of e.cells ?? []) {
|
||||
if (cell.r < HIDDEN_ROWS) continue;
|
||||
const fx = this.add.rectangle(
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
|
||||
buildTitle() {
|
||||
this.add.text(CX, 52, 'Craps', {
|
||||
fontFamily: 'JqkasWild', fontSize: '50px', color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: '50px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
this.statusText = this.add.text(CX, 96, '', {
|
||||
|
|
@ -209,7 +209,7 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
|
||||
_renderNumberBox(z) {
|
||||
this.add.text(z.center.x, z.center.y - 16, z.label, {
|
||||
fontFamily: 'JqkasWild', fontSize: '38px', color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: '38px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
this.add.text(z.center.x, z.center.y + 24, z.payout, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.accentHex,
|
||||
|
|
@ -218,14 +218,14 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
|
||||
_renderLineZone(z, text, fontSize) {
|
||||
this.add.text(z.center.x, z.center.y, text, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: `${fontSize}px`, color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: `${fontSize}px`, color: COLORS.textHex,
|
||||
letterSpacing: 14,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
}
|
||||
|
||||
_renderDontZone(z, fontSize, ls) {
|
||||
this.add.text(z.center.x, z.center.y - 11, "DON'T PASS", {
|
||||
fontFamily: '"Julius Sans One"', fontSize: `${fontSize}px`, color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: `${fontSize}px`, color: COLORS.textHex,
|
||||
letterSpacing: ls,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
this.add.text(z.center.x, z.center.y + 17, z.barText, {
|
||||
|
|
@ -235,10 +235,10 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
|
||||
_renderDontComeZone(z) {
|
||||
this.add.text(z.center.x, z.center.y - 44, "DON'T", {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: '18px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
this.add.text(z.center.x, z.center.y, 'COME', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: '18px', color: COLORS.textHex,
|
||||
letterSpacing: 8,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
this.add.text(z.center.x, z.center.y + 44, z.barText, {
|
||||
|
|
@ -249,7 +249,7 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
_renderFieldZone(z) {
|
||||
// "FIELD" label left-anchored
|
||||
this.add.text(z.rect.x + 55, z.center.y, 'FIELD', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: '20px', color: COLORS.textHex,
|
||||
letterSpacing: 6,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
|
||||
|
|
@ -271,13 +271,13 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
cg.lineStyle(2, COLORS.gold, 1);
|
||||
cg.strokeCircle(nx, ny, 22);
|
||||
this.add.text(nx, ny, String(n), {
|
||||
fontFamily: 'JqkasWild', fontSize: n === 12 ? '22px' : '22px',
|
||||
fontFamily: 'Caesar', fontSize: n === 12 ? '22px' : '22px',
|
||||
color: COLORS.textDarkHex,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
} else {
|
||||
const fs = (n === 10 || n === 11) ? '24px' : '28px';
|
||||
this.add.text(nx, ny, String(n), {
|
||||
fontFamily: 'JqkasWild', fontSize: fs, color: COLORS.textHex,
|
||||
fontFamily: 'Caesar', fontSize: fs, color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||
}
|
||||
});
|
||||
|
|
@ -327,7 +327,7 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
buildTooltip() {
|
||||
const bg = this.add.graphics();
|
||||
const title = this.add.text(0, 0, '', {
|
||||
fontFamily: 'JqkasWild', fontSize: '30px', color: COLORS.goldHex,
|
||||
fontFamily: 'Caesar', fontSize: '30px', color: COLORS.goldHex,
|
||||
});
|
||||
const desc = this.add.text(0, 0, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
|
||||
|
|
@ -389,7 +389,7 @@ export default class CrapsGame extends Phaser.Scene {
|
|||
this.puck.add(g);
|
||||
this.puckGfx = g;
|
||||
this.puckText = this.add.text(0, 0, 'OFF', {
|
||||
fontFamily: 'JqkasWild', fontSize: '18px', color: '#1a1208',
|
||||
fontFamily: 'Caesar', fontSize: '18px', color: '#1a1208',
|
||||
}).setOrigin(0.5);
|
||||
this.puck.add(this.puckText);
|
||||
this.drawPuck(false);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const CARD_H = 118;
|
|||
const CARD_R = 8;
|
||||
const FAN_Y = 26; // vertical fan offset for work piles
|
||||
const WASTE_FAN = 30; // horizontal fan for the waste's visible cards
|
||||
const NERTS_FAN = 8; // horizontal fan offset for the nerts pile cards
|
||||
|
||||
const D = {
|
||||
felt: -1, pile: 5, card: 10, drag: 40, ui: 30, fly: 50, panel: 28, modal: 80,
|
||||
|
|
@ -50,7 +51,7 @@ const FOUND_ROW_Y = [286, 286 + CARD_H + 24];
|
|||
// x values are recomputed in buildFoundations() based on player count; NERTS x mirrors STOCK x
|
||||
let NERTS_POS = { x: 340, y: 640 - CARD_H - 16 };
|
||||
let WORK_TOP_Y = 540;
|
||||
const WORK_X = [540, 720, 900, 1080];
|
||||
const WORK_X = [800, 990, 1180, 1370];
|
||||
let STOCK_POS = { x: 1470, y: 640 };
|
||||
let WASTE_POS = { x: 1600, y: 640 };
|
||||
const LOCAL_PORTRAIT = { x: 130, y: 820, r: 58 };
|
||||
|
|
@ -83,6 +84,7 @@ export default class NertsGame extends Phaser.Scene {
|
|||
this.foundationCardObjs = []; // foundation top-card sprites
|
||||
this.foundationPos = []; // idx → {x,y}
|
||||
this.foundationSlotRects = [];
|
||||
this.foundationHints = []; // idx → { bg, label } shown when slot is empty
|
||||
this.oppPanelPos = []; // seat → {x,y} (portrait pos, for fly origin)
|
||||
this.oppDynamic = []; // seat → { nertsText, scoreText }
|
||||
this.opponentPortraits = [];
|
||||
|
|
@ -98,6 +100,7 @@ export default class NertsGame extends Phaser.Scene {
|
|||
this.potentialDrag = null;
|
||||
this.dragState = null;
|
||||
this.dropHighlight = null;
|
||||
this.foundationPulseObjs = [];
|
||||
this.roundEnding = false;
|
||||
this.panelObjs = [];
|
||||
}
|
||||
|
|
@ -137,15 +140,15 @@ export default class NertsGame extends Phaser.Scene {
|
|||
const total = 4 * this.playerCount;
|
||||
const countInRow0 = Math.min(FOUND_PER_ROW, total);
|
||||
const rowW = countInRow0 * CARD_W + (countInRow0 - 1) * FOUND_GAP_X;
|
||||
const PAD = 14;
|
||||
STOCK_POS.x = CX + rowW / 2 + PAD + CARD_W / 2 - 150;
|
||||
NERTS_POS.x = STOCK_POS.x;
|
||||
WASTE_POS.x = STOCK_POS.x + CARD_W + FOUND_GAP_X;
|
||||
// Nerts + stock + waste live on the left; work piles are on the right.
|
||||
NERTS_POS.x = 600;
|
||||
STOCK_POS.x = 495;
|
||||
WASTE_POS.x = 605;
|
||||
|
||||
const yOffset = this.opponents.length >= 2 ? 100 : 0;
|
||||
WORK_TOP_Y = 540 + yOffset;
|
||||
STOCK_POS.y = 640 + yOffset;
|
||||
WASTE_POS.y = 640 + yOffset;
|
||||
STOCK_POS.y = 715 + yOffset;
|
||||
WASTE_POS.y = 715 + yOffset;
|
||||
NERTS_POS.y = 640 - CARD_H - 16 + yOffset;
|
||||
|
||||
for (let idx = 0; idx < total; idx++) {
|
||||
|
|
@ -160,6 +163,17 @@ export default class NertsGame extends Phaser.Scene {
|
|||
const r = this.add.rectangle(x, y, CARD_W + 4, CARD_H + 4, 0x000000, 0.22)
|
||||
.setStrokeStyle(2, COLORS.muted, 0.5).setDepth(D.pile);
|
||||
this.foundationSlotRects[idx] = r;
|
||||
|
||||
const HINT_SUITS = ['♠', '♥', '♦', '♣'];
|
||||
const HINT_COLORS = ['#aab0cc', '#cc8888', '#cc8888', '#aab0cc'];
|
||||
const suit = idx % 4;
|
||||
const hintBg = this.add.graphics().setDepth(D.card - 1);
|
||||
hintBg.fillStyle(0xffffff, 0.45);
|
||||
hintBg.fillRoundedRect(x - CARD_W / 2, y - CARD_H / 2, CARD_W, CARD_H, CARD_R);
|
||||
const hintLabel = this.add.text(x, y, HINT_SUITS[suit], {
|
||||
fontFamily: 'Arial', fontSize: '52px', color: HINT_COLORS[suit],
|
||||
}).setOrigin(0.5).setAlpha(0.95).setDepth(D.card - 1);
|
||||
this.foundationHints[idx] = { bg: hintBg, label: hintLabel };
|
||||
}
|
||||
this.add.text(CX, FOUND_ROW_Y[0] - CARD_H / 2 - 26, 'FOUNDATIONS — play Aces here, build up by suit', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||
|
|
@ -458,17 +472,19 @@ export default class NertsGame extends Phaser.Scene {
|
|||
this.clearLocalCards();
|
||||
const p = this.gs.players[0];
|
||||
|
||||
// Nerts pile: a back beneath (if >1) and the face-up top.
|
||||
if (p.nerts.length > 1) {
|
||||
this.localExtraObjs.push(
|
||||
this.makeCardSprite({ id: 'nerts-back' }, NERTS_POS.x, NERTS_POS.y, { faceUp: false, store: false })
|
||||
);
|
||||
}
|
||||
const nt = p.nerts[p.nerts.length - 1];
|
||||
if (nt) {
|
||||
const c = this.makeCardSprite(nt, NERTS_POS.x, NERTS_POS.y, { faceUp: true });
|
||||
this.makeDraggable(c, { kind: 'nerts' });
|
||||
}
|
||||
// Nerts pile: fan left-to-right so card count is visible; top card face-up and draggable.
|
||||
p.nerts.forEach((card, i) => {
|
||||
const x = NERTS_POS.x - (p.nerts.length - 1 - i) * NERTS_FAN;
|
||||
const isTop = i === p.nerts.length - 1;
|
||||
if (isTop) {
|
||||
const c = this.makeCardSprite(card, x, NERTS_POS.y, { faceUp: true });
|
||||
this.makeDraggable(c, { kind: 'nerts' });
|
||||
} else {
|
||||
this.localExtraObjs.push(
|
||||
this.makeCardSprite(card, x, NERTS_POS.y, { faceUp: false, store: false })
|
||||
);
|
||||
}
|
||||
});
|
||||
this.localNertsText.setText(`${p.nerts.length} left`);
|
||||
|
||||
// Work piles: fan downward; cards starting a valid run are draggable.
|
||||
|
|
@ -512,7 +528,10 @@ export default class NertsGame extends Phaser.Scene {
|
|||
const slot = this.gs.foundations[idx];
|
||||
const complete = slot && slot.cards.length > 0 && slot.cards[slot.cards.length - 1].rank === 'K';
|
||||
this.foundationSlotRects[idx]?.setFillStyle(complete ? 0x111111 : 0x000000, complete ? 0.5 : 0.22);
|
||||
if (!slot || slot.cards.length === 0) continue;
|
||||
const empty = !slot || slot.cards.length === 0;
|
||||
this.foundationHints[idx]?.bg.setVisible(empty);
|
||||
this.foundationHints[idx]?.label.setVisible(empty);
|
||||
if (empty) continue;
|
||||
const top = slot.cards[slot.cards.length - 1];
|
||||
const pos = this.foundationPos[idx];
|
||||
const c = this.makeCardSprite(top, pos.x, pos.y, {
|
||||
|
|
@ -601,6 +620,10 @@ export default class NertsGame extends Phaser.Scene {
|
|||
g.strokeRoundedRect(x + 6, y + 6, CARD_W - 12, CARD_H - 12, CARD_R - 2);
|
||||
container.add(g);
|
||||
}
|
||||
const edge = this.add.graphics();
|
||||
edge.lineStyle(1.5, 0xffffff, 0.28);
|
||||
edge.strokeRoundedRect(x, y, CARD_W, CARD_H, CARD_R);
|
||||
container.add(edge);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -690,6 +713,25 @@ export default class NertsGame extends Phaser.Scene {
|
|||
return [];
|
||||
}
|
||||
|
||||
startFoundationPulse(card) {
|
||||
if (!card || card.rank !== 'A') return;
|
||||
for (let f = 0; f < this.foundationPos.length; f++) {
|
||||
if (!canPlayOnFoundation(this.gs, card, f)) continue;
|
||||
const { x, y } = this.foundationPos[f];
|
||||
const rect = this.add.rectangle(x, y, CARD_W + 18, CARD_H + 18, 0xffd700, 0.55)
|
||||
.setStrokeStyle(3, 0xffd700, 1).setDepth(D.card - 1).setAlpha(0.55);
|
||||
const tween = this.tweens.add({
|
||||
targets: rect, alpha: 0.15, duration: 520, ease: 'Sine.easeInOut', yoyo: true, repeat: -1,
|
||||
});
|
||||
this.foundationPulseObjs.push({ rect, tween });
|
||||
}
|
||||
}
|
||||
|
||||
stopFoundationPulse() {
|
||||
for (const { rect, tween } of this.foundationPulseObjs) { tween.stop(); rect.destroy(); }
|
||||
this.foundationPulseObjs = [];
|
||||
}
|
||||
|
||||
setupDragHandlers() {
|
||||
this.input.on('pointermove', (pointer) => {
|
||||
if (!pointer.isDown) return;
|
||||
|
|
@ -719,6 +761,10 @@ export default class NertsGame extends Phaser.Scene {
|
|||
this.tweens.add({ targets: obj, scaleX: 1.06, scaleY: 1.06, duration: 90 });
|
||||
});
|
||||
this.dragState = pd;
|
||||
const dragCard = pd.descriptor.kind === 'nerts' ? nertsTop(this.gs, 0)
|
||||
: pd.descriptor.kind === 'waste' ? wasteTop(this.gs, 0)
|
||||
: workTop(this.gs, 0, pd.descriptor.idx);
|
||||
this.startFoundationPulse(dragCard);
|
||||
}
|
||||
|
||||
updateDrag(pointer) {
|
||||
|
|
@ -759,6 +805,7 @@ export default class NertsGame extends Phaser.Scene {
|
|||
const ds = this.dragState;
|
||||
this.dragState = null;
|
||||
if (this.dropHighlight) { this.dropHighlight.destroy(); this.dropHighlight = null; }
|
||||
this.stopFoundationPulse();
|
||||
|
||||
const primary = ds.sprites[0].obj;
|
||||
const target = this.getDropTargetAt(primary.x, primary.y);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
import { Deck } from '../cards/Deck.js';
|
||||
|
||||
export const NERTS_PILE_SIZE = 13;
|
||||
// Suit required to start each foundation slot (cycles every 4 slots).
|
||||
export const FOUNDATION_SUITS = ['s', 'h', 'd', 'c'];
|
||||
export const WORK_PILE_COUNT = 4;
|
||||
export const STOCK_FLIP = 3;
|
||||
export const DEFAULT_TARGET_SCORE = 100;
|
||||
|
|
@ -152,7 +154,7 @@ export function foundationTopRank(slot) {
|
|||
export function canPlayOnFoundation(state, card, fIdx) {
|
||||
if (!card) return false;
|
||||
const slot = state.foundations[fIdx];
|
||||
if (!slot) return rv(card) === 1; // empty slot needs an Ace
|
||||
if (!slot) return rv(card) === 1 && card.suit === FOUNDATION_SUITS[fIdx % 4]; // Ace of required suit
|
||||
if (card.suit !== slot.suit) return false;
|
||||
const top = foundationTopRank(slot);
|
||||
if (top >= 13) return false; // completed
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JqkasWild';
|
||||
src: url('/assets/fonts/JqkasWild.ttf') format('truetype');
|
||||
font-family: 'Caesar';
|
||||
src: url('/assets/fonts/Caesar.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue