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 <openfrontio@gmail.com>
This commit is contained in:
Scott Anderson
2025-05-15 19:39:40 -04:00
committed by GitHub
parent 369483b4ac
commit 70745faac4
119 changed files with 1428 additions and 1123 deletions
+32 -19
View File
@@ -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<TerrainMapData> | 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<TerrainMapData> | null,
): Promise<ClientGameRunner> {
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),
+1 -1
View File
@@ -26,7 +26,7 @@ export class FlagInput extends LitElement {
}
private setFlag(flag: string) {
if (flag == "xx") {
if (flag === "xx") {
flag = "";
}
this.flag = flag;
+2 -2
View File
@@ -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
) {
+8 -15
View File
@@ -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()}`,
)}
></map-display>
</div>
@@ -233,7 +232,7 @@ export class HostLobbyModal extends LitElement {
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
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`
<label
class="option-card ${this.disabledUnits.includes(
unitType,
@@ -405,7 +404,7 @@ export class HostLobbyModal extends LitElement {
}
</button>
</div>
</div>
</o-modal>
`;
@@ -503,10 +502,6 @@ export class HostLobbyModal extends LitElement {
this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
}
private handleDisableNukesChange(e: Event) {
this.disableNukes = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
}
private async handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
@@ -537,16 +532,14 @@ export class HostLobbyModal extends LitElement {
gameMap: this.selectedMap,
difficulty: this.selectedDifficulty,
disableNPCs: this.disableNPCs,
disableNukes: this.disableNukes,
bots: this.bots,
infiniteGold: this.infiniteGold,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
gameMode: this.gameMode,
numPlayerTeams: this.teamCount,
disabledUnits: this.disabledUnits,
playerTeams: this.teamCount,
} as GameConfig),
} satisfies Partial<GameConfig>),
},
);
return response;
@@ -607,7 +600,7 @@ export class HostLobbyModal extends LitElement {
.then((response) => response.json())
.then((data: GameInfo) => {
console.log(`got game info response: ${JSON.stringify(data)}`);
this.players = data.clients.map((p) => p.username);
this.players = data.clients?.map((p) => p.username) ?? [];
});
}
}
+5 -3
View File
@@ -99,7 +99,7 @@ export class InputHandler {
private alternateView = false;
private moveInterval: NodeJS.Timeout = null;
private moveInterval: NodeJS.Timeout | null = null;
private activeKeys = new Set<string>();
private readonly PAN_SPEED = 5;
@@ -300,7 +300,7 @@ export class InputHandler {
Math.abs(event.x - this.lastPointerDownX) +
Math.abs(event.y - this.lastPointerDownY);
if (dist < 10) {
if (event.pointerType == "touch") {
if (event.pointerType === "touch") {
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
event.preventDefault();
return;
@@ -385,7 +385,9 @@ export class InputHandler {
}
destroy() {
clearInterval(this.moveInterval);
if (this.moveInterval !== null) {
clearInterval(this.moveInterval);
}
this.activeKeys.clear();
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ export class JoinPrivateLobbyModal extends LitElement {
@state() private hasJoined = false;
@state() private players: string[] = [];
private playersInterval = null;
private playersInterval: NodeJS.Timeout | null = null;
render() {
return html`
@@ -98,7 +98,7 @@ export class JoinPrivateLobbyModal extends LitElement {
}
public close() {
this.lobbyIdInput.value = null;
this.lobbyIdInput.value = "";
this.modalEl?.close();
if (this.playersInterval) {
clearInterval(this.playersInterval);
@@ -263,7 +263,7 @@ export class JoinPrivateLobbyModal extends LitElement {
)
.then((response) => response.json())
.then((data: GameInfo) => {
this.players = data.clients.map((p) => p.username);
this.players = data.clients?.map((p) => p.username) ?? [];
})
.catch((error) => {
consolex.error("Error polling players:", error);
+4 -4
View File
@@ -3,7 +3,7 @@ import { GameConfig, GameID, GameRecord } from "../core/Schemas";
export interface LocalStatsData {
[key: GameID]: {
lobby: GameConfig;
lobby: Partial<GameConfig>;
// Only once the game is over
gameRecord?: GameRecord;
};
@@ -26,8 +26,8 @@ function save(stats: LocalStatsData) {
// The user can quit the game anytime so better save the lobby as soon as the
// game starts.
export function startGame(id: GameID, lobby: GameConfig) {
if (typeof localStorage === "undefined") {
export function startGame(id: GameID, lobby: Partial<GameConfig>) {
if (localStorage === undefined) {
return;
}
@@ -42,7 +42,7 @@ export function startTime() {
}
export function endGame(gameRecord: GameRecord) {
if (typeof localStorage === "undefined") {
if (localStorage === undefined) {
return;
}
+15 -9
View File
@@ -26,7 +26,7 @@ export class LocalServer {
private paused = false;
private winner: ClientSendWinnerMessage = null;
private winner: ClientSendWinnerMessage | null = null;
private allPlayersStats: AllPlayersStats = {};
private turnsExecuted = 0;
@@ -43,7 +43,7 @@ export class LocalServer {
start() {
this.turnCheckInterval = setInterval(() => {
if (this.turnsExecuted == this.turns.length) {
if (this.turnsExecuted === this.turns.length) {
if (
this.isReplay ||
Date.now() >
@@ -62,6 +62,9 @@ export class LocalServer {
this.lobbyConfig.gameRecord,
).turns;
}
if (this.lobbyConfig.gameStartInfo === undefined) {
throw new Error("missing gameStartInfo");
}
this.clientMessage(
ServerStartGameMessageSchema.parse({
type: "start",
@@ -84,13 +87,13 @@ export class LocalServer {
const clientMsg: ClientMessage = ClientMessageSchema.parse(
JSON.parse(message),
);
if (clientMsg.type == "intent") {
if (clientMsg.type === "intent") {
if (this.lobbyConfig.gameRecord) {
// If we are replaying a game, we don't want to process intents
return;
}
if (this.paused) {
if (clientMsg.intent.type == "troop_ratio") {
if (clientMsg.intent.type === "troop_ratio") {
// Store troop change events because otherwise they are
// not registered when game is paused.
this.intents.push(clientMsg.intent);
@@ -99,7 +102,7 @@ export class LocalServer {
}
this.intents.push(clientMsg.intent);
}
if (clientMsg.type == "hash") {
if (clientMsg.type === "hash") {
if (!this.lobbyConfig.gameRecord) {
// If we are playing a singleplayer then store hash.
this.turns[clientMsg.turnNumber].hash = clientMsg.hash;
@@ -113,7 +116,7 @@ export class LocalServer {
);
return;
}
if (archivedHash != clientMsg.hash) {
if (archivedHash !== clientMsg.hash) {
console.error(
`desync detected on turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}, server hash: ${archivedHash}`,
);
@@ -131,7 +134,7 @@ export class LocalServer {
);
}
}
if (clientMsg.type == "winner") {
if (clientMsg.type === "winner") {
this.winner = clientMsg;
this.allPlayersStats = clientMsg.allPlayersStats;
}
@@ -172,6 +175,9 @@ export class LocalServer {
clientID: this.lobbyConfig.clientID,
},
];
if (this.lobbyConfig.gameStartInfo === undefined) {
throw new Error("missing gameStartInfo");
}
const record = createGameRecord(
this.lobbyConfig.gameStartInfo.gameID,
this.lobbyConfig.gameStartInfo,
@@ -179,8 +185,8 @@ export class LocalServer {
this.turns,
this.startedAt,
Date.now(),
this.winner?.winner,
this.winner?.winnerType,
this.winner?.winner ?? null,
this.winner?.winnerType ?? null,
this.allPlayersStats,
);
if (!saveFullGame) {
+38 -29
View File
@@ -46,7 +46,7 @@ export interface JoinLobbyEvent {
}
class Client {
private gameStop: () => void;
private gameStop: (() => void) | null = null;
private usernameInput: UsernameInput | null = null;
private flagInput: FlagInput | null = null;
@@ -123,7 +123,7 @@ class Client {
window.addEventListener("beforeunload", () => {
consolex.log("Browser is closing");
if (this.gameStop != null) {
if (this.gameStop !== null) {
this.gameStop();
}
});
@@ -136,8 +136,10 @@ class Client {
"single-player-modal",
) as SinglePlayerModal;
spModal instanceof SinglePlayerModal;
document.getElementById("single-player").addEventListener("click", () => {
if (this.usernameInput.isValid()) {
const singlePlayer = document.getElementById("single-player");
if (singlePlayer === null) throw new Error("Missing single-player");
singlePlayer.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
spModal.open();
}
});
@@ -150,7 +152,9 @@ class Client {
const hlpModal = document.querySelector("help-modal") as HelpModal;
hlpModal instanceof HelpModal;
document.getElementById("help-button").addEventListener("click", () => {
const helpButton = document.getElementById("help-button");
if (helpButton === null) throw new Error("Missing help-button");
helpButton.addEventListener("click", () => {
hlpModal.open();
});
@@ -193,34 +197,39 @@ class Client {
"user-setting",
) as UserSettingModal;
settingsModal instanceof UserSettingModal;
document.getElementById("settings-button").addEventListener("click", () => {
settingsModal.open();
});
document
.getElementById("settings-button")
?.addEventListener("click", () => {
settingsModal.open();
});
const hostModal = document.querySelector(
"host-lobby-modal",
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
document
.getElementById("host-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
hostModal.open();
this.publicLobby.leaveLobby();
}
});
const hostLobbyButton = document.getElementById("host-lobby-button");
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
hostLobbyButton.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
hostModal.open();
this.publicLobby.leaveLobby();
}
});
this.joinModal = document.querySelector(
"join-private-lobby-modal",
) as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal;
document
.getElementById("join-private-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}
});
const joinPrivateLobbyButton = document.getElementById(
"join-private-lobby-button",
);
if (joinPrivateLobbyButton === null)
throw new Error("Missing join-private-lobby-button");
joinPrivateLobbyButton.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
this.joinModal.open();
}
});
if (this.userSettings.darkMode()) {
document.documentElement.classList.add("dark");
@@ -258,7 +267,7 @@ class Client {
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail as JoinLobbyEvent;
consolex.log(`joining lobby ${lobby.gameID}`);
if (this.gameStop != null) {
if (this.gameStop !== null) {
consolex.log("joining lobby, stopping existing game");
this.gameStop();
}
@@ -269,10 +278,10 @@ class Client {
gameID: lobby.gameID,
serverConfig: config,
flag:
this.flagInput.getCurrentFlag() == "xx"
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
? ""
: this.flagInput.getCurrentFlag(),
playerName: this.usernameInput.getCurrentUsername(),
playerName: this.usernameInput?.getCurrentUsername() ?? "",
token: localStorage.getItem("token") ?? getPersistentIDFromCookie(),
clientID: lobby.clientID,
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo,
@@ -280,7 +289,7 @@ class Client {
},
() => {
console.log("Closing modals");
document.getElementById("settings-button").classList.add("hidden");
document.getElementById("settings-button")?.classList.add("hidden");
[
"single-player-modal",
"host-lobby-modal",
@@ -319,7 +328,7 @@ class Client {
(ad as HTMLElement).style.display = "none";
});
if (event.detail.gameConfig?.gameType != GameType.Singleplayer) {
if (event.detail.gameConfig?.gameType !== GameType.Singleplayer) {
window.history.pushState({}, "", `/join/${lobby.gameID}`);
sessionStorage.setItem("inLobby", "true");
}
@@ -328,7 +337,7 @@ class Client {
}
private async handleLeaveLobby(/* event: CustomEvent */) {
if (this.gameStop == null) {
if (this.gameStop === null) {
return;
}
consolex.log("leaving lobby, cancelling game");
+8 -11
View File
@@ -14,7 +14,7 @@ export class PublicLobby extends LitElement {
@state() public isLobbyHighlighted: boolean = false;
@state() private isButtonDebounced: boolean = false;
private lobbiesInterval: number | null = null;
private currLobby: GameInfo = null;
private currLobby: GameInfo | null = null;
private debounceDelay: number = 750;
private lobbyIDToStart = new Map<GameID, number>();
@@ -46,7 +46,8 @@ export class PublicLobby extends LitElement {
// Store the start time on first fetch because endpoint is cached, causing
// the time to appear irregular.
if (!this.lobbyIDToStart.has(l.gameID)) {
this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now());
const msUntilStart = l.msUntilStart ?? 0;
this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now());
}
});
} catch (error) {
@@ -82,17 +83,13 @@ export class PublicLobby extends LitElement {
if (!lobby?.gameConfig) {
return;
}
const timeRemaining = Math.max(
0,
Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000),
);
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
// Format time to show minutes and seconds
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
const playersRemainingBeforeMax =
lobby.gameConfig.maxPlayers - lobby.numClients;
const teamCount =
lobby.gameConfig.gameMode === GameMode.Team
@@ -131,8 +128,8 @@ export class PublicLobby extends LitElement {
)}
</div>
<div class="text-md font-medium text-blue-100">
${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")}
</div>
</div>
@@ -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(
+3 -3
View File
@@ -73,7 +73,7 @@ export class SinglePlayerModal extends LitElement {
.selected=${!this.useRandomMap &&
this.selectedMap === mapValue}
.translation=${translateText(
`map.${mapKey.toLowerCase()}`,
`map.${mapKey?.toLowerCase()}`,
)}
></map-display>
</div>
@@ -204,7 +204,7 @@ export class SinglePlayerModal extends LitElement {
/>
<div class="option-card-title">
<span>${translateText("single_modal.bots")}</span>${this
.bots == 0
.bots === 0
? translateText("single_modal.bots_disabled")
: this.bots}
</div>
@@ -444,7 +444,7 @@ export class SinglePlayerModal extends LitElement {
clientID,
username: usernameInput.getCurrentUsername(),
flag:
flagInput.getCurrentFlag() == "xx"
flagInput.getCurrentFlag() === "xx"
? ""
: flagInput.getCurrentFlag(),
},
+29 -20
View File
@@ -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
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -64,7 +64,7 @@ export class UsernameInput extends LitElement {
this.storeUsername(this.username);
this.validationError = "";
} else {
this.validationError = result.error;
this.validationError = result.error ?? "";
}
}
+2 -2
View File
@@ -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 =>
@@ -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;
}
+3 -1
View File
@@ -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() {
+3
View File
@@ -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();
+5 -3
View File
@@ -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;
}
+11 -9
View File
@@ -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"
/>
<span class="build-name">${translateText(item.key)}</span>
<span class="build-name"
>${item.key && translateText(item.key)}</span
>
<span class="build-description"
>${translateText(item.description)}</span
>${item.description &&
translateText(item.description)}</span
>
<span class="build-cost" translate="no">
${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();
+3 -2
View File
@@ -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;
+5 -2
View File
@@ -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<string, string> = this.selectedPlayer
? { P1: this.selectedPlayer }
: {};
this.eventBus.emit(
new SendQuickChatEvent(
+4 -4
View File
@@ -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,
)})</label
>
<div class="relative h-8">
+1 -1
View File
@@ -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(
+22 -18
View File
@@ -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 {
<button
translate="no"
class="ml-2"
@click=${() =>
this.emitGoToPlayerEvent(attack.attackerID)}
@click=${() => {
attack.attackerID &&
this.emitGoToPlayerEvent(attack.attackerID);
}}
>
${renderTroops(attack.troops)}
${(
@@ -608,7 +610,7 @@ export class EventsDisplay extends LitElement implements Layer {
this.events.sort((a, b) => {
const aPrior = a.priority ?? 100000;
const bPrior = b.priority ?? 100000;
if (aPrior == bPrior) {
if (aPrior === bPrior) {
return a.createdAt - b.createdAt;
}
return bPrior - aPrior;
@@ -665,7 +667,8 @@ export class EventsDisplay extends LitElement implements Layer {
${event.focusID
? html`<button
@click=${() => {
this.emitGoToPlayerEvent(event.focusID);
event.focusID &&
this.emitGoToPlayerEvent(event.focusID);
}}
>
${this.getEventDescription(event)}
@@ -673,7 +676,8 @@ export class EventsDisplay extends LitElement implements Layer {
: event.unitView
? html`<button
@click=${() => {
this.emitGoToUnitEvent(event.unitView);
event.unitView &&
this.emitGoToUnitEvent(event.unitView);
}}
>
${this.getEventDescription(event)}
+17 -11
View File
@@ -28,9 +28,9 @@ export class GoToUnitEvent implements GameEvent {
@customElement("leader-board")
export class Leaderboard extends LitElement implements Layer {
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
public game: GameView | null = null;
public clientID: ClientID | null = null;
public eventBus: EventBus | null = null;
players: Entry[] = [];
@@ -42,6 +42,7 @@ export class Leaderboard extends LitElement implements Layer {
init() {}
tick() {
if (this.game === null) throw new Error("Not initialized");
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
this._shownOnInit = true;
this.showLeaderboard();
@@ -51,18 +52,19 @@ export class Leaderboard extends LitElement implements Layer {
return;
}
if (this.game.ticks() % 10 == 0) {
if (this.game.ticks() % 10 === 0) {
this.updateLeaderboard();
}
}
private updateLeaderboard() {
if (this.clientID == null) {
if (this.game === null) throw new Error("Not initialized");
if (this.clientID === null) {
return;
}
const myPlayer = this.game
.playerViews()
.find((p) => p.clientID() == this.clientID);
const myPlayer =
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
null;
const sorted = this.game
.playerViews()
@@ -86,16 +88,19 @@ export class Leaderboard extends LitElement implements Layer {
),
gold: renderNumber(player.gold()),
troops: renderNumber(troops),
isMyPlayer: player == myPlayer,
isMyPlayer: player === myPlayer,
player: player,
};
});
if (myPlayer != null && this.players.find((p) => p.isMyPlayer) == null) {
if (
myPlayer !== null &&
this.players.find((p) => p.isMyPlayer) === undefined
) {
let place = 0;
for (const p of sorted) {
place++;
if (p == myPlayer) {
if (p === myPlayer) {
break;
}
}
@@ -122,6 +127,7 @@ export class Leaderboard extends LitElement implements Layer {
}
private handleRowClickPlayer(player: PlayerView) {
if (this.eventBus === null) return;
this.eventBus.emit(new GoToPlayerEvent(player));
}
+2 -2
View File
@@ -30,8 +30,8 @@ export class MultiTabModal extends LitElement implements Layer {
tick() {
if (
this.game.inSpawnPhase() ||
this.game.config().gameConfig().gameType == GameType.Singleplayer ||
this.game.config().serverConfig().env() == GameEnv.Dev
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
this.game.config().serverConfig().env() === GameEnv.Dev
) {
return;
}
+21 -20
View File
@@ -22,7 +22,7 @@ class RenderInfo {
constructor(
public player: PlayerView,
public lastRenderCalc: number,
public location: Cell,
public location: Cell | null,
public fontSize: number,
public fontColor: string,
public element: HTMLElement,
@@ -104,7 +104,7 @@ export class NameLayer implements Layer {
}
public tick() {
if (this.game.ticks() % 10 != 0) {
if (this.game.ticks() % 10 !== 0) {
return;
}
const sorted = this.game
@@ -238,7 +238,7 @@ export class NameLayer implements Layer {
renderPlayerInfo(render: RenderInfo) {
if (!render.player.nameLocation() || !render.player.isAlive()) {
this.renders = this.renders.filter((r) => r != render);
this.renders = this.renders.filter((r) => r !== render);
render.element.remove();
return;
}
@@ -355,7 +355,7 @@ export class NameLayer implements Layer {
// Alliance icon
const existingAlliance = iconsDiv.querySelector('[data-icon="alliance"]');
if (myPlayer != null && myPlayer.isAlliedWith(render.player)) {
if (myPlayer !== null && myPlayer.isAlliedWith(render.player)) {
if (!existingAlliance) {
iconsDiv.appendChild(
this.createIconElement(
@@ -372,7 +372,7 @@ export class NameLayer implements Layer {
// Alliance request icon
const data = '[data-icon="alliance-request"]';
const existingRequestAlliance = iconsDiv.querySelector(data);
if (myPlayer != null && render.player.isRequestingAllianceWith(myPlayer)) {
if (myPlayer !== null && render.player.isRequestingAllianceWith(myPlayer)) {
if (!existingRequestAlliance) {
iconsDiv.appendChild(
this.createIconElement(
@@ -389,7 +389,7 @@ export class NameLayer implements Layer {
// Target icon
const existingTarget = iconsDiv.querySelector('[data-icon="target"]');
if (
myPlayer != null &&
myPlayer !== null &&
new Set(myPlayer.transitiveTargets()).has(render.player)
) {
if (!existingTarget) {
@@ -412,11 +412,11 @@ export class NameLayer implements Layer {
.outgoingEmojis()
.filter(
(emoji) =>
emoji.recipientID == AllPlayers ||
emoji.recipientID == myPlayer?.smallID(),
emoji.recipientID === AllPlayers ||
emoji.recipientID === myPlayer?.smallID(),
);
if (this.game.config().userSettings().emojis() && emojis.length > 0) {
if (this.game.config().userSettings()?.emojis() && emojis.length > 0) {
if (!existingEmoji) {
const emojiDiv = document.createElement("div");
emojiDiv.setAttribute("data-icon", "emoji");
@@ -451,8 +451,8 @@ export class NameLayer implements Layer {
}
const nukesSentByOtherPlayer = this.game.units().filter((unit) => {
const isSendingNuke = render.player.id() == unit.owner().id();
const notMyPlayer = !myPlayer || unit.owner().id() != myPlayer.id();
const isSendingNuke = render.player.id() === unit.owner().id();
const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
return (
nukeTypes.includes(unit.type()) &&
isSendingNuke &&
@@ -462,24 +462,25 @@ export class NameLayer implements Layer {
});
const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
const detonationDst = unit.detonationDst();
if (detonationDst === undefined) return false;
const targetId = this.game.owner(detonationDst).id();
return myPlayer && targetId == this.myPlayer.id();
return myPlayer && targetId === myPlayer.id();
});
const existingNuke = iconsDiv.querySelector(
'[data-icon="nuke"]',
) as HTMLImageElement;
if (existingNuke) {
if (nukesSentByOtherPlayer.length == 0) {
if (nukesSentByOtherPlayer.length === 0) {
existingNuke.remove();
} else if (
isMyPlayerTarget &&
existingNuke.src != this.nukeRedIconImage.src
existingNuke.src !== this.nukeRedIconImage.src
) {
existingNuke.src = this.nukeRedIconImage.src;
} else if (
!isMyPlayerTarget &&
existingNuke.src != this.nukeWhiteIconImage.src
existingNuke.src !== this.nukeWhiteIconImage.src
) {
existingNuke.src = this.nukeWhiteIconImage.src;
}
@@ -499,7 +500,7 @@ export class NameLayer implements Layer {
}
// Position element with scale
if (render.location && render.location != oldLocation) {
if (render.location && render.location !== oldLocation) {
const scale = Math.min(baseSize * 0.25, 3);
render.element.style.transform = `translate(${render.location.x}px, ${render.location.y}px) translate(-50%, -50%) scale(${scale})`;
}
@@ -525,12 +526,12 @@ export class NameLayer implements Layer {
}
private getPlayer(): PlayerView | null {
if (this.myPlayer != null) {
if (this.myPlayer !== null) {
return this.myPlayer;
}
this.myPlayer = this.game
.playerViews()
.find((p) => p.clientID() == this.clientID);
this.myPlayer =
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
null;
return this.myPlayer;
}
}
+6 -5
View File
@@ -122,19 +122,20 @@ export class OptionsMenu extends LitElement implements Layer {
init() {
console.log("init called from OptionsMenu");
this.showPauseButton =
this.game.config().gameConfig().gameType == GameType.Singleplayer ||
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
this.game.config().isReplay();
this.isVisible = true;
this.requestUpdate();
}
tick() {
this.hasWinner =
this.hasWinner ||
this.game.updatesSinceLastTick()[GameUpdateType.Win].length > 0;
const updates = this.game.updatesSinceLastTick();
if (updates) {
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
}
if (this.game.inSpawnPhase()) {
this.timer = 0;
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
} else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
this.timer++;
}
this.isVisible = true;
@@ -1,4 +1,4 @@
import { LitElement, html } from "lit";
import { LitElement, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
@@ -177,13 +177,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
let relationHtml = null;
let relationHtml: TemplateResult | null = null;
const attackingTroops = player
.outgoingAttacks()
.map((a) => a.troops)
.reduce((a, b) => a + b, 0);
if (player.type() == PlayerType.FakeHuman && myPlayer != null) {
if (player.type() === PlayerType.FakeHuman && myPlayer !== null) {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
@@ -224,7 +224,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
: ""}
${player.name()}
</div>
${player.team() != null
${player.team() !== null
? html`<div class="text-sm opacity-80">
${translateText("player_info_overlay.team")}: ${player.team()}
</div>`
@@ -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 {
<div
class="bg-opacity-60 bg-gray-900 rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
>
${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) : ""}
</div>
</div>
`;
+21 -19
View File
@@ -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`
<div
@@ -392,7 +394,7 @@ export class PlayerPanel extends LitElement implements Layer {
</button>`
: ""}
</div>
${canEmbargo && other != myPlayer
${canEmbargo && other !== myPlayer
? html`<button
@click=${(e) => this.handleEmbargoClick(e, myPlayer, other)}
class="w-100 h-10 flex items-center justify-center
@@ -402,7 +404,7 @@ export class PlayerPanel extends LitElement implements Layer {
${translateText("player_panel.stop_trade")}
</button>`
: ""}
${!canEmbargo && other != myPlayer
${!canEmbargo && other !== myPlayer
? html`<button
@click=${(e) =>
this.handleStopEmbargoClick(e, myPlayer, other)}
+19 -7
View File
@@ -52,7 +52,16 @@ export class RadialMenu implements Layer {
private originalTileOwner: PlayerView | TerraNullius;
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
private isVisible: boolean = false;
private readonly menuItems = new Map([
private readonly menuItems: Map<
Slot,
{
name: string;
disabled: boolean;
action: () => void;
color?: string | null;
icon?: string | null;
}
> = new Map([
[
Slot.Boat,
{
@@ -105,7 +114,7 @@ export class RadialMenu implements Layer {
e.x,
e.y,
);
if (clickedCell == null) {
if (clickedCell === null) {
return;
}
if (!this.g.isValidCoord(clickedCell.x, clickedCell.y)) {
@@ -113,7 +122,7 @@ export class RadialMenu implements Layer {
}
const tile = this.g.ref(clickedCell.x, clickedCell.y);
const p = this.g.playerByClientID(this.clientID);
if (p == null) {
if (p === null) {
return;
}
this.buildMenu.showMenu(tile);
@@ -280,12 +289,12 @@ export class RadialMenu implements Layer {
if (myPlayer === null || !myPlayer.isAlive()) return;
const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y);
if (this.originalTileOwner.isPlayer()) {
if (this.g.owner(tile) != this.originalTileOwner) {
if (this.g.owner(tile) !== this.originalTileOwner) {
this.closeMenu();
return;
}
} else {
if (this.g.owner(tile).isPlayer() || this.g.owner(tile) == myPlayer) {
if (this.g.owner(tile).isPlayer() || this.g.owner(tile) === myPlayer) {
this.closeMenu();
return;
}
@@ -383,7 +392,7 @@ export class RadialMenu implements Layer {
});
}
if (
actions.buildableUnits.find((bu) => bu.type == UnitType.TransportShip)
actions.buildableUnits.find((bu) => bu.type === UnitType.TransportShip)
?.canBuild
) {
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
@@ -395,6 +404,7 @@ export class RadialMenu implements Layer {
spawnTile = new Cell(this.g.x(spawn), this.g.y(spawn));
}
if (this.clickedCell === null) return;
this.eventBus.emit(
new SendBoatAttackIntentEvent(
this.g.owner(tile).id(),
@@ -446,12 +456,13 @@ export class RadialMenu implements Layer {
return;
}
consolex.log("Center button clicked");
if (this.clickedCell === null) return;
const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y);
if (this.g.inSpawnPhase()) {
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell));
} else {
const myPlayer = this.g.myPlayer();
if (myPlayer != null && this.g.owner(clicked) != myPlayer) {
if (myPlayer !== null && this.g.owner(clicked) !== myPlayer) {
this.eventBus.emit(
new SendAttackIntentEvent(
this.g.owner(clicked).id(),
@@ -478,6 +489,7 @@ export class RadialMenu implements Layer {
action: () => void,
) {
const menuItem = this.menuItems.get(slot);
if (menuItem === undefined) return;
menuItem.action = action;
menuItem.disabled = false;
menuItem.color = color;
+2 -1
View File
@@ -24,13 +24,14 @@ export class SpawnTimer implements Layer {
this.ratios = [];
this.colors = [];
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
return;
}
const teamTiles: Map<Team, number> = new Map();
for (const player of this.game.players()) {
const team = player.team();
if (team === null) throw new Error("Team is null");
const tiles = teamTiles.get(team) ?? 0;
const sum = tiles + player.numTilesOwned();
teamTiles.set(team, sum);
+21 -16
View File
@@ -43,7 +43,7 @@ export class StructureLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private unitIcons: Map<string, ImageData> = new Map();
private theme: Theme = null;
private theme: Theme;
// Configuration for supported unit types only
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
@@ -106,6 +106,7 @@ export class StructureLayer implements Layer {
// Create temporary canvas for icon processing
const tempCanvas = document.createElement("canvas");
const tempContext = tempCanvas.getContext("2d");
if (tempContext === null) throw new Error("2d context not supported");
tempCanvas.width = image.width;
tempCanvas.height = image.height;
@@ -135,11 +136,13 @@ export class StructureLayer implements Layer {
}
tick() {
this.game
.updatesSinceLastTick()
[
GameUpdateType.Unit
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
for (const u of unitUpdates) {
const unit = this.game.unit(u.id);
if (unit === undefined) continue;
this.handleUnitRendering(unit);
}
}
init() {
@@ -149,7 +152,9 @@ export class StructureLayer implements Layer {
redraw() {
console.log("structure layer redrawing");
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d", { alpha: true });
const context = this.canvas.getContext("2d", { alpha: true });
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.game.units().forEach((u) => this.handleUnitRendering(u));
@@ -193,7 +198,7 @@ export class StructureLayer implements Layer {
)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
unit.type() == UnitType.Construction
unit.type() === UnitType.Construction
? underConstructionColor
: this.theme.territoryColor(unit.owner()),
130,
@@ -220,15 +225,15 @@ export class StructureLayer implements Layer {
if (!this.isUnitTypeSupported(unitType)) return;
const config = this.unitConfigs[unitType];
let icon: ImageData;
let icon: ImageData | undefined;
if (unitType == UnitType.SAMLauncher && unit.isCooldown()) {
if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
icon = this.unitIcons.get("reloadingSam");
} else {
icon = this.unitIcons.get(iconType);
}
if (unitType == UnitType.MissileSilo && unit.isCooldown()) {
if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
icon = this.unitIcons.get("reloadingSilo");
} else {
icon = this.unitIcons.get(iconType);
@@ -248,15 +253,15 @@ export class StructureLayer implements Layer {
if (!unit.isActive()) return;
let borderColor = this.theme.borderColor(unit.owner());
if (unitType == UnitType.SAMLauncher && unit.isCooldown()) {
if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
borderColor = reloadingColor;
} else if (unit.type() == UnitType.Construction) {
} else if (unit.type() === UnitType.Construction) {
borderColor = underConstructionColor;
}
if (unitType == UnitType.MissileSilo && unit.isCooldown()) {
if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
borderColor = reloadingColor;
} else if (unit.type() == UnitType.Construction) {
} else if (unit.type() === UnitType.Construction) {
borderColor = underConstructionColor;
}
@@ -277,7 +282,7 @@ export class StructureLayer implements Layer {
unit: UnitView,
) {
let color = this.theme.borderColor(unit.owner());
if (unit.type() == UnitType.Construction) {
if (unit.type() === UnitType.Construction) {
color = underConstructionColor;
}
for (let y = 0; y < height; y++) {
+2 -1
View File
@@ -30,7 +30,7 @@ export class TeamStats extends LitElement implements Layer {
init() {}
tick() {
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
return;
}
@@ -53,6 +53,7 @@ export class TeamStats extends LitElement implements Layer {
const grouped: Record<number, PlayerView[]> = {};
for (const player of players) {
const team = player.team();
if (team === null) continue;
if (!grouped[team]) grouped[team] = [];
grouped[team].push(player);
}
+3 -1
View File
@@ -29,7 +29,9 @@ export class TerrainLayer implements Layer {
redraw(): void {
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.imageData = this.context.getImageData(
0,
+23 -17
View File
@@ -4,7 +4,7 @@ import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { AlternateViewEvent, DragEvent } from "../../InputHandler";
@@ -22,7 +22,7 @@ export class TerritoryLayer implements Layer {
return a.lastUpdate - b.lastUpdate;
});
private random = new PseudoRandom(123);
private theme: Theme = null;
private theme: Theme;
// Used for spawn highlighting
private highlightCanvas: HTMLCanvasElement;
@@ -58,17 +58,18 @@ export class TerritoryLayer implements Layer {
tick() {
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
this.game.updatesSinceLastTick()[GameUpdateType.Unit].forEach((u) => {
const update = u as UnitUpdate;
if (update.unitType == UnitType.DefensePost) {
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
unitUpdates.forEach((update) => {
if (update.unitType === UnitType.DefensePost) {
const tile = update.pos;
this.game
.bfs(tile, euclDistFN(tile, this.game.config().defensePostRange()))
.forEach((t) => {
if (
this.game.isBorder(t) &&
(this.game.ownerID(t) == update.ownerID ||
this.game.ownerID(t) == update.lastOwnerID)
(this.game.ownerID(t) === update.ownerID ||
this.game.ownerID(t) === update.lastOwnerID)
) {
this.enqueueTile(t);
}
@@ -90,7 +91,7 @@ export class TerritoryLayer implements Layer {
if (!this.game.inSpawnPhase()) {
return;
}
if (this.game.ticks() % 5 == 0) {
if (this.game.ticks() % 5 === 0) {
return;
}
@@ -102,7 +103,7 @@ export class TerritoryLayer implements Layer {
);
const humans = this.game
.playerViews()
.filter((p) => p.type() == PlayerType.Human);
.filter((p) => p.type() === PlayerType.Human);
for (const human of humans) {
const center = human.nameLocation();
@@ -114,10 +115,11 @@ export class TerritoryLayer implements Layer {
continue;
}
let color = this.theme.spawnHighlightColor();
const myPlayer = this.game.myPlayer();
if (
this.game.myPlayer() != null &&
this.game.myPlayer() != human &&
this.game.myPlayer().isFriendly(human)
myPlayer !== null &&
myPlayer !== human &&
myPlayer.isFriendly(human)
) {
color = this.theme.selfColor();
}
@@ -150,7 +152,9 @@ export class TerritoryLayer implements Layer {
redraw() {
console.log("redrew territory layer");
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.imageData = this.context.getImageData(
0,
@@ -165,9 +169,11 @@ export class TerritoryLayer implements Layer {
// Add a second canvas for highlights
this.highlightCanvas = document.createElement("canvas");
this.highlightContext = this.highlightCanvas.getContext("2d", {
const highlightContext = this.highlightCanvas.getContext("2d", {
alpha: true,
});
if (highlightContext === null) throw new Error("2d context not supported");
this.highlightContext = highlightContext;
this.highlightCanvas.width = this.game.width();
this.highlightCanvas.height = this.game.height();
@@ -219,7 +225,7 @@ export class TerritoryLayer implements Layer {
renderTerritory() {
let numToRender = Math.floor(this.tileToRenderQueue.size() / 10);
if (numToRender == 0 || this.game.inSpawnPhase()) {
if (numToRender === 0 || this.game.inSpawnPhase()) {
numToRender = this.tileToRenderQueue.size();
}
@@ -252,7 +258,7 @@ export class TerritoryLayer implements Layer {
}
const owner = this.game.owner(tile) as PlayerView;
if (this.game.isBorder(tile)) {
const playerIsFocused = owner && this.game.focusedPlayer() == owner;
const playerIsFocused = owner && this.game.focusedPlayer() === owner;
if (
this.game.hasUnitNearby(
tile,
@@ -265,7 +271,7 @@ export class TerritoryLayer implements Layer {
const x = this.game.x(tile);
const y = this.game.y(tile);
const lightTile =
(x % 2 == 0 && y % 2 == 0) || (y % 2 == 1 && x % 2 == 1);
(x % 2 === 0 && y % 2 === 0) || (y % 2 === 1 && x % 2 === 1);
const borderColor = lightTile ? borderColors.light : borderColors.dark;
this.paintCell(x, y, borderColor, 255);
} else {
+12 -9
View File
@@ -23,18 +23,21 @@ export class TopBar extends LitElement implements Layer {
}
tick() {
if (this.game?.myPlayer() !== null) {
const popIncreaseRate =
this.game.myPlayer().population() - this._population;
if (this.game.ticks() % 5 == 0) {
this._popRateIsIncreasing =
popIncreaseRate >= this._lastPopulationIncreaseRate;
this._lastPopulationIncreaseRate = popIncreaseRate;
}
}
this.updatePopulationIncrease();
this.requestUpdate();
}
private updatePopulationIncrease() {
const player = this.game?.myPlayer();
if (player === null) return;
const popIncreaseRate = player.population() - this._population;
if (this.game.ticks() % 5 === 0) {
this._popRateIsIncreasing =
popIncreaseRate >= this._lastPopulationIncreaseRate;
this._lastPopulationIncreaseRate = popIncreaseRate;
}
}
render() {
if (!this.isVisible) {
return html``;
+5 -2
View File
@@ -14,9 +14,9 @@ import { Layer } from "./Layer";
*/
export class UILayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private context: CanvasRenderingContext2D | null;
private theme: Theme = null;
private theme: Theme | null = null;
private selectionAnimTime = 0;
// Keep track of currently selected unit
@@ -136,6 +136,7 @@ export class UILayer implements Layer {
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
// Get the unit's owner color for the box
if (this.theme === null) throw new Error("missing theme");
const ownerColor = this.theme.territoryColor(unit.owner());
// Create a brighter version of the owner color for the selection
@@ -196,12 +197,14 @@ export class UILayer implements Layer {
}
paintCell(x: number, y: number, color: Colord, alpha: number) {
if (this.context === null) throw new Error("null context");
this.clearCell(x, y);
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
this.context.fillRect(x, y, 1, 1);
}
clearCell(x: number, y: number) {
if (this.context === null) throw new Error("null context");
this.context.clearRect(x, y, 1, 1);
}
}
+29 -26
View File
@@ -36,7 +36,7 @@ export class UnitLayer implements Layer {
private unitToTrail = new Map<UnitView, TileRef[]>();
private theme: Theme = null;
private theme: Theme;
private alternateView = false;
@@ -67,7 +67,7 @@ export class UnitLayer implements Layer {
}
tick() {
if (this.myPlayer == null) {
if (this.myPlayer === null) {
this.myPlayer = this.game.playerByClientID(this.clientID);
}
@@ -96,7 +96,7 @@ export class UnitLayer implements Layer {
const clickRef = this.game.ref(cell.x, cell.y);
// Make sure we have the current player
if (this.myPlayer == null) {
if (this.myPlayer === null) {
this.myPlayer = this.game.playerByClientID(this.clientID);
}
@@ -189,9 +189,13 @@ export class UnitLayer implements Layer {
redraw() {
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.transportShipTrailCanvas = document.createElement("canvas");
this.unitTrailContext = this.transportShipTrailCanvas.getContext("2d");
const trailContext = this.transportShipTrailCanvas.getContext("2d");
if (trailContext === null) throw new Error("2d context not supported");
this.unitTrailContext = trailContext;
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
@@ -217,15 +221,13 @@ export class UnitLayer implements Layer {
private updateUnitsSprites() {
const unitsToUpdate = this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id));
unitsToUpdate
?.filter((UnitView) => isSpriteReady(UnitView.type()))
.forEach((unitView) => {
this.clearUnitCells(unitView);
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
?.forEach((unitView) => {
if (unitView === undefined) return;
const ready = isSpriteReady(unitView.type());
if (ready) this.clearUnitCells(unitView);
this.onUnitEvent(unitView);
});
unitsToUpdate?.forEach((unitView) => {
this.onUnitEvent(unitView);
});
}
private clearUnitCells(unit: UnitView) {
@@ -243,10 +245,10 @@ export class UnitLayer implements Layer {
}
private relationship(unit: UnitView): Relationship {
if (this.myPlayer == null) {
if (this.myPlayer === null) {
return Relationship.Enemy;
}
if (this.myPlayer == unit.owner()) {
if (this.myPlayer === unit.owner()) {
return Relationship.Self;
}
if (this.myPlayer.isFriendly(unit.owner())) {
@@ -301,8 +303,8 @@ export class UnitLayer implements Layer {
// Clear current and previous positions
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
if (this.oldShellTile.has(unit)) {
const oldTile = this.oldShellTile.get(unit);
const oldTile = this.oldShellTile.get(unit);
if (oldTile !== undefined) {
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
}
@@ -348,7 +350,7 @@ export class UnitLayer implements Layer {
}
private clearTrail(unit: UnitView) {
const trail = this.unitToTrail.get(unit);
const trail = this.unitToTrail.get(unit) ?? [];
const rel = this.relationship(unit);
for (const t of trail) {
this.clearCell(this.game.x(t), this.game.y(t), this.unitTrailContext);
@@ -381,7 +383,7 @@ export class UnitLayer implements Layer {
}
let newTrailSize = 1;
const trail = this.unitToTrail.get(unit);
const trail = this.unitToTrail.get(unit) ?? [];
// It can move faster than 1 pixel, draw a line for the trail or else it will be dotted
if (trail.length >= 1) {
const cur = {
@@ -441,7 +443,7 @@ export class UnitLayer implements Layer {
if (!this.unitToTrail.has(unit)) {
this.unitToTrail.set(unit, []);
}
const trail = this.unitToTrail.get(unit);
const trail = this.unitToTrail.get(unit) ?? [];
trail.push(unit.lastTile());
// Paint trail
@@ -496,15 +498,16 @@ export class UnitLayer implements Layer {
const x = this.game.x(unit.tile());
const y = this.game.y(unit.tile());
let alternateViewColor = null;
let alternateViewColor: Colord | null = null;
if (this.alternateView) {
let rel = this.relationship(unit);
if (unit.type() == UnitType.TradeShip && unit.dstPortId() != null) {
const target = this.game.unit(unit.dstPortId())?.owner();
const dstPortId = unit.dstPortId();
if (unit.type() === UnitType.TradeShip && dstPortId !== undefined) {
const target = this.game.unit(dstPortId)?.owner();
const myPlayer = this.game.myPlayer();
if (myPlayer != null && target != null) {
if (myPlayer == target) {
if (myPlayer !== null && target !== undefined) {
if (myPlayer === target) {
rel = Relationship.Self;
} else if (myPlayer.isFriendly(target)) {
rel = Relationship.Ally;
@@ -528,7 +531,7 @@ export class UnitLayer implements Layer {
unit,
this.theme,
alternateViewColor ?? customTerritoryColor,
alternateViewColor,
alternateViewColor ?? undefined,
);
if (unit.isActive()) {
+11 -6
View File
@@ -182,12 +182,14 @@ export class WinModal extends LitElement implements Layer {
this._title = translateText("win_modal.died");
this.show();
}
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
const updates = this.game.updatesSinceLastTick();
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
winUpdates.forEach((wu) => {
if (wu.winnerType === "team") {
this.eventBus.emit(
new SendWinnerEvent(wu.winner as Team, wu.allPlayersStats, "team"),
);
if (wu.winner == this.game.myPlayer()?.team()) {
if (wu.winner === this.game.myPlayer()?.team()) {
this._title = translateText("win_modal.your_team");
} else {
this._title = translateText("win_modal.other_team", {
@@ -199,10 +201,13 @@ export class WinModal extends LitElement implements Layer {
const winner = this.game.playerBySmallID(
wu.winner as number,
) as PlayerView;
this.eventBus.emit(
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"),
);
if (winner == this.game.myPlayer()) {
const winnerClient = winner.clientID();
if (winnerClient !== null) {
this.eventBus.emit(
new SendWinnerEvent(winnerClient, wu.allPlayersStats, "player"),
);
}
if (winner === this.game.myPlayer()) {
this._title = translateText("win_modal.you_won");
} else {
this._title = translateText("win_modal.you_won", {