mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Fix/Perf/Refactor: playerActions and buildableUnits, their callers and related types (#3220)
## Description: TL;DR: it's faster. buildableUnits is called via PlayerView.actions from UnitDisplay (each tick without TileRef), BuildMenu (each tick when open), MainRadialMenu (each tick when open), PlayerPanel (each tick when open), StructureIconsLayer (when placing a building from build bar), NukeTrajectoryPreviewLayer (when placing nuke, on tick when tile changes), ClientGameRunner (on click to attack/auto-boat or hotkey B or G). After https://github.com/openfrontio/OpenFrontIO/pull/3213 got merged, the change with largest impact in https://github.com/openfrontio/OpenFrontIO/pull/3193 was done in such a different way that a new PR was needed The idea in 3193 was to not always ask for Transport Ship from buildableUnits. In such a way that very little extra data was send to the worker. This had the biggest impact on performance (the idea was months older btw, see https://github.com/openfrontio/OpenFrontIO/pull/2295). Now, we do it the other way around, by telling buildableUnits all unit types we want. Or we want them all (undefined). The downside is more data is send in the worker message. The upside is we have more options and can add more in this PR. This PR implements some of the leftovers in 3193 on top of 3213 and adds further improvements. (Some unrelated refactor/perf changes where moved out of this PR and into already merged https://github.com/openfrontio/OpenFrontIO/pull/3233, https://github.com/openfrontio/OpenFrontIO/pull/3234, https://github.com/openfrontio/OpenFrontIO/pull/3235, https://github.com/openfrontio/OpenFrontIO/pull/3236, https://github.com/openfrontio/OpenFrontIO/pull/3237, https://github.com/openfrontio/OpenFrontIO/pull/3238, https://github.com/openfrontio/OpenFrontIO/pull/3239) - **GameRunner**, **WorkerMessages**: _playerActions_ and _PlayerActionsMessage ._ Option to ask for no buildable units (null). It now has 3 modes: get all actions and all buildings (units undefined), get all actions and no buildings (units null), or get all actions and specific building (units contains Unit Types). - **GameRunner**: _playerActions_. fixes wrong assumption in PR 3213: that only if units was undefined, we have to know canAttack. ClientGameRunner wants to know both, in case of a click on non-bordering land, to decide if it should auto-boat using a Transport Ship. So units is not undefined (we only ask for Transport Ship now which has a positive effect on performance for each click/tap) but we need canAttack still. Solved by removing the unit === undefined check before _canAttack_ in _playerActions_. - **GameRunner**, **GameView**, **WorkerClient**, **WorkerMessages**, **Worker.worker**: added _playerBuildables_ / _buildables_ next to existing _playerActions_ / _actions_. With above solved, there was still no option to only get buildable units when the actions are not needed. While **StructureIconsLayer**, **NukeTrajectoryPreviewLayer**, **BuildMenu** and **UnitDisplay** need only that. To not make playerActions more convoluted with more params or so, i've added a new function _playerBuildables_ in **GameView** to only get buildable units (**GameRunner** _playerBuildables_). _playerBuildables_ has 2 modes: get all buildings (units undefined) or get specific buildings (units contains Unit Types). Also update some comments that mentioned .actions in **NukeTrajectoryPreviewLayer**. - **ClientGameRunner**, **PlayerPanel**, **BuildMenu**, **UnitDisplay**, **StructureIconsLayer** and **NukeTrajectoryPreviewLayer**: Since PR 3213, **StructureIconsLayer** and **NukeTrajectoryPreviewLayer** ask for specific types of units from **GameView** _actions_ (**GameRunner** playerActions). Now have the other files do the same. For example **BuildMenu** asks for the new _BuildMenuTypes_ when it calls ._buildables_ and **ClientGameRunner** asks for UnitType.TransportShip when sending a boat - **ClientGameRunner**: canBoatAttack now accepts BuildableUnit[] instead of PlayerActions so we can send it either actions.buildableUnits or just buildables. Have functions call myPlayer.buildables(tileRef, [UnitType.TransportShip]) when we only need a buildable unit and no actions. Or myPlayer.actions(tileRef, null) when we need actions but no buildable units. Or myPlayer.actions(tileRef, [UnitType.TransportShip]) when we need both actions, like canAttack, and a buildable unit. Then if needed send either actions.buildableUnits or buildables to to _canAutoBoat_ / _canBoatAttack_. - **MainRadialMenu**: needs all player buildable unit types including Transport Ship, so the _actions_ call argument for unit types can stay undefined (unchanged) there. - **MainRadialMenu**: now that **BuildMenu** uses _playerBuildables_ instead of _playerActions_, we must put data in _this.buildMenu.playerBuildables_. And since we're not putting the (unneeded) full _actions_ in there anymore, we can now put only the needed and expected _actions._buildableUnits_ in it. - **Game**, **PlayerImpl**, **StructureIconsLayer**: Typesafety and some added perf: new type _PlayerBuildableUnitType_ (see also the below point for how it is formed). So callers of _buildableUnits_ can never ask for the wrong type like e.g. UnitType.Train because it doesn't return data for that type. This type is now used in **PlayerImpl**, **BuildMenu**, **RadialMenuElements**, **StructureDrawingUtils** and **UnitDisplay** for that reason. And **InputHandler**, **StructureIconsLayer** and **UIState** (little more on that in point below). - **InputHandler**, **StructureIconsLayer**, **UIState**: In order to make type safety work for GhostUnit.buildableUnit.type too (line ~217 of StructureIconsLayer), changed type of interface _BuildableUnit_ to _PlayerBuildableType_. Which is only more accurate. Same for and this.structures and uiState.ghostStructure and with the latter, _renderUnitItem_ in **UnitDisplay** and _setGhostStructure_ in **InputHandler**. All Structures are of PlayerBuildableType (there are even some in PlayerBuildables that aren't Structures, but it is much more confined than UnitType). - **Game**: Typesafety and some added perf: added _BuildMenus_ and _BuildableAttacks_ in the same fashion that the existing StructureTypes was already used (simplified it a bit too, with it renamed _StructureTypes_ to _Structures_ and removed _isStructureType_). They can be used with .types or .has(). _BuildableAttacks_.has() is used in **RadialMenuElements**. _BuildableAttacks_ and existing _Structures_ now make up _BuildMenus_. Which is used in **BuildMenu**, **StructureIconsLayer** and **UnitDisplay**. Then _BuildMenus_ together with UnitType.TransportShip make up the _PlayerBuildables_. Which is used in **PlayerImpl** _buildableUnits_ (see point below). And with _PlayerBuildableUnits_ we get the new _PlayerBuildableUnitType_ (see above point on Game / PlayerImpl). - **RadialMenuElements**: replace non-central ATTACK_UNIT_TYPES in **RadialMenuElements** with centralized _BuildableAttackTypes_ too. Use _PlayerBuildableUnitType_ for more type safety (can't by mistake add UnitType.Train to its build menu). Make use of _BuildableAttackTypes_ instead of adding items hardcoded line by line in _getAllEnabledUnits_, just like we already did since PR 3239 with _StructureTypes_. And use _BuildableAttacks.types_ in the same fashion that existing _isStructureTypes_ (now Structures.types) was already used elsewhere. - **PlayerImpl**: _buildableUnits_ -- would do Object.values(UnitTypes) on every call. Now for better perf directly loop over player buildable units by using _PlayerBuildables_ (see above point). In this way we also exclude MIRVWarhead, TradeShip, Train, SamMissile and Shell so there are less unit types to loop through by default. Since a player doesn't build those by themselves, they are only build by Executions which use _canBuild_ directly and not _buildableUnits_. -- for more performance, do for loop instead of using .map and .filter, no intermediate array needed nor callback overhead. We just loop over the given units (which if undefined will contain _PlayerBuildables_). Also pre-allocate the results array to get the most out of it, even if V8 might already be very good at this. -- cache config, railNetwork and inSpawnPhase so they can be re-used inside the for loop. -- cache cost inside the loop -- it would check twice for tile!==null to decide to call findUnitToUpgrade and canBuild. Now once. -- eliminated double/triple checks for the same thing. It called _findUnitToUpgrade_ (and with that _canUpgradeUnit_) and then _canBuild_ which both check if player has enough gold for the cost of the unit type. And they both check if the unit type is disabled. Now we call private functions _canBuildUnitType_, _canUpgradeUnitType_ to first do checks on unit type level for early returns, and _findExistingUnitToUpgrade_ to find existing unit without doing anything extra. in a specific order to check everything only once. The public functions _findUnitToUpgrade_ and _canBuild_ have an unchanged functionality and we don't call them from _buildableUnits_ anymore. -- would get _overlappingRailRoads_ and _computeGhostRailPaths_ when canBuild was true. But this data is only meant for **StructureIconsLayer** and it logically only uses it when placing a new unit, not when upgrading one. Which is also commented on line 351 of **StructureIconsLayer**. So, we now only get overlapping railroads and ghost rails if we're not hovering to upgrade an existing unit. - **PlayerImpl**: _findUnitToUpgrade_: unchanged functionality, but have it call new private function _findExistingUnitToUpgrade_ to find existing unit. - **PlayerImpl**: _canBuild_: unchanged functionality, but have it call new private function _canBuildUnitType_ to do the checks it first did itself. And then new private function _canSpawnUnitType_ for the rest of the checks. This way we can call _canBuildUnitType_ and _canSpawnUnitType_ from _buildableUnits_ in a specific order to prevent double/triple checks. - **PlayerImpl**: _canBuildUnitType_: new private function to be shared by _buildableUnits_, _canBuild_ and _canUpgradeUnit_ to be able do unit type level checks in a specific order to prevent double/triple checks. Via parameter knownCost, _buildableUnits_ can send it the cost it already fetched so that it doesn't have to be fetched again. For caller _canUpgradeUnit_, the isAlive() check (which was previously only done in canBuild) is new but harmless, maybe even better to have also check isAlive() on upgrade now that Nations are also upgrading which might prevent some edge case bugs. - **PlayerImpl**: _canUpgradeUnitType_: new private function to be shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit type level checks in a specific order to prevent double/triple checks. - **PlayerImpl**: _canSpawnUnitType_: new private function to be shared by _buildableUnits_ and _canBuildUnit_ to be able do unit type level checks in a specific order to prevent double/triple checks. - **PlayerImpl**: _findExistingUnitToUpgrade_: new private function to be shared by _buildableUnits_ and _findUnitToUpgrade_ to be able do unit level checks in a specific order to prevent double/triple checks. - **PlayerImpl**: _isUnitValidToUpgrade_: new private function to be shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit level checks in a specific order to prevent double/triple checks. - **PlayerImpl.test.ts**: because of the isAlive() check in which is new for _canUpgradeUnit_ (see above at _canBuildUnitType_), the tests needed to have the players be alive at the start, in order to pass. - **BuildMenu**: use .find instead of .filter in canBuildOrUpgrade, a function we already needed to change. This is faster and prevents an allocation. **PERFORMANCE** As for calling ._buildables_ instead of unnecessarily getting ._actions_, there is an obvious win because there's less to send calculate and recieve. Also asking for only the needed buildings helps a lot (especially if TradeShip isn't needed, see the difference in benchmark in original #3193). But the real-world impact is hard to measure. gave it a try in #3193 and those results should be even better now. Now testing only _buildableUnits_ performance in a synthetic benchmark, we get these results. This is after other performance improvments so the base is already better than it was in original #3193: **BEFORE** (only buildableUnits itself) <img width="602" height="96" alt="image" src="https://github.com/user-attachments/assets/7770c0fa-a35e-42fc-90de-1de83242ec23" /> **AFTER** (only buildableUnits itself) <img width="603" height="91" alt="image" src="https://github.com/user-attachments/assets/a1578382-7010-4160-937c-7117bad18beb" /> ## 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: tryout33 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`
|
||||
<div class="build-row">
|
||||
${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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<UnitType> {
|
||||
const units: Set<UnitType> = new Set<UnitType>();
|
||||
function getAllEnabledUnits(
|
||||
myPlayer: boolean,
|
||||
config: Config,
|
||||
): Set<PlayerBuildableUnitType> {
|
||||
const units: Set<PlayerBuildableUnitType> =
|
||||
new Set<PlayerBuildableUnitType>();
|
||||
|
||||
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<UnitType> = getAllEnabledUnits(
|
||||
const unitTypes: Set<PlayerBuildableUnitType> = 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<UnitType, { visible: boolean }> = 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) {
|
||||
|
||||
@@ -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<string, { value: string; key: string }> = {};
|
||||
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,
|
||||
) {
|
||||
|
||||
+17
-3
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+45
-16
@@ -252,6 +252,15 @@ export interface UnitInfo {
|
||||
upgradable?: boolean;
|
||||
}
|
||||
|
||||
function unitTypeGroup<T extends readonly UnitType[]>(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<UnitType> = 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<T extends UnitType> = 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<T extends UnitType>(
|
||||
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[][];
|
||||
|
||||
@@ -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<PlayerActions> {
|
||||
async actions(
|
||||
tile?: TileRef,
|
||||
units?: readonly PlayerBuildableUnitType[] | null,
|
||||
): Promise<PlayerActions> {
|
||||
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<BuildableUnit[]> {
|
||||
return this.game.worker.playerBuildables(
|
||||
this.id(),
|
||||
tile && this.game.x(tile),
|
||||
tile && this.game.y(tile),
|
||||
units,
|
||||
);
|
||||
}
|
||||
|
||||
async borderTiles(): Promise<PlayerBorderTiles> {
|
||||
return this.game.worker.playerBorderTiles(this.id());
|
||||
}
|
||||
|
||||
+105
-57
@@ -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<BuildableUnit>(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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MainThreadMessage,
|
||||
PlayerActionsResultMessage,
|
||||
PlayerBorderTilesResultMessage,
|
||||
PlayerBuildablesResultMessage,
|
||||
PlayerProfileResultMessage,
|
||||
TransportShipSpawnResultMessage,
|
||||
WorkerMessage,
|
||||
@@ -184,6 +185,28 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
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");
|
||||
|
||||
@@ -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<PlayerActions> {
|
||||
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<BuildableUnit[]> {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user