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() { paintTracks() {
const g = this.trackGfx; const g = this.trackGfx;
g.clear(); g.clear();
const rows = this.gs.players.length + 1; const rows = this.gs.players.length + 1;
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH);
for (let i = 0; i < rows; i++) { for (let i = 0; i < rows; i++) {
const key = this.rowKeyFor(i); const key = this.rowKeyFor(i);
const y = this.rowY(i); const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles; const tiles = this.gs.trains[key].tiles;
const start = Math.max(0, tiles.length - maxTiles); if (tiles.length === 0) { this.clearTruncMark(i); continue; }
let drawX = TRACKS_LEFT + TRACK_HALF;
if (start > 0) { const { start, positions } = this.computeTrackLayout(key);
this.maybeTruncMark(i, start); if (start > 0) this.maybeTruncMark(i, start); else this.clearTruncMark(i);
drawX += 8;
} else { for (let ti = 0; ti < positions.length; ti++) {
this.clearTruncMark(i); const t = start + ti;
}
for (let t = start; t < tiles.length; t++) {
const tile = tiles[t]; const tile = tiles[t];
const isDouble = tile.left === tile.right; 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 : (isDouble ? 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, isDouble, border); this.paintDomino(g, positions[ti], y, TRACK_HALF, tile.left, tile.right, isDouble, border);
drawX += TRACK_PITCH;
} }
} }
} }
@ -331,7 +363,10 @@ export default class MexicanTrainGame extends Phaser.Scene {
if (!this.gs.trains[key].marker) continue; if (!this.gs.trains[key].marker) continue;
const y = this.rowY(i); const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles; 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 // little open-train flag
g.fillStyle(COLORS.danger, 1); g.fillStyle(COLORS.danger, 1);
g.fillCircle(x, y, 9); 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++) { for (let i = 0; i < this.gs.players.length; i++) {
const score = scores[i]; const score = scores[i];
const x = PORTRAIT_X; const x = PORTRAIT_X + Math.round(PORTRAIT_R * 0.85);
const y = this.rowY(i) - PORTRAIT_R - 20; const y = this.rowY(i) - Math.round(PORTRAIT_R * 0.85);
const borderColor = score > 75 ? COLORS.danger const borderColor = score > 75 ? COLORS.danger
: score > 50 ? 0xe07030 : score > 50 ? 0xe07030
: score > 25 ? COLORS.accent : score > 25 ? COLORS.accent
@ -676,14 +711,14 @@ export default class MexicanTrainGame extends Phaser.Scene {
// ─── Effects ────────────────────────────────────────────────────────── // ─── Effects ──────────────────────────────────────────────────────────
flashTrainEnd(key) { flashTrainEnd(key) {
const rows = this.gs.players.length + 1; 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 y = this.rowY(i);
const tiles = this.gs.trains[key].tiles; const tiles = this.gs.trains[key].tiles;
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH); if (!tiles.length) return;
const shown = Math.min(tiles.length, maxTiles); const { positions } = this.computeTrackLayout(key);
const x = TRACKS_LEFT + TRACK_HALF + (shown - 1) * TRACK_PITCH; const x = positions[positions.length - 1];
const lastTile = tiles[tiles.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 fw = isLastDouble ? TRACK_HALF : TRACK_TILE_W;
const fh = isLastDouble ? TRACK_TILE_W : TRACK_HALF; const fh = isLastDouble ? TRACK_TILE_W : TRACK_HALF;
const fx = this.add.graphics().setDepth(DEPTH.tileFx); 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 i = key === MEXICAN ? rows - 1 : Number(key);
const y = this.rowY(i); const y = this.rowY(i);
const tiles = this.gs.trains[key].tiles; const tiles = this.gs.trains[key].tiles;
const maxTiles = Math.floor((TRACKS_RIGHT - TRACKS_LEFT) / TRACK_PITCH); if (tiles.length === 0) return { x: TRACKS_LEFT + TRACK_HALF, y };
const newIdx = Math.min(tiles.length, maxTiles - 1); const { positions } = this.computeTrackLayout(key);
return { x: TRACKS_LEFT + TRACK_HALF + newIdx * TRACK_PITCH, y }; 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) { animateTilePlay(fromX, fromY, toX, toY, tileA, tileB, onComplete) {