mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 13:52:45 +00:00
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:
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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>) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 +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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 +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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -283,7 +283,7 @@ export class Nation {
|
||||
}
|
||||
|
||||
export class Cell {
|
||||
public index: number;
|
||||
public index: number | undefined;
|
||||
|
||||
private readonly strRepr: string;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -19,10 +19,9 @@
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"useDefineForClassFields": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"strict": true
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
||||
Reference in New Issue
Block a user