mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-14 14:06:55 +00:00
## Description:  The `RailroadLayer` simply displays tiles as instructed by the core worker. While it's practical for the layer to only care about the tiles, it also means it has no understanding of railroads as entities (their paths, connections, or identities). It also means that the core worker is responsible for rendering tasks such as tile orientation and construction animation, which is not expected. To support ID-based events and better separation of concerns, the rendering layer needs to be aware of complete railroads. With this change, the core worker can send the tiles once and subsequently reference railroads only by ID for all other events. #### Changes: - `RailroadLayer` now stores full railroad data instead of only individual tiles - `RailroadLayer` is responsible for animating newly built railroads - Add a new `RailroadSnapUpdate` sent when a new structure is built over an existing railroad. This event is used by `RailroadLayer` to keep railroad ID in sync. - When hovering over a railroad, the render worker is querying the core worker about overlapping railroads. Alternatively, RailroadLayer could compute overlaps itself now that it has full railroad knowledge, but this logic would need to be duplicated and kept in sync across workers. Keeping a single source of truth in the core worker is preferred. #### Edgecases: - When a structure snaps over a railroad, the original railroad is split into two new railroads. If the construction animation is still in progress, instead of resuming the animation at the correct point on the new railroads, all remaining tiles are rendered immediately - Previously, `RailroadUpdate` handled both construction and destruction. This no longer works with `RailroadSnapUpdate`, as event ordering is now pretty important and IDs may be lost before they are consumed. To address this, RailroadUpdate is split in two: `RailroadConstructionUpdate` and `RailroadDestructionUpdate`. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --------- Co-authored-by: jrouillard <jon@rouillard.org>
178 lines
4.7 KiB
TypeScript
178 lines
4.7 KiB
TypeScript
import { TileRef } from "../../../core/game/GameMap";
|
|
import { GameView } from "../../../core/game/GameView";
|
|
|
|
export enum RailType {
|
|
VERTICAL,
|
|
HORIZONTAL,
|
|
TOP_LEFT,
|
|
TOP_RIGHT,
|
|
BOTTOM_LEFT,
|
|
BOTTOM_RIGHT,
|
|
}
|
|
|
|
export type RailTile = {
|
|
tile: TileRef;
|
|
type: RailType;
|
|
};
|
|
|
|
export function computeRailTiles(game: GameView, tiles: TileRef[]): RailTile[] {
|
|
if (tiles.length === 0) return [];
|
|
if (tiles.length === 1) {
|
|
return [{ tile: tiles[0], type: RailType.VERTICAL }];
|
|
}
|
|
const railTypes: RailTile[] = [];
|
|
// Inverse direction computation for the first tile
|
|
railTypes.push({
|
|
tile: tiles[0],
|
|
type: computeExtremityDirection(game, tiles[0], tiles[1]),
|
|
});
|
|
for (let i = 1; i < tiles.length - 1; i++) {
|
|
const direction = computeDirection(
|
|
game,
|
|
tiles[i - 1],
|
|
tiles[i],
|
|
tiles[i + 1],
|
|
);
|
|
railTypes.push({ tile: tiles[i], type: direction });
|
|
}
|
|
railTypes.push({
|
|
tile: tiles[tiles.length - 1],
|
|
type: computeExtremityDirection(
|
|
game,
|
|
tiles[tiles.length - 1],
|
|
tiles[tiles.length - 2],
|
|
),
|
|
});
|
|
return railTypes;
|
|
}
|
|
|
|
function computeExtremityDirection(
|
|
game: GameView,
|
|
tile: TileRef,
|
|
next: TileRef,
|
|
): RailType {
|
|
const x = game.x(tile);
|
|
const y = game.y(tile);
|
|
const nextX = game.x(next);
|
|
const nextY = game.y(next);
|
|
|
|
const dx = nextX - x;
|
|
const dy = nextY - y;
|
|
|
|
if (dx === 0 && dy === 0) return RailType.VERTICAL; // No movement
|
|
|
|
if (dx === 0) {
|
|
return RailType.VERTICAL;
|
|
} else if (dy === 0) {
|
|
return RailType.HORIZONTAL;
|
|
}
|
|
return RailType.VERTICAL;
|
|
}
|
|
|
|
export function computeDirection(
|
|
game: GameView,
|
|
prev: TileRef,
|
|
current: TileRef,
|
|
next: TileRef,
|
|
): RailType {
|
|
const x1 = game.x(prev);
|
|
const y1 = game.y(prev);
|
|
const x2 = game.x(current);
|
|
const y2 = game.y(current);
|
|
const x3 = game.x(next);
|
|
const y3 = game.y(next);
|
|
|
|
const dx1 = x2 - x1;
|
|
const dy1 = y2 - y1;
|
|
const dx2 = x3 - x2;
|
|
const dy2 = y3 - y2;
|
|
|
|
// Straight line
|
|
if (dx1 === dx2 && dy1 === dy2) {
|
|
if (dx1 !== 0) return RailType.HORIZONTAL;
|
|
if (dy1 !== 0) return RailType.VERTICAL;
|
|
}
|
|
|
|
// Turn (corner) cases
|
|
if ((dx1 === 0 && dx2 !== 0) || (dx1 !== 0 && dx2 === 0)) {
|
|
// Now figure out which type of corner
|
|
if (dx1 === 0 && dx2 === 1 && dy1 === -1) return RailType.BOTTOM_RIGHT;
|
|
if (dx1 === 0 && dx2 === -1 && dy1 === -1) return RailType.BOTTOM_LEFT;
|
|
if (dx1 === 0 && dx2 === 1 && dy1 === 1) return RailType.TOP_RIGHT;
|
|
if (dx1 === 0 && dx2 === -1 && dy1 === 1) return RailType.TOP_LEFT;
|
|
|
|
if (dx1 === 1 && dx2 === 0 && dy2 === -1) return RailType.TOP_LEFT;
|
|
if (dx1 === -1 && dx2 === 0 && dy2 === -1) return RailType.TOP_RIGHT;
|
|
if (dx1 === 1 && dx2 === 0 && dy2 === 1) return RailType.BOTTOM_LEFT;
|
|
if (dx1 === -1 && dx2 === 0 && dy2 === 1) return RailType.BOTTOM_RIGHT;
|
|
}
|
|
console.warn(`Invalid rail segment: ${dx1}:${dy1}, ${dx2}:${dy2}`);
|
|
return RailType.VERTICAL;
|
|
}
|
|
|
|
/**
|
|
* A list of tile that can be incrementally painted each tick
|
|
*/
|
|
export class RailroadView {
|
|
private headIndex: number = 0;
|
|
private tailIndex: number;
|
|
private increment: number = 3;
|
|
constructor(
|
|
public id: number,
|
|
private railTiles: RailTile[],
|
|
complete: boolean = false,
|
|
) {
|
|
// If the railroad is considered complete, no drawing or animation is required
|
|
this.tailIndex = complete ? 0 : railTiles.length;
|
|
}
|
|
|
|
isComplete(): boolean {
|
|
return this.headIndex >= this.tailIndex;
|
|
}
|
|
|
|
tiles(): RailTile[] {
|
|
return this.railTiles;
|
|
}
|
|
|
|
remainingTiles(): RailTile[] {
|
|
if (this.isComplete()) {
|
|
// Animation complete, no tiles need to be painted
|
|
return [];
|
|
}
|
|
return this.railTiles.slice(this.headIndex, this.tailIndex);
|
|
}
|
|
|
|
drawnTiles(): RailTile[] {
|
|
if (this.isComplete()) {
|
|
// Animation complete, every tiles have been painted
|
|
return this.tiles();
|
|
}
|
|
let drawnTiles = this.railTiles.slice(0, this.headIndex);
|
|
drawnTiles = drawnTiles.concat(this.railTiles.slice(this.tailIndex));
|
|
return drawnTiles;
|
|
}
|
|
|
|
tick(): RailTile[] {
|
|
if (this.isComplete()) return [];
|
|
let updatedRailTiles: RailTile[];
|
|
// Check if remaining tiles can be done all at once
|
|
if (this.tailIndex - this.headIndex <= 2 * this.increment) {
|
|
updatedRailTiles = this.railTiles.slice(this.headIndex, this.tailIndex);
|
|
} else {
|
|
updatedRailTiles = [
|
|
...this.railTiles.slice(
|
|
this.headIndex,
|
|
this.headIndex + this.increment,
|
|
),
|
|
...this.railTiles.slice(
|
|
this.tailIndex - this.increment,
|
|
this.tailIndex,
|
|
),
|
|
];
|
|
}
|
|
this.headIndex = Math.min(this.headIndex + this.increment, this.tailIndex);
|
|
this.tailIndex = Math.max(this.tailIndex - this.increment, this.headIndex);
|
|
return updatedRailTiles;
|
|
}
|
|
}
|