Enable strictPropertyInitialization (#1909)

## Description:

Enable the tsconfig option `strictPropertyInitialization`.

Fixes #1907

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
This commit is contained in:
Scott Anderson
2025-08-23 19:21:40 -04:00
committed by GitHub
parent f5316cc378
commit 51519b0b9d
76 changed files with 599 additions and 367 deletions
+1 -1
View File
@@ -573,7 +573,7 @@ export class ClientGameRunner {
if (!this.myPlayer) return;
this.myPlayer.bestTransportShipSpawn(tile).then((spawn: number | false) => {
if (this.myPlayer === null) throw new Error("not initialized");
if (this.myPlayer === null) throw new Error("Not initialized");
this.eventBus.emit(
new SendBoatAttackIntentEvent(
this.gameView.owner(tile).id(),
+2 -2
View File
@@ -24,7 +24,7 @@ export class LocalServer {
private readonly turns: Turn[] = [];
private intents: Intent[] = [];
private startedAt: number;
private startedAt = 0;
private paused = false;
private replaySpeedMultiplier = defaultReplaySpeedMultiplier;
@@ -35,7 +35,7 @@ export class LocalServer {
private turnsExecuted = 0;
private turnStartTime = 0;
private turnCheckInterval: ReturnType<typeof setTimeout>;
private turnCheckInterval: ReturnType<typeof setTimeout> | undefined;
constructor(
private readonly lobbyConfig: LobbyConfig,
+10 -10
View File
@@ -91,8 +91,8 @@ class Client {
private flagInput: FlagInput | null = null;
private darkModeButton: DarkModeButton | null = null;
private joinModal: JoinPrivateLobbyModal;
private publicLobby: PublicLobby;
private joinModal: JoinPrivateLobbyModal | undefined;
private publicLobby: PublicLobby | undefined;
private readonly userSettings: UserSettings = new UserSettings();
constructor() {}
@@ -365,7 +365,7 @@ class Client {
hostLobbyButton.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
hostModal.open();
this.publicLobby.leaveLobby();
this.publicLobby?.leaveLobby();
}
});
@@ -380,7 +380,7 @@ class Client {
throw new Error("Missing join-private-lobby-button");
joinPrivateLobbyButton.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
this.joinModal.open();
this.joinModal?.open();
}
});
@@ -395,7 +395,7 @@ class Client {
const onHashUpdate = () => {
// Reset the UI to its initial state
this.joinModal.close();
this.joinModal?.close();
if (this.gameStop !== null) {
this.handleLeaveLobby();
}
@@ -449,7 +449,7 @@ class Client {
}
const lobbyId = params.get("join");
if (lobbyId && ID.safeParse(lobbyId).success) {
this.joinModal.open(lobbyId);
this.joinModal?.open(lobbyId);
console.log(`joining lobby ${lobbyId}`);
}
}
@@ -509,7 +509,7 @@ class Client {
modal.isModalOpen = false;
}
});
this.publicLobby.stop();
this.publicLobby?.stop();
document.querySelectorAll(".ad").forEach((ad) => {
(ad as HTMLElement).style.display = "none";
});
@@ -522,8 +522,8 @@ class Client {
startingModal.show();
},
() => {
this.joinModal.close();
this.publicLobby.stop();
this.joinModal?.close();
this.publicLobby?.stop();
incrementGamesPlayed();
try {
@@ -550,7 +550,7 @@ class Client {
console.log("leaving lobby, cancelling game");
this.gameStop();
this.gameStop = null;
this.publicLobby.leaveLobby();
this.publicLobby?.leaveLobby();
}
private handleKickPlayer(event: CustomEvent<KickPlayerEvent>) {
+2 -2
View File
@@ -34,7 +34,7 @@ export class TerritoryPatternsModal extends LitElement {
private patterns: Pattern[] = [];
private me: UserMeResponse | null = null;
public resizeObserver: ResizeObserver;
public resizeObserver: ResizeObserver | undefined;
private readonly userSettings: UserSettings = new UserSettings();
@@ -52,7 +52,7 @@ export class TerritoryPatternsModal extends LitElement {
const containers = this.renderRoot.querySelectorAll(".preview-container");
if (this.resizeObserver) {
containers.forEach((container) =>
this.resizeObserver.observe(container),
this.resizeObserver?.observe(container),
);
}
this.updatePreview();
+14 -10
View File
@@ -172,12 +172,12 @@ export class SendKickPlayerIntentEvent implements GameEvent {
export class Transport {
private socket: WebSocket | null = null;
private localServer: LocalServer;
private localServer: LocalServer | undefined;
private readonly buffer: string[] = [];
private onconnect: () => void;
private onmessage: (msg: ServerMessage) => void;
private onconnect: (() => void) | undefined;
private onmessage: ((msg: ServerMessage) => void) | undefined;
private pingInterval: number | null = null;
public readonly isLocal: boolean;
@@ -336,7 +336,7 @@ export class Transport {
console.error("Error parsing server message", error);
return;
}
this.onmessage(result.data);
this.onmessage?.(result.data);
} catch (e) {
console.error("Error in onmessage handler:", e, event.data);
return;
@@ -362,12 +362,14 @@ export class Transport {
}
public reconnect() {
if (this.onconnect === undefined) return;
if (this.onmessage === undefined) return;
this.connect(this.onconnect, this.onmessage);
}
public turnComplete() {
if (this.isLocal) {
this.localServer.turnComplete();
this.localServer?.turnComplete();
}
}
@@ -386,7 +388,7 @@ export class Transport {
leaveGame(saveFullGame = false) {
if (this.isLocal) {
this.localServer.endGame(saveFullGame);
this.localServer?.endGame(saveFullGame);
return;
}
this.stopPing();
@@ -550,9 +552,9 @@ export class Transport {
return;
}
if (event.paused) {
this.localServer.pause();
this.localServer?.pause();
} else {
this.localServer.resume();
this.localServer?.resume();
}
}
@@ -648,7 +650,7 @@ export class Transport {
private sendMsg(msg: ClientMessage) {
if (this.isLocal) {
// Forward message to local server
this.localServer.onMessage(msg);
this.localServer?.onMessage(msg);
return;
} else if (this.socket === null) {
// Socket missing, do nothing
@@ -660,7 +662,9 @@ export class Transport {
console.warn("socket not ready, closing and trying later");
this.socket.close();
this.socket = null;
this.connectRemote(this.onconnect, this.onmessage);
if (this.onconnect && this.onmessage) {
this.connectRemote(this.onconnect, this.onmessage);
}
this.buffer.push(str);
} else {
// Send the message directly
+2 -4
View File
@@ -61,9 +61,7 @@ export function createRenderer(
if (!emojiTable || !(emojiTable instanceof EmojiTable)) {
console.error("EmojiTable element not found in the DOM");
}
emojiTable.transformHandler = transformHandler;
emojiTable.game = game;
emojiTable.initEventBus(eventBus);
emojiTable.init(transformHandler, game, eventBus);
const buildMenu = document.querySelector("build-menu") as BuildMenu;
if (!buildMenu || !(buildMenu instanceof BuildMenu)) {
@@ -237,7 +235,7 @@ export function createRenderer(
new StructureIconsLayer(game, eventBus, transformHandler),
new UnitLayer(game, eventBus, transformHandler),
new FxLayer(game),
new UILayer(game, eventBus, transformHandler),
new UILayer(game, eventBus),
new NameLayer(game, transformHandler, eventBus),
eventsDisplay,
chatDisplay,
+1 -1
View File
@@ -19,7 +19,7 @@ export class TransformHandler {
private offsetY = -200;
private lastGoToCallTime: number | null = null;
private target: Cell | null;
private target: Cell | null = null;
private intervalID: ReturnType<typeof setTimeout> | null = null;
private changed = false;
+1 -1
View File
@@ -58,7 +58,7 @@ export class SpriteFx implements Fx {
theme,
);
if (!this.animatedSprite) {
console.error("Could not load animated sprite", fxType);
throw new Error(`Could not load animated sprite ${fxType}`);
} else {
this.waitToTheEnd = duration ? true : false;
this.duration = duration ?? this.animatedSprite.lifeTime() ?? 1000;
+2 -1
View File
@@ -14,7 +14,7 @@ const ALERT_COUNT = 2;
@customElement("alert-frame")
export class AlertFrame extends LitElement implements Layer {
public game: GameView;
public game: GameView | undefined;
private readonly userSettings: UserSettings = new UserSettings();
@state()
@@ -90,6 +90,7 @@ export class AlertFrame extends LitElement implements Layer {
}
private onBrokeAllianceUpdate(update: BrokeAllianceUpdate) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
+13 -7
View File
@@ -123,15 +123,18 @@ export const flattenedBuildTable = buildTable.flat();
@customElement("build-menu")
export class BuildMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
private clickedTile: TileRef;
public playerActions: PlayerActions | null;
public game: GameView | undefined;
public eventBus: EventBus | undefined;
private clickedTile: TileRef | undefined;
public playerActions: PlayerActions | null = null;
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
public transformHandler: TransformHandler;
public transformHandler: TransformHandler | undefined;
init() {
if (this.eventBus === undefined) throw new Error("Not initialized");
this.eventBus.on(ShowBuildMenuEvent, (e) => {
if (!this.game) return;
if (!this.transformHandler) return;
if (!this.game.myPlayer()?.isAlive()) {
return;
}
@@ -386,7 +389,9 @@ export class BuildMenu extends LitElement implements Layer {
return player.totalUnitLevels(item.unitType).toString();
}
public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile: TileRef): void {
public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile?: TileRef): void {
if (tile === undefined) throw new Error("Missing tile");
if (this.eventBus === undefined) throw new Error("Not initialized");
if (buildableUnit.canUpgrade !== false) {
this.eventBus.emit(
new SendUpgradeStructureIntentEvent(
@@ -481,8 +486,9 @@ export class BuildMenu extends LitElement implements Layer {
}
private refresh() {
if (this.clickedTile === undefined) return;
this.game
.myPlayer()
?.myPlayer()
?.actions(this.clickedTile)
.then((actions) => {
this.playerActions = actions;
+4 -2
View File
@@ -21,8 +21,8 @@ type ChatEvent = {
@customElement("chat-display")
export class ChatDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
public eventBus: EventBus | undefined;
public game: GameView | undefined;
private readonly active = false;
@@ -55,6 +55,7 @@ export class ChatDisplay extends LitElement implements Layer {
onDisplayMessageEvent(event: DisplayMessageUpdate) {
if (event.messageType !== MessageType.CHAT) return;
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (
event.playerID !== null &&
@@ -75,6 +76,7 @@ export class ChatDisplay extends LitElement implements Layer {
tick() {
// this.active = true;
if (!this.game) return;
const updates = this.game.updatesSinceLastTick();
if (updates === null) return;
const messages = updates[GameUpdateType.DisplayEvent] as
+7 -4
View File
@@ -39,11 +39,11 @@ export class ChatModal extends LitElement {
private selectedQuickChatKey: string | null = null;
private selectedPlayer: PlayerView | null = null;
private recipient: PlayerView;
private sender: PlayerView;
public eventBus: EventBus;
private recipient: PlayerView | undefined;
private sender: PlayerView | undefined;
public eventBus: EventBus | undefined;
public g: GameView;
public g: GameView | undefined;
quickChatPhrases: Record<
string,
@@ -220,6 +220,7 @@ export class ChatModal extends LitElement {
}
private sendChatMessage() {
if (!this.eventBus) return;
console.log("Sent message:", this.previewText);
console.log("Sender:", this.sender);
console.log("Recipient:", this.recipient);
@@ -270,6 +271,7 @@ export class ChatModal extends LitElement {
if (sender && recipient) {
console.log("Sent message:", recipient);
console.log("Sent message:", sender);
if (!this.g) throw new Error("Not initialized");
this.players = this.g
.players()
.filter((p) => p.isAlive() && p.data.playerType !== PlayerType.Bot);
@@ -304,6 +306,7 @@ export class ChatModal extends LitElement {
recipient?: PlayerView,
) {
if (sender && recipient) {
if (!this.g) throw new Error("Not initialized");
this.players = this.g
.players()
.filter((p) => p.isAlive() && p.data.playerType !== PlayerType.Bot);
+15 -10
View File
@@ -12,34 +12,36 @@ import { translateText } from "../../../client/Utils";
@customElement("control-panel")
export class ControlPanel extends LitElement implements Layer {
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
public uiState: UIState;
public game: GameView | undefined;
public clientID: ClientID | undefined;
public eventBus: EventBus | undefined;
public uiState: UIState | undefined;
@state()
private attackRatio = 0.2;
@state()
private _maxTroops: number;
private _maxTroops = 0;
@state()
private troopRate: number;
private troopRate = 0;
@state()
private _troops: number;
private _troops = 0;
@state()
private _isVisible = false;
@state()
private _gold: Gold;
private _gold: Gold = 0n;
private _troopRateIsIncreasing = true;
private _lastTroopIncreaseRate: number;
private _lastTroopIncreaseRate = 0;
init() {
if (!this.uiState) throw new Error("Not initialized");
if (!this.eventBus) throw new Error("Not initialized");
this.attackRatio = Number(
localStorage.getItem("settings.attackRatio") ?? "0.2",
);
@@ -71,6 +73,7 @@ export class ControlPanel extends LitElement implements Layer {
}
tick() {
if (!this.game) return;
if (!this._isVisible && !this.game.inSpawnPhase()) {
this.setVisibile(true);
}
@@ -94,7 +97,8 @@ export class ControlPanel extends LitElement implements Layer {
}
private updateTroopIncrease() {
const player = this.game?.myPlayer();
if (this.game === undefined) return;
const player = this.game.myPlayer();
if (player === null) return;
const troopIncreaseRate = this.game.config().troopIncreaseRate(player);
this._troopRateIsIncreasing =
@@ -103,6 +107,7 @@ export class ControlPanel extends LitElement implements Layer {
}
onAttackRatioChange(newRatio: number) {
if (this.uiState === undefined) return;
this.uiState.attackRatio = newRatio;
}
+11 -9
View File
@@ -12,23 +12,25 @@ import { TransformHandler } from "../TransformHandler";
@customElement("emoji-table")
export class EmojiTable extends LitElement {
@state() public isVisible = false;
public transformHandler: TransformHandler;
public game: GameView;
initEventBus(eventBus: EventBus) {
init(
transformHandler: TransformHandler,
game: GameView,
eventBus: EventBus,
) {
eventBus.on(ShowEmojiMenuEvent, (e) => {
this.isVisible = true;
const cell = this.transformHandler.screenToWorldCoordinates(e.x, e.y);
if (!this.game.isValidCoord(cell.x, cell.y)) {
const cell = transformHandler.screenToWorldCoordinates(e.x, e.y);
if (!game.isValidCoord(cell.x, cell.y)) {
return;
}
const tile = this.game.ref(cell.x, cell.y);
if (!this.game.hasOwner(tile)) {
const tile = game.ref(cell.x, cell.y);
if (!game.hasOwner(tile)) {
return;
}
const targetPlayer = this.game.owner(tile);
const targetPlayer = game.owner(tile);
// maybe redundant due to owner check but better safe than sorry
if (targetPlayer instanceof TerraNulliusImpl) {
return;
@@ -36,7 +38,7 @@ export class EmojiTable extends LitElement {
this.showTable((emoji) => {
const recipient =
targetPlayer === this.game.myPlayer()
targetPlayer === game.myPlayer()
? AllPlayers
: (targetPlayer as PlayerView);
eventBus.emit(
+48 -29
View File
@@ -69,8 +69,8 @@ type GameEvent = {
@customElement("events-display")
export class EventsDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
public eventBus: EventBus | undefined;
public game: GameView | undefined;
private active = false;
private events: GameEvent[] = [];
@@ -169,6 +169,7 @@ export class EventsDisplay extends LitElement implements Layer {
tick() {
this.active = true;
if (this.game === undefined) return;
if (!this._isVisible && !this.game.inSpawnPhase()) {
this._isVisible = true;
this.requestUpdate();
@@ -193,6 +194,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
let remainingEvents = this.events.filter((event) => {
if (this.game === undefined) return;
const shouldKeep =
this.game.ticks() - event.createdAt < (event.duration ?? 600);
if (!shouldKeep && event.onDelete) {
@@ -212,7 +214,10 @@ 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();
if (this.game === undefined) return false;
const p = this.game.playerBySmallID(a.attackerID);
if (!p.isPlayer()) return false;
const t = p.type();
return t !== PlayerType.Bot;
});
@@ -239,6 +244,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
private checkForAllianceExpirations() {
if (this.game === undefined) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer?.isAlive()) return;
@@ -273,7 +279,7 @@ export class EventsDisplay extends LitElement implements Layer {
{
text: translateText("events_display.focus"),
className: "btn-gray",
action: () => this.eventBus.emit(new GoToPlayerEvent(other)),
action: () => this.eventBus?.emit(new GoToPlayerEvent(other)),
preventClose: true,
},
{
@@ -282,7 +288,7 @@ export class EventsDisplay extends LitElement implements Layer {
}),
className: "btn",
action: () =>
this.eventBus.emit(new SendAllianceExtensionIntentEvent(other)),
this.eventBus?.emit(new SendAllianceExtensionIntentEvent(other)),
},
{
text: translateText("events_display.ignore"),
@@ -319,6 +325,7 @@ export class EventsDisplay extends LitElement implements Layer {
renderLayer(): void {}
onDisplayMessageEvent(event: DisplayMessageUpdate) {
if (this.game === undefined) return;
const myPlayer = this.game.myPlayer();
if (
event.playerID !== null &&
@@ -367,6 +374,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onDisplayChatEvent(event: DisplayChatMessageUpdate) {
if (this.game === undefined) return;
const myPlayer = this.game.myPlayer();
if (
event.playerID === null ||
@@ -412,6 +420,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onAllianceRequestEvent(update: AllianceRequestUpdate) {
if (this.game === undefined) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer || update.recipientID !== myPlayer.smallID()) {
return;
@@ -432,14 +441,14 @@ export class EventsDisplay extends LitElement implements Layer {
{
text: translateText("events_display.focus"),
className: "btn-gray",
action: () => this.eventBus.emit(new GoToPlayerEvent(requestor)),
action: () => this.eventBus?.emit(new GoToPlayerEvent(requestor)),
preventClose: true,
},
{
text: translateText("events_display.accept_alliance"),
className: "btn",
action: () =>
this.eventBus.emit(
this.eventBus?.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, true),
),
},
@@ -447,7 +456,7 @@ export class EventsDisplay extends LitElement implements Layer {
text: translateText("events_display.reject_alliance"),
className: "btn-info",
action: () =>
this.eventBus.emit(
this.eventBus?.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false),
),
},
@@ -456,7 +465,7 @@ export class EventsDisplay extends LitElement implements Layer {
type: MessageType.ALLIANCE_REQUEST,
createdAt: this.game.ticks(),
onDelete: () =>
this.eventBus.emit(
this.eventBus?.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false),
),
priority: 0,
@@ -466,6 +475,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onAllianceRequestReplyEvent(update: AllianceRequestReplyUpdate) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) {
return;
@@ -508,6 +518,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onBrokeAllianceEvent(update: BrokeAllianceUpdate) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
@@ -547,7 +558,7 @@ export class EventsDisplay extends LitElement implements Layer {
{
text: translateText("events_display.focus"),
className: "btn-gray",
action: () => this.eventBus.emit(new GoToPlayerEvent(traitor)),
action: () => this.eventBus?.emit(new GoToPlayerEvent(traitor)),
preventClose: true,
},
];
@@ -565,6 +576,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onAllianceExpiredEvent(update: AllianceExpiredUpdate) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
@@ -590,6 +602,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onTargetPlayerEvent(event: TargetPlayerUpdate) {
if (!this.game) return;
const other = this.game.playerBySmallID(event.playerID) as PlayerView;
const myPlayer = this.game.myPlayer() as PlayerView;
if (!myPlayer || !myPlayer.isFriendly(other)) return;
@@ -609,32 +622,36 @@ export class EventsDisplay extends LitElement implements Layer {
}
emitCancelAttackIntent(id: string) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelAttackIntentEvent(id));
this.eventBus?.emit(new CancelAttackIntentEvent(id));
}
emitBoatCancelIntent(id: number) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelBoatIntentEvent(id));
this.eventBus?.emit(new CancelBoatIntentEvent(id));
}
emitGoToPlayerEvent(attackerID: number) {
const attacker = this.game.playerBySmallID(attackerID) as PlayerView;
if (!attacker) return;
this.eventBus.emit(new GoToPlayerEvent(attacker));
if (!this.game) return;
const attacker = this.game.playerBySmallID(attackerID);
if (!attacker.isPlayer()) return;
this.eventBus?.emit(new GoToPlayerEvent(attacker));
}
emitGoToPositionEvent(x: number, y: number) {
this.eventBus.emit(new GoToPositionEvent(x, y));
this.eventBus?.emit(new GoToPositionEvent(x, y));
}
emitGoToUnitEvent(unit: UnitView) {
this.eventBus.emit(new GoToUnitEvent(unit));
this.eventBus?.emit(new GoToUnitEvent(unit));
}
onEmojiMessageEvent(update: EmojiUpdate) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
@@ -671,6 +688,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onUnitIncomingEvent(event: UnitIncomingUpdate) {
if (!this.game) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer || myPlayer.smallID() !== event.playerID) {
@@ -698,6 +716,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
private async attackWarningOnClick(attack: AttackUpdate) {
if (!this.game) return;
const playerView = this.game.playerBySmallID(attack.attackerID);
if (playerView !== undefined) {
if (playerView instanceof PlayerView) {
@@ -722,13 +741,13 @@ export class EventsDisplay extends LitElement implements Layer {
${this.incomingAttacks.length > 0
? html`
${this.incomingAttacks.map(
(attack) => html`
(attack) => {
const attacker = this.game?.playerBySmallID(attack.attackerID);
return html`
${this.renderButton({
content: html`
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(attack.attackerID) as PlayerView
)?.name()}
${attacker?.isPlayer() ? attacker.name() : "unknown"}
${attack.retreating
? `(${translateText("events_display.retreating")}...)`
: ""}
@@ -737,7 +756,8 @@ export class EventsDisplay extends LitElement implements Layer {
className: "text-left text-red-400",
translate: false,
})}
`,
`;
},
)}
`
: ""}
@@ -750,16 +770,14 @@ export class EventsDisplay extends LitElement implements Layer {
? html`
<div class="flex flex-wrap gap-y-1 gap-x-2">
${this.outgoingAttacks.map(
(attack) => html`
(attack) => {
const target = this.game?.playerBySmallID(attack.targetID);
return html`
<div class="inline-flex items-center gap-1">
${this.renderButton({
content: html`
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(
attack.targetID,
) as PlayerView
)?.name()}
${target?.isPlayer() ? target.name() : "unknown"}
`,
onClick: async () => this.attackWarningOnClick(attack),
className: "text-left text-blue-400",
@@ -778,7 +796,8 @@ export class EventsDisplay extends LitElement implements Layer {
)}...)</span
>`}
</div>
`,
`;
},
)}
</div>
`
+6 -2
View File
@@ -18,8 +18,8 @@ import { conquestFxFactory } from "../fx/ConquestFx";
import { renderNumber } from "../../Utils";
export class FxLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | undefined;
private lastRefresh = 0;
private readonly refreshRate = 10;
@@ -265,6 +265,7 @@ export class FxLayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.canvas === undefined) throw new Error("Not initialized");
const now = Date.now();
if (this.game.config().userSettings()?.fxLayer()) {
if (now > this.lastRefresh + this.refreshRate) {
@@ -283,6 +284,8 @@ export class FxLayer implements Layer {
}
renderAllFx(context: CanvasRenderingContext2D, delta: number) {
if (this.canvas === undefined) throw new Error("Not initialized");
if (this.context === undefined) throw new Error("Not initialized");
if (this.allFx.length > 0) {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.renderContextFx(delta);
@@ -290,6 +293,7 @@ export class FxLayer implements Layer {
}
renderContextFx(duration: number) {
if (this.context === undefined) throw new Error("Not initialized");
for (let i = this.allFx.length - 1; i >= 0; i--) {
if (!this.allFx[i].renderTick(duration, this.context)) {
this.allFx.splice(i, 1);
@@ -21,7 +21,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
private playerTeam: string | null = null;
private playerColor: Colord = new Colord("#FFFFFF");
public game: GameView;
public game: GameView | undefined;
private _shownOnInit = false;
createRenderRoot() {
@@ -42,6 +42,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
}
tick() {
if (!this.game) throw new Error("Not initialized");
if (!this.playerTeam && this.game.myPlayer()?.team()) {
const myPlayer = this.game.myPlayer();
if (myPlayer !== null) {
@@ -18,8 +18,8 @@ import { translateText } from "../../Utils";
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public game: GameView | undefined;
public eventBus: EventBus | undefined;
@state()
private _isSinglePlayer = false;
@@ -45,13 +45,13 @@ export class GameRightSidebar extends LitElement implements Layer {
init() {
this._isSinglePlayer =
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
this.game.config().isReplay();
(this.game?.config().isReplay() ?? false);
this._isVisible = true;
this.game.inSpawnPhase();
this.requestUpdate();
}
tick() {
if (!this.game) throw new Error("Not initialized");
// Timer logic
const updates = this.game.updatesSinceLastTick();
if (updates) {
@@ -76,18 +76,18 @@ export class GameRightSidebar extends LitElement implements Layer {
private toggleReplayPanel(): void {
this._isReplayVisible = !this._isReplayVisible;
this.eventBus.emit(
this.eventBus?.emit(
new ShowReplayPanelEvent(this._isReplayVisible, this._isSinglePlayer),
);
}
private onPauseButtonClick() {
this.isPaused = !this.isPaused;
this.eventBus.emit(new PauseGameEvent(this.isPaused));
this.eventBus?.emit(new PauseGameEvent(this.isPaused));
}
private onExitButtonClick() {
const isAlive = this.game.myPlayer()?.isAlive();
const isAlive = this.game?.myPlayer()?.isAlive();
if (isAlive) {
const isConfirmed = confirm(
translateText("help_modal.exit_confirmation"),
@@ -99,7 +99,7 @@ export class GameRightSidebar extends LitElement implements Layer {
}
private onSettingsButtonClick() {
this.eventBus.emit(
this.eventBus?.emit(
new ShowSettingsModalEvent(true, this._isSinglePlayer, this.isPaused),
);
}
+2 -1
View File
@@ -10,7 +10,7 @@ export class GutterAdModalEvent implements GameEvent {
@customElement("gutter-ad-modal")
export class GutterAdModal extends LitElement implements Layer {
public eventBus: EventBus;
public eventBus: EventBus | undefined;
@state()
private isVisible = false;
@@ -30,6 +30,7 @@ export class GutterAdModal extends LitElement implements Layer {
}
init() {
if (!this.eventBus) throw new Error("Not initialized");
if (getGamesPlayed() > 1) {
this.eventBus.on(GutterAdModalEvent, (event) => {
if (event.isVisible) {
+2 -1
View File
@@ -6,7 +6,7 @@ import { translateText } from "../../Utils";
@customElement("heads-up-message")
export class HeadsUpMessage extends LitElement implements Layer {
public game: GameView;
public game: GameView | undefined;
@state()
private isVisible = false;
@@ -21,6 +21,7 @@ export class HeadsUpMessage extends LitElement implements Layer {
}
tick() {
if (!this.game) throw new Error("Not initialzied");
if (!this.game.inSpawnPhase()) {
this.isVisible = false;
this.requestUpdate();
+4 -2
View File
@@ -9,9 +9,9 @@ import { translateText } from "../../Utils";
@customElement("multi-tab-modal")
export class MultiTabModal extends LitElement implements Layer {
public game: GameView;
public game: GameView | undefined;
private detector: MultiTabDetector;
private detector: MultiTabDetector | undefined;
@property({ type: Number }) duration = 5000;
@state() private countdown = 5;
@@ -28,6 +28,7 @@ export class MultiTabModal extends LitElement implements Layer {
}
tick() {
if (!this.game) throw new Error("Not initialzied");
if (
this.game.inSpawnPhase() ||
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
@@ -65,6 +66,7 @@ export class MultiTabModal extends LitElement implements Layer {
// Show the modal with penalty information
public show(duration: number): void {
if (!this.game) throw new Error("Not initialzied");
if (!this.game.myPlayer()?.isAlive()) {
return;
}
+6 -2
View File
@@ -36,7 +36,7 @@ class RenderInfo {
}
export class NameLayer implements Layer {
private canvas: HTMLCanvasElement;
private canvas: HTMLCanvasElement | undefined;
private lastChecked = 0;
private readonly renderCheckRate = 100;
private readonly renderRefreshRate = 500;
@@ -55,7 +55,7 @@ export class NameLayer implements Layer {
private readonly nukeWhiteIconImage: HTMLImageElement;
private readonly nukeRedIconImage: HTMLImageElement;
private readonly shieldIconImage: HTMLImageElement;
private container: HTMLDivElement;
private container: HTMLDivElement | undefined;
private firstPlace: PlayerView | null = null;
private theme: Theme = this.game.config().theme();
private readonly userSettings: UserSettings = new UserSettings();
@@ -93,6 +93,7 @@ export class NameLayer implements Layer {
}
resizeCanvas() {
if (!this.canvas) throw new Error("Not initialzied");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
@@ -185,6 +186,7 @@ export class NameLayer implements Layer {
screenPosOld.x - window.innerWidth / 2,
screenPosOld.y - window.innerHeight / 2,
);
if (!this.container) throw new Error("Not initialzied");
this.container.style.transform =
`translate(${screenPos.x}px, ${screenPos.y}px) ` +
`scale(${this.transformHandler.scale})`;
@@ -197,6 +199,7 @@ export class NameLayer implements Layer {
}
}
if (!this.canvas) throw new Error("Not initialzied");
mainContex.drawImage(
this.canvas,
0,
@@ -302,6 +305,7 @@ export class NameLayer implements Layer {
// Start off invisible so it doesn't flash at 0,0
element.style.display = "none";
if (!this.container) throw new Error("Not initialzied");
this.container.appendChild(element);
return element;
}
+8 -6
View File
@@ -43,8 +43,8 @@ const secondsToHms = (d: number): string => {
@customElement("options-menu")
export class OptionsMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public game: GameView | undefined;
public eventBus: EventBus | undefined;
private readonly userSettings: UserSettings = new UserSettings();
@state()
@@ -68,12 +68,12 @@ export class OptionsMenu extends LitElement implements Layer {
private onTerrainButtonClick() {
this.alternateView = !this.alternateView;
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
this.eventBus?.emit(new AlternateViewEvent(this.alternateView));
this.requestUpdate();
}
private onExitButtonClick() {
const isAlive = this.game.myPlayer()?.isAlive();
const isAlive = this.game?.myPlayer()?.isAlive();
if (isAlive) {
const isConfirmed = confirm(
translateText("help_modal.exit_confirmation"),
@@ -95,7 +95,7 @@ export class OptionsMenu extends LitElement implements Layer {
private onPauseButtonClick() {
this.isPaused = !this.isPaused;
this.eventBus.emit(new PauseGameEvent(this.isPaused));
this.eventBus?.emit(new PauseGameEvent(this.isPaused));
}
private onToggleEmojisButtonClick() {
@@ -116,7 +116,7 @@ export class OptionsMenu extends LitElement implements Layer {
private onToggleDarkModeButtonClick() {
this.userSettings.toggleDarkMode();
this.requestUpdate();
this.eventBus.emit(new RedrawGraphicsEvent());
this.eventBus?.emit(new RedrawGraphicsEvent());
}
private onToggleRandomNameModeButtonClick() {
@@ -143,6 +143,7 @@ export class OptionsMenu extends LitElement implements Layer {
}
init() {
if (!this.game) throw new Error("Not initialzied");
console.log("init called from OptionsMenu");
this.showPauseButton =
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
@@ -152,6 +153,7 @@ export class OptionsMenu extends LitElement implements Layer {
}
tick() {
if (!this.game) throw new Error("Not initialzied");
const updates = this.game.updatesSinceLastTick();
if (updates) {
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
+38 -38
View File
@@ -32,10 +32,11 @@ import { translateText } from "../../../client/Utils";
@customElement("player-panel")
export class PlayerPanel extends LitElement implements Layer {
public g: GameView;
public eventBus: EventBus;
public emojiTable: EmojiTable;
public uiState: UIState;
public g: GameView | undefined;
public eventBus: EventBus | undefined;
public emojiTable: EmojiTable | undefined;
public uiState: UIState = { attackRatio: 0 };
private ctModal: ChatModal | undefined;
private actions: PlayerActions | null = null;
private tile: TileRef | null = null;
@@ -69,7 +70,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other));
this.eventBus?.emit(new SendAllianceRequestIntentEvent(myPlayer, other));
this.hide();
}
@@ -79,7 +80,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other));
this.eventBus?.emit(new SendBreakAllianceIntentEvent(myPlayer, other));
this.hide();
}
@@ -89,7 +90,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(
this.eventBus?.emit(
new SendDonateTroopsIntentEvent(
other,
myPlayer.troops() * this.uiState.attackRatio,
@@ -104,7 +105,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendDonateGoldIntentEvent(other, null));
this.eventBus?.emit(new SendDonateGoldIntentEvent(other, null));
this.hide();
}
@@ -114,7 +115,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(other, "start"));
this.eventBus?.emit(new SendEmbargoIntentEvent(other, "start"));
this.hide();
}
@@ -124,38 +125,38 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(other, "stop"));
this.eventBus?.emit(new SendEmbargoIntentEvent(other, "stop"));
this.hide();
}
private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) {
e.stopPropagation();
this.emojiTable.showTable((emoji: string) => {
this.emojiTable?.showTable((emoji: string) => {
if (myPlayer === other) {
this.eventBus.emit(
this.eventBus?.emit(
new SendEmojiIntentEvent(
AllPlayers,
flattenedEmojiTable.indexOf(emoji),
),
);
} else {
this.eventBus.emit(
this.eventBus?.emit(
new SendEmojiIntentEvent(other, flattenedEmojiTable.indexOf(emoji)),
);
}
this.emojiTable.hideTable();
this.emojiTable?.hideTable();
this.hide();
});
}
private handleChat(e: Event, sender: PlayerView, other: PlayerView) {
this.ctModal.open(sender, other);
this.ctModal?.open(sender, other);
this.hide();
}
private handleTargetClick(e: Event, other: PlayerView) {
e.stopPropagation();
this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id()));
this.eventBus?.emit(new SendTargetPlayerIntentEvent(other.id()));
this.hide();
}
@@ -163,8 +164,6 @@ export class PlayerPanel extends LitElement implements Layer {
return this;
}
private ctModal: ChatModal;
initEventBus(eventBus: EventBus) {
this.eventBus = eventBus;
eventBus.on(CloseViewEvent, (e) => {
@@ -175,8 +174,8 @@ export class PlayerPanel extends LitElement implements Layer {
}
init() {
this.eventBus.on(MouseUpEvent, () => this.hide());
this.eventBus.on(CloseViewEvent, (e) => {
this.eventBus?.on(MouseUpEvent, () => this.hide());
this.eventBus?.on(CloseViewEvent, (e) => {
this.hide();
});
@@ -184,28 +183,28 @@ export class PlayerPanel extends LitElement implements Layer {
}
async tick() {
if (this.isVisible && this.tile) {
const myPlayer = this.g.myPlayer();
if (myPlayer !== null && myPlayer.isAlive()) {
this.actions = await myPlayer.actions(this.tile);
if (!this.g) return;
if (!this.isVisible) return;
if (!this.tile) return;
const myPlayer = this.g.myPlayer();
if (!myPlayer?.isAlive()) return;
this.actions = await myPlayer.actions(this.tile);
if (this.actions?.interaction?.allianceExpiresAt !== undefined) {
const expiresAt = this.actions.interaction.allianceExpiresAt;
const remainingTicks = expiresAt - this.g.ticks();
if (this.actions?.interaction?.allianceExpiresAt !== undefined) {
const expiresAt = this.actions.interaction.allianceExpiresAt;
const remainingTicks = expiresAt - this.g.ticks();
if (remainingTicks > 0) {
const remainingSeconds = Math.max(
0,
Math.floor(remainingTicks / 10),
); // 10 ticks per second
this.allianceExpiryText = this.formatDuration(remainingSeconds);
}
} else {
this.allianceExpiryText = null;
}
this.requestUpdate();
if (remainingTicks > 0) {
const remainingSeconds = Math.max(
0,
Math.floor(remainingTicks / 10),
); // 10 ticks per second
this.allianceExpiryText = this.formatDuration(remainingSeconds);
}
} else {
this.allianceExpiryText = null;
}
this.requestUpdate();
}
private formatDuration(totalSeconds: number): string {
@@ -222,6 +221,7 @@ export class PlayerPanel extends LitElement implements Layer {
if (!this.isVisible) {
return html``;
}
if (this.g === undefined) return;
const myPlayer = this.g.myPlayer();
if (myPlayer === null) return;
if (this.tile === null) return;
+9 -1
View File
@@ -41,7 +41,7 @@ type CenterButtonState = "default" | "back";
type RequiredRadialMenuConfig = Required<RadialMenuConfig>;
export class RadialMenu implements Layer {
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined> | undefined;
private tooltipElement: HTMLDivElement | null = null;
private isVisible = false;
@@ -253,6 +253,7 @@ export class RadialMenu implements Layer {
}
private renderMenuItems(items: MenuElement[], level: number) {
if (this.menuElement === undefined) throw new Error("Not initialized");
const container = this.menuElement.select(".menu-container");
container.selectAll(`.menu-level-${level}`).remove();
@@ -645,6 +646,7 @@ export class RadialMenu implements Layer {
}
private animatePreviousMenu() {
if (this.menuElement === undefined) throw new Error("Not initialized");
const container = this.menuElement.select(".menu-container");
const currentMenu = container.select(
`.menu-level-${this.currentLevel - 1}`,
@@ -704,6 +706,7 @@ export class RadialMenu implements Layer {
}
private animateMenuTransitions() {
if (this.menuElement === undefined) throw new Error("Not initialized");
const container = this.menuElement.select(".menu-container");
const currentSubmenu = container.select(
`.menu-level-${this.currentLevel + 1}`,
@@ -768,6 +771,7 @@ export class RadialMenu implements Layer {
this.anchorX = x;
this.anchorY = y;
if (this.menuElement === undefined) throw new Error("Not initialized");
this.menuElement.style("display", "block");
this.clampAndSetMenuPositionForLevel(this.currentLevel);
@@ -786,6 +790,7 @@ export class RadialMenu implements Layer {
// Force transition state to false to ensure menu hides
this.isTransitioning = false;
if (this.menuElement === undefined) throw new Error("Not initialized");
this.menuElement.style("display", "none");
this.isVisible = false;
this.selectedItemId = null;
@@ -826,6 +831,7 @@ export class RadialMenu implements Layer {
}
public updateCenterButtonState(state: CenterButtonState) {
if (this.menuElement === undefined) throw new Error("Not initialized");
this.centerButtonState = state;
if (state === "back") {
const backButtonSize = this.config.centerButtonSize * 0.8; // Make back button 20% smaller
@@ -904,6 +910,7 @@ export class RadialMenu implements Layer {
const scale = isHovering ? 1.2 : 1;
if (this.menuElement === undefined) throw new Error("Not initialized");
this.menuElement
.select(".center-button-hitbox")
.transition()
@@ -1068,6 +1075,7 @@ export class RadialMenu implements Layer {
const clampedX = 2 * margin > vw ? vw / 2 : Math.min(Math.max(this.anchorX, margin), vw - margin);
const clampedY = 2 * margin > vh ? vh / 2 : Math.min(Math.max(this.anchorY, margin), vh - margin);
if (this.menuElement === undefined) throw new Error("Not initialized");
const svgSel = this.menuElement.select("svg");
svgSel
.style("top", `${clampedY}px`)
+6 -2
View File
@@ -19,8 +19,8 @@ type RailRef = {
};
export class RailroadLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | undefined;
private readonly theme: Theme;
// Save the number of railroads per tiles. Delete when it reaches 0
private readonly existingRailroads = new Map<TileRef, RailRef>();
@@ -90,6 +90,7 @@ export class RailroadLayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.canvas === undefined) throw new Error("Not initialized");
this.updateRailColors();
context.drawImage(
this.canvas,
@@ -138,6 +139,7 @@ export class RailroadLayer implements Layer {
if (!ref || ref.numOccurence <= 0) {
this.existingRailroads.delete(railRoad.tile);
this.railTileList = this.railTileList.filter((t) => t !== railRoad.tile);
if (this.context === undefined) throw new Error("Not initialized");
this.context.clearRect(
this.game.x(railRoad.tile) * 2 - 1,
this.game.y(railRoad.tile) * 2 - 1,
@@ -155,11 +157,13 @@ export class RailroadLayer implements Layer {
const color = recipient
? this.theme.railroadColor(recipient)
: new Colord({ r: 255, g: 255, b: 255, a: 1 });
if (this.context === undefined) throw new Error("Not initialized");
this.context.fillStyle = color.toRgbString();
this.paintRailRects(x, y, railRoad.railType);
}
private paintRailRects(x: number, y: number, direction: RailType) {
if (this.context === undefined) throw new Error("Not initialized");
const railRects = getRailroadRects(direction);
for (const [dx, dy, w, h] of railRects) {
this.context.fillRect(x * 2 + dx, y * 2 + dy, w, h);
+27 -26
View File
@@ -26,8 +26,8 @@ export class ShowSettingsModalEvent {
@customElement("settings-modal")
export class SettingsModal extends LitElement implements Layer {
public eventBus: EventBus;
public userSettings: UserSettings;
public eventBus: EventBus | undefined;
public userSettings: UserSettings | undefined;
@state()
private isVisible = false;
@@ -45,6 +45,7 @@ export class SettingsModal extends LitElement implements Layer {
wasPausedWhenOpened = false;
init() {
if (this.eventBus === undefined) throw new Error("Not initialized");
this.eventBus.on(ShowSettingsModalEvent, (event) => {
this.isVisible = event.isVisible;
this.shouldPause = event.shouldPause;
@@ -100,48 +101,48 @@ export class SettingsModal extends LitElement implements Layer {
private pauseGame(pause: boolean) {
if (this.shouldPause && !this.wasPausedWhenOpened)
this.eventBus.emit(new PauseGameEvent(pause));
this.eventBus?.emit(new PauseGameEvent(pause));
}
private onTerrainButtonClick() {
this.alternateView = !this.alternateView;
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
this.eventBus?.emit(new AlternateViewEvent(this.alternateView));
this.requestUpdate();
}
private onToggleEmojisButtonClick() {
this.userSettings.toggleEmojis();
this.userSettings?.toggleEmojis();
this.requestUpdate();
}
private onToggleStructureSpritesButtonClick() {
this.userSettings.toggleStructureSprites();
this.userSettings?.toggleStructureSprites();
this.requestUpdate();
}
private onToggleSpecialEffectsButtonClick() {
this.userSettings.toggleFxLayer();
this.userSettings?.toggleFxLayer();
this.requestUpdate();
}
private onToggleDarkModeButtonClick() {
this.userSettings.toggleDarkMode();
this.eventBus.emit(new RedrawGraphicsEvent());
this.userSettings?.toggleDarkMode();
this.eventBus?.emit(new RedrawGraphicsEvent());
this.requestUpdate();
}
private onToggleRandomNameModeButtonClick() {
this.userSettings.toggleRandomName();
this.userSettings?.toggleRandomName();
this.requestUpdate();
}
private onToggleLeftClickOpensMenu() {
this.userSettings.toggleLeftClickOpenMenu();
this.userSettings?.toggleLeftClickOpenMenu();
this.requestUpdate();
}
private onTogglePerformanceOverlayButtonClick() {
this.userSettings.togglePerformanceOverlay();
this.userSettings?.togglePerformanceOverlay();
this.requestUpdate();
}
@@ -221,13 +222,13 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.emojis_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.emojis()
${this.userSettings?.emojis()
? translateText("user_setting.emojis_visible")
: translateText("user_setting.emojis_hidden")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.emojis()
${this.userSettings?.emojis()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
@@ -249,13 +250,13 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.dark_mode_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.darkMode()
${this.userSettings?.darkMode()
? translateText("user_setting.dark_mode_enabled")
: translateText("user_setting.light_mode_enabled")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.darkMode()
${this.userSettings?.darkMode()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
@@ -277,13 +278,13 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.special_effects_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.fxLayer()
${this.userSettings?.fxLayer()
? translateText("user_setting.special_effects_enabled")
: translateText("user_setting.special_effects_disabled")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.fxLayer()
${this.userSettings?.fxLayer()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
@@ -305,13 +306,13 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.structure_sprites_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.structureSprites()
${this.userSettings?.structureSprites()
? translateText("user_setting.structure_sprites_enabled")
: translateText("user_setting.structure_sprites_disabled")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.structureSprites()
${this.userSettings?.structureSprites()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
@@ -328,13 +329,13 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.anonymous_names_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.anonymousNames()
${this.userSettings?.anonymousNames()
? translateText("user_setting.anonymous_names_enabled")
: translateText("user_setting.real_names_shown")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.anonymousNames()
${this.userSettings?.anonymousNames()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
@@ -351,13 +352,13 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.left_click_menu")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.leftClickOpensMenu()
${this.userSettings?.leftClickOpensMenu()
? translateText("user_setting.left_click_opens_menu")
: translateText("user_setting.right_click_opens_menu")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.leftClickOpensMenu()
${this.userSettings?.leftClickOpensMenu()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
@@ -379,7 +380,7 @@ export class SettingsModal extends LitElement implements Layer {
${translateText("user_setting.performance_overlay_label")}
</div>
<div class="text-sm text-slate-400">
${this.userSettings.performanceOverlay()
${this.userSettings?.performanceOverlay()
? translateText("user_setting.performance_overlay_enabled")
: translateText(
"user_setting.performance_overlay_disabled",
@@ -387,7 +388,7 @@ export class SettingsModal extends LitElement implements Layer {
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.performanceOverlay()
${this.userSettings?.performanceOverlay()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
+2 -1
View File
@@ -10,7 +10,7 @@ const AD_CONTAINER_ID = "bottom-rail-ad-container";
@customElement("spawn-ad")
export class SpawnAd extends LitElement implements Layer {
public g: GameView;
public g: GameView | undefined;
@state()
private isVisible = false;
@@ -50,6 +50,7 @@ export class SpawnAd extends LitElement implements Layer {
}
public async tick() {
if (!this.g) return;
if (
!this.isVisible &&
this.g.inSpawnPhase() &&
@@ -54,14 +54,14 @@ const ICON_SIZE = {
const OFFSET_ZOOM_Y = 4; // offset for the y position of the level over the sprite
export class StructureIconsLayer implements Layer {
private pixicanvas: HTMLCanvasElement;
private iconsStage: PIXI.Container;
private levelsStage: PIXI.Container;
private dotsStage: PIXI.Container;
private pixicanvas: HTMLCanvasElement | undefined;
private iconsStage: PIXI.Container | undefined;
private levelsStage: PIXI.Container | undefined;
private dotsStage: PIXI.Container | undefined;
private shouldRedraw = true;
private readonly textureCache: Map<string, PIXI.Texture> = new Map();
private readonly theme: Theme;
private renderer: PIXI.Renderer;
private renderer: PIXI.Renderer | undefined;
private renders: StructureRenderInfo[] = [];
private readonly seenUnits: Set<UnitView> = new Set();
private readonly structures: Map<
@@ -163,7 +163,7 @@ export class StructureIconsLayer implements Layer {
}
resizeCanvas() {
if (this.renderer) {
if (this.renderer && this.pixicanvas) {
this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight;
this.renderer.resize(innerWidth, innerHeight, 1);
@@ -322,10 +322,13 @@ export class StructureIconsLayer implements Layer {
if (this.transformHandler.hasChanged() || this.shouldRedraw) {
if (this.transformHandler.scale > ZOOM_THRESHOLD && this.renderSprites) {
if (this.levelsStage === undefined) throw new Error("Not initialized");
this.renderer.render(this.levelsStage);
} else if (this.transformHandler.scale > DOTS_ZOOM_THRESHOLD) {
if (this.iconsStage === undefined) throw new Error("Not initialized");
this.renderer.render(this.iconsStage);
} else {
if (this.dotsStage === undefined) throw new Error("Not initialized");
this.renderer.render(this.dotsStage);
}
this.shouldRedraw = false;
@@ -504,6 +507,7 @@ export class StructureIconsLayer implements Layer {
}
private createLevelSprite(unit: UnitView): PIXI.Container {
if (this.levelsStage === undefined) throw new Error("Not initialized");
return this.createUnitContainer(unit, {
type: "level",
stage: this.levelsStage,
@@ -511,6 +515,7 @@ export class StructureIconsLayer implements Layer {
}
private createDotSprite(unit: UnitView): PIXI.Container {
if (this.dotsStage === undefined) throw new Error("Not initialized");
return this.createUnitContainer(unit, {
type: "dot",
stage: this.dotsStage,
@@ -518,6 +523,7 @@ export class StructureIconsLayer implements Layer {
}
private createIconSprite(unit: UnitView): PIXI.Container {
if (this.iconsStage === undefined) throw new Error("Not initialized");
return this.createUnitContainer(unit, {
type: "icon",
stage: this.iconsStage,
@@ -633,6 +639,7 @@ export class StructureIconsLayer implements Layer {
? ICON_SIZE[STRUCTURE_SHAPES[type]]
: 28;
if (this.pixicanvas === undefined) throw new Error("Not initialized");
const onScreen =
screenPos.x + margin > 0 &&
screenPos.x - margin < this.pixicanvas.width &&
+6 -2
View File
@@ -29,8 +29,8 @@ type UnitRenderConfig = {
};
export class StructureLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | undefined;
private readonly unitIcons: Map<string, HTMLImageElement> = new Map();
private readonly theme: Theme;
private readonly tempCanvas: HTMLCanvasElement;
@@ -145,6 +145,7 @@ export class StructureLayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.canvas === undefined) throw new Error("Not initialized");
if (
this.transformHandler.scale <= ZOOM_THRESHOLD ||
!this.game.config().userSettings()?.structureSprites()
@@ -264,16 +265,19 @@ export class StructureLayer implements Layer {
this.tempContext.drawImage(image, 0, 0, width * 2, height * 2);
// Draw the final result to the main canvas
if (this.context === undefined) throw new Error("Not initialized");
this.context.drawImage(this.tempCanvas, startX * 2, startY * 2);
}
paintCell(cell: Cell, color: Colord, alpha: number) {
this.clearCell(cell);
if (this.context === undefined) throw new Error("Not initialized");
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
this.context.fillRect(cell.x * 2, cell.y * 2, 2, 2);
}
clearCell(cell: Cell) {
if (this.context === undefined) throw new Error("Not initialized");
this.context.clearRect(cell.x * 2, cell.y * 2, 2, 2);
}
}
+5 -2
View File
@@ -21,8 +21,8 @@ type TeamEntry = {
@customElement("team-stats")
export class TeamStats extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public game: GameView | undefined;
public eventBus: EventBus | undefined;
@property({ type: Boolean }) visible = false;
teams: TeamEntry[] = [];
@@ -36,6 +36,7 @@ export class TeamStats extends LitElement implements Layer {
init() {}
tick() {
if (this.game === undefined) throw new Error("Not initialized");
if (this.game.config().gameConfig().gameMode !== GameMode.Team) return;
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
@@ -51,6 +52,7 @@ export class TeamStats extends LitElement implements Layer {
}
private updateTeamStats() {
if (this.game === undefined) throw new Error("Not initialized");
const players = this.game.playerViews();
const grouped: Record<Team, PlayerView[]> = {};
@@ -83,6 +85,7 @@ export class TeamStats extends LitElement implements Layer {
}
}
if (this.game === undefined) throw new Error("Not initialized");
const totalScorePercent = totalScoreSort / this.game.numLandTiles();
return {
+7 -5
View File
@@ -4,10 +4,10 @@ import { Theme } from "../../../core/configuration/Config";
import { TransformHandler } from "../TransformHandler";
export class TerrainLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private imageData: ImageData;
private theme: Theme;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | undefined;
private imageData: ImageData | undefined;
private theme: Theme | undefined;
constructor(
private readonly game: GameView,
@@ -48,7 +48,8 @@ export class TerrainLayer implements Layer {
initImageData() {
this.theme = this.game.config().theme();
this.game.forEachTile((tile) => {
const terrainColor = this.theme.terrainColor(this.game, tile);
const terrainColor = this.theme?.terrainColor(this.game, tile);
if (terrainColor === undefined || this.imageData === undefined) return;
// TODO: isn'te tileref and index the same?
const index = this.game.y(tile) * this.game.width() + this.game.x(tile);
const offset = index * 4;
@@ -66,6 +67,7 @@ export class TerrainLayer implements Layer {
} else {
context.imageSmoothingEnabled = false;
}
if (this.canvas === undefined) throw new Error("Not initialized");
context.drawImage(
this.canvas,
-this.game.width() / 2,
+22 -7
View File
@@ -19,10 +19,10 @@ import { UserSettings } from "../../../core/game/UserSettings";
export class TerritoryLayer implements Layer {
private readonly userSettings: UserSettings;
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private imageData: ImageData;
private alternativeImageData: ImageData;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | undefined;
private imageData: ImageData | undefined;
private alternativeImageData: ImageData | undefined;
private cachedTerritoryPatternsEnabled: boolean | undefined;
@@ -36,8 +36,8 @@ export class TerritoryLayer implements Layer {
private readonly theme: Theme;
// Used for spawn highlighting
private highlightCanvas: HTMLCanvasElement;
private highlightContext: CanvasRenderingContext2D;
private highlightCanvas: HTMLCanvasElement | undefined;
private highlightContext: CanvasRenderingContext2D | undefined;
private highlightedTerritory: PlayerView | null = null;
@@ -158,7 +158,7 @@ export class TerritoryLayer implements Layer {
return;
}
this.highlightContext.clearRect(
this.highlightContext?.clearRect(
0,
0,
this.game.width(),
@@ -322,6 +322,8 @@ export class TerritoryLayer implements Layer {
initImageData() {
this.game.forEachTile((tile) => {
if (this.imageData === undefined) throw new Error("Not initialized");
if (this.alternativeImageData === undefined) throw new Error("Not initialized");
const cell = new Cell(this.game.x(tile), this.game.y(tile));
const index = cell.y * this.game.width() + cell.x;
const offset = index * 4;
@@ -331,6 +333,11 @@ export class TerritoryLayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.canvas === undefined) throw new Error("Not initialized");
if (this.highlightCanvas === undefined) throw new Error("Not initialized");
if (this.context === undefined) throw new Error("Not initialized");
if (this.imageData === undefined) throw new Error("Not initialized");
if (this.alternativeImageData === undefined) throw new Error("Not initialized");
const now = Date.now();
if (
now > this.lastDragTime + this.nodrawDragDuration &&
@@ -405,6 +412,8 @@ export class TerritoryLayer implements Layer {
if (isBorder && !this.game.hasOwner(tile)) {
return;
}
if (this.imageData === undefined) throw new Error("Not initialized");
if (this.alternativeImageData === undefined) throw new Error("Not initialized");
if (!this.game.hasOwner(tile)) {
if (this.game.hasFallout(tile)) {
@@ -500,6 +509,7 @@ export class TerritoryLayer implements Layer {
}
paintAlternateViewTile(tile: TileRef, other: PlayerView) {
if (this.alternativeImageData === undefined) throw new Error("Not initialized");
const color = this.alternateViewColor(other);
this.paintTile(this.alternativeImageData, tile, color, 255);
}
@@ -514,12 +524,15 @@ export class TerritoryLayer implements Layer {
clearTile(tile: TileRef) {
const offset = tile * 4;
if (this.imageData === undefined) throw new Error("Not initialized");
if (this.alternativeImageData === undefined) throw new Error("Not initialized");
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
clearAlternativeTile(tile: TileRef) {
const offset = tile * 4;
if (this.alternativeImageData === undefined) throw new Error("Not initialized");
this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
@@ -541,6 +554,7 @@ export class TerritoryLayer implements Layer {
this.clearTile(tile);
const x = this.game.x(tile);
const y = this.game.y(tile);
if (this.highlightContext === undefined) throw new Error("Not initialized");
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
this.highlightContext.fillRect(x, y, 1, 1);
}
@@ -548,6 +562,7 @@ export class TerritoryLayer implements Layer {
clearHighlightTile(tile: TileRef) {
const x = this.game.x(tile);
const y = this.game.y(tile);
if (this.highlightContext === undefined) throw new Error("Not initialized");
this.highlightContext.clearRect(x, y, 1, 1);
}
}
+5 -5
View File
@@ -25,10 +25,9 @@ const PROGRESSBAR_HEIGHT = 3; // Height of a bar
* such as selection boxes, health bars, etc.
*/
export class UILayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D | null;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | null = null;
private readonly theme: Theme | null = null;
private readonly userSettings: UserSettings = new UserSettings();
private selectionAnimTime = 0;
private readonly allProgressBars: Map<
number,
@@ -51,7 +50,6 @@ export class UILayer implements Layer {
constructor(
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
}
@@ -71,7 +69,8 @@ export class UILayer implements Layer {
this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
?.[GameUpdateType.Unit]
?.map((unit) => this.game.unit(unit.id))
?.forEach((unitView) => {
if (unitView === undefined) return;
this.onUnitEvent(unitView);
@@ -85,6 +84,7 @@ export class UILayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.canvas === undefined) throw new Error("Not initialized");
context.drawImage(
this.canvas,
-this.game.width() / 2,
+6 -4
View File
@@ -15,8 +15,8 @@ import samLauncherIcon from "../../../../resources/non-commercial/svg/SamLaunche
@customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public game: GameView | undefined;
public eventBus: EventBus | undefined;
private readonly _selectedStructure: UnitType | null = null;
private _cities = 0;
private _factories = 0;
@@ -31,6 +31,7 @@ export class UnitDisplay extends LitElement implements Layer {
}
init() {
if (this.game === undefined) throw new Error("Not initialized");
const config = this.game.config();
this.allDisabled =
config.isUnitDisabled(UnitType.City) &&
@@ -60,6 +61,7 @@ export class UnitDisplay extends LitElement implements Layer {
unitType: UnitType,
altText: string,
) {
if (this.game === undefined) throw new Error("Not initialized");
if (this.game.config().isUnitDisabled(unitType)) {
return html``;
}
@@ -71,9 +73,9 @@ export class UnitDisplay extends LitElement implements Layer {
? "#ffffff2e"
: "none"}"
@mouseenter="${() =>
this.eventBus.emit(new ToggleStructureEvent(unitType))}"
this.eventBus?.emit(new ToggleStructureEvent(unitType))}"
@mouseleave="${() =>
this.eventBus.emit(new ToggleStructureEvent(null))}"
this.eventBus?.emit(new ToggleStructureEvent(null))}"
>
<img
src=${icon}
+22 -9
View File
@@ -27,10 +27,10 @@ enum Relationship {
}
export class UnitLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private transportShipTrailCanvas: HTMLCanvasElement;
private unitTrailContext: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement | undefined;
private context: CanvasRenderingContext2D | undefined;
private transportShipTrailCanvas: HTMLCanvasElement | undefined;
private unitTrailContext: CanvasRenderingContext2D | undefined;
private readonly unitToTrail = new Map<UnitView, TileRef[]>();
@@ -156,6 +156,8 @@ export class UnitLayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.transportShipTrailCanvas === undefined) throw new Error("Not initialized");
if (this.canvas === undefined) throw new Error("Not initialized");
context.drawImage(
this.transportShipTrailCanvas,
-this.game.width() / 2,
@@ -195,6 +197,7 @@ export class UnitLayer implements Layer {
this.updateUnitsSprites(this.game.units().map((unit) => unit.id()));
this.unitToTrail.forEach((trail, unit) => {
if (this.unitTrailContext === undefined) throw new Error("Not initialized");
for (const t of trail) {
this.paintCell(
this.game.x(t),
@@ -225,6 +228,7 @@ export class UnitLayer implements Layer {
unitViews
.filter((unitView) => isSpriteReady(unitView))
.forEach((unitView) => {
if (this.context === undefined) throw new Error("Not initialized");
const sprite = getColoredSprite(unitView, this.theme);
const clearsize = sprite.width + 1;
const lastX = this.game.x(unitView.lastTile());
@@ -301,13 +305,14 @@ export class UnitLayer implements Layer {
}
private handleShellEvent(unit: UnitView) {
if (this.context === undefined) throw new Error("Not initialized");
const rel = this.relationship(unit);
// Clear current and previous positions
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()), this.context);
const oldTile = this.oldShellTile.get(unit);
if (oldTile !== undefined) {
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
this.clearCell(this.game.x(oldTile), this.game.y(oldTile), this.context);
}
this.oldShellTile.set(unit, unit.lastTile());
@@ -322,6 +327,7 @@ export class UnitLayer implements Layer {
rel,
this.theme.borderColor(unit.owner()),
255,
this.context,
);
this.paintCell(
this.game.x(unit.lastTile()),
@@ -329,6 +335,7 @@ export class UnitLayer implements Layer {
rel,
this.theme.borderColor(unit.owner()),
255,
this.context,
);
}
@@ -338,6 +345,7 @@ export class UnitLayer implements Layer {
}
private drawTrail(trail: number[], color: Colord, rel: Relationship) {
if (this.unitTrailContext === undefined) throw new Error("Not initialized");
// Paint new trail
for (const t of trail) {
this.paintCell(
@@ -352,6 +360,7 @@ export class UnitLayer implements Layer {
}
private clearTrail(unit: UnitView) {
if (this.unitTrailContext === undefined) throw new Error("Not initialized");
const trail = this.unitToTrail.get(unit) ?? [];
const rel = this.relationship(unit);
for (const t of trail) {
@@ -360,6 +369,7 @@ export class UnitLayer implements Layer {
this.unitToTrail.delete(unit);
// Repaint overlapping trails
if (this.unitTrailContext === undefined) throw new Error("Not initialized");
const trailSet = new Set(trail);
for (const [other, trail] of this.unitToTrail) {
for (const t of trail) {
@@ -421,7 +431,8 @@ export class UnitLayer implements Layer {
private handleMIRVWarhead(unit: UnitView) {
const rel = this.relationship(unit);
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
if (this.context === undefined) throw new Error("Not initialized");
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()), this.context);
if (unit.isActive()) {
// Paint area
@@ -431,6 +442,7 @@ export class UnitLayer implements Layer {
rel,
this.theme.borderColor(unit.owner()),
255,
this.context,
);
}
}
@@ -471,7 +483,7 @@ export class UnitLayer implements Layer {
relationship: Relationship,
color: Colord,
alpha: number,
context: CanvasRenderingContext2D = this.context,
context: CanvasRenderingContext2D,
) {
this.clearCell(x, y, context);
if (this.alternateView) {
@@ -495,7 +507,7 @@ export class UnitLayer implements Layer {
clearCell(
x: number,
y: number,
context: CanvasRenderingContext2D = this.context,
context: CanvasRenderingContext2D,
) {
context.clearRect(x, y, 1, 1);
}
@@ -542,6 +554,7 @@ export class UnitLayer implements Layer {
if (unit.isActive()) {
const targetable = unit.targetable();
if (this.context === undefined) throw new Error("Not initialized");
if (!targetable) {
this.context.save();
this.context.globalAlpha = 0.5;
+10 -8
View File
@@ -10,8 +10,8 @@ import { translateText } from "../../../client/Utils";
@customElement("win-modal")
export class WinModal extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public game: GameView | undefined;
public eventBus: EventBus | undefined;
private hasShownDeathModal = false;
@@ -21,7 +21,7 @@ export class WinModal extends LitElement implements Layer {
@state()
showButtons = false;
private _title: string;
private _title = "";
// Override to prevent shadow DOM creation
createRenderRoot() {
@@ -137,7 +137,7 @@ export class WinModal extends LitElement implements Layer {
render() {
return html`
<div class="win-modal ${this.isVisible ? "visible" : ""}">
<h2>${this._title || ""}</h2>
<h2>${this._title}</h2>
${this.innerHtml()}
<div
class="button-container ${this.showButtons ? "visible" : "hidden"}"
@@ -175,7 +175,7 @@ export class WinModal extends LitElement implements Layer {
}
show() {
this.eventBus.emit(new GutterAdModalEvent(true));
this.eventBus?.emit(new GutterAdModalEvent(true));
setTimeout(() => {
this.isVisible = true;
this.requestUpdate();
@@ -187,7 +187,7 @@ export class WinModal extends LitElement implements Layer {
}
hide() {
this.eventBus.emit(new GutterAdModalEvent(false));
this.eventBus?.emit(new GutterAdModalEvent(false));
this.isVisible = false;
this.showButtons = false;
this.requestUpdate();
@@ -201,6 +201,7 @@ export class WinModal extends LitElement implements Layer {
init() {}
tick() {
if (this.game === undefined) throw new Error("Not initialized");
const myPlayer = this.game.myPlayer();
if (
!this.hasShownDeathModal &&
@@ -216,10 +217,11 @@ export class WinModal extends LitElement implements Layer {
const updates = this.game.updatesSinceLastTick();
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
winUpdates.forEach((wu) => {
if (this.game === undefined) return;
if (wu.winner === undefined) {
// ...
} else if (wu.winner[0] === "team") {
this.eventBus.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats));
this.eventBus?.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats));
if (wu.winner[1] === this.game.myPlayer()?.team()) {
this._title = translateText("win_modal.your_team");
} else {
@@ -233,7 +235,7 @@ export class WinModal extends LitElement implements Layer {
if (!winner?.isPlayer()) return;
const winnerClient = winner.clientID();
if (winnerClient !== null) {
this.eventBus.emit(
this.eventBus?.emit(
new SendWinnerEvent(["player", winnerClient], wu.allPlayersStats),
);
}
+1 -1
View File
@@ -100,7 +100,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
return process.env.CF_CREDS_PATH ?? "";
}
private publicKey: JWK;
private publicKey: JWK | undefined;
abstract jwtAudience(): string;
jwtIssuer(): string {
const audience = this.jwtAudience();
+22 -9
View File
@@ -23,9 +23,9 @@ export class AttackExecution implements Execution {
private readonly random = new PseudoRandom(123);
private target: Player | TerraNullius;
private target: Player | TerraNullius | undefined;
private mg: Game;
private mg: Game | undefined;
private attack: Attack | null = null;
@@ -171,6 +171,9 @@ export class AttackExecution implements Execution {
}
private retreat(malusPercent = 0) {
if (this.mg === undefined) {
throw new Error("Attack not initialized");
}
if (this.attack === null) {
throw new Error("Attack not initialized");
}
@@ -189,22 +192,27 @@ export class AttackExecution implements Execution {
this.active = false;
// Not all retreats are canceled attacks
if (this.attack.retreated()) {
if (this.attack.retreated() && this.target && this.target.isPlayer()) {
// Record stats
this.mg.stats().attackCancel(this._owner, this.target, survivors);
}
}
tick(ticks: number) {
if (this.mg === undefined) {
throw new Error("Attack not initialized");
}
if (this.target === undefined) {
throw new Error("Attack not initialized");
}
if (this.attack === null) {
throw new Error("Attack not initialized");
}
let troopCount = this.attack.troops(); // cache troop count
const targetIsPlayer = this.target.isPlayer(); // cache target type
const targetPlayer = targetIsPlayer ? (this.target as Player) : null; // cache target player
const targetPlayer: Player | null = this.target.isPlayer() ? this.target : null; // cache target player
if (this.attack.retreated()) {
if (targetIsPlayer) {
if (targetPlayer !== null) {
this.retreat(malusForRetreat);
} else {
this.retreat();
@@ -222,8 +230,8 @@ export class AttackExecution implements Execution {
return;
}
const alliance = targetPlayer
? this._owner.allianceWith(targetPlayer)
const alliance = this.target && this.target.isPlayer()
? this._owner.allianceWith(this.target)
: null;
if (this.breakAlliance && alliance !== null) {
this.breakAlliance = false;
@@ -309,6 +317,9 @@ export class AttackExecution implements Execution {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
if (this.mg === undefined) {
throw new Error("Attack not initialized");
}
const tickNow = this.mg.ticks(); // cache tick
@@ -349,6 +360,8 @@ export class AttackExecution implements Execution {
}
private handleDeadDefender() {
if (!this.mg) return;
if (!this.target) return;
if (!(this.target.isPlayer() && this.target.numTilesOwned() < 100)) return;
this.mg.conquerPlayer(this._owner, this.target);
@@ -357,7 +370,7 @@ export class AttackExecution implements Execution {
for (const tile of this.target.tiles()) {
const borders = this.mg
.neighbors(tile)
.some((t) => this.mg.owner(t) === this._owner);
.some((t) => this.mg?.owner(t) === this._owner);
if (borders) {
this._owner.conquer(tile);
} else {
+4 -2
View File
@@ -6,7 +6,7 @@ import { simpleHash } from "../Util";
export class BotExecution implements Execution {
private active = true;
private readonly random: PseudoRandom;
private mg: Game;
private mg: Game | undefined;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
@@ -42,6 +42,7 @@ export class BotExecution implements Execution {
}
if (this.behavior === null) {
if (this.mg === undefined) throw new Error("Not initialized");
this.behavior = new BotBehavior(
this.random,
this.mg,
@@ -63,7 +64,7 @@ export class BotExecution implements Execution {
private maybeAttack() {
if (this.behavior === null) {
throw new Error("not initialized");
throw new Error("Not initialized");
}
const toAttack = this.behavior.getNeighborTraitorToAttack();
if (toAttack !== null) {
@@ -75,6 +76,7 @@ export class BotExecution implements Execution {
}
if (this.neighborsTerraNullius) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
return;
+2 -1
View File
@@ -3,7 +3,7 @@ import { TileRef } from "../game/GameMap";
import { TrainStationExecution } from "./TrainStationExecution";
export class CityExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private city: Unit | null = null;
private active = true;
@@ -47,6 +47,7 @@ export class CityExecution implements Execution {
createStation(): void {
if (this.city !== null) {
if (this.mg === undefined) throw new Error("Not initialized");
const nearbyFactory = this.mg.hasUnitNearby(
this.city.tile(),
this.mg.config().trainStationMaxRange(),
+7 -3
View File
@@ -21,11 +21,11 @@ import { WarshipExecution } from "./WarshipExecution";
export class ConstructionExecution implements Execution {
private construction: Unit | null = null;
private active = true;
private mg: Game;
private mg: Game | undefined;
private ticksUntilComplete: Tick;
private ticksUntilComplete: Tick | undefined;
private cost: Gold;
private cost: Gold | undefined;
constructor(
private player: Player,
@@ -53,6 +53,7 @@ export class ConstructionExecution implements Execution {
tick(ticks: number): void {
if (this.construction === null) {
if (this.mg === undefined) throw new Error("Not initialized");
const info = this.mg.unitInfo(this.constructionType);
if (info.constructionDuration === undefined) {
this.completeConstruction();
@@ -90,15 +91,18 @@ export class ConstructionExecution implements Execution {
this.player = this.construction.owner();
this.construction.delete(false);
// refund the cost so player has the gold to build the unit
if (this.cost === undefined) throw new Error("Not initialized");
this.player.addGold(this.cost);
this.completeConstruction();
this.active = false;
return;
}
if (this.ticksUntilComplete === undefined) throw new Error("Not initialized");
this.ticksUntilComplete--;
}
private completeConstruction() {
if (this.mg === undefined) throw new Error("Not initialized");
const { player } = this;
switch (this.constructionType) {
case UnitType.AtomBomb:
+2 -1
View File
@@ -3,7 +3,7 @@ import { ShellExecution } from "./ShellExecution";
import { TileRef } from "../game/GameMap";
export class DefensePostExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private post: Unit | null = null;
private active = true;
@@ -24,6 +24,7 @@ export class DefensePostExecution implements Execution {
private shoot() {
if (this.post === null) return;
if (this.target === null) return;
if (this.mg === undefined) throw new Error("Not initialized");
const shellAttackRate = this.mg.config().defensePostShellAttackRate();
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
this.lastShellAttack = this.mg.ticks();
+1 -1
View File
@@ -2,7 +2,7 @@ import { Execution, Game, MessageType, Player } from "../game/Game";
export class DeleteUnitExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
constructor(
private readonly player: Player,
+3 -2
View File
@@ -1,7 +1,7 @@
import { Execution, Game, Gold, Player, PlayerID } from "../game/Game";
export class DonateGoldExecution implements Execution {
private recipient: Player;
private recipient: Player | undefined;
private active = true;
@@ -23,7 +23,8 @@ export class DonateGoldExecution implements Execution {
}
tick(ticks: number): void {
if (this.gold === null) throw new Error("not initialized");
if (this.gold === null) throw new Error("Not initialized");
if (this.recipient === undefined) throw new Error("Not initialized");
if (
this.sender.canDonateGold(this.recipient) &&
this.sender.donateGold(this.recipient, this.gold)
+3 -2
View File
@@ -1,7 +1,7 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class DonateTroopsExecution implements Execution {
private recipient: Player;
private recipient: Player | undefined;
private active = true;
@@ -26,7 +26,8 @@ export class DonateTroopsExecution implements Execution {
}
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
if (this.troops === null) throw new Error("Not initialized");
if (this.recipient === undefined) throw new Error("Not initialized");
if (
this.sender.canDonateTroops(this.recipient) &&
this.sender.donateTroops(this.recipient, this.troops)
+2 -1
View File
@@ -3,7 +3,7 @@ import { Execution, Game, Player, PlayerID } from "../game/Game";
export class EmbargoExecution implements Execution {
private active = true;
private target: Player;
private target: Player | undefined;
constructor(
private readonly player: Player,
@@ -21,6 +21,7 @@ export class EmbargoExecution implements Execution {
}
tick(_: number): void {
if (this.target === undefined) throw new Error("Not initialized");
if (this.action === "start") this.player.addEmbargo(this.target, false);
else this.player.stopEmbargo(this.target);
+2 -1
View File
@@ -9,7 +9,7 @@ import {
import { flattenedEmojiTable } from "../Util";
export class EmojiExecution implements Execution {
private recipient: Player | typeof AllPlayers;
private recipient: Player | typeof AllPlayers | undefined;
private active = true;
@@ -33,6 +33,7 @@ export class EmojiExecution implements Execution {
}
tick(ticks: number): void {
if (this.recipient === undefined) throw new Error("Not initialized");
const emojiString = flattenedEmojiTable[this.emoji];
if (emojiString === undefined) {
console.warn(
+3 -1
View File
@@ -5,7 +5,8 @@ import { TrainStationExecution } from "./TrainStationExecution";
export class FactoryExecution implements Execution {
private factory: Unit | null = null;
private active = true;
private game: Game;
private game: Game | undefined;
constructor(
private player: Player,
private readonly tile: TileRef,
@@ -46,6 +47,7 @@ export class FactoryExecution implements Execution {
createStation(): void {
if (this.factory !== null) {
if (this.game === undefined) throw new Error("Not initialized");
const structures = this.game.nearbyUnits(
this.factory.tile(),
this.game.config().trainStationMaxRange(),
+47 -27
View File
@@ -30,7 +30,7 @@ export class FakeHumanExecution implements Execution {
private active = true;
private readonly random: PseudoRandom;
private behavior: BotBehavior | null = null;
private mg: Game;
private mg: Game | undefined;
private player: Player | null = null;
private readonly attackRate: number;
@@ -69,6 +69,7 @@ export class FakeHumanExecution implements Execution {
private updateRelationsFromEmbargos() {
const { player } = this;
if (player === null) return;
if (this.mg === undefined) throw new Error("Not initialized");
const others = this.mg.players().filter((p) => p.id() !== player.id());
others.forEach((other: Player) => {
@@ -92,6 +93,7 @@ export class FakeHumanExecution implements Execution {
private handleEmbargoesToHostileNations() {
const { player } = this;
if (player === null) return;
if (this.mg === undefined) throw new Error("Not initialized");
const others = this.mg.players().filter((p) => p.id() !== player.id());
others.forEach((other: Player) => {
@@ -114,6 +116,7 @@ export class FakeHumanExecution implements Execution {
tick(ticks: number) {
if (ticks % this.attackRate !== this.attackTick) return;
if (this.mg === undefined) throw new Error("Not initialized");
if (this.mg.inSpawnPhase()) {
const rl = this.randomLand();
if (rl === null) {
@@ -164,13 +167,15 @@ export class FakeHumanExecution implements Execution {
private maybeAttack() {
if (this.player === null || this.behavior === null) {
throw new Error("not initialized");
throw new Error("Not initialized");
}
const game = this.mg;
if (game === undefined) throw new Error("Not initialized");
const enemyborder = Array.from(this.player.borderTiles())
.flatMap((t) => this.mg.neighbors(t))
.flatMap((t) => game.neighbors(t))
.filter(
(t) =>
this.mg.isLand(t) && this.mg.ownerID(t) !== this.player?.smallID(),
game.isLand(t) && game.ownerID(t) !== this.player?.smallID(),
);
if (enemyborder.length === 0) {
@@ -185,10 +190,10 @@ export class FakeHumanExecution implements Execution {
}
const borderPlayers = enemyborder.map((t) =>
this.mg.playerBySmallID(this.mg.ownerID(t)),
game.playerBySmallID(game.ownerID(t)),
);
if (borderPlayers.some((o) => !o.isPlayer())) {
this.behavior.sendAttack(this.mg.terraNullius());
this.behavior.sendAttack(game.terraNullius());
return;
}
@@ -228,7 +233,7 @@ export class FakeHumanExecution implements Execution {
}
private shouldAttack(other: Player): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
if (this.player.isOnSameTeam(other)) {
return false;
}
@@ -249,6 +254,7 @@ export class FakeHumanExecution implements Execution {
if (other.isTraitor()) {
return false;
}
if (this.mg === undefined) throw new Error("Not initialized");
const { difficulty } = this.mg.config().gameConfig();
if (
difficulty === Difficulty.Hard ||
@@ -264,9 +270,10 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendEmoji(enemy: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
if (enemy.type() !== PlayerType.Human) return;
const lastSent = this.lastEmojiSent.get(enemy) ?? -300;
if (this.mg === undefined) throw new Error("Not initialized");
if (this.mg.ticks() - lastSent <= 300) return;
this.lastEmojiSent.set(enemy, this.mg.ticks());
this.mg.addExecution(
@@ -279,7 +286,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendNuke(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const silos = this.player.units(UnitType.MissileSilo);
if (
silos.length === 0 ||
@@ -328,6 +336,7 @@ export class FakeHumanExecution implements Execution {
}
private removeOldNukeEvents() {
if (this.mg === undefined) throw new Error("Not initialized");
const maxAge = 500;
const tick = this.mg.ticks();
while (
@@ -339,7 +348,8 @@ export class FakeHumanExecution implements Execution {
}
private sendNuke(tile: TileRef) {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const tick = this.mg.ticks();
this.lastNukeSent.push([tick, tile]);
this.mg.addExecution(
@@ -348,10 +358,12 @@ export class FakeHumanExecution implements Execution {
}
private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number {
if (this.mg === undefined) throw new Error("Not initialized");
const game = this.mg;
// Potential damage in a 25-tile radius
const dist = euclDistFN(tile, 25, false);
let tileValue = targets
.filter((unit) => dist(this.mg, unit.tile()))
.filter((unit) => dist(game, unit.tile()))
.map((unit): number => {
switch (unit.type()) {
case UnitType.City:
@@ -374,7 +386,7 @@ export class FakeHumanExecution implements Execution {
50_000 *
targets.filter(
(unit) =>
unit.type() === UnitType.SAMLauncher && dist50(this.mg, unit.tile()),
unit.type() === UnitType.SAMLauncher && dist50(game, unit.tile()),
).length;
// Prefer tiles that are closer to a silo
@@ -388,7 +400,7 @@ export class FakeHumanExecution implements Execution {
// Don't target near recent targets
tileValue -= this.lastNukeSent
.filter(([_tick, tile]) => dist(this.mg, tile))
.filter(([_tick, tile]) => dist(game, tile))
.map((_) => 1_000_000)
.reduce((prev, cur) => prev + cur, 0);
@@ -396,14 +408,13 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendBoatAttack(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
if (this.player.isOnSameTeam(other)) return;
const closest = closestTwoTiles(
this.mg,
Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
),
Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t)),
Array.from(this.player.borderTiles()).filter((t) => this.mg?.isOceanShore(t)),
Array.from(other.borderTiles()).filter((t) => this.mg?.isOceanShore(t)),
);
if (closest === null) {
return;
@@ -430,7 +441,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSpawnStructure(type: UnitType): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const owned = this.player.unitsOwned(type);
const perceivedCostMultiplier = Math.min(owned + 1, 5);
const realCost = this.cost(type);
@@ -451,11 +463,11 @@ export class FakeHumanExecution implements Execution {
}
private structureSpawnTile(type: UnitType): TileRef | null {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
const tiles =
type === UnitType.Port
? Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
this.mg?.isOceanShore(t),
)
: Array.from(this.player.tiles());
if (tiles.length === 0) return null;
@@ -490,7 +502,8 @@ export class FakeHumanExecution implements Execution {
}
private structureSpawnTileValue(type: UnitType): (tile: TileRef) => number {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const borderTiles = this.player.borderTiles();
const { mg } = this;
const otherUnits = this.player.units(type);
@@ -547,7 +560,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSpawnWarship(): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
if (!this.random.chance(50)) {
return false;
}
@@ -577,6 +591,7 @@ export class FakeHumanExecution implements Execution {
}
private randTerritoryTile(p: Player): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
const boundingBox = calculateBoundingBox(this.mg, p.borderTiles());
for (let i = 0; i < 100; i++) {
const randX = this.random.nextInt(boundingBox.min.x, boundingBox.max.x);
@@ -594,6 +609,7 @@ export class FakeHumanExecution implements Execution {
}
private warshipSpawnTile(portTile: TileRef): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
const radius = 250;
for (let attempts = 0; attempts < 50; attempts++) {
const randX = this.random.nextInt(
@@ -618,14 +634,16 @@ export class FakeHumanExecution implements Execution {
}
private cost(type: UnitType): Gold {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
return this.mg.unitInfo(type).cost(this.player);
}
sendBoatRandomly() {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
this.mg?.isOceanShore(t),
);
if (oceanShore.length === 0) {
return;
@@ -651,6 +669,7 @@ export class FakeHumanExecution implements Execution {
}
randomLand(): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
const delta = 25;
let tries = 0;
while (tries < 50) {
@@ -676,7 +695,8 @@ export class FakeHumanExecution implements Execution {
}
private randomBoatTarget(tile: TileRef, dist: number): TileRef | null {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
const x = this.mg.x(tile);
const y = this.mg.y(tile);
for (let i = 0; i < 500; i++) {
+16 -7
View File
@@ -16,20 +16,20 @@ import { simpleHash } from "../Util";
export class MirvExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private nuke: Unit | null = null;
private readonly mirvRange = 1500;
private readonly warheadCount = 350;
private random: PseudoRandom;
private random: PseudoRandom | undefined;
private pathFinder: ParabolaPathFinder;
private pathFinder: ParabolaPathFinder | undefined;
private targetPlayer: Player | TerraNullius;
private targetPlayer: Player | TerraNullius | undefined;
private separateDst: TileRef;
private separateDst: TileRef | undefined;
private speed = -1;
@@ -61,6 +61,9 @@ export class MirvExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (this.targetPlayer === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
const spawn = this.player.canBuild(UnitType.MIRV, this.dst);
if (spawn === false) {
@@ -98,7 +101,9 @@ export class MirvExecution implements Execution {
}
private separate() {
if (this.nuke === null) throw new Error("uninitialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (this.nuke === null) throw new Error("Not initialized");
const dsts: TileRef[] = [this.dst];
let attempts = 1000;
while (attempts > 0 && dsts.length < this.warheadCount) {
@@ -110,9 +115,10 @@ export class MirvExecution implements Execution {
dsts.push(potential);
}
console.log(`dsts: ${dsts.length}`);
const game = this.mg;
dsts.sort(
(a, b) =>
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst),
game.manhattanDist(b, this.dst) - game.manhattanDist(a, this.dst),
);
console.log(`got ${dsts.length} dsts!!`);
@@ -133,6 +139,8 @@ export class MirvExecution implements Execution {
}
randomLand(ref: TileRef, taken: TileRef[]): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
let tries = 0;
const mirvRange2 = this.mirvRange * this.mirvRange;
while (tries < 100) {
@@ -168,6 +176,7 @@ export class MirvExecution implements Execution {
}
private proximityCheck(tile: TileRef, taken: TileRef[]): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
for (const t of taken) {
if (this.mg.manhattanDist(tile, t) < 55) {
return true;
+2 -1
View File
@@ -3,7 +3,7 @@ import { TileRef } from "../game/GameMap";
export class MissileSiloExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private silo: Unit | null = null;
constructor(
@@ -38,6 +38,7 @@ export class MissileSiloExecution implements Execution {
return;
}
if (this.mg === undefined) throw new Error("Not initialized");
const cooldown =
this.mg.config().SiloCooldown() - (this.mg.ticks() - frontTime);
+13 -2
View File
@@ -18,10 +18,10 @@ const SPRITE_RADIUS = 16;
export class NukeExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private nuke: Unit | null = null;
private tilesToDestroyCache: Set<TileRef> | undefined;
private pathFinder: ParabolaPathFinder;
private pathFinder: ParabolaPathFinder | undefined;
constructor(
private readonly nukeType: NukeType,
@@ -41,6 +41,7 @@ export class NukeExecution implements Execution {
}
public target(): Player | TerraNullius {
if (this.mg === undefined) throw new Error("Not initialized");
return this.mg.owner(this.dst);
}
@@ -51,6 +52,7 @@ export class NukeExecution implements Execution {
if (this.nuke === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
const rand = new PseudoRandom(this.mg.ticks());
const inner2 = magnitude.inner * magnitude.inner;
@@ -63,6 +65,7 @@ export class NukeExecution implements Execution {
}
private maybeBreakAlliances(toDestroy: Set<TileRef>) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
throw new Error("Not initialized");
}
@@ -94,6 +97,8 @@ export class NukeExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
const spawn = this.src ?? this.player.canBuild(this.nukeType, this.dst);
if (spawn === false) {
@@ -179,6 +184,8 @@ export class NukeExecution implements Execution {
}
private getTrajectory(target: TileRef): TrajectoryTile[] {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
const trajectoryTiles: TrajectoryTile[] = [];
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() ** 2;
@@ -199,6 +206,7 @@ export class NukeExecution implements Execution {
nukeTile: TileRef,
targetRangeSquared: number,
): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
return (
this.mg.euclideanDistSquared(nukeTile, targetTile) < targetRangeSquared ||
(this.src !== undefined &&
@@ -211,6 +219,7 @@ export class NukeExecution implements Execution {
if (this.nuke === null || this.nuke.targetTile() === undefined) {
return;
}
if (this.mg === undefined) throw new Error("Not initialized");
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() ** 2;
const targetTile = this.nuke.targetTile();
@@ -221,6 +230,7 @@ export class NukeExecution implements Execution {
}
private detonate() {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
throw new Error("Not initialized");
}
@@ -304,6 +314,7 @@ export class NukeExecution implements Execution {
}
private redrawBuildings(range: number) {
if (this.mg === undefined) throw new Error("Not initialized");
const rangeSquared = range * range;
for (const unit of this.mg.units()) {
if (isStructureType(unit.type())) {
+12 -5
View File
@@ -7,9 +7,9 @@ import { GameImpl } from "../game/GameImpl";
export class PlayerExecution implements Execution {
private readonly ticksPerClusterCalc = 20;
private config: Config;
private config: Config | undefined;
private lastCalc = 0;
private mg: Game;
private mg: Game | undefined;
private active = true;
constructor(private readonly player: Player) {}
@@ -26,13 +26,15 @@ export class PlayerExecution implements Execution {
}
tick(ticks: number) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.config === undefined) throw new Error("Not initialized");
this.player.decayRelations();
this.player.units().forEach((u) => {
const tileOwner = this.mg.owner(u.tile());
const tileOwner = this.mg?.owner(u.tile());
if (u.info().territoryBound) {
if (tileOwner.isPlayer()) {
if (tileOwner?.isPlayer()) {
if (tileOwner !== this.player) {
this.mg.player(tileOwner.id()).captureUnit(u);
this.mg?.player(tileOwner.id()).captureUnit(u);
}
} else {
u.delete();
@@ -98,6 +100,7 @@ export class PlayerExecution implements Execution {
}
private removeClusters() {
if (this.mg === undefined) throw new Error("Not initialized");
const clusters = this.calculateClusters();
clusters.sort((a, b) => b.size - a.size);
@@ -117,6 +120,7 @@ export class PlayerExecution implements Execution {
}
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
if (this.mg === undefined) throw new Error("Not initialized");
const enemies = new Set<number>();
for (const tile of cluster) {
const isOceanShore = this.mg.isOceanShore(tile);
@@ -151,6 +155,7 @@ export class PlayerExecution implements Execution {
}
private isSurrounded(cluster: Set<TileRef>): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
const enemyTiles = new Set<TileRef>();
for (const tr of cluster) {
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
@@ -174,6 +179,7 @@ export class PlayerExecution implements Execution {
}
private removeCluster(cluster: Set<TileRef>) {
if (this.mg === undefined) throw new Error("Not initialized");
if (
Array.from(cluster).some(
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
@@ -208,6 +214,7 @@ export class PlayerExecution implements Execution {
}
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
if (this.mg === undefined) throw new Error("Not initialized");
const neighborsIDs = new Set<number>();
for (const t of cluster) {
for (const neighbor of this.mg.neighbors(t)) {
+9 -6
View File
@@ -6,10 +6,10 @@ import { TrainStationExecution } from "./TrainStationExecution";
export class PortExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private port: Unit | null = null;
private random: PseudoRandom;
private checkOffset: number;
private random: PseudoRandom | undefined;
private checkOffset: number | undefined;
constructor(
private player: Player,
@@ -23,9 +23,9 @@ export class PortExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === null || this.random === null || this.checkOffset === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (this.checkOffset === undefined) throw new Error("Not initialized");
if (this.port === null) {
const { tile } = this;
const spawn = this.player.canBuild(UnitType.Port, tile);
@@ -77,6 +77,8 @@ export class PortExecution implements Execution {
}
shouldSpawnTradeShip(): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
const numTradeShips = this.mg.unitCount(UnitType.TradeShip);
const spawnRate = this.mg.config().tradeShipSpawnRate(numTradeShips);
const level = this.port?.level() ?? 0;
@@ -89,6 +91,7 @@ export class PortExecution implements Execution {
}
createStation(): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.port !== null) {
const nearbyFactory = this.mg.hasUnitNearby(
this.port.tile(),
+4 -2
View File
@@ -1,8 +1,8 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class QuickChatExecution implements Execution {
private recipient: Player;
private mg: Game;
private recipient: Player | undefined;
private mg: Game | undefined;
private active = true;
@@ -27,6 +27,8 @@ export class QuickChatExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.recipient === undefined) throw new Error("Not initialized");
const message = this.getMessageFromKey(this.quickChatKey);
this.mg.displayChat(
+4 -7
View File
@@ -4,7 +4,7 @@ import { Railroad } from "../game/Railroad";
import { TileRef } from "../game/GameMap";
export class RailroadExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private active = true;
private headIndex = 0;
private tailIndex = 0;
@@ -52,6 +52,7 @@ export class RailroadExecution implements Execution {
/* eslint-enable sort-keys */
private computeExtremityDirection(tile: TileRef, next: TileRef): RailType {
if (this.mg === undefined) throw new Error("Not initialized");
const x = this.mg.x(tile);
const y = this.mg.y(tile);
const nextX = this.mg.x(next);
@@ -75,9 +76,7 @@ export class RailroadExecution implements Execution {
current: TileRef,
next: TileRef,
): RailType {
if (this.mg === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
const x1 = this.mg.x(prev);
const y1 = this.mg.y(prev);
const x2 = this.mg.x(current);
@@ -114,9 +113,7 @@ export class RailroadExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
if (!this.activeSourceOrDestination()) {
this.active = false;
return;
+5 -2
View File
@@ -5,8 +5,8 @@ const cancelDelay = 20;
export class RetreatExecution implements Execution {
private active = true;
private retreatOrdered = false;
private startTick: number;
private mg: Game;
private startTick: number | undefined;
private mg: Game | undefined;
constructor(
private readonly player: Player,
private readonly attackID: string,
@@ -18,6 +18,9 @@ export class RetreatExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.startTick === undefined) throw new Error("Not initialized");
if (!this.retreatOrdered) {
this.player.orderRetreat(this.attackID);
this.retreatOrdered = true;
+5 -5
View File
@@ -126,14 +126,14 @@ class SAMTargetingSystem {
}
export class SAMLauncherExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private active = true;
// As MIRV go very fast we have to detect them very early but we only
// shoot the one targeting very close (MIRVWarheadProtectionRadius)
private readonly MIRVWarheadSearchRadius = 400;
private readonly MIRVWarheadProtectionRadius = 50;
private targetingSystem: SAMTargetingSystem;
private targetingSystem: SAMTargetingSystem | undefined;
private pseudoRandom: PseudoRandom | undefined;
@@ -156,6 +156,7 @@ export class SAMLauncherExecution implements Execution {
return true;
}
if (this.mg === undefined) throw new Error("Not initialized");
if (type === UnitType.MIRVWarhead) {
return random < this.mg.config().samWarheadHittingChance();
}
@@ -164,9 +165,7 @@ export class SAMLauncherExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
if (this.sam === null) {
if (this.tile === null) {
throw new Error("tile is null");
@@ -211,6 +210,7 @@ export class SAMLauncherExecution implements Execution {
this.MIRVWarheadSearchRadius,
UnitType.MIRVWarhead,
({ unit }) => {
if (this.mg === undefined) return false;
if (!isUnit(unit)) return false;
if (unit.owner() === this.player) return false;
if (this.player.isFriendly(unit.owner())) return false;
+4 -2
View File
@@ -13,9 +13,9 @@ import { TileRef } from "../game/GameMap";
export class SAMMissileExecution implements Execution {
private active = true;
private pathFinder: AirPathFinder;
private pathFinder: AirPathFinder | undefined;
private SAMMissile: Unit | undefined;
private mg: Game;
private mg: Game | undefined;
private speed = 0;
constructor(
@@ -33,6 +33,8 @@ export class SAMMissileExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
this.SAMMissile ??= this._owner.buildUnit(
UnitType.SAMMissile,
this.spawn,
+7 -3
View File
@@ -5,11 +5,11 @@ import { TileRef } from "../game/GameMap";
export class ShellExecution implements Execution {
private active = true;
private pathFinder: AirPathFinder;
private pathFinder: AirPathFinder | undefined;
private shell: Unit | undefined;
private mg: Game;
private mg: Game | undefined;
private destroyAtTick = -1;
private random: PseudoRandom;
private random: PseudoRandom | undefined;
constructor(
private readonly spawn: TileRef,
@@ -25,6 +25,8 @@ export class ShellExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
this.shell ??= this._owner.buildUnit(UnitType.Shell, this.spawn, {});
if (!this.shell.isActive()) {
this.active = false;
@@ -62,6 +64,8 @@ export class ShellExecution implements Execution {
}
private effectOnTarget(): number {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
const { damage } = this.mg.config().unitInfo(UnitType.Shell);
const baseDamage = damage ?? 250;
+2 -1
View File
@@ -6,7 +6,7 @@ import { getSpawnTiles } from "./Util";
export class SpawnExecution implements Execution {
active = true;
private mg: Game;
private mg: Game | undefined;
constructor(
private readonly playerInfo: PlayerInfo,
@@ -20,6 +20,7 @@ export class SpawnExecution implements Execution {
tick(ticks: number) {
this.active = false;
if (this.mg === undefined) throw new Error("Not initialized");
if (!this.mg.isValidRef(this.tile)) {
console.warn(`SpawnExecution: tile ${this.tile} not valid`);
return;
+2 -1
View File
@@ -1,7 +1,7 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class TargetPlayerExecution implements Execution {
private target: Player;
private target: Player | undefined;
private active = true;
@@ -21,6 +21,7 @@ export class TargetPlayerExecution implements Execution {
}
tick(ticks: number): void {
if (this.target === undefined) throw new Error("Not initialized");
if (this.requestor.canTarget(this.target)) {
this.requestor.target(this.target);
this.target.updateRelation(this.requestor, -40);
+5 -2
View File
@@ -14,10 +14,10 @@ import { renderNumber } from "../../client/Utils";
export class TradeShipExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private tradeShip: Unit | undefined;
private wasCaptured = false;
private pathFinder: PathFinder;
private pathFinder: PathFinder | undefined;
private tilesTraveled = 0;
constructor(
@@ -32,6 +32,8 @@ export class TradeShipExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (this.tradeShip === undefined) {
const spawn = this.origOwner.canBuild(
UnitType.TradeShip,
@@ -131,6 +133,7 @@ export class TradeShipExecution implements Execution {
}
private complete() {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.tradeShip === undefined) throw new Error("Not initialized");
this.active = false;
this.tradeShip.delete(false);
+6 -2
View File
@@ -4,9 +4,9 @@ import { TrainExecution } from "./TrainExecution";
import { TrainStation } from "../game/TrainStation";
export class TrainStationExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private active = true;
private random: PseudoRandom;
private random: PseudoRandom | undefined;
private station: TrainStation | null = null;
private readonly numCars = 5;
private lastSpawnTick = 0;
@@ -49,6 +49,8 @@ export class TrainStationExecution implements Execution {
}
private shouldSpawnTrain(clusterSize: number): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
const spawnRate = this.mg.config().trainSpawnRate(clusterSize);
for (let i = 0; i < this.unit.level(); i++) {
if (this.random.chance(spawnRate)) {
@@ -59,6 +61,8 @@ export class TrainStationExecution implements Execution {
}
private spawnTrain(station: TrainStation, currentTick: number) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (
!this.spawnTrains ||
currentTick - this.lastSpawnTick < this.ticksCooldown
+12 -7
View File
@@ -15,23 +15,23 @@ import { TileRef } from "../game/GameMap";
import { targetTransportTile } from "../game/TransportShipUtils";
export class TransportShipExecution implements Execution {
private lastMove: number;
private lastMove: number | undefined;
// TODO: make this configurable
private readonly ticksPerMove = 1;
private active = true;
private mg: Game;
private target: Player | TerraNullius;
private mg: Game | undefined;
private target: Player | TerraNullius | undefined;
// TODO make private
public path: TileRef[];
private dst: TileRef | null;
public path: TileRef[] | undefined;
private dst: TileRef | null = null;
private boat: Unit;
private boat: Unit | undefined;
private pathFinder: PathFinder;
private pathFinder: PathFinder | undefined;
constructor(
private readonly attacker: Player,
@@ -158,6 +158,11 @@ export class TransportShipExecution implements Execution {
if (!this.active) {
return;
}
if (this.boat === undefined) throw new Error("Not initialized");
if (this.lastMove === undefined) throw new Error("Not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.target === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (!this.boat.isActive()) {
this.active = false;
return;
@@ -2,7 +2,7 @@ import { Execution, Game, Player, Unit } from "../game/Game";
export class UpgradeStructureExecution implements Execution {
private structure: Unit | undefined;
private readonly cost: bigint;
private readonly cost: bigint | undefined;
constructor(
private readonly player: Player,
+17 -5
View File
@@ -14,10 +14,10 @@ import { ShellExecution } from "./ShellExecution";
import { TileRef } from "../game/GameMap";
export class WarshipExecution implements Execution {
private random: PseudoRandom;
private warship: Unit;
private mg: Game;
private pathfinder: PathFinder;
private random: PseudoRandom | undefined;
private warship: Unit | undefined;
private mg: Game | undefined;
private pathfinder: PathFinder | undefined;
private lastShellAttack = 0;
private readonly alreadySentShell = new Set<Unit>();
@@ -51,6 +51,7 @@ export class WarshipExecution implements Execution {
}
tick(ticks: number): void {
if (this.warship === undefined) throw new Error("Not initialized");
if (this.warship.health() <= 0) {
this.warship.delete();
return;
@@ -75,6 +76,8 @@ export class WarshipExecution implements Execution {
}
private findTargetUnit(): Unit | undefined {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
const hasPort = this.warship.owner().unitCount(UnitType.Port) > 0;
const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2;
@@ -152,6 +155,8 @@ export class WarshipExecution implements Execution {
}
private shootTarget() {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
const targetUnit = this.warship.targetUnit();
if (targetUnit === undefined) return;
const shellAttackRate = this.mg.config().warshipShellAttackRate();
@@ -178,6 +183,8 @@ export class WarshipExecution implements Execution {
}
private huntDownTradeShip() {
if (this.pathfinder === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
const targetUnit = this.warship.targetUnit();
if (targetUnit === undefined) return;
for (let i = 0; i < 2; i++) {
@@ -207,6 +214,8 @@ export class WarshipExecution implements Execution {
}
private patrol() {
if (this.pathfinder === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
let targetTile = this.warship.targetTile();
if (targetTile === undefined) {
targetTile = this.randomTile();
@@ -238,7 +247,7 @@ export class WarshipExecution implements Execution {
}
isActive(): boolean {
return this.warship?.isActive();
return this.warship?.isActive() ?? false;
}
activeDuringSpawnPhase(): boolean {
@@ -246,6 +255,9 @@ export class WarshipExecution implements Execution {
}
randomTile(allowShoreline = false): TileRef | undefined {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
const maxAttemptBeforeExpand = 500;
let attempts = 0;
+2 -1
View File
@@ -15,7 +15,7 @@ import { flattenedEmojiTable } from "../../Util";
export class BotBehavior {
private enemy: Player | null = null;
private enemyUpdated: Tick;
private enemyUpdated: Tick | undefined;
private readonly assistAcceptEmoji = flattenedEmojiTable.indexOf("👍");
@@ -75,6 +75,7 @@ export class BotBehavior {
}
forgetOldEnemies() {
if (this.enemyUpdated === undefined) return;
// Forget old enemies
if (this.game.ticks() - this.enemyUpdated > 100) {
this.clearEnemy();
+1 -1
View File
@@ -283,7 +283,7 @@ export class Nation {
}
export class Cell {
public index: number;
public index: number | undefined;
private readonly strRepr: string;
+3 -2
View File
@@ -78,7 +78,7 @@ export class GameImpl implements Game {
private updates: GameUpdates = createGameUpdatesMap();
private readonly unitGrid: UnitGrid;
private playerTeams: Team[];
private playerTeams: Team[] = [];
private readonly botTeam: Team = ColoredTeams.Bot;
private readonly _railNetwork: RailNetwork = createRailNetwork(this);
@@ -125,7 +125,8 @@ export class GameImpl implements Game {
if (numPlayerTeams < 2) {
throw new Error(`Too few teams: ${numPlayerTeams}`);
} else if (numPlayerTeams < 8) {
this.playerTeams = [ColoredTeams.Red, ColoredTeams.Blue];
this.playerTeams.push(ColoredTeams.Red);
this.playerTeams.push(ColoredTeams.Blue);
if (numPlayerTeams >= 3) this.playerTeams.push(ColoredTeams.Yellow);
if (numPlayerTeams >= 4) this.playerTeams.push(ColoredTeams.Green);
if (numPlayerTeams >= 5) this.playerTeams.push(ColoredTeams.Purple);
+1 -1
View File
@@ -119,7 +119,7 @@ export class PlayerImpl implements Player {
this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id));
}
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
largestClusterBoundingBox: { min: Cell; max: Cell } | null = null;
toUpdate(): PlayerUpdate {
const outgoingAllianceRequests = this.outgoingAllianceRequests().map((ar) =>
+1 -1
View File
@@ -87,7 +87,7 @@ export function createTrainStopHandlers(
export class TrainStation {
private readonly stopHandlers: Partial<Record<UnitType, TrainStopHandler>> =
{};
private cluster: Cluster | null;
private cluster: Cluster | null = null;
private readonly railroads: Set<Railroad> = new Set();
constructor(
+2 -2
View File
@@ -106,7 +106,7 @@ export class PathFinder {
private curr: TileRef | null = null;
private dst: TileRef | null = null;
private path: TileRef[] | null = null;
private aStar: AStar<TileRef>;
private aStar: AStar<TileRef> | undefined;
private computeFinished = true;
private constructor(
@@ -170,7 +170,7 @@ export class PathFinder {
}
}
switch (this.aStar.compute()) {
switch (this.aStar?.compute()) {
case PathFindResultType.Completed:
this.computeFinished = true;
this.path = this.aStar.reconstructPath();
+13 -11
View File
@@ -8,7 +8,6 @@ import { UnitView } from "../../../src/core/game/GameView";
describe("UILayer", () => {
let game: any;
let eventBus: any;
let transformHandler: any;
beforeEach(() => {
game = {
@@ -29,19 +28,22 @@ describe("UILayer", () => {
updatesSinceLastTick: () => undefined,
};
eventBus = { on: jest.fn() };
transformHandler = {};
});
it("should initialize and redraw canvas", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
expect(ui["canvas"].width).toBe(100);
expect(ui["canvas"].height).toBe(100);
// eslint-disable-next-line prefer-destructuring
const canvas = ui["canvas"];
expect(canvas).toBeDefined();
if (canvas === undefined) throw new Error();
expect(canvas.width).toBe(100);
expect(canvas.height).toBe(100);
expect(ui["context"]).not.toBeNull();
});
it("should handle unit selection event", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
const unit = {
type: () => "Warship",
@@ -56,7 +58,7 @@ describe("UILayer", () => {
});
it("should add and clear health bars", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
const unit = {
id: () => 1,
@@ -85,7 +87,7 @@ describe("UILayer", () => {
});
it("should remove health bars for inactive units", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
const unit = {
id: () => 1,
@@ -105,7 +107,7 @@ describe("UILayer", () => {
});
it("should add loading bar for unit", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
const unit = {
id: () => 2,
@@ -117,7 +119,7 @@ describe("UILayer", () => {
});
it("should remove loading bar for inactive unit", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
const unit = {
id: () => 2,
@@ -137,7 +139,7 @@ describe("UILayer", () => {
});
it("should remove loading bar for a finished progress bar", () => {
const ui = new UILayer(game, eventBus, transformHandler);
const ui = new UILayer(game, eventBus);
ui.redraw();
const unit = {
id: () => 2,
+2 -3
View File
@@ -19,10 +19,9 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
"useDefineForClassFields": false,
"strictPropertyInitialization": false,
"strict": true
"useDefineForClassFields": false
},
"include": [
"src/**/*",