diff --git a/eslint.config.js b/eslint.config.js index 275c4d87d..8eccab6cb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,6 +19,23 @@ export default [ pluginJs.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier, + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: [ + "__mocks__/fileMock.js", + "eslint.config.js", + "jest.config.ts", + "postcss.config.js", + "tailwind.config.js", + "webpack.config.js", + ], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, { rules: { // Disable rules that would fail. The failures should be fixed, and the entries here removed. @@ -34,6 +51,7 @@ export default [ { rules: { // Enable rules + "@typescript-eslint/prefer-nullish-coalescing": "error", eqeqeq: "error", }, }, diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 642a8bbe6..af764994d 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -99,7 +99,7 @@ export class LangSelector extends LitElement { private async initializeLanguage() { const browserLocale = navigator.language; const savedLang = localStorage.getItem("lang"); - const userLang = this.getClosestSupportedLang(savedLang || browserLocale); + const userLang = this.getClosestSupportedLang(savedLang ?? browserLocale); this.defaultTranslations = this.loadLanguage("en"); this.translations = this.loadLanguage(userLang); @@ -110,7 +110,7 @@ export class LangSelector extends LitElement { } private loadLanguage(lang: string): Record { - const language = this.languageMap[lang] || {}; + const language = this.languageMap[lang] ?? {}; const flat = flattenTranslations(language); return flat; } diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 0490dd9f0..fa6490e55 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -110,7 +110,7 @@ export class PublicLobby extends LitElement { const teamCount = lobby.gameConfig.gameMode === GameMode.Team - ? lobby.gameConfig.playerTeams || 0 + ? (lobby.gameConfig.playerTeams ?? 0) : null; const mapImageSrc = this.mapImages.get(lobby.gameID); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index b5cb44c30..71cece02b 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -245,16 +245,14 @@ export class Transport { } private startPing() { - if (this.isLocal || this.pingInterval) return; - if (this.pingInterval === null) { - this.pingInterval = window.setInterval(() => { - if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) { - this.sendMsg({ - type: "ping", - } satisfies ClientPingMessage); - } - }, 5 * 1000); - } + if (this.isLocal) return; + this.pingInterval ??= window.setInterval(() => { + if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) { + this.sendMsg({ + type: "ping", + } satisfies ClientPingMessage); + } + }, 5 * 1000); } private stopPing() { diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 88e8fe1b3..98517c45e 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -172,7 +172,7 @@ export function getAltKey(): string { export function getGamesPlayed(): number { try { - return parseInt(localStorage.getItem("gamesPlayed") || "0", 10) || 0; + return parseInt(localStorage.getItem("gamesPlayed") ?? "0", 10) || 0; } catch (error) { console.warn("Failed to read games played from localStorage:", error); return 0; diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 32bffc156..6ef193123 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -91,9 +91,9 @@ const getSpriteForUnit = (unit: UnitView): ImageBitmap | null => { const unitType = unit.type(); if (unitType === UnitType.Train) { const trainType = trainTypeToSpriteType(unit); - return spriteMap.get(trainType) || null; + return spriteMap.get(trainType) ?? null; } - return spriteMap.get(unitType) || null; + return spriteMap.get(unitType) ?? null; }; export const isSpriteReady = (unit: UnitView): boolean => { diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 51247d581..7bb364d9b 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -137,7 +137,7 @@ export class EventsDisplay extends LitElement implements Layer { } private toggleEventFilter(filterName: MessageCategory) { - const currentState = this.eventsFilters.get(filterName) || false; + const currentState = this.eventsFilters.get(filterName) ?? false; this.eventsFilters.set(filterName, !currentState); this.requestUpdate(); } diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index e905f2dea..78bfcbaa3 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -298,14 +298,14 @@ export class RadialMenu implements Layer { const disabled = this.params === null || d.data.disabled(this.params); const color = disabled ? this.config.disabledColor - : d.data.color || "#333333"; + : (d.data.color ?? "#333333"); const opacity = disabled ? 0.5 : 0.7; if (d.data.id === this.selectedItemId && this.currentLevel > level) { return color; } - return d3.color(color)?.copy({ opacity: opacity })?.toString() || color; + return d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color; }) .attr("stroke", "#ffffff") .attr("stroke-width", "2") @@ -341,7 +341,7 @@ export class RadialMenu implements Layer { const color = this.params === null || d.data.disabled(this.params) ? this.config.disabledColor - : d.data.color || "#333333"; + : (d.data.color ?? "#333333"); path.attr("fill", color); } }); @@ -405,11 +405,11 @@ export class RadialMenu implements Layer { path.attr("stroke-width", "2"); const color = disabled ? this.config.disabledColor - : d.data.color || "#333333"; + : (d.data.color ?? "#333333"); const opacity = disabled ? 0.5 : 0.7; path.attr( "fill", - d3.color(color)?.copy({ opacity: opacity })?.toString() || color, + d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color, ); }; @@ -625,7 +625,7 @@ export class RadialMenu implements Layer { this.selectedItemId = null; } - this.currentMenuItems = previousItems || []; + this.currentMenuItems = previousItems ?? []; if (this.currentLevel === 0) { this.updateCenterButtonState("default"); @@ -645,11 +645,11 @@ export class RadialMenu implements Layer { const disabled = this.params === null || item.disabled(this.params); const color = disabled ? this.config.disabledColor - : item.color || "#333333"; + : (item.color ?? "#333333"); const opacity = disabled ? 0.5 : 0.7; selectedPath.attr( "fill", - d3.color(color)?.copy({ opacity: opacity })?.toString() || color, + d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color, ); } } diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts index 4f74982a4..1425fac73 100644 --- a/src/client/graphics/layers/RadialMenuElements.ts +++ b/src/client/graphics/layers/RadialMenuElements.ts @@ -352,9 +352,9 @@ export const buildMenuElement: MenuElement = { : undefined, icon: item.icon, tooltipItems: [ - { text: translateText(item.key || ""), className: "title" }, + { text: translateText(item.key ?? ""), className: "title" }, { - text: translateText(item.description || ""), + text: translateText(item.description ?? ""), className: "description", }, { @@ -401,7 +401,7 @@ export const boatMenuElement: MenuElement = { params.playerActionHandler.handleBoatAttack( params.myPlayer, - params.selected?.id() || null, + params.selected?.id() ?? null, params.tile, spawn !== false ? spawn : null, ); diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts index 872feb42c..87c03746d 100644 --- a/src/client/graphics/layers/TeamStats.ts +++ b/src/client/graphics/layers/TeamStats.ts @@ -52,7 +52,7 @@ export class TeamStats extends LitElement implements Layer { for (const player of players) { const team = player.team(); if (team === null) continue; - if (!grouped[team]) grouped[team] = []; + grouped[team] ??= []; grouped[team].push(player); } diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 3ebd6aca2..c05b8b1b1 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -317,10 +317,16 @@ export class UILayer implements Layer { if (constructionType === undefined) { return 1; } + const constDuration = + this.game.unitInfo(constructionType).constructionDuration; + if (constDuration === undefined) { + throw new Error("unit does not have constructionTime"); + } return ( (this.game.ticks() - unit.createdAt()) / - (this.game.unitInfo(constructionType).constructionDuration || 1) + (constDuration === 0 ? 1 : constDuration) ); + case UnitType.MissileSilo: case UnitType.SAMLauncher: return unit.missileReadinesss(); diff --git a/src/client/jwt.ts b/src/client/jwt.ts index 93221592d..87d64c8f9 100644 --- a/src/client/jwt.ts +++ b/src/client/jwt.ts @@ -104,9 +104,8 @@ export type IsLoggedInResponse = | false; let __isLoggedIn: IsLoggedInResponse | undefined = undefined; export function isLoggedIn(): IsLoggedInResponse { - if (__isLoggedIn === undefined) { - __isLoggedIn = _isLoggedIn(); - } + __isLoggedIn ??= _isLoggedIn(); + return __isLoggedIn; } function _isLoggedIn(): IsLoggedInResponse { diff --git a/src/core/Util.ts b/src/core/Util.ts index b83bbe894..06d2246b2 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -119,7 +119,7 @@ export function getMode(list: Set): number { // Count occurrences const counts = new Map(); for (const item of list) { - counts.set(item, (counts.get(item) || 0) + 1); + counts.set(item, (counts.get(item) ?? 0) + 1); } // Find the item with the highest count diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 1926d9991..5038c28ad 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -97,11 +97,9 @@ export class AttackExecution implements Execution { } } - if (this.startTroops === null) { - this.startTroops = this.mg - .config() - .attackAmount(this._owner, this.target); - } + this.startTroops ??= this.mg + .config() + .attackAmount(this._owner, this.target); if (this.removeTroops) { this.startTroops = Math.min(this._owner.troops(), this.startTroops); this._owner.removeTroops(this.startTroops); diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index c4b65939f..5f43b10eb 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -19,9 +19,7 @@ export class DonateGoldExecution implements Execution { } this.recipient = mg.player(this.recipientID); - if (this.gold === null) { - this.gold = this.sender.gold() / 3n; - } + this.gold ??= this.sender.gold() / 3n; } tick(ticks: number): void { diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 0570ac641..de737357f 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -19,9 +19,7 @@ export class DonateTroopsExecution implements Execution { } this.recipient = mg.player(this.recipientID); - if (this.troops === null) { - this.troops = mg.config().defaultDonationAmount(this.sender); - } + this.troops ??= mg.config().defaultDonationAmount(this.sender); const maxDonation = mg.config().maxPopulation(this.recipient) - this.recipient.population(); this.troops = Math.min(this.troops, maxDonation); diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 35fe39e93..1ff7f0b03 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -109,9 +109,7 @@ export class SAMLauncherExecution implements Execution { this.player = this.sam.owner(); } - if (this.pseudoRandom === undefined) { - this.pseudoRandom = new PseudoRandom(this.sam.id()); - } + this.pseudoRandom ??= new PseudoRandom(this.sam.id()); const mirvWarheadTargets = this.mg.nearbyUnits( this.sam.tile(), diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index 2d94a0518..4cf680805 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -31,13 +31,11 @@ export class SAMMissileExecution implements Execution { } tick(ticks: number): void { - if (this.SAMMissile === undefined) { - this.SAMMissile = this._owner.buildUnit( - UnitType.SAMMissile, - this.spawn, - {}, - ); - } + this.SAMMissile ??= this._owner.buildUnit( + UnitType.SAMMissile, + this.spawn, + {}, + ); if (!this.SAMMissile.isActive()) { this.active = false; return; diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 456585198..c86362c2c 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -23,9 +23,7 @@ export class ShellExecution implements Execution { } tick(ticks: number): void { - if (this.shell === undefined) { - this.shell = this._owner.buildUnit(UnitType.Shell, this.spawn, {}); - } + this.shell ??= this._owner.buildUnit(UnitType.Shell, this.spawn, {}); if (!this.shell.isActive()) { this.active = false; return; diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 291e5551f..dd589ad00 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -89,11 +89,9 @@ export class TransportShipExecution implements Execution { this.target = mg.player(this.targetID); } - if (this.troops === null) { - this.troops = this.mg - .config() - .boatAttackAmount(this.attacker, this.target); - } + this.troops ??= this.mg + .config() + .boatAttackAmount(this.attacker, this.target); this.troops = Math.min(this.troops, this.attacker.troops()); diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index e6bf174a8..939f1e09c 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -507,9 +507,7 @@ export class GameView implements GameMap { } myPlayer(): PlayerView | null { - if (this._myPlayer === null) { - this._myPlayer = this.playerByClientID(this._myClientID); - } + this._myPlayer ??= this.playerByClientID(this._myClientID); return this._myPlayer; } diff --git a/src/core/game/StatsImpl.ts b/src/core/game/StatsImpl.ts index 801a17cf0..bd77d1dff 100644 --- a/src/core/game/StatsImpl.ts +++ b/src/core/game/StatsImpl.ts @@ -66,7 +66,7 @@ export class StatsImpl implements Stats { private _addAttack(player: Player, index: number, value: BigIntLike) { const p = this._makePlayerStats(player); if (p === undefined) return; - if (p.attacks === undefined) p.attacks = [0n]; + p.attacks ??= [0n]; while (p.attacks.length <= index) p.attacks.push(0n); p.attacks[index] += _bigint(value); } @@ -89,8 +89,8 @@ export class StatsImpl implements Stats { ) { const p = this._makePlayerStats(player); if (p === undefined) return; - if (p.boats === undefined) p.boats = { [type]: [0n] }; - if (p.boats[type] === undefined) p.boats[type] = [0n]; + p.boats ??= { [type]: [0n] }; + p.boats[type] ??= [0n]; while (p.boats[type].length <= index) p.boats[type].push(0n); p.boats[type][index] += _bigint(value); } @@ -104,8 +104,8 @@ export class StatsImpl implements Stats { const type = unitTypeToBombUnit[nukeType]; const p = this._makePlayerStats(player); if (p === undefined) return; - if (p.bombs === undefined) p.bombs = { [type]: [0n] }; - if (p.bombs[type] === undefined) p.bombs[type] = [0n]; + p.bombs ??= { [type]: [0n] }; + p.bombs[type] ??= [0n]; while (p.bombs[type].length <= index) p.bombs[type].push(0n); p.bombs[type][index] += _bigint(value); } @@ -113,7 +113,7 @@ export class StatsImpl implements Stats { private _addGold(player: Player, index: number, value: BigIntLike) { const p = this._makePlayerStats(player); if (p === undefined) return; - if (p.gold === undefined) p.gold = [0n]; + p.gold ??= [0n]; while (p.gold.length <= index) p.gold.push(0n); p.gold[index] += _bigint(value); } @@ -127,8 +127,8 @@ export class StatsImpl implements Stats { const type = unitTypeToOtherUnit[otherUnitType]; const p = this._makePlayerStats(player); if (p === undefined) return; - if (p.units === undefined) p.units = { [type]: [0n] }; - if (p.units[type] === undefined) p.units[type] = [0n]; + p.units ??= { [type]: [0n] }; + p.units[type] ??= [0n]; while (p.units[type].length <= index) p.units[type].push(0n); p.units[type][index] += _bigint(value); } diff --git a/src/core/game/TerrainMapFileLoader.ts b/src/core/game/TerrainMapFileLoader.ts index e225dd56b..62bca0a2c 100644 --- a/src/core/game/TerrainMapFileLoader.ts +++ b/src/core/game/TerrainMapFileLoader.ts @@ -26,9 +26,7 @@ class GameMapLoader { private createLazyLoader(importFn: () => Promise): () => Promise { let cache: Promise | null = null; return () => { - if (!cache) { - cache = importFn(); - } + cache ??= importFn(); return cache; }; } diff --git a/src/server/Archive.ts b/src/server/Archive.ts index e9d4cdd55..9f5cfc6b5 100644 --- a/src/server/Archive.ts +++ b/src/server/Archive.ts @@ -37,7 +37,7 @@ export async function archive(gameRecord: GameRecord) { } } catch (error) { log.error(`${gameRecord.info.gameID}: Final archive error: ${error}`, { - message: error?.message || error, + message: error?.message ?? error, stack: error?.stack, name: error?.name, ...(error && typeof error === "object" ? error : {}), @@ -70,7 +70,7 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) { log.info(`${info.gameID}: successfully wrote game analytics to R2`); } catch (error) { log.error(`${info.gameID}: Error writing game analytics to R2: ${error}`, { - message: error?.message || error, + message: error?.message ?? error, stack: error?.stack, name: error?.name, ...(error && typeof error === "object" ? error : {}), @@ -119,7 +119,7 @@ export async function readGameRecord( } catch (error) { // Log the error for monitoring purposes log.error(`${gameId}: Error reading game record from R2: ${error}`, { - message: error?.message || error, + message: error?.message ?? error, stack: error?.stack, name: error?.name, ...(error && typeof error === "object" ? error : {}), @@ -142,7 +142,7 @@ export async function gameRecordExists(gameId: GameID): Promise { return false; } log.error(`${gameId}: Error checking archive existence: ${error}`, { - message: error?.message || error, + message: error?.message ?? error, stack: error?.stack, name: error?.name, ...(error && typeof error === "object" ? error : {}), diff --git a/src/server/Cloudflare.ts b/src/server/Cloudflare.ts index aac1f3bf7..758706937 100644 --- a/src/server/Cloudflare.ts +++ b/src/server/Cloudflare.ts @@ -162,7 +162,9 @@ export class Cloudflare { ); const credentials = { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing AccountTag: tokenData.a || this.accountId, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing TunnelID: tokenData.t || tunnelId, TunnelName: tunnelName, TunnelSecret: tokenData.s, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 73ccbd97c..76e7615b0 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -694,7 +694,7 @@ export class GameServer { for (const client of this.activeClients) { if (client.hashes.has(turnNumber)) { const clientHash = client.hashes.get(turnNumber)!; - counts.set(clientHash, (counts.get(clientHash) || 0) + 1); + counts.set(clientHash, (counts.get(clientHash) ?? 0) + 1); } } diff --git a/src/server/Master.ts b/src/server/Master.ts index 3a131b12e..2b835ff85 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -146,8 +146,9 @@ app.get( "/api/env", gatekeeper.httpHandler(LimiterType.Get, async (req, res) => { const envConfig = { - game_env: process.env.GAME_ENV || "prod", + game_env: process.env.GAME_ENV, }; + if (!envConfig.game_env) return res.sendStatus(500); res.json(envConfig); }), ); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 489c0258f..b15e5262b 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -29,7 +29,7 @@ import { initWorkerMetrics } from "./WorkerMetrics"; const config = getServerConfigFromServer(); -const workerId = parseInt(process.env.WORKER_ID || "0"); +const workerId = parseInt(process.env.WORKER_ID ?? "0"); const log = logger.child({ comp: `w_${workerId}` }); // Worker setup @@ -94,6 +94,7 @@ export function startWorker() { log.warn(`cannot create game, id not found`); return res.status(400).json({ error: "Game ID is required" }); } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const clientIP = req.ip || req.socket.remoteAddress || "unknown"; const result = CreateGameInputSchema.safeParse(req.body); if (!result.success) { @@ -140,6 +141,7 @@ export function startWorker() { return; } if (game.isPublic()) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const clientIP = req.ip || req.socket.remoteAddress || "unknown"; log.info( `cannot start public game ${game.id}, game is public, ip: ${ipAnonymize(clientIP)}`, @@ -171,6 +173,7 @@ export function startWorker() { return res.status(400).json({ error: "Game not found" }); } if (game.isPublic()) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const clientIP = req.ip || req.socket.remoteAddress || "unknown"; log.warn( `cannot update public game ${game.id}, ip: ${ipAnonymize(clientIP)}`, @@ -296,7 +299,8 @@ export function startWorker() { const forwarded = req.headers["x-forwarded-for"]; const ip = Array.isArray(forwarded) ? forwarded[0] - : forwarded || req.socket.remoteAddress || "unknown"; + : // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + forwarded || req.socket.remoteAddress || "unknown"; try { // Parse and handle client messages