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; });