mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-26 13:22:46 +00:00
4cd22a9b5c
The render/ tree was the only place in the client still using kebab-case filenames. Brings ~80 files in line with the rest of src/client/ (BuildPreviewController, TransformHandler, etc.). Directories kept as they were (name-pass/, fx-pass/, passes/, utils/, debug/) since the codebase already mixes those. Two collisions surfaced and got resolved: render/types/ is a directory, not a file, so its imports kept the lowercase form; and the sed pass incidentally normalized core/pathfinding imports, which had to be reverted since that file is actually lowercase on disk despite some imports having referenced it as ./Types under macOS case-insensitive resolution.
274 lines
8.4 KiB
TypeScript
274 lines
8.4 KiB
TypeScript
/**
|
|
* RailroadCache — always-on accumulator for railroad events.
|
|
*
|
|
* The game doesn't expose current railroad state via any API — it only sends
|
|
* construction/destruction/snap delta events. This cache accumulates them
|
|
* every tick so consumers that start later can reconstruct the full set.
|
|
*
|
|
* Includes orientation computation, construction animation, and a per-tile
|
|
* Uint8Array ready for GPU upload.
|
|
*
|
|
* Ported verbatim from openfront-workspace/packages/shim/src/railroad-cache.ts;
|
|
* only imports changed (types come from src/core/game/GameUpdates instead of
|
|
* the shim's local types module).
|
|
*/
|
|
|
|
import {
|
|
GameUpdateType,
|
|
GameUpdateViewData,
|
|
RailroadConstructionUpdate,
|
|
RailroadDestructionUpdate,
|
|
RailroadSnapUpdate,
|
|
} from "../../../core/game/GameUpdates";
|
|
|
|
// Regular enum (not const enum) for cross-package use.
|
|
export enum RailType {
|
|
VERTICAL,
|
|
HORIZONTAL,
|
|
TOP_LEFT,
|
|
TOP_RIGHT,
|
|
BOTTOM_LEFT,
|
|
BOTTOM_RIGHT,
|
|
}
|
|
|
|
interface RailTile {
|
|
ref: number;
|
|
type: RailType;
|
|
}
|
|
|
|
interface RailroadAnim {
|
|
tiles: RailTile[];
|
|
headIndex: number;
|
|
tailIndex: number;
|
|
complete: boolean;
|
|
}
|
|
|
|
const RAIL_INCREMENT = 3;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Orientation helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function railExtremity(tile: number, next: number, w: number): RailType {
|
|
const dx = (next % w) - (tile % w);
|
|
const dy = (next - (next % w)) / w - (tile - (tile % w)) / w;
|
|
if (dx === 0) return RailType.VERTICAL;
|
|
if (dy === 0) return RailType.HORIZONTAL;
|
|
return RailType.VERTICAL;
|
|
}
|
|
|
|
function railDirection(
|
|
prev: number,
|
|
cur: number,
|
|
next: number,
|
|
w: number,
|
|
): RailType {
|
|
const x1 = prev % w,
|
|
y1 = (prev - x1) / w;
|
|
const x2 = cur % w,
|
|
y2 = (cur - x2) / w;
|
|
const x3 = next % w,
|
|
y3 = (next - x3) / w;
|
|
const dx1 = x2 - x1,
|
|
dy1 = y2 - y1;
|
|
const dx2 = x3 - x2,
|
|
dy2 = y3 - y2;
|
|
if (dx1 === dx2 && dy1 === dy2) {
|
|
return dx1 !== 0 ? RailType.HORIZONTAL : RailType.VERTICAL;
|
|
}
|
|
if ((dx1 === 0 && dx2 !== 0) || (dx1 !== 0 && dx2 === 0)) {
|
|
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;
|
|
}
|
|
return RailType.VERTICAL;
|
|
}
|
|
|
|
function computeRailTiles(tileRefs: number[], w: number): RailTile[] {
|
|
if (tileRefs.length === 0) return [];
|
|
if (tileRefs.length === 1)
|
|
return [{ ref: tileRefs[0]!, type: RailType.VERTICAL }];
|
|
const result: RailTile[] = [];
|
|
result.push({
|
|
ref: tileRefs[0]!,
|
|
type: railExtremity(tileRefs[0]!, tileRefs[1]!, w),
|
|
});
|
|
for (let i = 1; i < tileRefs.length - 1; i++) {
|
|
result.push({
|
|
ref: tileRefs[i]!,
|
|
type: railDirection(tileRefs[i - 1]!, tileRefs[i]!, tileRefs[i + 1]!, w),
|
|
});
|
|
}
|
|
const last = tileRefs.length - 1;
|
|
result.push({
|
|
ref: tileRefs[last]!,
|
|
type: railExtremity(tileRefs[last]!, tileRefs[last - 1]!, w),
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export class RailroadCache {
|
|
private mapW: number;
|
|
private anims = new Map<number, RailroadAnim>();
|
|
|
|
/**
|
|
* Per-tile reference count. Multiple railroads can share tiles at junctions
|
|
* (near stations). A tile is only cleared from railroadState when its ref
|
|
* count drops to zero.
|
|
*/
|
|
private tileRefCount = new Map<number, number>();
|
|
|
|
/** Per-tile railroad state (0=none, 1-6 = RailType+1). Ready for GPU upload. */
|
|
readonly railroadState: Uint8Array;
|
|
|
|
/** True if railroadState changed this tick. */
|
|
railroadDirty = false;
|
|
|
|
/** Tile refs revealed by animation this tick (for dust FX). */
|
|
readonly revealedRailTiles: number[] = [];
|
|
|
|
constructor(mapW: number, mapH: number) {
|
|
this.mapW = mapW;
|
|
this.railroadState = new Uint8Array(mapW * mapH);
|
|
}
|
|
|
|
/**
|
|
* Process this tick's railroad events and advance animations.
|
|
* Event order matches the upstream game client: Construction → Snap → Destruction.
|
|
*/
|
|
apply(gu: GameUpdateViewData): void {
|
|
const constructs = (gu.updates[GameUpdateType.RailroadConstructionEvent] ??
|
|
[]) as RailroadConstructionUpdate[];
|
|
for (const evt of constructs) this.addRailroad(evt.id, evt.tiles, false);
|
|
|
|
const snaps = (gu.updates[GameUpdateType.RailroadSnapEvent] ??
|
|
[]) as RailroadSnapUpdate[];
|
|
for (const evt of snaps) {
|
|
this.removeRailroad(evt.originalId);
|
|
this.addRailroad(evt.newId1, evt.tiles1, true);
|
|
this.addRailroad(evt.newId2, evt.tiles2, true);
|
|
}
|
|
|
|
const destructs = (gu.updates[GameUpdateType.RailroadDestructionEvent] ??
|
|
[]) as RailroadDestructionUpdate[];
|
|
for (const evt of destructs) this.removeRailroad(evt.id);
|
|
|
|
this.tickAnimations();
|
|
}
|
|
|
|
/** Clear the dirty flag after the consumer has uploaded the state. */
|
|
clearDirty(): void {
|
|
this.railroadDirty = false;
|
|
}
|
|
|
|
/** Get raw tile refs for the given railroad IDs (for ghost manager overlap resolution). */
|
|
getRailroadTileRefs(ids: number[]): number[] {
|
|
const tiles: number[] = [];
|
|
for (const id of ids) {
|
|
const anim = this.anims.get(id);
|
|
if (anim) for (const t of anim.tiles) tiles.push(t.ref);
|
|
}
|
|
return tiles;
|
|
}
|
|
|
|
/** Read-only view of current railroads: id → raw tile refs. */
|
|
getRailroads(): ReadonlyMap<number, number[]> {
|
|
const result = new Map<number, number[]>();
|
|
for (const [id, anim] of this.anims) {
|
|
result.set(
|
|
id,
|
|
anim.tiles.map((t) => t.ref),
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
reset(): void {
|
|
this.anims.clear();
|
|
this.tileRefCount.clear();
|
|
this.railroadState.fill(0);
|
|
this.railroadDirty = false;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private
|
|
// -------------------------------------------------------------------------
|
|
|
|
private addRailroad(id: number, tileRefs: number[], complete: boolean): void {
|
|
const tiles = computeRailTiles(tileRefs, this.mapW);
|
|
const anim: RailroadAnim = {
|
|
tiles,
|
|
headIndex: complete ? tiles.length : 0,
|
|
tailIndex: complete ? 0 : tiles.length,
|
|
complete,
|
|
};
|
|
this.anims.set(id, anim);
|
|
|
|
// Increment ref counts for all tiles in this railroad
|
|
for (const rt of tiles) {
|
|
this.tileRefCount.set(rt.ref, (this.tileRefCount.get(rt.ref) ?? 0) + 1);
|
|
}
|
|
|
|
if (complete) {
|
|
for (const rt of tiles) this.railroadState[rt.ref] = rt.type + 1;
|
|
this.railroadDirty = true;
|
|
}
|
|
}
|
|
|
|
private removeRailroad(id: number): void {
|
|
const anim = this.anims.get(id);
|
|
if (!anim) return;
|
|
|
|
// Decrement ref counts; only clear tiles whose count drops to zero
|
|
for (const rt of anim.tiles) {
|
|
const count = (this.tileRefCount.get(rt.ref) ?? 1) - 1;
|
|
if (count <= 0) {
|
|
this.tileRefCount.delete(rt.ref);
|
|
this.railroadState[rt.ref] = 0;
|
|
} else {
|
|
this.tileRefCount.set(rt.ref, count);
|
|
}
|
|
}
|
|
|
|
this.anims.delete(id);
|
|
this.railroadDirty = true;
|
|
}
|
|
|
|
private tickAnimations(): void {
|
|
this.revealedRailTiles.length = 0;
|
|
for (const anim of this.anims.values()) {
|
|
if (anim.complete) continue;
|
|
if (anim.tailIndex - anim.headIndex <= 2 * RAIL_INCREMENT) {
|
|
for (let i = anim.headIndex; i < anim.tailIndex; i++) {
|
|
const t = anim.tiles[i]!;
|
|
this.railroadState[t.ref] = t.type + 1;
|
|
this.revealedRailTiles.push(t.ref);
|
|
}
|
|
anim.headIndex = anim.tailIndex;
|
|
anim.complete = true;
|
|
this.railroadDirty = true;
|
|
} else {
|
|
for (let i = anim.headIndex; i < anim.headIndex + RAIL_INCREMENT; i++) {
|
|
const t = anim.tiles[i]!;
|
|
this.railroadState[t.ref] = t.type + 1;
|
|
this.revealedRailTiles.push(t.ref);
|
|
}
|
|
for (let i = anim.tailIndex - RAIL_INCREMENT; i < anim.tailIndex; i++) {
|
|
const t = anim.tiles[i]!;
|
|
this.railroadState[t.ref] = t.type + 1;
|
|
this.revealedRailTiles.push(t.ref);
|
|
}
|
|
anim.headIndex += RAIL_INCREMENT;
|
|
anim.tailIndex -= RAIL_INCREMENT;
|
|
if (anim.headIndex >= anim.tailIndex) anim.complete = true;
|
|
this.railroadDirty = true;
|
|
}
|
|
}
|
|
}
|
|
}
|