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",
|
"file": "track16.mp3",
|
||||||
"artist": "Surf Sundown",
|
"artist": "Surf Sundown",
|
||||||
"title": "Khruangbin"
|
"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 locked = false;
|
||||||
let events = null;
|
let events = null;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'left': if (moveLeft(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.PIECE_CLICK); 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.PIECE_CLICK); 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.PIECE_CLICK); break;
|
case 'rotateCCW': if (rotateCCW(m, idx) && idx === 0) playSound(this, SFX.UI_PICK); break;
|
||||||
case 'softDrop': {
|
case 'softDrop': {
|
||||||
const r = stepDown(m, idx);
|
const r = stepDown(m, idx);
|
||||||
locked = r.locked; events = r.events;
|
locked = r.locked; events = r.events;
|
||||||
|
|
@ -525,7 +525,7 @@ export default class BlockFighterGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLocked(idx, events) {
|
onLocked(idx, events) {
|
||||||
playSound(this, SFX.CARD_PLACE);
|
playSound(this, SFX.UI_PLACE);
|
||||||
this.updatePieceSprites(idx); // hides the piece (now null)
|
this.updatePieceSprites(idx); // hides the piece (now null)
|
||||||
this.enqueue(idx, events);
|
this.enqueue(idx, events);
|
||||||
}
|
}
|
||||||
|
|
@ -542,6 +542,7 @@ export default class BlockFighterGame extends Phaser.Scene {
|
||||||
case 'spawn':
|
case 'spawn':
|
||||||
this.updatePieceSprites(idx);
|
this.updatePieceSprites(idx);
|
||||||
this.updatePreviews();
|
this.updatePreviews();
|
||||||
|
if (idx === 0) playSound(this, SFX.UI_CHIME);
|
||||||
break;
|
break;
|
||||||
case 'place':
|
case 'place':
|
||||||
case 'settle':
|
case 'settle':
|
||||||
|
|
@ -550,12 +551,12 @@ export default class BlockFighterGame extends Phaser.Scene {
|
||||||
break;
|
break;
|
||||||
case 'garbage':
|
case 'garbage':
|
||||||
this.renderBoard(idx, e);
|
this.renderBoard(idx, e);
|
||||||
if (e.cells?.length) playSound(this, SFX.DICE_ROLL);
|
if (e.cells?.length) playSound(this, SFX.GEM_CHAIN);
|
||||||
break;
|
break;
|
||||||
case 'clear':
|
case 'clear':
|
||||||
case 'diamond': {
|
case 'diamond': {
|
||||||
this.renderBoard(idx, e);
|
this.renderBoard(idx, e);
|
||||||
playSound(this, SFX.MASTERMIND_MATCH ?? SFX.CARD_SHOW);
|
playSound(this, SFX.GEM_MATCH_4);
|
||||||
for (const cell of e.cells ?? []) {
|
for (const cell of e.cells ?? []) {
|
||||||
if (cell.r < HIDDEN_ROWS) continue;
|
if (cell.r < HIDDEN_ROWS) continue;
|
||||||
const fx = this.add.rectangle(
|
const fx = this.add.rectangle(
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export default class CrapsGame extends Phaser.Scene {
|
||||||
|
|
||||||
buildTitle() {
|
buildTitle() {
|
||||||
this.add.text(CX, 52, 'Craps', {
|
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);
|
}).setOrigin(0.5).setDepth(D.ui);
|
||||||
|
|
||||||
this.statusText = this.add.text(CX, 96, '', {
|
this.statusText = this.add.text(CX, 96, '', {
|
||||||
|
|
@ -209,7 +209,7 @@ export default class CrapsGame extends Phaser.Scene {
|
||||||
|
|
||||||
_renderNumberBox(z) {
|
_renderNumberBox(z) {
|
||||||
this.add.text(z.center.x, z.center.y - 16, z.label, {
|
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);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
this.add.text(z.center.x, z.center.y + 24, z.payout, {
|
this.add.text(z.center.x, z.center.y + 24, z.payout, {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.accentHex,
|
fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.accentHex,
|
||||||
|
|
@ -218,14 +218,14 @@ export default class CrapsGame extends Phaser.Scene {
|
||||||
|
|
||||||
_renderLineZone(z, text, fontSize) {
|
_renderLineZone(z, text, fontSize) {
|
||||||
this.add.text(z.center.x, z.center.y, text, {
|
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,
|
letterSpacing: 14,
|
||||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderDontZone(z, fontSize, ls) {
|
_renderDontZone(z, fontSize, ls) {
|
||||||
this.add.text(z.center.x, z.center.y - 11, "DON'T PASS", {
|
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,
|
letterSpacing: ls,
|
||||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
this.add.text(z.center.x, z.center.y + 17, z.barText, {
|
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) {
|
_renderDontComeZone(z) {
|
||||||
this.add.text(z.center.x, z.center.y - 44, "DON'T", {
|
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);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
this.add.text(z.center.x, z.center.y, 'COME', {
|
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,
|
letterSpacing: 8,
|
||||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
this.add.text(z.center.x, z.center.y + 44, z.barText, {
|
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) {
|
_renderFieldZone(z) {
|
||||||
// "FIELD" label left-anchored
|
// "FIELD" label left-anchored
|
||||||
this.add.text(z.rect.x + 55, z.center.y, 'FIELD', {
|
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,
|
letterSpacing: 6,
|
||||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
|
|
||||||
|
|
@ -271,13 +271,13 @@ export default class CrapsGame extends Phaser.Scene {
|
||||||
cg.lineStyle(2, COLORS.gold, 1);
|
cg.lineStyle(2, COLORS.gold, 1);
|
||||||
cg.strokeCircle(nx, ny, 22);
|
cg.strokeCircle(nx, ny, 22);
|
||||||
this.add.text(nx, ny, String(n), {
|
this.add.text(nx, ny, String(n), {
|
||||||
fontFamily: 'JqkasWild', fontSize: n === 12 ? '22px' : '22px',
|
fontFamily: 'Caesar', fontSize: n === 12 ? '22px' : '22px',
|
||||||
color: COLORS.textDarkHex,
|
color: COLORS.textDarkHex,
|
||||||
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
} else {
|
} else {
|
||||||
const fs = (n === 10 || n === 11) ? '24px' : '28px';
|
const fs = (n === 10 || n === 11) ? '24px' : '28px';
|
||||||
this.add.text(nx, ny, String(n), {
|
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);
|
}).setOrigin(0.5).setDepth(D.zoneLabel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -327,7 +327,7 @@ export default class CrapsGame extends Phaser.Scene {
|
||||||
buildTooltip() {
|
buildTooltip() {
|
||||||
const bg = this.add.graphics();
|
const bg = this.add.graphics();
|
||||||
const title = this.add.text(0, 0, '', {
|
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, '', {
|
const desc = this.add.text(0, 0, '', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
|
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.puck.add(g);
|
||||||
this.puckGfx = g;
|
this.puckGfx = g;
|
||||||
this.puckText = this.add.text(0, 0, 'OFF', {
|
this.puckText = this.add.text(0, 0, 'OFF', {
|
||||||
fontFamily: 'JqkasWild', fontSize: '18px', color: '#1a1208',
|
fontFamily: 'Caesar', fontSize: '18px', color: '#1a1208',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.puck.add(this.puckText);
|
this.puck.add(this.puckText);
|
||||||
this.drawPuck(false);
|
this.drawPuck(false);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ const CARD_H = 118;
|
||||||
const CARD_R = 8;
|
const CARD_R = 8;
|
||||||
const FAN_Y = 26; // vertical fan offset for work piles
|
const FAN_Y = 26; // vertical fan offset for work piles
|
||||||
const WASTE_FAN = 30; // horizontal fan for the waste's visible cards
|
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 = {
|
const D = {
|
||||||
felt: -1, pile: 5, card: 10, drag: 40, ui: 30, fly: 50, panel: 28, modal: 80,
|
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
|
// 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 NERTS_POS = { x: 340, y: 640 - CARD_H - 16 };
|
||||||
let WORK_TOP_Y = 540;
|
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 STOCK_POS = { x: 1470, y: 640 };
|
||||||
let WASTE_POS = { x: 1600, y: 640 };
|
let WASTE_POS = { x: 1600, y: 640 };
|
||||||
const LOCAL_PORTRAIT = { x: 130, y: 820, r: 58 };
|
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.foundationCardObjs = []; // foundation top-card sprites
|
||||||
this.foundationPos = []; // idx → {x,y}
|
this.foundationPos = []; // idx → {x,y}
|
||||||
this.foundationSlotRects = [];
|
this.foundationSlotRects = [];
|
||||||
|
this.foundationHints = []; // idx → { bg, label } shown when slot is empty
|
||||||
this.oppPanelPos = []; // seat → {x,y} (portrait pos, for fly origin)
|
this.oppPanelPos = []; // seat → {x,y} (portrait pos, for fly origin)
|
||||||
this.oppDynamic = []; // seat → { nertsText, scoreText }
|
this.oppDynamic = []; // seat → { nertsText, scoreText }
|
||||||
this.opponentPortraits = [];
|
this.opponentPortraits = [];
|
||||||
|
|
@ -98,6 +100,7 @@ export default class NertsGame extends Phaser.Scene {
|
||||||
this.potentialDrag = null;
|
this.potentialDrag = null;
|
||||||
this.dragState = null;
|
this.dragState = null;
|
||||||
this.dropHighlight = null;
|
this.dropHighlight = null;
|
||||||
|
this.foundationPulseObjs = [];
|
||||||
this.roundEnding = false;
|
this.roundEnding = false;
|
||||||
this.panelObjs = [];
|
this.panelObjs = [];
|
||||||
}
|
}
|
||||||
|
|
@ -137,15 +140,15 @@ export default class NertsGame extends Phaser.Scene {
|
||||||
const total = 4 * this.playerCount;
|
const total = 4 * this.playerCount;
|
||||||
const countInRow0 = Math.min(FOUND_PER_ROW, total);
|
const countInRow0 = Math.min(FOUND_PER_ROW, total);
|
||||||
const rowW = countInRow0 * CARD_W + (countInRow0 - 1) * FOUND_GAP_X;
|
const rowW = countInRow0 * CARD_W + (countInRow0 - 1) * FOUND_GAP_X;
|
||||||
const PAD = 14;
|
// Nerts + stock + waste live on the left; work piles are on the right.
|
||||||
STOCK_POS.x = CX + rowW / 2 + PAD + CARD_W / 2 - 150;
|
NERTS_POS.x = 600;
|
||||||
NERTS_POS.x = STOCK_POS.x;
|
STOCK_POS.x = 495;
|
||||||
WASTE_POS.x = STOCK_POS.x + CARD_W + FOUND_GAP_X;
|
WASTE_POS.x = 605;
|
||||||
|
|
||||||
const yOffset = this.opponents.length >= 2 ? 100 : 0;
|
const yOffset = this.opponents.length >= 2 ? 100 : 0;
|
||||||
WORK_TOP_Y = 540 + yOffset;
|
WORK_TOP_Y = 540 + yOffset;
|
||||||
STOCK_POS.y = 640 + yOffset;
|
STOCK_POS.y = 715 + yOffset;
|
||||||
WASTE_POS.y = 640 + yOffset;
|
WASTE_POS.y = 715 + yOffset;
|
||||||
NERTS_POS.y = 640 - CARD_H - 16 + yOffset;
|
NERTS_POS.y = 640 - CARD_H - 16 + yOffset;
|
||||||
|
|
||||||
for (let idx = 0; idx < total; idx++) {
|
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)
|
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);
|
.setStrokeStyle(2, COLORS.muted, 0.5).setDepth(D.pile);
|
||||||
this.foundationSlotRects[idx] = r;
|
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', {
|
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,
|
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||||
|
|
@ -458,17 +472,19 @@ export default class NertsGame extends Phaser.Scene {
|
||||||
this.clearLocalCards();
|
this.clearLocalCards();
|
||||||
const p = this.gs.players[0];
|
const p = this.gs.players[0];
|
||||||
|
|
||||||
// Nerts pile: a back beneath (if >1) and the face-up top.
|
// Nerts pile: fan left-to-right so card count is visible; top card face-up and draggable.
|
||||||
if (p.nerts.length > 1) {
|
p.nerts.forEach((card, i) => {
|
||||||
this.localExtraObjs.push(
|
const x = NERTS_POS.x - (p.nerts.length - 1 - i) * NERTS_FAN;
|
||||||
this.makeCardSprite({ id: 'nerts-back' }, NERTS_POS.x, NERTS_POS.y, { faceUp: false, store: false })
|
const isTop = i === p.nerts.length - 1;
|
||||||
);
|
if (isTop) {
|
||||||
}
|
const c = this.makeCardSprite(card, x, NERTS_POS.y, { faceUp: true });
|
||||||
const nt = p.nerts[p.nerts.length - 1];
|
this.makeDraggable(c, { kind: 'nerts' });
|
||||||
if (nt) {
|
} else {
|
||||||
const c = this.makeCardSprite(nt, NERTS_POS.x, NERTS_POS.y, { faceUp: true });
|
this.localExtraObjs.push(
|
||||||
this.makeDraggable(c, { kind: 'nerts' });
|
this.makeCardSprite(card, x, NERTS_POS.y, { faceUp: false, store: false })
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.localNertsText.setText(`${p.nerts.length} left`);
|
this.localNertsText.setText(`${p.nerts.length} left`);
|
||||||
|
|
||||||
// Work piles: fan downward; cards starting a valid run are draggable.
|
// 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 slot = this.gs.foundations[idx];
|
||||||
const complete = slot && slot.cards.length > 0 && slot.cards[slot.cards.length - 1].rank === 'K';
|
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);
|
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 top = slot.cards[slot.cards.length - 1];
|
||||||
const pos = this.foundationPos[idx];
|
const pos = this.foundationPos[idx];
|
||||||
const c = this.makeCardSprite(top, pos.x, pos.y, {
|
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);
|
g.strokeRoundedRect(x + 6, y + 6, CARD_W - 12, CARD_H - 12, CARD_R - 2);
|
||||||
container.add(g);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -690,6 +713,25 @@ export default class NertsGame extends Phaser.Scene {
|
||||||
return [];
|
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() {
|
setupDragHandlers() {
|
||||||
this.input.on('pointermove', (pointer) => {
|
this.input.on('pointermove', (pointer) => {
|
||||||
if (!pointer.isDown) return;
|
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.tweens.add({ targets: obj, scaleX: 1.06, scaleY: 1.06, duration: 90 });
|
||||||
});
|
});
|
||||||
this.dragState = pd;
|
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) {
|
updateDrag(pointer) {
|
||||||
|
|
@ -759,6 +805,7 @@ export default class NertsGame extends Phaser.Scene {
|
||||||
const ds = this.dragState;
|
const ds = this.dragState;
|
||||||
this.dragState = null;
|
this.dragState = null;
|
||||||
if (this.dropHighlight) { this.dropHighlight.destroy(); this.dropHighlight = null; }
|
if (this.dropHighlight) { this.dropHighlight.destroy(); this.dropHighlight = null; }
|
||||||
|
this.stopFoundationPulse();
|
||||||
|
|
||||||
const primary = ds.sprites[0].obj;
|
const primary = ds.sprites[0].obj;
|
||||||
const target = this.getDropTargetAt(primary.x, primary.y);
|
const target = this.getDropTargetAt(primary.x, primary.y);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@
|
||||||
import { Deck } from '../cards/Deck.js';
|
import { Deck } from '../cards/Deck.js';
|
||||||
|
|
||||||
export const NERTS_PILE_SIZE = 13;
|
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 WORK_PILE_COUNT = 4;
|
||||||
export const STOCK_FLIP = 3;
|
export const STOCK_FLIP = 3;
|
||||||
export const DEFAULT_TARGET_SCORE = 100;
|
export const DEFAULT_TARGET_SCORE = 100;
|
||||||
|
|
@ -152,7 +154,7 @@ export function foundationTopRank(slot) {
|
||||||
export function canPlayOnFoundation(state, card, fIdx) {
|
export function canPlayOnFoundation(state, card, fIdx) {
|
||||||
if (!card) return false;
|
if (!card) return false;
|
||||||
const slot = state.foundations[fIdx];
|
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;
|
if (card.suit !== slot.suit) return false;
|
||||||
const top = foundationTopRank(slot);
|
const top = foundationTopRank(slot);
|
||||||
if (top >= 13) return false; // completed
|
if (top >= 13) return false; // completed
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JqkasWild';
|
font-family: 'Caesar';
|
||||||
src: url('/assets/fonts/JqkasWild.ttf') format('truetype');
|
src: url('/assets/fonts/Caesar.ttf') format('truetype');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue