mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 19:04:36 +00:00
strictNullChecks
This commit is contained in:
@@ -61,7 +61,7 @@ export function joinLobby(
|
||||
);
|
||||
|
||||
const userSettings: UserSettings = new UserSettings();
|
||||
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config);
|
||||
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {});
|
||||
|
||||
const transport = new Transport(lobbyConfig, eventBus);
|
||||
|
||||
@@ -107,6 +107,9 @@ export async function createClientGame(
|
||||
userSettings: UserSettings,
|
||||
terrainLoad: Promise<TerrainMapData> | null,
|
||||
): Promise<ClientGameRunner> {
|
||||
if (typeof lobbyConfig.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const config = await getConfig(
|
||||
lobbyConfig.gameStartInfo.config,
|
||||
userSettings,
|
||||
@@ -198,6 +201,9 @@ export class ClientGameRunner {
|
||||
winner = update.winner as Team;
|
||||
}
|
||||
|
||||
if (typeof this.lobby.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const record = createGameRecord(
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.gameStartInfo,
|
||||
@@ -229,16 +235,20 @@ export class ClientGameRunner {
|
||||
this.renderer.initialize();
|
||||
this.input.initialize();
|
||||
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
|
||||
if (typeof this.lobby.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
if ("errMsg" in gu) {
|
||||
showErrorModal(
|
||||
gu.errMsg,
|
||||
gu.stack,
|
||||
gu.stack ?? "missing",
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.clientID,
|
||||
);
|
||||
this.stop(true);
|
||||
return;
|
||||
}
|
||||
if (gu.updates === null) return;
|
||||
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
|
||||
this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash));
|
||||
});
|
||||
@@ -282,6 +292,9 @@ export class ClientGameRunner {
|
||||
}
|
||||
}
|
||||
if (message.type == "desync") {
|
||||
if (typeof this.lobby.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
showErrorModal(
|
||||
`desync from server: ${JSON.stringify(message)}`,
|
||||
"",
|
||||
@@ -344,10 +357,9 @@ export class ClientGameRunner {
|
||||
return;
|
||||
}
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
if (this.myPlayer == null) {
|
||||
return;
|
||||
}
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
this.myPlayer.actions(tile).then((actions) => {
|
||||
console.log(`got actions: ${JSON.stringify(actions)}`);
|
||||
|
||||
@@ -33,7 +33,7 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private players: string[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
|
||||
private playersInterval = null;
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
// Add a new timer for debouncing bot changes
|
||||
private botsUpdateTimer: number | null = null;
|
||||
|
||||
@@ -493,7 +493,7 @@ export class HostLobbyModal extends LitElement {
|
||||
.then((response) => response.json())
|
||||
.then((data: GameInfo) => {
|
||||
console.log(`got game info response: ${JSON.stringify(data)}`);
|
||||
this.players = data.clients.map((p) => p.username);
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export class InputHandler {
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
private moveInterval: NodeJS.Timeout = null;
|
||||
private moveInterval: NodeJS.Timeout | null = null;
|
||||
private activeKeys = new Set<string>();
|
||||
|
||||
private readonly PAN_SPEED = 5;
|
||||
@@ -392,7 +392,9 @@ export class InputHandler {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.moveInterval);
|
||||
if (this.moveInterval !== null) {
|
||||
clearInterval(this.moveInterval);
|
||||
}
|
||||
this.activeKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
@state() private hasJoined = false;
|
||||
@state() private players: string[] = [];
|
||||
|
||||
private playersInterval = null;
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -98,7 +98,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.lobbyIdInput.value = null;
|
||||
this.lobbyIdInput.value = "";
|
||||
this.modalEl?.close();
|
||||
if (this.playersInterval) {
|
||||
clearInterval(this.playersInterval);
|
||||
@@ -263,7 +263,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data: GameInfo) => {
|
||||
this.players = data.clients.map((p) => p.username);
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
})
|
||||
.catch((error) => {
|
||||
consolex.error("Error polling players:", error);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GameConfig, GameID, GameRecord } from "../core/Schemas";
|
||||
|
||||
export interface LocalStatsData {
|
||||
[key: GameID]: {
|
||||
lobby: GameConfig;
|
||||
lobby: Partial<GameConfig>;
|
||||
// Only once the game is over
|
||||
gameRecord?: GameRecord;
|
||||
};
|
||||
@@ -26,7 +26,7 @@ function save(stats: LocalStatsData) {
|
||||
|
||||
// The user can quit the game anytime so better save the lobby as soon as the
|
||||
// game starts.
|
||||
export function startGame(id: GameID, lobby: GameConfig) {
|
||||
export function startGame(id: GameID, lobby: Partial<GameConfig>) {
|
||||
if (typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ export class LocalServer {
|
||||
private intents: Intent[] = [];
|
||||
private startedAt: number;
|
||||
|
||||
private endTurnIntervalID;
|
||||
private endTurnIntervalID: NodeJS.Timeout;
|
||||
|
||||
private paused = false;
|
||||
|
||||
private winner: ClientSendWinnerMessage = null;
|
||||
private winner: ClientSendWinnerMessage | null = null;
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
|
||||
constructor(
|
||||
@@ -46,6 +46,9 @@ export class LocalServer {
|
||||
this.turns = decompressGameRecord(this.lobbyConfig.gameRecord).turns;
|
||||
console.log(`loaded turns: ${JSON.stringify(this.turns)}`);
|
||||
}
|
||||
if (typeof this.lobbyConfig.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
this.clientMessage(
|
||||
ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
@@ -125,6 +128,9 @@ export class LocalServer {
|
||||
if (this.paused) {
|
||||
return;
|
||||
}
|
||||
if (typeof this.lobbyConfig.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.lobbyConfig.gameStartInfo.gameID,
|
||||
@@ -149,6 +155,9 @@ export class LocalServer {
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
},
|
||||
];
|
||||
if (typeof this.lobbyConfig.gameStartInfo === "undefined") {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const record = createGameRecord(
|
||||
this.lobbyConfig.gameStartInfo.gameID,
|
||||
this.lobbyConfig.gameStartInfo,
|
||||
@@ -156,8 +165,8 @@ export class LocalServer {
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
Date.now(),
|
||||
this.winner?.winner,
|
||||
this.winner?.winnerType,
|
||||
this.winner?.winner ?? null,
|
||||
this.winner?.winnerType ?? null,
|
||||
this.allPlayersStats,
|
||||
);
|
||||
if (!saveFullGame) {
|
||||
|
||||
+28
-21
@@ -40,7 +40,7 @@ export interface JoinLobbyEvent {
|
||||
}
|
||||
|
||||
class Client {
|
||||
private gameStop: () => void;
|
||||
private gameStop: (() => void) | null;
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
@@ -106,15 +106,19 @@ class Client {
|
||||
"single-player-modal",
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
document.getElementById("single-player").addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
const singlePlayer = document.getElementById("single-player");
|
||||
if (singlePlayer === null) throw new Error("Missing single-player");
|
||||
singlePlayer.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
spModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
const hlpModal = document.querySelector("help-modal") as HelpModal;
|
||||
hlpModal instanceof HelpModal;
|
||||
document.getElementById("help-button").addEventListener("click", () => {
|
||||
const helpButton = document.getElementById("help-button");
|
||||
if (helpButton === null) throw new Error("Missing help-button");
|
||||
helpButton.addEventListener("click", () => {
|
||||
hlpModal.open();
|
||||
});
|
||||
|
||||
@@ -122,26 +126,29 @@ class Client {
|
||||
"host-lobby-modal",
|
||||
) as HostPrivateLobbyModal;
|
||||
hostModal instanceof HostPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("host-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
hostModal.open();
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
});
|
||||
const hostLobbyButton = document.getElementById("host-lobby-button");
|
||||
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
|
||||
hostLobbyButton.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
hostModal.open();
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
});
|
||||
|
||||
this.joinModal = document.querySelector(
|
||||
"join-private-lobby-modal",
|
||||
) as JoinPrivateLobbyModal;
|
||||
this.joinModal instanceof JoinPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("join-private-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
this.joinModal.open();
|
||||
}
|
||||
});
|
||||
const joinPrivateLobbyButton = document.getElementById(
|
||||
"join-private-lobby-button",
|
||||
);
|
||||
if (joinPrivateLobbyButton === null)
|
||||
throw new Error("Missing join-private-lobby-button");
|
||||
joinPrivateLobbyButton.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
this.joinModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.userSettings.darkMode()) {
|
||||
document.documentElement.classList.add("dark");
|
||||
@@ -190,10 +197,10 @@ class Client {
|
||||
gameID: lobby.gameID,
|
||||
serverConfig: config,
|
||||
flag:
|
||||
this.flagInput.getCurrentFlag() == "xx"
|
||||
this.flagInput === null || this.flagInput.getCurrentFlag() == "xx"
|
||||
? ""
|
||||
: this.flagInput.getCurrentFlag(),
|
||||
playerName: this.usernameInput.getCurrentUsername(),
|
||||
playerName: this.usernameInput?.getCurrentUsername() ?? "",
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
clientID: lobby.clientID,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo,
|
||||
|
||||
@@ -14,7 +14,7 @@ export class PublicLobby extends LitElement {
|
||||
@state() public isLobbyHighlighted: boolean = false;
|
||||
@state() private isButtonDebounced: boolean = false;
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: GameInfo = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
private debounceDelay: number = 750;
|
||||
private lobbyIDToStart = new Map<GameID, number>();
|
||||
|
||||
@@ -46,7 +46,8 @@ export class PublicLobby extends LitElement {
|
||||
// Store the start time on first fetch because endpoint is cached, causing
|
||||
// the time to appear irregular.
|
||||
if (!this.lobbyIDToStart.has(l.gameID)) {
|
||||
this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now());
|
||||
const msUntilStart = l.msUntilStart ?? 0;
|
||||
this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now());
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -82,17 +83,13 @@ export class PublicLobby extends LitElement {
|
||||
if (!lobby?.gameConfig) {
|
||||
return;
|
||||
}
|
||||
const timeRemaining = Math.max(
|
||||
0,
|
||||
Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000),
|
||||
);
|
||||
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
|
||||
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
|
||||
|
||||
// Format time to show minutes and seconds
|
||||
const minutes = Math.floor(timeRemaining / 60);
|
||||
const seconds = timeRemaining % 60;
|
||||
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
||||
const playersRemainingBeforeMax =
|
||||
lobby.gameConfig.maxPlayers - lobby.numClients;
|
||||
|
||||
return html`
|
||||
<button
|
||||
|
||||
+15
-4
@@ -60,14 +60,14 @@ export class SendSpawnIntentEvent implements GameEvent {
|
||||
|
||||
export class SendAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly targetID: PlayerID | null,
|
||||
public readonly troops: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendBoatAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly targetID: PlayerID | null,
|
||||
public readonly cell: Cell,
|
||||
public readonly troops: number,
|
||||
) {}
|
||||
@@ -148,7 +148,7 @@ export class MoveWarshipIntentEvent implements GameEvent {
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private socket: WebSocket;
|
||||
private socket: WebSocket | null;
|
||||
|
||||
private localServer: LocalServer;
|
||||
|
||||
@@ -279,7 +279,12 @@ export class Transport {
|
||||
console.log("Connected to game server!");
|
||||
while (this.buffer.length > 0) {
|
||||
console.log("sending dropped message");
|
||||
this.sendMsg(this.buffer.pop());
|
||||
const msg = this.buffer.pop();
|
||||
if (typeof msg === "undefined") {
|
||||
console.warn("msg is undefined");
|
||||
continue;
|
||||
}
|
||||
this.sendMsg(msg);
|
||||
}
|
||||
onconnect();
|
||||
};
|
||||
@@ -295,6 +300,7 @@ export class Transport {
|
||||
};
|
||||
this.socket.onerror = (err) => {
|
||||
console.error("Socket encountered error: ", err, "Closing socket");
|
||||
if (this.socket === null) return;
|
||||
this.socket.close();
|
||||
};
|
||||
this.socket.onclose = (event: CloseEvent) => {
|
||||
@@ -349,6 +355,7 @@ export class Transport {
|
||||
return;
|
||||
}
|
||||
this.stopPing();
|
||||
if (this.socket === null) return;
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
console.log("on stop: leaving game");
|
||||
this.socket.close();
|
||||
@@ -495,6 +502,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private onSendWinnerEvent(event: SendWinnerEvent) {
|
||||
if (this.socket === null) return;
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientSendWinnerSchema.parse({
|
||||
type: "winner",
|
||||
@@ -516,6 +524,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private onSendHashEvent(event: SendHashEvent) {
|
||||
if (this.socket === null) return;
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientMessageSchema.parse({
|
||||
type: "hash",
|
||||
@@ -553,6 +562,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.socket === null) return;
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
type: "intent",
|
||||
@@ -575,6 +585,7 @@ export class Transport {
|
||||
if (this.isLocal) {
|
||||
this.localServer.onMessage(msg);
|
||||
} else {
|
||||
if (this.socket === null) return;
|
||||
if (
|
||||
this.socket.readyState == WebSocket.CLOSED ||
|
||||
this.socket.readyState == WebSocket.CLOSED
|
||||
|
||||
@@ -64,7 +64,7 @@ export class UsernameInput extends LitElement {
|
||||
this.storeUsername(this.username);
|
||||
this.validationError = "";
|
||||
} else {
|
||||
this.validationError = result.error;
|
||||
this.validationError = result.error ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,9 @@ export class GameRenderer {
|
||||
public uiState: UIState,
|
||||
private layers: Layer[],
|
||||
) {
|
||||
this.context = canvas.getContext("2d");
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
|
||||
@@ -9,8 +9,8 @@ export class TransformHandler {
|
||||
private offsetX: number = -350;
|
||||
private offsetY: number = -200;
|
||||
|
||||
private target: Cell;
|
||||
private intervalID = null;
|
||||
private target: Cell | null;
|
||||
private intervalID: NodeJS.Timeout | null = null;
|
||||
private changed = false;
|
||||
|
||||
constructor(
|
||||
@@ -166,6 +166,8 @@ export class TransformHandler {
|
||||
const { screenX, screenY } = this.screenCenter();
|
||||
const screenMapCenter = new Cell(screenX, screenY);
|
||||
|
||||
if (this.target === null) throw new Error("null target");
|
||||
|
||||
if (
|
||||
this.game.manhattanDist(
|
||||
this.game.ref(screenX, screenY),
|
||||
|
||||
@@ -398,7 +398,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
private refresh() {
|
||||
this.game
|
||||
.myPlayer()
|
||||
.actions(this.clickedTile)
|
||||
?.actions(this.clickedTile)
|
||||
.then((actions) => {
|
||||
this.playerActions = actions;
|
||||
this.requestUpdate();
|
||||
|
||||
@@ -253,7 +253,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
<label class="block text-white mb-1" translate="no"
|
||||
>Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%
|
||||
(${renderTroops(
|
||||
this.game?.myPlayer()?.troops() * this.attackRatio,
|
||||
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
||||
)})</label
|
||||
>
|
||||
<div class="relative h-8">
|
||||
|
||||
@@ -100,8 +100,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
tick() {
|
||||
this.active = true;
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
for (const [ut, fn] of this.updateMap) {
|
||||
updates[ut]?.forEach((u) => fn(u));
|
||||
if (updates) {
|
||||
for (const [ut, fn] of this.updateMap) {
|
||||
updates[ut]?.forEach(fn);
|
||||
}
|
||||
}
|
||||
|
||||
let remainingEvents = this.events.filter((event) => {
|
||||
@@ -301,6 +303,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
: update.player2ID === myPlayer.smallID()
|
||||
? update.player1ID
|
||||
: null;
|
||||
if (otherID === null) return;
|
||||
const other = this.game.playerBySmallID(otherID) as PlayerView;
|
||||
if (!other || !myPlayer.isAlive() || !other.isAlive()) return;
|
||||
|
||||
@@ -414,8 +417,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
<button
|
||||
translate="no"
|
||||
class="ml-2"
|
||||
@click=${() =>
|
||||
this.emitGoToPlayerEvent(attack.attackerID)}
|
||||
@click=${() => {
|
||||
attack.attackerID &&
|
||||
this.emitGoToPlayerEvent(attack.attackerID);
|
||||
}}
|
||||
>
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
@@ -597,7 +602,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
${event.focusID
|
||||
? html`<button
|
||||
@click=${() => {
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
event.focusID &&
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
}}
|
||||
>
|
||||
${this.getEventDescription(event)}
|
||||
|
||||
@@ -21,7 +21,7 @@ class RenderInfo {
|
||||
constructor(
|
||||
public player: PlayerView,
|
||||
public lastRenderCalc: number,
|
||||
public location: Cell,
|
||||
public location: Cell | null,
|
||||
public fontSize: number,
|
||||
public fontColor: string,
|
||||
public element: HTMLElement,
|
||||
@@ -372,7 +372,7 @@ export class NameLayer implements Layer {
|
||||
emoji.recipientID == myPlayer?.smallID(),
|
||||
);
|
||||
|
||||
if (this.game.config().userSettings().emojis() && emojis.length > 0) {
|
||||
if (this.game.config().userSettings()?.emojis() && emojis.length > 0) {
|
||||
if (!existingEmoji) {
|
||||
const emojiDiv = document.createElement("div");
|
||||
emojiDiv.setAttribute("data-icon", "emoji");
|
||||
@@ -418,8 +418,9 @@ export class NameLayer implements Layer {
|
||||
});
|
||||
const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
|
||||
const detonationDst = unit.detonationDst();
|
||||
if (typeof detonationDst === "undefined") return false;
|
||||
const targetId = this.game.owner(detonationDst).id();
|
||||
return myPlayer && targetId == this.myPlayer.id();
|
||||
return myPlayer && targetId == myPlayer.id();
|
||||
});
|
||||
const existingNuke = iconsDiv.querySelector(
|
||||
'[data-icon="nuke"]',
|
||||
@@ -484,9 +485,9 @@ export class NameLayer implements Layer {
|
||||
if (this.myPlayer != null) {
|
||||
return this.myPlayer;
|
||||
}
|
||||
this.myPlayer = this.game
|
||||
.playerViews()
|
||||
.find((p) => p.clientID() == this.clientID);
|
||||
this.myPlayer =
|
||||
this.game.playerViews().find((p) => p.clientID() == this.clientID) ??
|
||||
null;
|
||||
return this.myPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +124,10 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.hasWinner =
|
||||
this.hasWinner ||
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].length > 0;
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = 0;
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import {
|
||||
@@ -161,7 +161,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
private renderPlayerInfo(player: PlayerView) {
|
||||
const myPlayer = this.myPlayer();
|
||||
const isFriendly = myPlayer?.isFriendly(player);
|
||||
let relationHtml = null;
|
||||
let relationHtml: TemplateResult | null = null;
|
||||
const attackingTroops = player
|
||||
.outgoingAttacks()
|
||||
.map((a) => a.troops)
|
||||
|
||||
@@ -35,8 +35,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
public emojiTable: EmojiTable;
|
||||
|
||||
private actions: PlayerActions = null;
|
||||
private tile: TileRef = null;
|
||||
private actions: PlayerActions | null = null;
|
||||
private tile: TileRef | null = null;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
@@ -155,7 +155,11 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return 0;
|
||||
}
|
||||
let sum = 0;
|
||||
const nukes = stats.sentNukes[this.g.myPlayer().id()];
|
||||
const player = this.g.myPlayer();
|
||||
if (player === null) {
|
||||
return 0;
|
||||
}
|
||||
const nukes = stats.sentNukes[player.id()];
|
||||
if (!nukes) {
|
||||
return 0;
|
||||
}
|
||||
@@ -172,26 +176,24 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return html``;
|
||||
}
|
||||
const myPlayer = this.g.myPlayer();
|
||||
if (myPlayer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (myPlayer == null) return;
|
||||
if (this.tile === null) return;
|
||||
let other = this.g.owner(this.tile);
|
||||
if (!other.isPlayer()) {
|
||||
throw new Error("Tile is not owned by a player");
|
||||
}
|
||||
other = other as PlayerView;
|
||||
|
||||
const canDonate = this.actions.interaction?.canDonate;
|
||||
const canDonate = this.actions?.interaction?.canDonate;
|
||||
const canSendAllianceRequest =
|
||||
this.actions.interaction?.canSendAllianceRequest;
|
||||
this.actions?.interaction?.canSendAllianceRequest;
|
||||
const canSendEmoji =
|
||||
other == myPlayer
|
||||
? this.actions.canSendEmojiAllPlayers
|
||||
: this.actions.interaction?.canSendEmoji;
|
||||
const canBreakAlliance = this.actions.interaction?.canBreakAlliance;
|
||||
const canTarget = this.actions.interaction?.canTarget;
|
||||
const canEmbargo = this.actions.interaction?.canEmbargo;
|
||||
? this.actions?.canSendEmojiAllPlayers
|
||||
: this.actions?.interaction?.canSendEmoji;
|
||||
const canBreakAlliance = this.actions?.interaction?.canBreakAlliance;
|
||||
const canTarget = this.actions?.interaction?.canTarget;
|
||||
const canEmbargo = this.actions?.interaction?.canEmbargo;
|
||||
|
||||
return html`
|
||||
<div
|
||||
|
||||
@@ -46,7 +46,16 @@ export class RadialMenu implements Layer {
|
||||
|
||||
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
|
||||
private isVisible: boolean = false;
|
||||
private readonly menuItems = new Map([
|
||||
private readonly menuItems: Map<
|
||||
Slot,
|
||||
{
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
action: () => void;
|
||||
color?: string | null;
|
||||
icon?: string | null;
|
||||
}
|
||||
> = new Map([
|
||||
[
|
||||
Slot.Boat,
|
||||
{
|
||||
@@ -363,6 +372,7 @@ export class RadialMenu implements Layer {
|
||||
}
|
||||
if (actions.canBoat) {
|
||||
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
|
||||
if (this.clickedCell === null) return;
|
||||
this.eventBus.emit(
|
||||
new SendBoatAttackIntentEvent(
|
||||
this.g.owner(tile).id(),
|
||||
@@ -412,6 +422,7 @@ export class RadialMenu implements Layer {
|
||||
return;
|
||||
}
|
||||
consolex.log("Center button clicked");
|
||||
if (this.clickedCell === null) return;
|
||||
const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y);
|
||||
if (this.g.inSpawnPhase()) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell));
|
||||
@@ -436,6 +447,7 @@ export class RadialMenu implements Layer {
|
||||
action: () => void,
|
||||
) {
|
||||
const menuItem = this.menuItems.get(slot);
|
||||
if (typeof menuItem === "undefined") return;
|
||||
menuItem.action = action;
|
||||
menuItem.disabled = false;
|
||||
menuItem.color = color;
|
||||
|
||||
@@ -43,7 +43,7 @@ export class StructureLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private unitIcons: Map<string, ImageData> = new Map();
|
||||
private theme: Theme = null;
|
||||
private theme: Theme;
|
||||
|
||||
// Configuration for supported unit types only
|
||||
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
|
||||
@@ -106,6 +106,7 @@ export class StructureLayer implements Layer {
|
||||
// Create temporary canvas for icon processing
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const tempContext = tempCanvas.getContext("2d");
|
||||
if (tempContext === null) throw new Error("2d context not supported");
|
||||
tempCanvas.width = image.width;
|
||||
tempCanvas.height = image.height;
|
||||
|
||||
@@ -135,11 +136,13 @@ export class StructureLayer implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
[
|
||||
GameUpdateType.Unit
|
||||
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
||||
for (const u of unitUpdates) {
|
||||
const unit = this.game.unit(u.id);
|
||||
if (typeof unit === "undefined") continue;
|
||||
this.handleUnitRendering(unit);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -149,7 +152,9 @@ export class StructureLayer implements Layer {
|
||||
redraw() {
|
||||
console.log("structure layer redrawing");
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d", { alpha: true });
|
||||
const context = this.canvas.getContext("2d", { alpha: true });
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.game.units().forEach((u) => this.handleUnitRendering(u));
|
||||
@@ -220,7 +225,7 @@ export class StructureLayer implements Layer {
|
||||
if (!this.isUnitTypeSupported(unitType)) return;
|
||||
|
||||
const config = this.unitConfigs[unitType];
|
||||
let icon: ImageData;
|
||||
let icon: ImageData | undefined;
|
||||
|
||||
if (unitType == UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
icon = this.unitIcons.get("reloadingSam");
|
||||
|
||||
@@ -29,7 +29,9 @@ export class TerrainLayer implements Layer {
|
||||
|
||||
redraw(): void {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
|
||||
this.imageData = this.context.getImageData(
|
||||
0,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
manhattanDistFN,
|
||||
TileRef,
|
||||
} from "../../../core/game/GameMap";
|
||||
import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { AlternateViewEvent, DragEvent } from "../../InputHandler";
|
||||
@@ -26,7 +26,7 @@ export class TerritoryLayer implements Layer {
|
||||
return a.lastUpdate - b.lastUpdate;
|
||||
});
|
||||
private random = new PseudoRandom(123);
|
||||
private theme: Theme = null;
|
||||
private theme: Theme;
|
||||
|
||||
// Used for spawn highlighting
|
||||
private highlightCanvas: HTMLCanvasElement;
|
||||
@@ -62,8 +62,9 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
tick() {
|
||||
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Unit].forEach((u) => {
|
||||
const update = u as UnitUpdate;
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
||||
unitUpdates.forEach((update) => {
|
||||
if (update.unitType == UnitType.DefensePost && update.isActive) {
|
||||
const tile = update.pos;
|
||||
this.game
|
||||
@@ -148,7 +149,9 @@ export class TerritoryLayer implements Layer {
|
||||
redraw() {
|
||||
console.log("redrew territory layer");
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
|
||||
this.imageData = this.context.getImageData(
|
||||
0,
|
||||
@@ -163,9 +166,11 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
// Add a second canvas for highlights
|
||||
this.highlightCanvas = document.createElement("canvas");
|
||||
this.highlightContext = this.highlightCanvas.getContext("2d", {
|
||||
const highlightContext = this.highlightCanvas.getContext("2d", {
|
||||
alpha: true,
|
||||
});
|
||||
if (highlightContext === null) throw new Error("2d context not supported");
|
||||
this.highlightContext = highlightContext;
|
||||
this.highlightCanvas.width = this.game.width();
|
||||
this.highlightCanvas.height = this.game.height();
|
||||
|
||||
|
||||
@@ -22,18 +22,21 @@ export class TopBar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.game?.myPlayer() !== null) {
|
||||
const popIncreaseRate =
|
||||
this.game.myPlayer().population() - this._population;
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
this.updatePopulationIncrease();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
|
||||
@@ -14,9 +14,9 @@ import { Layer } from "./Layer";
|
||||
*/
|
||||
export class UILayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private context: CanvasRenderingContext2D | null;
|
||||
|
||||
private theme: Theme = null;
|
||||
private theme: Theme | null = null;
|
||||
private selectionAnimTime = 0;
|
||||
|
||||
// Keep track of currently selected unit
|
||||
@@ -136,6 +136,7 @@ export class UILayer implements Layer {
|
||||
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
|
||||
|
||||
// Get the unit's owner color for the box
|
||||
if (this.theme === null) throw new Error("missing theme");
|
||||
const ownerColor = this.theme.territoryColor(unit.owner());
|
||||
|
||||
// Create a brighter version of the owner color for the selection
|
||||
@@ -196,12 +197,14 @@ export class UILayer implements Layer {
|
||||
}
|
||||
|
||||
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
||||
if (this.context === null) throw new Error("null context");
|
||||
this.clearCell(x, y);
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
clearCell(x: number, y: number) {
|
||||
if (this.context === null) throw new Error("null context");
|
||||
this.context.clearRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class UnitLayer implements Layer {
|
||||
|
||||
private boatToTrail = new Map<UnitView, TileRef[]>();
|
||||
|
||||
private theme: Theme = null;
|
||||
private theme: Theme;
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
@@ -67,9 +67,13 @@ export class UnitLayer implements Layer {
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.game.playerByClientID(this.clientID);
|
||||
}
|
||||
this.game.updatesSinceLastTick()?.[GameUpdateType.Unit]?.forEach((unit) => {
|
||||
this.onUnitEvent(this.game.unit(unit.id));
|
||||
});
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates?.[GameUpdateType.Unit] ?? [];
|
||||
for (const u of unitUpdates) {
|
||||
const unit = this.game.unit(u.id);
|
||||
if (typeof unit === "undefined") continue;
|
||||
this.onUnitEvent(unit);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -185,20 +189,29 @@ export class UnitLayer implements Layer {
|
||||
|
||||
redraw() {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
this.transportShipTrailCanvas = document.createElement("canvas");
|
||||
this.transportShipTrailContext =
|
||||
const transportShipTrailContext =
|
||||
this.transportShipTrailCanvas.getContext("2d");
|
||||
if (transportShipTrailContext === null) {
|
||||
throw new Error("2d context not supported");
|
||||
}
|
||||
this.transportShipTrailContext = transportShipTrailContext;
|
||||
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.transportShipTrailCanvas.width = this.game.width();
|
||||
this.transportShipTrailCanvas.height = this.game.height();
|
||||
this.game
|
||||
?.updatesSinceLastTick()
|
||||
?.[GameUpdateType.Unit]?.forEach((unit) => {
|
||||
this.onUnitEvent(this.game.unit(unit.id));
|
||||
});
|
||||
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates?.[GameUpdateType.Unit] ?? [];
|
||||
for (const u of unitUpdates) {
|
||||
const unit = this.game.unit(u.id);
|
||||
if (typeof unit === "undefined") continue;
|
||||
this.onUnitEvent(unit);
|
||||
}
|
||||
}
|
||||
|
||||
private relationship(unit: UnitView): Relationship {
|
||||
@@ -315,8 +328,8 @@ export class UnitLayer implements Layer {
|
||||
|
||||
// Clear current and previous positions
|
||||
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
|
||||
if (this.oldShellTile.has(unit)) {
|
||||
const oldTile = this.oldShellTile.get(unit);
|
||||
const oldTile = this.oldShellTile.get(unit);
|
||||
if (typeof oldTile !== "undefined") {
|
||||
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
|
||||
}
|
||||
|
||||
@@ -495,6 +508,7 @@ export class UnitLayer implements Layer {
|
||||
this.boatToTrail.set(unit, []);
|
||||
}
|
||||
const trail = this.boatToTrail.get(unit);
|
||||
if (typeof trail === "undefined") return;
|
||||
trail.push(unit.lastTile());
|
||||
|
||||
// Clear previous area
|
||||
|
||||
@@ -223,7 +223,9 @@ export class WinModal extends LitElement implements Layer {
|
||||
this.won = false;
|
||||
this.show();
|
||||
}
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
|
||||
winUpdates.forEach((wu) => {
|
||||
if (wu.winnerType === "team") {
|
||||
this.eventBus.emit(
|
||||
new SendWinnerEvent(wu.winner as Team, wu.allPlayersStats, "team"),
|
||||
@@ -240,9 +242,12 @@ export class WinModal extends LitElement implements Layer {
|
||||
const winner = this.game.playerBySmallID(
|
||||
wu.winner as number,
|
||||
) as PlayerView;
|
||||
this.eventBus.emit(
|
||||
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"),
|
||||
);
|
||||
const winnerClient = winner.clientID();
|
||||
if (winnerClient !== null) {
|
||||
this.eventBus.emit(
|
||||
new SendWinnerEvent(winnerClient, wu.allPlayersStats, "player"),
|
||||
);
|
||||
}
|
||||
if (winner == this.game.myPlayer()) {
|
||||
this._title = "You Won!";
|
||||
this.won = true;
|
||||
|
||||
@@ -116,8 +116,10 @@ export class GameRunner {
|
||||
errMsg: error.message,
|
||||
stack: error.stack,
|
||||
} as ErrorUpdate);
|
||||
return;
|
||||
} else {
|
||||
console.error("Game tick error:", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.game.inSpawnPhase() && this.game.ticks() % 2 == 0) {
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("boat"),
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
troops: z.number(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
+7
-8
@@ -108,7 +108,7 @@ function closestShoreTN(
|
||||
gm: GameMap,
|
||||
tile: TileRef,
|
||||
searchDist: number,
|
||||
): TileRef {
|
||||
): TileRef | null {
|
||||
const tn = Array.from(
|
||||
gm.bfs(
|
||||
tile,
|
||||
@@ -260,12 +260,17 @@ export function createGameRecord(
|
||||
const record: GameRecord = {
|
||||
id: id,
|
||||
gameStartInfo: gameStart,
|
||||
players,
|
||||
startTimestampMS: start,
|
||||
endTimestampMS: end,
|
||||
durationSeconds: Math.floor((end - start) / 1000),
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
num_turns: 0,
|
||||
turns: [],
|
||||
allPlayersStats,
|
||||
version: "v0.0.1",
|
||||
winner,
|
||||
winnerType,
|
||||
};
|
||||
|
||||
for (const turn of turns) {
|
||||
@@ -282,18 +287,12 @@ export function createGameRecord(
|
||||
}
|
||||
}
|
||||
}
|
||||
record.players = players;
|
||||
record.durationSeconds = Math.floor(
|
||||
(record.endTimestampMS - record.startTimestampMS) / 1000,
|
||||
);
|
||||
record.num_turns = turns.length;
|
||||
record.winner = winner;
|
||||
record.winnerType = winnerType;
|
||||
return record;
|
||||
}
|
||||
|
||||
export function decompressGameRecord(gameRecord: GameRecord) {
|
||||
const turns = [];
|
||||
const turns: Turn[] = [];
|
||||
let lastTurnNum = -1;
|
||||
for (const turn of gameRecord.turns) {
|
||||
while (lastTurnNum < turn.turnNumber - 1) {
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface Config {
|
||||
infiniteTroops(): boolean;
|
||||
instantBuild(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
userSettings(): UserSettings;
|
||||
userSettings(): UserSettings | null;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
populationIncreaseRate(player: Player | PlayerView): number;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DevConfig, DevServerConfig } from "./DevConfig";
|
||||
import { preprodConfig } from "./PreprodConfig";
|
||||
import { prodConfig } from "./ProdConfig";
|
||||
|
||||
export let cachedSC: ServerConfig = null;
|
||||
export let cachedSC: ServerConfig | null = null;
|
||||
|
||||
export async function getConfig(
|
||||
gameConfig: GameConfig,
|
||||
@@ -44,7 +44,7 @@ export async function getServerConfigFromClient(): Promise<ServerConfig> {
|
||||
return cachedSC;
|
||||
}
|
||||
export function getServerConfigFromServer(): ServerConfig {
|
||||
const gameEnv = process.env.GAME_ENV;
|
||||
const gameEnv = process.env.GAME_ENV ?? "dev";
|
||||
return getServerConfig(gameEnv);
|
||||
}
|
||||
export function getServerConfig(gameEnv: string) {
|
||||
|
||||
@@ -28,26 +28,26 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
if (this.env() == GameEnv.Dev) {
|
||||
return "dev";
|
||||
}
|
||||
return process.env.REGION;
|
||||
return process.env.REGION ?? "undefined";
|
||||
}
|
||||
gitCommit(): string {
|
||||
return process.env.GIT_COMMIT;
|
||||
return process.env.GIT_COMMIT ?? "undefined";
|
||||
}
|
||||
r2Endpoint(): string {
|
||||
return process.env.R2_ENDPOINT;
|
||||
return process.env.R2_ENDPOINT ?? "undefined";
|
||||
}
|
||||
r2AccessKey(): string {
|
||||
return process.env.R2_ACCESS_KEY;
|
||||
return process.env.R2_ACCESS_KEY ?? "undefined";
|
||||
}
|
||||
r2SecretKey(): string {
|
||||
return process.env.R2_SECRET_KEY;
|
||||
return process.env.R2_SECRET_KEY ?? "undefined";
|
||||
}
|
||||
abstract r2Bucket(): string;
|
||||
adminHeader(): string {
|
||||
return "x-admin-key";
|
||||
}
|
||||
adminToken(): string {
|
||||
return process.env.ADMIN_TOKEN;
|
||||
return process.env.ADMIN_TOKEN ?? "undefined";
|
||||
}
|
||||
abstract numWorkers(): number;
|
||||
abstract env(): GameEnv;
|
||||
@@ -128,7 +128,7 @@ export class DefaultConfig implements Config {
|
||||
constructor(
|
||||
private _serverConfig: ServerConfig,
|
||||
private _gameConfig: GameConfig,
|
||||
private _userSettings: UserSettings,
|
||||
private _userSettings: UserSettings | null,
|
||||
) {}
|
||||
|
||||
samHittingChance(): number {
|
||||
@@ -394,7 +394,7 @@ export class DefaultConfig implements Config {
|
||||
return this.bots();
|
||||
}
|
||||
theme(): Theme {
|
||||
return this.userSettings().darkMode() ? pastelThemeDark : pastelTheme;
|
||||
return this.userSettings()?.darkMode() ? pastelThemeDark : pastelTheme;
|
||||
}
|
||||
|
||||
attackLogic(
|
||||
@@ -647,6 +647,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.HydrogenBomb:
|
||||
return { inner: 80, outer: 100 };
|
||||
}
|
||||
throw new Error(`Unknown nuke type: ${unitType}`);
|
||||
}
|
||||
|
||||
defaultNukeSpeed(): number {
|
||||
|
||||
@@ -36,7 +36,7 @@ export class DevServerConfig extends DefaultServerConfig {
|
||||
}
|
||||
|
||||
export class DevConfig extends DefaultConfig {
|
||||
constructor(sc: ServerConfig, gc: GameConfig, us: UserSettings) {
|
||||
constructor(sc: ServerConfig, gc: GameConfig, us: UserSettings | null) {
|
||||
super(sc, gc, us);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class AttackExecution implements Execution {
|
||||
|
||||
private border = new Set<TileRef>();
|
||||
|
||||
private attack: Attack = null;
|
||||
private attack: Attack | null = null;
|
||||
|
||||
constructor(
|
||||
private startTroops: number | null = null,
|
||||
@@ -49,7 +49,7 @@ export class AttackExecution implements Execution {
|
||||
private removeTroops: boolean = true,
|
||||
) {}
|
||||
|
||||
public targetID(): PlayerID {
|
||||
public targetID(): PlayerID | null {
|
||||
return this._targetID;
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
|
||||
private retreat(malusPercent = 0) {
|
||||
if (this.attack === null) {
|
||||
throw new Error("Attack not initialized");
|
||||
}
|
||||
|
||||
const deaths = this.attack.troops() * (malusPercent / 100);
|
||||
if (deaths) {
|
||||
this.mg.displayMessage(
|
||||
@@ -189,6 +193,10 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (this.attack === null) {
|
||||
throw new Error("Attack not initialized");
|
||||
}
|
||||
|
||||
if (this.attack.retreated()) {
|
||||
if (this.attack.target().isPlayer()) {
|
||||
this.retreat(malusForRetreat);
|
||||
|
||||
@@ -33,6 +33,7 @@ export class DonateGoldExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.gold === null) throw new Error("not initialized");
|
||||
if (this.sender.canDonate(this.recipient)) {
|
||||
this.sender.donateGold(this.recipient, this.gold);
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
@@ -44,10 +45,6 @@ export class DonateGoldExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export class DonateTroopsExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.troops === null) throw new Error("not initialized");
|
||||
if (this.sender.canDonate(this.recipient)) {
|
||||
this.sender.donateTroops(this.recipient, this.troops);
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
@@ -44,10 +45,6 @@ export class DonateTroopsExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -29,10 +29,6 @@ export class EmbargoExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,6 @@ export class EmojiExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { TransportShipExecution } from "./TransportShipExecution";
|
||||
|
||||
export class Executor {
|
||||
// private random = new PseudoRandom(999)
|
||||
private random: PseudoRandom = null;
|
||||
private random: PseudoRandom;
|
||||
|
||||
constructor(
|
||||
private mg: Game,
|
||||
@@ -113,7 +113,7 @@ export class Executor {
|
||||
}
|
||||
|
||||
fakeHumanExecutions(): Execution[] {
|
||||
const execs = [];
|
||||
const execs: Execution[] = [];
|
||||
for (const nation of this.mg.nations()) {
|
||||
execs.push(
|
||||
new FakeHumanExecution(
|
||||
|
||||
@@ -34,7 +34,7 @@ export class FakeHumanExecution implements Execution {
|
||||
private active = true;
|
||||
private random: PseudoRandom;
|
||||
private mg: Game;
|
||||
private player: Player = null;
|
||||
private player: Player | null = null;
|
||||
|
||||
private enemy: Player | null = null;
|
||||
|
||||
@@ -59,41 +59,45 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private updateRelationsFromEmbargos() {
|
||||
const others = this.mg.players().filter((p) => p.id() != this.player.id());
|
||||
const player = this.player;
|
||||
if (player === null) return;
|
||||
const others = this.mg.players().filter((p) => p.id() != player.id());
|
||||
|
||||
others.forEach((other: Player) => {
|
||||
const embargoMalus = -20;
|
||||
if (
|
||||
other.hasEmbargoAgainst(this.player) &&
|
||||
other.hasEmbargoAgainst(player) &&
|
||||
!this.embargoMalusApplied.has(other.id())
|
||||
) {
|
||||
this.player.updateRelation(other, embargoMalus);
|
||||
player.updateRelation(other, embargoMalus);
|
||||
this.embargoMalusApplied.add(other.id());
|
||||
} else if (
|
||||
!other.hasEmbargoAgainst(this.player) &&
|
||||
!other.hasEmbargoAgainst(player) &&
|
||||
this.embargoMalusApplied.has(other.id())
|
||||
) {
|
||||
this.player.updateRelation(other, -embargoMalus);
|
||||
player.updateRelation(other, -embargoMalus);
|
||||
this.embargoMalusApplied.delete(other.id());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleEmbargoesToHostileNations() {
|
||||
const others = this.mg.players().filter((p) => p.id() != this.player.id());
|
||||
const player = this.player;
|
||||
if (player === null) return;
|
||||
const others = this.mg.players().filter((p) => p.id() != player.id());
|
||||
|
||||
others.forEach((other: Player) => {
|
||||
/* When player is hostile starts embargo. Do not stop until neutral again */
|
||||
if (
|
||||
this.player.relation(other) <= Relation.Hostile &&
|
||||
!this.player.hasEmbargoAgainst(other)
|
||||
player.relation(other) <= Relation.Hostile &&
|
||||
!player.hasEmbargoAgainst(other)
|
||||
) {
|
||||
this.player.addEmbargo(other.id());
|
||||
player.addEmbargo(other.id());
|
||||
} else if (
|
||||
this.player.relation(other) >= Relation.Neutral &&
|
||||
this.player.hasEmbargoAgainst(other)
|
||||
player.relation(other) >= Relation.Neutral &&
|
||||
player.hasEmbargoAgainst(other)
|
||||
) {
|
||||
this.player.stopEmbargo(other.id());
|
||||
player.stopEmbargo(other.id());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -111,7 +115,8 @@ export class FakeHumanExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
if (this.player == null) {
|
||||
this.player = this.mg.players().find((p) => p.id() == this.playerInfo.id);
|
||||
this.player =
|
||||
this.mg.players().find((p) => p.id() == this.playerInfo.id) ?? null;
|
||||
if (this.player == null) {
|
||||
return;
|
||||
}
|
||||
@@ -146,7 +151,8 @@ export class FakeHumanExecution implements Execution {
|
||||
const enemyborder = Array.from(this.player.borderTiles())
|
||||
.flatMap((t) => this.mg.neighbors(t))
|
||||
.filter(
|
||||
(t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.player.smallID(),
|
||||
(t) =>
|
||||
this.mg.isLand(t) && this.mg.ownerID(t) != this.player?.smallID(),
|
||||
);
|
||||
|
||||
if (enemyborder.length == 0) {
|
||||
@@ -191,6 +197,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
@@ -227,30 +234,33 @@ export class FakeHumanExecution implements Execution {
|
||||
this.enemy = null;
|
||||
}
|
||||
|
||||
const player = this.player;
|
||||
if (player === null) return;
|
||||
|
||||
const target =
|
||||
this.player
|
||||
player
|
||||
.allies()
|
||||
.filter((ally) => this.player.relation(ally) == Relation.Friendly)
|
||||
.filter((ally) => player.relation(ally) == Relation.Friendly)
|
||||
.filter((ally) => ally.targets().length > 0)
|
||||
.map((ally) => ({ ally: ally, t: ally.targets()[0] }))[0] ?? null;
|
||||
|
||||
if (
|
||||
target != null &&
|
||||
target.t != this.player &&
|
||||
!this.player.isAlliedWith(target.t)
|
||||
target.t != player &&
|
||||
!player.isAlliedWith(target.t)
|
||||
) {
|
||||
this.player.updateRelation(target.ally, -20);
|
||||
player.updateRelation(target.ally, -20);
|
||||
this.enemy = target.t;
|
||||
this.lastEnemyUpdateTick = this.mg.ticks();
|
||||
if (target.ally.type() == PlayerType.Human) {
|
||||
this.mg.addExecution(
|
||||
new EmojiExecution(this.player.id(), target.ally.id(), "👍"),
|
||||
new EmojiExecution(player.id(), target.ally.id(), "👍"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.enemy == null) {
|
||||
const mostHated = this.player.allRelationsSorted()[0] ?? null;
|
||||
const mostHated = player.allRelationsSorted()[0] ?? null;
|
||||
if (mostHated != null && mostHated.relation == Relation.Hostile) {
|
||||
this.enemy = mostHated.player;
|
||||
this.lastEnemyUpdateTick = this.mg.ticks();
|
||||
@@ -258,13 +268,13 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
if (this.enemy) {
|
||||
if (this.player.isFriendly(this.enemy)) {
|
||||
if (player.isFriendly(this.enemy)) {
|
||||
this.enemy = null;
|
||||
return;
|
||||
}
|
||||
this.maybeSendEmoji();
|
||||
this.maybeSendNuke(this.enemy);
|
||||
if (this.player.sharesBorderWith(this.enemy)) {
|
||||
if (player.sharesBorderWith(this.enemy)) {
|
||||
this.sendAttack(this.enemy);
|
||||
} else {
|
||||
this.maybeSendBoatAttack(this.enemy);
|
||||
@@ -274,6 +284,8 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSendEmoji() {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.enemy === null) return;
|
||||
if (this.enemy.type() != PlayerType.Human) return;
|
||||
const lastSent = this.lastEmojiSent.get(this.enemy) ?? -300;
|
||||
if (this.mg.ticks() - lastSent <= 300) return;
|
||||
@@ -288,6 +300,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSendNuke(other: Player) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (
|
||||
this.player.units(UnitType.MissileSilo).length == 0 ||
|
||||
this.player.gold() <
|
||||
@@ -317,6 +330,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSendBoatAttack(other: Player) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.player.isOnSameTeam(other)) return;
|
||||
const closest = closestTwoTiles(
|
||||
this.mg,
|
||||
@@ -339,15 +353,17 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private handleUnits() {
|
||||
const ports = this.player.units(UnitType.Port);
|
||||
if (ports.length == 0 && this.player.gold() > this.cost(UnitType.Port)) {
|
||||
const oceanTiles = Array.from(this.player.borderTiles()).filter((t) =>
|
||||
const player = this.player;
|
||||
if (player === null) return;
|
||||
const ports = player.units(UnitType.Port);
|
||||
if (ports.length == 0 && player.gold() > this.cost(UnitType.Port)) {
|
||||
const oceanTiles = Array.from(player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t),
|
||||
);
|
||||
if (oceanTiles.length > 0) {
|
||||
const buildTile = this.random.randElement(oceanTiles);
|
||||
this.mg.addExecution(
|
||||
new ConstructionExecution(this.player.id(), buildTile, UnitType.Port),
|
||||
new ConstructionExecution(player.id(), buildTile, UnitType.Port),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -355,7 +371,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.City,
|
||||
2,
|
||||
(t) => new ConstructionExecution(this.player.id(), t, UnitType.City),
|
||||
(t) => new ConstructionExecution(player.id(), t, UnitType.City),
|
||||
);
|
||||
if (this.maybeSpawnWarship()) {
|
||||
return;
|
||||
@@ -364,8 +380,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.MissileSilo,
|
||||
1,
|
||||
(t) =>
|
||||
new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo),
|
||||
(t) => new ConstructionExecution(player.id(), t, UnitType.MissileSilo),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -375,6 +390,7 @@ export class FakeHumanExecution implements Execution {
|
||||
maxNum: number,
|
||||
build: (tile: TileRef) => Execution,
|
||||
) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const units = this.player.units(type);
|
||||
if (units.length >= maxNum) {
|
||||
return;
|
||||
@@ -396,6 +412,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSpawnWarship(): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (!this.random.chance(50)) {
|
||||
return false;
|
||||
}
|
||||
@@ -470,10 +487,12 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private cost(type: UnitType): number {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
return this.mg.unitInfo(type).cost(this.player);
|
||||
}
|
||||
|
||||
handleAllianceRequests() {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
for (const req of this.player.incomingAllianceRequests()) {
|
||||
if (req.requestor().isTraitor()) {
|
||||
this.replyToAllianceRequest(req, false);
|
||||
@@ -494,6 +513,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private replyToAllianceRequest(req: AllianceRequest, accept: boolean): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
this.mg.addExecution(
|
||||
new AllianceRequestReplyExecution(
|
||||
req.requestor().id(),
|
||||
@@ -504,6 +524,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
sendBoatRandomly() {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t),
|
||||
);
|
||||
@@ -532,9 +553,9 @@ export class FakeHumanExecution implements Execution {
|
||||
randomLand(): TileRef | null {
|
||||
const delta = 25;
|
||||
let tries = 0;
|
||||
while (tries < 50) {
|
||||
tries++;
|
||||
const cell = this.playerInfo.nation.cell;
|
||||
const cell = this.playerInfo.nation?.cell;
|
||||
if (typeof cell === "undefined") return null;
|
||||
while (tries++ < 50) {
|
||||
const x = this.random.nextInt(cell.x - delta, cell.x + delta);
|
||||
const y = this.random.nextInt(cell.y - delta, cell.y + delta);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
@@ -555,6 +576,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
sendAttack(toAttack: Player | TerraNullius) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (toAttack.isPlayer() && this.player.isOnSameTeam(toAttack)) return;
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(
|
||||
@@ -566,6 +588,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private randOceanShoreTile(tile: TileRef, dist: number): TileRef | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const x = this.mg.x(tile);
|
||||
const y = this.mg.y(tile);
|
||||
for (let i = 0; i < 500; i++) {
|
||||
@@ -589,10 +612,6 @@ export class FakeHumanExecution implements Execution {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Execution, Game, Player } from "../game/Game";
|
||||
import { Execution, Game } from "../game/Game";
|
||||
|
||||
export class NoOpExecution implements Execution {
|
||||
isActive(): boolean {
|
||||
@@ -9,7 +9,4 @@ export class NoOpExecution implements Execution {
|
||||
}
|
||||
init(mg: Game, ticks: number): void {}
|
||||
tick(ticks: number): void {}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export class NukeExecution implements Execution {
|
||||
private type: NukeType,
|
||||
private senderID: PlayerID,
|
||||
private dst: TileRef,
|
||||
private src?: TileRef,
|
||||
private src?: TileRef | null,
|
||||
private speed: number = -1,
|
||||
private waitTicks = 0,
|
||||
) {}
|
||||
|
||||
@@ -115,6 +115,7 @@ export class PlayerExecution implements Execution {
|
||||
clusters.sort((a, b) => b.size - a.size);
|
||||
|
||||
const main = clusters.shift();
|
||||
if (typeof main === "undefined") throw new Error("No clusters");
|
||||
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
|
||||
const surroundedBy = this.surroundedBySamePlayer(main);
|
||||
if (surroundedBy && !this.player.isFriendly(surroundedBy)) {
|
||||
@@ -276,6 +277,7 @@ export class PlayerExecution implements Execution {
|
||||
seen.add(tile);
|
||||
while (queue.length > 0) {
|
||||
const curr = queue.shift();
|
||||
if (typeof curr === "undefined") throw new Error("curr is undefined");
|
||||
cluster.add(curr);
|
||||
|
||||
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
|
||||
|
||||
@@ -18,7 +18,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
private sam: Unit;
|
||||
private active: boolean = true;
|
||||
|
||||
private target: Unit = null;
|
||||
private target: Unit | null = null;
|
||||
|
||||
private searchRangeRadius = 75;
|
||||
|
||||
|
||||
@@ -31,10 +31,6 @@ export class SetTargetTroopRatioExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ShellExecution implements Execution {
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.active = false;
|
||||
this.target.modifyHealth(-this.shell.info().damage);
|
||||
this.target.modifyHealth(-(this.shell.info().damage ?? 0));
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
|
||||
@@ -25,7 +25,7 @@ export class SpawnExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
let player: Player = null;
|
||||
let player: Player | null = null;
|
||||
if (this.mg.hasPlayer(this.playerInfo.id)) {
|
||||
player = this.mg.player(this.playerInfo.id);
|
||||
} else {
|
||||
@@ -46,9 +46,6 @@ export class SpawnExecution implements Execution {
|
||||
player.setHasSpawned(true);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,6 @@ export class TargetPlayerExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class TransportShipExecution implements Execution {
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private ref: TileRef,
|
||||
private troops: number | null,
|
||||
private troops: number,
|
||||
) {}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
@@ -129,6 +129,10 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (this.dst == null) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function closestTwoTiles(
|
||||
gm: GameMap,
|
||||
x: Iterable<TileRef>,
|
||||
y: Iterable<TileRef>,
|
||||
): { x: TileRef; y: TileRef } {
|
||||
): { x: TileRef; y: TileRef } | null {
|
||||
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
|
||||
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
private _owner: Player;
|
||||
private active = true;
|
||||
private warship: Unit = null;
|
||||
private mg: Game = null;
|
||||
private warship: Unit | null = null;
|
||||
private mg: Game | null = null;
|
||||
|
||||
private target: Unit = null;
|
||||
private target: Unit | null = null;
|
||||
private pathfinder: PathFinder;
|
||||
|
||||
private patrolTile: TileRef;
|
||||
@@ -53,7 +53,8 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
// Only for warships with "moveTarget" set
|
||||
goToMoveTarget(target: TileRef): boolean {
|
||||
goToMoveTarget(target: TileRef) {
|
||||
if (this.warship === null) throw new Error("Warship not initialized");
|
||||
// Patrol unless we are hunting down a tradeship
|
||||
const result = this.pathfinder.nextTile(this.warship.tile(), target);
|
||||
switch (result.type) {
|
||||
@@ -72,6 +73,9 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
if (this.mg === null) throw new Error("Game not initialized");
|
||||
if (this.warship === null) throw new Error("Warship not initialized");
|
||||
if (this.target === null) throw new Error("Target not initialized");
|
||||
if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
this.mg.addExecution(
|
||||
@@ -92,6 +96,7 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private patrol() {
|
||||
if (this.warship === null) throw new Error("Warship not initialized");
|
||||
this.warship.setWarshipTarget(this.target);
|
||||
if (this.target == null || this.target.type() != UnitType.TradeShip) {
|
||||
// Patrol unless we are hunting down a tradeship
|
||||
@@ -134,17 +139,20 @@ export class WarshipExecution implements Execution {
|
||||
this.target = null;
|
||||
}
|
||||
const hasPort = this._owner.units(UnitType.Port).length > 0;
|
||||
if (this.mg === null) throw new Error("Game not initialized");
|
||||
const warship = this.warship;
|
||||
if (warship === null) throw new Error("Warship not initialized");
|
||||
const ships = this.mg
|
||||
.nearbyUnits(
|
||||
this.warship.tile(),
|
||||
warship.tile(),
|
||||
130, // Search range
|
||||
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
||||
)
|
||||
.filter(
|
||||
({ unit }) =>
|
||||
unit.owner() !== this.warship.owner() &&
|
||||
unit !== this.warship &&
|
||||
!unit.owner().isFriendly(this.warship.owner()) &&
|
||||
unit.owner() !== warship.owner() &&
|
||||
unit !== warship &&
|
||||
!unit.owner().isFriendly(warship.owner()) &&
|
||||
!this.alreadySentShell.has(unit) &&
|
||||
(unit.type() !== UnitType.TradeShip || hasPort) &&
|
||||
(unit.type() !== UnitType.TradeShip ||
|
||||
@@ -184,8 +192,9 @@ export class WarshipExecution implements Execution {
|
||||
return distA - distB;
|
||||
})[0]?.unit ?? null;
|
||||
|
||||
if (this.warship.moveTarget()) {
|
||||
this.goToMoveTarget(this.warship.moveTarget());
|
||||
const moveTarget = this.warship.moveTarget();
|
||||
if (moveTarget) {
|
||||
this.goToMoveTarget(moveTarget);
|
||||
// If we have a "move target" then we cannot target trade ships as it
|
||||
// requires moving.
|
||||
if (this.target && this.target.type() == UnitType.TradeShip) {
|
||||
@@ -250,6 +259,7 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
if (this.mg === null) throw new Error("Game not initialized");
|
||||
while (true) {
|
||||
const x =
|
||||
this.mg.x(this.patrolCenterTile) +
|
||||
|
||||
@@ -50,9 +50,12 @@ export class WinCheckExecution implements Execution {
|
||||
checkWinnerTeam(): void {
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of this.mg.players()) {
|
||||
const team = player.team();
|
||||
// Sanity check, team should not be null here
|
||||
if (team === null) continue;
|
||||
teamToTiles.set(
|
||||
player.team(),
|
||||
(teamToTiles.get(player.team()) ?? 0) + player.numTilesOwned(),
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
const sorted = Array.from(teamToTiles.entries()).sort(
|
||||
@@ -72,9 +75,6 @@ export class WinCheckExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
|
||||
|
||||
export class AllianceRequestExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game = null;
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
|
||||
@@ -28,7 +27,6 @@ export class AllianceRequestExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
@@ -44,10 +42,6 @@ export class AllianceRequestExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
|
||||
|
||||
export class AllianceRequestReplyExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game = null;
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
|
||||
@@ -28,7 +27,6 @@ export class AllianceRequestReplyExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
@@ -55,10 +53,6 @@ export class AllianceRequestReplyExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -48,10 +48,6 @@ export class BreakAllianceExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
+10
-10
@@ -212,7 +212,7 @@ export class PlayerInfo {
|
||||
public readonly clan: string | null;
|
||||
|
||||
constructor(
|
||||
public readonly flag: string,
|
||||
public readonly flag: string | undefined,
|
||||
public readonly name: string,
|
||||
public readonly playerType: PlayerType,
|
||||
// null if bot.
|
||||
@@ -259,17 +259,17 @@ export interface Unit {
|
||||
health(): number;
|
||||
modifyHealth(delta: number): void;
|
||||
|
||||
setWarshipTarget(target: Unit): void; // warship only
|
||||
warshipTarget(): Unit;
|
||||
setWarshipTarget(target: Unit | null): void; // warship only
|
||||
warshipTarget(): Unit | null;
|
||||
|
||||
setCooldown(triggerCooldown: boolean): void;
|
||||
ticksLeftInCooldown(cooldownDuration: number): Tick;
|
||||
isCooldown(): boolean;
|
||||
setDstPort(dstPort: Unit): void;
|
||||
dstPort(): Unit; // Only for trade ships
|
||||
detonationDst(): TileRef; // Only for nukes
|
||||
dstPort(): Unit | null; // Only for trade ships
|
||||
detonationDst(): TileRef | null; // Only for nukes
|
||||
|
||||
setMoveTarget(cell: TileRef): void;
|
||||
setMoveTarget(cell: TileRef | null): void;
|
||||
moveTarget(): TileRef | null;
|
||||
|
||||
setTargetedBySAM(targeted: boolean): void;
|
||||
@@ -289,7 +289,7 @@ export interface Unit {
|
||||
|
||||
export interface TerraNullius {
|
||||
isPlayer(): false;
|
||||
id(): PlayerID; // always zero, maybe make it TerraNulliusID?
|
||||
id(): null;
|
||||
clientID(): ClientID;
|
||||
smallID(): number;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ export interface Player {
|
||||
info(): PlayerInfo;
|
||||
name(): string;
|
||||
displayName(): string;
|
||||
clientID(): ClientID;
|
||||
clientID(): ClientID | null;
|
||||
id(): PlayerID;
|
||||
type(): PlayerType;
|
||||
isPlayer(): this is Player;
|
||||
@@ -369,7 +369,7 @@ export interface Player {
|
||||
allianceWith(other: Player): MutableAlliance | null;
|
||||
canSendAllianceRequest(other: Player): boolean;
|
||||
breakAlliance(alliance: Alliance): void;
|
||||
createAllianceRequest(recipient: Player): AllianceRequest;
|
||||
createAllianceRequest(recipient: Player): AllianceRequest | null;
|
||||
|
||||
// Targeting
|
||||
canTarget(other: Player): boolean;
|
||||
@@ -399,7 +399,7 @@ export interface Player {
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef,
|
||||
sourceTile: TileRef | null,
|
||||
): Attack;
|
||||
outgoingAttacks(): Attack[];
|
||||
incomingAttacks(): Attack[];
|
||||
|
||||
+15
-11
@@ -57,7 +57,7 @@ export class GameImpl implements Game {
|
||||
private nations_: Nation[] = [];
|
||||
|
||||
_players: Map<PlayerID, PlayerImpl> = new Map<PlayerID, PlayerImpl>();
|
||||
_playersBySmallID = [];
|
||||
_playersBySmallID: Player[] = [];
|
||||
|
||||
private execs: Execution[] = [];
|
||||
private _width: number;
|
||||
@@ -170,10 +170,13 @@ export class GameImpl implements Game {
|
||||
return this.nations_;
|
||||
}
|
||||
|
||||
createAllianceRequest(requestor: Player, recipient: Player): AllianceRequest {
|
||||
createAllianceRequest(
|
||||
requestor: Player,
|
||||
recipient: Player,
|
||||
): AllianceRequest | null {
|
||||
if (requestor.isAlliedWith(recipient)) {
|
||||
consolex.log("cannot request alliance, already allied");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
recipient
|
||||
@@ -181,7 +184,7 @@ export class GameImpl implements Game {
|
||||
.find((ar) => ar.requestor() == requestor) != null
|
||||
) {
|
||||
consolex.log(`duplicate alliance request from ${requestor.name()}`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const correspondingReq = requestor
|
||||
.incomingAllianceRequests()
|
||||
@@ -189,7 +192,7 @@ export class GameImpl implements Game {
|
||||
if (correspondingReq != null) {
|
||||
consolex.log(`got corresponding alliance requests, accepting`);
|
||||
correspondingReq.accept();
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const ar = new AllianceRequestImpl(requestor, recipient, this._ticks, this);
|
||||
this.allianceRequests.push(ar);
|
||||
@@ -342,7 +345,7 @@ export class GameImpl implements Game {
|
||||
return this.player(id);
|
||||
}
|
||||
|
||||
addPlayer(playerInfo: PlayerInfo, team: Team = null): Player {
|
||||
addPlayer(playerInfo: PlayerInfo, team: Team | null = null): Player {
|
||||
const player = new PlayerImpl(
|
||||
this,
|
||||
this.nextPlayerID,
|
||||
@@ -367,11 +370,12 @@ export class GameImpl implements Game {
|
||||
return this.playerTeams[rand % this.playerTeams.length];
|
||||
}
|
||||
|
||||
player(id: PlayerID | null): Player {
|
||||
if (!this._players.has(id)) {
|
||||
player(id: PlayerID): Player {
|
||||
const player = this._players.get(id);
|
||||
if (typeof player === "undefined") {
|
||||
throw new Error(`Player with id ${id} not found`);
|
||||
}
|
||||
return this._players.get(id);
|
||||
return player;
|
||||
}
|
||||
|
||||
playerByClientID(id: ClientID): Player | null {
|
||||
@@ -495,7 +499,7 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
public breakAlliance(breaker: Player, alliance: Alliance) {
|
||||
let other: Player = null;
|
||||
let other: Player;
|
||||
if (alliance.requestor() == breaker) {
|
||||
other = alliance.recipient();
|
||||
} else {
|
||||
@@ -572,7 +576,7 @@ export class GameImpl implements Game {
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null,
|
||||
): void {
|
||||
let id = null;
|
||||
let id: number | null = null;
|
||||
if (playerID != null) {
|
||||
id = this.player(playerID).smallID();
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ export class GameMapImpl implements GameMap {
|
||||
q.push(tile);
|
||||
while (q.length > 0) {
|
||||
const curr = q.pop();
|
||||
if (typeof curr === "undefined") continue;
|
||||
for (const n of this.neighbors(curr)) {
|
||||
if (!seen.has(n) && filter(this, n)) {
|
||||
seen.add(n);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { TileRef, TileUpdate } from "./GameMap";
|
||||
|
||||
export interface GameUpdateViewData {
|
||||
tick: number;
|
||||
updates: GameUpdates;
|
||||
updates: GameUpdates | null;
|
||||
packedTileUpdates: BigUint64Array;
|
||||
playerNameViewData: Record<number, NameViewData>;
|
||||
}
|
||||
@@ -87,8 +87,8 @@ export interface AttackUpdate {
|
||||
export interface PlayerUpdate {
|
||||
type: GameUpdateType.Player;
|
||||
nameViewData?: NameViewData;
|
||||
clientID: ClientID;
|
||||
flag: string;
|
||||
clientID: ClientID | null;
|
||||
flag: string | undefined;
|
||||
name: string;
|
||||
displayName: string;
|
||||
id: PlayerID;
|
||||
|
||||
+26
-20
@@ -96,28 +96,29 @@ export class UnitView {
|
||||
constructionType(): UnitType | undefined {
|
||||
return this.data.constructionType;
|
||||
}
|
||||
dstPortId(): number {
|
||||
dstPortId(): number | undefined {
|
||||
if (this.type() != UnitType.TradeShip) {
|
||||
throw Error("Must be a trade ship");
|
||||
}
|
||||
return this.data.dstPortId;
|
||||
}
|
||||
detonationDst(): TileRef {
|
||||
detonationDst(): TileRef | undefined {
|
||||
if (!nukeTypes.includes(this.type())) {
|
||||
throw Error("Must be a nuke");
|
||||
}
|
||||
return this.data.detonationDst;
|
||||
}
|
||||
warshipTargetId(): number {
|
||||
warshipTargetId(): number | undefined {
|
||||
if (this.type() != UnitType.Warship) {
|
||||
throw Error("Must be a warship");
|
||||
}
|
||||
return this.data.warshipTargetId;
|
||||
}
|
||||
ticksLeftInCooldown(): Tick {
|
||||
ticksLeftInCooldown(): Tick | undefined {
|
||||
return this.data.ticksLeftInCooldown;
|
||||
}
|
||||
isCooldown(): boolean {
|
||||
if (typeof this.data.ticksLeftInCooldown === "undefined") return false;
|
||||
return this.data.ticksLeftInCooldown > 0;
|
||||
}
|
||||
}
|
||||
@@ -162,7 +163,7 @@ export class PlayerView {
|
||||
smallID(): number {
|
||||
return this.data.smallID;
|
||||
}
|
||||
flag(): string {
|
||||
flag(): string | undefined {
|
||||
return this.data.flag;
|
||||
}
|
||||
name(): string {
|
||||
@@ -171,13 +172,13 @@ export class PlayerView {
|
||||
displayName(): string {
|
||||
return this.data.displayName;
|
||||
}
|
||||
clientID(): ClientID {
|
||||
clientID(): ClientID | null {
|
||||
return this.data.clientID;
|
||||
}
|
||||
id(): PlayerID {
|
||||
return this.data.id;
|
||||
}
|
||||
team(): Team | null {
|
||||
team(): Team | undefined {
|
||||
return this.data.team;
|
||||
}
|
||||
type(): PlayerType {
|
||||
@@ -303,7 +304,7 @@ export class GameView implements GameMap {
|
||||
return this._map.isOnEdgeOfMap(ref);
|
||||
}
|
||||
|
||||
public updatesSinceLastTick(): GameUpdates {
|
||||
public updatesSinceLastTick(): GameUpdates | null {
|
||||
return this.lastUpdate.updates;
|
||||
}
|
||||
|
||||
@@ -318,11 +319,15 @@ export class GameView implements GameMap {
|
||||
this.updatedTiles.push(this.updateTile(tu));
|
||||
});
|
||||
|
||||
if (gu.updates === null) {
|
||||
throw new Error("lastUpdate.updates not initialized");
|
||||
}
|
||||
gu.updates[GameUpdateType.Player].forEach((pu) => {
|
||||
this.smallIDToID.set(pu.smallID, pu.id);
|
||||
if (this._players.has(pu.id)) {
|
||||
this._players.get(pu.id).data = pu;
|
||||
this._players.get(pu.id).nameData = gu.playerNameViewData[pu.id];
|
||||
const player = this._players.get(pu.id);
|
||||
if (typeof player !== "undefined") {
|
||||
player.data = pu;
|
||||
player.nameData = gu.playerNameViewData[pu.id];
|
||||
} else {
|
||||
this._players.set(
|
||||
pu.id,
|
||||
@@ -335,9 +340,8 @@ export class GameView implements GameMap {
|
||||
unit.lastPos = unit.lastPos.slice(-1);
|
||||
}
|
||||
gu.updates[GameUpdateType.Unit].forEach((update) => {
|
||||
let unit: UnitView = null;
|
||||
if (this._units.has(update.id)) {
|
||||
unit = this._units.get(update.id);
|
||||
let unit = this._units.get(update.id);
|
||||
if (typeof unit !== "undefined") {
|
||||
unit.update(update);
|
||||
} else {
|
||||
unit = new UnitView(this, update);
|
||||
@@ -382,10 +386,11 @@ export class GameView implements GameMap {
|
||||
}
|
||||
|
||||
player(id: PlayerID): PlayerView {
|
||||
if (this._players.has(id)) {
|
||||
return this._players.get(id);
|
||||
const player = this._players.get(id);
|
||||
if (typeof player === "undefined") {
|
||||
throw Error(`player id ${id} not found`);
|
||||
}
|
||||
throw Error(`player id ${id} not found`);
|
||||
return player;
|
||||
}
|
||||
|
||||
players(): PlayerView[] {
|
||||
@@ -396,10 +401,11 @@ export class GameView implements GameMap {
|
||||
if (id == 0) {
|
||||
return new TerraNulliusImpl();
|
||||
}
|
||||
if (!this.smallIDToID.has(id)) {
|
||||
const playerId = this.smallIDToID.get(id);
|
||||
if (typeof playerId === "undefined") {
|
||||
throw new Error(`small id ${id} not found`);
|
||||
}
|
||||
return this.player(this.smallIDToID.get(id));
|
||||
return this.player(playerId);
|
||||
}
|
||||
|
||||
playerByClientID(id: ClientID): PlayerView | null {
|
||||
@@ -439,7 +445,7 @@ export class GameView implements GameMap {
|
||||
(u) => u.isActive() && types.includes(u.type()),
|
||||
);
|
||||
}
|
||||
unit(id: number): UnitView {
|
||||
unit(id: number): UnitView | undefined {
|
||||
return this._units.get(id);
|
||||
}
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
|
||||
+13
-16
@@ -76,7 +76,7 @@ export class PlayerImpl implements Player {
|
||||
public _units: UnitImpl[] = [];
|
||||
public _tiles: Set<TileRef> = new Set();
|
||||
|
||||
private _flag: string;
|
||||
private _flag: string | undefined;
|
||||
private _name: string;
|
||||
private _displayName: string;
|
||||
|
||||
@@ -127,7 +127,7 @@ export class PlayerImpl implements Player {
|
||||
name: this.name(),
|
||||
displayName: this.displayName(),
|
||||
id: this.id(),
|
||||
team: this.team(),
|
||||
team: this.team() ?? undefined,
|
||||
smallID: this.smallID(),
|
||||
playerType: this.type(),
|
||||
isAlive: this.isAlive(),
|
||||
@@ -172,7 +172,7 @@ export class PlayerImpl implements Player {
|
||||
return this._smallID;
|
||||
}
|
||||
|
||||
flag(): string {
|
||||
flag(): string | undefined {
|
||||
return this._flag;
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export class PlayerImpl implements Player {
|
||||
return this._displayName;
|
||||
}
|
||||
|
||||
clientID(): ClientID {
|
||||
clientID(): ClientID | null {
|
||||
return this.playerInfo.clientID;
|
||||
}
|
||||
|
||||
@@ -331,8 +331,10 @@ export class PlayerImpl implements Player {
|
||||
if (other == this) {
|
||||
return null;
|
||||
}
|
||||
return this.alliances().find(
|
||||
(a) => a.recipient() == other || a.requestor() == other,
|
||||
return (
|
||||
this.alliances().find(
|
||||
(a) => a.recipient() == other || a.requestor() == other,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -375,7 +377,7 @@ export class PlayerImpl implements Player {
|
||||
return this.isTraitor_;
|
||||
}
|
||||
|
||||
createAllianceRequest(recipient: Player): AllianceRequest {
|
||||
createAllianceRequest(recipient: Player): AllianceRequest | null {
|
||||
if (this.isAlliedWith(recipient)) {
|
||||
throw new Error(`cannot create alliance request, already allies`);
|
||||
}
|
||||
@@ -386,10 +388,8 @@ export class PlayerImpl implements Player {
|
||||
if (other == this) {
|
||||
throw new Error(`cannot get relation with self: ${this}`);
|
||||
}
|
||||
if (this.relations.has(other)) {
|
||||
return this.relationFromValue(this.relations.get(other));
|
||||
}
|
||||
return Relation.Neutral;
|
||||
const relation = this.relations.get(other) ?? 0;
|
||||
return this.relationFromValue(relation);
|
||||
}
|
||||
|
||||
private relationFromValue(relationValue: number): Relation {
|
||||
@@ -418,10 +418,7 @@ export class PlayerImpl implements Player {
|
||||
if (other == this) {
|
||||
throw new Error(`cannot update relation with self: ${this}`);
|
||||
}
|
||||
let relation = 0;
|
||||
if (this.relations.has(other)) {
|
||||
relation = this.relations.get(other);
|
||||
}
|
||||
const relation = this.relations.get(other) ?? 0;
|
||||
const newRelation = within(relation + delta, -100, 100);
|
||||
this.relations.set(other, newRelation);
|
||||
}
|
||||
@@ -953,7 +950,7 @@ export class PlayerImpl implements Player {
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef,
|
||||
sourceTile: TileRef | null,
|
||||
): Attack {
|
||||
const attack = new AttackImpl(
|
||||
this._pseudo_random.nextID(),
|
||||
|
||||
@@ -2,7 +2,11 @@ import { AllPlayersStats, PlayerStats } from "../Schemas";
|
||||
import { NukeType, PlayerID } from "./Game";
|
||||
|
||||
export interface Stats {
|
||||
increaseNukeCount(sender: PlayerID, target: PlayerID, type: NukeType): void;
|
||||
increaseNukeCount(
|
||||
sender: PlayerID,
|
||||
target: PlayerID | null,
|
||||
type: NukeType,
|
||||
): void;
|
||||
getPlayerStats(player: PlayerID): PlayerStats;
|
||||
stats(): AllPlayersStats;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ClientID } from "../Schemas";
|
||||
import { PlayerID, TerraNullius } from "./Game";
|
||||
import { TerraNullius } from "./Game";
|
||||
|
||||
export class TerraNulliusImpl implements TerraNullius {
|
||||
constructor() {}
|
||||
@@ -10,7 +10,7 @@ export class TerraNulliusImpl implements TerraNullius {
|
||||
return "TERRA_NULLIUS_CLIENT_ID";
|
||||
}
|
||||
|
||||
id(): PlayerID {
|
||||
id() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,8 @@ export interface Nation {
|
||||
export async function loadTerrainMap(
|
||||
map: GameMapType,
|
||||
): Promise<TerrainMapData> {
|
||||
if (loadedMaps.has(map)) {
|
||||
return loadedMaps.get(map);
|
||||
}
|
||||
const cached = loadedMaps.get(map);
|
||||
if (typeof cached !== "undefined") return cached;
|
||||
const mapFiles = await terrainMapFileLoader.getMapData(map);
|
||||
|
||||
const gameMap = await genTerrainFromBin(mapFiles.mapBin);
|
||||
|
||||
+27
-23
@@ -16,19 +16,19 @@ import { PlayerImpl } from "./PlayerImpl";
|
||||
export class UnitImpl implements Unit {
|
||||
private _active = true;
|
||||
private _health: bigint;
|
||||
private _lastTile: TileRef = null;
|
||||
private _lastTile: TileRef | null = null;
|
||||
// Currently only warship use it
|
||||
private _target: Unit = null;
|
||||
private _moveTarget: TileRef = null;
|
||||
private _target: Unit | null = null;
|
||||
private _moveTarget: TileRef | null = null;
|
||||
private _targetedBySAM = false;
|
||||
|
||||
private _constructionType: UnitType = undefined;
|
||||
private _constructionType: UnitType | undefined = undefined;
|
||||
|
||||
private _cooldownTick: Tick | null = null;
|
||||
private _dstPort: Unit | null = null; // Only for trade ships
|
||||
private _detonationDst: TileRef | null = null; // Only for nukes
|
||||
private _warshipTarget: Unit | null = null;
|
||||
private _cooldownDuration: number | null = null;
|
||||
private _dstPort: Unit | undefined = undefined; // Only for trade ships
|
||||
private _detonationDst: TileRef | undefined = undefined; // Only for nukes
|
||||
private _warshipTarget: Unit | undefined = undefined;
|
||||
private _cooldownDuration: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private _type: UnitType,
|
||||
@@ -54,6 +54,11 @@ export class UnitImpl implements Unit {
|
||||
toUpdate(): UnitUpdate {
|
||||
const warshipTarget = this.warshipTarget();
|
||||
const dstPort = this.dstPort();
|
||||
if (this._lastTile === null) throw new Error("null _lastTile");
|
||||
const ticksLeftInCooldown =
|
||||
typeof this._cooldownDuration !== "undefined"
|
||||
? this.ticksLeftInCooldown(this._cooldownDuration)
|
||||
: undefined;
|
||||
return {
|
||||
type: GameUpdateType.Unit,
|
||||
unitType: this._type,
|
||||
@@ -65,10 +70,10 @@ export class UnitImpl implements Unit {
|
||||
lastPos: this._lastTile,
|
||||
health: this.hasHealth() ? Number(this._health) : undefined,
|
||||
constructionType: this._constructionType,
|
||||
dstPortId: dstPort ? dstPort.id() : null,
|
||||
warshipTargetId: warshipTarget ? warshipTarget.id() : null,
|
||||
detonationDst: this.detonationDst(),
|
||||
ticksLeftInCooldown: this.ticksLeftInCooldown(this._cooldownDuration),
|
||||
dstPortId: dstPort?.id() ?? undefined,
|
||||
warshipTargetId: warshipTarget?.id() ?? undefined,
|
||||
detonationDst: this.detonationDst() ?? undefined,
|
||||
ticksLeftInCooldown,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +82,7 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
|
||||
lastTile(): TileRef {
|
||||
if (this._lastTile === null) throw new Error("null _lastTile");
|
||||
return this._lastTile;
|
||||
}
|
||||
|
||||
@@ -157,7 +163,7 @@ export class UnitImpl implements Unit {
|
||||
if (this.type() != UnitType.Construction) {
|
||||
throw new Error(`Cannot get construction type on ${this.type()}`);
|
||||
}
|
||||
return this._constructionType;
|
||||
return this._constructionType ?? null;
|
||||
}
|
||||
|
||||
setConstructionType(type: UnitType): void {
|
||||
@@ -180,16 +186,16 @@ export class UnitImpl implements Unit {
|
||||
this._warshipTarget = target;
|
||||
}
|
||||
|
||||
warshipTarget(): Unit {
|
||||
return this._warshipTarget;
|
||||
warshipTarget(): Unit | null {
|
||||
return this._warshipTarget ?? null;
|
||||
}
|
||||
|
||||
detonationDst(): TileRef {
|
||||
return this._detonationDst;
|
||||
detonationDst(): TileRef | null {
|
||||
return this._detonationDst ?? null;
|
||||
}
|
||||
|
||||
dstPort(): Unit {
|
||||
return this._dstPort;
|
||||
dstPort(): Unit | null {
|
||||
return this._dstPort ?? null;
|
||||
}
|
||||
|
||||
// set the cooldown to the current tick or remove it
|
||||
@@ -204,10 +210,8 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
|
||||
ticksLeftInCooldown(cooldownDuration: number): Tick {
|
||||
return Math.max(
|
||||
0,
|
||||
cooldownDuration - (this.mg.ticks() - this._cooldownTick),
|
||||
);
|
||||
const cooldownTick = this._cooldownTick ?? 0;
|
||||
return Math.max(0, cooldownDuration - (this.mg.ticks() - cooldownTick));
|
||||
}
|
||||
|
||||
isCooldown(): boolean {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
export class UserSettings {
|
||||
get(key: string, defaultValue: boolean) {
|
||||
get(key: string, defaultValue: boolean): boolean {
|
||||
const value = localStorage.getItem(key);
|
||||
if (!value) return defaultValue;
|
||||
|
||||
if (value === "true") return true;
|
||||
|
||||
if (value === "false") return false;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
set(key: string, value: boolean) {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { AStar, PathFindResultType, TileResult } from "./AStar";
|
||||
import { MiniAStar } from "./MiniAStar";
|
||||
|
||||
export class PathFinder {
|
||||
private curr: TileRef = null;
|
||||
private dst: TileRef = null;
|
||||
private path: TileRef[];
|
||||
private curr: TileRef | null = null;
|
||||
private dst: TileRef | null = null;
|
||||
private path: TileRef[] | null = null;
|
||||
private aStar: AStar;
|
||||
private computeFinished = true;
|
||||
|
||||
@@ -63,7 +63,11 @@ export class PathFinder {
|
||||
this.computeFinished = false;
|
||||
return this.nextTile(curr, dst);
|
||||
} else {
|
||||
return { type: PathFindResultType.NextTile, tile: this.path.shift() };
|
||||
const tile = this.path?.shift();
|
||||
if (typeof tile === "undefined") {
|
||||
throw new Error("missing tile");
|
||||
}
|
||||
return { type: PathFindResultType.NextTile, tile };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +83,8 @@ export class PathFinder {
|
||||
return { type: PathFindResultType.Pending };
|
||||
case PathFindResultType.PathNotFound:
|
||||
return { type: PathFindResultType.PathNotFound };
|
||||
default:
|
||||
throw new Error("unexpected compute result");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,8 +119,9 @@ export class SerialAStar implements AStar {
|
||||
1.1 * Math.abs(this.gameMap.x(a) - this.gameMap.x(b)) +
|
||||
Math.abs(this.gameMap.y(a) - this.gameMap.y(b))
|
||||
);
|
||||
} catch {
|
||||
consolex.log("uh oh");
|
||||
} catch (e) {
|
||||
consolex.log("uh oh", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
|
||||
switch (message.type) {
|
||||
case "heartbeat":
|
||||
(await gameRunner).executeNextTick();
|
||||
(await gameRunner)?.executeNextTick();
|
||||
break;
|
||||
case "init":
|
||||
try {
|
||||
|
||||
@@ -419,33 +419,32 @@ function getThumbnailColor(t: Terrain): {
|
||||
return { r: 204, g: 203, b: 158, a: 255 };
|
||||
}
|
||||
let adjRGB: number;
|
||||
switch (true) {
|
||||
//plains
|
||||
case t.magnitude < 10:
|
||||
adjRGB = 220 - 2 * t.magnitude;
|
||||
return {
|
||||
r: 190,
|
||||
g: adjRGB,
|
||||
b: 138,
|
||||
a: 255,
|
||||
};
|
||||
//highlands
|
||||
case t.magnitude < 20:
|
||||
adjRGB = 2 * t.magnitude;
|
||||
return {
|
||||
r: 200 + adjRGB,
|
||||
g: 183 + adjRGB,
|
||||
b: 138 + adjRGB,
|
||||
a: 255,
|
||||
};
|
||||
//mountains
|
||||
case t.magnitude >= 20:
|
||||
adjRGB = Math.floor(230 + t.magnitude / 2);
|
||||
return {
|
||||
r: adjRGB,
|
||||
g: adjRGB,
|
||||
b: adjRGB,
|
||||
a: 255,
|
||||
};
|
||||
if (t.magnitude < 10) {
|
||||
// Plains
|
||||
adjRGB = 220 - 2 * t.magnitude;
|
||||
return {
|
||||
r: 190,
|
||||
g: adjRGB,
|
||||
b: 138,
|
||||
a: 255,
|
||||
};
|
||||
} else if (t.magnitude < 20) {
|
||||
// Highlands
|
||||
adjRGB = 2 * t.magnitude;
|
||||
return {
|
||||
r: 200 + adjRGB,
|
||||
g: 183 + adjRGB,
|
||||
b: 138 + adjRGB,
|
||||
a: 255,
|
||||
};
|
||||
} else {
|
||||
// Mountains
|
||||
adjRGB = Math.floor(230 + t.magnitude / 2);
|
||||
return {
|
||||
r: adjRGB,
|
||||
g: adjRGB,
|
||||
b: adjRGB,
|
||||
a: 255,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function readGameRecord(
|
||||
Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder
|
||||
});
|
||||
// Parse the response body
|
||||
if (typeof response.Body === "undefined") return null;
|
||||
const bodyContents = await response.Body.transformToString();
|
||||
return JSON.parse(bodyContents) as GameRecord;
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class GameManager {
|
||||
}
|
||||
|
||||
public game(id: GameID): GameServer | null {
|
||||
return this.games.get(id);
|
||||
return this.games.get(id) ?? null;
|
||||
}
|
||||
|
||||
addClient(client: Client, gameID: GameID, lastTurn: number): boolean {
|
||||
|
||||
+22
-22
@@ -42,13 +42,13 @@ export class GameServer {
|
||||
// Used for record record keeping
|
||||
private allClients: Map<ClientID, Client> = new Map();
|
||||
private _hasStarted = false;
|
||||
private _startTime: number = null;
|
||||
private _startTime: number | null = null;
|
||||
|
||||
private endTurnIntervalID;
|
||||
|
||||
private lastPingUpdate = 0;
|
||||
|
||||
private winner: ClientSendWinnerMessage = null;
|
||||
private winner: ClientSendWinnerMessage | null = null;
|
||||
// This field is currently only filled at victory
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
|
||||
@@ -68,32 +68,32 @@ export class GameServer {
|
||||
this.log = log_.child({ gameID: id });
|
||||
}
|
||||
|
||||
public updateGameConfig(gameConfig: GameConfig): void {
|
||||
if (gameConfig.gameMap != null) {
|
||||
public updateGameConfig(gameConfig: Partial<GameConfig>): void {
|
||||
if (typeof gameConfig.gameMap !== "undefined") {
|
||||
this.gameConfig.gameMap = gameConfig.gameMap;
|
||||
}
|
||||
if (gameConfig.difficulty != null) {
|
||||
if (typeof gameConfig.difficulty !== "undefined") {
|
||||
this.gameConfig.difficulty = gameConfig.difficulty;
|
||||
}
|
||||
if (gameConfig.disableNPCs != null) {
|
||||
if (typeof gameConfig.disableNPCs !== "undefined") {
|
||||
this.gameConfig.disableNPCs = gameConfig.disableNPCs;
|
||||
}
|
||||
if (gameConfig.disableNukes != null) {
|
||||
if (typeof gameConfig.disableNukes !== "undefined") {
|
||||
this.gameConfig.disableNukes = gameConfig.disableNukes;
|
||||
}
|
||||
if (gameConfig.bots != null) {
|
||||
if (typeof gameConfig.bots !== "undefined") {
|
||||
this.gameConfig.bots = gameConfig.bots;
|
||||
}
|
||||
if (gameConfig.infiniteGold != null) {
|
||||
if (typeof gameConfig.infiniteGold !== "undefined") {
|
||||
this.gameConfig.infiniteGold = gameConfig.infiniteGold;
|
||||
}
|
||||
if (gameConfig.infiniteTroops != null) {
|
||||
if (typeof gameConfig.infiniteTroops !== "undefined") {
|
||||
this.gameConfig.infiniteTroops = gameConfig.infiniteTroops;
|
||||
}
|
||||
if (gameConfig.instantBuild != null) {
|
||||
if (typeof gameConfig.instantBuild !== "undefined") {
|
||||
this.gameConfig.instantBuild = gameConfig.instantBuild;
|
||||
}
|
||||
if (gameConfig.gameMode != null) {
|
||||
if (typeof gameConfig.gameMode !== "undefined") {
|
||||
this.gameConfig.gameMode = gameConfig.gameMode;
|
||||
}
|
||||
}
|
||||
@@ -145,17 +145,17 @@ export class GameServer {
|
||||
"message",
|
||||
gatekeeper.wsHandler(client.ip, async (message: string) => {
|
||||
try {
|
||||
let clientMsg: ClientMessage = null;
|
||||
let clientMsg: ClientMessage | null = null;
|
||||
try {
|
||||
clientMsg = ClientMessageSchema.parse(JSON.parse(message));
|
||||
} catch (error) {
|
||||
throw Error(`error parsing schema for ${client.ip}`);
|
||||
}
|
||||
if (this.allClients.has(clientMsg.clientID)) {
|
||||
const client = this.allClients.get(clientMsg.clientID);
|
||||
if (client.persistentID != clientMsg.persistentID) {
|
||||
const c = this.allClients.get(clientMsg.clientID);
|
||||
if (typeof c !== "undefined") {
|
||||
if (c.persistentID != clientMsg.persistentID) {
|
||||
this.log.warn(
|
||||
`Client ID ${clientMsg.clientID} sent incorrect id ${clientMsg.persistentID}, does not match persistent id ${client.persistentID}`,
|
||||
`Client ID ${clientMsg.clientID} sent incorrect id ${clientMsg.persistentID}, does not match persistent id ${c.persistentID}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export class GameServer {
|
||||
}
|
||||
|
||||
public startTime(): number {
|
||||
if (this._startTime > 0) {
|
||||
if (this._startTime !== null && this._startTime > 0) {
|
||||
return this._startTime;
|
||||
} else {
|
||||
//game hasn't started yet, only works for public games
|
||||
@@ -368,10 +368,10 @@ export class GameServer {
|
||||
this.gameStartInfo,
|
||||
playerRecords,
|
||||
this.turns,
|
||||
this._startTime,
|
||||
this._startTime ?? 0,
|
||||
Date.now(),
|
||||
this.winner?.winner,
|
||||
this.winner?.winnerType,
|
||||
this.winner?.winner ?? null,
|
||||
this.winner?.winnerType ?? null,
|
||||
this.allPlayersStats,
|
||||
),
|
||||
);
|
||||
@@ -405,7 +405,7 @@ export class GameServer {
|
||||
|
||||
phase(): GamePhase {
|
||||
const now = Date.now();
|
||||
const alive = [];
|
||||
const alive: Client[] = [];
|
||||
for (const client of this.activeClients) {
|
||||
if (now - client.lastPing > 60_000) {
|
||||
this.log.info(
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface Gatekeeper {
|
||||
) => (message: string) => Promise<void>;
|
||||
}
|
||||
|
||||
let gk: Gatekeeper = null;
|
||||
let gk: Gatekeeper | null = null;
|
||||
|
||||
async function getGatekeeperCached(): Promise<Gatekeeper> {
|
||||
if (gk != null) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class MapPlaylist {
|
||||
case 1:
|
||||
this.currentPlaylistCounter++;
|
||||
return PlaylistType.BigMaps;
|
||||
case 2:
|
||||
default:
|
||||
this.currentPlaylistCounter = 0;
|
||||
return PlaylistType.SmallMaps;
|
||||
}
|
||||
|
||||
+20
-2
@@ -170,7 +170,7 @@ app.get(
|
||||
);
|
||||
|
||||
async function fetchLobbies(): Promise<number> {
|
||||
const fetchPromises = [];
|
||||
const fetchPromises: Promise<GameInfo | null>[] = [];
|
||||
|
||||
for (const gameID of publicLobbyIDs) {
|
||||
const controller = new AbortController();
|
||||
@@ -209,8 +209,26 @@ async function fetchLobbies(): Promise<number> {
|
||||
});
|
||||
|
||||
lobbyInfos.forEach((l) => {
|
||||
if (l.msUntilStart <= 250 || l.gameConfig.maxPlayers <= l.numClients) {
|
||||
if (
|
||||
"msUntilStart" in l &&
|
||||
typeof l.msUntilStart !== "undefined" &&
|
||||
l.msUntilStart <= 250
|
||||
) {
|
||||
publicLobbyIDs.delete(l.gameID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
"gameConfig" in l &&
|
||||
typeof l.gameConfig !== "undefined" &&
|
||||
"maxPlayers" in l.gameConfig &&
|
||||
typeof l.gameConfig.maxPlayers !== "undefined" &&
|
||||
"numClients" in l &&
|
||||
typeof l.numClients !== "undefined" &&
|
||||
l.gameConfig.maxPlayers <= l.numClients
|
||||
) {
|
||||
publicLobbyIDs.delete(l.gameID);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ export function setupMetricsServer() {
|
||||
|
||||
// Track seen metric names to avoid duplicate metadata
|
||||
const seenMetrics = new Set();
|
||||
const processedLines = [];
|
||||
const allMetricValues = [];
|
||||
const processedLines: string[] = [];
|
||||
const allMetricValues: string[] = [];
|
||||
|
||||
// Process all metadata information in the master metrics first
|
||||
const masterLines = masterMetrics.split("\n");
|
||||
|
||||
@@ -15,7 +15,7 @@ let game: Game;
|
||||
let attacker: Player;
|
||||
|
||||
function attackerBuildsNuke(
|
||||
source: TileRef,
|
||||
source: TileRef | null,
|
||||
target: TileRef,
|
||||
initialize = true,
|
||||
) {
|
||||
|
||||
@@ -58,6 +58,10 @@ describe("Warship", () => {
|
||||
|
||||
test("Warship heals only if player has port", async () => {
|
||||
const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth;
|
||||
if (typeof maxHealth !== "number") {
|
||||
expect(typeof maxHealth).toBe("number");
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
const port = player1.buildUnit(UnitType.Port, 0, game.ref(coastX, 10));
|
||||
const warship = player1.buildUnit(
|
||||
|
||||
+14
-4
@@ -1,6 +1,11 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { Difficulty, GameType } from "../../src/core/game/Game";
|
||||
import {
|
||||
Difficulty,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
} from "../../src/core/game/Game";
|
||||
import { createGame } from "../../src/core/game/GameImpl";
|
||||
import { genTerrainFromBin } from "../../src/core/game/TerrainMapLoader";
|
||||
import { UserSettings } from "../../src/core/game/UserSettings";
|
||||
@@ -9,7 +14,10 @@ import { generateMap } from "../../src/scripts/TerrainMapGenerator";
|
||||
import { TestConfig } from "./TestConfig";
|
||||
import { TestServerConfig } from "./TestServerConfig";
|
||||
|
||||
export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
|
||||
export async function setup(
|
||||
mapName: string,
|
||||
_gameConfig: Partial<GameConfig> = {},
|
||||
) {
|
||||
// Load the specified map
|
||||
const mapPath = path.join(__dirname, "..", "testdata", `${mapName}.png`);
|
||||
const imageBuffer = await fs.readFile(mapPath);
|
||||
@@ -22,11 +30,13 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
|
||||
|
||||
// Configure the game
|
||||
const serverConfig = new TestServerConfig();
|
||||
const gameConfig = {
|
||||
gameMap: null,
|
||||
const gameConfig: GameConfig = {
|
||||
gameMap: GameMapType.Asia,
|
||||
gameMode: GameMode.FFA,
|
||||
gameType: GameType.Singleplayer,
|
||||
difficulty: Difficulty.Medium,
|
||||
disableNPCs: false,
|
||||
disableNukes: false,
|
||||
bots: 0,
|
||||
infiniteGold: false,
|
||||
infiniteTroops: false,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"resolveJsonModule": true,
|
||||
"strictNullChecks": true,
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user