From 51519b0b9d265e548ef475832f6e060dd98c0edc Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Sat, 23 Aug 2025 19:21:40 -0400 Subject: [PATCH] Enable `strictPropertyInitialization` (#1909) ## Description: Enable the tsconfig option `strictPropertyInitialization`. Fixes #1907 ## 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 --- src/client/ClientGameRunner.ts | 2 +- src/client/LocalServer.ts | 4 +- src/client/Main.ts | 20 ++--- src/client/TerritoryPatternsModal.ts | 4 +- src/client/Transport.ts | 24 +++--- src/client/graphics/GameRenderer.ts | 6 +- src/client/graphics/TransformHandler.ts | 2 +- src/client/graphics/fx/SpriteFx.ts | 2 +- src/client/graphics/layers/AlertFrame.ts | 3 +- src/client/graphics/layers/BuildMenu.ts | 20 +++-- src/client/graphics/layers/ChatDisplay.ts | 6 +- src/client/graphics/layers/ChatModal.ts | 11 ++- src/client/graphics/layers/ControlPanel.ts | 25 +++--- src/client/graphics/layers/EmojiTable.ts | 20 ++--- src/client/graphics/layers/EventsDisplay.ts | 77 ++++++++++++------- src/client/graphics/layers/FxLayer.ts | 8 +- src/client/graphics/layers/GameLeftSidebar.ts | 3 +- .../graphics/layers/GameRightSidebar.ts | 16 ++-- src/client/graphics/layers/GutterAdModal.ts | 3 +- src/client/graphics/layers/HeadsUpMessage.ts | 3 +- src/client/graphics/layers/MultiTabModal.ts | 6 +- src/client/graphics/layers/NameLayer.ts | 8 +- src/client/graphics/layers/OptionsMenu.ts | 14 ++-- src/client/graphics/layers/PlayerPanel.ts | 76 +++++++++--------- src/client/graphics/layers/RadialMenu.ts | 10 ++- src/client/graphics/layers/RailroadLayer.ts | 8 +- src/client/graphics/layers/SettingsModal.ts | 53 ++++++------- src/client/graphics/layers/SpawnAd.ts | 3 +- .../graphics/layers/StructureIconsLayer.ts | 19 +++-- src/client/graphics/layers/StructureLayer.ts | 8 +- src/client/graphics/layers/TeamStats.ts | 7 +- src/client/graphics/layers/TerrainLayer.ts | 12 +-- src/client/graphics/layers/TerritoryLayer.ts | 29 +++++-- src/client/graphics/layers/UILayer.ts | 10 +-- src/client/graphics/layers/UnitDisplay.ts | 10 ++- src/client/graphics/layers/UnitLayer.ts | 31 +++++--- src/client/graphics/layers/WinModal.ts | 18 +++-- src/core/configuration/DefaultConfig.ts | 2 +- src/core/execution/AttackExecution.ts | 31 +++++--- src/core/execution/BotExecution.ts | 6 +- src/core/execution/CityExecution.ts | 3 +- src/core/execution/ConstructionExecution.ts | 10 ++- src/core/execution/DefensePostExecution.ts | 3 +- src/core/execution/DeleteUnitExecution.ts | 2 +- src/core/execution/DonateGoldExecution.ts | 5 +- src/core/execution/DonateTroopExecution.ts | 5 +- src/core/execution/EmbargoExecution.ts | 3 +- src/core/execution/EmojiExecution.ts | 3 +- src/core/execution/FactoryExecution.ts | 4 +- src/core/execution/FakeHumanExecution.ts | 74 +++++++++++------- src/core/execution/MIRVExecution.ts | 23 ++++-- src/core/execution/MissileSiloExecution.ts | 3 +- src/core/execution/NukeExecution.ts | 15 +++- src/core/execution/PlayerExecution.ts | 17 ++-- src/core/execution/PortExecution.ts | 15 ++-- src/core/execution/QuickChatExecution.ts | 6 +- src/core/execution/RailroadExecution.ts | 11 +-- src/core/execution/RetreatExecution.ts | 7 +- src/core/execution/SAMLauncherExecution.ts | 10 +-- src/core/execution/SAMMissileExecution.ts | 6 +- src/core/execution/ShellExecution.ts | 10 ++- src/core/execution/SpawnExecution.ts | 3 +- src/core/execution/TargetPlayerExecution.ts | 3 +- src/core/execution/TradeShipExecution.ts | 7 +- src/core/execution/TrainStationExecution.ts | 8 +- src/core/execution/TransportShipExecution.ts | 19 +++-- .../execution/UpgradeStructureExecution.ts | 2 +- src/core/execution/WarshipExecution.ts | 22 ++++-- src/core/execution/utils/BotBehavior.ts | 3 +- src/core/game/Game.ts | 2 +- src/core/game/GameImpl.ts | 5 +- src/core/game/PlayerImpl.ts | 2 +- src/core/game/TrainStation.ts | 2 +- src/core/pathfinding/PathFinding.ts | 4 +- tests/client/graphics/UILayer.test.ts | 24 +++--- tsconfig.json | 5 +- 76 files changed, 599 insertions(+), 367 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 0d54f484e..ab29a1b78 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -573,7 +573,7 @@ export class ClientGameRunner { if (!this.myPlayer) return; this.myPlayer.bestTransportShipSpawn(tile).then((spawn: number | false) => { - if (this.myPlayer === null) throw new Error("not initialized"); + if (this.myPlayer === null) throw new Error("Not initialized"); this.eventBus.emit( new SendBoatAttackIntentEvent( this.gameView.owner(tile).id(), diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 12cbb78dd..c68496dce 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -24,7 +24,7 @@ export class LocalServer { private readonly turns: Turn[] = []; private intents: Intent[] = []; - private startedAt: number; + private startedAt = 0; private paused = false; private replaySpeedMultiplier = defaultReplaySpeedMultiplier; @@ -35,7 +35,7 @@ export class LocalServer { private turnsExecuted = 0; private turnStartTime = 0; - private turnCheckInterval: ReturnType; + private turnCheckInterval: ReturnType | undefined; constructor( private readonly lobbyConfig: LobbyConfig, diff --git a/src/client/Main.ts b/src/client/Main.ts index b5bcd23d2..aa7ac7a08 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -91,8 +91,8 @@ class Client { private flagInput: FlagInput | null = null; private darkModeButton: DarkModeButton | null = null; - private joinModal: JoinPrivateLobbyModal; - private publicLobby: PublicLobby; + private joinModal: JoinPrivateLobbyModal | undefined; + private publicLobby: PublicLobby | undefined; private readonly userSettings: UserSettings = new UserSettings(); constructor() {} @@ -365,7 +365,7 @@ class Client { hostLobbyButton.addEventListener("click", () => { if (this.usernameInput?.isValid()) { hostModal.open(); - this.publicLobby.leaveLobby(); + this.publicLobby?.leaveLobby(); } }); @@ -380,7 +380,7 @@ class Client { throw new Error("Missing join-private-lobby-button"); joinPrivateLobbyButton.addEventListener("click", () => { if (this.usernameInput?.isValid()) { - this.joinModal.open(); + this.joinModal?.open(); } }); @@ -395,7 +395,7 @@ class Client { const onHashUpdate = () => { // Reset the UI to its initial state - this.joinModal.close(); + this.joinModal?.close(); if (this.gameStop !== null) { this.handleLeaveLobby(); } @@ -449,7 +449,7 @@ class Client { } const lobbyId = params.get("join"); if (lobbyId && ID.safeParse(lobbyId).success) { - this.joinModal.open(lobbyId); + this.joinModal?.open(lobbyId); console.log(`joining lobby ${lobbyId}`); } } @@ -509,7 +509,7 @@ class Client { modal.isModalOpen = false; } }); - this.publicLobby.stop(); + this.publicLobby?.stop(); document.querySelectorAll(".ad").forEach((ad) => { (ad as HTMLElement).style.display = "none"; }); @@ -522,8 +522,8 @@ class Client { startingModal.show(); }, () => { - this.joinModal.close(); - this.publicLobby.stop(); + this.joinModal?.close(); + this.publicLobby?.stop(); incrementGamesPlayed(); try { @@ -550,7 +550,7 @@ class Client { console.log("leaving lobby, cancelling game"); this.gameStop(); this.gameStop = null; - this.publicLobby.leaveLobby(); + this.publicLobby?.leaveLobby(); } private handleKickPlayer(event: CustomEvent) { diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index 09e34db74..6793e7f78 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -34,7 +34,7 @@ export class TerritoryPatternsModal extends LitElement { private patterns: Pattern[] = []; private me: UserMeResponse | null = null; - public resizeObserver: ResizeObserver; + public resizeObserver: ResizeObserver | undefined; private readonly userSettings: UserSettings = new UserSettings(); @@ -52,7 +52,7 @@ export class TerritoryPatternsModal extends LitElement { const containers = this.renderRoot.querySelectorAll(".preview-container"); if (this.resizeObserver) { containers.forEach((container) => - this.resizeObserver.observe(container), + this.resizeObserver?.observe(container), ); } this.updatePreview(); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 0fd75d17c..e98a7de28 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -172,12 +172,12 @@ export class SendKickPlayerIntentEvent implements GameEvent { export class Transport { private socket: WebSocket | null = null; - private localServer: LocalServer; + private localServer: LocalServer | undefined; private readonly buffer: string[] = []; - private onconnect: () => void; - private onmessage: (msg: ServerMessage) => void; + private onconnect: (() => void) | undefined; + private onmessage: ((msg: ServerMessage) => void) | undefined; private pingInterval: number | null = null; public readonly isLocal: boolean; @@ -336,7 +336,7 @@ export class Transport { console.error("Error parsing server message", error); return; } - this.onmessage(result.data); + this.onmessage?.(result.data); } catch (e) { console.error("Error in onmessage handler:", e, event.data); return; @@ -362,12 +362,14 @@ export class Transport { } public reconnect() { + if (this.onconnect === undefined) return; + if (this.onmessage === undefined) return; this.connect(this.onconnect, this.onmessage); } public turnComplete() { if (this.isLocal) { - this.localServer.turnComplete(); + this.localServer?.turnComplete(); } } @@ -386,7 +388,7 @@ export class Transport { leaveGame(saveFullGame = false) { if (this.isLocal) { - this.localServer.endGame(saveFullGame); + this.localServer?.endGame(saveFullGame); return; } this.stopPing(); @@ -550,9 +552,9 @@ export class Transport { return; } if (event.paused) { - this.localServer.pause(); + this.localServer?.pause(); } else { - this.localServer.resume(); + this.localServer?.resume(); } } @@ -648,7 +650,7 @@ export class Transport { private sendMsg(msg: ClientMessage) { if (this.isLocal) { // Forward message to local server - this.localServer.onMessage(msg); + this.localServer?.onMessage(msg); return; } else if (this.socket === null) { // Socket missing, do nothing @@ -660,7 +662,9 @@ export class Transport { console.warn("socket not ready, closing and trying later"); this.socket.close(); this.socket = null; - this.connectRemote(this.onconnect, this.onmessage); + if (this.onconnect && this.onmessage) { + this.connectRemote(this.onconnect, this.onmessage); + } this.buffer.push(str); } else { // Send the message directly diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 0e2343ebc..327495154 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -61,9 +61,7 @@ export function createRenderer( if (!emojiTable || !(emojiTable instanceof EmojiTable)) { console.error("EmojiTable element not found in the DOM"); } - emojiTable.transformHandler = transformHandler; - emojiTable.game = game; - emojiTable.initEventBus(eventBus); + emojiTable.init(transformHandler, game, eventBus); const buildMenu = document.querySelector("build-menu") as BuildMenu; if (!buildMenu || !(buildMenu instanceof BuildMenu)) { @@ -237,7 +235,7 @@ export function createRenderer( new StructureIconsLayer(game, eventBus, transformHandler), new UnitLayer(game, eventBus, transformHandler), new FxLayer(game), - new UILayer(game, eventBus, transformHandler), + new UILayer(game, eventBus), new NameLayer(game, transformHandler, eventBus), eventsDisplay, chatDisplay, diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index ec4587456..b1569e463 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -19,7 +19,7 @@ export class TransformHandler { private offsetY = -200; private lastGoToCallTime: number | null = null; - private target: Cell | null; + private target: Cell | null = null; private intervalID: ReturnType | null = null; private changed = false; diff --git a/src/client/graphics/fx/SpriteFx.ts b/src/client/graphics/fx/SpriteFx.ts index 7ef479daa..be5e7ddb7 100644 --- a/src/client/graphics/fx/SpriteFx.ts +++ b/src/client/graphics/fx/SpriteFx.ts @@ -58,7 +58,7 @@ export class SpriteFx implements Fx { theme, ); if (!this.animatedSprite) { - console.error("Could not load animated sprite", fxType); + throw new Error(`Could not load animated sprite ${fxType}`); } else { this.waitToTheEnd = duration ? true : false; this.duration = duration ?? this.animatedSprite.lifeTime() ?? 1000; diff --git a/src/client/graphics/layers/AlertFrame.ts b/src/client/graphics/layers/AlertFrame.ts index 515d6e34b..a77e0603c 100644 --- a/src/client/graphics/layers/AlertFrame.ts +++ b/src/client/graphics/layers/AlertFrame.ts @@ -14,7 +14,7 @@ const ALERT_COUNT = 2; @customElement("alert-frame") export class AlertFrame extends LitElement implements Layer { - public game: GameView; + public game: GameView | undefined; private readonly userSettings: UserSettings = new UserSettings(); @state() @@ -90,6 +90,7 @@ export class AlertFrame extends LitElement implements Layer { } private onBrokeAllianceUpdate(update: BrokeAllianceUpdate) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) return; diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index 0dfb9fe56..53702ed82 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -123,15 +123,18 @@ export const flattenedBuildTable = buildTable.flat(); @customElement("build-menu") export class BuildMenu extends LitElement implements Layer { - public game: GameView; - public eventBus: EventBus; - private clickedTile: TileRef; - public playerActions: PlayerActions | null; + public game: GameView | undefined; + public eventBus: EventBus | undefined; + private clickedTile: TileRef | undefined; + public playerActions: PlayerActions | null = null; private filteredBuildTable: BuildItemDisplay[][] = buildTable; - public transformHandler: TransformHandler; + public transformHandler: TransformHandler | undefined; init() { + if (this.eventBus === undefined) throw new Error("Not initialized"); this.eventBus.on(ShowBuildMenuEvent, (e) => { + if (!this.game) return; + if (!this.transformHandler) return; if (!this.game.myPlayer()?.isAlive()) { return; } @@ -386,7 +389,9 @@ export class BuildMenu extends LitElement implements Layer { return player.totalUnitLevels(item.unitType).toString(); } - public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile: TileRef): void { + public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile?: TileRef): void { + if (tile === undefined) throw new Error("Missing tile"); + if (this.eventBus === undefined) throw new Error("Not initialized"); if (buildableUnit.canUpgrade !== false) { this.eventBus.emit( new SendUpgradeStructureIntentEvent( @@ -481,8 +486,9 @@ export class BuildMenu extends LitElement implements Layer { } private refresh() { + if (this.clickedTile === undefined) return; this.game - .myPlayer() + ?.myPlayer() ?.actions(this.clickedTile) .then((actions) => { this.playerActions = actions; diff --git a/src/client/graphics/layers/ChatDisplay.ts b/src/client/graphics/layers/ChatDisplay.ts index 6679f2979..da81e5c21 100644 --- a/src/client/graphics/layers/ChatDisplay.ts +++ b/src/client/graphics/layers/ChatDisplay.ts @@ -21,8 +21,8 @@ type ChatEvent = { @customElement("chat-display") export class ChatDisplay extends LitElement implements Layer { - public eventBus: EventBus; - public game: GameView; + public eventBus: EventBus | undefined; + public game: GameView | undefined; private readonly active = false; @@ -55,6 +55,7 @@ export class ChatDisplay extends LitElement implements Layer { onDisplayMessageEvent(event: DisplayMessageUpdate) { if (event.messageType !== MessageType.CHAT) return; + if (!this.game) return; const myPlayer = this.game.myPlayer(); if ( event.playerID !== null && @@ -75,6 +76,7 @@ export class ChatDisplay extends LitElement implements Layer { tick() { // this.active = true; + if (!this.game) return; const updates = this.game.updatesSinceLastTick(); if (updates === null) return; const messages = updates[GameUpdateType.DisplayEvent] as diff --git a/src/client/graphics/layers/ChatModal.ts b/src/client/graphics/layers/ChatModal.ts index 94afb2658..3aacd9538 100644 --- a/src/client/graphics/layers/ChatModal.ts +++ b/src/client/graphics/layers/ChatModal.ts @@ -39,11 +39,11 @@ export class ChatModal extends LitElement { private selectedQuickChatKey: string | null = null; private selectedPlayer: PlayerView | null = null; - private recipient: PlayerView; - private sender: PlayerView; - public eventBus: EventBus; + private recipient: PlayerView | undefined; + private sender: PlayerView | undefined; + public eventBus: EventBus | undefined; - public g: GameView; + public g: GameView | undefined; quickChatPhrases: Record< string, @@ -220,6 +220,7 @@ export class ChatModal extends LitElement { } private sendChatMessage() { + if (!this.eventBus) return; console.log("Sent message:", this.previewText); console.log("Sender:", this.sender); console.log("Recipient:", this.recipient); @@ -270,6 +271,7 @@ export class ChatModal extends LitElement { if (sender && recipient) { console.log("Sent message:", recipient); console.log("Sent message:", sender); + if (!this.g) throw new Error("Not initialized"); this.players = this.g .players() .filter((p) => p.isAlive() && p.data.playerType !== PlayerType.Bot); @@ -304,6 +306,7 @@ export class ChatModal extends LitElement { recipient?: PlayerView, ) { if (sender && recipient) { + if (!this.g) throw new Error("Not initialized"); this.players = this.g .players() .filter((p) => p.isAlive() && p.data.playerType !== PlayerType.Bot); diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 1e790f216..3257f0f6f 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -12,34 +12,36 @@ import { translateText } from "../../../client/Utils"; @customElement("control-panel") export class ControlPanel extends LitElement implements Layer { - public game: GameView; - public clientID: ClientID; - public eventBus: EventBus; - public uiState: UIState; + public game: GameView | undefined; + public clientID: ClientID | undefined; + public eventBus: EventBus | undefined; + public uiState: UIState | undefined; @state() private attackRatio = 0.2; @state() - private _maxTroops: number; + private _maxTroops = 0; @state() - private troopRate: number; + private troopRate = 0; @state() - private _troops: number; + private _troops = 0; @state() private _isVisible = false; @state() - private _gold: Gold; + private _gold: Gold = 0n; private _troopRateIsIncreasing = true; - private _lastTroopIncreaseRate: number; + private _lastTroopIncreaseRate = 0; init() { + if (!this.uiState) throw new Error("Not initialized"); + if (!this.eventBus) throw new Error("Not initialized"); this.attackRatio = Number( localStorage.getItem("settings.attackRatio") ?? "0.2", ); @@ -71,6 +73,7 @@ export class ControlPanel extends LitElement implements Layer { } tick() { + if (!this.game) return; if (!this._isVisible && !this.game.inSpawnPhase()) { this.setVisibile(true); } @@ -94,7 +97,8 @@ export class ControlPanel extends LitElement implements Layer { } private updateTroopIncrease() { - const player = this.game?.myPlayer(); + if (this.game === undefined) return; + const player = this.game.myPlayer(); if (player === null) return; const troopIncreaseRate = this.game.config().troopIncreaseRate(player); this._troopRateIsIncreasing = @@ -103,6 +107,7 @@ export class ControlPanel extends LitElement implements Layer { } onAttackRatioChange(newRatio: number) { + if (this.uiState === undefined) return; this.uiState.attackRatio = newRatio; } diff --git a/src/client/graphics/layers/EmojiTable.ts b/src/client/graphics/layers/EmojiTable.ts index 441cfc130..4625f3427 100644 --- a/src/client/graphics/layers/EmojiTable.ts +++ b/src/client/graphics/layers/EmojiTable.ts @@ -12,23 +12,25 @@ import { TransformHandler } from "../TransformHandler"; @customElement("emoji-table") export class EmojiTable extends LitElement { @state() public isVisible = false; - public transformHandler: TransformHandler; - public game: GameView; - initEventBus(eventBus: EventBus) { + init( + transformHandler: TransformHandler, + game: GameView, + eventBus: EventBus, + ) { eventBus.on(ShowEmojiMenuEvent, (e) => { this.isVisible = true; - const cell = this.transformHandler.screenToWorldCoordinates(e.x, e.y); - if (!this.game.isValidCoord(cell.x, cell.y)) { + const cell = transformHandler.screenToWorldCoordinates(e.x, e.y); + if (!game.isValidCoord(cell.x, cell.y)) { return; } - const tile = this.game.ref(cell.x, cell.y); - if (!this.game.hasOwner(tile)) { + const tile = game.ref(cell.x, cell.y); + if (!game.hasOwner(tile)) { return; } - const targetPlayer = this.game.owner(tile); + const targetPlayer = game.owner(tile); // maybe redundant due to owner check but better safe than sorry if (targetPlayer instanceof TerraNulliusImpl) { return; @@ -36,7 +38,7 @@ export class EmojiTable extends LitElement { this.showTable((emoji) => { const recipient = - targetPlayer === this.game.myPlayer() + targetPlayer === game.myPlayer() ? AllPlayers : (targetPlayer as PlayerView); eventBus.emit( diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index efbac843f..193e5f314 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -69,8 +69,8 @@ type GameEvent = { @customElement("events-display") export class EventsDisplay extends LitElement implements Layer { - public eventBus: EventBus; - public game: GameView; + public eventBus: EventBus | undefined; + public game: GameView | undefined; private active = false; private events: GameEvent[] = []; @@ -169,6 +169,7 @@ export class EventsDisplay extends LitElement implements Layer { tick() { this.active = true; + if (this.game === undefined) return; if (!this._isVisible && !this.game.inSpawnPhase()) { this._isVisible = true; this.requestUpdate(); @@ -193,6 +194,7 @@ export class EventsDisplay extends LitElement implements Layer { } let remainingEvents = this.events.filter((event) => { + if (this.game === undefined) return; const shouldKeep = this.game.ticks() - event.createdAt < (event.duration ?? 600); if (!shouldKeep && event.onDelete) { @@ -212,7 +214,10 @@ export class EventsDisplay extends LitElement implements Layer { // Update attacks this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => { - const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type(); + if (this.game === undefined) return false; + const p = this.game.playerBySmallID(a.attackerID); + if (!p.isPlayer()) return false; + const t = p.type(); return t !== PlayerType.Bot; }); @@ -239,6 +244,7 @@ export class EventsDisplay extends LitElement implements Layer { } private checkForAllianceExpirations() { + if (this.game === undefined) return; const myPlayer = this.game.myPlayer(); if (!myPlayer?.isAlive()) return; @@ -273,7 +279,7 @@ export class EventsDisplay extends LitElement implements Layer { { text: translateText("events_display.focus"), className: "btn-gray", - action: () => this.eventBus.emit(new GoToPlayerEvent(other)), + action: () => this.eventBus?.emit(new GoToPlayerEvent(other)), preventClose: true, }, { @@ -282,7 +288,7 @@ export class EventsDisplay extends LitElement implements Layer { }), className: "btn", action: () => - this.eventBus.emit(new SendAllianceExtensionIntentEvent(other)), + this.eventBus?.emit(new SendAllianceExtensionIntentEvent(other)), }, { text: translateText("events_display.ignore"), @@ -319,6 +325,7 @@ export class EventsDisplay extends LitElement implements Layer { renderLayer(): void {} onDisplayMessageEvent(event: DisplayMessageUpdate) { + if (this.game === undefined) return; const myPlayer = this.game.myPlayer(); if ( event.playerID !== null && @@ -367,6 +374,7 @@ export class EventsDisplay extends LitElement implements Layer { } onDisplayChatEvent(event: DisplayChatMessageUpdate) { + if (this.game === undefined) return; const myPlayer = this.game.myPlayer(); if ( event.playerID === null || @@ -412,6 +420,7 @@ export class EventsDisplay extends LitElement implements Layer { } onAllianceRequestEvent(update: AllianceRequestUpdate) { + if (this.game === undefined) return; const myPlayer = this.game.myPlayer(); if (!myPlayer || update.recipientID !== myPlayer.smallID()) { return; @@ -432,14 +441,14 @@ export class EventsDisplay extends LitElement implements Layer { { text: translateText("events_display.focus"), className: "btn-gray", - action: () => this.eventBus.emit(new GoToPlayerEvent(requestor)), + action: () => this.eventBus?.emit(new GoToPlayerEvent(requestor)), preventClose: true, }, { text: translateText("events_display.accept_alliance"), className: "btn", action: () => - this.eventBus.emit( + this.eventBus?.emit( new SendAllianceReplyIntentEvent(requestor, recipient, true), ), }, @@ -447,7 +456,7 @@ export class EventsDisplay extends LitElement implements Layer { text: translateText("events_display.reject_alliance"), className: "btn-info", action: () => - this.eventBus.emit( + this.eventBus?.emit( new SendAllianceReplyIntentEvent(requestor, recipient, false), ), }, @@ -456,7 +465,7 @@ export class EventsDisplay extends LitElement implements Layer { type: MessageType.ALLIANCE_REQUEST, createdAt: this.game.ticks(), onDelete: () => - this.eventBus.emit( + this.eventBus?.emit( new SendAllianceReplyIntentEvent(requestor, recipient, false), ), priority: 0, @@ -466,6 +475,7 @@ export class EventsDisplay extends LitElement implements Layer { } onAllianceRequestReplyEvent(update: AllianceRequestReplyUpdate) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) { return; @@ -508,6 +518,7 @@ export class EventsDisplay extends LitElement implements Layer { } onBrokeAllianceEvent(update: BrokeAllianceUpdate) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) return; @@ -547,7 +558,7 @@ export class EventsDisplay extends LitElement implements Layer { { text: translateText("events_display.focus"), className: "btn-gray", - action: () => this.eventBus.emit(new GoToPlayerEvent(traitor)), + action: () => this.eventBus?.emit(new GoToPlayerEvent(traitor)), preventClose: true, }, ]; @@ -565,6 +576,7 @@ export class EventsDisplay extends LitElement implements Layer { } onAllianceExpiredEvent(update: AllianceExpiredUpdate) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) return; @@ -590,6 +602,7 @@ export class EventsDisplay extends LitElement implements Layer { } onTargetPlayerEvent(event: TargetPlayerUpdate) { + if (!this.game) return; const other = this.game.playerBySmallID(event.playerID) as PlayerView; const myPlayer = this.game.myPlayer() as PlayerView; if (!myPlayer || !myPlayer.isFriendly(other)) return; @@ -609,32 +622,36 @@ export class EventsDisplay extends LitElement implements Layer { } emitCancelAttackIntent(id: string) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) return; - this.eventBus.emit(new CancelAttackIntentEvent(id)); + this.eventBus?.emit(new CancelAttackIntentEvent(id)); } emitBoatCancelIntent(id: number) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) return; - this.eventBus.emit(new CancelBoatIntentEvent(id)); + this.eventBus?.emit(new CancelBoatIntentEvent(id)); } emitGoToPlayerEvent(attackerID: number) { - const attacker = this.game.playerBySmallID(attackerID) as PlayerView; - if (!attacker) return; - this.eventBus.emit(new GoToPlayerEvent(attacker)); + if (!this.game) return; + const attacker = this.game.playerBySmallID(attackerID); + if (!attacker.isPlayer()) return; + this.eventBus?.emit(new GoToPlayerEvent(attacker)); } emitGoToPositionEvent(x: number, y: number) { - this.eventBus.emit(new GoToPositionEvent(x, y)); + this.eventBus?.emit(new GoToPositionEvent(x, y)); } emitGoToUnitEvent(unit: UnitView) { - this.eventBus.emit(new GoToUnitEvent(unit)); + this.eventBus?.emit(new GoToUnitEvent(unit)); } onEmojiMessageEvent(update: EmojiUpdate) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer) return; @@ -671,6 +688,7 @@ export class EventsDisplay extends LitElement implements Layer { } onUnitIncomingEvent(event: UnitIncomingUpdate) { + if (!this.game) return; const myPlayer = this.game.myPlayer(); if (!myPlayer || myPlayer.smallID() !== event.playerID) { @@ -698,6 +716,7 @@ export class EventsDisplay extends LitElement implements Layer { } private async attackWarningOnClick(attack: AttackUpdate) { + if (!this.game) return; const playerView = this.game.playerBySmallID(attack.attackerID); if (playerView !== undefined) { if (playerView instanceof PlayerView) { @@ -722,13 +741,13 @@ export class EventsDisplay extends LitElement implements Layer { ${this.incomingAttacks.length > 0 ? html` ${this.incomingAttacks.map( - (attack) => html` + (attack) => { + const attacker = this.game?.playerBySmallID(attack.attackerID); + return html` ${this.renderButton({ content: html` ${renderTroops(attack.troops)} - ${( - this.game.playerBySmallID(attack.attackerID) as PlayerView - )?.name()} + ${attacker?.isPlayer() ? attacker.name() : "unknown"} ${attack.retreating ? `(${translateText("events_display.retreating")}...)` : ""} @@ -737,7 +756,8 @@ export class EventsDisplay extends LitElement implements Layer { className: "text-left text-red-400", translate: false, })} - `, + `; + }, )} ` : ""} @@ -750,16 +770,14 @@ export class EventsDisplay extends LitElement implements Layer { ? html`
${this.outgoingAttacks.map( - (attack) => html` + (attack) => { + const target = this.game?.playerBySmallID(attack.targetID); + return html`
${this.renderButton({ content: html` ${renderTroops(attack.troops)} - ${( - this.game.playerBySmallID( - attack.targetID, - ) as PlayerView - )?.name()} + ${target?.isPlayer() ? target.name() : "unknown"} `, onClick: async () => this.attackWarningOnClick(attack), className: "text-left text-blue-400", @@ -778,7 +796,8 @@ export class EventsDisplay extends LitElement implements Layer { )}...)`}
- `, + `; + }, )}
` diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index 982c204bb..3fa6a2072 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -18,8 +18,8 @@ import { conquestFxFactory } from "../fx/ConquestFx"; import { renderNumber } from "../../Utils"; export class FxLayer implements Layer { - private canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D; + private canvas: HTMLCanvasElement | undefined; + private context: CanvasRenderingContext2D | undefined; private lastRefresh = 0; private readonly refreshRate = 10; @@ -265,6 +265,7 @@ export class FxLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.canvas === undefined) throw new Error("Not initialized"); const now = Date.now(); if (this.game.config().userSettings()?.fxLayer()) { if (now > this.lastRefresh + this.refreshRate) { @@ -283,6 +284,8 @@ export class FxLayer implements Layer { } renderAllFx(context: CanvasRenderingContext2D, delta: number) { + if (this.canvas === undefined) throw new Error("Not initialized"); + if (this.context === undefined) throw new Error("Not initialized"); if (this.allFx.length > 0) { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); this.renderContextFx(delta); @@ -290,6 +293,7 @@ export class FxLayer implements Layer { } renderContextFx(duration: number) { + if (this.context === undefined) throw new Error("Not initialized"); for (let i = this.allFx.length - 1; i >= 0; i--) { if (!this.allFx[i].renderTick(duration, this.context)) { this.allFx.splice(i, 1); diff --git a/src/client/graphics/layers/GameLeftSidebar.ts b/src/client/graphics/layers/GameLeftSidebar.ts index 5cc4dae9f..11cddc2d1 100644 --- a/src/client/graphics/layers/GameLeftSidebar.ts +++ b/src/client/graphics/layers/GameLeftSidebar.ts @@ -21,7 +21,7 @@ export class GameLeftSidebar extends LitElement implements Layer { private playerTeam: string | null = null; private playerColor: Colord = new Colord("#FFFFFF"); - public game: GameView; + public game: GameView | undefined; private _shownOnInit = false; createRenderRoot() { @@ -42,6 +42,7 @@ export class GameLeftSidebar extends LitElement implements Layer { } tick() { + if (!this.game) throw new Error("Not initialized"); if (!this.playerTeam && this.game.myPlayer()?.team()) { const myPlayer = this.game.myPlayer(); if (myPlayer !== null) { diff --git a/src/client/graphics/layers/GameRightSidebar.ts b/src/client/graphics/layers/GameRightSidebar.ts index 611b0d995..7c8d1c9b6 100644 --- a/src/client/graphics/layers/GameRightSidebar.ts +++ b/src/client/graphics/layers/GameRightSidebar.ts @@ -18,8 +18,8 @@ import { translateText } from "../../Utils"; @customElement("game-right-sidebar") export class GameRightSidebar extends LitElement implements Layer { - public game: GameView; - public eventBus: EventBus; + public game: GameView | undefined; + public eventBus: EventBus | undefined; @state() private _isSinglePlayer = false; @@ -45,13 +45,13 @@ export class GameRightSidebar extends LitElement implements Layer { init() { this._isSinglePlayer = this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer || - this.game.config().isReplay(); + (this.game?.config().isReplay() ?? false); this._isVisible = true; - this.game.inSpawnPhase(); this.requestUpdate(); } tick() { + if (!this.game) throw new Error("Not initialized"); // Timer logic const updates = this.game.updatesSinceLastTick(); if (updates) { @@ -76,18 +76,18 @@ export class GameRightSidebar extends LitElement implements Layer { private toggleReplayPanel(): void { this._isReplayVisible = !this._isReplayVisible; - this.eventBus.emit( + this.eventBus?.emit( new ShowReplayPanelEvent(this._isReplayVisible, this._isSinglePlayer), ); } private onPauseButtonClick() { this.isPaused = !this.isPaused; - this.eventBus.emit(new PauseGameEvent(this.isPaused)); + this.eventBus?.emit(new PauseGameEvent(this.isPaused)); } private onExitButtonClick() { - const isAlive = this.game.myPlayer()?.isAlive(); + const isAlive = this.game?.myPlayer()?.isAlive(); if (isAlive) { const isConfirmed = confirm( translateText("help_modal.exit_confirmation"), @@ -99,7 +99,7 @@ export class GameRightSidebar extends LitElement implements Layer { } private onSettingsButtonClick() { - this.eventBus.emit( + this.eventBus?.emit( new ShowSettingsModalEvent(true, this._isSinglePlayer, this.isPaused), ); } diff --git a/src/client/graphics/layers/GutterAdModal.ts b/src/client/graphics/layers/GutterAdModal.ts index 809133971..2b9ed8991 100644 --- a/src/client/graphics/layers/GutterAdModal.ts +++ b/src/client/graphics/layers/GutterAdModal.ts @@ -10,7 +10,7 @@ export class GutterAdModalEvent implements GameEvent { @customElement("gutter-ad-modal") export class GutterAdModal extends LitElement implements Layer { - public eventBus: EventBus; + public eventBus: EventBus | undefined; @state() private isVisible = false; @@ -30,6 +30,7 @@ export class GutterAdModal extends LitElement implements Layer { } init() { + if (!this.eventBus) throw new Error("Not initialized"); if (getGamesPlayed() > 1) { this.eventBus.on(GutterAdModalEvent, (event) => { if (event.isVisible) { diff --git a/src/client/graphics/layers/HeadsUpMessage.ts b/src/client/graphics/layers/HeadsUpMessage.ts index f2d9521e9..2bf546eb4 100644 --- a/src/client/graphics/layers/HeadsUpMessage.ts +++ b/src/client/graphics/layers/HeadsUpMessage.ts @@ -6,7 +6,7 @@ import { translateText } from "../../Utils"; @customElement("heads-up-message") export class HeadsUpMessage extends LitElement implements Layer { - public game: GameView; + public game: GameView | undefined; @state() private isVisible = false; @@ -21,6 +21,7 @@ export class HeadsUpMessage extends LitElement implements Layer { } tick() { + if (!this.game) throw new Error("Not initialzied"); if (!this.game.inSpawnPhase()) { this.isVisible = false; this.requestUpdate(); diff --git a/src/client/graphics/layers/MultiTabModal.ts b/src/client/graphics/layers/MultiTabModal.ts index 956aaf8ce..412d0f18e 100644 --- a/src/client/graphics/layers/MultiTabModal.ts +++ b/src/client/graphics/layers/MultiTabModal.ts @@ -9,9 +9,9 @@ import { translateText } from "../../Utils"; @customElement("multi-tab-modal") export class MultiTabModal extends LitElement implements Layer { - public game: GameView; + public game: GameView | undefined; - private detector: MultiTabDetector; + private detector: MultiTabDetector | undefined; @property({ type: Number }) duration = 5000; @state() private countdown = 5; @@ -28,6 +28,7 @@ export class MultiTabModal extends LitElement implements Layer { } tick() { + if (!this.game) throw new Error("Not initialzied"); if ( this.game.inSpawnPhase() || this.game.config().gameConfig().gameType === GameType.Singleplayer || @@ -65,6 +66,7 @@ export class MultiTabModal extends LitElement implements Layer { // Show the modal with penalty information public show(duration: number): void { + if (!this.game) throw new Error("Not initialzied"); if (!this.game.myPlayer()?.isAlive()) { return; } diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index bbb0e8477..c089e04bd 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -36,7 +36,7 @@ class RenderInfo { } export class NameLayer implements Layer { - private canvas: HTMLCanvasElement; + private canvas: HTMLCanvasElement | undefined; private lastChecked = 0; private readonly renderCheckRate = 100; private readonly renderRefreshRate = 500; @@ -55,7 +55,7 @@ export class NameLayer implements Layer { private readonly nukeWhiteIconImage: HTMLImageElement; private readonly nukeRedIconImage: HTMLImageElement; private readonly shieldIconImage: HTMLImageElement; - private container: HTMLDivElement; + private container: HTMLDivElement | undefined; private firstPlace: PlayerView | null = null; private theme: Theme = this.game.config().theme(); private readonly userSettings: UserSettings = new UserSettings(); @@ -93,6 +93,7 @@ export class NameLayer implements Layer { } resizeCanvas() { + if (!this.canvas) throw new Error("Not initialzied"); this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } @@ -185,6 +186,7 @@ export class NameLayer implements Layer { screenPosOld.x - window.innerWidth / 2, screenPosOld.y - window.innerHeight / 2, ); + if (!this.container) throw new Error("Not initialzied"); this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) ` + `scale(${this.transformHandler.scale})`; @@ -197,6 +199,7 @@ export class NameLayer implements Layer { } } + if (!this.canvas) throw new Error("Not initialzied"); mainContex.drawImage( this.canvas, 0, @@ -302,6 +305,7 @@ export class NameLayer implements Layer { // Start off invisible so it doesn't flash at 0,0 element.style.display = "none"; + if (!this.container) throw new Error("Not initialzied"); this.container.appendChild(element); return element; } diff --git a/src/client/graphics/layers/OptionsMenu.ts b/src/client/graphics/layers/OptionsMenu.ts index 37b55c03a..130977c68 100644 --- a/src/client/graphics/layers/OptionsMenu.ts +++ b/src/client/graphics/layers/OptionsMenu.ts @@ -43,8 +43,8 @@ const secondsToHms = (d: number): string => { @customElement("options-menu") export class OptionsMenu extends LitElement implements Layer { - public game: GameView; - public eventBus: EventBus; + public game: GameView | undefined; + public eventBus: EventBus | undefined; private readonly userSettings: UserSettings = new UserSettings(); @state() @@ -68,12 +68,12 @@ export class OptionsMenu extends LitElement implements Layer { private onTerrainButtonClick() { this.alternateView = !this.alternateView; - this.eventBus.emit(new AlternateViewEvent(this.alternateView)); + this.eventBus?.emit(new AlternateViewEvent(this.alternateView)); this.requestUpdate(); } private onExitButtonClick() { - const isAlive = this.game.myPlayer()?.isAlive(); + const isAlive = this.game?.myPlayer()?.isAlive(); if (isAlive) { const isConfirmed = confirm( translateText("help_modal.exit_confirmation"), @@ -95,7 +95,7 @@ export class OptionsMenu extends LitElement implements Layer { private onPauseButtonClick() { this.isPaused = !this.isPaused; - this.eventBus.emit(new PauseGameEvent(this.isPaused)); + this.eventBus?.emit(new PauseGameEvent(this.isPaused)); } private onToggleEmojisButtonClick() { @@ -116,7 +116,7 @@ export class OptionsMenu extends LitElement implements Layer { private onToggleDarkModeButtonClick() { this.userSettings.toggleDarkMode(); this.requestUpdate(); - this.eventBus.emit(new RedrawGraphicsEvent()); + this.eventBus?.emit(new RedrawGraphicsEvent()); } private onToggleRandomNameModeButtonClick() { @@ -143,6 +143,7 @@ export class OptionsMenu extends LitElement implements Layer { } init() { + if (!this.game) throw new Error("Not initialzied"); console.log("init called from OptionsMenu"); this.showPauseButton = this.game.config().gameConfig().gameType === GameType.Singleplayer || @@ -152,6 +153,7 @@ export class OptionsMenu extends LitElement implements Layer { } tick() { + if (!this.game) throw new Error("Not initialzied"); const updates = this.game.updatesSinceLastTick(); if (updates) { this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0; diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index a2c18ef5a..0c97996a8 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -32,10 +32,11 @@ import { translateText } from "../../../client/Utils"; @customElement("player-panel") export class PlayerPanel extends LitElement implements Layer { - public g: GameView; - public eventBus: EventBus; - public emojiTable: EmojiTable; - public uiState: UIState; + public g: GameView | undefined; + public eventBus: EventBus | undefined; + public emojiTable: EmojiTable | undefined; + public uiState: UIState = { attackRatio: 0 }; + private ctModal: ChatModal | undefined; private actions: PlayerActions | null = null; private tile: TileRef | null = null; @@ -69,7 +70,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other)); + this.eventBus?.emit(new SendAllianceRequestIntentEvent(myPlayer, other)); this.hide(); } @@ -79,7 +80,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other)); + this.eventBus?.emit(new SendBreakAllianceIntentEvent(myPlayer, other)); this.hide(); } @@ -89,7 +90,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit( + this.eventBus?.emit( new SendDonateTroopsIntentEvent( other, myPlayer.troops() * this.uiState.attackRatio, @@ -104,7 +105,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendDonateGoldIntentEvent(other, null)); + this.eventBus?.emit(new SendDonateGoldIntentEvent(other, null)); this.hide(); } @@ -114,7 +115,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendEmbargoIntentEvent(other, "start")); + this.eventBus?.emit(new SendEmbargoIntentEvent(other, "start")); this.hide(); } @@ -124,38 +125,38 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendEmbargoIntentEvent(other, "stop")); + this.eventBus?.emit(new SendEmbargoIntentEvent(other, "stop")); this.hide(); } private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) { e.stopPropagation(); - this.emojiTable.showTable((emoji: string) => { + this.emojiTable?.showTable((emoji: string) => { if (myPlayer === other) { - this.eventBus.emit( + this.eventBus?.emit( new SendEmojiIntentEvent( AllPlayers, flattenedEmojiTable.indexOf(emoji), ), ); } else { - this.eventBus.emit( + this.eventBus?.emit( new SendEmojiIntentEvent(other, flattenedEmojiTable.indexOf(emoji)), ); } - this.emojiTable.hideTable(); + this.emojiTable?.hideTable(); this.hide(); }); } private handleChat(e: Event, sender: PlayerView, other: PlayerView) { - this.ctModal.open(sender, other); + this.ctModal?.open(sender, other); this.hide(); } private handleTargetClick(e: Event, other: PlayerView) { e.stopPropagation(); - this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id())); + this.eventBus?.emit(new SendTargetPlayerIntentEvent(other.id())); this.hide(); } @@ -163,8 +164,6 @@ export class PlayerPanel extends LitElement implements Layer { return this; } - private ctModal: ChatModal; - initEventBus(eventBus: EventBus) { this.eventBus = eventBus; eventBus.on(CloseViewEvent, (e) => { @@ -175,8 +174,8 @@ export class PlayerPanel extends LitElement implements Layer { } init() { - this.eventBus.on(MouseUpEvent, () => this.hide()); - this.eventBus.on(CloseViewEvent, (e) => { + this.eventBus?.on(MouseUpEvent, () => this.hide()); + this.eventBus?.on(CloseViewEvent, (e) => { this.hide(); }); @@ -184,28 +183,28 @@ export class PlayerPanel extends LitElement implements Layer { } async tick() { - if (this.isVisible && this.tile) { - const myPlayer = this.g.myPlayer(); - if (myPlayer !== null && myPlayer.isAlive()) { - this.actions = await myPlayer.actions(this.tile); + if (!this.g) return; + if (!this.isVisible) return; + if (!this.tile) return; + const myPlayer = this.g.myPlayer(); + if (!myPlayer?.isAlive()) return; + this.actions = await myPlayer.actions(this.tile); - if (this.actions?.interaction?.allianceExpiresAt !== undefined) { - const expiresAt = this.actions.interaction.allianceExpiresAt; - const remainingTicks = expiresAt - this.g.ticks(); + if (this.actions?.interaction?.allianceExpiresAt !== undefined) { + const expiresAt = this.actions.interaction.allianceExpiresAt; + const remainingTicks = expiresAt - this.g.ticks(); - if (remainingTicks > 0) { - const remainingSeconds = Math.max( - 0, - Math.floor(remainingTicks / 10), - ); // 10 ticks per second - this.allianceExpiryText = this.formatDuration(remainingSeconds); - } - } else { - this.allianceExpiryText = null; - } - this.requestUpdate(); + if (remainingTicks > 0) { + const remainingSeconds = Math.max( + 0, + Math.floor(remainingTicks / 10), + ); // 10 ticks per second + this.allianceExpiryText = this.formatDuration(remainingSeconds); } + } else { + this.allianceExpiryText = null; } + this.requestUpdate(); } private formatDuration(totalSeconds: number): string { @@ -222,6 +221,7 @@ export class PlayerPanel extends LitElement implements Layer { if (!this.isVisible) { return html``; } + if (this.g === undefined) return; const myPlayer = this.g.myPlayer(); if (myPlayer === null) return; if (this.tile === null) return; diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index a9e018b81..d9740d5ba 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -41,7 +41,7 @@ type CenterButtonState = "default" | "back"; type RequiredRadialMenuConfig = Required; export class RadialMenu implements Layer { - private menuElement: d3.Selection; + private menuElement: d3.Selection | undefined; private tooltipElement: HTMLDivElement | null = null; private isVisible = false; @@ -253,6 +253,7 @@ export class RadialMenu implements Layer { } private renderMenuItems(items: MenuElement[], level: number) { + if (this.menuElement === undefined) throw new Error("Not initialized"); const container = this.menuElement.select(".menu-container"); container.selectAll(`.menu-level-${level}`).remove(); @@ -645,6 +646,7 @@ export class RadialMenu implements Layer { } private animatePreviousMenu() { + if (this.menuElement === undefined) throw new Error("Not initialized"); const container = this.menuElement.select(".menu-container"); const currentMenu = container.select( `.menu-level-${this.currentLevel - 1}`, @@ -704,6 +706,7 @@ export class RadialMenu implements Layer { } private animateMenuTransitions() { + if (this.menuElement === undefined) throw new Error("Not initialized"); const container = this.menuElement.select(".menu-container"); const currentSubmenu = container.select( `.menu-level-${this.currentLevel + 1}`, @@ -768,6 +771,7 @@ export class RadialMenu implements Layer { this.anchorX = x; this.anchorY = y; + if (this.menuElement === undefined) throw new Error("Not initialized"); this.menuElement.style("display", "block"); this.clampAndSetMenuPositionForLevel(this.currentLevel); @@ -786,6 +790,7 @@ export class RadialMenu implements Layer { // Force transition state to false to ensure menu hides this.isTransitioning = false; + if (this.menuElement === undefined) throw new Error("Not initialized"); this.menuElement.style("display", "none"); this.isVisible = false; this.selectedItemId = null; @@ -826,6 +831,7 @@ export class RadialMenu implements Layer { } public updateCenterButtonState(state: CenterButtonState) { + if (this.menuElement === undefined) throw new Error("Not initialized"); this.centerButtonState = state; if (state === "back") { const backButtonSize = this.config.centerButtonSize * 0.8; // Make back button 20% smaller @@ -904,6 +910,7 @@ export class RadialMenu implements Layer { const scale = isHovering ? 1.2 : 1; + if (this.menuElement === undefined) throw new Error("Not initialized"); this.menuElement .select(".center-button-hitbox") .transition() @@ -1068,6 +1075,7 @@ export class RadialMenu implements Layer { const clampedX = 2 * margin > vw ? vw / 2 : Math.min(Math.max(this.anchorX, margin), vw - margin); const clampedY = 2 * margin > vh ? vh / 2 : Math.min(Math.max(this.anchorY, margin), vh - margin); + if (this.menuElement === undefined) throw new Error("Not initialized"); const svgSel = this.menuElement.select("svg"); svgSel .style("top", `${clampedY}px`) diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index e829c879d..d69494a36 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -19,8 +19,8 @@ type RailRef = { }; export class RailroadLayer implements Layer { - private canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D; + private canvas: HTMLCanvasElement | undefined; + private context: CanvasRenderingContext2D | undefined; private readonly theme: Theme; // Save the number of railroads per tiles. Delete when it reaches 0 private readonly existingRailroads = new Map(); @@ -90,6 +90,7 @@ export class RailroadLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.canvas === undefined) throw new Error("Not initialized"); this.updateRailColors(); context.drawImage( this.canvas, @@ -138,6 +139,7 @@ export class RailroadLayer implements Layer { if (!ref || ref.numOccurence <= 0) { this.existingRailroads.delete(railRoad.tile); this.railTileList = this.railTileList.filter((t) => t !== railRoad.tile); + if (this.context === undefined) throw new Error("Not initialized"); this.context.clearRect( this.game.x(railRoad.tile) * 2 - 1, this.game.y(railRoad.tile) * 2 - 1, @@ -155,11 +157,13 @@ export class RailroadLayer implements Layer { const color = recipient ? this.theme.railroadColor(recipient) : new Colord({ r: 255, g: 255, b: 255, a: 1 }); + if (this.context === undefined) throw new Error("Not initialized"); this.context.fillStyle = color.toRgbString(); this.paintRailRects(x, y, railRoad.railType); } private paintRailRects(x: number, y: number, direction: RailType) { + if (this.context === undefined) throw new Error("Not initialized"); const railRects = getRailroadRects(direction); for (const [dx, dy, w, h] of railRects) { this.context.fillRect(x * 2 + dx, y * 2 + dy, w, h); diff --git a/src/client/graphics/layers/SettingsModal.ts b/src/client/graphics/layers/SettingsModal.ts index 5d0fe60d4..54efb6493 100644 --- a/src/client/graphics/layers/SettingsModal.ts +++ b/src/client/graphics/layers/SettingsModal.ts @@ -26,8 +26,8 @@ export class ShowSettingsModalEvent { @customElement("settings-modal") export class SettingsModal extends LitElement implements Layer { - public eventBus: EventBus; - public userSettings: UserSettings; + public eventBus: EventBus | undefined; + public userSettings: UserSettings | undefined; @state() private isVisible = false; @@ -45,6 +45,7 @@ export class SettingsModal extends LitElement implements Layer { wasPausedWhenOpened = false; init() { + if (this.eventBus === undefined) throw new Error("Not initialized"); this.eventBus.on(ShowSettingsModalEvent, (event) => { this.isVisible = event.isVisible; this.shouldPause = event.shouldPause; @@ -100,48 +101,48 @@ export class SettingsModal extends LitElement implements Layer { private pauseGame(pause: boolean) { if (this.shouldPause && !this.wasPausedWhenOpened) - this.eventBus.emit(new PauseGameEvent(pause)); + this.eventBus?.emit(new PauseGameEvent(pause)); } private onTerrainButtonClick() { this.alternateView = !this.alternateView; - this.eventBus.emit(new AlternateViewEvent(this.alternateView)); + this.eventBus?.emit(new AlternateViewEvent(this.alternateView)); this.requestUpdate(); } private onToggleEmojisButtonClick() { - this.userSettings.toggleEmojis(); + this.userSettings?.toggleEmojis(); this.requestUpdate(); } private onToggleStructureSpritesButtonClick() { - this.userSettings.toggleStructureSprites(); + this.userSettings?.toggleStructureSprites(); this.requestUpdate(); } private onToggleSpecialEffectsButtonClick() { - this.userSettings.toggleFxLayer(); + this.userSettings?.toggleFxLayer(); this.requestUpdate(); } private onToggleDarkModeButtonClick() { - this.userSettings.toggleDarkMode(); - this.eventBus.emit(new RedrawGraphicsEvent()); + this.userSettings?.toggleDarkMode(); + this.eventBus?.emit(new RedrawGraphicsEvent()); this.requestUpdate(); } private onToggleRandomNameModeButtonClick() { - this.userSettings.toggleRandomName(); + this.userSettings?.toggleRandomName(); this.requestUpdate(); } private onToggleLeftClickOpensMenu() { - this.userSettings.toggleLeftClickOpenMenu(); + this.userSettings?.toggleLeftClickOpenMenu(); this.requestUpdate(); } private onTogglePerformanceOverlayButtonClick() { - this.userSettings.togglePerformanceOverlay(); + this.userSettings?.togglePerformanceOverlay(); this.requestUpdate(); } @@ -221,13 +222,13 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.emojis_label")}
- ${this.userSettings.emojis() + ${this.userSettings?.emojis() ? translateText("user_setting.emojis_visible") : translateText("user_setting.emojis_hidden")}
- ${this.userSettings.emojis() + ${this.userSettings?.emojis() ? translateText("user_setting.on") : translateText("user_setting.off")}
@@ -249,13 +250,13 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.dark_mode_label")}
- ${this.userSettings.darkMode() + ${this.userSettings?.darkMode() ? translateText("user_setting.dark_mode_enabled") : translateText("user_setting.light_mode_enabled")}
- ${this.userSettings.darkMode() + ${this.userSettings?.darkMode() ? translateText("user_setting.on") : translateText("user_setting.off")}
@@ -277,13 +278,13 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.special_effects_label")}
- ${this.userSettings.fxLayer() + ${this.userSettings?.fxLayer() ? translateText("user_setting.special_effects_enabled") : translateText("user_setting.special_effects_disabled")}
- ${this.userSettings.fxLayer() + ${this.userSettings?.fxLayer() ? translateText("user_setting.on") : translateText("user_setting.off")}
@@ -305,13 +306,13 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.structure_sprites_label")}
- ${this.userSettings.structureSprites() + ${this.userSettings?.structureSprites() ? translateText("user_setting.structure_sprites_enabled") : translateText("user_setting.structure_sprites_disabled")}
- ${this.userSettings.structureSprites() + ${this.userSettings?.structureSprites() ? translateText("user_setting.on") : translateText("user_setting.off")}
@@ -328,13 +329,13 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.anonymous_names_label")}
- ${this.userSettings.anonymousNames() + ${this.userSettings?.anonymousNames() ? translateText("user_setting.anonymous_names_enabled") : translateText("user_setting.real_names_shown")}
- ${this.userSettings.anonymousNames() + ${this.userSettings?.anonymousNames() ? translateText("user_setting.on") : translateText("user_setting.off")}
@@ -351,13 +352,13 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.left_click_menu")}
- ${this.userSettings.leftClickOpensMenu() + ${this.userSettings?.leftClickOpensMenu() ? translateText("user_setting.left_click_opens_menu") : translateText("user_setting.right_click_opens_menu")}
- ${this.userSettings.leftClickOpensMenu() + ${this.userSettings?.leftClickOpensMenu() ? translateText("user_setting.on") : translateText("user_setting.off")}
@@ -379,7 +380,7 @@ export class SettingsModal extends LitElement implements Layer { ${translateText("user_setting.performance_overlay_label")}
- ${this.userSettings.performanceOverlay() + ${this.userSettings?.performanceOverlay() ? translateText("user_setting.performance_overlay_enabled") : translateText( "user_setting.performance_overlay_disabled", @@ -387,7 +388,7 @@ export class SettingsModal extends LitElement implements Layer {
- ${this.userSettings.performanceOverlay() + ${this.userSettings?.performanceOverlay() ? translateText("user_setting.on") : translateText("user_setting.off")}
diff --git a/src/client/graphics/layers/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts index 8e9c49da1..960260af5 100644 --- a/src/client/graphics/layers/SpawnAd.ts +++ b/src/client/graphics/layers/SpawnAd.ts @@ -10,7 +10,7 @@ const AD_CONTAINER_ID = "bottom-rail-ad-container"; @customElement("spawn-ad") export class SpawnAd extends LitElement implements Layer { - public g: GameView; + public g: GameView | undefined; @state() private isVisible = false; @@ -50,6 +50,7 @@ export class SpawnAd extends LitElement implements Layer { } public async tick() { + if (!this.g) return; if ( !this.isVisible && this.g.inSpawnPhase() && diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 195fdd3b9..71f6d1ebd 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -54,14 +54,14 @@ const ICON_SIZE = { const OFFSET_ZOOM_Y = 4; // offset for the y position of the level over the sprite export class StructureIconsLayer implements Layer { - private pixicanvas: HTMLCanvasElement; - private iconsStage: PIXI.Container; - private levelsStage: PIXI.Container; - private dotsStage: PIXI.Container; + private pixicanvas: HTMLCanvasElement | undefined; + private iconsStage: PIXI.Container | undefined; + private levelsStage: PIXI.Container | undefined; + private dotsStage: PIXI.Container | undefined; private shouldRedraw = true; private readonly textureCache: Map = new Map(); private readonly theme: Theme; - private renderer: PIXI.Renderer; + private renderer: PIXI.Renderer | undefined; private renders: StructureRenderInfo[] = []; private readonly seenUnits: Set = new Set(); private readonly structures: Map< @@ -163,7 +163,7 @@ export class StructureIconsLayer implements Layer { } resizeCanvas() { - if (this.renderer) { + if (this.renderer && this.pixicanvas) { this.pixicanvas.width = window.innerWidth; this.pixicanvas.height = window.innerHeight; this.renderer.resize(innerWidth, innerHeight, 1); @@ -322,10 +322,13 @@ export class StructureIconsLayer implements Layer { if (this.transformHandler.hasChanged() || this.shouldRedraw) { if (this.transformHandler.scale > ZOOM_THRESHOLD && this.renderSprites) { + if (this.levelsStage === undefined) throw new Error("Not initialized"); this.renderer.render(this.levelsStage); } else if (this.transformHandler.scale > DOTS_ZOOM_THRESHOLD) { + if (this.iconsStage === undefined) throw new Error("Not initialized"); this.renderer.render(this.iconsStage); } else { + if (this.dotsStage === undefined) throw new Error("Not initialized"); this.renderer.render(this.dotsStage); } this.shouldRedraw = false; @@ -504,6 +507,7 @@ export class StructureIconsLayer implements Layer { } private createLevelSprite(unit: UnitView): PIXI.Container { + if (this.levelsStage === undefined) throw new Error("Not initialized"); return this.createUnitContainer(unit, { type: "level", stage: this.levelsStage, @@ -511,6 +515,7 @@ export class StructureIconsLayer implements Layer { } private createDotSprite(unit: UnitView): PIXI.Container { + if (this.dotsStage === undefined) throw new Error("Not initialized"); return this.createUnitContainer(unit, { type: "dot", stage: this.dotsStage, @@ -518,6 +523,7 @@ export class StructureIconsLayer implements Layer { } private createIconSprite(unit: UnitView): PIXI.Container { + if (this.iconsStage === undefined) throw new Error("Not initialized"); return this.createUnitContainer(unit, { type: "icon", stage: this.iconsStage, @@ -633,6 +639,7 @@ export class StructureIconsLayer implements Layer { ? ICON_SIZE[STRUCTURE_SHAPES[type]] : 28; + if (this.pixicanvas === undefined) throw new Error("Not initialized"); const onScreen = screenPos.x + margin > 0 && screenPos.x - margin < this.pixicanvas.width && diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 623bda2df..b02d21402 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -29,8 +29,8 @@ type UnitRenderConfig = { }; export class StructureLayer implements Layer { - private canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D; + private canvas: HTMLCanvasElement | undefined; + private context: CanvasRenderingContext2D | undefined; private readonly unitIcons: Map = new Map(); private readonly theme: Theme; private readonly tempCanvas: HTMLCanvasElement; @@ -145,6 +145,7 @@ export class StructureLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.canvas === undefined) throw new Error("Not initialized"); if ( this.transformHandler.scale <= ZOOM_THRESHOLD || !this.game.config().userSettings()?.structureSprites() @@ -264,16 +265,19 @@ export class StructureLayer implements Layer { this.tempContext.drawImage(image, 0, 0, width * 2, height * 2); // Draw the final result to the main canvas + if (this.context === undefined) throw new Error("Not initialized"); this.context.drawImage(this.tempCanvas, startX * 2, startY * 2); } paintCell(cell: Cell, color: Colord, alpha: number) { this.clearCell(cell); + if (this.context === undefined) throw new Error("Not initialized"); this.context.fillStyle = color.alpha(alpha / 255).toRgbString(); this.context.fillRect(cell.x * 2, cell.y * 2, 2, 2); } clearCell(cell: Cell) { + if (this.context === undefined) throw new Error("Not initialized"); this.context.clearRect(cell.x * 2, cell.y * 2, 2, 2); } } diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts index fea4d9c67..8d3830633 100644 --- a/src/client/graphics/layers/TeamStats.ts +++ b/src/client/graphics/layers/TeamStats.ts @@ -21,8 +21,8 @@ type TeamEntry = { @customElement("team-stats") export class TeamStats extends LitElement implements Layer { - public game: GameView; - public eventBus: EventBus; + public game: GameView | undefined; + public eventBus: EventBus | undefined; @property({ type: Boolean }) visible = false; teams: TeamEntry[] = []; @@ -36,6 +36,7 @@ export class TeamStats extends LitElement implements Layer { init() {} tick() { + if (this.game === undefined) throw new Error("Not initialized"); if (this.game.config().gameConfig().gameMode !== GameMode.Team) return; if (!this._shownOnInit && !this.game.inSpawnPhase()) { @@ -51,6 +52,7 @@ export class TeamStats extends LitElement implements Layer { } private updateTeamStats() { + if (this.game === undefined) throw new Error("Not initialized"); const players = this.game.playerViews(); const grouped: Record = {}; @@ -83,6 +85,7 @@ export class TeamStats extends LitElement implements Layer { } } + if (this.game === undefined) throw new Error("Not initialized"); const totalScorePercent = totalScoreSort / this.game.numLandTiles(); return { diff --git a/src/client/graphics/layers/TerrainLayer.ts b/src/client/graphics/layers/TerrainLayer.ts index 51ba72f2d..e5d4d6b99 100644 --- a/src/client/graphics/layers/TerrainLayer.ts +++ b/src/client/graphics/layers/TerrainLayer.ts @@ -4,10 +4,10 @@ import { Theme } from "../../../core/configuration/Config"; import { TransformHandler } from "../TransformHandler"; export class TerrainLayer implements Layer { - private canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D; - private imageData: ImageData; - private theme: Theme; + private canvas: HTMLCanvasElement | undefined; + private context: CanvasRenderingContext2D | undefined; + private imageData: ImageData | undefined; + private theme: Theme | undefined; constructor( private readonly game: GameView, @@ -48,7 +48,8 @@ export class TerrainLayer implements Layer { initImageData() { this.theme = this.game.config().theme(); this.game.forEachTile((tile) => { - const terrainColor = this.theme.terrainColor(this.game, tile); + const terrainColor = this.theme?.terrainColor(this.game, tile); + if (terrainColor === undefined || this.imageData === undefined) return; // TODO: isn'te tileref and index the same? const index = this.game.y(tile) * this.game.width() + this.game.x(tile); const offset = index * 4; @@ -66,6 +67,7 @@ export class TerrainLayer implements Layer { } else { context.imageSmoothingEnabled = false; } + if (this.canvas === undefined) throw new Error("Not initialized"); context.drawImage( this.canvas, -this.game.width() / 2, diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 42a9f0969..58d77526b 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -19,10 +19,10 @@ import { UserSettings } from "../../../core/game/UserSettings"; export class TerritoryLayer implements Layer { private readonly userSettings: UserSettings; - private canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D; - private imageData: ImageData; - private alternativeImageData: ImageData; + private canvas: HTMLCanvasElement | undefined; + private context: CanvasRenderingContext2D | undefined; + private imageData: ImageData | undefined; + private alternativeImageData: ImageData | undefined; private cachedTerritoryPatternsEnabled: boolean | undefined; @@ -36,8 +36,8 @@ export class TerritoryLayer implements Layer { private readonly theme: Theme; // Used for spawn highlighting - private highlightCanvas: HTMLCanvasElement; - private highlightContext: CanvasRenderingContext2D; + private highlightCanvas: HTMLCanvasElement | undefined; + private highlightContext: CanvasRenderingContext2D | undefined; private highlightedTerritory: PlayerView | null = null; @@ -158,7 +158,7 @@ export class TerritoryLayer implements Layer { return; } - this.highlightContext.clearRect( + this.highlightContext?.clearRect( 0, 0, this.game.width(), @@ -322,6 +322,8 @@ export class TerritoryLayer implements Layer { initImageData() { this.game.forEachTile((tile) => { + if (this.imageData === undefined) throw new Error("Not initialized"); + if (this.alternativeImageData === undefined) throw new Error("Not initialized"); const cell = new Cell(this.game.x(tile), this.game.y(tile)); const index = cell.y * this.game.width() + cell.x; const offset = index * 4; @@ -331,6 +333,11 @@ export class TerritoryLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.canvas === undefined) throw new Error("Not initialized"); + if (this.highlightCanvas === undefined) throw new Error("Not initialized"); + if (this.context === undefined) throw new Error("Not initialized"); + if (this.imageData === undefined) throw new Error("Not initialized"); + if (this.alternativeImageData === undefined) throw new Error("Not initialized"); const now = Date.now(); if ( now > this.lastDragTime + this.nodrawDragDuration && @@ -405,6 +412,8 @@ export class TerritoryLayer implements Layer { if (isBorder && !this.game.hasOwner(tile)) { return; } + if (this.imageData === undefined) throw new Error("Not initialized"); + if (this.alternativeImageData === undefined) throw new Error("Not initialized"); if (!this.game.hasOwner(tile)) { if (this.game.hasFallout(tile)) { @@ -500,6 +509,7 @@ export class TerritoryLayer implements Layer { } paintAlternateViewTile(tile: TileRef, other: PlayerView) { + if (this.alternativeImageData === undefined) throw new Error("Not initialized"); const color = this.alternateViewColor(other); this.paintTile(this.alternativeImageData, tile, color, 255); } @@ -514,12 +524,15 @@ export class TerritoryLayer implements Layer { clearTile(tile: TileRef) { const offset = tile * 4; + if (this.imageData === undefined) throw new Error("Not initialized"); + if (this.alternativeImageData === undefined) throw new Error("Not initialized"); this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) } clearAlternativeTile(tile: TileRef) { const offset = tile * 4; + if (this.alternativeImageData === undefined) throw new Error("Not initialized"); this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) } @@ -541,6 +554,7 @@ export class TerritoryLayer implements Layer { this.clearTile(tile); const x = this.game.x(tile); const y = this.game.y(tile); + if (this.highlightContext === undefined) throw new Error("Not initialized"); this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString(); this.highlightContext.fillRect(x, y, 1, 1); } @@ -548,6 +562,7 @@ export class TerritoryLayer implements Layer { clearHighlightTile(tile: TileRef) { const x = this.game.x(tile); const y = this.game.y(tile); + if (this.highlightContext === undefined) throw new Error("Not initialized"); this.highlightContext.clearRect(x, y, 1, 1); } } diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 9f1d8c75b..bfaa63b16 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -25,10 +25,9 @@ const PROGRESSBAR_HEIGHT = 3; // Height of a bar * such as selection boxes, health bars, etc. */ export class UILayer implements Layer { - private canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D | null; + private canvas: HTMLCanvasElement | undefined; + private context: CanvasRenderingContext2D | null = null; private readonly theme: Theme | null = null; - private readonly userSettings: UserSettings = new UserSettings(); private selectionAnimTime = 0; private readonly allProgressBars: Map< number, @@ -51,7 +50,6 @@ export class UILayer implements Layer { constructor( private readonly game: GameView, private readonly eventBus: EventBus, - private readonly transformHandler: TransformHandler, ) { this.theme = game.config().theme(); } @@ -71,7 +69,8 @@ export class UILayer implements Layer { this.game .updatesSinceLastTick() - ?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id)) + ?.[GameUpdateType.Unit] + ?.map((unit) => this.game.unit(unit.id)) ?.forEach((unitView) => { if (unitView === undefined) return; this.onUnitEvent(unitView); @@ -85,6 +84,7 @@ export class UILayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.canvas === undefined) throw new Error("Not initialized"); context.drawImage( this.canvas, -this.game.width() / 2, diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts index 5dfbd319b..2876d1e9a 100644 --- a/src/client/graphics/layers/UnitDisplay.ts +++ b/src/client/graphics/layers/UnitDisplay.ts @@ -15,8 +15,8 @@ import samLauncherIcon from "../../../../resources/non-commercial/svg/SamLaunche @customElement("unit-display") export class UnitDisplay extends LitElement implements Layer { - public game: GameView; - public eventBus: EventBus; + public game: GameView | undefined; + public eventBus: EventBus | undefined; private readonly _selectedStructure: UnitType | null = null; private _cities = 0; private _factories = 0; @@ -31,6 +31,7 @@ export class UnitDisplay extends LitElement implements Layer { } init() { + if (this.game === undefined) throw new Error("Not initialized"); const config = this.game.config(); this.allDisabled = config.isUnitDisabled(UnitType.City) && @@ -60,6 +61,7 @@ export class UnitDisplay extends LitElement implements Layer { unitType: UnitType, altText: string, ) { + if (this.game === undefined) throw new Error("Not initialized"); if (this.game.config().isUnitDisabled(unitType)) { return html``; } @@ -71,9 +73,9 @@ export class UnitDisplay extends LitElement implements Layer { ? "#ffffff2e" : "none"}" @mouseenter="${() => - this.eventBus.emit(new ToggleStructureEvent(unitType))}" + this.eventBus?.emit(new ToggleStructureEvent(unitType))}" @mouseleave="${() => - this.eventBus.emit(new ToggleStructureEvent(null))}" + this.eventBus?.emit(new ToggleStructureEvent(null))}" > (); @@ -156,6 +156,8 @@ export class UnitLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.transportShipTrailCanvas === undefined) throw new Error("Not initialized"); + if (this.canvas === undefined) throw new Error("Not initialized"); context.drawImage( this.transportShipTrailCanvas, -this.game.width() / 2, @@ -195,6 +197,7 @@ export class UnitLayer implements Layer { this.updateUnitsSprites(this.game.units().map((unit) => unit.id())); this.unitToTrail.forEach((trail, unit) => { + if (this.unitTrailContext === undefined) throw new Error("Not initialized"); for (const t of trail) { this.paintCell( this.game.x(t), @@ -225,6 +228,7 @@ export class UnitLayer implements Layer { unitViews .filter((unitView) => isSpriteReady(unitView)) .forEach((unitView) => { + if (this.context === undefined) throw new Error("Not initialized"); const sprite = getColoredSprite(unitView, this.theme); const clearsize = sprite.width + 1; const lastX = this.game.x(unitView.lastTile()); @@ -301,13 +305,14 @@ export class UnitLayer implements Layer { } private handleShellEvent(unit: UnitView) { + if (this.context === undefined) throw new Error("Not initialized"); const rel = this.relationship(unit); // Clear current and previous positions - this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile())); + this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()), this.context); const oldTile = this.oldShellTile.get(unit); if (oldTile !== undefined) { - this.clearCell(this.game.x(oldTile), this.game.y(oldTile)); + this.clearCell(this.game.x(oldTile), this.game.y(oldTile), this.context); } this.oldShellTile.set(unit, unit.lastTile()); @@ -322,6 +327,7 @@ export class UnitLayer implements Layer { rel, this.theme.borderColor(unit.owner()), 255, + this.context, ); this.paintCell( this.game.x(unit.lastTile()), @@ -329,6 +335,7 @@ export class UnitLayer implements Layer { rel, this.theme.borderColor(unit.owner()), 255, + this.context, ); } @@ -338,6 +345,7 @@ export class UnitLayer implements Layer { } private drawTrail(trail: number[], color: Colord, rel: Relationship) { + if (this.unitTrailContext === undefined) throw new Error("Not initialized"); // Paint new trail for (const t of trail) { this.paintCell( @@ -352,6 +360,7 @@ export class UnitLayer implements Layer { } private clearTrail(unit: UnitView) { + if (this.unitTrailContext === undefined) throw new Error("Not initialized"); const trail = this.unitToTrail.get(unit) ?? []; const rel = this.relationship(unit); for (const t of trail) { @@ -360,6 +369,7 @@ export class UnitLayer implements Layer { this.unitToTrail.delete(unit); // Repaint overlapping trails + if (this.unitTrailContext === undefined) throw new Error("Not initialized"); const trailSet = new Set(trail); for (const [other, trail] of this.unitToTrail) { for (const t of trail) { @@ -421,7 +431,8 @@ export class UnitLayer implements Layer { private handleMIRVWarhead(unit: UnitView) { const rel = this.relationship(unit); - this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile())); + if (this.context === undefined) throw new Error("Not initialized"); + this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()), this.context); if (unit.isActive()) { // Paint area @@ -431,6 +442,7 @@ export class UnitLayer implements Layer { rel, this.theme.borderColor(unit.owner()), 255, + this.context, ); } } @@ -471,7 +483,7 @@ export class UnitLayer implements Layer { relationship: Relationship, color: Colord, alpha: number, - context: CanvasRenderingContext2D = this.context, + context: CanvasRenderingContext2D, ) { this.clearCell(x, y, context); if (this.alternateView) { @@ -495,7 +507,7 @@ export class UnitLayer implements Layer { clearCell( x: number, y: number, - context: CanvasRenderingContext2D = this.context, + context: CanvasRenderingContext2D, ) { context.clearRect(x, y, 1, 1); } @@ -542,6 +554,7 @@ export class UnitLayer implements Layer { if (unit.isActive()) { const targetable = unit.targetable(); + if (this.context === undefined) throw new Error("Not initialized"); if (!targetable) { this.context.save(); this.context.globalAlpha = 0.5; diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index ef465378c..1f8605641 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -10,8 +10,8 @@ import { translateText } from "../../../client/Utils"; @customElement("win-modal") export class WinModal extends LitElement implements Layer { - public game: GameView; - public eventBus: EventBus; + public game: GameView | undefined; + public eventBus: EventBus | undefined; private hasShownDeathModal = false; @@ -21,7 +21,7 @@ export class WinModal extends LitElement implements Layer { @state() showButtons = false; - private _title: string; + private _title = ""; // Override to prevent shadow DOM creation createRenderRoot() { @@ -137,7 +137,7 @@ export class WinModal extends LitElement implements Layer { render() { return html`
-

${this._title || ""}

+

${this._title}

${this.innerHtml()}
{ this.isVisible = true; this.requestUpdate(); @@ -187,7 +187,7 @@ export class WinModal extends LitElement implements Layer { } hide() { - this.eventBus.emit(new GutterAdModalEvent(false)); + this.eventBus?.emit(new GutterAdModalEvent(false)); this.isVisible = false; this.showButtons = false; this.requestUpdate(); @@ -201,6 +201,7 @@ export class WinModal extends LitElement implements Layer { init() {} tick() { + if (this.game === undefined) throw new Error("Not initialized"); const myPlayer = this.game.myPlayer(); if ( !this.hasShownDeathModal && @@ -216,10 +217,11 @@ export class WinModal extends LitElement implements Layer { const updates = this.game.updatesSinceLastTick(); const winUpdates = updates !== null ? updates[GameUpdateType.Win] : []; winUpdates.forEach((wu) => { + if (this.game === undefined) return; if (wu.winner === undefined) { // ... } else if (wu.winner[0] === "team") { - this.eventBus.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats)); + this.eventBus?.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats)); if (wu.winner[1] === this.game.myPlayer()?.team()) { this._title = translateText("win_modal.your_team"); } else { @@ -233,7 +235,7 @@ export class WinModal extends LitElement implements Layer { if (!winner?.isPlayer()) return; const winnerClient = winner.clientID(); if (winnerClient !== null) { - this.eventBus.emit( + this.eventBus?.emit( new SendWinnerEvent(["player", winnerClient], wu.allPlayersStats), ); } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index a531bb2eb..ba7ba83b6 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -100,7 +100,7 @@ export abstract class DefaultServerConfig implements ServerConfig { return process.env.CF_CREDS_PATH ?? ""; } - private publicKey: JWK; + private publicKey: JWK | undefined; abstract jwtAudience(): string; jwtIssuer(): string { const audience = this.jwtAudience(); diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 6dc11d425..44bddccda 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -23,9 +23,9 @@ export class AttackExecution implements Execution { private readonly random = new PseudoRandom(123); - private target: Player | TerraNullius; + private target: Player | TerraNullius | undefined; - private mg: Game; + private mg: Game | undefined; private attack: Attack | null = null; @@ -171,6 +171,9 @@ export class AttackExecution implements Execution { } private retreat(malusPercent = 0) { + if (this.mg === undefined) { + throw new Error("Attack not initialized"); + } if (this.attack === null) { throw new Error("Attack not initialized"); } @@ -189,22 +192,27 @@ export class AttackExecution implements Execution { this.active = false; // Not all retreats are canceled attacks - if (this.attack.retreated()) { + if (this.attack.retreated() && this.target && this.target.isPlayer()) { // Record stats this.mg.stats().attackCancel(this._owner, this.target, survivors); } } tick(ticks: number) { + if (this.mg === undefined) { + throw new Error("Attack not initialized"); + } + if (this.target === undefined) { + throw new Error("Attack not initialized"); + } if (this.attack === null) { throw new Error("Attack not initialized"); } let troopCount = this.attack.troops(); // cache troop count - const targetIsPlayer = this.target.isPlayer(); // cache target type - const targetPlayer = targetIsPlayer ? (this.target as Player) : null; // cache target player + const targetPlayer: Player | null = this.target.isPlayer() ? this.target : null; // cache target player if (this.attack.retreated()) { - if (targetIsPlayer) { + if (targetPlayer !== null) { this.retreat(malusForRetreat); } else { this.retreat(); @@ -222,8 +230,8 @@ export class AttackExecution implements Execution { return; } - const alliance = targetPlayer - ? this._owner.allianceWith(targetPlayer) + const alliance = this.target && this.target.isPlayer() + ? this._owner.allianceWith(this.target) : null; if (this.breakAlliance && alliance !== null) { this.breakAlliance = false; @@ -309,6 +317,9 @@ export class AttackExecution implements Execution { if (this.attack === null) { throw new Error("Attack not initialized"); } + if (this.mg === undefined) { + throw new Error("Attack not initialized"); + } const tickNow = this.mg.ticks(); // cache tick @@ -349,6 +360,8 @@ export class AttackExecution implements Execution { } private handleDeadDefender() { + if (!this.mg) return; + if (!this.target) return; if (!(this.target.isPlayer() && this.target.numTilesOwned() < 100)) return; this.mg.conquerPlayer(this._owner, this.target); @@ -357,7 +370,7 @@ export class AttackExecution implements Execution { for (const tile of this.target.tiles()) { const borders = this.mg .neighbors(tile) - .some((t) => this.mg.owner(t) === this._owner); + .some((t) => this.mg?.owner(t) === this._owner); if (borders) { this._owner.conquer(tile); } else { diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index f2557c4d1..75d843632 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -6,7 +6,7 @@ import { simpleHash } from "../Util"; export class BotExecution implements Execution { private active = true; private readonly random: PseudoRandom; - private mg: Game; + private mg: Game | undefined; private neighborsTerraNullius = true; private behavior: BotBehavior | null = null; @@ -42,6 +42,7 @@ export class BotExecution implements Execution { } if (this.behavior === null) { + if (this.mg === undefined) throw new Error("Not initialized"); this.behavior = new BotBehavior( this.random, this.mg, @@ -63,7 +64,7 @@ export class BotExecution implements Execution { private maybeAttack() { if (this.behavior === null) { - throw new Error("not initialized"); + throw new Error("Not initialized"); } const toAttack = this.behavior.getNeighborTraitorToAttack(); if (toAttack !== null) { @@ -75,6 +76,7 @@ export class BotExecution implements Execution { } if (this.neighborsTerraNullius) { + if (this.mg === undefined) throw new Error("Not initialized"); if (this.bot.sharesBorderWith(this.mg.terraNullius())) { this.behavior.sendAttack(this.mg.terraNullius()); return; diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts index 93635258c..863ba716e 100644 --- a/src/core/execution/CityExecution.ts +++ b/src/core/execution/CityExecution.ts @@ -3,7 +3,7 @@ import { TileRef } from "../game/GameMap"; import { TrainStationExecution } from "./TrainStationExecution"; export class CityExecution implements Execution { - private mg: Game; + private mg: Game | undefined; private city: Unit | null = null; private active = true; @@ -47,6 +47,7 @@ export class CityExecution implements Execution { createStation(): void { if (this.city !== null) { + if (this.mg === undefined) throw new Error("Not initialized"); const nearbyFactory = this.mg.hasUnitNearby( this.city.tile(), this.mg.config().trainStationMaxRange(), diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index 226af4e9d..033505ff4 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -21,11 +21,11 @@ import { WarshipExecution } from "./WarshipExecution"; export class ConstructionExecution implements Execution { private construction: Unit | null = null; private active = true; - private mg: Game; + private mg: Game | undefined; - private ticksUntilComplete: Tick; + private ticksUntilComplete: Tick | undefined; - private cost: Gold; + private cost: Gold | undefined; constructor( private player: Player, @@ -53,6 +53,7 @@ export class ConstructionExecution implements Execution { tick(ticks: number): void { if (this.construction === null) { + if (this.mg === undefined) throw new Error("Not initialized"); const info = this.mg.unitInfo(this.constructionType); if (info.constructionDuration === undefined) { this.completeConstruction(); @@ -90,15 +91,18 @@ export class ConstructionExecution implements Execution { this.player = this.construction.owner(); this.construction.delete(false); // refund the cost so player has the gold to build the unit + if (this.cost === undefined) throw new Error("Not initialized"); this.player.addGold(this.cost); this.completeConstruction(); this.active = false; return; } + if (this.ticksUntilComplete === undefined) throw new Error("Not initialized"); this.ticksUntilComplete--; } private completeConstruction() { + if (this.mg === undefined) throw new Error("Not initialized"); const { player } = this; switch (this.constructionType) { case UnitType.AtomBomb: diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index 33b4776bc..62243c3fc 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -3,7 +3,7 @@ import { ShellExecution } from "./ShellExecution"; import { TileRef } from "../game/GameMap"; export class DefensePostExecution implements Execution { - private mg: Game; + private mg: Game | undefined; private post: Unit | null = null; private active = true; @@ -24,6 +24,7 @@ export class DefensePostExecution implements Execution { private shoot() { if (this.post === null) return; if (this.target === null) return; + if (this.mg === undefined) throw new Error("Not initialized"); const shellAttackRate = this.mg.config().defensePostShellAttackRate(); if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { this.lastShellAttack = this.mg.ticks(); diff --git a/src/core/execution/DeleteUnitExecution.ts b/src/core/execution/DeleteUnitExecution.ts index 20e7db02d..996578f43 100644 --- a/src/core/execution/DeleteUnitExecution.ts +++ b/src/core/execution/DeleteUnitExecution.ts @@ -2,7 +2,7 @@ import { Execution, Game, MessageType, Player } from "../game/Game"; export class DeleteUnitExecution implements Execution { private active = true; - private mg: Game; + private mg: Game | undefined; constructor( private readonly player: Player, diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index e0b64db75..1f9a8ba52 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -1,7 +1,7 @@ import { Execution, Game, Gold, Player, PlayerID } from "../game/Game"; export class DonateGoldExecution implements Execution { - private recipient: Player; + private recipient: Player | undefined; private active = true; @@ -23,7 +23,8 @@ export class DonateGoldExecution implements Execution { } tick(ticks: number): void { - if (this.gold === null) throw new Error("not initialized"); + if (this.gold === null) throw new Error("Not initialized"); + if (this.recipient === undefined) throw new Error("Not initialized"); if ( this.sender.canDonateGold(this.recipient) && this.sender.donateGold(this.recipient, this.gold) diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 424dd8809..26d0ab14b 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -1,7 +1,7 @@ import { Execution, Game, Player, PlayerID } from "../game/Game"; export class DonateTroopsExecution implements Execution { - private recipient: Player; + private recipient: Player | undefined; private active = true; @@ -26,7 +26,8 @@ export class DonateTroopsExecution implements Execution { } tick(ticks: number): void { - if (this.troops === null) throw new Error("not initialized"); + if (this.troops === null) throw new Error("Not initialized"); + if (this.recipient === undefined) throw new Error("Not initialized"); if ( this.sender.canDonateTroops(this.recipient) && this.sender.donateTroops(this.recipient, this.troops) diff --git a/src/core/execution/EmbargoExecution.ts b/src/core/execution/EmbargoExecution.ts index f4d8ed557..c24a4aa75 100644 --- a/src/core/execution/EmbargoExecution.ts +++ b/src/core/execution/EmbargoExecution.ts @@ -3,7 +3,7 @@ import { Execution, Game, Player, PlayerID } from "../game/Game"; export class EmbargoExecution implements Execution { private active = true; - private target: Player; + private target: Player | undefined; constructor( private readonly player: Player, @@ -21,6 +21,7 @@ export class EmbargoExecution implements Execution { } tick(_: number): void { + if (this.target === undefined) throw new Error("Not initialized"); if (this.action === "start") this.player.addEmbargo(this.target, false); else this.player.stopEmbargo(this.target); diff --git a/src/core/execution/EmojiExecution.ts b/src/core/execution/EmojiExecution.ts index cbd15f6c1..37bd4fc3e 100644 --- a/src/core/execution/EmojiExecution.ts +++ b/src/core/execution/EmojiExecution.ts @@ -9,7 +9,7 @@ import { import { flattenedEmojiTable } from "../Util"; export class EmojiExecution implements Execution { - private recipient: Player | typeof AllPlayers; + private recipient: Player | typeof AllPlayers | undefined; private active = true; @@ -33,6 +33,7 @@ export class EmojiExecution implements Execution { } tick(ticks: number): void { + if (this.recipient === undefined) throw new Error("Not initialized"); const emojiString = flattenedEmojiTable[this.emoji]; if (emojiString === undefined) { console.warn( diff --git a/src/core/execution/FactoryExecution.ts b/src/core/execution/FactoryExecution.ts index 598662ff0..5658523a2 100644 --- a/src/core/execution/FactoryExecution.ts +++ b/src/core/execution/FactoryExecution.ts @@ -5,7 +5,8 @@ import { TrainStationExecution } from "./TrainStationExecution"; export class FactoryExecution implements Execution { private factory: Unit | null = null; private active = true; - private game: Game; + private game: Game | undefined; + constructor( private player: Player, private readonly tile: TileRef, @@ -46,6 +47,7 @@ export class FactoryExecution implements Execution { createStation(): void { if (this.factory !== null) { + if (this.game === undefined) throw new Error("Not initialized"); const structures = this.game.nearbyUnits( this.factory.tile(), this.game.config().trainStationMaxRange(), diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 1622a08d8..dac000a3d 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -30,7 +30,7 @@ export class FakeHumanExecution implements Execution { private active = true; private readonly random: PseudoRandom; private behavior: BotBehavior | null = null; - private mg: Game; + private mg: Game | undefined; private player: Player | null = null; private readonly attackRate: number; @@ -69,6 +69,7 @@ export class FakeHumanExecution implements Execution { private updateRelationsFromEmbargos() { const { player } = this; if (player === null) return; + if (this.mg === undefined) throw new Error("Not initialized"); const others = this.mg.players().filter((p) => p.id() !== player.id()); others.forEach((other: Player) => { @@ -92,6 +93,7 @@ export class FakeHumanExecution implements Execution { private handleEmbargoesToHostileNations() { const { player } = this; if (player === null) return; + if (this.mg === undefined) throw new Error("Not initialized"); const others = this.mg.players().filter((p) => p.id() !== player.id()); others.forEach((other: Player) => { @@ -114,6 +116,7 @@ export class FakeHumanExecution implements Execution { tick(ticks: number) { if (ticks % this.attackRate !== this.attackTick) return; + if (this.mg === undefined) throw new Error("Not initialized"); if (this.mg.inSpawnPhase()) { const rl = this.randomLand(); if (rl === null) { @@ -164,13 +167,15 @@ export class FakeHumanExecution implements Execution { private maybeAttack() { if (this.player === null || this.behavior === null) { - throw new Error("not initialized"); + throw new Error("Not initialized"); } + const game = this.mg; + if (game === undefined) throw new Error("Not initialized"); const enemyborder = Array.from(this.player.borderTiles()) - .flatMap((t) => this.mg.neighbors(t)) + .flatMap((t) => game.neighbors(t)) .filter( (t) => - this.mg.isLand(t) && this.mg.ownerID(t) !== this.player?.smallID(), + game.isLand(t) && game.ownerID(t) !== this.player?.smallID(), ); if (enemyborder.length === 0) { @@ -185,10 +190,10 @@ export class FakeHumanExecution implements Execution { } const borderPlayers = enemyborder.map((t) => - this.mg.playerBySmallID(this.mg.ownerID(t)), + game.playerBySmallID(game.ownerID(t)), ); if (borderPlayers.some((o) => !o.isPlayer())) { - this.behavior.sendAttack(this.mg.terraNullius()); + this.behavior.sendAttack(game.terraNullius()); return; } @@ -228,7 +233,7 @@ export class FakeHumanExecution implements Execution { } private shouldAttack(other: Player): boolean { - if (this.player === null) throw new Error("not initialized"); + if (this.player === null) throw new Error("Not initialized"); if (this.player.isOnSameTeam(other)) { return false; } @@ -249,6 +254,7 @@ export class FakeHumanExecution implements Execution { if (other.isTraitor()) { return false; } + if (this.mg === undefined) throw new Error("Not initialized"); const { difficulty } = this.mg.config().gameConfig(); if ( difficulty === Difficulty.Hard || @@ -264,9 +270,10 @@ export class FakeHumanExecution implements Execution { } private maybeSendEmoji(enemy: Player) { - if (this.player === null) throw new Error("not initialized"); + if (this.player === null) throw new Error("Not initialized"); if (enemy.type() !== PlayerType.Human) return; const lastSent = this.lastEmojiSent.get(enemy) ?? -300; + if (this.mg === undefined) throw new Error("Not initialized"); if (this.mg.ticks() - lastSent <= 300) return; this.lastEmojiSent.set(enemy, this.mg.ticks()); this.mg.addExecution( @@ -279,7 +286,8 @@ export class FakeHumanExecution implements Execution { } private maybeSendNuke(other: Player) { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); const silos = this.player.units(UnitType.MissileSilo); if ( silos.length === 0 || @@ -328,6 +336,7 @@ export class FakeHumanExecution implements Execution { } private removeOldNukeEvents() { + if (this.mg === undefined) throw new Error("Not initialized"); const maxAge = 500; const tick = this.mg.ticks(); while ( @@ -339,7 +348,8 @@ export class FakeHumanExecution implements Execution { } private sendNuke(tile: TileRef) { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); const tick = this.mg.ticks(); this.lastNukeSent.push([tick, tile]); this.mg.addExecution( @@ -348,10 +358,12 @@ export class FakeHumanExecution implements Execution { } private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number { + if (this.mg === undefined) throw new Error("Not initialized"); + const game = this.mg; // Potential damage in a 25-tile radius const dist = euclDistFN(tile, 25, false); let tileValue = targets - .filter((unit) => dist(this.mg, unit.tile())) + .filter((unit) => dist(game, unit.tile())) .map((unit): number => { switch (unit.type()) { case UnitType.City: @@ -374,7 +386,7 @@ export class FakeHumanExecution implements Execution { 50_000 * targets.filter( (unit) => - unit.type() === UnitType.SAMLauncher && dist50(this.mg, unit.tile()), + unit.type() === UnitType.SAMLauncher && dist50(game, unit.tile()), ).length; // Prefer tiles that are closer to a silo @@ -388,7 +400,7 @@ export class FakeHumanExecution implements Execution { // Don't target near recent targets tileValue -= this.lastNukeSent - .filter(([_tick, tile]) => dist(this.mg, tile)) + .filter(([_tick, tile]) => dist(game, tile)) .map((_) => 1_000_000) .reduce((prev, cur) => prev + cur, 0); @@ -396,14 +408,13 @@ export class FakeHumanExecution implements Execution { } private maybeSendBoatAttack(other: Player) { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); if (this.player.isOnSameTeam(other)) return; const closest = closestTwoTiles( this.mg, - Array.from(this.player.borderTiles()).filter((t) => - this.mg.isOceanShore(t), - ), - Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t)), + Array.from(this.player.borderTiles()).filter((t) => this.mg?.isOceanShore(t)), + Array.from(other.borderTiles()).filter((t) => this.mg?.isOceanShore(t)), ); if (closest === null) { return; @@ -430,7 +441,8 @@ export class FakeHumanExecution implements Execution { } private maybeSpawnStructure(type: UnitType): boolean { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); const owned = this.player.unitsOwned(type); const perceivedCostMultiplier = Math.min(owned + 1, 5); const realCost = this.cost(type); @@ -451,11 +463,11 @@ export class FakeHumanExecution implements Execution { } private structureSpawnTile(type: UnitType): TileRef | null { - if (this.player === null) throw new Error("not initialized"); + if (this.player === null) throw new Error("Not initialized"); const tiles = type === UnitType.Port ? Array.from(this.player.borderTiles()).filter((t) => - this.mg.isOceanShore(t), + this.mg?.isOceanShore(t), ) : Array.from(this.player.tiles()); if (tiles.length === 0) return null; @@ -490,7 +502,8 @@ export class FakeHumanExecution implements Execution { } private structureSpawnTileValue(type: UnitType): (tile: TileRef) => number { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); const borderTiles = this.player.borderTiles(); const { mg } = this; const otherUnits = this.player.units(type); @@ -547,7 +560,8 @@ export class FakeHumanExecution implements Execution { } private maybeSpawnWarship(): boolean { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); if (!this.random.chance(50)) { return false; } @@ -577,6 +591,7 @@ export class FakeHumanExecution implements Execution { } private randTerritoryTile(p: Player): TileRef | null { + if (this.mg === undefined) throw new Error("Not initialized"); const boundingBox = calculateBoundingBox(this.mg, p.borderTiles()); for (let i = 0; i < 100; i++) { const randX = this.random.nextInt(boundingBox.min.x, boundingBox.max.x); @@ -594,6 +609,7 @@ export class FakeHumanExecution implements Execution { } private warshipSpawnTile(portTile: TileRef): TileRef | null { + if (this.mg === undefined) throw new Error("Not initialized"); const radius = 250; for (let attempts = 0; attempts < 50; attempts++) { const randX = this.random.nextInt( @@ -618,14 +634,16 @@ export class FakeHumanExecution implements Execution { } private cost(type: UnitType): Gold { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); return this.mg.unitInfo(type).cost(this.player); } sendBoatRandomly() { - if (this.player === null) throw new Error("not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.player === null) throw new Error("Not initialized"); const oceanShore = Array.from(this.player.borderTiles()).filter((t) => - this.mg.isOceanShore(t), + this.mg?.isOceanShore(t), ); if (oceanShore.length === 0) { return; @@ -651,6 +669,7 @@ export class FakeHumanExecution implements Execution { } randomLand(): TileRef | null { + if (this.mg === undefined) throw new Error("Not initialized"); const delta = 25; let tries = 0; while (tries < 50) { @@ -676,7 +695,8 @@ export class FakeHumanExecution implements Execution { } private randomBoatTarget(tile: TileRef, dist: number): TileRef | null { - if (this.player === null) throw new Error("not initialized"); + if (this.player === null) throw new Error("Not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); const x = this.mg.x(tile); const y = this.mg.y(tile); for (let i = 0; i < 500; i++) { diff --git a/src/core/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts index b6e0b1e9f..6360ba826 100644 --- a/src/core/execution/MIRVExecution.ts +++ b/src/core/execution/MIRVExecution.ts @@ -16,20 +16,20 @@ import { simpleHash } from "../Util"; export class MirvExecution implements Execution { private active = true; - private mg: Game; + private mg: Game | undefined; private nuke: Unit | null = null; private readonly mirvRange = 1500; private readonly warheadCount = 350; - private random: PseudoRandom; + private random: PseudoRandom | undefined; - private pathFinder: ParabolaPathFinder; + private pathFinder: ParabolaPathFinder | undefined; - private targetPlayer: Player | TerraNullius; + private targetPlayer: Player | TerraNullius | undefined; - private separateDst: TileRef; + private separateDst: TileRef | undefined; private speed = -1; @@ -61,6 +61,9 @@ export class MirvExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); + if (this.targetPlayer === undefined) throw new Error("Not initialized"); if (this.nuke === null) { const spawn = this.player.canBuild(UnitType.MIRV, this.dst); if (spawn === false) { @@ -98,7 +101,9 @@ export class MirvExecution implements Execution { } private separate() { - if (this.nuke === null) throw new Error("uninitialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); + if (this.nuke === null) throw new Error("Not initialized"); const dsts: TileRef[] = [this.dst]; let attempts = 1000; while (attempts > 0 && dsts.length < this.warheadCount) { @@ -110,9 +115,10 @@ export class MirvExecution implements Execution { dsts.push(potential); } console.log(`dsts: ${dsts.length}`); + const game = this.mg; dsts.sort( (a, b) => - this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst), + game.manhattanDist(b, this.dst) - game.manhattanDist(a, this.dst), ); console.log(`got ${dsts.length} dsts!!`); @@ -133,6 +139,8 @@ export class MirvExecution implements Execution { } randomLand(ref: TileRef, taken: TileRef[]): TileRef | null { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); let tries = 0; const mirvRange2 = this.mirvRange * this.mirvRange; while (tries < 100) { @@ -168,6 +176,7 @@ export class MirvExecution implements Execution { } private proximityCheck(tile: TileRef, taken: TileRef[]): boolean { + if (this.mg === undefined) throw new Error("Not initialized"); for (const t of taken) { if (this.mg.manhattanDist(tile, t) < 55) { return true; diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index aa7dbba27..72d269cde 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -3,7 +3,7 @@ import { TileRef } from "../game/GameMap"; export class MissileSiloExecution implements Execution { private active = true; - private mg: Game; + private mg: Game | undefined; private silo: Unit | null = null; constructor( @@ -38,6 +38,7 @@ export class MissileSiloExecution implements Execution { return; } + if (this.mg === undefined) throw new Error("Not initialized"); const cooldown = this.mg.config().SiloCooldown() - (this.mg.ticks() - frontTime); diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 73867f4a0..575cbaf86 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -18,10 +18,10 @@ const SPRITE_RADIUS = 16; export class NukeExecution implements Execution { private active = true; - private mg: Game; + private mg: Game | undefined; private nuke: Unit | null = null; private tilesToDestroyCache: Set | undefined; - private pathFinder: ParabolaPathFinder; + private pathFinder: ParabolaPathFinder | undefined; constructor( private readonly nukeType: NukeType, @@ -41,6 +41,7 @@ export class NukeExecution implements Execution { } public target(): Player | TerraNullius { + if (this.mg === undefined) throw new Error("Not initialized"); return this.mg.owner(this.dst); } @@ -51,6 +52,7 @@ export class NukeExecution implements Execution { if (this.nuke === null) { throw new Error("Not initialized"); } + if (this.mg === undefined) throw new Error("Not initialized"); const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); const rand = new PseudoRandom(this.mg.ticks()); const inner2 = magnitude.inner * magnitude.inner; @@ -63,6 +65,7 @@ export class NukeExecution implements Execution { } private maybeBreakAlliances(toDestroy: Set) { + if (this.mg === undefined) throw new Error("Not initialized"); if (this.nuke === null) { throw new Error("Not initialized"); } @@ -94,6 +97,8 @@ export class NukeExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); if (this.nuke === null) { const spawn = this.src ?? this.player.canBuild(this.nukeType, this.dst); if (spawn === false) { @@ -179,6 +184,8 @@ export class NukeExecution implements Execution { } private getTrajectory(target: TileRef): TrajectoryTile[] { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); const trajectoryTiles: TrajectoryTile[] = []; const targetRangeSquared = this.mg.config().defaultNukeTargetableRange() ** 2; @@ -199,6 +206,7 @@ export class NukeExecution implements Execution { nukeTile: TileRef, targetRangeSquared: number, ): boolean { + if (this.mg === undefined) throw new Error("Not initialized"); return ( this.mg.euclideanDistSquared(nukeTile, targetTile) < targetRangeSquared || (this.src !== undefined && @@ -211,6 +219,7 @@ export class NukeExecution implements Execution { if (this.nuke === null || this.nuke.targetTile() === undefined) { return; } + if (this.mg === undefined) throw new Error("Not initialized"); const targetRangeSquared = this.mg.config().defaultNukeTargetableRange() ** 2; const targetTile = this.nuke.targetTile(); @@ -221,6 +230,7 @@ export class NukeExecution implements Execution { } private detonate() { + if (this.mg === undefined) throw new Error("Not initialized"); if (this.nuke === null) { throw new Error("Not initialized"); } @@ -304,6 +314,7 @@ export class NukeExecution implements Execution { } private redrawBuildings(range: number) { + if (this.mg === undefined) throw new Error("Not initialized"); const rangeSquared = range * range; for (const unit of this.mg.units()) { if (isStructureType(unit.type())) { diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index bf8740019..457a79222 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -7,9 +7,9 @@ import { GameImpl } from "../game/GameImpl"; export class PlayerExecution implements Execution { private readonly ticksPerClusterCalc = 20; - private config: Config; + private config: Config | undefined; private lastCalc = 0; - private mg: Game; + private mg: Game | undefined; private active = true; constructor(private readonly player: Player) {} @@ -26,13 +26,15 @@ export class PlayerExecution implements Execution { } tick(ticks: number) { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.config === undefined) throw new Error("Not initialized"); this.player.decayRelations(); this.player.units().forEach((u) => { - const tileOwner = this.mg.owner(u.tile()); + const tileOwner = this.mg?.owner(u.tile()); if (u.info().territoryBound) { - if (tileOwner.isPlayer()) { + if (tileOwner?.isPlayer()) { if (tileOwner !== this.player) { - this.mg.player(tileOwner.id()).captureUnit(u); + this.mg?.player(tileOwner.id()).captureUnit(u); } } else { u.delete(); @@ -98,6 +100,7 @@ export class PlayerExecution implements Execution { } private removeClusters() { + if (this.mg === undefined) throw new Error("Not initialized"); const clusters = this.calculateClusters(); clusters.sort((a, b) => b.size - a.size); @@ -117,6 +120,7 @@ export class PlayerExecution implements Execution { } private surroundedBySamePlayer(cluster: Set): false | Player { + if (this.mg === undefined) throw new Error("Not initialized"); const enemies = new Set(); for (const tile of cluster) { const isOceanShore = this.mg.isOceanShore(tile); @@ -151,6 +155,7 @@ export class PlayerExecution implements Execution { } private isSurrounded(cluster: Set): boolean { + if (this.mg === undefined) throw new Error("Not initialized"); const enemyTiles = new Set(); for (const tr of cluster) { if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) { @@ -174,6 +179,7 @@ export class PlayerExecution implements Execution { } private removeCluster(cluster: Set) { + if (this.mg === undefined) throw new Error("Not initialized"); if ( Array.from(cluster).some( (t) => this.mg?.ownerID(t) !== this.player?.smallID(), @@ -208,6 +214,7 @@ export class PlayerExecution implements Execution { } private getCapturingPlayer(cluster: Set): Player | null { + if (this.mg === undefined) throw new Error("Not initialized"); const neighborsIDs = new Set(); for (const t of cluster) { for (const neighbor of this.mg.neighbors(t)) { diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 3fb6d8bdf..fc8971b2d 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -6,10 +6,10 @@ import { TrainStationExecution } from "./TrainStationExecution"; export class PortExecution implements Execution { private active = true; - private mg: Game; + private mg: Game | undefined; private port: Unit | null = null; - private random: PseudoRandom; - private checkOffset: number; + private random: PseudoRandom | undefined; + private checkOffset: number | undefined; constructor( private player: Player, @@ -23,9 +23,9 @@ export class PortExecution implements Execution { } tick(ticks: number): void { - if (this.mg === null || this.random === null || this.checkOffset === null) { - throw new Error("Not initialized"); - } + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); + if (this.checkOffset === undefined) throw new Error("Not initialized"); if (this.port === null) { const { tile } = this; const spawn = this.player.canBuild(UnitType.Port, tile); @@ -77,6 +77,8 @@ export class PortExecution implements Execution { } shouldSpawnTradeShip(): boolean { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); const numTradeShips = this.mg.unitCount(UnitType.TradeShip); const spawnRate = this.mg.config().tradeShipSpawnRate(numTradeShips); const level = this.port?.level() ?? 0; @@ -89,6 +91,7 @@ export class PortExecution implements Execution { } createStation(): void { + if (this.mg === undefined) throw new Error("Not initialized"); if (this.port !== null) { const nearbyFactory = this.mg.hasUnitNearby( this.port.tile(), diff --git a/src/core/execution/QuickChatExecution.ts b/src/core/execution/QuickChatExecution.ts index 5905245fe..8cb035c66 100644 --- a/src/core/execution/QuickChatExecution.ts +++ b/src/core/execution/QuickChatExecution.ts @@ -1,8 +1,8 @@ import { Execution, Game, Player, PlayerID } from "../game/Game"; export class QuickChatExecution implements Execution { - private recipient: Player; - private mg: Game; + private recipient: Player | undefined; + private mg: Game | undefined; private active = true; @@ -27,6 +27,8 @@ export class QuickChatExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.recipient === undefined) throw new Error("Not initialized"); const message = this.getMessageFromKey(this.quickChatKey); this.mg.displayChat( diff --git a/src/core/execution/RailroadExecution.ts b/src/core/execution/RailroadExecution.ts index 7bcfa8fa3..33ddaa804 100644 --- a/src/core/execution/RailroadExecution.ts +++ b/src/core/execution/RailroadExecution.ts @@ -4,7 +4,7 @@ import { Railroad } from "../game/Railroad"; import { TileRef } from "../game/GameMap"; export class RailroadExecution implements Execution { - private mg: Game; + private mg: Game | undefined; private active = true; private headIndex = 0; private tailIndex = 0; @@ -52,6 +52,7 @@ export class RailroadExecution implements Execution { /* eslint-enable sort-keys */ private computeExtremityDirection(tile: TileRef, next: TileRef): RailType { + if (this.mg === undefined) throw new Error("Not initialized"); const x = this.mg.x(tile); const y = this.mg.y(tile); const nextX = this.mg.x(next); @@ -75,9 +76,7 @@ export class RailroadExecution implements Execution { current: TileRef, next: TileRef, ): RailType { - if (this.mg === null) { - throw new Error("Not initialized"); - } + if (this.mg === undefined) throw new Error("Not initialized"); const x1 = this.mg.x(prev); const y1 = this.mg.y(prev); const x2 = this.mg.x(current); @@ -114,9 +113,7 @@ export class RailroadExecution implements Execution { } tick(ticks: number): void { - if (this.mg === null) { - throw new Error("Not initialized"); - } + if (this.mg === undefined) throw new Error("Not initialized"); if (!this.activeSourceOrDestination()) { this.active = false; return; diff --git a/src/core/execution/RetreatExecution.ts b/src/core/execution/RetreatExecution.ts index 7bd033081..1d9c7956f 100644 --- a/src/core/execution/RetreatExecution.ts +++ b/src/core/execution/RetreatExecution.ts @@ -5,8 +5,8 @@ const cancelDelay = 20; export class RetreatExecution implements Execution { private active = true; private retreatOrdered = false; - private startTick: number; - private mg: Game; + private startTick: number | undefined; + private mg: Game | undefined; constructor( private readonly player: Player, private readonly attackID: string, @@ -18,6 +18,9 @@ export class RetreatExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.startTick === undefined) throw new Error("Not initialized"); + if (!this.retreatOrdered) { this.player.orderRetreat(this.attackID); this.retreatOrdered = true; diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index ae8f8f083..744021ce3 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -126,14 +126,14 @@ class SAMTargetingSystem { } export class SAMLauncherExecution implements Execution { - private mg: Game; + private mg: Game | undefined; private active = true; // As MIRV go very fast we have to detect them very early but we only // shoot the one targeting very close (MIRVWarheadProtectionRadius) private readonly MIRVWarheadSearchRadius = 400; private readonly MIRVWarheadProtectionRadius = 50; - private targetingSystem: SAMTargetingSystem; + private targetingSystem: SAMTargetingSystem | undefined; private pseudoRandom: PseudoRandom | undefined; @@ -156,6 +156,7 @@ export class SAMLauncherExecution implements Execution { return true; } + if (this.mg === undefined) throw new Error("Not initialized"); if (type === UnitType.MIRVWarhead) { return random < this.mg.config().samWarheadHittingChance(); } @@ -164,9 +165,7 @@ export class SAMLauncherExecution implements Execution { } tick(ticks: number): void { - if (this.mg === null || this.player === null) { - throw new Error("Not initialized"); - } + if (this.mg === undefined) throw new Error("Not initialized"); if (this.sam === null) { if (this.tile === null) { throw new Error("tile is null"); @@ -211,6 +210,7 @@ export class SAMLauncherExecution implements Execution { this.MIRVWarheadSearchRadius, UnitType.MIRVWarhead, ({ unit }) => { + if (this.mg === undefined) return false; if (!isUnit(unit)) return false; if (unit.owner() === this.player) return false; if (this.player.isFriendly(unit.owner())) return false; diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index 88babace7..0b598f072 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -13,9 +13,9 @@ import { TileRef } from "../game/GameMap"; export class SAMMissileExecution implements Execution { private active = true; - private pathFinder: AirPathFinder; + private pathFinder: AirPathFinder | undefined; private SAMMissile: Unit | undefined; - private mg: Game; + private mg: Game | undefined; private speed = 0; constructor( @@ -33,6 +33,8 @@ export class SAMMissileExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); this.SAMMissile ??= this._owner.buildUnit( UnitType.SAMMissile, this.spawn, diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 3aabba1d5..3573a0690 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -5,11 +5,11 @@ import { TileRef } from "../game/GameMap"; export class ShellExecution implements Execution { private active = true; - private pathFinder: AirPathFinder; + private pathFinder: AirPathFinder | undefined; private shell: Unit | undefined; - private mg: Game; + private mg: Game | undefined; private destroyAtTick = -1; - private random: PseudoRandom; + private random: PseudoRandom | undefined; constructor( private readonly spawn: TileRef, @@ -25,6 +25,8 @@ export class ShellExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); this.shell ??= this._owner.buildUnit(UnitType.Shell, this.spawn, {}); if (!this.shell.isActive()) { this.active = false; @@ -62,6 +64,8 @@ export class ShellExecution implements Execution { } private effectOnTarget(): number { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); const { damage } = this.mg.config().unitInfo(UnitType.Shell); const baseDamage = damage ?? 250; diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 64c4a0da3..3ed33dd38 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -6,7 +6,7 @@ import { getSpawnTiles } from "./Util"; export class SpawnExecution implements Execution { active = true; - private mg: Game; + private mg: Game | undefined; constructor( private readonly playerInfo: PlayerInfo, @@ -20,6 +20,7 @@ export class SpawnExecution implements Execution { tick(ticks: number) { this.active = false; + if (this.mg === undefined) throw new Error("Not initialized"); if (!this.mg.isValidRef(this.tile)) { console.warn(`SpawnExecution: tile ${this.tile} not valid`); return; diff --git a/src/core/execution/TargetPlayerExecution.ts b/src/core/execution/TargetPlayerExecution.ts index a3ea4cb15..a0a5aca43 100644 --- a/src/core/execution/TargetPlayerExecution.ts +++ b/src/core/execution/TargetPlayerExecution.ts @@ -1,7 +1,7 @@ import { Execution, Game, Player, PlayerID } from "../game/Game"; export class TargetPlayerExecution implements Execution { - private target: Player; + private target: Player | undefined; private active = true; @@ -21,6 +21,7 @@ export class TargetPlayerExecution implements Execution { } tick(ticks: number): void { + if (this.target === undefined) throw new Error("Not initialized"); if (this.requestor.canTarget(this.target)) { this.requestor.target(this.target); this.target.updateRelation(this.requestor, -40); diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 93c70a40d..04e9fbbc7 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -14,10 +14,10 @@ import { renderNumber } from "../../client/Utils"; export class TradeShipExecution implements Execution { private active = true; - private mg: Game; + private mg: Game | undefined; private tradeShip: Unit | undefined; private wasCaptured = false; - private pathFinder: PathFinder; + private pathFinder: PathFinder | undefined; private tilesTraveled = 0; constructor( @@ -32,6 +32,8 @@ export class TradeShipExecution implements Execution { } tick(ticks: number): void { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); if (this.tradeShip === undefined) { const spawn = this.origOwner.canBuild( UnitType.TradeShip, @@ -131,6 +133,7 @@ export class TradeShipExecution implements Execution { } private complete() { + if (this.mg === undefined) throw new Error("Not initialized"); if (this.tradeShip === undefined) throw new Error("Not initialized"); this.active = false; this.tradeShip.delete(false); diff --git a/src/core/execution/TrainStationExecution.ts b/src/core/execution/TrainStationExecution.ts index 8293bc745..5053dffd7 100644 --- a/src/core/execution/TrainStationExecution.ts +++ b/src/core/execution/TrainStationExecution.ts @@ -4,9 +4,9 @@ import { TrainExecution } from "./TrainExecution"; import { TrainStation } from "../game/TrainStation"; export class TrainStationExecution implements Execution { - private mg: Game; + private mg: Game | undefined; private active = true; - private random: PseudoRandom; + private random: PseudoRandom | undefined; private station: TrainStation | null = null; private readonly numCars = 5; private lastSpawnTick = 0; @@ -49,6 +49,8 @@ export class TrainStationExecution implements Execution { } private shouldSpawnTrain(clusterSize: number): boolean { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); const spawnRate = this.mg.config().trainSpawnRate(clusterSize); for (let i = 0; i < this.unit.level(); i++) { if (this.random.chance(spawnRate)) { @@ -59,6 +61,8 @@ export class TrainStationExecution implements Execution { } private spawnTrain(station: TrainStation, currentTick: number) { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); if ( !this.spawnTrains || currentTick - this.lastSpawnTick < this.ticksCooldown diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 70fb16007..4862d53af 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -15,23 +15,23 @@ import { TileRef } from "../game/GameMap"; import { targetTransportTile } from "../game/TransportShipUtils"; export class TransportShipExecution implements Execution { - private lastMove: number; + private lastMove: number | undefined; // TODO: make this configurable private readonly ticksPerMove = 1; private active = true; - private mg: Game; - private target: Player | TerraNullius; + private mg: Game | undefined; + private target: Player | TerraNullius | undefined; // TODO make private - public path: TileRef[]; - private dst: TileRef | null; + public path: TileRef[] | undefined; + private dst: TileRef | null = null; - private boat: Unit; + private boat: Unit | undefined; - private pathFinder: PathFinder; + private pathFinder: PathFinder | undefined; constructor( private readonly attacker: Player, @@ -158,6 +158,11 @@ export class TransportShipExecution implements Execution { if (!this.active) { return; } + if (this.boat === undefined) throw new Error("Not initialized"); + if (this.lastMove === undefined) throw new Error("Not initialized"); + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.target === undefined) throw new Error("Not initialized"); + if (this.pathFinder === undefined) throw new Error("Not initialized"); if (!this.boat.isActive()) { this.active = false; return; diff --git a/src/core/execution/UpgradeStructureExecution.ts b/src/core/execution/UpgradeStructureExecution.ts index 15f53c4e0..1a5d72fe7 100644 --- a/src/core/execution/UpgradeStructureExecution.ts +++ b/src/core/execution/UpgradeStructureExecution.ts @@ -2,7 +2,7 @@ import { Execution, Game, Player, Unit } from "../game/Game"; export class UpgradeStructureExecution implements Execution { private structure: Unit | undefined; - private readonly cost: bigint; + private readonly cost: bigint | undefined; constructor( private readonly player: Player, diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index dffbc8651..2ba6dacfc 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -14,10 +14,10 @@ import { ShellExecution } from "./ShellExecution"; import { TileRef } from "../game/GameMap"; export class WarshipExecution implements Execution { - private random: PseudoRandom; - private warship: Unit; - private mg: Game; - private pathfinder: PathFinder; + private random: PseudoRandom | undefined; + private warship: Unit | undefined; + private mg: Game | undefined; + private pathfinder: PathFinder | undefined; private lastShellAttack = 0; private readonly alreadySentShell = new Set(); @@ -51,6 +51,7 @@ export class WarshipExecution implements Execution { } tick(ticks: number): void { + if (this.warship === undefined) throw new Error("Not initialized"); if (this.warship.health() <= 0) { this.warship.delete(); return; @@ -75,6 +76,8 @@ export class WarshipExecution implements Execution { } private findTargetUnit(): Unit | undefined { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.warship === undefined) throw new Error("Not initialized"); const hasPort = this.warship.owner().unitCount(UnitType.Port) > 0; const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2; @@ -152,6 +155,8 @@ export class WarshipExecution implements Execution { } private shootTarget() { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.warship === undefined) throw new Error("Not initialized"); const targetUnit = this.warship.targetUnit(); if (targetUnit === undefined) return; const shellAttackRate = this.mg.config().warshipShellAttackRate(); @@ -178,6 +183,8 @@ export class WarshipExecution implements Execution { } private huntDownTradeShip() { + if (this.pathfinder === undefined) throw new Error("Not initialized"); + if (this.warship === undefined) throw new Error("Not initialized"); const targetUnit = this.warship.targetUnit(); if (targetUnit === undefined) return; for (let i = 0; i < 2; i++) { @@ -207,6 +214,8 @@ export class WarshipExecution implements Execution { } private patrol() { + if (this.pathfinder === undefined) throw new Error("Not initialized"); + if (this.warship === undefined) throw new Error("Not initialized"); let targetTile = this.warship.targetTile(); if (targetTile === undefined) { targetTile = this.randomTile(); @@ -238,7 +247,7 @@ export class WarshipExecution implements Execution { } isActive(): boolean { - return this.warship?.isActive(); + return this.warship?.isActive() ?? false; } activeDuringSpawnPhase(): boolean { @@ -246,6 +255,9 @@ export class WarshipExecution implements Execution { } randomTile(allowShoreline = false): TileRef | undefined { + if (this.mg === undefined) throw new Error("Not initialized"); + if (this.random === undefined) throw new Error("Not initialized"); + if (this.warship === undefined) throw new Error("Not initialized"); let warshipPatrolRange = this.mg.config().warshipPatrolRange(); const maxAttemptBeforeExpand = 500; let attempts = 0; diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 3e4f4245e..76497968c 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -15,7 +15,7 @@ import { flattenedEmojiTable } from "../../Util"; export class BotBehavior { private enemy: Player | null = null; - private enemyUpdated: Tick; + private enemyUpdated: Tick | undefined; private readonly assistAcceptEmoji = flattenedEmojiTable.indexOf("👍"); @@ -75,6 +75,7 @@ export class BotBehavior { } forgetOldEnemies() { + if (this.enemyUpdated === undefined) return; // Forget old enemies if (this.game.ticks() - this.enemyUpdated > 100) { this.clearEnemy(); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 57f055937..713b46b2c 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -283,7 +283,7 @@ export class Nation { } export class Cell { - public index: number; + public index: number | undefined; private readonly strRepr: string; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 7a64172dd..91e6ff98d 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -78,7 +78,7 @@ export class GameImpl implements Game { private updates: GameUpdates = createGameUpdatesMap(); private readonly unitGrid: UnitGrid; - private playerTeams: Team[]; + private playerTeams: Team[] = []; private readonly botTeam: Team = ColoredTeams.Bot; private readonly _railNetwork: RailNetwork = createRailNetwork(this); @@ -125,7 +125,8 @@ export class GameImpl implements Game { if (numPlayerTeams < 2) { throw new Error(`Too few teams: ${numPlayerTeams}`); } else if (numPlayerTeams < 8) { - this.playerTeams = [ColoredTeams.Red, ColoredTeams.Blue]; + this.playerTeams.push(ColoredTeams.Red); + this.playerTeams.push(ColoredTeams.Blue); if (numPlayerTeams >= 3) this.playerTeams.push(ColoredTeams.Yellow); if (numPlayerTeams >= 4) this.playerTeams.push(ColoredTeams.Green); if (numPlayerTeams >= 5) this.playerTeams.push(ColoredTeams.Purple); diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 5d12c95c6..0483b2064 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -119,7 +119,7 @@ export class PlayerImpl implements Player { this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id)); } - largestClusterBoundingBox: { min: Cell; max: Cell } | null; + largestClusterBoundingBox: { min: Cell; max: Cell } | null = null; toUpdate(): PlayerUpdate { const outgoingAllianceRequests = this.outgoingAllianceRequests().map((ar) => diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts index 3d6856f9b..7e844439c 100644 --- a/src/core/game/TrainStation.ts +++ b/src/core/game/TrainStation.ts @@ -87,7 +87,7 @@ export function createTrainStopHandlers( export class TrainStation { private readonly stopHandlers: Partial> = {}; - private cluster: Cluster | null; + private cluster: Cluster | null = null; private readonly railroads: Set = new Set(); constructor( diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index ba5c32793..2711b8b39 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -106,7 +106,7 @@ export class PathFinder { private curr: TileRef | null = null; private dst: TileRef | null = null; private path: TileRef[] | null = null; - private aStar: AStar; + private aStar: AStar | undefined; private computeFinished = true; private constructor( @@ -170,7 +170,7 @@ export class PathFinder { } } - switch (this.aStar.compute()) { + switch (this.aStar?.compute()) { case PathFindResultType.Completed: this.computeFinished = true; this.path = this.aStar.reconstructPath(); diff --git a/tests/client/graphics/UILayer.test.ts b/tests/client/graphics/UILayer.test.ts index c899ca079..6a25048a5 100644 --- a/tests/client/graphics/UILayer.test.ts +++ b/tests/client/graphics/UILayer.test.ts @@ -8,7 +8,6 @@ import { UnitView } from "../../../src/core/game/GameView"; describe("UILayer", () => { let game: any; let eventBus: any; - let transformHandler: any; beforeEach(() => { game = { @@ -29,19 +28,22 @@ describe("UILayer", () => { updatesSinceLastTick: () => undefined, }; eventBus = { on: jest.fn() }; - transformHandler = {}; }); it("should initialize and redraw canvas", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); - expect(ui["canvas"].width).toBe(100); - expect(ui["canvas"].height).toBe(100); + // eslint-disable-next-line prefer-destructuring + const canvas = ui["canvas"]; + expect(canvas).toBeDefined(); + if (canvas === undefined) throw new Error(); + expect(canvas.width).toBe(100); + expect(canvas.height).toBe(100); expect(ui["context"]).not.toBeNull(); }); it("should handle unit selection event", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); const unit = { type: () => "Warship", @@ -56,7 +58,7 @@ describe("UILayer", () => { }); it("should add and clear health bars", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); const unit = { id: () => 1, @@ -85,7 +87,7 @@ describe("UILayer", () => { }); it("should remove health bars for inactive units", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); const unit = { id: () => 1, @@ -105,7 +107,7 @@ describe("UILayer", () => { }); it("should add loading bar for unit", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); const unit = { id: () => 2, @@ -117,7 +119,7 @@ describe("UILayer", () => { }); it("should remove loading bar for inactive unit", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); const unit = { id: () => 2, @@ -137,7 +139,7 @@ describe("UILayer", () => { }); it("should remove loading bar for a finished progress bar", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new UILayer(game, eventBus); ui.redraw(); const unit = { id: () => 2, diff --git a/tsconfig.json b/tsconfig.json index 6de45daab..f216485ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,10 +19,9 @@ "esModuleInterop": true, "experimentalDecorators": true, "resolveJsonModule": true, + "strict": true, "strictNullChecks": true, - "useDefineForClassFields": false, - "strictPropertyInitialization": false, - "strict": true + "useDefineForClassFields": false }, "include": [ "src/**/*",