mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:50:44 +00:00
Train motion plans
This commit is contained in:
@@ -70,7 +70,7 @@ export class UnitLayer implements Layer {
|
||||
.updatesSinceLastTick()
|
||||
?.[GameUpdateType.Unit]?.map((unit) => unit.id) ?? [];
|
||||
|
||||
const motionPlanUnitIds = Array.from(this.game.motionPlans().keys());
|
||||
const motionPlanUnitIds = this.game.motionPlannedUnitIds();
|
||||
|
||||
if (updatedUnitIds.length === 0) {
|
||||
this.updateUnitsSprites(motionPlanUnitIds);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { MotionPlanRecord } from "../game/MotionPlans";
|
||||
import { RailNetwork } from "../game/RailNetwork";
|
||||
import { getOrientedRailroad, OrientedRailroad } from "../game/Railroad";
|
||||
import { TrainStation } from "../game/TrainStation";
|
||||
@@ -63,6 +64,36 @@ export class TrainExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
this.train = this.createTrainUnits(spawn);
|
||||
|
||||
const carUnitIds = this.cars.map((c) => c.id());
|
||||
const pathTiles: TileRef[] = [];
|
||||
for (let i = 0; i + 1 < this.stations.length; i++) {
|
||||
const segment = getOrientedRailroad(
|
||||
this.stations[i],
|
||||
this.stations[i + 1],
|
||||
);
|
||||
if (!segment) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
pathTiles.push(...segment.getTiles());
|
||||
}
|
||||
const startTile = this.train.tile();
|
||||
if (pathTiles.length === 0 || pathTiles[0] !== startTile) {
|
||||
pathTiles.unshift(startTile);
|
||||
}
|
||||
|
||||
const plan: MotionPlanRecord = {
|
||||
kind: "train",
|
||||
engineUnitId: this.train.id(),
|
||||
carUnitIds,
|
||||
planId: 1,
|
||||
startTick: ticks + 1,
|
||||
speed: this.speed,
|
||||
spacing: this.spacing,
|
||||
path: pathTiles,
|
||||
};
|
||||
this.mg.recordMotionPlan(plan);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
|
||||
@@ -438,6 +438,12 @@ export class GameImpl implements Game {
|
||||
case "grid":
|
||||
this.planDrivenUnitIds.add(record.unitId);
|
||||
break;
|
||||
case "train":
|
||||
this.planDrivenUnitIds.add(record.engineUnitId);
|
||||
for (const unitId of record.carUnitIds) {
|
||||
this.planDrivenUnitIds.add(unitId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.motionPlanRecords.push(record);
|
||||
}
|
||||
|
||||
@@ -598,6 +598,20 @@ export class PlayerView {
|
||||
}
|
||||
}
|
||||
|
||||
type TrainPlanState = {
|
||||
planId: number;
|
||||
startTick: number;
|
||||
speed: number;
|
||||
spacing: number;
|
||||
carUnitIds: Uint32Array;
|
||||
path: Uint32Array;
|
||||
cursor: number;
|
||||
usedTilesBuf: Uint32Array;
|
||||
usedHead: number;
|
||||
usedLen: number;
|
||||
lastAdvancedTick: Tick;
|
||||
};
|
||||
|
||||
export class GameView implements GameMap {
|
||||
private lastUpdate: GameUpdateViewData | null;
|
||||
private smallIDToID = new Map<number, PlayerID>();
|
||||
@@ -617,6 +631,8 @@ export class GameView implements GameMap {
|
||||
path: Uint32Array;
|
||||
}
|
||||
>();
|
||||
private trainMotionPlans = new Map<number, TrainPlanState>();
|
||||
private trainUnitToEngine = new Map<number, number>();
|
||||
|
||||
private toDelete = new Set<number>();
|
||||
|
||||
@@ -674,6 +690,21 @@ export class GameView implements GameMap {
|
||||
return this.unitMotionPlans;
|
||||
}
|
||||
|
||||
public motionPlannedUnitIds(): number[] {
|
||||
const out: number[] = [];
|
||||
for (const unitId of this.unitMotionPlans.keys()) {
|
||||
out.push(unitId);
|
||||
}
|
||||
for (const [engineId, plan] of this.trainMotionPlans) {
|
||||
out.push(engineId);
|
||||
for (let i = 0; i < plan.carUnitIds.length; i++) {
|
||||
const id = plan.carUnitIds[i] >>> 0;
|
||||
if (id !== 0) out.push(id);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public isCatchingUp(): boolean {
|
||||
return (this.lastUpdate?.pendingTurns ?? 0) > 1;
|
||||
}
|
||||
@@ -751,6 +782,7 @@ export class GameView implements GameMap {
|
||||
// Wait until next tick to delete the unit.
|
||||
this.toDelete.add(unit.id());
|
||||
this.unitMotionPlans.delete(unit.id());
|
||||
this.clearTrainPlanForUnit(unit.id());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -773,6 +805,106 @@ export class GameView implements GameMap {
|
||||
this.unitGrid.updateUnitCell(unit);
|
||||
}
|
||||
}
|
||||
|
||||
this.advanceTrainMotionPlannedUnits(currentTick);
|
||||
}
|
||||
|
||||
private clearTrainPlanForUnit(unitId: number): void {
|
||||
const engineId =
|
||||
this.trainUnitToEngine.get(unitId) ??
|
||||
(this.trainMotionPlans.has(unitId) ? unitId : null);
|
||||
if (engineId === null) {
|
||||
return;
|
||||
}
|
||||
const plan = this.trainMotionPlans.get(engineId);
|
||||
if (!plan) {
|
||||
this.trainUnitToEngine.delete(unitId);
|
||||
return;
|
||||
}
|
||||
this.trainMotionPlans.delete(engineId);
|
||||
this.trainUnitToEngine.delete(engineId);
|
||||
for (let i = 0; i < plan.carUnitIds.length; i++) {
|
||||
const id = plan.carUnitIds[i] >>> 0;
|
||||
if (id !== 0) this.trainUnitToEngine.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
private advanceTrainMotionPlannedUnits(currentTick: Tick): void {
|
||||
const staleEngineIds: number[] = [];
|
||||
for (const [engineId, plan] of this.trainMotionPlans) {
|
||||
const engine = this._units.get(engineId);
|
||||
if (!engine || !engine.isActive()) {
|
||||
staleEngineIds.push(engineId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const steps = currentTick - plan.lastAdvancedTick;
|
||||
if (steps <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const path = plan.path;
|
||||
const cap = plan.usedTilesBuf.length;
|
||||
|
||||
const pushUsed = (tile: TileRef) => {
|
||||
if (cap === 0) return;
|
||||
if (plan.usedLen < cap) {
|
||||
const idx = (plan.usedHead + plan.usedLen) % cap;
|
||||
plan.usedTilesBuf[idx] = tile >>> 0;
|
||||
plan.usedLen++;
|
||||
} else {
|
||||
plan.usedTilesBuf[plan.usedHead] = tile >>> 0;
|
||||
plan.usedHead = (plan.usedHead + 1) % cap;
|
||||
plan.usedLen = cap;
|
||||
}
|
||||
};
|
||||
|
||||
const usedGet = (index: number): TileRef | null => {
|
||||
if (index < 0 || index >= plan.usedLen || cap === 0) return null;
|
||||
const idx = (plan.usedHead + index) % cap;
|
||||
return plan.usedTilesBuf[idx] as TileRef;
|
||||
};
|
||||
|
||||
for (let step = 0; step < steps; step++) {
|
||||
const cursor = plan.cursor;
|
||||
for (let i = 0; i < plan.speed && cursor + i < path.length; i++) {
|
||||
pushUsed(path[cursor + i] as TileRef);
|
||||
}
|
||||
|
||||
plan.cursor = Math.min(path.length - 1, cursor + plan.speed);
|
||||
|
||||
for (let i = plan.carUnitIds.length - 1; i >= 0; --i) {
|
||||
const carId = plan.carUnitIds[i] >>> 0;
|
||||
if (carId === 0) continue;
|
||||
const car = this._units.get(carId);
|
||||
if (!car || !car.isActive()) {
|
||||
continue;
|
||||
}
|
||||
const carTileIndex = (i + 1) * plan.spacing + 2;
|
||||
const tile = usedGet(carTileIndex);
|
||||
if (tile !== null) {
|
||||
const oldTile = car.tile();
|
||||
car.applyDerivedPosition(tile);
|
||||
if (tile !== oldTile) {
|
||||
this.unitGrid.updateUnitCell(car);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newEngineTile = path[plan.cursor] as TileRef;
|
||||
const oldEngineTile = engine.tile();
|
||||
engine.applyDerivedPosition(newEngineTile);
|
||||
if (newEngineTile !== oldEngineTile) {
|
||||
this.unitGrid.updateUnitCell(engine);
|
||||
}
|
||||
}
|
||||
|
||||
plan.lastAdvancedTick = currentTick;
|
||||
}
|
||||
|
||||
for (const engineId of staleEngineIds) {
|
||||
this.clearTrainPlanForUnit(engineId);
|
||||
}
|
||||
}
|
||||
|
||||
private motionTileAtTick(
|
||||
@@ -814,6 +946,52 @@ export class GameView implements GameMap {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "train": {
|
||||
if (record.speed < 1 || record.path.length < 1) {
|
||||
break;
|
||||
}
|
||||
const existing = this.trainMotionPlans.get(record.engineUnitId);
|
||||
if (existing && record.planId <= existing.planId) {
|
||||
break;
|
||||
}
|
||||
if (existing) {
|
||||
this.clearTrainPlanForUnit(record.engineUnitId);
|
||||
}
|
||||
|
||||
const carUnitIds =
|
||||
record.carUnitIds instanceof Uint32Array
|
||||
? record.carUnitIds
|
||||
: Uint32Array.from(record.carUnitIds);
|
||||
const path =
|
||||
record.path instanceof Uint32Array
|
||||
? record.path
|
||||
: Uint32Array.from(record.path);
|
||||
|
||||
const usedCap = carUnitIds.length * record.spacing + 3;
|
||||
const usedTilesBuf = new Uint32Array(Math.max(0, usedCap));
|
||||
|
||||
this.trainMotionPlans.set(record.engineUnitId, {
|
||||
planId: record.planId,
|
||||
startTick: record.startTick,
|
||||
speed: record.speed,
|
||||
spacing: record.spacing,
|
||||
carUnitIds,
|
||||
path,
|
||||
cursor: 0,
|
||||
usedTilesBuf,
|
||||
usedHead: 0,
|
||||
usedLen: 0,
|
||||
lastAdvancedTick: record.startTick,
|
||||
});
|
||||
|
||||
this.trainUnitToEngine.set(record.engineUnitId, record.engineUnitId);
|
||||
for (let i = 0; i < carUnitIds.length; i++) {
|
||||
const carId = carUnitIds[i] >>> 0;
|
||||
if (carId !== 0)
|
||||
this.trainUnitToEngine.set(carId, record.engineUnitId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { TileRef } from "./GameMap";
|
||||
|
||||
export const MOTION_PLANS_SCHEMA_VERSION = 3;
|
||||
export const MOTION_PLANS_SCHEMA_VERSION = 4;
|
||||
|
||||
export enum PackedMotionPlanKind {
|
||||
GridPathSet = 1,
|
||||
TrainRailPathSet = 2,
|
||||
}
|
||||
|
||||
export interface GridPathPlan {
|
||||
@@ -18,7 +19,24 @@ export interface GridPathPlan {
|
||||
path: readonly TileRef[] | Uint32Array;
|
||||
}
|
||||
|
||||
export type MotionPlanRecord = GridPathPlan;
|
||||
export interface TrainRailPathPlan {
|
||||
kind: "train";
|
||||
engineUnitId: number;
|
||||
/**
|
||||
* TrainExecution `cars[]` order (tail engine + carriages).
|
||||
*/
|
||||
carUnitIds: readonly number[] | Uint32Array;
|
||||
planId: number;
|
||||
startTick: number;
|
||||
speed: number;
|
||||
spacing: number;
|
||||
/**
|
||||
* Concatenated rail tile path across all segments, without de-duplicating at stations.
|
||||
*/
|
||||
path: readonly TileRef[] | Uint32Array;
|
||||
}
|
||||
|
||||
export type MotionPlanRecord = GridPathPlan | TrainRailPathPlan;
|
||||
|
||||
export function packMotionPlans(
|
||||
records: readonly MotionPlanRecord[],
|
||||
@@ -48,6 +66,39 @@ export function packMotionPlans(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "train": {
|
||||
const carUnitIds =
|
||||
record.carUnitIds instanceof Uint32Array
|
||||
? record.carUnitIds
|
||||
: Uint32Array.from(record.carUnitIds);
|
||||
const carCount = carUnitIds.length >>> 0;
|
||||
|
||||
const path =
|
||||
record.path instanceof Uint32Array
|
||||
? record.path
|
||||
: Uint32Array.from(record.path);
|
||||
const pathLen = path.length >>> 0;
|
||||
|
||||
const wordCount = 2 + 7 + carCount + pathLen;
|
||||
out.push(
|
||||
PackedMotionPlanKind.TrainRailPathSet,
|
||||
wordCount,
|
||||
record.engineUnitId >>> 0,
|
||||
record.planId >>> 0,
|
||||
record.startTick >>> 0,
|
||||
record.speed >>> 0,
|
||||
record.spacing >>> 0,
|
||||
carCount,
|
||||
pathLen,
|
||||
);
|
||||
for (let i = 0; i < carUnitIds.length; i++) {
|
||||
out.push(carUnitIds[i] >>> 0);
|
||||
}
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
out.push(path[i] >>> 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +157,42 @@ export function unpackMotionPlans(packed: Uint32Array): {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PackedMotionPlanKind.TrainRailPathSet: {
|
||||
if (wordCount < 2 + 7) {
|
||||
break;
|
||||
}
|
||||
const engineUnitId = packed[offset + 2] >>> 0;
|
||||
const planId = packed[offset + 3] >>> 0;
|
||||
const startTick = packed[offset + 4] >>> 0;
|
||||
const speed = packed[offset + 5] >>> 0;
|
||||
const spacing = packed[offset + 6] >>> 0;
|
||||
const carCount = packed[offset + 7] >>> 0;
|
||||
const pathLen = packed[offset + 8] >>> 0;
|
||||
|
||||
const expectedWordCount = 2 + 7 + carCount + pathLen;
|
||||
if (expectedWordCount !== wordCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
const carStart = offset + 9;
|
||||
const carEnd = carStart + carCount;
|
||||
const pathStart = carEnd;
|
||||
const pathEnd = pathStart + pathLen;
|
||||
const carUnitIds = packed.slice(carStart, carEnd);
|
||||
const path = packed.slice(pathStart, pathEnd);
|
||||
|
||||
records.push({
|
||||
kind: "train",
|
||||
engineUnitId,
|
||||
carUnitIds,
|
||||
planId,
|
||||
startTick,
|
||||
speed,
|
||||
spacing,
|
||||
path,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Unknown kind: skip.
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user