Files
OpenFrontIO/src/client/graphics/layers/RailroadLayer.ts
T
2025-09-26 10:53:57 -07:00

206 lines
5.8 KiB
TypeScript

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<TileRef, RailRef>();
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();
}
}