diff --git a/public/assets/fonts/Caesar.ttf b/public/assets/fonts/Caesar.ttf new file mode 100644 index 0000000..3c1c48c Binary files /dev/null and b/public/assets/fonts/Caesar.ttf differ diff --git a/public/assets/music/track17.mp3 b/public/assets/music/track17.mp3 new file mode 100644 index 0000000..06b60d0 Binary files /dev/null and b/public/assets/music/track17.mp3 differ diff --git a/public/assets/music/track18.mp3 b/public/assets/music/track18.mp3 new file mode 100644 index 0000000..191e67b Binary files /dev/null and b/public/assets/music/track18.mp3 differ diff --git a/public/data/music.json b/public/data/music.json index aa56a3b..6582b41 100644 --- a/public/data/music.json +++ b/public/data/music.json @@ -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" } ] } \ No newline at end of file diff --git a/public/src/games/blockfighter/BlockFighterGame.js b/public/src/games/blockfighter/BlockFighterGame.js index 408ded7..4a27354 100644 --- a/public/src/games/blockfighter/BlockFighterGame.js +++ b/public/src/games/blockfighter/BlockFighterGame.js @@ -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( diff --git a/public/src/games/craps/CrapsGame.js b/public/src/games/craps/CrapsGame.js index bae7a63..cadc7a9 100644 --- a/public/src/games/craps/CrapsGame.js +++ b/public/src/games/craps/CrapsGame.js @@ -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); diff --git a/public/src/games/nerts/NertsGame.js b/public/src/games/nerts/NertsGame.js index 725f271..4094d21 100644 --- a/public/src/games/nerts/NertsGame.js +++ b/public/src/games/nerts/NertsGame.js @@ -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); diff --git a/public/src/games/nerts/NertsLogic.js b/public/src/games/nerts/NertsLogic.js index e90f66b..6ee4079 100644 --- a/public/src/games/nerts/NertsLogic.js +++ b/public/src/games/nerts/NertsLogic.js @@ -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 diff --git a/public/styles.css b/public/styles.css index c73b3f8..39fc416 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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; }