Color railroad segments based on cost and add changes required for that

Implement fare-only updates for railroads and enhance client-side coloring

- Introduce fare tracking in RailroadLayer and update rendering logic to handle fare-only updates.
- Modify FxLayer to skip effects for fare-only updates.
- Add fare calculation and update logic in Railroad and TrainExecution classes.
- Update RailTile interface to include optional fare property for client-side use.
- Ensure significant fare changes trigger updates to clients for visual feedback.
This commit is contained in:
scamiv
2025-11-21 18:29:03 +01:00
parent 5d87c2189c
commit 160c8b2e94
6 changed files with 104 additions and 2 deletions
+4
View File
@@ -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
+44 -1
View File
@@ -27,6 +27,9 @@ export class RailroadLayer implements Layer {
private existingRailroads = new Map<TileRef, RailRef>();
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);
}
+6 -1
View File
@@ -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 {
+4
View File
@@ -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() {
+10
View File
@@ -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 {
+36
View File
@@ -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(