From 70745faac4be22d1f4278e25788ab4f25ecc0352 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Thu, 15 May 2025 19:39:40 -0400 Subject: [PATCH] Enable strictNullChecks, eqeqeq (#436) ## Description: Improve type safety and runtime correctness by: 1. Enabling TypeScript's [strictNullChecks](https://www.typescriptlang.org/tsconfig/#strictNullChecks) compiler option. 2. Replacing all loose equality operators (`==` and `!=`) with strict equality operators (`===` and `!==`). 3. Cleaning up of type declarations, null handling logic, and equality expressions throughout the project. Currently, the code allows implicit assumptions that `null` and `undefined` are interchangeable, and relies on type-coercing equality checks that can introduce subtle bugs. These practices make it difficult to reason about when values may be absent and hinder the effectiveness of static analysis. Migrating to strict null checks and enforcing strict equality comparisons will clarify intent, reduce bugs, and make the codebase safer and easier to maintain. Fixes #466 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> Co-authored-by: evanpelle --- eslint.config.js | 6 + src/client/ClientGameRunner.ts | 51 +++--- src/client/FlagInput.ts | 2 +- src/client/GoogleAdElement.ts | 4 +- src/client/HostLobbyModal.ts | 23 +-- src/client/InputHandler.ts | 8 +- src/client/JoinPrivateLobbyModal.ts | 6 +- src/client/LocalPersistantStats.ts | 8 +- src/client/LocalServer.ts | 24 +-- src/client/Main.ts | 67 ++++---- src/client/PublicLobby.ts | 19 +-- src/client/SinglePlayerModal.ts | 6 +- src/client/Transport.ts | 49 +++--- src/client/UserSettingModal.ts | 4 +- src/client/UsernameInput.ts | 2 +- src/client/Utils.ts | 4 +- .../baseComponents/setting/SettingSlider.ts | 2 +- src/client/graphics/GameRenderer.ts | 4 +- src/client/graphics/SpriteLoader.ts | 3 + src/client/graphics/TransformHandler.ts | 8 +- src/client/graphics/layers/BuildMenu.ts | 20 +-- src/client/graphics/layers/ChatDisplay.ts | 5 +- src/client/graphics/layers/ChatModal.ts | 7 +- src/client/graphics/layers/ControlPanel.ts | 8 +- src/client/graphics/layers/EmojiTable.ts | 2 +- src/client/graphics/layers/EventsDisplay.ts | 40 ++--- src/client/graphics/layers/Leaderboard.ts | 28 ++-- src/client/graphics/layers/MultiTabModal.ts | 4 +- src/client/graphics/layers/NameLayer.ts | 41 ++--- src/client/graphics/layers/OptionsMenu.ts | 11 +- .../graphics/layers/PlayerInfoOverlay.ts | 14 +- src/client/graphics/layers/PlayerPanel.ts | 40 ++--- src/client/graphics/layers/RadialMenu.ts | 26 +++- src/client/graphics/layers/SpawnTimer.ts | 3 +- src/client/graphics/layers/StructureLayer.ts | 37 +++-- src/client/graphics/layers/TeamStats.ts | 3 +- src/client/graphics/layers/TerrainLayer.ts | 4 +- src/client/graphics/layers/TerritoryLayer.ts | 40 +++-- src/client/graphics/layers/TopBar.ts | 21 +-- src/client/graphics/layers/UILayer.ts | 7 +- src/client/graphics/layers/UnitLayer.ts | 55 +++---- src/client/graphics/layers/WinModal.ts | 17 +- src/core/ApiSchemas.ts | 4 +- src/core/GameRunner.ts | 12 +- src/core/PseudoRandom.ts | 4 +- src/core/Schemas.ts | 9 +- src/core/Util.ts | 25 ++- src/core/configuration/ConfigLoader.ts | 6 +- src/core/configuration/DefaultConfig.ts | 89 ++++++----- src/core/configuration/DevConfig.ts | 2 +- src/core/configuration/PastelTheme.ts | 13 +- src/core/configuration/PastelThemeDark.ts | 13 +- src/core/execution/AttackExecution.ts | 53 ++++--- src/core/execution/BotExecution.ts | 5 +- src/core/execution/BotSpawner.ts | 2 +- src/core/execution/CityExecution.ts | 8 +- src/core/execution/ConstructionExecution.ts | 12 +- src/core/execution/DefensePostExecution.ts | 99 ++++++------ src/core/execution/DonateGoldExecution.ts | 7 +- src/core/execution/DonateTroopExecution.ts | 7 +- src/core/execution/EmbargoExecution.ts | 6 +- src/core/execution/EmojiExecution.ts | 23 +-- src/core/execution/ExecutionManager.ts | 9 +- src/core/execution/FakeHumanExecution.ts | 126 +++++++++------ src/core/execution/MIRVExecution.ts | 15 +- src/core/execution/MissileSiloExecution.ts | 15 +- src/core/execution/MoveWarshipExecution.ts | 7 +- src/core/execution/NoOpExecution.ts | 5 +- src/core/execution/NukeExecution.ts | 69 ++++++--- src/core/execution/PlayerExecution.ts | 75 ++++++--- src/core/execution/PortExecution.ts | 22 ++- src/core/execution/SAMLauncherExecution.ts | 50 +++--- src/core/execution/SAMMissileExecution.ts | 6 +- .../execution/SetTargetTroopRatioExecution.ts | 4 - src/core/execution/ShellExecution.ts | 14 +- src/core/execution/SpawnExecution.ts | 7 +- src/core/execution/TargetPlayerExecution.ts | 4 - src/core/execution/TradeShipExecution.ts | 23 ++- src/core/execution/TransportShipExecution.ts | 25 +-- src/core/execution/Util.ts | 4 +- src/core/execution/WarshipExecution.ts | 63 +++++--- src/core/execution/WinCheckExecution.ts | 25 +-- .../alliance/AllianceRequestExecution.ts | 13 +- .../alliance/AllianceRequestReplyExecution.ts | 17 +- .../alliance/BreakAllianceExecution.ts | 21 +-- src/core/execution/utils/BotBehavior.ts | 6 +- src/core/game/AllianceImpl.ts | 14 +- src/core/game/AttackImpl.ts | 4 +- src/core/game/Game.ts | 23 +-- src/core/game/GameImpl.ts | 81 +++++----- src/core/game/GameMap.ts | 9 +- src/core/game/GameUpdates.ts | 4 +- src/core/game/GameView.ts | 99 ++++++------ src/core/game/PlayerImpl.ts | 146 +++++++++--------- src/core/game/Stats.ts | 6 +- src/core/game/TeamAssignment.ts | 3 + src/core/game/TerraNulliusImpl.ts | 4 +- src/core/game/TerrainMapLoader.ts | 7 +- src/core/game/TransportShipUtils.ts | 38 +++-- src/core/game/UnitGrid.ts | 4 +- src/core/game/UnitImpl.ts | 73 +++++---- src/core/game/UserSettings.ts | 4 +- src/core/pathfinding/PathFinding.ts | 32 ++-- src/core/pathfinding/SerialAStar.ts | 2 +- src/core/utilities/Line.ts | 2 +- src/core/worker/Worker.worker.ts | 2 +- src/scripts/TerrainMapGenerator.ts | 71 +++++---- src/server/Archive.ts | 1 + src/server/GameManager.ts | 6 +- src/server/GameServer.ts | 72 ++++----- src/server/Gatekeeper.ts | 4 +- src/server/Logger.ts | 2 +- src/server/MapPlaylist.ts | 2 +- src/server/Master.ts | 24 ++- src/server/Worker.ts | 16 +- tests/MissileSilo.test.ts | 2 +- tests/Warship.test.ts | 17 +- tests/util/Setup.ts | 9 +- tsconfig.json | 11 ++ 119 files changed, 1428 insertions(+), 1123 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b204b29c7..275c4d87d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,4 +31,10 @@ export default [ "no-useless-escape": "off", }, }, + { + rules: { + // Enable rules + eqeqeq: "error", + }, + }, ]; diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 1e89ff700..cd4233b0d 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -63,7 +63,7 @@ export function joinLobby( ); const userSettings: UserSettings = new UserSettings(); - startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config); + startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {}); const transport = new Transport(lobbyConfig, eventBus); @@ -74,12 +74,12 @@ export function joinLobby( let terrainLoad: Promise | null = null; const onmessage = (message: ServerMessage) => { - if (message.type == "prestart") { + if (message.type === "prestart") { consolex.log(`lobby: game prestarting: ${JSON.stringify(message)}`); terrainLoad = loadTerrainMap(message.gameMap); onPrestart(); } - if (message.type == "start") { + if (message.type === "start") { // Trigger prestart for singleplayer games onPrestart(); consolex.log(`lobby: game started: ${JSON.stringify(message, null, 2)}`); @@ -109,10 +109,13 @@ export async function createClientGame( userSettings: UserSettings, terrainLoad: Promise | null, ): Promise { + if (lobbyConfig.gameStartInfo === undefined) { + throw new Error("missing gameStartInfo"); + } const config = await getConfig( lobbyConfig.gameStartInfo.config, userSettings, - lobbyConfig.gameRecord != null, + lobbyConfig.gameRecord !== undefined, ); let gameMap: TerrainMapData | null = null; @@ -160,7 +163,7 @@ export async function createClientGame( } export class ClientGameRunner { - private myPlayer: PlayerView; + private myPlayer: PlayerView | null = null; private isActive = false; private turnsSeen = 0; @@ -193,7 +196,7 @@ export class ClientGameRunner { }, ]; let winner: ClientID | Team | null = null; - if (update.winnerType == "player") { + if (update.winnerType === "player") { winner = this.gameView .playerBySmallID(update.winner as number) .clientID(); @@ -201,6 +204,9 @@ export class ClientGameRunner { winner = update.winner as Team; } + if (this.lobby.gameStartInfo === undefined) { + throw new Error("missing gameStartInfo"); + } const record = createGameRecord( this.lobby.gameStartInfo.gameID, this.lobby.gameStartInfo, @@ -233,10 +239,13 @@ export class ClientGameRunner { this.renderer.initialize(); this.input.initialize(); this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => { + if (this.lobby.gameStartInfo === undefined) { + throw new Error("missing gameStartInfo"); + } if ("errMsg" in gu) { showErrorModal( gu.errMsg, - gu.stack, + gu.stack ?? "missing", this.lobby.gameStartInfo.gameID, this.lobby.clientID, ); @@ -268,7 +277,7 @@ export class ClientGameRunner { }; const onmessage = (message: ServerMessage) => { this.lastMessageTime = Date.now(); - if (message.type == "start") { + if (message.type === "start") { this.hasJoined = true; consolex.log("starting game!"); for (const turn of message.turns) { @@ -286,7 +295,10 @@ export class ClientGameRunner { this.turnsSeen++; } } - if (message.type == "desync") { + if (message.type === "desync") { + if (this.lobby.gameStartInfo === undefined) { + throw new Error("missing gameStartInfo"); + } showErrorModal( `desync from server: ${JSON.stringify(message)}`, "", @@ -296,12 +308,12 @@ export class ClientGameRunner { "You are desynced from other players. What you see might differ from other players.", ); } - if (message.type == "turn") { + if (message.type === "turn") { if (!this.hasJoined) { this.transport.joinGame(0); return; } - if (this.turnsSeen != message.turn.turnNumber) { + if (this.turnsSeen !== message.turn.turnNumber) { consolex.error( `got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`, ); @@ -348,17 +360,17 @@ export class ClientGameRunner { if (this.gameView.inSpawnPhase()) { return; } - if (this.myPlayer == null) { - this.myPlayer = this.gameView.playerByClientID(this.lobby.clientID); - if (this.myPlayer == null) { - return; - } + if (this.myPlayer === null) { + const myPlayer = this.gameView.playerByClientID(this.lobby.clientID); + if (myPlayer === null) return; + this.myPlayer = myPlayer; } this.myPlayer.actions(tile).then((actions) => { + if (this.myPlayer === null) return; const bu = actions.buildableUnits.find( - (bu) => bu.type == UnitType.TransportShip, + (bu) => bu.type === UnitType.TransportShip, ); - if (bu == null) { + if (bu === undefined) { console.warn(`no transport ship buildable units`); return; } @@ -377,7 +389,8 @@ export class ClientGameRunner { this.myPlayer .bestTransportShipSpawn(this.gameView.ref(cell.x, cell.y)) .then((spawn: number | false) => { - let spawnCell = null; + if (this.myPlayer === null) throw new Error("not initialized"); + let spawnCell: Cell | null = null; if (spawn !== false) { spawnCell = new Cell( this.gameView.x(spawn), diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index 672f07e63..08d7cad13 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -26,7 +26,7 @@ export class FlagInput extends LitElement { } private setFlag(flag: string) { - if (flag == "xx") { + if (flag === "xx") { flag = ""; } this.flag = flag; diff --git a/src/client/GoogleAdElement.ts b/src/client/GoogleAdElement.ts index 6ae2b0eaa..8d86e47fa 100644 --- a/src/client/GoogleAdElement.ts +++ b/src/client/GoogleAdElement.ts @@ -87,7 +87,7 @@ export class GoogleAdElement extends LitElement { const isElectron = () => { // Renderer process if ( - typeof window !== "undefined" && + window !== undefined && typeof window.process === "object" && // @ts-expect-error hidden window.process.type === "renderer" @@ -97,7 +97,7 @@ const isElectron = () => { // Main process if ( - typeof process !== "undefined" && + process !== undefined && typeof process.versions === "object" && !!process.versions.electron ) { diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 0f86f05b8..d532ef3a4 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -31,7 +31,6 @@ export class HostLobbyModal extends LitElement { @state() private disableNPCs = false; @state() private gameMode: GameMode = GameMode.FFA; @state() private teamCount: number | typeof Duos = 2; - @state() private disableNukes: boolean = false; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @state() private infiniteTroops: boolean = false; @@ -40,9 +39,9 @@ export class HostLobbyModal extends LitElement { @state() private copySuccess = false; @state() private players: string[] = []; @state() private useRandomMap: boolean = false; - @state() private disabledUnits: string[] = []; + @state() private disabledUnits: UnitType[] = []; - private playersInterval = null; + private playersInterval: NodeJS.Timeout | null = null; // Add a new timer for debouncing bot changes private botsUpdateTimer: number | null = null; @@ -106,7 +105,7 @@ export class HostLobbyModal extends LitElement { .selected=${!this.useRandomMap && this.selectedMap === mapValue} .translation=${translateText( - `map.${mapKey.toLowerCase()}`, + `map.${mapKey?.toLowerCase()}`, )} > @@ -233,7 +232,7 @@ export class HostLobbyModal extends LitElement { />
${translateText("host_modal.bots")}${ - this.bots == 0 + this.bots === 0 ? translateText("host_modal.bots_disabled") : this.bots } @@ -326,7 +325,7 @@ export class HostLobbyModal extends LitElement { [UnitType.HydrogenBomb, "unit_type.hydrogen_bomb"], [UnitType.MIRV, "unit_type.mirv"], ].map( - ([unitType, translationKey]) => html` + ([unitType, translationKey]: [UnitType, string]) => html`
- ${lobby.gameConfig.gameMode == GameMode.Team - ? translateText("public_lobby.teams", { num: teamCount }) + ${lobby.gameConfig.gameMode === GameMode.Team + ? translateText("public_lobby.teams", { num: teamCount ?? 0 }) : translateText("game_mode.ffa")}
@@ -168,7 +165,7 @@ export class PublicLobby extends LitElement { this.isButtonDebounced = false; }, this.debounceDelay); - if (this.currLobby == null) { + if (this.currLobby === null) { this.isLobbyHighlighted = true; this.currLobby = lobby; this.dispatchEvent( diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index f6960e2c8..b1030c21f 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -73,7 +73,7 @@ export class SinglePlayerModal extends LitElement { .selected=${!this.useRandomMap && this.selectedMap === mapValue} .translation=${translateText( - `map.${mapKey.toLowerCase()}`, + `map.${mapKey?.toLowerCase()}`, )} > @@ -204,7 +204,7 @@ export class SinglePlayerModal extends LitElement { />
${translateText("single_modal.bots")}${this - .bots == 0 + .bots === 0 ? translateText("single_modal.bots_disabled") : this.bots}
@@ -444,7 +444,7 @@ export class SinglePlayerModal extends LitElement { clientID, username: usernameInput.getCurrentUsername(), flag: - flagInput.getCurrentFlag() == "xx" + flagInput.getCurrentFlag() === "xx" ? "" : flagInput.getCurrentFlag(), }, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index a9659ba49..eba9b82b0 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -60,14 +60,14 @@ export class SendSpawnIntentEvent implements GameEvent { export class SendAttackIntentEvent implements GameEvent { constructor( - public readonly targetID: PlayerID, + public readonly targetID: PlayerID | null, public readonly troops: number, ) {} } export class SendBoatAttackIntentEvent implements GameEvent { constructor( - public readonly targetID: PlayerID, + public readonly targetID: PlayerID | null, public readonly dst: Cell, public readonly troops: number, public readonly src: Cell | null = null, @@ -158,7 +158,7 @@ export class MoveWarshipIntentEvent implements GameEvent { } export class Transport { - private socket: WebSocket; + private socket: WebSocket | null = null; private localServer: LocalServer; @@ -176,8 +176,8 @@ export class Transport { // If gameRecord is not null, we are replaying an archived game. // For multiplayer games, GameConfig is not known until game starts. this.isLocal = - lobbyConfig.gameRecord != null || - lobbyConfig.gameStartInfo?.config.gameType == GameType.Singleplayer; + lobbyConfig.gameRecord !== undefined || + lobbyConfig.gameStartInfo?.config.gameType === GameType.Singleplayer; this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e), @@ -228,9 +228,9 @@ export class Transport { private startPing() { if (this.isLocal || this.pingInterval) return; - if (this.pingInterval == null) { + if (this.pingInterval === null) { this.pingInterval = window.setInterval(() => { - if (this.socket != null && this.socket.readyState === WebSocket.OPEN) { + if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) { this.sendMsg( JSON.stringify({ type: "ping", @@ -267,7 +267,7 @@ export class Transport { this.lobbyConfig, onconnect, onmessage, - this.lobbyConfig.gameRecord != null, + this.lobbyConfig.gameRecord !== undefined, ); this.localServer.start(); } @@ -290,7 +290,12 @@ export class Transport { console.log("Connected to game server!"); while (this.buffer.length > 0) { console.log("sending dropped message"); - this.sendMsg(this.buffer.pop()); + const msg = this.buffer.pop(); + if (msg === undefined) { + console.warn("msg is undefined"); + continue; + } + this.sendMsg(msg); } onconnect(); }; @@ -306,13 +311,14 @@ export class Transport { }; this.socket.onerror = (err) => { console.error("Socket encountered error: ", err, "Closing socket"); + if (this.socket === null) return; this.socket.close(); }; this.socket.onclose = (event: CloseEvent) => { console.log( `WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`, ); - if (event.code != 1000) { + if (event.code !== 1000) { console.log(`reconnecting`); this.reconnect(); } @@ -359,6 +365,7 @@ export class Transport { return; } this.stopPing(); + if (this.socket === null) return; if (this.socket.readyState === WebSocket.OPEN) { console.log("on stop: leaving game"); this.socket.close(); @@ -426,8 +433,8 @@ export class Transport { troops: event.troops, dstX: event.dst.x, dstY: event.dst.y, - srcX: event.src?.x, - srcY: event.src?.y, + srcX: event.src?.x ?? null, + srcY: event.src?.y ?? null, }); } @@ -444,7 +451,7 @@ export class Transport { type: "emoji", clientID: this.lobbyConfig.clientID, recipient: - event.recipient == AllPlayers ? AllPlayers : event.recipient.id(), + event.recipient === AllPlayers ? AllPlayers : event.recipient.id(), emoji: event.emoji, }); } @@ -517,7 +524,7 @@ export class Transport { } private onSendWinnerEvent(event: SendWinnerEvent) { - if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { + if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) { const msg = { type: "winner", winner: event.winner, @@ -528,13 +535,14 @@ export class Transport { } else { console.log( "WebSocket is not open. Current state:", - this.socket.readyState, + this.socket?.readyState, ); console.log("attempting reconnect"); } } private onSendHashEvent(event: SendHashEvent) { + if (this.socket === null) return; if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { this.sendMsg( JSON.stringify({ @@ -570,7 +578,7 @@ export class Transport { } private sendIntent(intent: Intent) { - if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { + if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) { const msg = { type: "intent", intent: intent, @@ -579,7 +587,7 @@ export class Transport { } else { console.log( "WebSocket is not open. Current state:", - this.socket.readyState, + this.socket?.readyState, ); console.log("attempting reconnect"); } @@ -589,9 +597,10 @@ export class Transport { if (this.isLocal) { this.localServer.onMessage(msg); } else { + if (this.socket === null) return; if ( - this.socket.readyState == WebSocket.CLOSED || - this.socket.readyState == WebSocket.CLOSED + this.socket.readyState === WebSocket.CLOSED || + this.socket.readyState === WebSocket.CLOSED ) { console.warn("socket not ready, closing and trying later"); this.socket.close(); @@ -605,7 +614,7 @@ export class Transport { } private killExistingSocket(): void { - if (this.socket == null) { + if (this.socket === null) { return; } // Remove all event listeners diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 45d911905..f94f0591a 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -281,7 +281,7 @@ export class UserSettingModal extends LitElement { easter="true" @change=${(e: CustomEvent) => { const value = e.detail?.value; - if (typeof value !== "undefined") { + if (value !== undefined) { console.log("Changed:", value); } else { console.warn("Slider event missing detail.value", e); @@ -300,7 +300,7 @@ export class UserSettingModal extends LitElement { easter="true" @change=${(e: CustomEvent) => { const value = e.detail?.value; - if (typeof value !== "undefined") { + if (value !== undefined) { console.log("Changed:", value); } else { console.warn("Slider event missing detail.value", e); diff --git a/src/client/UsernameInput.ts b/src/client/UsernameInput.ts index f185fb931..39b94d73d 100644 --- a/src/client/UsernameInput.ts +++ b/src/client/UsernameInput.ts @@ -64,7 +64,7 @@ export class UsernameInput extends LitElement { this.storeUsername(this.username); this.validationError = ""; } else { - this.validationError = result.error; + this.validationError = result.error ?? ""; } } diff --git a/src/client/Utils.ts b/src/client/Utils.ts index a0d3ff912..a6c90191f 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -45,12 +45,12 @@ export function createCanvas(): HTMLCanvasElement { */ export function generateCryptoRandomUUID(): string { // Type guard to check if randomUUID is available - if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + if (crypto !== undefined && "randomUUID" in crypto) { return crypto.randomUUID(); } // Fallback using crypto.getRandomValues - if (typeof crypto !== "undefined" && "getRandomValues" in crypto) { + if (crypto !== undefined && "getRandomValues" in crypto) { return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace( /[018]/g, (c: number): string => diff --git a/src/client/components/baseComponents/setting/SettingSlider.ts b/src/client/components/baseComponents/setting/SettingSlider.ts index 989756b57..87927d467 100644 --- a/src/client/components/baseComponents/setting/SettingSlider.ts +++ b/src/client/components/baseComponents/setting/SettingSlider.ts @@ -30,7 +30,7 @@ export class SettingSlider extends LitElement { private handleSliderChange(e: Event) { const detail = (e as CustomEvent)?.detail; - if (!detail || typeof detail.value === "undefined") { + if (!detail || detail.value === undefined) { console.warn("Invalid slider change event", e); return; } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index b778aea1d..3f83a1c41 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -214,7 +214,9 @@ export class GameRenderer { public uiState: UIState, private layers: Layer[], ) { - this.context = canvas.getContext("2d"); + const context = canvas.getContext("2d"); + if (context === null) throw new Error("2d context not supported"); + this.context = context; } initialize() { diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 873affd3a..49abd3c1e 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -90,6 +90,9 @@ export const getColoredSprite = ( } const sprite = getSpriteForUnit(unit.type()); + if (sprite === null) { + throw new Error(`Failed to load sprite for ${unit.type()}`); + } const territoryRgb = territoryColor.toRgb(); const borderRgb = borderColor.toRgb(); diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index cdf0d316f..5a7be6c84 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -9,8 +9,8 @@ export class TransformHandler { private offsetX: number = -350; private offsetY: number = -200; - private target: Cell; - private intervalID = null; + private target: Cell | null; + private intervalID: NodeJS.Timeout | null = null; private changed = false; constructor( @@ -170,6 +170,8 @@ export class TransformHandler { const { screenX, screenY } = this.screenCenter(); const screenMapCenter = new Cell(screenX, screenY); + if (this.target === null) throw new Error("null target"); + if ( this.game.manhattanDist( this.game.ref(screenX, screenY), @@ -234,7 +236,7 @@ export class TransformHandler { } private clearTarget() { - if (this.intervalID != null) { + if (this.intervalID !== null) { clearInterval(this.intervalID); this.intervalID = null; } diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index e051f6519..f048c2271 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -303,13 +303,12 @@ export class BuildMenu extends LitElement implements Layer { private _hidden = true; private canBuild(item: BuildItemDisplay): boolean { - if (this.game?.myPlayer() == null || this.playerActions == null) { + if (this.game?.myPlayer() === null || this.playerActions === null) { return false; } - const unit = this.playerActions.buildableUnits.filter( - (u) => u.type == item.unitType, - ); - if (!unit) { + const buildableUnits = this.playerActions?.buildableUnits ?? []; + const unit = buildableUnits.filter((u) => u.type === item.unitType); + if (unit.length === 0) { return false; } return unit[0].canBuild !== false; @@ -317,7 +316,7 @@ export class BuildMenu extends LitElement implements Layer { private cost(item: BuildItemDisplay): number { for (const bu of this.playerActions?.buildableUnits ?? []) { - if (bu.type == item.unitType) { + if (bu.type === item.unitType) { return bu.cost; } } @@ -368,9 +367,12 @@ export class BuildMenu extends LitElement implements Layer { width="40" height="40" /> - ${translateText(item.key)} + ${item.key && translateText(item.key)} ${translateText(item.description)}${item.description && + translateText(item.description)} ${renderNumber( @@ -413,7 +415,7 @@ export class BuildMenu extends LitElement implements Layer { private refresh() { this.game .myPlayer() - .actions(this.clickedTile) + ?.actions(this.clickedTile) .then((actions) => { this.playerActions = actions; this.requestUpdate(); diff --git a/src/client/graphics/layers/ChatDisplay.ts b/src/client/graphics/layers/ChatDisplay.ts index 847dd35d0..e4f376e18 100644 --- a/src/client/graphics/layers/ChatDisplay.ts +++ b/src/client/graphics/layers/ChatDisplay.ts @@ -63,7 +63,7 @@ export class ChatDisplay extends LitElement implements Layer { if (event.messageType !== MessageType.CHAT) return; const myPlayer = this.game.playerByClientID(this.clientID); if ( - event.playerID != null && + event.playerID !== null && (!myPlayer || myPlayer.smallID() !== event.playerID) ) { return; @@ -82,6 +82,7 @@ export class ChatDisplay extends LitElement implements Layer { tick() { // this.active = true; const updates = this.game.updatesSinceLastTick(); + if (updates === null) throw new Error("null updates"); const messages = updates[GameUpdateType.DisplayEvent] as | DisplayMessageUpdate[] | undefined; @@ -91,7 +92,7 @@ export class ChatDisplay extends LitElement implements Layer { if (msg.messageType === MessageType.CHAT) { const myPlayer = this.game.playerByClientID(this.clientID); if ( - msg.playerID != null && + msg.playerID !== null && (!myPlayer || myPlayer.smallID() !== msg.playerID) ) { continue; diff --git a/src/client/graphics/layers/ChatModal.ts b/src/client/graphics/layers/ChatModal.ts index 84162fd33..13827eb7c 100644 --- a/src/client/graphics/layers/ChatModal.ts +++ b/src/client/graphics/layers/ChatModal.ts @@ -214,7 +214,8 @@ export class ChatModal extends LitElement { private selectPlayer(player: string) { if (this.previewText) { - this.previewText = this.selectedPhraseTemplate.replace("[P1]", player); + this.previewText = + this.selectedPhraseTemplate?.replace("[P1]", player) ?? null; this.selectedPlayer = player; this.requiresPlayerSelection = false; this.requestUpdate(); @@ -228,7 +229,9 @@ export class ChatModal extends LitElement { console.log("Key:", this.selectedQuickChatKey); if (this.sender && this.recipient && this.selectedQuickChatKey) { - const variables = this.selectedPlayer ? { P1: this.selectedPlayer } : {}; + const variables: Record = this.selectedPlayer + ? { P1: this.selectedPlayer } + : {}; this.eventBus.emit( new SendQuickChatEvent( diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 1fa6e1185..0ec35f9af 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -85,7 +85,7 @@ export class ControlPanel extends LitElement implements Layer { newAttackRatio = 1; } - if (newAttackRatio == 0.11 && this.attackRatio == 0.01) { + if (newAttackRatio === 0.11 && this.attackRatio === 0.01) { // If we're changing the ratio from 1%, then set it to 10% instead of 11% to keep a consistency newAttackRatio = 0.1; } @@ -108,13 +108,13 @@ export class ControlPanel extends LitElement implements Layer { } const player = this.game.myPlayer(); - if (player == null || !player.isAlive()) { + if (player === null || !player.isAlive()) { this.setVisibile(false); return; } const popIncreaseRate = player.population() - this._population; - if (this.game.ticks() % 5 == 0) { + if (this.game.ticks() % 5 === 0) { this._popRateIsIncreasing = popIncreaseRate >= this._lastPopulationIncreaseRate; this._lastPopulationIncreaseRate = popIncreaseRate; @@ -275,7 +275,7 @@ export class ControlPanel extends LitElement implements Layer { >${translateText("control_panel.attack_ratio")}: ${(this.attackRatio * 100).toFixed(0)}% (${renderTroops( - this.game?.myPlayer()?.troops() * this.attackRatio, + (this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio, )})
diff --git a/src/client/graphics/layers/EmojiTable.ts b/src/client/graphics/layers/EmojiTable.ts index b8a9e4bd6..938340672 100644 --- a/src/client/graphics/layers/EmojiTable.ts +++ b/src/client/graphics/layers/EmojiTable.ts @@ -114,7 +114,7 @@ export class EmojiTable extends LitElement { this.showTable((emoji) => { const recipient = - targetPlayer == this.game.myPlayer() + targetPlayer === this.game.myPlayer() ? AllPlayers : (targetPlayer as PlayerView); this.eventBus.emit( diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 34eb7ee02..70630d894 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -107,8 +107,10 @@ export class EventsDisplay extends LitElement implements Layer { tick() { this.active = true; const updates = this.game.updatesSinceLastTick(); - for (const [ut, fn] of this.updateMap) { - updates[ut]?.forEach((u) => fn(u)); + if (updates) { + for (const [ut, fn] of this.updateMap) { + updates[ut]?.forEach(fn); + } } let remainingEvents = this.events.filter((event) => { @@ -137,16 +139,16 @@ 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(); - return t != PlayerType.Bot; + return t !== PlayerType.Bot; }); this.outgoingAttacks = myPlayer .outgoingAttacks() - .filter((a) => a.targetID != 0); + .filter((a) => a.targetID !== 0); this.outgoingLandAttacks = myPlayer .outgoingAttacks() - .filter((a) => a.targetID == 0); + .filter((a) => a.targetID === 0); this.outgoingBoats = myPlayer .units() @@ -157,7 +159,7 @@ export class EventsDisplay extends LitElement implements Layer { private addEvent(event: Event) { this.events = [...this.events, event]; - if (this._hidden == true) { + if (this._hidden === true) { this.newEvents++; } this.requestUpdate(); @@ -179,7 +181,7 @@ export class EventsDisplay extends LitElement implements Layer { onDisplayMessageEvent(event: DisplayMessageUpdate) { const myPlayer = this.game.playerByClientID(this.clientID); if ( - event.playerID != null && + event.playerID !== null && (!myPlayer || myPlayer.smallID() !== event.playerID) ) { return; @@ -345,6 +347,7 @@ export class EventsDisplay extends LitElement implements Layer { : update.player2ID === myPlayer.smallID() ? update.player1ID : null; + if (otherID === null) return; const other = this.game.playerBySmallID(otherID) as PlayerView; if (!other || !myPlayer.isAlive() || !other.isAlive()) return; @@ -394,14 +397,14 @@ export class EventsDisplay extends LitElement implements Layer { if (!myPlayer) return; const recipient = - update.emoji.recipientID == AllPlayers + update.emoji.recipientID === AllPlayers ? AllPlayers : this.game.playerBySmallID(update.emoji.recipientID); const sender = this.game.playerBySmallID( update.emoji.senderID, ) as PlayerView; - if (recipient == myPlayer) { + if (recipient === myPlayer) { this.addEvent({ description: `${sender.displayName()}:${update.emoji.message}`, unsafeDescription: true, @@ -427,10 +430,7 @@ export class EventsDisplay extends LitElement implements Layer { onUnitIncomingEvent(event: UnitIncomingUpdate) { const myPlayer = this.game.playerByClientID(this.clientID); - if ( - event.playerID != null && - (!myPlayer || myPlayer.smallID() !== event.playerID) - ) { + if (!myPlayer || myPlayer.smallID() !== event.playerID) { return; } @@ -482,8 +482,10 @@ export class EventsDisplay extends LitElement implements Layer {
- ${player.team() != null + ${player.team() !== null ? html`
${translateText("player_info_overlay.team")}: ${player.team()}
` @@ -271,7 +271,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { private renderUnitInfo(unit: UnitView) { const isAlly = - (unit.owner() == this.myPlayer() || + (unit.owner() === this.myPlayer() || this.myPlayer()?.isFriendly(unit.owner())) ?? false; @@ -312,8 +312,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
- ${this.player != null ? this.renderPlayerInfo(this.player) : ""} - ${this.unit != null ? this.renderUnitInfo(this.unit) : ""} + ${this.player !== null ? this.renderPlayerInfo(this.player) : ""} + ${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
`; diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index bfe293ad9..547bd54fd 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -39,8 +39,8 @@ export class PlayerPanel extends LitElement implements Layer { public eventBus: EventBus; public emojiTable: EmojiTable; - private actions: PlayerActions = null; - private tile: TileRef = null; + private actions: PlayerActions | null = null; + private tile: TileRef | null = null; @state() private isVisible: boolean = false; @@ -125,7 +125,7 @@ export class PlayerPanel extends LitElement implements Layer { private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) { e.stopPropagation(); this.emojiTable.showTable((emoji: string) => { - if (myPlayer == other) { + if (myPlayer === other) { this.eventBus.emit( new SendEmojiIntentEvent( AllPlayers, @@ -181,12 +181,16 @@ export class PlayerPanel extends LitElement implements Layer { return 0; } let sum = 0; - const nukes = stats.sentNukes[this.g.myPlayer().id()]; + const player = this.g.myPlayer(); + if (player === null) { + return 0; + } + const nukes = stats.sentNukes[player.id()]; if (!nukes) { return 0; } for (const nukeType in nukes) { - if (nukeType != UnitType.MIRVWarhead) { + if (nukeType !== UnitType.MIRVWarhead) { sum += nukes[nukeType]; } } @@ -198,10 +202,8 @@ export class PlayerPanel extends LitElement implements Layer { return html``; } const myPlayer = this.g.myPlayer(); - if (myPlayer == null) { - return; - } - + if (myPlayer === null) return; + if (this.tile === null) return; let other = this.g.owner(this.tile); if (!other.isPlayer()) { this.hide(); @@ -210,16 +212,16 @@ export class PlayerPanel extends LitElement implements Layer { } other = other as PlayerView; - const canDonate = this.actions.interaction?.canDonate; + const canDonate = this.actions?.interaction?.canDonate; const canSendAllianceRequest = - this.actions.interaction?.canSendAllianceRequest; + this.actions?.interaction?.canSendAllianceRequest; const canSendEmoji = - other == myPlayer - ? this.actions.canSendEmojiAllPlayers - : this.actions.interaction?.canSendEmoji; - const canBreakAlliance = this.actions.interaction?.canBreakAlliance; - const canTarget = this.actions.interaction?.canTarget; - const canEmbargo = this.actions.interaction?.canEmbargo; + other === myPlayer + ? this.actions?.canSendEmojiAllPlayers + : this.actions?.interaction?.canSendEmoji; + const canBreakAlliance = this.actions?.interaction?.canBreakAlliance; + const canTarget = this.actions?.interaction?.canTarget; + const canEmbargo = this.actions?.interaction?.canEmbargo; return html`
` : ""}
- ${canEmbargo && other != myPlayer + ${canEmbargo && other !== myPlayer ? html`` : ""} - ${!canEmbargo && other != myPlayer + ${!canEmbargo && other !== myPlayer ? html`