feat: improve Mexican Train UI with boneyard interaction and readability
- Add interactive boneyard pile with click-to-draw functionality and animations - Display opponent hand counts as mini-domino dots next to portraits - Apply background padding to all UI text elements for improved readability - Refactor draw animations to support both human and AI tile drawing from the boneyard
This commit is contained in:
parent
f1fc560cd1
commit
7602da4cbe
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue