diff --git a/eslint.config.js b/eslint.config.js index b4e1bf857..166d7e035 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -79,6 +79,7 @@ export default [ "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-mixed-enums": "error", + "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/prefer-as-const": "error", @@ -143,6 +144,7 @@ export default [ rules: { // Disabled rules for tests, configs "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index c5204a532..0d54f484e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -452,11 +452,7 @@ export class ClientGameRunner { return; } - this.findAndUpgradeNearestBuilding(tile); - } - - private findAndUpgradeNearestBuilding(clickedTile: TileRef) { - this.myPlayer!.actions(clickedTile).then((actions) => { + this.myPlayer.actions(tile).then((actions) => { const upgradeUnits: { unitId: number; unitType: UnitType; @@ -470,7 +466,7 @@ export class ClientGameRunner { .find((unit) => unit.id() === bu.canUpgrade); if (existingUnit) { const distance = this.gameView.manhattanDist( - clickedTile, + tile, existingUnit.tile(), ); diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index f1a111821..09e34db74 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -169,7 +169,8 @@ export class TerritoryPatternsModal extends LitElement { text-white text-xs font-medium rounded transition-colors" @click=${(e: Event) => { e.stopPropagation(); - handlePurchase(pattern.product!.priceId); + if (!pattern.product) return; + handlePurchase(pattern.product.priceId); }} > ${translateText("territory_patterns.purchase")} @@ -362,9 +363,8 @@ export function generatePreviewDataUrl( ): string { pattern ??= DEFAULT_PATTERN_B64; - if (patternCache.has(pattern)) { - return patternCache.get(pattern)!; - } + const cached = patternCache.get(pattern); + if (cached !== undefined) return cached; // Calculate canvas size const decoder = new PatternDecoder(pattern, base64url.decode); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index e19eabd7a..0fd75d17c 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -582,7 +582,7 @@ export class Transport { } else { console.log( "WebSocket is not open. Current state:", - this.socket!.readyState, + this.socket?.readyState, ); console.log("attempting reconnect"); } diff --git a/src/client/graphics/AnimatedSpriteLoader.ts b/src/client/graphics/AnimatedSpriteLoader.ts index db949f872..2c82ee27e 100644 --- a/src/client/graphics/AnimatedSpriteLoader.ts +++ b/src/client/graphics/AnimatedSpriteLoader.ts @@ -154,7 +154,9 @@ export class AnimatedSpriteLoader { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - canvas.getContext("2d")!.drawImage(img, 0, 0); + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("2D context not supported"); + ctx.drawImage(img, 0, 0); this.animatedSpriteImageMap.set(typedFxType, canvas); } catch (err) { @@ -192,10 +194,8 @@ export class AnimatedSpriteLoader { const borderColor = theme.borderColor(owner); const spawnHighlightColor = theme.spawnHighlightColor(); const key = `${fxType}-${owner.id()}`; - let coloredCanvas: HTMLCanvasElement; - if (this.coloredAnimatedSpriteCache.has(key)) { - coloredCanvas = this.coloredAnimatedSpriteCache.get(key)!; - } else { + let coloredCanvas = this.coloredAnimatedSpriteCache.get(key); + if (coloredCanvas === undefined) { coloredCanvas = colorizeCanvas( baseImage, territoryColor, diff --git a/src/client/graphics/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts index c15807f78..2abc98573 100644 --- a/src/client/graphics/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -143,7 +143,9 @@ export function largestRectangleInHistogram(widths: number[]): Rectangle { const h = i === widths.length ? 0 : widths[i]; while (stack.length > 0 && h < widths[stack[stack.length - 1]]) { - const height = widths[stack.pop()!]; + const lastIndex = stack.pop(); + if (lastIndex === undefined) break; // cannot happen due to the while guard + const height = widths[lastIndex]; const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1; if (height * width > maxArea) { diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 51b7054d6..4e140ec4e 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -120,7 +120,8 @@ export const colorizeCanvas = ( canvas.width = source.width; canvas.height = source.height; - const ctx = canvas.getContext("2d")!; + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("2D context not supported"); ctx.drawImage(source, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -177,9 +178,8 @@ export const getColoredSprite = ( const borderColor: Colord = customBorderColor ?? theme.borderColor(owner); const spawnHighlightColor = theme.spawnHighlightColor(); const key = computeSpriteKey(unit, territoryColor, borderColor); - if (coloredSpriteCache.has(key)) { - return coloredSpriteCache.get(key)!; - } + const cached = coloredSpriteCache.get(key); + if (cached !== undefined) return cached; const sprite = getSpriteForUnit(unit); if (sprite === null) { diff --git a/src/client/graphics/layers/ChatModal.ts b/src/client/graphics/layers/ChatModal.ts index aed36bfa8..94afb2658 100644 --- a/src/client/graphics/layers/ChatModal.ts +++ b/src/client/graphics/layers/ChatModal.ts @@ -189,8 +189,9 @@ export class ChatModal extends LitElement { } private selectPhrase(phrase: QuickChatPhrase) { + if (this.selectedCategory === null) return; this.selectedQuickChatKey = this.getFullQuickChatKey( - this.selectedCategory!, + this.selectedCategory, phrase.key, ); this.selectedPhraseTemplate = translateText( diff --git a/src/client/graphics/layers/GameLeftSidebar.ts b/src/client/graphics/layers/GameLeftSidebar.ts index fd85aeb2b..5cc4dae9f 100644 --- a/src/client/graphics/layers/GameLeftSidebar.ts +++ b/src/client/graphics/layers/GameLeftSidebar.ts @@ -43,7 +43,10 @@ export class GameLeftSidebar extends LitElement implements Layer { tick() { if (!this.playerTeam && this.game.myPlayer()?.team()) { - this.playerTeam = this.game.myPlayer()!.team(); + const myPlayer = this.game.myPlayer(); + if (myPlayer !== null) { + this.playerTeam = myPlayer.team(); + } if (this.playerTeam) { this.playerColor = this.game .config() diff --git a/src/client/graphics/layers/Layer.ts b/src/client/graphics/layers/Layer.ts index 36a850f78..43a9ad826 100644 --- a/src/client/graphics/layers/Layer.ts +++ b/src/client/graphics/layers/Layer.ts @@ -1,6 +1,6 @@ export type Layer = { init?: () => void; - tick?: () => void; + tick?: () => void | Promise; renderLayer?: (context: CanvasRenderingContext2D) => void; shouldTransform?: () => boolean; redraw?: () => void; diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts index 2a49afd32..97b9d577c 100644 --- a/src/client/graphics/layers/MainRadialMenu.ts +++ b/src/client/graphics/layers/MainRadialMenu.ts @@ -72,7 +72,7 @@ export class MainRadialMenu extends LitElement implements Layer { init() { this.radialMenu.init(); - this.eventBus.on(ContextMenuEvent, (event) => { + this.eventBus.on(ContextMenuEvent, async (event) => { const worldCoords = this.transformHandler.screenToWorldCoordinates( event.x, event.y, @@ -80,22 +80,26 @@ export class MainRadialMenu extends LitElement implements Layer { if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) { return; } - if (this.game.myPlayer() === null) { + const myPlayer = this.game.myPlayer(); + if (myPlayer === null) { return; } - this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y); - this.game - .myPlayer()! - .actions(this.clickedTile) - .then((actions) => { - this.updatePlayerActions( - this.game.myPlayer()!, - actions, - this.clickedTile!, - event.x, - event.y, - ); - }); + const tile = this.game.ref(worldCoords.x, worldCoords.y); + this.clickedTile = tile; + try { + const actions = await myPlayer.actions(tile); + // Stale check: user might have clicked somewhere else already + if (this.clickedTile !== tile) return; + this.updatePlayerActions( + myPlayer, + actions, + tile, + event.x, + event.y, + ); + } catch (err) { + console.error("Failed to fetch player actions:", err); + } }); } @@ -141,16 +145,21 @@ export class MainRadialMenu extends LitElement implements Layer { async tick() { if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return; if (this.game.ticks() % 5 === 0) { - this.game - .myPlayer()! - .actions(this.clickedTile) - .then((actions) => { - this.updatePlayerActions( - this.game.myPlayer()!, - actions, - this.clickedTile!, - ); - }); + const myPlayer = this.game.myPlayer(); + if (myPlayer === null) return; + const tile = this.clickedTile; + if (tile === null) return; + try { + const actions = await myPlayer.actions(tile); + if (this.clickedTile !== tile) return; // stale + this.updatePlayerActions( + myPlayer, + actions, + tile, + ); + } catch (err) { + console.error("Failed to refresh player actions:", err); + } } } diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 47b47ab6e..8074f1081 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -237,7 +237,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { ${ref((el) => { if (el instanceof HTMLElement) { requestAnimationFrame(() => { - renderPlayerFlag(player.cosmetics.flag!, el); + renderPlayerFlag(player.cosmetics.flag, el); }); } })} diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 4ed69673c..a9e018b81 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -552,6 +552,7 @@ export class RadialMenu implements Layer { } else { content .append("image") + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion .attr("xlink:href", d.data.icon!) .attr("width", this.config.iconSize) .attr("height", this.config.iconSize) @@ -936,7 +937,11 @@ export class RadialMenu implements Layer { this.currentLevel = 0; this.menuStack = []; - this.currentMenuItems = this.rootMenu.subMenu!(this.params!); + if (this.rootMenu.subMenu === undefined || this.params === null) { + this.currentMenuItems = []; + } else { + this.currentMenuItems = this.rootMenu.subMenu(this.params); + } this.navigationInProgress = false; diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts index ed7b77144..2db9e4f97 100644 --- a/src/client/graphics/layers/RadialMenuElements.ts +++ b/src/client/graphics/layers/RadialMenuElements.ts @@ -105,6 +105,7 @@ export enum Slot { Delete = "delete", } +/* eslint-disable @typescript-eslint/no-non-null-assertion */ const infoChatElement: MenuElement = { id: "info_chat", name: "chat", @@ -292,6 +293,7 @@ const infoEmojiElement: MenuElement = { return emojiElements; }, }; +/* eslint-enable @typescript-eslint/no-non-null-assertion */ export const infoMenuElement: MenuElement = { id: Slot.Info, diff --git a/src/client/graphics/layers/ReplayPanel.ts b/src/client/graphics/layers/ReplayPanel.ts index 9cf12a99d..fc8ca7700 100644 --- a/src/client/graphics/layers/ReplayPanel.ts +++ b/src/client/graphics/layers/ReplayPanel.ts @@ -46,7 +46,8 @@ export class ReplayPanel extends LitElement implements Layer { tick() { if (!this.visible) return; - if (this.game!.ticks() % 10 === 0) { + if (!this.game) return; + if (this.game.ticks() % 10 === 0) { this.requestUpdate(); } } diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 7ceec7fd1..195fdd3b9 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -232,8 +232,9 @@ export class StructureIconsLayer implements Layer { private modifyVisibility(render: StructureRenderInfo) { const structureType = render.unit.type() === UnitType.Construction - ? render.unit.constructionType()! + ? render.unit.constructionType() : render.unit.type(); + if (structureType === undefined) return; const structureInfos = this.structures.get(structureType); let focusStructure = false; @@ -341,14 +342,14 @@ export class StructureIconsLayer implements Layer { ); return PIXI.Texture.EMPTY; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const structureType = isConstruction ? constructionType! : unit.type(); const cacheKey = isConstruction ? `construction-${structureType}` + (renderIcon ? "-icon" : "") : `${this.theme.territoryColor(unit.owner()).toRgbString()}-${structureType}` + (renderIcon ? "-icon" : ""); - if (this.textureCache.has(cacheKey)) { - return this.textureCache.get(cacheKey)!; - } + const cached = this.textureCache.get(cacheKey); + if (cached !== undefined) return cached; const shape = STRUCTURE_SHAPES[structureType]; const texture = shape @@ -379,7 +380,8 @@ export class StructureIconsLayer implements Layer { } structureCanvas.width = Math.ceil(iconSize); structureCanvas.height = Math.ceil(iconSize); - const context = structureCanvas.getContext("2d")!; + const context = structureCanvas.getContext("2d"); + if (!context) throw new Error("2D context not supported"); let borderColor: string; if (isConstruction) { @@ -561,7 +563,8 @@ export class StructureIconsLayer implements Layer { unit.type() === UnitType.Construction ? unit.constructionType() : unit.type(); - const shape = STRUCTURE_SHAPES[unitType!]; + const shape = + unitType !== undefined ? STRUCTURE_SHAPES[unitType] : undefined; if (shape !== undefined) { text.position.y = Math.round(-ICON_SIZE[shape] / 2 - 2); } @@ -598,7 +601,8 @@ export class StructureIconsLayer implements Layer { const imageCanvas = document.createElement("canvas"); imageCanvas.width = image.width; imageCanvas.height = image.height; - const ctx = imageCanvas.getContext("2d")!; + const ctx = imageCanvas.getContext("2d"); + if (!ctx) throw new Error("2D context not supported"); ctx.fillStyle = color; ctx.fillRect(0, 0, imageCanvas.width, imageCanvas.height); ctx.globalCompositeOperation = "destination-in"; diff --git a/src/core/CustomFlag.ts b/src/core/CustomFlag.ts index 815ff8fff..16f44a6c2 100644 --- a/src/core/CustomFlag.ts +++ b/src/core/CustomFlag.ts @@ -14,7 +14,7 @@ const ANIMATION_DURATIONS: Record = { // TODO: Pass in cosmetics as a parameter when // remote cosmetics are implemented for custom flags export function renderPlayerFlag( - flag: string, + flag: string | undefined, target: HTMLElement, cosmetics: Cosmetics | undefined = undefined, ) { @@ -23,7 +23,7 @@ export function renderPlayerFlag( return; } - if (!flag.startsWith("!")) return; + if (!flag?.startsWith("!")) return; const code = flag.slice("!".length); const layers = code.split("_").map((segment) => { diff --git a/src/core/EventBus.ts b/src/core/EventBus.ts index 2e0eacdc5..60e351d8f 100644 --- a/src/core/EventBus.ts +++ b/src/core/EventBus.ts @@ -23,10 +23,11 @@ export class EventBus { eventType: EventConstructor, callback: (event: T) => void, ): void { - if (!this.listeners.has(eventType)) { - this.listeners.set(eventType, []); + let callbacks = this.listeners.get(eventType); + if (callbacks === undefined) { + callbacks = []; + this.listeners.set(eventType, callbacks); } - const callbacks = this.listeners.get(eventType)!; callbacks.push(callback as (event: GameEvent) => void); } diff --git a/src/core/configuration/ColorAllocator.ts b/src/core/configuration/ColorAllocator.ts index d0e123147..97d24eadf 100644 --- a/src/core/configuration/ColorAllocator.ts +++ b/src/core/configuration/ColorAllocator.ts @@ -53,9 +53,8 @@ export class ColorAllocator { } assignColor(id: string): Colord { - if (this.assigned.has(id)) { - return this.assigned.get(id)!; - } + const cached = this.assigned.get(id); + if (cached !== undefined) return cached; if (this.availableColors.length === 0) { this.availableColors = [...this.fallbackColors]; @@ -87,9 +86,8 @@ export class ColorAllocator { } assignTeamPlayerColor(team: Team, playerId: string): Colord { - if (this.teamPlayerColors.has(playerId)) { - return this.teamPlayerColors.get(playerId)!; - } + const cached = this.teamPlayerColors.get(playerId); + if (cached !== undefined) return cached; const teamColors = this.getTeamColorVariations(team); const hashValue = simpleHash(playerId); diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 61ba532ab..0c72cfe13 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -84,9 +84,9 @@ export class PastelTheme implements Theme { } borderColor(player: PlayerView): Colord { - if (this.borderColorCache.has(player.id())) { - return this.borderColorCache.get(player.id())!; - } + const cached = this.borderColorCache.get(player.id()); + if (cached !== undefined) return cached; + const tc = this.territoryColor(player).rgba; /* eslint-disable sort-keys */ const color = colord({ diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index ded3093dd..cfed90ba5 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -81,9 +81,9 @@ export class PastelThemeDark implements Theme { } borderColor(player: PlayerView): Colord { - if (this.borderColorCache.has(player.id())) { - return this.borderColorCache.get(player.id())!; - } + const cached = this.borderColorCache.get(player.id()); + if (cached !== undefined) return cached; + const tc = this.territoryColor(player).rgba; const color = colord({ r: Math.max(tc.r - 40, 0), diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 425ee5fc9..73867f4a0 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -214,8 +214,9 @@ export class NukeExecution implements Execution { const targetRangeSquared = this.mg.config().defaultNukeTargetableRange() ** 2; const targetTile = this.nuke.targetTile(); + if (targetTile === undefined) return; this.nuke.setTargetable( - this.isTargetable(targetTile!, this.nuke.tile(), targetRangeSquared), + this.isTargetable(targetTile, this.nuke.tile(), targetRangeSquared), ); } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 0ed2548b7..3fb6d8bdf 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -79,7 +79,8 @@ export class PortExecution implements Execution { shouldSpawnTradeShip(): boolean { const numTradeShips = this.mg.unitCount(UnitType.TradeShip); const spawnRate = this.mg.config().tradeShipSpawnRate(numTradeShips); - for (let i = 0; i < this.port!.level(); i++) { + const level = this.port?.level() ?? 0; + for (let i = 0; i < level; i++) { if (this.random.chance(spawnRate)) { return true; } diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 2a4bb90b2..93c70a40d 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -131,21 +131,22 @@ export class TradeShipExecution implements Execution { } private complete() { + if (this.tradeShip === undefined) throw new Error("Not initialized"); this.active = false; - this.tradeShip!.delete(false); + this.tradeShip.delete(false); const gold = this.mg .config() .tradeShipGold( this.tilesTraveled, - this.tradeShip!.owner().unitCount(UnitType.Port), + this.tradeShip.owner().unitCount(UnitType.Port), ); if (this.wasCaptured) { - this.tradeShip!.owner().addGold(gold, this._dstPort.tile()); + this.tradeShip.owner().addGold(gold, this._dstPort.tile()); this.mg.displayMessage( `Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`, MessageType.CAPTURED_ENEMY_UNIT, - this.tradeShip!.owner().id(), + this.tradeShip.owner().id(), gold, ); } else { diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index ce0d4646d..70fb16007 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -168,6 +168,7 @@ export class TransportShipExecution implements Execution { this.lastMove = ticks; if (this.boat.retreating()) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.dst = this.src!; // src is guaranteed to be set at this point } diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 5b67cff7c..dffbc8651 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -102,9 +102,11 @@ export class WarshipExecution implements Execution { ) { continue; } + const patrolTile = this.warship.patrolTile(); if ( + patrolTile !== undefined && this.mg.euclideanDistSquared( - this.warship.patrolTile()!, + patrolTile, unit.tile(), ) > patrolRangeSquared ) { @@ -150,9 +152,11 @@ export class WarshipExecution implements Execution { } private shootTarget() { + const targetUnit = this.warship.targetUnit(); + if (targetUnit === undefined) return; const shellAttackRate = this.mg.config().warshipShellAttackRate(); if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { - if (this.warship.targetUnit()?.type() !== UnitType.TransportShip) { + if (targetUnit?.type() !== UnitType.TransportShip) { // Warships don't need to reload when attacking transport ships. this.lastShellAttack = this.mg.ticks(); } @@ -161,12 +165,12 @@ export class WarshipExecution implements Execution { this.warship.tile(), this.warship.owner(), this.warship, - this.warship.targetUnit()!, + targetUnit, ), ); - if (!this.warship.targetUnit()!.hasHealth()) { + if (!targetUnit.hasHealth()) { // Don't send multiple shells to target that can be oneshotted - this.alreadySentShell.add(this.warship.targetUnit()!); + this.alreadySentShell.add(targetUnit); this.warship.setTargetUnit(undefined); return; } @@ -174,16 +178,18 @@ export class WarshipExecution implements Execution { } private huntDownTradeShip() { + const targetUnit = this.warship.targetUnit(); + if (targetUnit === undefined) return; for (let i = 0; i < 2; i++) { // target is trade ship so capture it. const result = this.pathfinder.nextTile( this.warship.tile(), - this.warship.targetUnit()!.tile(), + targetUnit.tile(), 5, ); switch (result.type) { case PathFindResultType.Completed: - this.warship.owner().captureUnit(this.warship.targetUnit()!); + this.warship.owner().captureUnit(targetUnit); this.warship.setTargetUnit(undefined); this.warship.move(this.warship.tile()); return; @@ -201,16 +207,17 @@ export class WarshipExecution implements Execution { } private patrol() { - if (this.warship.targetTile() === undefined) { - this.warship.setTargetTile(this.randomTile()); - if (this.warship.targetTile() === undefined) { + let targetTile = this.warship.targetTile(); + if (targetTile === undefined) { + targetTile = this.randomTile(); + this.warship.setTargetTile(targetTile); + if (targetTile === undefined) { return; } } - const result = this.pathfinder.nextTile( this.warship.tile(), - this.warship.targetTile()!, + targetTile, ); switch (result.type) { case PathFindResultType.Completed: @@ -243,12 +250,14 @@ export class WarshipExecution implements Execution { const maxAttemptBeforeExpand = 500; let attempts = 0; let expandCount = 0; + const patrolTile = this.warship.patrolTile(); + if (patrolTile === undefined) return; while (expandCount < 3) { const x = - this.mg.x(this.warship.patrolTile()!) + + this.mg.x(patrolTile) + this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2); const y = - this.mg.y(this.warship.patrolTile()!) + + this.mg.y(patrolTile) + this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2); if (!this.mg.isValidCoord(x, y)) { continue; diff --git a/src/core/game/AttackImpl.ts b/src/core/game/AttackImpl.ts index a78b02e6e..6ab6dd56d 100644 --- a/src/core/game/AttackImpl.ts +++ b/src/core/game/AttackImpl.ts @@ -104,7 +104,8 @@ export class AttackImpl implements Attack { return null; } // No border tiles yet—use the source tile's location - const tile: number = this.sourceTile()!; + const tile = this.sourceTile(); + if (tile === null) return null; return new Cell(this._mg.map().x(tile), this._mg.map().y(tile)); } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 1a0d5541d..7a64172dd 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -651,8 +651,9 @@ export class GameImpl implements Game { "team", winner, ...this.players() - .filter((p) => p.team() === winner && p.clientID() !== null) - .map((p) => p.clientID()!), + .filter((p) => p.team() === winner) + .map((p) => p.clientID()) + .filter((id): id is ClientID => id !== null), ]; } else { const clientId = winner.clientID(); diff --git a/src/core/game/RailNetworkImpl.ts b/src/core/game/RailNetworkImpl.ts index cca3cb3e6..a77d218a0 100644 --- a/src/core/game/RailNetworkImpl.ts +++ b/src/core/game/RailNetworkImpl.ts @@ -220,8 +220,9 @@ export class RailNetworkImpl implements RailNetwork { { station: start, distance: 0 }, ]; - while (queue.length > 0) { - const { station, distance } = queue.shift()!; + let head = 0; + while (head < queue.length) { + const { station, distance } = queue[head++]; if (visited.has(station)) continue; visited.add(station); @@ -244,8 +245,9 @@ export class RailNetworkImpl implements RailNetwork { const visited = new Set(); const queue = [start]; - while (queue.length > 0) { - const current = queue.shift()!; + let head = 0; + while (head < queue.length) { + const current = queue[head++]; if (visited.has(current)) continue; visited.add(current); diff --git a/src/core/game/TeamAssignment.ts b/src/core/game/TeamAssignment.ts index 1754920d6..fcef06c55 100644 --- a/src/core/game/TeamAssignment.ts +++ b/src/core/game/TeamAssignment.ts @@ -16,10 +16,12 @@ export function assignTeams( // Sort players into clan groups or no-clan list for (const player of players) { if (player.clan) { - if (!clanGroups.has(player.clan)) { - clanGroups.set(player.clan, []); + let group = clanGroups.get(player.clan); + if (group === undefined) { + group = []; + clanGroups.set(player.clan, group); } - clanGroups.get(player.clan)!.push(player); + group.push(player); } else { noClanPlayers.push(player); } diff --git a/src/core/pathfinding/SerialAStar.ts b/src/core/pathfinding/SerialAStar.ts index ee7a25bde..13439c818 100644 --- a/src/core/pathfinding/SerialAStar.ts +++ b/src/core/pathfinding/SerialAStar.ts @@ -87,6 +87,7 @@ export class SerialAStar implements AStar { } // Process forward search + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const fwdCurrent = this.fwdOpenSet.poll()!.tile; // Check if we've found a meeting point @@ -98,6 +99,7 @@ export class SerialAStar implements AStar { this.expandNode(fwdCurrent, true); // Process backward search + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const bwdCurrent = this.bwdOpenSet.poll()!.tile; // Check if we've found a meeting point @@ -126,7 +128,7 @@ export class SerialAStar implements AStar { const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet; const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom; - const tentativeGScore = gScore.get(current)! + this.graph.cost(neighbor); + const tentativeGScore = (gScore.get(current) ?? 0) + this.graph.cost(neighbor); let penalty = 0; // With a direction change penalty, the path will get as straight as possible if (this.directionChangePenalty > 0) { @@ -141,7 +143,8 @@ export class SerialAStar implements AStar { } const totalG = tentativeGScore + penalty; - if (!gScore.has(neighbor) || totalG < gScore.get(neighbor)!) { + const g = gScore.get(neighbor); + if (g === undefined || totalG < g) { cameFrom.set(neighbor, current); gScore.set(neighbor, totalG); const fScore = @@ -172,19 +175,23 @@ export class SerialAStar implements AStar { // Reconstruct path from start to meeting point const fwdPath: NodeType[] = [this.meetingPoint]; - let current = this.meetingPoint; + let current: NodeType = this.meetingPoint; - while (this.fwdCameFrom.has(current)) { - current = this.fwdCameFrom.get(current)!; + let f = this.fwdCameFrom.get(current); + while (f !== undefined) { + current = f; fwdPath.unshift(current); + f = this.fwdCameFrom.get(current); } // Reconstruct path from meeting point to goal current = this.meetingPoint; - while (this.bwdCameFrom.has(current)) { - current = this.bwdCameFrom.get(current)!; + let b = this.bwdCameFrom.get(current); + while (b !== undefined) { + current = b; fwdPath.push(current); + b = this.bwdCameFrom.get(current); } return fwdPath; diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index f6a1f8923..1017cc7f1 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -44,13 +44,17 @@ export class WorkerClient { break; case "initialized": - default: - if (message.id && this.messageHandlers.has(message.id)) { - const handler = this.messageHandlers.get(message.id)!; + default: { + if (message.id === undefined) return; + const handler = this.messageHandlers.get(message.id); + if (handler === undefined) return; + try { handler(message); + } finally { this.messageHandlers.delete(message.id); } break; + } } } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index a0d0736a2..dd8d0d81b 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -662,8 +662,8 @@ export class GameServer { // Count occurrences of each hash for (const client of this.activeClients) { - if (client.hashes.has(turnNumber)) { - const clientHash = client.hashes.get(turnNumber)!; + const clientHash = client.hashes.get(turnNumber); + if (clientHash !== undefined) { counts.set(clientHash, (counts.get(clientHash) ?? 0) + 1); } } @@ -683,8 +683,8 @@ export class GameServer { let outOfSyncClients: Client[] = []; for (const client of this.activeClients) { - if (client.hashes.has(turnNumber)) { - const clientHash = client.hashes.get(turnNumber)!; + const clientHash = client.hashes.get(turnNumber); + if (clientHash !== undefined) { if (clientHash !== mostCommonHash) { outOfSyncClients.push(client); } diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index a0a1ce507..ab2b78492 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -103,14 +103,20 @@ export class MapPlaylist { const numAttempts = 10000; for (let i = 0; i < numAttempts; i++) { if (this.shuffleMapsPlaylist()) { - log.info(`Generated map playlist in ${i} attempts`); - return this.mapsPlaylist.shift()!; + log.info(`Generated map playlist in ${i + 1} attempts`); + const next = this.mapsPlaylist.shift(); + if (next !== undefined) return next; + log.error("Playlist unexpectedly empty after successful shuffle; using fallback."); + return { map: GameMapType.World, mode: GameMode.FFA }; } } log.error("Failed to generate a valid map playlist"); } - // Even if it failed, playlist will be partially populated. - return this.mapsPlaylist.shift()!; + // Even if it failed, playlist may be partially populated. + const fallback = this.mapsPlaylist.shift(); + if (fallback !== undefined) return fallback; + log.error("Playlist empty after shuffle failure; using fallback."); + return { map: GameMapType.World, mode: GameMode.FFA }; } private shuffleMapsPlaylist(): boolean { diff --git a/src/server/worker/websocket/handler/message/PostJoinHandler.ts b/src/server/worker/websocket/handler/message/PostJoinHandler.ts index a31eab538..00b5b783a 100644 --- a/src/server/worker/websocket/handler/message/PostJoinHandler.ts +++ b/src/server/worker/websocket/handler/message/PostJoinHandler.ts @@ -131,10 +131,11 @@ function handleWinner( // Add client vote const winnerKey = JSON.stringify(clientMsg.winner); - if (!gs.winnerVotes.has(winnerKey)) { - gs.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg }); + let potentialWinner = gs.winnerVotes.get(winnerKey); + if (potentialWinner === undefined) { + potentialWinner = { ips: new Set(), winner: clientMsg }; + gs.winnerVotes.set(winnerKey, potentialWinner); } - const potentialWinner = gs.winnerVotes.get(winnerKey)!; potentialWinner.ips.add(client.ip); const activeUniqueIPs = new Set(gs.activeClients.map((c) => c.ip));