feat: add railway tracks and score badges to Mexican Train game

- Implement `paintRails()` to render decorative railroad ties and rails along player tracks
- Add `updateScoreBadges()` to display animated score circles for each player with color-coded borders based on score thresholds
- Fix double tile highlighting logic to correctly identify and style open doubles
- Update tile rendering to respect double tile orientation (vertical vs horizontal)
- Adjust track background colors for better visual contrast
This commit is contained in:
Brian Fertig 2026-05-21 20:38:55 -06:00
parent 7602da4cbe
commit ed7da9fcf0
1 changed files with 79 additions and 5 deletions

View File

@ -99,6 +99,7 @@ export default class MexicanTrainGame extends Phaser.Scene {
this.trackGfx = this.add.graphics().setDepth(DEPTH.tile); this.trackGfx = this.add.graphics().setDepth(DEPTH.tile);
this.bandGfx = this.add.graphics().setDepth(DEPTH.band); this.bandGfx = this.add.graphics().setDepth(DEPTH.band);
this.railGfx = this.add.graphics().setDepth(DEPTH.spine);
this.spineGfx = this.add.graphics().setDepth(DEPTH.spine); this.spineGfx = this.add.graphics().setDepth(DEPTH.spine);
this.hubGfx = this.add.graphics().setDepth(DEPTH.hub); this.hubGfx = this.add.graphics().setDepth(DEPTH.hub);
this.markerGfx = this.add.graphics().setDepth(DEPTH.tileFx); this.markerGfx = this.add.graphics().setDepth(DEPTH.tileFx);
@ -202,6 +203,7 @@ export default class MexicanTrainGame extends Phaser.Scene {
// ─── Render ─────────────────────────────────────────────────────────── // ─── Render ───────────────────────────────────────────────────────────
refresh() { refresh() {
this.paintBands(); this.paintBands();
this.paintRails();
this.paintSpineAndHub(); this.paintSpineAndHub();
this.paintTracks(); this.paintTracks();
this.paintMarkers(); this.paintMarkers();
@ -214,6 +216,7 @@ export default class MexicanTrainGame extends Phaser.Scene {
`Round ${this.gs.round + 1} • Engine ${hub}-${hub} • First to ${this.gs.target} ends it — lowest score wins`, `Round ${this.gs.round + 1} • Engine ${hub}-${hub} • First to ${this.gs.target} ends it — lowest score wins`,
); );
this.updateBoneyard(); this.updateBoneyard();
this.updateScoreBadges();
} }
paintBands() { paintBands() {
@ -224,11 +227,33 @@ export default class MexicanTrainGame extends Phaser.Scene {
for (let i = 0; i < rows; i++) { for (let i = 0; i < rows; i++) {
const y = this.rowY(i); const y = this.rowY(i);
const active = i < this.gs.players.length && i === this.gs.current; const active = i < this.gs.players.length && i === this.gs.current;
g.fillStyle(active ? COLORS.accent : COLORS.panel, active ? 0.16 : 0.5); g.fillStyle(0x0c1a10, active ? 0.55 : 0.92);
g.fillRoundedRect(TRACKS_LEFT - 8, y - gap / 2 + 4, TRACKS_RIGHT - TRACKS_LEFT + 16, gap - 8, 10); g.fillRoundedRect(TRACKS_LEFT - 8, y - gap / 2 + 4, TRACKS_RIGHT - TRACKS_LEFT + 16, gap - 8, 10);
} }
} }
paintRails() {
const g = this.railGfx;
g.clear();
const rows = this.gs.players.length + 1;
const railOff = 11; // px from lane center to each rail
const tieHalf = 15; // tie extends 4px beyond each rail
const tieStep = 22; // horizontal spacing between ties
for (let i = 0; i < rows; i++) {
const y = this.rowY(i);
// Ties (wood) — drawn first so rails appear on top
g.lineStyle(4, 0x4a3018, 0.55);
for (let x = TRACKS_LEFT; x <= TRACKS_RIGHT; x += tieStep) {
g.lineBetween(x, y - tieHalf, x, y + tieHalf);
}
// Rails (metal)
g.lineStyle(2.5, 0x908060, 0.7);
g.lineBetween(TRACKS_LEFT, y - railOff, TRACKS_RIGHT, y - railOff);
g.lineBetween(TRACKS_LEFT, y + railOff, TRACKS_RIGHT, y + railOff);
}
}
paintSpineAndHub() { paintSpineAndHub() {
const g = this.spineGfx; const g = this.spineGfx;
g.clear(); g.clear();
@ -271,9 +296,10 @@ export default class MexicanTrainGame extends Phaser.Scene {
} }
for (let t = start; t < tiles.length; t++) { for (let t = start; t < tiles.length; t++) {
const tile = tiles[t]; const tile = tiles[t];
const isDouble = tile.left === tile.right;
const isOpenDouble = this.gs.openDouble?.train === key && t === tiles.length - 1; const isOpenDouble = this.gs.openDouble?.train === key && t === tiles.length - 1;
const border = isOpenDouble ? COLORS.danger : (tile.left === tile.right ? COLORS.gold : COLORS.accent); const border = isOpenDouble ? COLORS.danger : (isDouble ? COLORS.gold : COLORS.accent);
this.paintDomino(g, drawX, y, TRACK_HALF, tile.left, tile.right, false, border); this.paintDomino(g, drawX, y, TRACK_HALF, tile.left, tile.right, isDouble, border);
drawX += TRACK_PITCH; drawX += TRACK_PITCH;
} }
} }
@ -377,6 +403,49 @@ export default class MexicanTrainGame extends Phaser.Scene {
this.updateHandDots(); this.updateHandDots();
} }
updateScoreBadges() {
const scores = this.gs.players.map(p => p.score);
if (scores.every(s => s === 0)) {
this._scoreBadgeCtrs?.forEach(c => c.destroy());
this._scoreBadgeCtrs = [];
return;
}
const changed = !this._lastBadgeScores || scores.some((s, i) => s !== this._lastBadgeScores[i]);
if (!changed) return;
this._lastBadgeScores = [...scores];
this._scoreBadgeCtrs?.forEach(c => c.destroy());
this._scoreBadgeCtrs = [];
for (let i = 0; i < this.gs.players.length; i++) {
const score = scores[i];
const x = PORTRAIT_X;
const y = this.rowY(i) - PORTRAIT_R - 20;
const borderColor = score > 75 ? COLORS.danger
: score > 50 ? 0xe07030
: score > 25 ? COLORS.accent
: COLORS.gold;
const c = this.add.container(x, y).setDepth(DEPTH.portrait + 2).setScale(0.1);
const g = this.add.graphics();
g.fillStyle(0x1e1a30, 1);
g.fillCircle(0, 0, 22);
g.lineStyle(2.5, borderColor, 1);
g.strokeCircle(0, 0, 22);
g.lineStyle(1.5, 0xffffff, 0.25);
g.strokeCircle(0, 0, 17);
const fontSize = score >= 100 ? '11px' : '14px';
const t = this.add.text(0, 0, String(score), {
fontFamily: '"Julius Sans One"', fontSize, color: COLORS.textHex, fontStyle: 'bold',
}).setOrigin(0.5);
c.add([g, t]);
this._scoreBadgeCtrs.push(c);
this.tweens.add({ targets: c, scale: 1, duration: 260, ease: 'Back.Out' });
}
}
updateHandDots() { updateHandDots() {
if (!this._handDotGfx) return; if (!this._handDotGfx) return;
const n = this.gs.players.length; const n = this.gs.players.length;
@ -613,9 +682,13 @@ export default class MexicanTrainGame extends Phaser.Scene {
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH); const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH);
const shown = Math.min(tiles.length, maxTiles); const shown = Math.min(tiles.length, maxTiles);
const x = TRACKS_LEFT + TRACK_HALF + (shown - 1) * TRACK_PITCH; const x = TRACKS_LEFT + TRACK_HALF + (shown - 1) * TRACK_PITCH;
const lastTile = tiles[tiles.length - 1];
const isLastDouble = lastTile && lastTile.left === lastTile.right;
const fw = isLastDouble ? TRACK_HALF : TRACK_TILE_W;
const fh = isLastDouble ? TRACK_TILE_W : TRACK_HALF;
const fx = this.add.graphics().setDepth(DEPTH.tileFx); const fx = this.add.graphics().setDepth(DEPTH.tileFx);
fx.lineStyle(4, COLORS.gold, 1); fx.lineStyle(4, COLORS.gold, 1);
fx.strokeRoundedRect(x - TRACK_TILE_W / 2 - 3, y - TRACK_HALF / 2 - 3, TRACK_TILE_W + 6, TRACK_HALF + 6, 6); fx.strokeRoundedRect(x - fw / 2 - 3, y - fh / 2 - 3, fw + 6, fh + 6, 6);
this.tweens.add({ targets: fx, alpha: { from: 1, to: 0 }, duration: 500, onComplete: () => fx.destroy() }); this.tweens.add({ targets: fx, alpha: { from: 1, to: 0 }, duration: 500, onComplete: () => fx.destroy() });
} }
@ -630,8 +703,9 @@ export default class MexicanTrainGame extends Phaser.Scene {
} }
animateTilePlay(fromX, fromY, toX, toY, tileA, tileB, onComplete) { animateTilePlay(fromX, fromY, toX, toY, tileA, tileB, onComplete) {
const isDouble = tileA === tileB;
const g = this.add.graphics().setDepth(DEPTH.tileFx + 2); const g = this.add.graphics().setDepth(DEPTH.tileFx + 2);
this.paintDomino(g, 0, 0, TRACK_HALF, tileA, tileB, false, COLORS.accent); this.paintDomino(g, 0, 0, TRACK_HALF, tileA, tileB, isDouble, COLORS.accent);
g.setPosition(fromX, fromY); g.setPosition(fromX, fromY);
this.tweens.add({ this.tweens.add({
targets: g, targets: g,