diff --git a/public/assets/images/opponents.png b/public/assets/images/opponents.png index c32f9d4..637471f 100644 Binary files a/public/assets/images/opponents.png and b/public/assets/images/opponents.png differ diff --git a/public/assets/images/opponents.psd b/public/assets/images/opponents.psd index d4d4b06..eea1bf2 100644 Binary files a/public/assets/images/opponents.psd and b/public/assets/images/opponents.psd differ diff --git a/public/assets/images/originals/kage.png b/public/assets/images/originals/kage.png index 538a36d..b79bc9a 100644 Binary files a/public/assets/images/originals/kage.png and b/public/assets/images/originals/kage.png differ diff --git a/public/assets/speech/dv-8-2303-happy-01.mp3 b/public/assets/speech/dv-8-2303-happy-01.mp3 new file mode 100644 index 0000000..c166f58 Binary files /dev/null and b/public/assets/speech/dv-8-2303-happy-01.mp3 differ diff --git a/public/assets/speech/dv-8-2303-happy-02.mp3 b/public/assets/speech/dv-8-2303-happy-02.mp3 new file mode 100644 index 0000000..0e875ee Binary files /dev/null and b/public/assets/speech/dv-8-2303-happy-02.mp3 differ diff --git a/public/assets/speech/dv-8-2303-happy-03.mp3 b/public/assets/speech/dv-8-2303-happy-03.mp3 new file mode 100644 index 0000000..e7fb55d Binary files /dev/null and b/public/assets/speech/dv-8-2303-happy-03.mp3 differ diff --git a/public/assets/speech/dv-8-2303-happy-04.mp3 b/public/assets/speech/dv-8-2303-happy-04.mp3 new file mode 100644 index 0000000..f1ce952 Binary files /dev/null and b/public/assets/speech/dv-8-2303-happy-04.mp3 differ diff --git a/public/assets/speech/dv-8-2303-happy-05.mp3 b/public/assets/speech/dv-8-2303-happy-05.mp3 new file mode 100644 index 0000000..77f177a Binary files /dev/null and b/public/assets/speech/dv-8-2303-happy-05.mp3 differ diff --git a/public/assets/speech/dv-8-2303-intro-01.mp3 b/public/assets/speech/dv-8-2303-intro-01.mp3 new file mode 100644 index 0000000..f8c9a1c Binary files /dev/null and b/public/assets/speech/dv-8-2303-intro-01.mp3 differ diff --git a/public/assets/speech/dv-8-2303-intro-02.mp3 b/public/assets/speech/dv-8-2303-intro-02.mp3 new file mode 100644 index 0000000..7787546 Binary files /dev/null and b/public/assets/speech/dv-8-2303-intro-02.mp3 differ diff --git a/public/assets/speech/dv-8-2303-pick.mp3 b/public/assets/speech/dv-8-2303-pick.mp3 new file mode 100644 index 0000000..c49e416 Binary files /dev/null and b/public/assets/speech/dv-8-2303-pick.mp3 differ diff --git a/public/assets/speech/dv-8-2303-upset-01.mp3 b/public/assets/speech/dv-8-2303-upset-01.mp3 new file mode 100644 index 0000000..b815dba Binary files /dev/null and b/public/assets/speech/dv-8-2303-upset-01.mp3 differ diff --git a/public/assets/speech/dv-8-2303-upset-02.mp3 b/public/assets/speech/dv-8-2303-upset-02.mp3 new file mode 100644 index 0000000..ee21a32 Binary files /dev/null and b/public/assets/speech/dv-8-2303-upset-02.mp3 differ diff --git a/public/assets/speech/dv-8-2303-upset-03.mp3 b/public/assets/speech/dv-8-2303-upset-03.mp3 new file mode 100644 index 0000000..d3f1047 Binary files /dev/null and b/public/assets/speech/dv-8-2303-upset-03.mp3 differ diff --git a/public/assets/speech/dv-8-2303-upset-04.mp3 b/public/assets/speech/dv-8-2303-upset-04.mp3 new file mode 100644 index 0000000..5343a56 Binary files /dev/null and b/public/assets/speech/dv-8-2303-upset-04.mp3 differ diff --git a/public/assets/speech/dv-8-2303-upset-05.mp3 b/public/assets/speech/dv-8-2303-upset-05.mp3 new file mode 100644 index 0000000..65ea9d3 Binary files /dev/null and b/public/assets/speech/dv-8-2303-upset-05.mp3 differ diff --git a/public/assets/speech/kage-happy-01.mp3 b/public/assets/speech/kage-happy-01.mp3 new file mode 100644 index 0000000..8314fe3 Binary files /dev/null and b/public/assets/speech/kage-happy-01.mp3 differ diff --git a/public/assets/speech/kage-happy-02.mp3 b/public/assets/speech/kage-happy-02.mp3 new file mode 100644 index 0000000..31dd297 Binary files /dev/null and b/public/assets/speech/kage-happy-02.mp3 differ diff --git a/public/assets/speech/kage-happy-03.mp3 b/public/assets/speech/kage-happy-03.mp3 new file mode 100644 index 0000000..9e27122 Binary files /dev/null and b/public/assets/speech/kage-happy-03.mp3 differ diff --git a/public/assets/speech/kage-happy-04.mp3 b/public/assets/speech/kage-happy-04.mp3 new file mode 100644 index 0000000..8ab20b8 Binary files /dev/null and b/public/assets/speech/kage-happy-04.mp3 differ diff --git a/public/assets/speech/kage-happy-05.mp3 b/public/assets/speech/kage-happy-05.mp3 new file mode 100644 index 0000000..f77d349 Binary files /dev/null and b/public/assets/speech/kage-happy-05.mp3 differ diff --git a/public/assets/speech/kage-intro-01.mp3 b/public/assets/speech/kage-intro-01.mp3 new file mode 100644 index 0000000..bbc57c3 Binary files /dev/null and b/public/assets/speech/kage-intro-01.mp3 differ diff --git a/public/assets/speech/kage-intro-02.mp3 b/public/assets/speech/kage-intro-02.mp3 new file mode 100644 index 0000000..458a271 Binary files /dev/null and b/public/assets/speech/kage-intro-02.mp3 differ diff --git a/public/assets/speech/kage-pick.mp3 b/public/assets/speech/kage-pick.mp3 new file mode 100644 index 0000000..515ae7b Binary files /dev/null and b/public/assets/speech/kage-pick.mp3 differ diff --git a/public/assets/speech/kage-upset-01.mp3 b/public/assets/speech/kage-upset-01.mp3 new file mode 100644 index 0000000..fb3129a Binary files /dev/null and b/public/assets/speech/kage-upset-01.mp3 differ diff --git a/public/assets/speech/kage-upset-02.mp3 b/public/assets/speech/kage-upset-02.mp3 new file mode 100644 index 0000000..58cd590 Binary files /dev/null and b/public/assets/speech/kage-upset-02.mp3 differ diff --git a/public/assets/speech/kage-upset-03.mp3 b/public/assets/speech/kage-upset-03.mp3 new file mode 100644 index 0000000..67de570 Binary files /dev/null and b/public/assets/speech/kage-upset-03.mp3 differ diff --git a/public/assets/speech/kage-upset-04.mp3 b/public/assets/speech/kage-upset-04.mp3 new file mode 100644 index 0000000..b95ce98 Binary files /dev/null and b/public/assets/speech/kage-upset-04.mp3 differ diff --git a/public/assets/speech/kage-upset-05.mp3 b/public/assets/speech/kage-upset-05.mp3 new file mode 100644 index 0000000..c16d79f Binary files /dev/null and b/public/assets/speech/kage-upset-05.mp3 differ diff --git a/public/assets/speech/nicole-happy-01.mp3 b/public/assets/speech/nicole-happy-01.mp3 new file mode 100644 index 0000000..7bac345 Binary files /dev/null and b/public/assets/speech/nicole-happy-01.mp3 differ diff --git a/public/assets/speech/nicole-happy-02.mp3 b/public/assets/speech/nicole-happy-02.mp3 new file mode 100644 index 0000000..473f0a2 Binary files /dev/null and b/public/assets/speech/nicole-happy-02.mp3 differ diff --git a/public/assets/speech/nicole-happy-03.mp3 b/public/assets/speech/nicole-happy-03.mp3 new file mode 100644 index 0000000..83665fb Binary files /dev/null and b/public/assets/speech/nicole-happy-03.mp3 differ diff --git a/public/assets/speech/nicole-happy-04.mp3 b/public/assets/speech/nicole-happy-04.mp3 new file mode 100644 index 0000000..592ed3c Binary files /dev/null and b/public/assets/speech/nicole-happy-04.mp3 differ diff --git a/public/assets/speech/nicole-happy-05.mp3 b/public/assets/speech/nicole-happy-05.mp3 new file mode 100644 index 0000000..85d3f2a Binary files /dev/null and b/public/assets/speech/nicole-happy-05.mp3 differ diff --git a/public/assets/speech/nicole-intro-01.mp3 b/public/assets/speech/nicole-intro-01.mp3 new file mode 100644 index 0000000..e450938 Binary files /dev/null and b/public/assets/speech/nicole-intro-01.mp3 differ diff --git a/public/assets/speech/nicole-intro-02.mp3 b/public/assets/speech/nicole-intro-02.mp3 new file mode 100644 index 0000000..6b2f155 Binary files /dev/null and b/public/assets/speech/nicole-intro-02.mp3 differ diff --git a/public/assets/speech/nicole-pick.mp3 b/public/assets/speech/nicole-pick.mp3 new file mode 100644 index 0000000..d50b015 Binary files /dev/null and b/public/assets/speech/nicole-pick.mp3 differ diff --git a/public/assets/speech/nicole-upset-01.mp3 b/public/assets/speech/nicole-upset-01.mp3 new file mode 100644 index 0000000..e6b3b8c Binary files /dev/null and b/public/assets/speech/nicole-upset-01.mp3 differ diff --git a/public/assets/speech/nicole-upset-02.mp3 b/public/assets/speech/nicole-upset-02.mp3 new file mode 100644 index 0000000..bb1e8c4 Binary files /dev/null and b/public/assets/speech/nicole-upset-02.mp3 differ diff --git a/public/assets/speech/nicole-upset-03.mp3 b/public/assets/speech/nicole-upset-03.mp3 new file mode 100644 index 0000000..aae8490 Binary files /dev/null and b/public/assets/speech/nicole-upset-03.mp3 differ diff --git a/public/assets/speech/nicole-upset-04.mp3 b/public/assets/speech/nicole-upset-04.mp3 new file mode 100644 index 0000000..c0fa332 Binary files /dev/null and b/public/assets/speech/nicole-upset-04.mp3 differ diff --git a/public/assets/speech/nicole-upset-05.mp3 b/public/assets/speech/nicole-upset-05.mp3 new file mode 100644 index 0000000..8e5ce13 Binary files /dev/null and b/public/assets/speech/nicole-upset-05.mp3 differ diff --git a/public/assets/videos/kage-happy.mp4 b/public/assets/videos/kage-happy.mp4 new file mode 100644 index 0000000..4428ebc Binary files /dev/null and b/public/assets/videos/kage-happy.mp4 differ diff --git a/public/assets/videos/kage-idle.mp4 b/public/assets/videos/kage-idle.mp4 new file mode 100644 index 0000000..98b4f62 Binary files /dev/null and b/public/assets/videos/kage-idle.mp4 differ diff --git a/public/assets/videos/kage-upset.mp4 b/public/assets/videos/kage-upset.mp4 new file mode 100644 index 0000000..b9ddf92 Binary files /dev/null and b/public/assets/videos/kage-upset.mp4 differ diff --git a/public/assets/videos/nicole-happy.mp4 b/public/assets/videos/nicole-happy.mp4 new file mode 100644 index 0000000..2abfc97 Binary files /dev/null and b/public/assets/videos/nicole-happy.mp4 differ diff --git a/public/assets/videos/nicole-idle.mp4 b/public/assets/videos/nicole-idle.mp4 new file mode 100644 index 0000000..63fed66 Binary files /dev/null and b/public/assets/videos/nicole-idle.mp4 differ diff --git a/public/assets/videos/nicole-upset.mp4 b/public/assets/videos/nicole-upset.mp4 new file mode 100644 index 0000000..2386d1b Binary files /dev/null and b/public/assets/videos/nicole-upset.mp4 differ diff --git a/public/data/opponents.json b/public/data/opponents.json index 08eb8e2..f7ba626 100644 --- a/public/data/opponents.json +++ b/public/data/opponents.json @@ -590,7 +590,7 @@ }, { "id": "smasher", - "spriteIndex": 19, + "spriteIndex": 21, "name": "The Smasher", "bio": "Oooooooh yeah brother!!! You wanna play with The Smasher?!", "speech": { @@ -616,6 +616,93 @@ "smasher-pick" ] } + }, + { + "id": "dv-8-2303", + "spriteIndex": 22, + "name": "DV-8-2303", + "bio": "Cybernetic Systems online. Activate human defeat protocol... EOF", + "speech": { + "intro": [ + "dv-8-2303-intro-01", + "dv-8-2303-intro-02" + ], + "happy": [ + "dv-8-2303-happy-01", + "dv-8-2303-happy-02", + "dv-8-2303-happy-03", + "dv-8-2303-happy-04", + "dv-8-2303-happy-05" + ], + "upset": [ + "dv-8-2303-upset-01", + "dv-8-2303-upset-02", + "dv-8-2303-upset-03", + "dv-8-2303-upset-04", + "dv-8-2303-upset-05" + ], + "pick": [ + "dv-8-2303-pick" + ] + } + }, + { + "id": "nicole", + "spriteIndex": 23, + "name": "Nicole", + "bio": "I've disabled the firewall protections on this website. Now to hack the core!", + "speech": { + "intro": [ + "nicole-intro-01", + "nicole-intro-02" + ], + "happy": [ + "nicole-happy-01", + "nicole-happy-02", + "nicole-happy-03", + "nicole-happy-04", + "nicole-happy-05" + ], + "upset": [ + "nicole-upset-01", + "nicole-upset-02", + "nicole-upset-03", + "nicole-upset-04", + "nicole-upset-05" + ], + "pick": [ + "nicole-pick" + ] + } + }, + { + "id": "kage", + "spriteIndex": 24, + "name": "Kage", + "bio": "I have traveled far to play these games. Honor me and let us play!", + "speech": { + "intro": [ + "kage-intro-01", + "kage-intro-02" + ], + "happy": [ + "kage-happy-01", + "kage-happy-02", + "kage-happy-03", + "kage-happy-04", + "kage-happy-05" + ], + "upset": [ + "kage-upset-01", + "kage-upset-02", + "kage-upset-03", + "kage-upset-04", + "kage-upset-05" + ], + "pick": [ + "kage-pick" + ] + } } ] } \ No newline at end of file diff --git a/public/src/games/canasta/CanastaGame.js b/public/src/games/canasta/CanastaGame.js index 7909f91..c2d2ddd 100644 --- a/public/src/games/canasta/CanastaGame.js +++ b/public/src/games/canasta/CanastaGame.js @@ -5,7 +5,7 @@ import { MusicPlayer } from '../../ui/MusicPlayer.js'; import { playSound, SFX } from '../../ui/Sounds.js'; import { api } from '../../services/api.js'; import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; -import { THEME, TEAM_OF_SEAT, isWild, isRedThree, minimumMeld } from './CanastaData.js'; +import { THEME, TEAM_OF_SEAT, isWild, isRedThree, minimumMeld, cardScore, NATURAL_CANASTA, MIXED_CANASTA } from './CanastaData.js'; import { createInitialState, drawStock, takeDiscard, meld, discard, startNextHand, takePlan, teamMeld, isCanasta, isNaturalCanasta, meetsInitialMeld, teamHasCanasta, @@ -24,10 +24,10 @@ const STOCK = { x: CX - 96, y: CY }; const DISCARD = { x: CX + 96, y: CY }; const SEAT = { - 0: { hand: { x: CX, y: 930 }, port: { x: 150, y: 930 }, name: { x: 230, y: 858 } }, - 1: { hand: { x: 96, y: CY }, port: { x: 110, y: 215 }, name: { x: 110, y: 285 } }, - 2: { hand: { x: CX, y: 78 }, port: { x: 150, y: 96 }, name: { x: 230, y: 150 } }, - 3: { hand: { x: GAME_WIDTH - 96, y: CY }, port: { x: GAME_WIDTH - 110, y: 215 }, name: { x: GAME_WIDTH - 110, y: 285 } }, + 0: { hand: { x: CX, y: 930 }, port: { x: 650, y: 930 }, name: { x: 530, y: 858 } }, + 1: { hand: { x: 96, y: CY }, port: { x: 110, y: 290 }, name: { x: 110, y: 360 } }, + 2: { hand: { x: CX, y: 78 }, port: { x: 650, y: 96 }, name: { x: 530, y: 150 } }, + 3: { hand: { x: GAME_WIDTH - 96, y: CY }, port: { x: GAME_WIDTH - 110, y: 290 }, name: { x: GAME_WIDTH - 110, y: 360 } }, }; const MELD_ZONE = { 0: { y: 690 }, 1: { y: 282 } }; @@ -56,10 +56,14 @@ export default class CanastaGame extends Phaser.Scene { 3: this.opponents[2]?.name ?? 'East', }; + this.playfield = data.playfield ?? null; + this.state = createInitialState(); this.selected = new Set(); this.busy = false; this.recorded = false; + this.turnStartState = null; + this.meldDropZones = []; this.handObjs = { 0: [], 1: [], 2: [], 3: [] }; this.meldObjs = []; this.pileObjs = []; @@ -70,6 +74,7 @@ export default class CanastaGame extends Phaser.Scene { create() { try { new MusicPlayer(this, this.cache.json.get('music')?.tracks ?? []); } catch { /* optional */ } + this.input.dragDistanceThreshold = 8; this.buildBackdrop(); this.buildCenterLabels(); @@ -81,16 +86,40 @@ export default class CanastaGame extends Phaser.Scene { }).setOrigin(0.5).setDepth(D.ui).setShadow(0, 2, '#000', 6); this.buildButtons(); + + // Meld drop-zone highlight — drawn once in local space, repositioned during drag. + this.meldHighlight = this.add.graphics().setDepth(D.zone + 1).setVisible(false); + this.meldHighlight.fillStyle(THEME.brassHi, 0.13); + this.meldHighlight.lineStyle(2.5, THEME.brassHi, 0.9); + this.meldHighlight.fillRoundedRect(-72, -52, 144, 104, 14); + this.meldHighlight.strokeRoundedRect(-72, -52, 144, 104, 14); + this.renderDynamic(); this.advance(); } // ── Backdrop: luxe felt with a spotlight, burgundy rail and brass oval ──────── buildBackdrop() { + const pf = this.playfield; + const hasPlayfield = pf?.key && this.textures.exists(pf.key); + + if (hasPlayfield) { + this.add.image(CX, GAME_HEIGHT / 2, pf.key) + .setDisplaySize(GAME_WIDTH, GAME_HEIGHT) + .setDepth(D.felt - 1); + } + const g = this.add.graphics().setDepth(D.felt); g.fillGradientStyle(THEME.feltMid, THEME.feltMid, THEME.feltEdge, THEME.feltEdge, 1); g.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); + if (hasPlayfield) { + const maskShape = this.add.graphics().setVisible(false); + maskShape.fillStyle(0xffffff); + maskShape.fillEllipse(CX, CY + 30, 1640, 1180); + g.setMask(new Phaser.Display.Masks.GeometryMask(this, maskShape)); + } + // Soft radial spotlight over the table centre (stacked translucent ellipses). const glow = this.add.graphics().setDepth(D.glow); for (let i = 8; i >= 1; i--) { @@ -135,23 +164,29 @@ export default class CanastaGame extends Phaser.Scene { // ── Team score panels ──────────────────────────────────────────────────────── buildPanels() { this.panel = {}; - const make = (teamId, x, y, accent, title) => { + const make = (teamId, x, y, accent, title, backSeat, frontSeat) => { const g = this.add.graphics().setDepth(D.ui); - g.fillStyle(0x000000, 0.42); g.fillRoundedRect(x, y, 290, 96, 12); - g.lineStyle(2, accent, 0.9); g.strokeRoundedRect(x, y, 290, 96, 12); - this.add.text(x + 18, y + 14, title, { + g.fillStyle(0x000000, 0.42); g.fillRoundedRect(x, y, 360, 114, 12); + g.lineStyle(2, accent, 0.9); g.strokeRoundedRect(x, y, 360, 114, 12); + + // Two overlapping portrait pictures left of the text content. + const _p = []; + this.addMiniPortrait(_p, backSeat, x + 24, y + 48, 20, D.ui + 1); + this.addMiniPortrait(_p, frontSeat, x + 44, y + 64, 20, D.ui + 4); + + this.add.text(x + 74, y + 14, title, { fontFamily: '"Julius Sans One"', fontSize: '18px', color: HEX(accent), - }).setDepth(D.ui); - const score = this.add.text(x + 18, y + 40, '0', { + }).setDepth(D.ui + 7); + const score = this.add.text(x + 74, y + 38, '0', { fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex, - }).setDepth(D.ui); - const sub = this.add.text(x + 270, y + 54, '', { + }).setDepth(D.ui + 7); + const sub = this.add.text(x + 338, y + 82, '', { fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, - }).setOrigin(1, 0.5).setDepth(D.ui); + }).setOrigin(1, 0.5).setDepth(D.ui + 7); this.panel[teamId] = { score, sub }; }; - make(0, GAME_WIDTH - 320, GAME_HEIGHT - 130, THEME.team0, 'YOUR TEAM'); - make(1, GAME_WIDTH - 320, 30, THEME.team1, 'OPPONENTS'); + make(0, 1400, MELD_ZONE[0].y - 57 + 75, THEME.team0, 'YOUR TEAM', 2, 0); + make(1, 200, MELD_ZONE[1].y - 57 - 75, THEME.team1, 'OPPONENTS', 3, 1); } buildPortraits() { @@ -182,6 +217,8 @@ export default class CanastaGame extends Phaser.Scene { { width: 220, height: 56, fontSize: 22 }).setDepth(D.ui); this.btn.meld = new Button(this, CX + 110, y, 'Meld', () => this.humanMeld(), { width: 200, height: 56, fontSize: 22 }).setDepth(D.ui); + this.btn.reset = new Button(this, CX - 110, y, 'Reset Turn', () => this.humanResetTurn(), + { variant: 'ghost', width: 200, height: 56, fontSize: 20 }).setDepth(D.ui); this.btn.discard = new Button(this, CX + 330, y, 'Discard', () => this.humanDiscard(), { width: 200, height: 56, fontSize: 22 }).setDepth(D.ui); this.btn.next = new Button(this, CX, y, 'Next Hand', () => this.onNextHand(), @@ -192,7 +229,7 @@ export default class CanastaGame extends Phaser.Scene { } hideActionButtons() { - ['draw', 'take', 'meld', 'discard'].forEach((k) => this.btn[k].setVisible(false)); + ['draw', 'take', 'meld', 'reset', 'discard'].forEach((k) => this.btn[k].setVisible(false)); } // ── Card rendering ─────────────────────────────────────────────────────────── @@ -252,25 +289,45 @@ export default class CanastaGame extends Phaser.Scene { renderPiles() { const s = this.state; + const humanDrawPhase = s.phase === 'draw' && s.currentPlayer === 0; + // Stock as a neat stack of backs. const n = Math.min(6, Math.ceil(s.stock.length / 12)); + let stockTop = null; for (let i = 0; i < n; i++) { - this.pileObjs.push(this.makeCard(null, STOCK.x - i * 2, STOCK.y - i * 2, { faceUp: false, depth: D.pile + i })); + const c = this.makeCard(null, STOCK.x - i * 2, STOCK.y - i * 2, { faceUp: false, depth: D.pile + i }); + this.pileObjs.push(c); + stockTop = c; } this.stockCount.setText(`${s.stock.length} left`); + if (humanDrawPhase && stockTop) { + this.makeInteractive(stockTop, () => this.humanDraw('stock')); + } + // Discard pile: a slightly fanned stack, top card face up. const dn = Math.min(5, s.discard.length); + let discardTop = null; for (let i = 0; i < dn; i++) { const card = s.discard[s.discard.length - dn + i]; const top = i === dn - 1; - this.pileObjs.push(this.makeCard(card, DISCARD.x + (i - dn) * 3, DISCARD.y + (i - dn) * 2, { faceUp: top, depth: D.pile + i })); + const c = this.makeCard(card, DISCARD.x + (i - dn) * 3, DISCARD.y + (i - dn) * 2, { faceUp: top, depth: D.pile + i }); + this.pileObjs.push(c); + if (top) discardTop = c; } this.frozenBadge.setVisible(s.frozen); + + if (humanDrawPhase && discardTop) { + const plan = takePlan(s, 0); + const t = s.teams[0]; + const legal = !!plan && (t.hasMelded || takeYieldsLegalTurn(s, 0, 5, plan)); + if (legal) this.makeInteractive(discardTop, () => this.humanDraw('take')); + } } renderHands() { const s = this.state; + const meldPhase = s.phase === 'meld' && s.currentPlayer === 0; // Human hand — face up, fanned, sorted for readability. const me = s.players[0].hand.slice().sort(this.handSort); const total = (me.length - 1) * HAND_GAP + CARD_W; @@ -280,7 +337,53 @@ export default class CanastaGame extends Phaser.Scene { const c = this.makeCard(card, x, SEAT[0].hand.y - (sel ? 26 : 0), { depth: D.card + i }); c.baseY = SEAT[0].hand.y; if (isRedThree(card)) this.glowCard(c); - this.makeInteractive(c, () => this.toggleSelect(card.id)); + if (meldPhase) { + c.setSize(CARD_W, CARD_H); + c.setInteractive({ useHandCursor: true }); + this.input.setDraggable(c); + let dragged = false; + c.on('dragstart', () => { + dragged = true; + c._ox = c.x; c._oy = c.y; + c.setDepth(D.card + 50); + }); + c.on('drag', (pointer, dragX, dragY) => { + c.x = dragX; c.y = dragY; + const zone = this.meldDropZones.find(z => Math.abs(dragX - z.x) < 75 && Math.abs(dragY - z.y) < 65); + if (zone) { + this.meldHighlight.setVisible(true).setPosition(zone.x, zone.y); + } else { + this.meldHighlight.setVisible(false); + } + }); + c.on('dragend', () => { + this.meldHighlight.setVisible(false); + c.setDepth(D.card + i); + const dropX = c.x, dropY = c.y; + c.x = c._ox; c.y = c._oy; + // Check meld drop zones first. + const zone = this.meldDropZones.find(z => Math.abs(dropX - z.x) < 75 && Math.abs(dropY - z.y) < 65); + if (zone) { + const next = meld(this.state, zone.rank, [card.id]); + if (next !== this.state) { + this.state = next; + playSound(this, SFX.CARD_PLACE); + this.renderDynamic(); + this.updateMeldPhaseUi(); + } else { + this.toast('Cannot add that card to this meld', CX, 560, '#e06c75'); + } + return; + } + if (Math.abs(dropX - DISCARD.x) < 80 && Math.abs(dropY - DISCARD.y) < 100) { + this.selected = new Set([card.id]); + this.humanDiscard({ animate: false }); + } + }); + c.on('pointerup', () => { if (!dragged) this.toggleSelect(card.id); dragged = false; }); + } else { + this.makeInteractive(c, () => this.toggleSelect(card.id)); + } this.handObjs[0].push(c); }); @@ -304,6 +407,8 @@ export default class CanastaGame extends Phaser.Scene { } renderMelds() { + this.meldDropZones = []; + const meldPhase = this.state.phase === 'meld' && this.state.currentPlayer === 0; for (const teamId of [0, 1]) { const melds = this.state.teams[teamId].melds; const reds = this.state.teams[teamId].redThrees.length; @@ -311,7 +416,11 @@ export default class CanastaGame extends Phaser.Scene { const groupW = 150; const totalW = Math.max(1, melds.length + (reds ? 1 : 0)) * groupW; let gx = CX - totalW / 2 + groupW / 2; - for (const m of melds) { this.renderMeldGroup(m, gx, y); gx += groupW; } + for (const m of melds) { + this.renderMeldGroup(m, gx, y, teamId === 0 && meldPhase); + if (teamId === 0 && meldPhase) this.meldDropZones.push({ rank: m.rank, x: gx, y }); + gx += groupW; + } if (reds) { const badge = this.add.container(gx, y).setDepth(D.zone); const g = this.add.graphics(); @@ -325,7 +434,7 @@ export default class CanastaGame extends Phaser.Scene { } } - renderMeldGroup(m, x, y) { + renderMeldGroup(m, x, y, showScore = false) { const cont = this.add.container(x, y).setDepth(D.zone); const canasta = isCanasta(m); if (canasta) { @@ -347,6 +456,14 @@ export default class CanastaGame extends Phaser.Scene { fontFamily: 'Righteous', fontSize: canasta ? '14px' : '15px', color: canasta ? HEX(isNaturalCanasta(m) ? THEME.brassHi : 0xcfd6e0) : COLORS.mutedHex, }).setOrigin(0.5)); + if (showScore) { + const cardVal = m.cards.reduce((s, c) => s + cardScore(c), 0); + const bonus = canasta ? (isNaturalCanasta(m) ? NATURAL_CANASTA : MIXED_CANASTA) : 0; + const scoreLabel = bonus > 0 ? `+${bonus}+${cardVal}` : `+${cardVal}`; + cont.add(this.add.text(0, 55, scoreLabel, { + fontFamily: '"Julius Sans One"', fontSize: '12px', color: '#90c878', + }).setOrigin(0.5)); + } this.meldObjs.push(cont); } @@ -382,6 +499,55 @@ export default class CanastaGame extends Phaser.Scene { c.on('pointerdown', handler); } + // ── Draw animation ────────────────────────────────────────────────────────── + // Animates a single card flying from (fromX,fromY) to (toX,toY) over 1200 ms. + // faceUp: starting face state. flip: if true, scaleX collapses at 600 ms, + // the container is redrawn face-up, then scaleX expands back to 1. + animateDraw({ fromX, fromY, toX, toY, card, faceUp = true, flip = false, duration = 1200, startDelay = 0 }) { + return new Promise(resolve => { + const anim = this.makeCard(card, fromX, fromY, { faceUp, depth: D.card + 100 }); + const half = duration / 2; + + this.tweens.add({ + targets: anim, + x: toX, y: toY, + duration, + delay: startDelay, + ease: 'Power2.easeInOut', + onComplete: () => { anim.destroy(); resolve(); }, + }); + + if (flip) { + // Fold (scaleX → 0) during first half, swap to face-up, unfold during second half. + this.tweens.add({ + targets: anim, + scaleX: 0, + duration: half, + delay: startDelay, + ease: 'Sine.easeIn', + onComplete: () => { + anim.removeAll(true); + this.drawFace(anim, card, true); + this.tweens.add({ targets: anim, scaleX: 1, duration: half, ease: 'Sine.easeOut' }); + }, + }); + } + }); + } + + getMeldGroupPos(teamId, rank) { + const melds = this.state.teams[teamId].melds; + const reds = this.state.teams[teamId].redThrees.length; + const groupW = 150; + const totalW = Math.max(1, melds.length + (reds ? 1 : 0)) * groupW; + let gx = CX - totalW / 2 + groupW / 2; + for (const m of melds) { + if (m.rank === rank) return { x: gx, y: MELD_ZONE[teamId].y }; + gx += groupW; + } + return { x: CX, y: MELD_ZONE[teamId].y }; + } + // ── Turn flow ──────────────────────────────────────────────────────────────── advance() { this.updateTurnRing(); @@ -403,38 +569,68 @@ export default class CanastaGame extends Phaser.Scene { // Draw. const draw = chooseDraw(this.state, seat, skill); + let drawnCard, drawFromX, drawFromY, drawFaceUp; if (draw.type === 'take') { + drawnCard = this.state.discard[this.state.discard.length - 1]; + drawFromX = DISCARD.x; drawFromY = DISCARD.y; drawFaceUp = true; this.state = takeDiscard(this.state, draw.plan); this.toast(`${this.seatName[seat]} takes the pile`, DISCARD.x, DISCARD.y - 80); playSound(this, SFX.CARD_SHOW); } else { + drawnCard = this.state.stock[0]; + drawFromX = STOCK.x; drawFromY = STOCK.y; drawFaceUp = false; this.state = drawStock(this.state); playSound(this, SFX.CARD_DEAL); } + if (drawnCard && !['handOver', 'gameOver'].includes(this.state.phase)) { + await this.animateDraw({ + fromX: drawFromX, fromY: drawFromY, + toX: SEAT[seat].hand.x, toY: SEAT[seat].hand.y, + card: drawnCard, faceUp: drawFaceUp, flip: false, + }); + } this.renderDynamic(); if (['handOver', 'gameOver'].includes(this.state.phase)) { this.busy = false; return this.advance(); } - await this.delay(420); + await this.delay(750); // Meld. const { actions } = chooseMelds(this.state, seat, skill); + const teamId = TEAM_OF_SEAT[seat]; for (const a of actions) { - const before = this.state.teams[TEAM_OF_SEAT[seat]].melds.find((m) => m.rank === a.rank); + const before = this.state.teams[teamId].melds.find((m) => m.rank === a.rank); const wasCanasta = before ? isCanasta(before) : false; + const meldCards = a.cardIds.map(id => this.state.players[seat].hand.find(c => c.id === id)).filter(Boolean); this.state = meld(this.state, a.rank, a.cardIds); - const after = this.state.teams[TEAM_OF_SEAT[seat]].melds.find((m) => m.rank === a.rank); + const after = this.state.teams[teamId].melds.find((m) => m.rank === a.rank); + const meldPos = this.getMeldGroupPos(teamId, a.rank); + const duration = before ? 750 : 500; + const stagger = before ? 80 : 60; + await Promise.all(meldCards.map((card, i) => + this.animateDraw({ + fromX: SEAT[seat].hand.x, fromY: SEAT[seat].hand.y, + toX: meldPos.x, toY: meldPos.y, + card, faceUp: false, flip: true, duration, startDelay: i * stagger, + }) + )); this.renderDynamic(); playSound(this, SFX.CARD_PLACE); - if (after && isCanasta(after) && !wasCanasta) this.toast('CANASTA!', CX, MELD_ZONE[TEAM_OF_SEAT[seat]].y - 70, '#f0d77a'); - await this.delay(360); + if (after && isCanasta(after) && !wasCanasta) this.toast('CANASTA!', CX, MELD_ZONE[teamId].y - 70, '#f0d77a'); } // Discard (ends the turn, or the hand if it empties). if (this.state.phase === 'meld') { const cardId = chooseDiscard(this.state, seat, skill); + const discardCard = this.state.players[seat].hand.find((c) => c.id === cardId); this.state = discard(this.state, cardId); + if (discardCard) { + await this.animateDraw({ + fromX: SEAT[seat].hand.x, fromY: SEAT[seat].hand.y, + toX: DISCARD.x, toY: DISCARD.y, + card: discardCard, faceUp: false, flip: true, duration: 1000, + }); + } this.renderDynamic(); playSound(this, SFX.CARD_PLACE); - await this.delay(360); } this.busy = false; @@ -445,6 +641,7 @@ export default class CanastaGame extends Phaser.Scene { enableHumanTurn() { this.busy = false; const s = this.state; + if (s.phase === 'meld') this.turnStartState = this.state; if (s.phase === 'draw') { this.statusText.setText('Draw from the stock, or take the discard pile'); this.btn.draw.setVisible(true).setEnabled(true); @@ -462,13 +659,20 @@ export default class CanastaGame extends Phaser.Scene { updateMeldPhaseUi() { const s = this.state; const t = s.teams[0]; - const need = !t.hasMelded; - this.statusText.setText(need - ? `Select cards to meld (first meld must total ${this.minNeeded()} pts), then discard one` - : 'Meld more, or select one card and discard'); + const noCanasta = s.players[0].hand.length === 1 && !teamHasCanasta(s, 0); + let statusMsg; + if (noCanasta) { + statusMsg = 'No canasta yet — discarding will end the hand early'; + } else if (!t.hasMelded) { + statusMsg = `Select cards to meld (first meld must total ${this.minNeeded()} pts), then discard one`; + } else { + statusMsg = 'Meld more, or select one card and discard'; + } + this.statusText.setText(statusMsg); this.btn.draw.setVisible(false); this.btn.take.setVisible(false); this.btn.meld.setVisible(true).setEnabled(this.selected.size >= 1); + this.btn.reset.setVisible(s.meldedThisTurn.length > 0); this.btn.discard.setVisible(true).setEnabled(this.selected.size === 1 && this.discardAllowed()); } @@ -480,8 +684,6 @@ export default class CanastaGame extends Phaser.Scene { // Mirror the engine: an under-minimum initial meld can't be closed with a discard. const t = this.state.teams[0]; if (!t.hasMelded && this.state.meldedThisTurn.length > 0 && !meetsInitialMeld(this.state, 0)) return false; - // Going out requires a canasta. - if (this.state.players[0].hand.length === 1 && !teamHasCanasta(this.state, 0)) return false; return true; } @@ -496,19 +698,42 @@ export default class CanastaGame extends Phaser.Scene { this.updateMeldPhaseUi(); } - humanDraw(kind) { + async humanDraw(kind) { if (this.busy) return; + this.busy = true; + this.hideActionButtons(); + + let drawnCard, fromX, fromY, faceUp, flip; if (kind === 'stock') { + drawnCard = this.state.stock[0]; + fromX = STOCK.x; fromY = STOCK.y; faceUp = false; flip = true; this.state = drawStock(this.state); playSound(this, SFX.CARD_DEAL); } else { + drawnCard = this.state.discard[this.state.discard.length - 1]; + fromX = DISCARD.x; fromY = DISCARD.y; faceUp = true; flip = false; const plan = takePlan(this.state, 0); - if (!plan) return; + if (!plan) { this.busy = false; return; } this.state = takeDiscard(this.state, plan); playSound(this, SFX.CARD_SHOW); } + + // Compute where the card will land in the human hand after the state update. + // For discard take, the top card goes to a meld (not hand), so idx = -1 → fallback centre. + const me = this.state.players[0].hand.slice().sort(this.handSort); + const total = (me.length - 1) * HAND_GAP + CARD_W; + const idx = drawnCard ? me.findIndex(c => c.id === drawnCard.id) : -1; + const toX = idx >= 0 + ? SEAT[0].hand.x - total / 2 + CARD_W / 2 + idx * HAND_GAP + : SEAT[0].hand.x; + const toY = SEAT[0].hand.y; + this.selected.clear(); + if (drawnCard) { + await this.animateDraw({ fromX, fromY, toX, toY, card: drawnCard, faceUp, flip }); + } this.renderDynamic(); + this.busy = false; this.advance(); } @@ -522,7 +747,7 @@ export default class CanastaGame extends Phaser.Scene { if (!rank) { // All wilds — only valid as an addition to an existing single team meld. const melds = this.state.teams[0].melds; - if (melds.length !== 1) return this.toast('Add wilds to a specific meld', CX, 560, '#e06c75'); + if (melds.length !== 1) return this.toast('Drag wild cards onto a specific meld', CX, 560, '#e06c75'); rank = melds[0].rank; } const next = meld(this.state, rank, ids); @@ -534,27 +759,109 @@ export default class CanastaGame extends Phaser.Scene { this.updateMeldPhaseUi(); } - humanDiscard() { + humanResetTurn() { + if (this.busy || !this.turnStartState) return; + this.state = this.turnStartState; + this.selected.clear(); + playSound(this, SFX.CARD_PLACE); + this.renderDynamic(); + this.updateMeldPhaseUi(); + } + + async humanDiscard({ animate = true } = {}) { if (this.busy || this.selected.size !== 1) return; if (!this.discardAllowed()) { return this.toast(`Initial meld needs ${this.minNeeded()} points`, CX, 560, '#e06c75'); } + this.busy = true; + this.hideActionButtons(); + const id = [...this.selected][0]; + const card = this.state.players[0].hand.find((c) => c.id === id); + + // Compute card's current hand position before state changes. + const me = this.state.players[0].hand.slice().sort(this.handSort); + const total = (me.length - 1) * HAND_GAP + CARD_W; + const idx = me.findIndex((c) => c.id === id); + const fromX = SEAT[0].hand.x - total / 2 + CARD_W / 2 + idx * HAND_GAP; + const fromY = SEAT[0].hand.y; + const goingOut = this.state.players[0].hand.length === 1; this.state = discard(this.state, id); + this.turnStartState = null; this.selected.clear(); + + if (animate && card) { + await this.animateDraw({ + fromX, fromY, toX: DISCARD.x, toY: DISCARD.y, + card, faceUp: true, flip: false, duration: 1000, + }); + } + playSound(this, SFX.CARD_PLACE); - if (goingOut) this.toast('You go out!', CX, 560, '#f0d77a'); + if (goingOut && teamHasCanasta(this.state, 0)) this.toast('You go out!', CX, 560, '#f0d77a'); this.renderDynamic(); + this.busy = false; this.advance(); } // ── Hand-over & game-over ──────────────────────────────────────────────────── + addMiniPortrait(layer, seat, cx, cy, radius, depth) { + const size = radius * 2; + const backing = this.add.graphics().setDepth(depth); + backing.fillStyle(0x1a1208, 1); + backing.fillCircle(cx, cy, radius); + layer.push(backing); + + if (seat === 0) { + const avatarKey = Object.keys(this.textures.list).find(k => k.startsWith('player-avatar-')); + if (avatarKey) { + const maskG = this.make.graphics({ x: 0, y: 0, add: false }); + maskG.fillStyle(0xffffff); maskG.fillCircle(cx, cy, radius); + layer.push(this.add.image(cx, cy, avatarKey).setDisplaySize(size, size) + .setMask(maskG.createGeometryMask()).setDepth(depth + 1)); + } else { + // Show initial while avatar loads; swap it in when the texture arrives. + const placeholder = this.add.text(cx, cy, 'Y', { + fontFamily: '"Julius Sans One"', fontSize: `${Math.round(radius * 0.85)}px`, + color: HEX(THEME.brassHi), + }).setOrigin(0.5).setDepth(depth + 1); + layer.push(placeholder); + const onTex = (key) => { + if (!key.startsWith('player-avatar-')) return; + this.textures.off('addtexture', onTex); + if (!placeholder.active) return; + placeholder.destroy(); + const maskG = this.make.graphics({ x: 0, y: 0, add: false }); + maskG.fillStyle(0xffffff); maskG.fillCircle(cx, cy, radius); + this.add.image(cx, cy, key).setDisplaySize(size, size) + .setMask(maskG.createGeometryMask()).setDepth(depth + 1); + }; + this.textures.on('addtexture', onTex); + } + } else { + const opp = this.seatOpp[seat]; + if (this.textures.exists('opponents')) { + const maskG = this.make.graphics({ x: 0, y: 0, add: false }); + maskG.fillStyle(0xffffff); maskG.fillCircle(cx, cy, radius); + layer.push(this.add.image(cx, cy, 'opponents', opp?.spriteIndex ?? 0) + .setDisplaySize(size, size) + .setMask(maskG.createGeometryMask()) + .setDepth(depth + 1)); + } + } + + const ring = this.add.graphics().setDepth(depth + 2); + ring.lineStyle(2.5, THEME.rail, 1); ring.strokeCircle(cx, cy, radius); + layer.push(ring); + } + showHandOver() { this.hideActionButtons(); this.updateTurnRing(); const scores = this.state.handScores; - const w = 760, h = 420, x = CX - w / 2, y = CY - h / 2 + 20; + const w = 840, h = 540, x = CX - w / 2, y = CY - h / 2 + 20; + const halfW = w / 2; // 420 const layer = []; const g = this.add.graphics().setDepth(D.modal); g.fillStyle(0x000000, 0.6); g.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); @@ -563,11 +870,21 @@ export default class CanastaGame extends Phaser.Scene { layer.push(g); layer.push(this.add.text(CX, y + 36, 'Hand Over', { fontFamily: 'Righteous', fontSize: '40px', color: HEX(THEME.brassHi) }).setOrigin(0.5).setDepth(D.modalUI)); + // Portrait radius and diagonal offset for the overlapping pair + const PR = 22; const names = ['YOUR TEAM', 'OPPONENTS']; [0, 1].forEach((teamId, idx) => { const d = scores[idx]; const col = idx === 0 ? THEME.team0 : THEME.team1; - const px = x + 60 + idx * (w / 2 - 20); + const colX = x + idx * halfW; + const px = colX + 95; + const portMidY = y + 215; + + // Two portraits overlapping diagonally — back (up-left) then front (down-right) + const [backSeat, frontSeat] = teamId === 0 ? [2, 0] : [3, 1]; + this.addMiniPortrait(layer, backSeat, colX + 36, portMidY - 12, PR, D.modalUI); + this.addMiniPortrait(layer, frontSeat, colX + 58, portMidY + 10, PR, D.modalUI + 2); + layer.push(this.add.text(px, y + 92, names[idx], { fontFamily: '"Julius Sans One"', fontSize: '22px', color: HEX(col) }).setDepth(D.modalUI)); const lines = [ `Melds ${d.meldPoints}`, @@ -583,8 +900,9 @@ export default class CanastaGame extends Phaser.Scene { fontFamily: 'Righteous', fontSize: '20px', color: COLORS.textHex, lineSpacing: 8, }).setDepth(D.modalUI)); }); + layer.push(new Button(this, CX, y + h - 52, 'Next Round', () => this.onNextHand(), + { width: 280, height: 56, fontSize: 24 }).setDepth(D.modalUI)); this.handOverLayer = layer; - this.btn.next.setVisible(true); } onNextHand() { diff --git a/public/src/games/cribbage/CribbageGame.js b/public/src/games/cribbage/CribbageGame.js index b1729c9..4a65e73 100644 --- a/public/src/games/cribbage/CribbageGame.js +++ b/public/src/games/cribbage/CribbageGame.js @@ -209,7 +209,7 @@ export default class CribbageGame extends Phaser.Scene { fontFamily: 'Righteous', fontSize: '30px', color: '#f0d77a', }).setOrigin(0.5).setDepth(D.ui); - this.statusText = this.add.text(CENTER_X, 412, '', { + this.statusText = this.add.text(CENTER_X, PLAYER_HAND_Y - 100, '', { fontFamily: '"Julius Sans One"', fontSize: '28px', color: COLORS.textHex, }).setOrigin(0.5).setDepth(D.ui); @@ -382,8 +382,10 @@ export default class CribbageGame extends Phaser.Scene { playSound(this, SFX.CARD_SHOW); await this.delay(300); // flip - this.tweens.add({ targets: this.starterSprite, scaleX: 0, duration: 140, yoyo: true, - onYoyo: () => { this.starterSprite.removeAll(true); this.drawFace(this.starterSprite, starter, true); } }); + this.tweens.add({ + targets: this.starterSprite, scaleX: 0, duration: 140, yoyo: true, + onYoyo: () => { this.starterSprite.removeAll(true); this.drawFace(this.starterSprite, starter, true); } + }); await this.delay(360); if (heels) { this.floatScore(this.logic.dealer, 2, 'His Heels!'); @@ -484,8 +486,10 @@ export default class CribbageGame extends Phaser.Scene { this.countText.setText('Count: 0'); const going = this.pegSprites; this.pegSprites = []; - going.forEach((c) => this.tweens.add({ targets: c, y: PEG_ROW_Y + 120, alpha: 0, duration: 300, - onComplete: () => c.destroy() })); + going.forEach((c) => this.tweens.add({ + targets: c, y: PEG_ROW_Y + 120, alpha: 0, duration: 300, + onComplete: () => c.destroy() + })); await this.delay(340); } @@ -499,8 +503,10 @@ export default class CribbageGame extends Phaser.Scene { } labelFor(reasons) { - const map = { '15': 'Fifteen', '31': 'Thirty-one', pair: 'Pair', 'pair-royal': 'Three!', 'double-pair-royal': 'Four!', - run3: 'Run', run4: 'Run', run5: 'Run', run6: 'Run', run7: 'Run', go: 'Go', 'last-card': 'Last card' }; + const map = { + '15': 'Fifteen', '31': 'Thirty-one', pair: 'Pair', 'pair-royal': 'Three!', 'double-pair-royal': 'Four!', + run3: 'Run', run4: 'Run', run5: 'Run', run6: 'Run', run7: 'Run', go: 'Go', 'last-card': 'Last card' + }; return (reasons || []).map((r) => map[r] || r).join(' + '); } @@ -510,8 +516,10 @@ export default class CribbageGame extends Phaser.Scene { const txt = this.add.text(pt.x, pt.y - 18, `+${points}${label ? ' ' + label : ''}`, { fontFamily: 'Righteous', fontSize: '24px', color: player === 0 ? '#f07a6d' : '#fff4d8', }).setOrigin(0.5).setDepth(D.toast).setShadow(0, 0, 'rgba(255,207,74,0.85)', 12); - this.tweens.add({ targets: txt, y: pt.y - 64, alpha: 0, duration: 1100, ease: 'Cubic.easeOut', - onComplete: () => txt.destroy() }); + this.tweens.add({ + targets: txt, y: pt.y - 64, alpha: 0, duration: 1100, ease: 'Cubic.easeOut', + onComplete: () => txt.destroy() + }); } // ── The show ──────────────────────────────────────────────────────────────── @@ -526,8 +534,10 @@ export default class CribbageGame extends Phaser.Scene { const card = this.logic.keep[1][i]; if (!card) return; c.cardRef = card; - this.tweens.add({ targets: c, scaleX: 0, duration: 120, yoyo: true, - onYoyo: () => { c.removeAll(true); this.drawFace(c, card, true); } }); + this.tweens.add({ + targets: c, scaleX: 0, duration: 120, yoyo: true, + onYoyo: () => { c.removeAll(true); this.drawFace(c, card, true); } + }); }); await this.delay(500); @@ -571,11 +581,14 @@ export default class CribbageGame extends Phaser.Scene { g.fillStyle(THEME.woodDark, 0.96); g.fillRoundedRect(cx - w / 2, cy - h / 2, w, h, 14); g.lineStyle(3, THEME.brass, 0.9); g.strokeRoundedRect(cx - w / 2, cy - h / 2, w, h, 14); const t1 = this.add.text(cx, cy - 34, `${o.title} — ${o.isCrib ? 'CRIB' : 'HAND'}`, { - fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(D.modalUI); + fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.goldHex + }).setOrigin(0.5).setDepth(D.modalUI); const t2 = this.add.text(cx, cy + 2, body, { - fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.modalUI); + fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex + }).setOrigin(0.5).setDepth(D.modalUI); const t3 = this.add.text(cx, cy + 40, `${r.total} point${r.total === 1 ? '' : 's'}`, { - fontFamily: 'Righteous', fontSize: '30px', color: '#f0d77a' }).setOrigin(0.5).setDepth(D.modalUI) + fontFamily: 'Righteous', fontSize: '30px', color: '#f0d77a' + }).setOrigin(0.5).setDepth(D.modalUI) .setShadow(0, 0, 'rgba(255,207,74,0.8)', 12); this.countPanel = [g, t1, t2, t3]; } @@ -617,10 +630,12 @@ export default class CribbageGame extends Phaser.Scene { g.lineStyle(4, THEME.brass, 1); g.strokeRoundedRect(GAME_WIDTH / 2 - 360, 360, 720, 360, 24); this.add.text(GAME_WIDTH / 2, 450, won ? 'You Win!' : `${this.aiName} Wins`, { - fontFamily: 'Righteous', fontSize: '60px', color: won ? '#f0d77a' : '#e9e1cf' }) + fontFamily: 'Righteous', fontSize: '60px', color: won ? '#f0d77a' : '#e9e1cf' + }) .setOrigin(0.5).setDepth(D.modalUI); this.add.text(GAME_WIDTH / 2, 530, `${this.logic.scores[0]} – ${this.logic.scores[1]}${skunk ? ' (Skunk!)' : ''}`, { - fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex }) + fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex + }) .setOrigin(0.5).setDepth(D.modalUI); new Button(this, GAME_WIDTH / 2, 650, 'Back to Menu', () => this.scene.start('GameMenu'), { width: 300, fontSize: 26 }).setDepth(D.modalUI);