diff --git a/public/src/games/mexicantrain/MexicanTrainGame.js b/public/src/games/mexicantrain/MexicanTrainGame.js index f243cc2..638728b 100644 --- a/public/src/games/mexicantrain/MexicanTrainGame.js +++ b/public/src/games/mexicantrain/MexicanTrainGame.js @@ -31,6 +31,21 @@ const HAND_Y = 942; const HAND_HALF = 50; // square half-cell for hand tiles const HAND_PITCH = HAND_HALF + 14; +const BONEYARD_CX = 1600; +const BONEYARD_CY = 950; +const BONEYARD_HALF = 40; // half-cell for face-down pile tiles +// Fixed offsets so the pile looks like a casual scatter (deterministic) +const PILE_OFFSETS = [ + { dx: 0, dy: 0, angle: 2 }, + { dx: -11, dy: 7, angle: 13 }, + { dx: 13, dy: -6, angle: -9 }, + { dx: -15, dy: -5, angle: 20 }, + { dx: 10, dy: 12, angle:-16 }, + { dx: -5, dy:-13, angle: 7 }, + { dx: 19, dy: 4, angle:-24 }, + { dx:-20, dy: 9, angle: 26 }, +]; + const DEPTH = { bg: -1, band: 0, spine: 1, tile: 2, tileFx: 5, hub: 6, portrait: 10, ui: 20, hand: 22, toast: 50, modal: 60, @@ -91,6 +106,7 @@ export default class MexicanTrainGame extends Phaser.Scene { this.buildHeader(); this.buildPortraitsAndLabels(); this.buildRowZones(); + this.buildBoneyard(); new Button(this, 86, GAME_HEIGHT - 44, 'Leave', () => this.scene.start('GameMenu'), { variant: 'ghost', width: 150, fontSize: 20, @@ -104,19 +120,23 @@ export default class MexicanTrainGame extends Phaser.Scene { buildHeader() { this.add.text(CX, 44, 'Mexican Train', { fontFamily: 'Righteous', fontSize: '46px', color: COLORS.textHex, - }).setOrigin(0.5).setDepth(DEPTH.ui); + }).setOrigin(0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(14, 7); this.roundText = this.add.text(CX, 86, '', { fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, - }).setOrigin(0.5).setDepth(DEPTH.ui); + }).setOrigin(0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(12, 4); this.statusText = this.add.text(CX, 118, '', { fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.accentHex, - }).setOrigin(0.5).setDepth(DEPTH.ui); + }).setOrigin(0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(12, 5); - this.boneText = this.add.text(HUB_X, TRACKS_BOTTOM + 18, '', { - fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex, - }).setOrigin(0.5).setDepth(DEPTH.ui); + this.boneText = this.add.text(BONEYARD_CX, BONEYARD_CY + 76, '', { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(10, 4); } rowY(i) { @@ -131,6 +151,7 @@ export default class MexicanTrainGame extends Phaser.Scene { buildPortraitsAndLabels() { const n = this.gs.players.length; + this._handDotGfx = []; for (let i = 0; i < n; i++) { const y = this.rowY(i); const ring = this.add.graphics().setDepth(DEPTH.portrait); @@ -146,8 +167,11 @@ export default class MexicanTrainGame extends Phaser.Scene { const label = this.add.text(LABEL_X, y, '', { fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.textHex, align: 'left', lineSpacing: 2, - }).setOrigin(0, 0.5).setDepth(DEPTH.ui); + }).setOrigin(0, 0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(8, 5); this.labelTexts[String(i)] = label; + + this._handDotGfx.push(this.add.graphics().setDepth(DEPTH.ui)); } // Mexican train row label @@ -155,7 +179,8 @@ export default class MexicanTrainGame extends Phaser.Scene { const mlabel = this.add.text(LABEL_X - 60, my, '', { fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.goldHex, align: 'left', - }).setOrigin(0, 0.5).setDepth(DEPTH.ui); + }).setOrigin(0, 0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(8, 5); this.labelTexts[MEXICAN] = mlabel; } @@ -188,7 +213,7 @@ export default class MexicanTrainGame extends Phaser.Scene { this.roundText.setText( `Round ${this.gs.round + 1} • Engine ${hub}-${hub} • First to ${this.gs.target} ends it — lowest score wins`, ); - this.boneText.setText(`Boneyard: ${this.gs.boneyard.length}`); + this.updateBoneyard(); } paintBands() { @@ -220,7 +245,8 @@ export default class MexicanTrainGame extends Phaser.Scene { if (!this._hubLabel) { this._hubLabel = this.add.text(HUB_X, hubY - 58, 'ENGINE', { fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.goldHex, - }).setOrigin(0.5).setDepth(DEPTH.hub); + }).setOrigin(0.5).setDepth(DEPTH.hub) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(8, 3); } else { this._hubLabel.setY(hubY - 58); } @@ -259,7 +285,8 @@ export default class MexicanTrainGame extends Phaser.Scene { if (!this._truncMarks[i]) { this._truncMarks[i] = this.add.text(TRACKS_LEFT - 2, y, '', { fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, - }).setOrigin(0, 0.5).setDepth(DEPTH.tileFx); + }).setOrigin(0, 0.5).setDepth(DEPTH.tileFx) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(5, 2); } this._truncMarks[i].setText(`+${hidden}«`).setVisible(true); } @@ -347,6 +374,38 @@ export default class MexicanTrainGame extends Phaser.Scene { this.labelTexts[String(i)].setColor(marked ? COLORS.dangerHex : COLORS.textHex); } this.labelTexts[MEXICAN].setText('Mexican Train'); + this.updateHandDots(); + } + + updateHandDots() { + if (!this._handDotGfx) return; + const n = this.gs.players.length; + const half = 7; + const normalPitch = half * 2 + 3; + const maxWidth = TRACKS_LEFT - LABEL_X - half; // drawable x range + for (let i = 0; i < n; i++) { + const g = this._handDotGfx[i]; + g.clear(); + const count = this.gs.players[i].hand.length; + if (count === 0) continue; + const y = this.rowY(i) + 30; + // Compress pitch if tiles would overflow the label area + const pitch = count > 1 ? Math.min(normalPitch, (maxWidth - half) / (count - 1)) : normalPitch; + for (let t = 0; t < count; t++) { + this.paintMiniDomino(g, LABEL_X + half + t * pitch, y, half, true); + } + } + } + + paintMiniDomino(g, cx, cy, half, faceUp = false) { + const w = half * 2; + const h = half; + g.fillStyle(faceUp ? COLORS.text : COLORS.panel, 1); + g.fillRoundedRect(cx - w / 2, cy - h / 2, w, h, 2); + g.lineStyle(1, faceUp ? COLORS.accent : COLORS.muted, faceUp ? 1 : 0.7); + g.strokeRoundedRect(cx - w / 2, cy - h / 2, w, h, 2); + g.lineStyle(1, COLORS.muted, faceUp ? 0.5 : 0.3); + g.lineBetween(cx, cy - h / 2 + 1, cx, cy + h / 2 - 1); } updateStatus() { @@ -395,6 +454,7 @@ export default class MexicanTrainGame extends Phaser.Scene { // ─── Turn flow ──────────────────────────────────────────────────────── nextTurn() { + this.showBoneyardClickable(false); if (this.gs.phase === 'gameover') { this.showGameOverModal(); return; } if (this.gs.phase === 'roundover') { this.showRoundOverModal(); return; } this.refresh(); @@ -418,13 +478,8 @@ export default class MexicanTrainGame extends Phaser.Scene { // No legal play — draw one (once), else pass. this.inputLocked = true; if (!this.humanDrew && canDraw(this.gs)) { - this.statusText.setText('No play — drawing a tile…'); - this.time.delayedCall(650, async () => { - await this.animateDraw(); - this.gs = drawTile(this.gs); - this.humanDrew = true; - this.promptHuman(); - }); + this.statusText.setText('No tiles to play — click the boneyard to draw'); + this.showBoneyardClickable(true); } else { this.statusText.setText('No play — passing'); this.time.delayedCall(750, () => { @@ -533,9 +588,11 @@ export default class MexicanTrainGame extends Phaser.Scene { break; } if (!drew && canDraw(this.gs)) { - await this.delay(420); - await this.animateDraw(); + await this.delay(300); + const portraitSrc = this.portraitCtrls[startPlayer] ?? { x: PORTRAIT_X, y: this.rowY(startPlayer) }; + await this.animateDrawFromPile(portraitSrc.x, portraitSrc.y); this.gs = drawTile(this.gs); + this.refresh(); drew = true; continue; } @@ -586,11 +643,113 @@ export default class MexicanTrainGame extends Phaser.Scene { }); } - async animateDraw() { + // ─── Boneyard pile ─────────────────────────────────────────────────── + buildBoneyard() { + // Pre-create 8 face-down tile containers; show/hide based on boneyard count. + this._boneyardTiles = PILE_OFFSETS.map(({ dx, dy, angle }) => { + const g = this.add.graphics(); + this.paintFaceDownDomino(g, 0, 0, BONEYARD_HALF); + const c = this.add.container(BONEYARD_CX + dx, BONEYARD_CY + dy, [g]); + c.setAngle(angle).setDepth(DEPTH.tile); + return c; + }); + + this.add.text(BONEYARD_CX, BONEYARD_CY - 72, 'BONEYARD', { + fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(DEPTH.ui) + .setBackgroundColor('rgba(0,0,0,0.55)').setPadding(10, 4); + + this._drawToast = this.add.text(BONEYARD_CX, BONEYARD_CY - 100, 'DRAW', { + fontFamily: 'Righteous', fontSize: '38px', color: COLORS.goldHex, + }).setOrigin(0.5).setAlpha(0).setDepth(DEPTH.toast); + + this._boneyardZone = this.add.zone(BONEYARD_CX, BONEYARD_CY, BONEYARD_HALF * 2 + 60, BONEYARD_HALF * 2 + 30) + .setOrigin(0.5).setDepth(DEPTH.tileFx); + this._boneyardZoneActive = false; + this._boneyardZone.on('pointerup', () => this.onBoneyardClick()); + } + + updateBoneyard() { + if (!this._boneyardTiles) return; + const count = this.gs.boneyard.length; + const shown = Math.min(count, 8); + for (let i = 0; i < 8; i++) this._boneyardTiles[i].setVisible(i < shown); + this.boneText.setText(count > 0 ? `${count} tiles` : 'Empty'); + } + + paintFaceDownDomino(g, cx, cy, half) { + const w = half * 2; + const h = half; + g.fillStyle(COLORS.text, 1); + g.fillRoundedRect(cx - w / 2, cy - h / 2, w, h, 5); + g.lineStyle(2, COLORS.accent, 1); + g.strokeRoundedRect(cx - w / 2, cy - h / 2, w, h, 5); + g.lineStyle(1.5, COLORS.muted, 0.5); + g.lineBetween(cx, cy - h / 2 + 3, cx, cy + h / 2 - 3); + g.fillStyle(COLORS.muted, 0.4); + g.fillCircle(cx - half / 2, cy, 3); + g.fillCircle(cx + half / 2, cy, 3); + } + + showBoneyardClickable(on) { + if (!this._boneyardZone) return; + this._boneyardZoneActive = on; + if (on) { + this._boneyardZone.setInteractive({ useHandCursor: true }); + for (const c of this._boneyardTiles) { + if (c.visible) { + this.tweens.add({ targets: c, scale: 1.1, duration: 320, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); + } + } + } else { + this._boneyardZone.disableInteractive(); + for (const c of this._boneyardTiles) { + this.tweens.killTweensOf(c); + c.setScale(1); + } + } + } + + onBoneyardClick() { + if (!this._boneyardZoneActive) return; + this.showBoneyardClickable(false); + const hand = this.gs.players[0].hand; + const destX = CX + (hand.length * HAND_PITCH) / 2; + this.animateDrawFromPile(Math.min(destX, GAME_WIDTH - 150), HAND_Y).then(() => { + this.gs = drawTile(this.gs); + this.humanDrew = true; + this.promptHuman(); + }); + } + + animateDrawFromPile(toX, toY) { + this.flashDrawToast(); playSound(this, SFX.CARD_DEAL); - this.boneText.setScale(1.25); - this.tweens.add({ targets: this.boneText, scale: 1, duration: 300 }); - await this.delay(180); + return new Promise((resolve) => { + const g = this.add.graphics().setDepth(DEPTH.tileFx + 2); + this.paintFaceDownDomino(g, 0, 0, BONEYARD_HALF); + g.setPosition(BONEYARD_CX, BONEYARD_CY); + this.tweens.add({ + targets: g, + x: toX, + y: toY, + duration: 420, + ease: 'Cubic.easeOut', + onComplete: () => { g.destroy(); resolve(); }, + }); + }); + } + + flashDrawToast() { + this.tweens.killTweensOf(this._drawToast); + this._drawToast.setAlpha(1).setY(BONEYARD_CY - 100); + this.tweens.add({ + targets: this._drawToast, + alpha: 0, + y: BONEYARD_CY - 140, + duration: 900, + ease: 'Cubic.easeOut', + }); } showToast(msg) {