diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index d69494a36..46341a7df 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -4,13 +4,13 @@ import { RailType, RailroadUpdate, } from "../../../core/game/GameUpdates"; +import { getBridgeRects, getRailroadRects } from "./RailroadSprites"; import { Colord } from "colord"; import { GameView } from "../../../core/game/GameView"; import { Layer } from "./Layer"; import { PlayerID } from "../../../core/game/Game"; import { Theme } from "../../../core/configuration/Config"; import { TileRef } from "../../../core/game/GameMap"; -import { getRailroadRects } from "./RailroadSprites"; type RailRef = { tile: RailTile; @@ -140,33 +140,57 @@ export class RailroadLayer implements Layer { this.existingRailroads.delete(railRoad.tile); this.railTileList = this.railTileList.filter((t) => t !== railRoad.tile); if (this.context === undefined) throw new Error("Not initialized"); - this.context.clearRect( - this.game.x(railRoad.tile) * 2 - 1, - this.game.y(railRoad.tile) * 2 - 1, - 3, - 3, - ); + 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) { - const x = this.game.x(railRoad.tile); - const y = this.game.y(railRoad.tile); - const owner = this.game.owner(railRoad.tile); + 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 ? this.theme.railroadColor(recipient) : new Colord({ r: 255, g: 255, b: 255, a: 1 }); - if (this.context === undefined) throw new Error("Not initialized"); this.context.fillStyle = color.toRgbString(); - this.paintRailRects(x, y, railRoad.railType); + this.paintRailRects(this.context, x, y, railType); } - private paintRailRects(x: number, y: number, direction: RailType) { - if (this.context === undefined) throw new Error("Not initialized"); + private paintRailRects(context: CanvasRenderingContext2D, x: number, y: number, direction: RailType) { const railRects = getRailroadRects(direction); for (const [dx, dy, w, h] of railRects) { - this.context.fillRect(x * 2 + dx, y * 2 + dy, w, h); + 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(); + } } diff --git a/src/client/graphics/layers/RailroadSprites.ts b/src/client/graphics/layers/RailroadSprites.ts index d76a41804..d0734acad 100644 --- a/src/client/graphics/layers/RailroadSprites.ts +++ b/src/client/graphics/layers/RailroadSprites.ts @@ -9,6 +9,15 @@ const railTypeToFunctionMap: Record number[][]> = { [RailType.VERTICAL]: verticalRailroadRects, }; +const railTypeToBridgeFunctionMap: Record number[][]> = { + [RailType.TOP_RIGHT]: topRightBridgeCornerRects, + [RailType.BOTTOM_LEFT]: bottomLeftBridgeCornerRects, + [RailType.TOP_LEFT]: topLeftBridgeCornerRects, + [RailType.BOTTOM_RIGHT]: bottomRightBridgeCornerRects, + [RailType.HORIZONTAL]: horizontalBridge, + [RailType.VERTICAL]: verticalBridge, +}; + export function getRailroadRects(type: RailType): number[][] { const railRects = railTypeToFunctionMap[type]; if (!railRects) { @@ -77,3 +86,76 @@ function bottomLeftRailroadCornerRects(): number[][] { ]; return rects; } + +export function getBridgeRects(type: RailType): number[][] { + const bridgeRects = railTypeToBridgeFunctionMap[type]; + if (!bridgeRects) { + // Should never happen + throw new Error(`Unsupported RailType: ${type}`); + } + return bridgeRects(); +} + +function horizontalBridge(): number[][] { + // x/y/w/h + return [ + [-1, -2, 3, 1], + [-1, 2, 3, 1], + [-1, 3, 1, 1], + [1, 3, 1, 1], + ]; +} + +function verticalBridge(): number[][] { + // x/y/w/h + return [ + [-2, -2, 1, 3], + [2, -2, 1, 3], + ]; +} +// ⌞ +function topRightBridgeCornerRects(): number[][] { + return [ + [-2, -2, 1, 2], + [-1, 0, 1, 1], + [0, 1, 1, 1], + [1, 2, 2, 1], + [2, -2, 1, 1], + ]; +} +// ⌝ +function bottomLeftBridgeCornerRects(): number[][] { + // x/y/w/h + const rects = [ + [-2, -2, 2, 1], + [0, -1, 1, 1], + [1, 0, 1, 1], + [2, 1, 1, 2], + [-2, 2, 1, 1], + ]; + return rects; +} +// ⌟ +function topLeftBridgeCornerRects(): number[][] { + // x/y/w/h + const rects = [ + [-2, -2, 1, 1], + [-2, 2, 2, 1], + [0, 1, 1, 1], + [1, 0, 1, 1], + [2, -2, 1, 2], + ]; + return rects; +} +// ⌜ +function bottomRightBridgeCornerRects(): number[][] { + // x/y/w/h + const rects = [ + [-2, 1, 1, 2], + [-1, 0, 1, 1], + [0, -1, 1, 1], + [1, -2, 2, 1], + [2, 2, 1, 1], + ]; + return rects; +} diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts index c4c227b04..0b484161f 100644 --- a/src/core/pathfinding/MiniAStar.ts +++ b/src/core/pathfinding/MiniAStar.ts @@ -4,6 +4,7 @@ import { GraphAdapter, SerialAStar } from "./SerialAStar"; import { Cell } from "../game/Game"; export class GameMapAdapter implements GraphAdapter { + private readonly waterPenalty = 3; constructor( private readonly gameMap: GameMap, private readonly waterPath: boolean, @@ -14,7 +15,12 @@ export class GameMapAdapter implements GraphAdapter { } cost(node: TileRef): number { - return this.gameMap.cost(node); + let base = this.gameMap.cost(node); + // Avoid crossing water when possible + if (!this.waterPath && this.gameMap.isWater(node)) { + base += this.waterPenalty; + } + return base; } position(node: TileRef): { x: number; y: number } { @@ -22,8 +28,14 @@ export class GameMapAdapter implements GraphAdapter { } isTraversable(from: TileRef, to: TileRef): boolean { - const isWater = this.gameMap.isWater(to); - return this.waterPath ? isWater : !isWater; + const toWater = this.gameMap.isWater(to); + if (this.waterPath) { + return toWater; + } + // Allow water access from/to shore + const fromShore = this.gameMap.isShoreline(from); + const toShore = this.gameMap.isShoreline(to); + return !toWater || fromShore || toShore; } } export class MiniAStar implements AStar {