import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; import { PlayerID } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameUpdateType, RailroadUpdate, RailTile, RailType, } from "../../../core/game/GameUpdates"; import { GameView } from "../../../core/game/GameView"; import { Layer } from "./Layer"; import { getBridgeRects, getRailroadRects } from "./RailroadSprites"; type RailRef = { tile: RailTile; numOccurence: number; lastOwnerId: PlayerID | null; }; export class RailroadLayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private theme: Theme; // Save the number of railroads per tiles. Delete when it reaches 0 private existingRailroads = new Map(); private nextRailIndexToCheck = 0; private railTileList: TileRef[] = []; constructor(private game: GameView) { this.theme = game.config().theme(); } shouldTransform(): boolean { return true; } tick() { const updates = this.game.updatesSinceLastTick(); const railUpdates = updates !== null ? updates[GameUpdateType.RailroadEvent] : []; for (const rail of railUpdates) { this.handleRailroadRendering(rail); } } updateRailColors() { const maxTilesPerFrame = this.railTileList.length / 60; let checked = 0; while (checked < maxTilesPerFrame && this.railTileList.length > 0) { const tile = this.railTileList[this.nextRailIndexToCheck]; const railRef = this.existingRailroads.get(tile); if (railRef) { const currentOwner = this.game.owner(tile)?.id() ?? null; if (railRef.lastOwnerId !== currentOwner) { railRef.lastOwnerId = currentOwner; this.paintRail(railRef.tile); } } this.nextRailIndexToCheck++; if (this.nextRailIndexToCheck >= this.railTileList.length) { this.nextRailIndexToCheck = 0; } checked++; } } init() { this.redraw(); } redraw() { this.canvas = document.createElement("canvas"); const context = this.canvas.getContext("2d", { alpha: true }); if (context === null) throw new Error("2d context not supported"); this.context = context; // Enable smooth scaling this.context.imageSmoothingEnabled = true; this.context.imageSmoothingQuality = "high"; this.canvas.width = this.game.width() * 2; this.canvas.height = this.game.height() * 2; for (const [_, rail] of this.existingRailroads) { this.paintRail(rail.tile); } } renderLayer(context: CanvasRenderingContext2D) { this.updateRailColors(); context.drawImage( this.canvas, -this.game.width() / 2, -this.game.height() / 2, this.game.width(), this.game.height(), ); } private handleRailroadRendering(railUpdate: RailroadUpdate) { for (const railRoad of railUpdate.railTiles) { const x = this.game.x(railRoad.tile); const y = this.game.y(railRoad.tile); if (railUpdate.isActive) { this.paintRailroad(railRoad); } else { this.clearRailroad(railRoad); } } } private paintRailroad(railRoad: RailTile) { const currentOwner = this.game.owner(railRoad.tile)?.id() ?? null; const railTile = this.existingRailroads.get(railRoad.tile); if (railTile) { railTile.numOccurence++; railTile.tile = railRoad; railTile.lastOwnerId = currentOwner; } else { this.existingRailroads.set(railRoad.tile, { tile: railRoad, numOccurence: 1, lastOwnerId: currentOwner, }); this.railTileList.push(railRoad.tile); this.paintRail(railRoad); } } private clearRailroad(railRoad: RailTile) { const ref = this.existingRailroads.get(railRoad.tile); if (ref) ref.numOccurence--; if (!ref || ref.numOccurence <= 0) { this.existingRailroads.delete(railRoad.tile); this.railTileList = this.railTileList.filter((t) => t !== railRoad.tile); if (this.context === undefined) throw new Error("Not initialized"); if (this.game.isWater(railRoad.tile)) { this.context.clearRect( this.game.x(railRoad.tile) * 2 - 2, this.game.y(railRoad.tile) * 2 - 2, 5, 6, ); } else { this.context.clearRect( this.game.x(railRoad.tile) * 2 - 1, this.game.y(railRoad.tile) * 2 - 1, 3, 3, ); } } } paintRail(railRoad: RailTile) { if (this.context === undefined) throw new Error("Not initialized"); const { tile } = railRoad; const { railType } = railRoad; const x = this.game.x(tile); const y = this.game.y(tile); // If rail tile is over water, paint a bridge underlay first if (this.game.isWater(tile)) { this.paintBridge(this.context, x, y, railType); } const owner = this.game.owner(tile); const recipient = owner.isPlayer() ? owner : null; const color = recipient ? recipient.borderColor() : new Colord({ r: 255, g: 255, b: 255, a: 1 }); this.context.fillStyle = color.toRgbString(); this.paintRailRects(this.context, x, y, railType); } private paintRailRects( context: CanvasRenderingContext2D, x: number, y: number, direction: RailType, ) { const railRects = getRailroadRects(direction); for (const [dx, dy, w, h] of railRects) { context.fillRect(x * 2 + dx, y * 2 + dy, w, h); } } private paintBridge( context: CanvasRenderingContext2D, x: number, y: number, direction: RailType, ) { context.save(); context.fillStyle = "rgb(197,69,72)"; const bridgeRects = getBridgeRects(direction); for (const [dx, dy, w, h] of bridgeRects) { context.fillRect(x * 2 + dx, y * 2 + dy, w, h); } context.restore(); } }