diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts
index b1c90180b..5cc88c3eb 100644
--- a/src/client/ClientGameRunner.ts
+++ b/src/client/ClientGameRunner.ts
@@ -13,7 +13,7 @@ import {
import { createPartialGameRecord, replacer } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader";
-import { PlayerActions, UnitType } from "../core/game/Game";
+import { BuildableUnit, Structures, UnitType } from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { GameMapLoader } from "../core/game/GameMapLoader";
import {
@@ -548,7 +548,7 @@ export class ClientGameRunner {
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
- this.myPlayer.actions(tile).then((actions) => {
+ this.myPlayer.actions(tile, [UnitType.TransportShip]).then((actions) => {
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
@@ -556,7 +556,7 @@ export class ClientGameRunner {
this.myPlayer!.troops() * this.renderer.uiState.attackRatio,
),
);
- } else if (this.canAutoBoat(actions, tile)) {
+ } else if (this.canAutoBoat(actions.buildableUnits, tile)) {
this.sendBoatAttackIntent(tile);
}
});
@@ -591,7 +591,7 @@ export class ClientGameRunner {
}
private findAndUpgradeNearestBuilding(clickedTile: TileRef) {
- this.myPlayer!.actions(clickedTile).then((actions) => {
+ this.myPlayer!.actions(clickedTile, Structures.types).then((actions) => {
const upgradeUnits: {
unitId: number;
unitType: UnitType;
@@ -644,15 +644,17 @@ export class ClientGameRunner {
this.myPlayer = myPlayer;
}
- this.myPlayer.actions(tile).then((actions) => {
- if (this.canBoatAttack(actions) === false) {
- console.warn(
- "Boat attack triggered but can't send Transport Ship to tile",
- );
- return;
- }
- this.sendBoatAttackIntent(tile);
- });
+ this.myPlayer
+ .buildables(tile, [UnitType.TransportShip])
+ .then((buildables) => {
+ if (this.canBoatAttack(buildables) !== false) {
+ this.sendBoatAttackIntent(tile);
+ } else {
+ console.warn(
+ "Boat attack triggered but can't send Transport Ship to tile",
+ );
+ }
+ });
}
private doGroundAttackUnderCursor(): void {
@@ -667,7 +669,7 @@ export class ClientGameRunner {
this.myPlayer = myPlayer;
}
- this.myPlayer.actions(tile).then((actions) => {
+ this.myPlayer.actions(tile, null).then((actions) => {
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
@@ -696,10 +698,8 @@ export class ClientGameRunner {
return this.gameView.ref(cell.x, cell.y);
}
- private canBoatAttack(actions: PlayerActions): false | TileRef {
- const bu = actions.buildableUnits.find(
- (bu) => bu.type === UnitType.TransportShip,
- );
+ private canBoatAttack(buildables: BuildableUnit[]): false | TileRef {
+ const bu = buildables.find((bu) => bu.type === UnitType.TransportShip);
return bu?.canBuild ?? false;
}
@@ -714,10 +714,10 @@ export class ClientGameRunner {
);
}
- private canAutoBoat(actions: PlayerActions, tile: TileRef): boolean {
+ private canAutoBoat(buildables: BuildableUnit[], tile: TileRef): boolean {
if (!this.gameView.isLand(tile)) return false;
- const canBuild = this.canBoatAttack(actions);
+ const canBuild = this.canBoatAttack(buildables);
if (canBuild === false) return false;
// TODO: Global enable flag
diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts
index 5c16bb931..2645ead48 100644
--- a/src/client/InputHandler.ts
+++ b/src/client/InputHandler.ts
@@ -1,5 +1,5 @@
import { EventBus, GameEvent } from "../core/EventBus";
-import { UnitType } from "../core/game/Game";
+import { PlayerBuildableUnitType, UnitType } from "../core/game/Game";
import { UnitView } from "../core/game/GameView";
import { UserSettings } from "../core/game/UserSettings";
import { UIState } from "./graphics/UIState";
@@ -83,11 +83,13 @@ export class RefreshGraphicsEvent implements GameEvent {}
export class TogglePerformanceOverlayEvent implements GameEvent {}
export class ToggleStructureEvent implements GameEvent {
- constructor(public readonly structureTypes: UnitType[] | null) {}
+ constructor(
+ public readonly structureTypes: PlayerBuildableUnitType[] | null,
+ ) {}
}
export class GhostStructureChangedEvent implements GameEvent {
- constructor(public readonly ghostStructure: UnitType | null) {}
+ constructor(public readonly ghostStructure: PlayerBuildableUnitType | null) {}
}
export class SwapRocketDirectionEvent implements GameEvent {
@@ -609,7 +611,7 @@ export class InputHandler {
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
}
- private setGhostStructure(ghostStructure: UnitType | null) {
+ private setGhostStructure(ghostStructure: PlayerBuildableUnitType | null) {
this.uiState.ghostStructure = ghostStructure;
this.eventBus.emit(new GhostStructureChangedEvent(ghostStructure));
}
diff --git a/src/client/graphics/PlayerIcons.ts b/src/client/graphics/PlayerIcons.ts
index d5e86315a..672d82f2e 100644
--- a/src/client/graphics/PlayerIcons.ts
+++ b/src/client/graphics/PlayerIcons.ts
@@ -1,4 +1,4 @@
-import { AllPlayers, nukeTypes } from "../../core/game/Game";
+import { AllPlayers, Nukes } from "../../core/game/Game";
import { GameView, PlayerView } from "../../core/game/GameView";
import allianceIcon from "/images/AllianceIcon.svg?url";
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
@@ -134,7 +134,7 @@ export function getPlayerIcons(
}
// Nuke icon (different color depending on whether the local player is the target)
- const nukesSentByOtherPlayer = game.units(...nukeTypes).filter((unit) => {
+ const nukesSentByOtherPlayer = game.units(...Nukes.types).filter((unit) => {
const isSendingNuke = player.id() === unit.owner().id();
const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
return isSendingNuke && notMyPlayer && unit.isActive();
diff --git a/src/client/graphics/UIState.ts b/src/client/graphics/UIState.ts
index 277c91d42..c43a773f1 100644
--- a/src/client/graphics/UIState.ts
+++ b/src/client/graphics/UIState.ts
@@ -1,9 +1,9 @@
-import { UnitType } from "../../core/game/Game";
+import { PlayerBuildableUnitType } from "../../core/game/Game";
import { TileRef } from "../../core/game/GameMap";
export interface UIState {
attackRatio: number;
- ghostStructure: UnitType | null;
+ ghostStructure: PlayerBuildableUnitType | null;
overlappingRailroads: number[];
ghostRailPaths: TileRef[][];
rocketDirectionUp: boolean;
diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts
index 62e9c46fc..2d67f2b3c 100644
--- a/src/client/graphics/layers/BuildMenu.ts
+++ b/src/client/graphics/layers/BuildMenu.ts
@@ -1,11 +1,12 @@
-import { LitElement, css, html } from "lit";
+import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
BuildableUnit,
+ BuildMenus,
Gold,
- PlayerActions,
+ PlayerBuildableUnitType,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -37,7 +38,7 @@ import samlauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
export interface BuildItemDisplay {
- unitType: UnitType;
+ unitType: PlayerBuildableUnitType;
icon: string;
description?: string;
key?: string;
@@ -127,7 +128,7 @@ export class BuildMenu extends LitElement implements Layer {
public eventBus: EventBus;
public uiState: UIState;
private clickedTile: TileRef;
- public playerActions: PlayerActions | null = null;
+ public playerBuildables: BuildableUnit[] | null = null;
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
public transformHandler: TransformHandler;
@@ -358,20 +359,15 @@ export class BuildMenu extends LitElement implements Layer {
private _hidden = true;
public canBuildOrUpgrade(item: BuildItemDisplay): boolean {
- if (this.game?.myPlayer() === null || this.playerActions === null) {
+ if (this.game?.myPlayer() === null || this.playerBuildables === null) {
return false;
}
- const unit = this.playerActions.buildableUnits.filter(
- (u) => u.type === item.unitType,
- );
- if (unit.length === 0) {
- return false;
- }
- return unit[0].canBuild !== false || unit[0].canUpgrade !== false;
+ const unit = this.playerBuildables.find((u) => u.type === item.unitType);
+ return unit ? unit.canBuild !== false || unit.canUpgrade !== false : false;
}
public cost(item: BuildItemDisplay): Gold {
- for (const bu of this.playerActions?.buildableUnits ?? []) {
+ for (const bu of this.playerBuildables ?? []) {
if (bu.type === item.unitType) {
return bu.cost;
}
@@ -419,7 +415,7 @@ export class BuildMenu extends LitElement implements Layer {
(row) => html`
${row.map((item) => {
- const buildableUnit = this.playerActions?.buildableUnits.find(
+ const buildableUnit = this.playerBuildables?.find(
(bu) => bu.type === item.unitType,
);
if (buildableUnit === undefined) {
@@ -492,9 +488,9 @@ export class BuildMenu extends LitElement implements Layer {
private refresh() {
this.game
.myPlayer()
- ?.actions(this.clickedTile)
- .then((actions) => {
- this.playerActions = actions;
+ ?.buildables(this.clickedTile, BuildMenus.types)
+ .then((buildables) => {
+ this.playerBuildables = buildables;
this.requestUpdate();
});
diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts
index 989b5aa79..c2029ecea 100644
--- a/src/client/graphics/layers/MainRadialMenu.ts
+++ b/src/client/graphics/layers/MainRadialMenu.ts
@@ -112,7 +112,7 @@ export class MainRadialMenu extends LitElement implements Layer {
screenX: number | null = null,
screenY: number | null = null,
) {
- this.buildMenu.playerActions = actions;
+ this.buildMenu.playerBuildables = actions.buildableUnits;
const tileOwner = this.game.owner(tile);
const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null;
diff --git a/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts b/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
index 341254ab8..02932ccfb 100644
--- a/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
+++ b/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
@@ -26,7 +26,7 @@ export class NukeTrajectoryPreviewLayer implements Layer {
private lastTrajectoryUpdate: number = 0;
private lastTargetTile: TileRef | null = null;
private currentGhostStructure: UnitType | null = null;
- // Cache spawn tile to avoid expensive player.actions() calls
+ // Cache spawn tile to avoid expensive player.buildables() calls
private cachedSpawnTile: TileRef | null = null;
constructor(
@@ -75,7 +75,7 @@ export class NukeTrajectoryPreviewLayer implements Layer {
}
/**
- * Update trajectory preview - called from tick() to cache spawn tile via expensive player.actions() call
+ * Update trajectory preview - called from tick() to cache spawn tile via expensive player.buildables() call
* This only runs when target tile changes, minimizing worker thread communication
*/
private updateTrajectoryPreview() {
@@ -138,14 +138,14 @@ export class NukeTrajectoryPreviewLayer implements Layer {
// Get buildable units to find spawn tile (expensive call - only on tick when tile changes)
player
- .actions(targetTile, [ghostStructure])
- .then((actions) => {
+ .buildables(targetTile, [ghostStructure])
+ .then((buildables) => {
// Ignore stale results if target changed
if (this.lastTargetTile !== targetTile) {
return;
}
- const buildableUnit = actions.buildableUnits.find(
+ const buildableUnit = buildables.find(
(bu) => bu.type === ghostStructure,
);
@@ -171,7 +171,7 @@ export class NukeTrajectoryPreviewLayer implements Layer {
/**
* Update trajectory path - called from renderLayer() each frame for smooth visual feedback
- * Uses cached spawn tile to avoid expensive player.actions() calls
+ * Uses cached spawn tile to avoid expensive player.buildables() calls
*/
private updateTrajectoryPath() {
const ghostStructure = this.currentGhostStructure;
diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts
index 1b759577b..f1df0750d 100644
--- a/src/client/graphics/layers/PlayerPanel.ts
+++ b/src/client/graphics/layers/PlayerPanel.ts
@@ -121,7 +121,7 @@ export class PlayerPanel extends LitElement implements Layer {
// Refresh actions & alliance expiry
const myPlayer = this.g.myPlayer();
if (myPlayer !== null && myPlayer.isAlive()) {
- this.actions = await myPlayer.actions(this.tile);
+ this.actions = await myPlayer.actions(this.tile, null);
if (this.actions?.interaction?.allianceInfo?.expiresAt !== undefined) {
const expiresAt = this.actions.interaction.allianceInfo.expiresAt;
const remainingTicks = expiresAt - this.g.ticks();
diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts
index f331c9e31..72c00852d 100644
--- a/src/client/graphics/layers/RadialMenuElements.ts
+++ b/src/client/graphics/layers/RadialMenuElements.ts
@@ -1,8 +1,10 @@
import { Config } from "../../../core/configuration/Config";
import {
AllPlayers,
+ BuildableAttacks,
PlayerActions,
- StructureTypes,
+ PlayerBuildableUnitType,
+ Structures,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -376,40 +378,34 @@ export const infoMenuElement: MenuElement = {
},
};
-function getAllEnabledUnits(myPlayer: boolean, config: Config): Set
{
- const units: Set = new Set();
+function getAllEnabledUnits(
+ myPlayer: boolean,
+ config: Config,
+): Set {
+ const units: Set =
+ new Set();
- const addIfEnabled = (unitType: UnitType) => {
+ const addIfEnabled = (unitType: PlayerBuildableUnitType) => {
if (!config.isUnitDisabled(unitType)) {
units.add(unitType);
}
};
if (myPlayer) {
- StructureTypes.forEach(addIfEnabled);
+ Structures.types.forEach(addIfEnabled);
} else {
- addIfEnabled(UnitType.Warship);
- addIfEnabled(UnitType.HydrogenBomb);
- addIfEnabled(UnitType.MIRV);
- addIfEnabled(UnitType.AtomBomb);
+ BuildableAttacks.types.forEach(addIfEnabled);
}
return units;
}
-const ATTACK_UNIT_TYPES: UnitType[] = [
- UnitType.AtomBomb,
- UnitType.MIRV,
- UnitType.HydrogenBomb,
- UnitType.Warship,
-];
-
function createMenuElements(
params: MenuElementParams,
filterType: "attack" | "build",
elementIdPrefix: string,
): MenuElement[] {
- const unitTypes: Set = getAllEnabledUnits(
+ const unitTypes: Set = getAllEnabledUnits(
params.selected === params.myPlayer,
params.game.config(),
);
@@ -419,8 +415,8 @@ function createMenuElements(
(item) =>
unitTypes.has(item.unitType) &&
(filterType === "attack"
- ? ATTACK_UNIT_TYPES.includes(item.unitType)
- : !ATTACK_UNIT_TYPES.includes(item.unitType)),
+ ? BuildableAttacks.has(item.unitType)
+ : !BuildableAttacks.has(item.unitType)),
)
.map((item: BuildItemDisplay) => {
const canBuildOrUpgrade = params.buildMenu.canBuildOrUpgrade(item);
diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts
index 9fe6a940b..663547170 100644
--- a/src/client/graphics/layers/StructureDrawingUtils.ts
+++ b/src/client/graphics/layers/StructureDrawingUtils.ts
@@ -1,6 +1,10 @@
import * as PIXI from "pixi.js";
import { Theme } from "../../../core/configuration/Config";
-import { Cell, UnitType } from "../../../core/game/Game";
+import {
+ Cell,
+ PlayerBuildableUnitType,
+ UnitType,
+} from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "/images/AnchorIcon.png?url";
@@ -108,7 +112,7 @@ export class SpriteFactory {
player: PlayerView,
ghostStage: PIXI.Container,
pos: { x: number; y: number },
- structureType: UnitType,
+ structureType: PlayerBuildableUnitType,
): {
container: PIXI.Container;
priceText: PIXI.BitmapText;
diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts
index 6c5432f9b..5b17d24aa 100644
--- a/src/client/graphics/layers/StructureIconsLayer.ts
+++ b/src/client/graphics/layers/StructureIconsLayer.ts
@@ -8,7 +8,9 @@ import { wouldNukeBreakAlliance } from "../../../core/execution/Util";
import {
BuildableUnit,
Cell,
+ PlayerBuildableUnitType,
PlayerID,
+ Structures,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -84,14 +86,10 @@ export class StructureIconsLayer implements Layer {
private readonly mousePos = { x: 0, y: 0 };
private renderSprites = true;
private factory: SpriteFactory;
- private readonly structures: Map = new Map([
- [UnitType.City, { visible: true }],
- [UnitType.Factory, { visible: true }],
- [UnitType.DefensePost, { visible: true }],
- [UnitType.Port, { visible: true }],
- [UnitType.MissileSilo, { visible: true }],
- [UnitType.SAMLauncher, { visible: true }],
- ]);
+ private readonly structures: Map<
+ PlayerBuildableUnitType,
+ { visible: boolean }
+ > = new Map(Structures.types.map((type) => [type, { visible: true }]));
private lastGhostQueryAt: number;
private visibilityStateDirty = true;
private hasHiddenStructure = false;
@@ -299,8 +297,8 @@ export class StructureIconsLayer implements Layer {
this.game
?.myPlayer()
- ?.actions(tileRef, [this.ghostUnit?.buildableUnit.type])
- .then((actions) => {
+ ?.buildables(tileRef, [this.ghostUnit?.buildableUnit.type])
+ .then((buildables) => {
if (this.potentialUpgrade) {
this.potentialUpgrade.iconContainer.filters = [];
this.potentialUpgrade.dotContainer.filters = [];
@@ -311,7 +309,7 @@ export class StructureIconsLayer implements Layer {
if (!this.ghostUnit) return;
- const unit = actions.buildableUnits.find(
+ const unit = buildables.find(
(u) => u.type === this.ghostUnit!.buildableUnit.type,
);
const showPrice = this.game.config().userSettings().cursorCostLabel();
@@ -453,7 +451,7 @@ export class StructureIconsLayer implements Layer {
this.ghostUnit.range?.position.set(localX, localY);
}
- private createGhostStructure(type: UnitType | null) {
+ private createGhostStructure(type: PlayerBuildableUnitType | null) {
const player = this.game.myPlayer();
if (!player) return;
if (type === null) {
@@ -559,7 +557,9 @@ export class StructureIconsLayer implements Layer {
}
}
- private toggleStructures(toggleStructureType: UnitType[] | null): void {
+ private toggleStructures(
+ toggleStructureType: PlayerBuildableUnitType[] | null,
+ ): void {
for (const [structureType, infos] of this.structures) {
infos.visible =
toggleStructureType?.indexOf(structureType) !== -1 ||
@@ -602,7 +602,9 @@ export class StructureIconsLayer implements Layer {
this.checkForOwnershipChange(render, unitView);
this.checkForLevelChange(render, unitView);
}
- } else if (this.structures.has(unitView.type())) {
+ } else if (
+ this.structures.has(unitView.type() as PlayerBuildableUnitType)
+ ) {
this.addNewStructure(unitView);
}
}
@@ -621,7 +623,7 @@ export class StructureIconsLayer implements Layer {
private modifyVisibility(render: StructureRenderInfo) {
this.refreshVisibilityStateCache();
- const structureType = render.unit.type();
+ const structureType = render.unit.type() as PlayerBuildableUnitType;
const structureInfos = this.structures.get(structureType);
if (structureInfos) {
diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts
index 82d530dcb..cedb5292a 100644
--- a/src/client/graphics/layers/UnitDisplay.ts
+++ b/src/client/graphics/layers/UnitDisplay.ts
@@ -1,7 +1,13 @@
-import { LitElement, html } from "lit";
+import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
-import { Gold, PlayerActions, UnitType } from "../../../core/game/Game";
+import {
+ BuildableUnit,
+ BuildMenus,
+ Gold,
+ PlayerBuildableUnitType,
+ UnitType,
+} from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import {
GhostStructureChangedEvent,
@@ -22,25 +28,12 @@ import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import defensePostIcon from "/images/ShieldIconWhite.svg?url";
-const BUILDABLE_UNITS: UnitType[] = [
- UnitType.City,
- UnitType.Factory,
- UnitType.Port,
- UnitType.DefensePost,
- UnitType.MissileSilo,
- UnitType.SAMLauncher,
- UnitType.Warship,
- UnitType.AtomBomb,
- UnitType.HydrogenBomb,
- UnitType.MIRV,
-];
-
@customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public uiState: UIState;
- private playerActions: PlayerActions | null = null;
+ private playerBuildables: BuildableUnit[] | null = null;
private keybinds: Record = {};
private _cities = 0;
private _warships = 0;
@@ -50,7 +43,7 @@ export class UnitDisplay extends LitElement implements Layer {
private _defensePost = 0;
private _samLauncher = 0;
private allDisabled = false;
- private _hoveredUnit: UnitType | null = null;
+ private _hoveredUnit: PlayerBuildableUnitType | null = null;
createRenderRoot() {
return this;
@@ -68,12 +61,12 @@ export class UnitDisplay extends LitElement implements Layer {
}
}
- this.allDisabled = BUILDABLE_UNITS.every((u) => config.isUnitDisabled(u));
+ this.allDisabled = BuildMenus.types.every((u) => config.isUnitDisabled(u));
this.requestUpdate();
}
private cost(item: UnitType): Gold {
- for (const bu of this.playerActions?.buildableUnits ?? []) {
+ for (const bu of this.playerBuildables ?? []) {
if (bu.type === item) {
return bu.cost;
}
@@ -104,10 +97,10 @@ export class UnitDisplay extends LitElement implements Layer {
tick() {
const player = this.game?.myPlayer();
- player?.actions(undefined, BUILDABLE_UNITS).then((actions) => {
- this.playerActions = actions;
- });
if (!player) return;
+ player.buildables(undefined, BuildMenus.types).then((buildables) => {
+ this.playerBuildables = buildables;
+ });
this._cities = player.totalUnitLevels(UnitType.City);
this._missileSilo = player.totalUnitLevels(UnitType.MissileSilo);
this._port = player.totalUnitLevels(UnitType.Port);
@@ -221,7 +214,7 @@ export class UnitDisplay extends LitElement implements Layer {
private renderUnitItem(
icon: string,
number: number | null,
- unitType: UnitType,
+ unitType: PlayerBuildableUnitType,
structureKey: string,
hotkey: string,
) {
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 3f9fd20ce..9019a78e5 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -6,6 +6,7 @@ import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
Attack,
+ BuildableUnit,
Cell,
Game,
GameUpdates,
@@ -13,6 +14,7 @@ import {
Player,
PlayerActions,
PlayerBorderTiles,
+ PlayerBuildableUnitType,
PlayerID,
PlayerInfo,
PlayerProfile,
@@ -189,18 +191,30 @@ export class GameRunner {
return Math.max(0, this.turns.length - this.currTurn);
}
+ public playerBuildables(
+ playerID: PlayerID,
+ x?: number,
+ y?: number,
+ units?: readonly PlayerBuildableUnitType[],
+ ): BuildableUnit[] {
+ const player = this.game.player(playerID);
+ const tile =
+ x !== undefined && y !== undefined ? this.game.ref(x, y) : null;
+ return player.buildableUnits(tile, units);
+ }
+
public playerActions(
playerID: PlayerID,
x?: number,
y?: number,
- units?: UnitType[],
+ units?: readonly PlayerBuildableUnitType[] | null,
): PlayerActions {
const player = this.game.player(playerID);
const tile =
x !== undefined && y !== undefined ? this.game.ref(x, y) : null;
const actions = {
- canAttack: tile !== null && units === undefined && player.canAttack(tile),
- buildableUnits: player.buildableUnits(tile, units),
+ canAttack: tile !== null && player.canAttack(tile),
+ buildableUnits: units === null ? [] : player.buildableUnits(tile, units),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
canEmbargoAll: player.canEmbargoAll(),
} as PlayerActions;
diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts
index 491bf1b21..11b74cdeb 100644
--- a/src/core/execution/BotExecution.ts
+++ b/src/core/execution/BotExecution.ts
@@ -1,4 +1,4 @@
-import { Execution, Game, isStructureType, Player } from "../game/Game";
+import { Execution, Game, Player, Structures } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
@@ -85,7 +85,7 @@ export class BotExecution implements Execution {
private deleteAllStructures() {
for (const unit of this.bot.units()) {
- if (isStructureType(unit.type()) && this.bot.canDeleteUnit()) {
+ if (Structures.has(unit.type()) && this.bot.canDeleteUnit()) {
this.mg.addExecution(new DeleteUnitExecution(this.bot, unit.id()));
}
}
diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts
index fc1743f26..2bf2055b7 100644
--- a/src/core/execution/NukeExecution.ts
+++ b/src/core/execution/NukeExecution.ts
@@ -1,9 +1,9 @@
import {
Execution,
Game,
- isStructureType,
MessageType,
Player,
+ Structures,
TerraNullius,
TrajectoryTile,
Unit,
@@ -346,7 +346,7 @@ export class NukeExecution implements Execution {
private redrawBuildings(range: number) {
const rangeSquared = range * range;
for (const unit of this.mg.units()) {
- if (isStructureType(unit.type())) {
+ if (Structures.has(unit.type())) {
if (
this.mg.euclideanDistSquared(this.dst, unit.tile()) < rangeSquared
) {
diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts
index 55d2eb062..28c734d12 100644
--- a/src/core/execution/PlayerExecution.ts
+++ b/src/core/execution/PlayerExecution.ts
@@ -3,8 +3,8 @@ import {
Cell,
Execution,
Game,
- isStructureType,
Player,
+ Structures,
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
@@ -42,7 +42,7 @@ export class PlayerExecution implements Execution {
tick(ticks: number) {
this.player.decayRelations();
for (const u of this.player.units()) {
- if (!isStructureType(u.type())) {
+ if (!Structures.has(u.type())) {
continue;
}
diff --git a/src/core/execution/Util.ts b/src/core/execution/Util.ts
index 790be42c9..aa394b343 100644
--- a/src/core/execution/Util.ts
+++ b/src/core/execution/Util.ts
@@ -1,5 +1,5 @@
import { NukeMagnitude } from "../configuration/Config";
-import { Game, Player, StructureTypes } from "../game/Game";
+import { Game, Player, Structures } from "../game/Game";
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
import { GameView } from "../game/GameView";
@@ -60,7 +60,7 @@ export function wouldNukeBreakAlliance(
const wouldDestroyAlliedStructure = game.anyUnitNearby(
targetTile,
magnitude.outer,
- StructureTypes,
+ Structures.types,
(unit) =>
unit.owner().isPlayer() && allySmallIds.has(unit.owner().smallID()),
);
@@ -119,7 +119,7 @@ export function listNukeBreakAlliance(
// Also check if any allied structures would be destroyed
game
- .nearbyUnits(targetTile, magnitude.outer, StructureTypes)
+ .nearbyUnits(targetTile, magnitude.outer, Structures.types)
.forEach(({ unit }) =>
playersToBreakAllianceWith.add(unit.owner().smallID()),
);
diff --git a/src/core/execution/nation/NationStructureBehavior.ts b/src/core/execution/nation/NationStructureBehavior.ts
index 29b606816..1e9478803 100644
--- a/src/core/execution/nation/NationStructureBehavior.ts
+++ b/src/core/execution/nation/NationStructureBehavior.ts
@@ -5,7 +5,7 @@ import {
Player,
PlayerType,
Relation,
- StructureTypes,
+ Structures,
Unit,
UnitType,
} from "../../game/Game";
@@ -356,7 +356,7 @@ export class NationStructureBehavior {
private getTotalStructureDensity(): number {
const tilesOwned = this.player.numTilesOwned();
return tilesOwned > 0
- ? this.player.units(...StructureTypes).length / tilesOwned
+ ? this.player.units(...Structures.types).length / tilesOwned
: 0; //ignoring levels for structures
}
diff --git a/src/core/execution/utils/AiAttackBehavior.ts b/src/core/execution/utils/AiAttackBehavior.ts
index 8d393b148..acdbfc662 100644
--- a/src/core/execution/utils/AiAttackBehavior.ts
+++ b/src/core/execution/utils/AiAttackBehavior.ts
@@ -3,11 +3,11 @@ import {
Game,
GameMode,
HumansVsNations,
- isStructureType,
Player,
PlayerID,
PlayerType,
Relation,
+ Structures,
TerraNullius,
UnitType,
} from "../../game/Game";
@@ -361,7 +361,7 @@ export class AiAttackBehavior {
n.isPlayer() &&
n.type() === PlayerType.Bot &&
!this.player.isFriendly(n) &&
- n.units().some((u) => isStructureType(u.type())),
+ n.units().some((u) => Structures.has(u.type())),
);
}
@@ -419,7 +419,7 @@ export class AiAttackBehavior {
const density = (p: Player) => p.troops() / p.numTilesOwned();
const ownsStructures = (p: Player) =>
- p.units().some((u) => isStructureType(u.type()));
+ p.units().some((u) => Structures.has(u.type()));
const sortedBots = bots.slice().sort((a, b) => {
const aHasStructures = ownsStructures(a);
const bHasStructures = ownsStructures(b);
@@ -743,7 +743,7 @@ export class AiAttackBehavior {
const botWithStructures =
target.isPlayer() &&
target.type() === PlayerType.Bot &&
- target.units().some((u) => isStructureType(u.type()));
+ target.units().some((u) => Structures.has(u.type()));
// Use the expand ratio when attacking a bot that owns structures — we need to
// recapture those structures ASAP, even before reaching the normal reserve.
const useReserve = target.isPlayer() && !botWithStructures;
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index bd04419a9..fbe176991 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -252,6 +252,15 @@ export interface UnitInfo {
upgradable?: boolean;
}
+function unitTypeGroup(types: T) {
+ return {
+ types,
+ has(type: UnitType): type is T[number] {
+ return (types as readonly UnitType[]).includes(type);
+ },
+ };
+}
+
export enum UnitType {
TransportShip = "Transport",
Warship = "Warship",
@@ -277,20 +286,40 @@ export enum TrainType {
Carriage = "Carriage",
}
-const _structureTypes: ReadonlySet = new Set([
+export const Nukes = unitTypeGroup([
+ UnitType.AtomBomb,
+ UnitType.HydrogenBomb,
+ UnitType.MIRVWarhead,
+ UnitType.MIRV,
+] as const);
+
+export const BuildableAttacks = unitTypeGroup([
+ UnitType.AtomBomb,
+ UnitType.HydrogenBomb,
+ UnitType.MIRV,
+ UnitType.Warship,
+] as const);
+
+export const Structures = unitTypeGroup([
UnitType.City,
UnitType.DefensePost,
UnitType.SAMLauncher,
UnitType.MissileSilo,
UnitType.Port,
UnitType.Factory,
-]);
+] as const);
-export const StructureTypes: readonly UnitType[] = [..._structureTypes];
+export const BuildMenus = unitTypeGroup([
+ ...Structures.types,
+ ...BuildableAttacks.types,
+] as const);
-export function isStructureType(type: UnitType): boolean {
- return _structureTypes.has(type);
-}
+export const PlayerBuildable = unitTypeGroup([
+ ...BuildMenus.types,
+ UnitType.TransportShip,
+] as const);
+
+export type PlayerBuildableUnitType = (typeof PlayerBuildable.types)[number];
export interface OwnerComp {
owner: Player;
@@ -361,13 +390,6 @@ export type UnitParams = UnitParamsMap[T];
export type AllUnitParams = UnitParamsMap[keyof UnitParamsMap];
-export const nukeTypes = [
- UnitType.AtomBomb,
- UnitType.HydrogenBomb,
- UnitType.MIRVWarhead,
- UnitType.MIRV,
-] as UnitType[];
-
export enum Relation {
Hostile = 0,
Distrustful = 1,
@@ -646,8 +668,15 @@ export interface Player {
unitCount(type: UnitType): number;
unitsConstructed(type: UnitType): number;
unitsOwned(type: UnitType): number;
- buildableUnits(tile: TileRef | null, units?: UnitType[]): BuildableUnit[];
- canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
+ buildableUnits(
+ tile: TileRef | null,
+ units?: readonly PlayerBuildableUnitType[],
+ ): BuildableUnit[];
+ canBuild(
+ type: UnitType,
+ targetTile: TileRef,
+ validTiles?: TileRef[] | null,
+ ): TileRef | false;
buildUnit(
type: T,
spawnTile: TileRef,
@@ -873,7 +902,7 @@ export interface BuildableUnit {
canBuild: TileRef | false;
// unit id of the existing unit that can be upgraded, or false if it cannot be upgraded.
canUpgrade: number | false;
- type: UnitType;
+ type: PlayerBuildableUnitType;
cost: Gold;
overlappingRailroads: number[];
ghostRailPaths: TileRef[][];
diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts
index f08b8178b..bdd9f2092 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -7,6 +7,7 @@ import { ClientID, GameID, Player, PlayerCosmetics } from "../Schemas";
import { createRandomName } from "../Util";
import { WorkerClient } from "../worker/WorkerClient";
import {
+ BuildableUnit,
Cell,
EmojiMessage,
GameUpdates,
@@ -14,6 +15,7 @@ import {
NameViewData,
PlayerActions,
PlayerBorderTiles,
+ PlayerBuildableUnitType,
PlayerID,
PlayerProfile,
PlayerType,
@@ -415,7 +417,10 @@ export class PlayerView {
return { hasEmbargo, hasFriendly };
}
- async actions(tile?: TileRef, units?: UnitType[]): Promise {
+ async actions(
+ tile?: TileRef,
+ units?: readonly PlayerBuildableUnitType[] | null,
+ ): Promise {
return this.game.worker.playerInteraction(
this.id(),
tile && this.game.x(tile),
@@ -424,6 +429,18 @@ export class PlayerView {
);
}
+ async buildables(
+ tile?: TileRef,
+ units?: readonly PlayerBuildableUnitType[],
+ ): Promise {
+ return this.game.worker.playerBuildables(
+ this.id(),
+ tile && this.game.x(tile),
+ tile && this.game.y(tile),
+ units,
+ );
+ }
+
async borderTiles(): Promise {
return this.game.worker.playerBorderTiles(this.id());
}
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index cd8ebc05a..027560215 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -23,16 +23,17 @@ import {
EmojiMessage,
GameMode,
Gold,
- isStructureType,
MessageType,
MutableAlliance,
Player,
+ PlayerBuildable,
+ PlayerBuildableUnitType,
PlayerID,
PlayerInfo,
PlayerProfile,
PlayerType,
Relation,
- StructureTypes,
+ Structures,
Team,
TerraNullius,
Tick,
@@ -988,6 +989,17 @@ export class PlayerImpl implements Player {
}
public findUnitToUpgrade(type: UnitType, targetTile: TileRef): Unit | false {
+ const unit = this.findExistingUnitToUpgrade(type, targetTile);
+ if (unit === false || !this.canUpgradeUnit(unit)) {
+ return false;
+ }
+ return unit;
+ }
+
+ private findExistingUnitToUpgrade(
+ type: UnitType,
+ targetTile: TileRef,
+ ): Unit | false {
const range = this.mg.config().structureMinDist();
const existing = this.mg
.nearbyUnits(targetTile, range, type, undefined, true)
@@ -995,29 +1007,35 @@ export class PlayerImpl implements Player {
if (existing.length === 0) {
return false;
}
- const unit = existing[0].unit;
- if (!this.canUpgradeUnit(unit)) {
- return false;
- }
- return unit;
+ return existing[0].unit;
}
- public canUpgradeUnit(unit: Unit): boolean {
- if (unit.isMarkedForDeletion()) {
+ private canBuildUnitType(
+ unitType: UnitType,
+ knownCost: Gold | null = null,
+ ): boolean {
+ if (this.mg.config().isUnitDisabled(unitType)) {
return false;
}
+ const cost = knownCost ?? this.mg.unitInfo(unitType).cost(this.mg, this);
+ if (this._gold < cost) {
+ return false;
+ }
+ if (unitType !== UnitType.MIRVWarhead && !this.isAlive()) {
+ return false;
+ }
+ return true;
+ }
+
+ private canUpgradeUnitType(unitType: UnitType): boolean {
+ return Boolean(this.mg.config().unitInfo(unitType).upgradable);
+ }
+
+ private isUnitValidToUpgrade(unit: Unit): boolean {
if (unit.isUnderConstruction()) {
return false;
}
- if (!this.mg.config().unitInfo(unit.type()).upgradable) {
- return false;
- }
- if (this.mg.config().isUnitDisabled(unit.type())) {
- return false;
- }
- if (
- this._gold < this.mg.config().unitInfo(unit.type()).cost(this.mg, this)
- ) {
+ if (unit.isMarkedForDeletion()) {
return false;
}
if (unit.owner() !== this) {
@@ -1026,6 +1044,19 @@ export class PlayerImpl implements Player {
return true;
}
+ public canUpgradeUnit(unit: Unit): boolean {
+ if (!this.canUpgradeUnitType(unit.type())) {
+ return false;
+ }
+ if (!this.canBuildUnitType(unit.type())) {
+ return false;
+ }
+ if (!this.isUnitValidToUpgrade(unit)) {
+ return false;
+ }
+ return true;
+ }
+
upgradeUnit(unit: Unit) {
const cost = this.mg.unitInfo(unit.type()).cost(this.mg, this);
this.removeGold(cost);
@@ -1035,42 +1066,58 @@ export class PlayerImpl implements Player {
public buildableUnits(
tile: TileRef | null,
- units?: UnitType[],
+ units: readonly PlayerBuildableUnitType[] = PlayerBuildable.types,
): BuildableUnit[] {
+ const mg = this.mg;
+ const config = mg.config();
+ const rail = mg.railNetwork();
+ const inSpawnPhase = mg.inSpawnPhase();
+
const validTiles =
- tile !== null &&
- (units === undefined || units.some((u) => isStructureType(u)))
+ tile !== null && units.some((u) => Structures.has(u))
? this.validStructureSpawnTiles(tile)
: [];
- return Object.values(UnitType)
- .filter((u) => units === undefined || units.includes(u))
- .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) {
+
+ const len = units.length;
+ const result = new Array(len);
+
+ for (let i = 0; i < len; i++) {
+ const u = units[i];
+
+ const cost = config.unitInfo(u).cost(mg, this);
+ let canUpgrade: number | false = false;
+ let canBuild: TileRef | false = false;
+
+ if (tile !== null && this.canBuildUnitType(u, cost) && !inSpawnPhase) {
+ if (this.canUpgradeUnitType(u)) {
+ const existingUnit = this.findExistingUnitToUpgrade(u, tile);
+ if (
+ existingUnit !== false &&
+ this.isUnitValidToUpgrade(existingUnit)
+ ) {
canUpgrade = existingUnit.id();
}
- if (tile !== null) {
- canBuild = this.canBuild(u, tile, validTiles);
- }
}
- return {
- type: u,
- canBuild,
- canUpgrade,
- cost: this.mg.config().unitInfo(u).cost(this.mg, this),
- overlappingRailroads:
- canBuild !== false
- ? this.mg.railNetwork().overlappingRailroads(canBuild)
- : [],
- ghostRailPaths:
- canBuild !== false
- ? this.mg.railNetwork().computeGhostRailPaths(u, canBuild)
- : [],
- };
- });
+ canBuild = this.canSpawnUnitType(u, tile, validTiles);
+ }
+
+ const buildNew = canBuild !== false && canUpgrade === false;
+
+ result[i] = {
+ type: u,
+ canBuild,
+ canUpgrade,
+ cost,
+ overlappingRailroads: buildNew
+ ? rail.overlappingRailroads(canBuild as TileRef)
+ : [],
+ ghostRailPaths: buildNew
+ ? rail.computeGhostRailPaths(u, canBuild as TileRef)
+ : [],
+ };
+ }
+
+ return result;
}
canBuild(
@@ -1078,17 +1125,18 @@ export class PlayerImpl implements Player {
targetTile: TileRef,
validTiles: TileRef[] | null = null,
): TileRef | false {
- if (this.mg.config().isUnitDisabled(unitType)) {
+ if (!this.canBuildUnitType(unitType)) {
return false;
}
- const cost = this.mg.unitInfo(unitType).cost(this.mg, this);
- if (
- unitType !== UnitType.MIRVWarhead &&
- (!this.isAlive() || this.gold() < cost)
- ) {
- return false;
- }
+ return this.canSpawnUnitType(unitType, targetTile, validTiles);
+ }
+
+ private canSpawnUnitType(
+ unitType: UnitType,
+ targetTile: TileRef,
+ validTiles: TileRef[] | null,
+ ): TileRef | false {
switch (unitType) {
case UnitType.MIRV:
if (!this.mg.hasOwner(targetTile)) {
@@ -1144,7 +1192,7 @@ export class PlayerImpl implements Player {
const wouldHitTeammate = this.mg.anyUnitNearby(
tile,
magnitude.outer,
- StructureTypes,
+ Structures.types,
(unit) => unit.owner().isPlayer() && this.isOnSameTeam(unit.owner()),
);
if (wouldHitTeammate) {
@@ -1227,7 +1275,7 @@ export class PlayerImpl implements Player {
const nearbyUnits = this.mg.nearbyUnits(
tile,
searchRadius * 2,
- StructureTypes,
+ Structures.types,
undefined,
true,
);
diff --git a/src/core/game/UnitGrid.ts b/src/core/game/UnitGrid.ts
index c763e2545..835c55819 100644
--- a/src/core/game/UnitGrid.ts
+++ b/src/core/game/UnitGrid.ts
@@ -149,9 +149,7 @@ export class UnitGrid {
);
const rangeSquared = searchRange * searchRange;
- // `Array.isArray` does not reliably narrow `readonly T[]` in TS, so use a
- // cheap runtime check that narrows correctly for our string-backed UnitType.
- if (typeof types !== "string") {
+ if (Array.isArray(types)) {
for (let cy = startGridY; cy <= endGridY; cy++) {
for (let cx = startGridX; cx <= endGridX; cx++) {
const cell = this.grid[cy][cx];
@@ -182,7 +180,7 @@ export class UnitGrid {
const type = types;
for (let cy = startGridY; cy <= endGridY; cy++) {
for (let cx = startGridX; cx <= endGridX; cx++) {
- const unitSet = this.grid[cy][cx].get(type);
+ const unitSet = this.grid[cy][cx].get(type as UnitType);
if (unitSet === undefined) continue;
for (const unit of unitSet) {
if (!unit.isActive()) continue;
diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts
index b34e68c82..5f72c7554 100644
--- a/src/core/worker/Worker.worker.ts
+++ b/src/core/worker/Worker.worker.ts
@@ -8,6 +8,7 @@ import {
MainThreadMessage,
PlayerActionsResultMessage,
PlayerBorderTilesResultMessage,
+ PlayerBuildablesResultMessage,
PlayerProfileResultMessage,
TransportShipSpawnResultMessage,
WorkerMessage,
@@ -184,6 +185,28 @@ ctx.addEventListener("message", async (e: MessageEvent) => {
throw error;
}
break;
+ case "player_buildables":
+ if (!gameRunner) {
+ throw new Error("Game runner not initialized");
+ }
+
+ try {
+ const buildables = (await gameRunner).playerBuildables(
+ message.playerID,
+ message.x,
+ message.y,
+ message.units,
+ );
+ sendMessage({
+ type: "player_buildables_result",
+ id: message.id,
+ result: buildables,
+ } as PlayerBuildablesResultMessage);
+ } catch (error) {
+ console.error("Failed to get buildables:", error);
+ throw error;
+ }
+ break;
case "player_profile":
if (!gameRunner) {
throw new Error("Game runner not initialized");
diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts
index 80867706a..7655f9050 100644
--- a/src/core/worker/WorkerClient.ts
+++ b/src/core/worker/WorkerClient.ts
@@ -1,10 +1,11 @@
import {
+ BuildableUnit,
Cell,
PlayerActions,
PlayerBorderTiles,
+ PlayerBuildableUnitType,
PlayerID,
PlayerProfile,
- UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
@@ -166,7 +167,7 @@ export class WorkerClient {
playerID: PlayerID,
x?: number,
y?: number,
- units?: UnitType[],
+ units?: readonly PlayerBuildableUnitType[] | null,
): Promise {
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
@@ -196,6 +197,40 @@ export class WorkerClient {
});
}
+ playerBuildables(
+ playerID: PlayerID,
+ x?: number,
+ y?: number,
+ units?: readonly PlayerBuildableUnitType[],
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.isInitialized) {
+ reject(new Error("Worker not initialized"));
+ return;
+ }
+
+ const messageId = generateID();
+
+ this.messageHandlers.set(messageId, (message) => {
+ if (
+ message.type === "player_buildables_result" &&
+ message.result !== undefined
+ ) {
+ resolve(message.result);
+ }
+ });
+
+ this.worker.postMessage({
+ type: "player_buildables",
+ id: messageId,
+ playerID,
+ x,
+ y,
+ units,
+ });
+ });
+ }
+
attackAveragePosition(
playerID: number,
attackID: string,
diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts
index 07b0cc550..b8b04740d 100644
--- a/src/core/worker/WorkerMessages.ts
+++ b/src/core/worker/WorkerMessages.ts
@@ -1,9 +1,10 @@
import {
+ BuildableUnit,
PlayerActions,
PlayerBorderTiles,
+ PlayerBuildableUnitType,
PlayerID,
PlayerProfile,
- UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { GameUpdateViewData } from "../game/GameUpdates";
@@ -17,6 +18,8 @@ export type WorkerMessageType =
| "game_update_batch"
| "player_actions"
| "player_actions_result"
+ | "player_buildables"
+ | "player_buildables_result"
| "player_profile"
| "player_profile_result"
| "player_border_tiles"
@@ -64,7 +67,7 @@ export interface PlayerActionsMessage extends BaseWorkerMessage {
playerID: PlayerID;
x?: number;
y?: number;
- units?: UnitType[];
+ units?: readonly PlayerBuildableUnitType[] | null;
}
export interface PlayerActionsResultMessage extends BaseWorkerMessage {
@@ -72,6 +75,19 @@ export interface PlayerActionsResultMessage extends BaseWorkerMessage {
result: PlayerActions;
}
+export interface PlayerBuildablesMessage extends BaseWorkerMessage {
+ type: "player_buildables";
+ playerID: PlayerID;
+ x?: number;
+ y?: number;
+ units?: readonly PlayerBuildableUnitType[];
+}
+
+export interface PlayerBuildablesResultMessage extends BaseWorkerMessage {
+ type: "player_buildables_result";
+ result: BuildableUnit[];
+}
+
export interface PlayerProfileMessage extends BaseWorkerMessage {
type: "player_profile";
playerID: number;
@@ -120,6 +136,7 @@ export type MainThreadMessage =
| InitMessage
| TurnMessage
| PlayerActionsMessage
+ | PlayerBuildablesMessage
| PlayerProfileMessage
| PlayerBorderTilesMessage
| AttackAveragePositionMessage
@@ -131,6 +148,7 @@ export type WorkerMessage =
| GameUpdateMessage
| GameUpdateBatchMessage
| PlayerActionsResultMessage
+ | PlayerBuildablesResultMessage
| PlayerProfileResultMessage
| PlayerBorderTilesResultMessage
| AttackAveragePositionResultMessage
diff --git a/tests/PlayerImpl.test.ts b/tests/PlayerImpl.test.ts
index 71d022f22..900b35488 100644
--- a/tests/PlayerImpl.test.ts
+++ b/tests/PlayerImpl.test.ts
@@ -29,9 +29,12 @@ describe("PlayerImpl", () => {
}
player = game.player("player_id");
- player.addGold(BigInt(1000000));
other = game.player("other_id");
+ player.conquer(game.ref(0, 0));
+ other.conquer(game.ref(50, 50));
+ player.addGold(BigInt(1000000));
+
game.config().structureMinDist = () => 10;
});