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
+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", {