diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index b33fd100e..f9b07cc04 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -230,6 +230,10 @@ export class FxLayer implements Layer { } onRailroadEvent(railroad: RailroadUpdate) { + // Skip FX for fare-only color updates + if (railroad.isFareUpdate) { + return; + } const railTiles = railroad.railTiles; for (const rail of railTiles) { // No need for pseudorandom, this is fx diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index 3a863a699..13b4b1e4e 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -27,6 +27,9 @@ export class RailroadLayer implements Layer { private existingRailroads = new Map(); private nextRailIndexToCheck = 0; private railTileList: TileRef[] = []; + // Track global fare range for coloring + private minFare: number = Infinity; + private maxFare: number = 0; constructor( private game: GameView, @@ -116,6 +119,24 @@ export class RailroadLayer implements Layer { } private handleRailroadRendering(railUpdate: RailroadUpdate) { + // Fare-only updates: recolor existing segments without touching counts + if (railUpdate.isActive && railUpdate.isFareUpdate) { + for (const railRoad of railUpdate.railTiles) { + const ref = this.existingRailroads.get(railRoad.tile); + if (!ref) continue; + + if (railRoad.fare !== undefined) { + this.minFare = Math.min(this.minFare, railRoad.fare); + this.maxFare = Math.max(this.maxFare, railRoad.fare); + } + // Update the stored tile's fare and repaint + ref.tile.fare = railRoad.fare; + this.paintRail(ref.tile); + } + return; + } + + // Construction / deletion events for (const railRoad of railUpdate.railTiles) { if (railUpdate.isActive) { this.paintRailroad(railRoad); @@ -126,6 +147,11 @@ export class RailroadLayer implements Layer { } private paintRailroad(railRoad: RailTile) { + if (railRoad.fare !== undefined) { + this.minFare = Math.min(this.minFare, railRoad.fare); + this.maxFare = Math.max(this.maxFare, railRoad.fare); + } + const currentOwner = this.game.owner(railRoad.tile)?.id() ?? null; const railTile = this.existingRailroads.get(railRoad.tile); @@ -182,9 +208,26 @@ export class RailroadLayer implements Layer { } const owner = this.game.owner(tile); const recipient = owner.isPlayer() ? owner : null; - const color = recipient + let color = recipient ? recipient.borderColor() : colord("rgba(255,255,255,1)"); + + const fare = railRoad.fare; + if ( + fare !== undefined && + Number.isFinite(this.minFare) && + this.maxFare > this.minFare + ) { + const t = (fare - this.minFare) / (this.maxFare - this.minFare); + const clampedT = Math.max(0, Math.min(1, t)); + const baseRgb = color.toRgb(); + const redTarget = 255; + const r = Math.round(baseRgb.r + (redTarget - baseRgb.r) * clampedT); + const g = Math.round(baseRgb.g * (1 - clampedT)); + const b = Math.round(baseRgb.b * (1 - clampedT)); + color = colord({ r, g, b }); + } + this.context.fillStyle = color.toRgbString(); this.paintRailRects(this.context, x, y, railType); } diff --git a/src/core/execution/RailroadExecution.ts b/src/core/execution/RailroadExecution.ts index 97bc8d1d9..28b286b9d 100644 --- a/src/core/execution/RailroadExecution.ts +++ b/src/core/execution/RailroadExecution.ts @@ -21,6 +21,7 @@ export class RailroadExecution implements Execution { init(mg: Game, ticks: number): void { this.mg = mg; const tiles = this.railRoad.tiles; + const fare = Number(this.railRoad.getFare()); // Inverse direction computation for the first tile this.railTiles.push({ tile: tiles[0], @@ -28,6 +29,7 @@ export class RailroadExecution implements Execution { tiles.length > 0 ? this.computeExtremityDirection(tiles[0], tiles[1]) : RailType.VERTICAL, + fare, }); for (let i = 1; i < tiles.length - 1; i++) { const direction = this.computeDirection( @@ -35,7 +37,7 @@ export class RailroadExecution implements Execution { tiles[i], tiles[i + 1], ); - this.railTiles.push({ tile: tiles[i], railType: direction }); + this.railTiles.push({ tile: tiles[i], railType: direction, fare }); } this.railTiles.push({ tile: tiles[tiles.length - 1], @@ -46,7 +48,10 @@ export class RailroadExecution implements Execution { tiles[tiles.length - 2], ) : RailType.VERTICAL, + fare, }); + // Cache full geometry on the railroad so it can later emit fare-only updates + this.railRoad.setRailTiles(this.railTiles); } private computeExtremityDirection(tile: TileRef, next: TileRef): RailType { diff --git a/src/core/execution/TrainExecution.ts b/src/core/execution/TrainExecution.ts index 61437743a..44efb7b73 100644 --- a/src/core/execution/TrainExecution.ts +++ b/src/core/execution/TrainExecution.ts @@ -118,6 +118,10 @@ export class TrainExecution implements Execution { ? tiles[Math.floor(tiles.length / 2)] : railroad.getStart().tile(); this.player.addGold(-fare, midTile); + // Update client-side coloring when fare changes significantly + if (this.mg !== null) { + rail.updateFare(this.mg); + } } private leaveRailroad() { diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 455ef1ac1..7208e3c93 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -90,12 +90,22 @@ export enum RailType { export interface RailTile { tile: TileRef; railType: RailType; + /** + * Optional current fare for this railroad segment, used for client-side coloring. + * Represented as a number for convenience (server BigInt is converted). + */ + fare?: number; } export interface RailroadUpdate { type: GameUpdateType.RailroadEvent; isActive: boolean; railTiles: RailTile[]; + /** + * When true, this update only signals a fare / color change for existing + * rail segments and should not be treated as a construction / deletion event. + */ + isFareUpdate?: boolean; } export interface ConquestUpdate { diff --git a/src/core/game/Railroad.ts b/src/core/game/Railroad.ts index 2851f56eb..c8f41a648 100644 --- a/src/core/game/Railroad.ts +++ b/src/core/game/Railroad.ts @@ -83,6 +83,42 @@ export class Railroad { const congestionFare = baseCongestionFare * congestionFactor; return lengthFare + congestionFare; } + + setRailTiles(tiles: RailTile[]) { + this.railTiles = tiles; + } + + /** + * Emit a fare update to clients if the fare has changed significantly. + * Currently uses a 10% relative-change threshold. + */ + updateFare(game: Game) { + if (!this.railTiles || this.railTiles.length === 0) return; + const newFare = this.getFare(); + if (this.lastFare !== null) { + const prev = this.lastFare; + const diff = newFare > prev ? newFare - prev : prev - newFare; + const threshold = prev / 10n; // 10% + if (threshold > 0n && diff < threshold) { + this.lastFare = newFare; + return; + } + } + this.lastFare = newFare; + + const numericFare = Number(newFare); + const railTilesWithFare: RailTile[] = this.railTiles.map((t) => ({ + ...t, + fare: numericFare, + })); + + game.addUpdate({ + type: GameUpdateType.RailroadEvent, + isActive: true, + isFareUpdate: true, + railTiles: railTilesWithFare, + }); + } } export function getOrientedRailroad(