mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-22 21:18:44 +00:00
Highlight hovering railroad (#3156)
## 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>
This commit is contained in:
co-authored by
jrouillard
parent
742cbf90f3
commit
c6c793f6b3
@@ -265,11 +265,11 @@ export function createRenderer(
|
||||
const layers: Layer[] = [
|
||||
new TerrainLayer(game, transformHandler),
|
||||
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
|
||||
new RailroadLayer(game, eventBus, transformHandler),
|
||||
new RailroadLayer(game, eventBus, transformHandler, uiState),
|
||||
structureLayer,
|
||||
samRadiusLayer,
|
||||
new UnitLayer(game, eventBus, transformHandler),
|
||||
new FxLayer(game, transformHandler),
|
||||
new FxLayer(game, eventBus, transformHandler),
|
||||
new UILayer(game, eventBus, transformHandler),
|
||||
new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState),
|
||||
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
|
||||
|
||||
@@ -3,5 +3,6 @@ import { UnitType } from "../../core/game/Game";
|
||||
export interface UIState {
|
||||
attackRatio: number;
|
||||
ghostStructure: UnitType | null;
|
||||
overlappingRailroads: number[];
|
||||
rocketDirectionUp: boolean;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import {
|
||||
ConquestUpdate,
|
||||
GameUpdateType,
|
||||
RailroadUpdate,
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { ConquestUpdate, GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import SoundManager, { SoundEffect } from "../../sound/SoundManager";
|
||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
@@ -15,6 +13,7 @@ import { SpriteFx } from "../fx/SpriteFx";
|
||||
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
import { RailTileChangedEvent } from "./RailroadLayer";
|
||||
export class FxLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
@@ -30,6 +29,7 @@ export class FxLayer implements Layer {
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
) {
|
||||
this.theme = this.game.config().theme();
|
||||
@@ -50,12 +50,6 @@ export class FxLayer implements Layer {
|
||||
if (unitView === undefined) return;
|
||||
this.onUnitEvent(unitView);
|
||||
});
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
?.[GameUpdateType.RailroadEvent]?.forEach((update) => {
|
||||
if (update === undefined) return;
|
||||
this.onRailroadEvent(update);
|
||||
});
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
?.[GameUpdateType.ConquestEvent]?.forEach((update) => {
|
||||
@@ -129,22 +123,19 @@ export class FxLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
onRailroadEvent(railroad: RailroadUpdate) {
|
||||
const railTiles = railroad.railTiles;
|
||||
for (const rail of railTiles) {
|
||||
// No need for pseudorandom, this is fx
|
||||
const chanceFx = Math.floor(Math.random() * 3);
|
||||
if (chanceFx === 0) {
|
||||
const x = this.game.x(rail.tile);
|
||||
const y = this.game.y(rail.tile);
|
||||
const animation = new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
x,
|
||||
y,
|
||||
FxType.Dust,
|
||||
);
|
||||
this.allFx.push(animation);
|
||||
}
|
||||
onRailroadEvent(tile: TileRef) {
|
||||
// No need for pseudorandom, this is fx
|
||||
const chanceFx = Math.floor(Math.random() * 3);
|
||||
if (chanceFx === 0) {
|
||||
const x = this.game.x(tile);
|
||||
const y = this.game.y(tile);
|
||||
const animation = new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
x,
|
||||
y,
|
||||
FxType.Dust,
|
||||
);
|
||||
this.allFx.push(animation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +231,10 @@ export class FxLayer implements Layer {
|
||||
|
||||
async init() {
|
||||
this.redraw();
|
||||
|
||||
this.eventBus.on(RailTileChangedEvent, (e) => {
|
||||
this.onRailroadEvent(e.tile);
|
||||
});
|
||||
try {
|
||||
this.animatedSpriteLoader.loadAllAnimatedSpriteImages();
|
||||
console.log("FX sprites loaded successfully");
|
||||
|
||||
@@ -1,33 +1,49 @@
|
||||
import { colord } from "colord";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { PlayerID } from "../../../core/game/Game";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { PlayerID, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import {
|
||||
GameUpdateType,
|
||||
RailroadUpdate,
|
||||
RailTile,
|
||||
RailType,
|
||||
RailroadConstructionUpdate,
|
||||
RailroadDestructionUpdate,
|
||||
RailroadSnapUpdate,
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { AlternateViewEvent } from "../../InputHandler";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
import { getBridgeRects, getRailroadRects } from "./RailroadSprites";
|
||||
import {
|
||||
computeRailTiles,
|
||||
RailroadView,
|
||||
RailTile,
|
||||
RailType,
|
||||
} from "./RailroadView";
|
||||
|
||||
type RailRef = {
|
||||
tile: RailTile;
|
||||
numOccurence: number;
|
||||
lastOwnerId: PlayerID | null;
|
||||
};
|
||||
const SNAPPABLE_STRUCTURES: UnitType[] = [
|
||||
UnitType.Port,
|
||||
UnitType.City,
|
||||
UnitType.Factory,
|
||||
];
|
||||
export class RailTileChangedEvent implements GameEvent {
|
||||
constructor(public tile: TileRef) {}
|
||||
}
|
||||
|
||||
export class RailroadLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private theme: Theme;
|
||||
private alternativeView = false;
|
||||
// Save the number of railroads per tiles. Delete when it reaches 0
|
||||
private existingRailroads = new Map<TileRef, RailRef>();
|
||||
private railroads = new Map<number, RailroadView>();
|
||||
// Railroads under construction
|
||||
private pendingRailroads = new Set<number>();
|
||||
private nextRailIndexToCheck = 0;
|
||||
private railTileList: TileRef[] = [];
|
||||
private railTileIndex = new Map<TileRef, number>();
|
||||
@@ -38,20 +54,52 @@ export class RailroadLayer implements Layer {
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
private uiState: UIState,
|
||||
) {}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.updatePendingRailroads();
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const railUpdates =
|
||||
updates !== null ? updates[GameUpdateType.RailroadEvent] : [];
|
||||
for (const rail of railUpdates) {
|
||||
this.handleRailroadRendering(rail);
|
||||
if (!updates) return;
|
||||
// The event has to be handled in this specific order: construction / snap / destruction
|
||||
// Otherwise some ID may not be available yet/anymore
|
||||
updates[GameUpdateType.RailroadConstructionEvent]?.forEach((update) => {
|
||||
if (update === undefined) return;
|
||||
this.onRailroadConstruction(update);
|
||||
});
|
||||
updates[GameUpdateType.RailroadSnapEvent]?.forEach((update) => {
|
||||
if (update === undefined) return;
|
||||
this.onRailroadSnapEvent(update);
|
||||
});
|
||||
updates[GameUpdateType.RailroadDestructionEvent]?.forEach((update) => {
|
||||
if (update === undefined) return;
|
||||
this.onRailroadDestruction(update);
|
||||
});
|
||||
}
|
||||
|
||||
updatePendingRailroads() {
|
||||
for (const id of this.pendingRailroads) {
|
||||
const pending = this.railroads.get(id);
|
||||
if (pending === undefined) {
|
||||
// Rail deleted or snapped before the end of the animation
|
||||
this.pendingRailroads.delete(id);
|
||||
continue;
|
||||
}
|
||||
const newTiles = pending.tick();
|
||||
if (newTiles.length === 0) {
|
||||
// Animation complete
|
||||
this.pendingRailroads.delete(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const railTile of newTiles) {
|
||||
this.paintRailTile(railTile);
|
||||
this.eventBus.emit(new RailTileChangedEvent(railTile.tile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +168,32 @@ export class RailroadLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private highlightOverlappingRailroads(context: CanvasRenderingContext2D) {
|
||||
if (
|
||||
this.uiState.ghostStructure === null ||
|
||||
!SNAPPABLE_STRUCTURES.includes(this.uiState.ghostStructure)
|
||||
)
|
||||
return;
|
||||
if (
|
||||
this.uiState.overlappingRailroads === undefined ||
|
||||
this.uiState.overlappingRailroads.length === 0
|
||||
)
|
||||
return;
|
||||
const offsetX = -this.game.width() / 2;
|
||||
const offsetY = -this.game.height() / 2;
|
||||
context.fillStyle = "rgba(0, 255, 0, 0.4)";
|
||||
for (const id of this.uiState.overlappingRailroads) {
|
||||
const rail = this.railroads.get(id);
|
||||
if (rail) {
|
||||
for (const railTile of rail.drawnTiles()) {
|
||||
const x = this.game.x(railTile.tile);
|
||||
const y = this.game.y(railTile.tile);
|
||||
context.fillRect(x + offsetX - 1, y + offsetY - 1, 2.5, 2.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
const scale = this.transformHandler.scale;
|
||||
if (scale <= 1) {
|
||||
@@ -154,6 +228,7 @@ export class RailroadLayer implements Layer {
|
||||
|
||||
context.save();
|
||||
context.globalAlpha = alpha;
|
||||
this.highlightOverlappingRailroads(context);
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
srcX,
|
||||
@@ -168,55 +243,115 @@ export class RailroadLayer implements Layer {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
private handleRailroadRendering(railUpdate: RailroadUpdate) {
|
||||
for (const railRoad of railUpdate.railTiles) {
|
||||
if (railUpdate.isActive) {
|
||||
this.paintRailroad(railRoad);
|
||||
} else {
|
||||
this.clearRailroad(railRoad);
|
||||
}
|
||||
private onRailroadSnapEvent(update: RailroadSnapUpdate) {
|
||||
const original = this.railroads.get(update.originalId);
|
||||
if (!original) {
|
||||
console.warn("Could not snap railroad: ", update.originalId);
|
||||
return;
|
||||
}
|
||||
if (!original.isComplete()) {
|
||||
// The animation is not complete but we don't want to compute where the animation should resume
|
||||
// Just draw every remaining rails at once
|
||||
this.drawRemainingTiles(original);
|
||||
}
|
||||
|
||||
// No need to compute the directions here, the rails are already painted
|
||||
const directions1: RailTile[] = update.tiles1.map((tile) => ({
|
||||
tile,
|
||||
type: RailType.HORIZONTAL,
|
||||
}));
|
||||
const directions2: RailTile[] = update.tiles2.map((tile) => ({
|
||||
tile,
|
||||
type: RailType.HORIZONTAL,
|
||||
}));
|
||||
// The rails are already painted, consider them complete
|
||||
this.railroads.set(
|
||||
update.newId1,
|
||||
new RailroadView(update.newId1, directions1, true),
|
||||
);
|
||||
this.railroads.set(
|
||||
update.newId2,
|
||||
new RailroadView(update.newId2, directions2, true),
|
||||
);
|
||||
|
||||
this.railroads.delete(update.originalId);
|
||||
}
|
||||
|
||||
private paintRailroad(railRoad: RailTile) {
|
||||
const currentOwner = this.game.owner(railRoad.tile)?.id() ?? null;
|
||||
const railTile = this.existingRailroads.get(railRoad.tile);
|
||||
private drawRemainingTiles(railroad: RailroadView) {
|
||||
for (const tile of railroad.remainingTiles()) {
|
||||
this.paintRail(tile);
|
||||
}
|
||||
this.pendingRailroads.delete(railroad.id);
|
||||
}
|
||||
|
||||
if (railTile) {
|
||||
railTile.numOccurence++;
|
||||
railTile.tile = railRoad;
|
||||
railTile.lastOwnerId = currentOwner;
|
||||
private onRailroadConstruction(railUpdate: RailroadConstructionUpdate) {
|
||||
const railTiles = computeRailTiles(this.game, railUpdate.tiles);
|
||||
const rail = new RailroadView(railUpdate.id, railTiles);
|
||||
this.addRailroad(rail);
|
||||
}
|
||||
|
||||
private onRailroadDestruction(railUpdate: RailroadDestructionUpdate) {
|
||||
const railroad = this.railroads.get(railUpdate.id);
|
||||
if (!railroad) {
|
||||
console.warn("Can't remove unexisting railroad: ", railUpdate.id);
|
||||
return;
|
||||
}
|
||||
this.removeRailroad(railroad);
|
||||
}
|
||||
|
||||
private addRailroad(railroad: RailroadView) {
|
||||
this.railroads.set(railroad.id, railroad);
|
||||
this.pendingRailroads.add(railroad.id);
|
||||
}
|
||||
|
||||
private removeRailroad(railroad: RailroadView) {
|
||||
this.pendingRailroads.delete(railroad.id);
|
||||
for (const railTile of railroad.drawnTiles()) {
|
||||
this.clearRailroad(railTile.tile);
|
||||
this.eventBus.emit(new RailTileChangedEvent(railTile.tile));
|
||||
}
|
||||
this.railroads.delete(railroad.id);
|
||||
}
|
||||
|
||||
private paintRailTile(railTile: RailTile) {
|
||||
const currentOwner = this.game.owner(railTile.tile)?.id() ?? null;
|
||||
const railRef = this.existingRailroads.get(railTile.tile);
|
||||
|
||||
if (railRef) {
|
||||
railRef.numOccurence++;
|
||||
railRef.tile = railTile;
|
||||
railRef.lastOwnerId = currentOwner;
|
||||
} else {
|
||||
this.existingRailroads.set(railRoad.tile, {
|
||||
tile: railRoad,
|
||||
this.existingRailroads.set(railTile.tile, {
|
||||
tile: railTile,
|
||||
numOccurence: 1,
|
||||
lastOwnerId: currentOwner,
|
||||
});
|
||||
this.railTileIndex.set(railRoad.tile, this.railTileList.length);
|
||||
this.railTileList.push(railRoad.tile);
|
||||
this.paintRail(railRoad);
|
||||
this.railTileIndex.set(railTile.tile, this.railTileList.length);
|
||||
this.railTileList.push(railTile.tile);
|
||||
this.paintRail(railTile);
|
||||
}
|
||||
}
|
||||
|
||||
private clearRailroad(railRoad: RailTile) {
|
||||
const ref = this.existingRailroads.get(railRoad.tile);
|
||||
private clearRailroad(railroad: TileRef) {
|
||||
const ref = this.existingRailroads.get(railroad);
|
||||
if (ref) ref.numOccurence--;
|
||||
|
||||
if (!ref || ref.numOccurence <= 0) {
|
||||
this.existingRailroads.delete(railRoad.tile);
|
||||
this.removeRailTile(railRoad.tile);
|
||||
this.existingRailroads.delete(railroad);
|
||||
this.removeRailTile(railroad);
|
||||
if (this.context === undefined) throw new Error("Not initialized");
|
||||
if (this.game.isWater(railRoad.tile)) {
|
||||
if (this.game.isWater(railroad)) {
|
||||
this.context.clearRect(
|
||||
this.game.x(railRoad.tile) * 2 - 2,
|
||||
this.game.y(railRoad.tile) * 2 - 2,
|
||||
this.game.x(railroad) * 2 - 2,
|
||||
this.game.y(railroad) * 2 - 2,
|
||||
5,
|
||||
6,
|
||||
);
|
||||
} else {
|
||||
this.context.clearRect(
|
||||
this.game.x(railRoad.tile) * 2 - 1,
|
||||
this.game.y(railRoad.tile) * 2 - 1,
|
||||
this.game.x(railroad) * 2 - 1,
|
||||
this.game.y(railroad) * 2 - 1,
|
||||
3,
|
||||
3,
|
||||
);
|
||||
@@ -242,15 +377,15 @@ export class RailroadLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
paintRail(railRoad: RailTile) {
|
||||
paintRail(railTile: RailTile) {
|
||||
if (this.context === undefined) throw new Error("Not initialized");
|
||||
const { tile } = railRoad;
|
||||
const { railType } = railRoad;
|
||||
const { tile } = railTile;
|
||||
const { type } = railTile;
|
||||
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);
|
||||
this.paintBridge(this.context, x, y, type);
|
||||
}
|
||||
const owner = this.game.owner(tile);
|
||||
const recipient = owner.isPlayer() ? owner : null;
|
||||
@@ -263,7 +398,7 @@ export class RailroadLayer implements Layer {
|
||||
}
|
||||
|
||||
this.context.fillStyle = color.toRgbString();
|
||||
this.paintRailRects(this.context, x, y, railType);
|
||||
this.paintRailRects(this.context, x, y, type);
|
||||
}
|
||||
|
||||
private paintRailRects(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RailType } from "../../../core/game/GameUpdates";
|
||||
import { RailType } from "./RailroadView";
|
||||
|
||||
const railTypeToFunctionMap: Record<RailType, () => number[][]> = {
|
||||
[RailType.TOP_RIGHT]: topRightRailroadCornerRects,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -333,10 +333,15 @@ export class StructureIconsLayer implements Layer {
|
||||
new OutlineFilter({ thickness: 2, color: "rgba(0, 255, 0, 1)" }),
|
||||
];
|
||||
}
|
||||
// No overlapping when a structure is upgradable
|
||||
this.uiState.overlappingRailroads = [];
|
||||
} else if (unit.canBuild === false) {
|
||||
this.ghostUnit.container.filters = [
|
||||
new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }),
|
||||
];
|
||||
this.uiState.overlappingRailroads = [];
|
||||
} else {
|
||||
this.uiState.overlappingRailroads = unit.overlappingRailroads;
|
||||
}
|
||||
|
||||
const scale = this.transformHandler.scale;
|
||||
@@ -450,7 +455,13 @@ export class StructureIconsLayer implements Layer {
|
||||
priceGroup: ghost.priceGroup,
|
||||
priceBox: ghost.priceBox,
|
||||
range: null,
|
||||
buildableUnit: { type, canBuild: false, canUpgrade: false, cost: 0n },
|
||||
buildableUnit: {
|
||||
type,
|
||||
canBuild: false,
|
||||
canUpgrade: false,
|
||||
cost: 0n,
|
||||
overlappingRailroads: [],
|
||||
},
|
||||
};
|
||||
const showPrice = this.game.config().userSettings().cursorCostLabel();
|
||||
this.updateGhostPrice(0, showPrice);
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import { Execution, Game } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { GameUpdateType, RailTile, RailType } from "../game/GameUpdates";
|
||||
import { Railroad } from "../game/Railroad";
|
||||
|
||||
export class RailroadExecution implements Execution {
|
||||
private mg: Game;
|
||||
private active: boolean = true;
|
||||
private headIndex: number = 0;
|
||||
private tailIndex: number = 0;
|
||||
private increment: number = 3;
|
||||
private railTiles: RailTile[] = [];
|
||||
constructor(private railRoad: Railroad) {
|
||||
this.tailIndex = railRoad.tiles.length;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
const tiles = this.railRoad.tiles;
|
||||
// Inverse direction computation for the first tile
|
||||
this.railTiles.push({
|
||||
tile: tiles[0],
|
||||
railType:
|
||||
tiles.length > 0
|
||||
? this.computeExtremityDirection(tiles[0], tiles[1])
|
||||
: RailType.VERTICAL,
|
||||
});
|
||||
for (let i = 1; i < tiles.length - 1; i++) {
|
||||
const direction = this.computeDirection(
|
||||
tiles[i - 1],
|
||||
tiles[i],
|
||||
tiles[i + 1],
|
||||
);
|
||||
this.railTiles.push({ tile: tiles[i], railType: direction });
|
||||
}
|
||||
this.railTiles.push({
|
||||
tile: tiles[tiles.length - 1],
|
||||
railType:
|
||||
tiles.length > 0
|
||||
? this.computeExtremityDirection(
|
||||
tiles[tiles.length - 1],
|
||||
tiles[tiles.length - 2],
|
||||
)
|
||||
: RailType.VERTICAL,
|
||||
});
|
||||
}
|
||||
|
||||
private computeExtremityDirection(tile: TileRef, next: TileRef): RailType {
|
||||
const x = this.mg.x(tile);
|
||||
const y = this.mg.y(tile);
|
||||
const nextX = this.mg.x(next);
|
||||
const nextY = this.mg.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;
|
||||
}
|
||||
|
||||
private computeDirection(
|
||||
prev: TileRef,
|
||||
current: TileRef,
|
||||
next: TileRef,
|
||||
): RailType {
|
||||
if (this.mg === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const x1 = this.mg.x(prev);
|
||||
const y1 = this.mg.y(prev);
|
||||
const x2 = this.mg.x(current);
|
||||
const y2 = this.mg.y(current);
|
||||
const x3 = this.mg.x(next);
|
||||
const y3 = this.mg.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;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.mg === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (!this.activeSourceOrDestination()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.headIndex > this.tailIndex) {
|
||||
// Construction complete
|
||||
this.constructionComplete();
|
||||
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);
|
||||
this.constructionComplete();
|
||||
} else {
|
||||
updatedRailTiles = this.railTiles.slice(
|
||||
this.headIndex,
|
||||
this.headIndex + this.increment,
|
||||
);
|
||||
updatedRailTiles = updatedRailTiles.concat(
|
||||
this.railTiles.slice(this.tailIndex - this.increment, this.tailIndex),
|
||||
);
|
||||
this.headIndex += this.increment;
|
||||
this.tailIndex -= this.increment;
|
||||
}
|
||||
if (updatedRailTiles) {
|
||||
this.mg.addUpdate({
|
||||
type: GameUpdateType.RailroadEvent,
|
||||
isActive: true,
|
||||
railTiles: updatedRailTiles,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private activeSourceOrDestination(): boolean {
|
||||
return this.railRoad.from.isActive() && this.railRoad.to.isActive();
|
||||
}
|
||||
|
||||
private constructionComplete() {
|
||||
this.redrawBuildings();
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
private redrawBuildings() {
|
||||
if (this.railRoad.from.unit.isActive()) this.railRoad.from.unit.touch();
|
||||
if (this.railRoad.to.unit.isActive()) this.railRoad.to.unit.touch();
|
||||
}
|
||||
}
|
||||
@@ -845,6 +845,7 @@ export interface BuildableUnit {
|
||||
canUpgrade: number | false;
|
||||
type: UnitType;
|
||||
cost: Gold;
|
||||
overlappingRailroads: number[];
|
||||
}
|
||||
|
||||
export interface PlayerProfile {
|
||||
|
||||
@@ -44,7 +44,9 @@ export enum GameUpdateType {
|
||||
Hash,
|
||||
UnitIncoming,
|
||||
BonusEvent,
|
||||
RailroadEvent,
|
||||
RailroadDestructionEvent,
|
||||
RailroadConstructionEvent,
|
||||
RailroadSnapEvent,
|
||||
ConquestEvent,
|
||||
EmbargoEvent,
|
||||
GamePaused,
|
||||
@@ -67,7 +69,9 @@ export type GameUpdate =
|
||||
| UnitIncomingUpdate
|
||||
| AllianceExtensionUpdate
|
||||
| BonusEventUpdate
|
||||
| RailroadUpdate
|
||||
| RailroadConstructionUpdate
|
||||
| RailroadDestructionUpdate
|
||||
| RailroadSnapUpdate
|
||||
| ConquestUpdate
|
||||
| EmbargoUpdate
|
||||
| GamePausedUpdate;
|
||||
@@ -80,24 +84,24 @@ export interface BonusEventUpdate {
|
||||
troops: number;
|
||||
}
|
||||
|
||||
export enum RailType {
|
||||
VERTICAL,
|
||||
HORIZONTAL,
|
||||
TOP_LEFT,
|
||||
TOP_RIGHT,
|
||||
BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT,
|
||||
export interface RailroadConstructionUpdate {
|
||||
type: GameUpdateType.RailroadConstructionEvent;
|
||||
id: number;
|
||||
tiles: TileRef[];
|
||||
}
|
||||
|
||||
export interface RailTile {
|
||||
tile: TileRef;
|
||||
railType: RailType;
|
||||
export interface RailroadDestructionUpdate {
|
||||
type: GameUpdateType.RailroadDestructionEvent;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface RailroadUpdate {
|
||||
type: GameUpdateType.RailroadEvent;
|
||||
isActive: boolean;
|
||||
railTiles: RailTile[];
|
||||
export interface RailroadSnapUpdate {
|
||||
type: GameUpdateType.RailroadSnapEvent;
|
||||
originalId: number;
|
||||
newId1: number;
|
||||
newId2: number;
|
||||
tiles1: TileRef[];
|
||||
tiles2: TileRef[];
|
||||
}
|
||||
|
||||
export interface ConquestUpdate {
|
||||
|
||||
@@ -960,20 +960,25 @@ export class PlayerImpl implements Player {
|
||||
const validTiles = tile !== null ? this.validStructureSpawnTiles(tile) : [];
|
||||
return Object.values(UnitType).map((u) => {
|
||||
let canUpgrade: number | false = false;
|
||||
let canBuild: TileRef | false = false;
|
||||
if (!this.mg.inSpawnPhase()) {
|
||||
const existingUnit = tile !== null && this.findUnitToUpgrade(u, tile);
|
||||
if (existingUnit !== false) {
|
||||
canUpgrade = existingUnit.id();
|
||||
}
|
||||
if (tile !== null) {
|
||||
canBuild = this.canBuild(u, tile, validTiles);
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: u,
|
||||
canBuild:
|
||||
this.mg.inSpawnPhase() || tile === null
|
||||
? false
|
||||
: this.canBuild(u, tile, validTiles),
|
||||
canUpgrade: canUpgrade,
|
||||
canBuild,
|
||||
canUpgrade,
|
||||
cost: this.mg.config().unitInfo(u).cost(this.mg, this),
|
||||
overlappingRailroads:
|
||||
canBuild !== false
|
||||
? this.mg.railNetwork().overlappingRailroads(canBuild)
|
||||
: [],
|
||||
} as BuildableUnit;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Unit } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { StationManager } from "./RailNetworkImpl";
|
||||
import { TrainStation } from "./TrainStation";
|
||||
|
||||
@@ -7,4 +8,5 @@ export interface RailNetwork {
|
||||
removeStation(unit: Unit): void;
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
||||
stationManager(): StationManager;
|
||||
overlappingRailroads(tile: TileRef): number[];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RailroadExecution } from "../execution/RailroadExecution";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { Game, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { RailNetwork } from "./RailNetwork";
|
||||
import { Railroad } from "./Railroad";
|
||||
import { RailSpatialGrid } from "./RailroadSpatialGrid";
|
||||
@@ -85,6 +85,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
private stationRadius: number = 3;
|
||||
private gridCellSize: number = 4;
|
||||
private railGrid: RailSpatialGrid;
|
||||
private nextId: number = 0;
|
||||
|
||||
constructor(
|
||||
private game: Game,
|
||||
@@ -141,6 +142,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
for (const rail of rails) {
|
||||
const from = rail.from;
|
||||
const to = rail.to;
|
||||
const originalId = rail.id;
|
||||
const closestRailIndex = rail.getClosestTileIndex(
|
||||
this.game,
|
||||
station.tile(),
|
||||
@@ -158,11 +160,13 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
from,
|
||||
station,
|
||||
rail.tiles.slice(0, closestRailIndex),
|
||||
this.nextId++,
|
||||
);
|
||||
const newRailTo = new Railroad(
|
||||
station,
|
||||
to,
|
||||
rail.tiles.slice(closestRailIndex),
|
||||
this.nextId++,
|
||||
);
|
||||
|
||||
// New station is connected to both new rails
|
||||
@@ -179,6 +183,14 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
cluster.addStation(station);
|
||||
editedClusters.add(cluster);
|
||||
}
|
||||
this.game.addUpdate({
|
||||
type: GameUpdateType.RailroadSnapEvent,
|
||||
originalId,
|
||||
newId1: newRailFrom.id,
|
||||
newId2: newRailTo.id,
|
||||
tiles1: newRailFrom.tiles,
|
||||
tiles2: newRailTo.tiles,
|
||||
});
|
||||
}
|
||||
// If multiple clusters own the new station, merge them into a single cluster
|
||||
if (editedClusters.size > 1) {
|
||||
@@ -187,6 +199,12 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
return editedClusters.size !== 0;
|
||||
}
|
||||
|
||||
overlappingRailroads(tile: TileRef): number[] {
|
||||
return [...this.railGrid.query(tile, this.stationRadius)].map(
|
||||
(railroad: Railroad) => railroad.id,
|
||||
);
|
||||
}
|
||||
|
||||
private connectToNearbyStations(station: TrainStation) {
|
||||
const neighbors = this.game.nearbyUnits(
|
||||
station.tile(),
|
||||
@@ -256,11 +274,15 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
private connect(from: TrainStation, to: TrainStation) {
|
||||
const path = this.pathService.findTilePath(from.tile(), to.tile());
|
||||
if (path.length > 0 && path.length < this.game.config().railroadMaxSize()) {
|
||||
const railRoad = new Railroad(from, to, path);
|
||||
this.game.addExecution(new RailroadExecution(railRoad));
|
||||
from.addRailroad(railRoad);
|
||||
to.addRailroad(railRoad);
|
||||
this.railGrid.register(railRoad);
|
||||
const railroad = new Railroad(from, to, path, this.nextId++);
|
||||
this.game.addUpdate({
|
||||
type: GameUpdateType.RailroadConstructionEvent,
|
||||
id: railroad.id,
|
||||
tiles: railroad.tiles,
|
||||
});
|
||||
from.addRailroad(railroad);
|
||||
to.addRailroad(railroad);
|
||||
this.railGrid.register(railroad);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Game } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { GameUpdateType, RailTile, RailType } from "./GameUpdates";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { TrainStation } from "./TrainStation";
|
||||
|
||||
export class Railroad {
|
||||
@@ -8,17 +8,13 @@ export class Railroad {
|
||||
public from: TrainStation,
|
||||
public to: TrainStation,
|
||||
public tiles: TileRef[],
|
||||
public id: number,
|
||||
) {}
|
||||
|
||||
delete(game: Game) {
|
||||
const railTiles: RailTile[] = this.tiles.map((tile) => ({
|
||||
tile,
|
||||
railType: RailType.VERTICAL,
|
||||
}));
|
||||
game.addUpdate({
|
||||
type: GameUpdateType.RailroadEvent,
|
||||
isActive: false,
|
||||
railTiles,
|
||||
type: GameUpdateType.RailroadDestructionEvent,
|
||||
id: this.id,
|
||||
});
|
||||
this.from.removeRailroad(this);
|
||||
this.to.removeRailroad(this);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TrainExecution } from "../execution/TrainExecution";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { Game, Player, Unit, UnitType } from "./Game";
|
||||
import { TileRef } from "./GameMap";
|
||||
import { GameUpdateType, RailTile, RailType } from "./GameUpdates";
|
||||
import { GameUpdateType } from "./GameUpdates";
|
||||
import { Railroad } from "./Railroad";
|
||||
|
||||
/**
|
||||
@@ -92,14 +92,9 @@ export class TrainStation {
|
||||
(r) => r.from === station || r.to === station,
|
||||
);
|
||||
if (toRemove) {
|
||||
const railTiles: RailTile[] = toRemove.tiles.map((tile) => ({
|
||||
tile,
|
||||
railType: RailType.VERTICAL,
|
||||
}));
|
||||
this.mg.addUpdate({
|
||||
type: GameUpdateType.RailroadEvent,
|
||||
isActive: false,
|
||||
railTiles,
|
||||
type: GameUpdateType.RailroadDestructionEvent,
|
||||
id: toRemove.id,
|
||||
});
|
||||
this.removeRailroad(toRemove);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,12 @@ describe("InputHandler AutoUpgrade", () => {
|
||||
eventBus = new EventBus();
|
||||
|
||||
inputHandler = new InputHandler(
|
||||
{ attackRatio: 20, ghostStructure: null, rocketDirectionUp: true },
|
||||
{
|
||||
attackRatio: 20,
|
||||
ghostStructure: null,
|
||||
rocketDirectionUp: true,
|
||||
overlappingRailroads: [],
|
||||
},
|
||||
mockCanvas,
|
||||
eventBus,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GameUpdateType } from "src/core/game/GameUpdates";
|
||||
import { vi, type Mocked } from "vitest";
|
||||
import { TrainExecution } from "../../../src/core/execution/TrainExecution";
|
||||
import { Game, Player, Unit, UnitType } from "../../../src/core/game/Game";
|
||||
@@ -112,7 +113,7 @@ describe("TrainStation", () => {
|
||||
|
||||
expect(game.addUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isActive: false,
|
||||
type: GameUpdateType.RailroadDestructionEvent,
|
||||
}),
|
||||
);
|
||||
expect(stationA.getRailroads().size).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user