refactor: implement dynamic track layout for overflowing trains

- Replace fixed `TRACK_PITCH` tiling with `computeTrackLayout()` to dynamically fit tiles within track bounds
- Add `tileHalfW()` helper to correctly calculate width for double vs. single tiles
- Add truncation marks ("+N«") when the tile count exceeds visible track space
- Update marker placement, flash effects, and next-tile positioning to use the new dynamic layout coordinates
- Adjust score badge positioning to align relative to portrait radius
This commit is contained in:
Brian Fertig 2026-05-21 20:52:00 -06:00
parent ed7da9fcf0
commit 4644b8e3af
1 changed files with 61 additions and 23 deletions

View File

@ -277,30 +277,62 @@ export default class MexicanTrainGame extends Phaser.Scene {
}
}
// Returns the horizontal half-width a tile occupies when painted.
tileHalfW(tile) {
return tile.left === tile.right ? TRACK_HALF / 2 : TRACK_HALF;
}
// Returns { start, positions[] } for the visible slice of a train.
// Positions are the x-centres of each visible tile (index start..end).
computeTrackLayout(key) {
const tiles = this.gs.trains[key].tiles;
if (tiles.length === 0) return { start: 0, positions: [] };
const GAP = 6;
const TRUNC = 16; // px reserved for the "+N«" truncation mark
const avail = TRACKS_RIGHT - TRACKS_LEFT;
// Walk backwards to find the first tile that still fits.
let accumulated = 0;
let start = tiles.length;
for (let t = tiles.length - 1; t >= 0; t--) {
const addW = (start < tiles.length ? GAP : 0) + this.tileHalfW(tiles[t]) * 2;
if (accumulated + addW > avail - (t > 0 ? TRUNC : 0)) break;
accumulated += addW;
start = t;
}
// Build centre-x positions for each visible tile.
const positions = [];
let x = TRACKS_LEFT + (start > 0 ? TRUNC : 0) + this.tileHalfW(tiles[start]);
for (let t = start; t < tiles.length; t++) {
positions.push(x);
if (t < tiles.length - 1) {
x += this.tileHalfW(tiles[t]) + GAP + this.tileHalfW(tiles[t + 1]);
}
}
return { start, positions };
}
paintTracks() {
const g = this.trackGfx;
g.clear();
const rows = this.gs.players.length + 1;
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH);
for (let i = 0; i < rows; i++) {
const key = this.rowKeyFor(i);
const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles;
const start = Math.max(0, tiles.length - maxTiles);
let drawX = TRACKS_LEFT + TRACK_HALF;
if (start > 0) {
this.maybeTruncMark(i, start);
drawX += 8;
} else {
this.clearTruncMark(i);
}
for (let t = start; t < tiles.length; t++) {
if (tiles.length === 0) { this.clearTruncMark(i); continue; }
const { start, positions } = this.computeTrackLayout(key);
if (start > 0) this.maybeTruncMark(i, start); else this.clearTruncMark(i);
for (let ti = 0; ti < positions.length; ti++) {
const t = start + ti;
const tile = tiles[t];
const isDouble = tile.left === tile.right;
const isOpenDouble = this.gs.openDouble?.train === key && t === tiles.length - 1;
const border = isOpenDouble ? COLORS.danger : (isDouble ? COLORS.gold : COLORS.accent);
this.paintDomino(g, drawX, y, TRACK_HALF, tile.left, tile.right, isDouble, border);
drawX += TRACK_PITCH;
this.paintDomino(g, positions[ti], y, TRACK_HALF, tile.left, tile.right, isDouble, border);
}
}
}
@ -331,7 +363,10 @@ export default class MexicanTrainGame extends Phaser.Scene {
if (!this.gs.trains[key].marker) continue;
const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles;
const x = TRACKS_LEFT + TRACK_HALF + Math.min(tiles.length, Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH)) * TRACK_PITCH + 6;
const { positions } = this.computeTrackLayout(key);
const lastPos = positions.length ? positions[positions.length - 1] : TRACKS_LEFT;
const lastHW = tiles.length ? this.tileHalfW(tiles[tiles.length - 1]) : TRACK_HALF;
const x = lastPos + lastHW + 6;
// little open-train flag
g.fillStyle(COLORS.danger, 1);
g.fillCircle(x, y, 9);
@ -419,8 +454,8 @@ export default class MexicanTrainGame extends Phaser.Scene {
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 x = PORTRAIT_X + Math.round(PORTRAIT_R * 0.85);
const y = this.rowY(i) - Math.round(PORTRAIT_R * 0.85);
const borderColor = score > 75 ? COLORS.danger
: score > 50 ? 0xe07030
: score > 25 ? COLORS.accent
@ -676,14 +711,14 @@ export default class MexicanTrainGame extends Phaser.Scene {
// ─── Effects ──────────────────────────────────────────────────────────
flashTrainEnd(key) {
const rows = this.gs.players.length + 1;
let i = key === MEXICAN ? rows - 1 : Number(key);
const i = key === MEXICAN ? rows - 1 : Number(key);
const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles;
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH);
const shown = Math.min(tiles.length, maxTiles);
const x = TRACKS_LEFT + TRACK_HALF + (shown - 1) * TRACK_PITCH;
if (!tiles.length) return;
const { positions } = this.computeTrackLayout(key);
const x = positions[positions.length - 1];
const lastTile = tiles[tiles.length - 1];
const isLastDouble = lastTile && lastTile.left === lastTile.right;
const isLastDouble = 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);
@ -697,9 +732,12 @@ export default class MexicanTrainGame extends Phaser.Scene {
const i = key === MEXICAN ? rows - 1 : Number(key);
const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles;
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH);
const newIdx = Math.min(tiles.length, maxTiles - 1);
return { x: TRACKS_LEFT + TRACK_HALF + newIdx * TRACK_PITCH, y };
if (tiles.length === 0) return { x: TRACKS_LEFT + TRACK_HALF, y };
const { positions } = this.computeTrackLayout(key);
const lastX = positions[positions.length - 1];
const lastHW = this.tileHalfW(tiles[tiles.length - 1]);
const nextX = Math.min(lastX + lastHW + 6 + TRACK_HALF, TRACKS_RIGHT - TRACK_HALF);
return { x: nextX, y };
}
animateTilePlay(fromX, fromY, toX, toY, tileA, tileB, onComplete) {