mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Enable strictNullChecks, eqeqeq (#436)
## Description: Improve type safety and runtime correctness by: 1. Enabling TypeScript's [strictNullChecks](https://www.typescriptlang.org/tsconfig/#strictNullChecks) compiler option. 2. Replacing all loose equality operators (`==` and `!=`) with strict equality operators (`===` and `!==`). 3. Cleaning up of type declarations, null handling logic, and equality expressions throughout the project. Currently, the code allows implicit assumptions that `null` and `undefined` are interchangeable, and relies on type-coercing equality checks that can introduce subtle bugs. These practices make it difficult to reason about when values may be absent and hinder the effectiveness of static analysis. Migrating to strict null checks and enforcing strict equality comparisons will clarify intent, reduce bugs, and make the codebase safer and easier to maintain. Fixes #466 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> Co-authored-by: evanpelle <openfrontio@gmail.com>
This commit is contained in:
@@ -31,4 +31,10 @@ export default [
|
||||
"no-useless-escape": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
// Enable rules
|
||||
eqeqeq: "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -63,7 +63,7 @@ export function joinLobby(
|
||||
);
|
||||
|
||||
const userSettings: UserSettings = new UserSettings();
|
||||
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config);
|
||||
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {});
|
||||
|
||||
const transport = new Transport(lobbyConfig, eventBus);
|
||||
|
||||
@@ -74,12 +74,12 @@ export function joinLobby(
|
||||
let terrainLoad: Promise<TerrainMapData> | null = null;
|
||||
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type == "prestart") {
|
||||
if (message.type === "prestart") {
|
||||
consolex.log(`lobby: game prestarting: ${JSON.stringify(message)}`);
|
||||
terrainLoad = loadTerrainMap(message.gameMap);
|
||||
onPrestart();
|
||||
}
|
||||
if (message.type == "start") {
|
||||
if (message.type === "start") {
|
||||
// Trigger prestart for singleplayer games
|
||||
onPrestart();
|
||||
consolex.log(`lobby: game started: ${JSON.stringify(message, null, 2)}`);
|
||||
@@ -109,10 +109,13 @@ export async function createClientGame(
|
||||
userSettings: UserSettings,
|
||||
terrainLoad: Promise<TerrainMapData> | null,
|
||||
): Promise<ClientGameRunner> {
|
||||
if (lobbyConfig.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const config = await getConfig(
|
||||
lobbyConfig.gameStartInfo.config,
|
||||
userSettings,
|
||||
lobbyConfig.gameRecord != null,
|
||||
lobbyConfig.gameRecord !== undefined,
|
||||
);
|
||||
let gameMap: TerrainMapData | null = null;
|
||||
|
||||
@@ -160,7 +163,7 @@ export async function createClientGame(
|
||||
}
|
||||
|
||||
export class ClientGameRunner {
|
||||
private myPlayer: PlayerView;
|
||||
private myPlayer: PlayerView | null = null;
|
||||
private isActive = false;
|
||||
|
||||
private turnsSeen = 0;
|
||||
@@ -193,7 +196,7 @@ export class ClientGameRunner {
|
||||
},
|
||||
];
|
||||
let winner: ClientID | Team | null = null;
|
||||
if (update.winnerType == "player") {
|
||||
if (update.winnerType === "player") {
|
||||
winner = this.gameView
|
||||
.playerBySmallID(update.winner as number)
|
||||
.clientID();
|
||||
@@ -201,6 +204,9 @@ export class ClientGameRunner {
|
||||
winner = update.winner as Team;
|
||||
}
|
||||
|
||||
if (this.lobby.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const record = createGameRecord(
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.gameStartInfo,
|
||||
@@ -233,10 +239,13 @@ export class ClientGameRunner {
|
||||
this.renderer.initialize();
|
||||
this.input.initialize();
|
||||
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
|
||||
if (this.lobby.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
if ("errMsg" in gu) {
|
||||
showErrorModal(
|
||||
gu.errMsg,
|
||||
gu.stack,
|
||||
gu.stack ?? "missing",
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.clientID,
|
||||
);
|
||||
@@ -268,7 +277,7 @@ export class ClientGameRunner {
|
||||
};
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
this.lastMessageTime = Date.now();
|
||||
if (message.type == "start") {
|
||||
if (message.type === "start") {
|
||||
this.hasJoined = true;
|
||||
consolex.log("starting game!");
|
||||
for (const turn of message.turns) {
|
||||
@@ -286,7 +295,10 @@ export class ClientGameRunner {
|
||||
this.turnsSeen++;
|
||||
}
|
||||
}
|
||||
if (message.type == "desync") {
|
||||
if (message.type === "desync") {
|
||||
if (this.lobby.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
showErrorModal(
|
||||
`desync from server: ${JSON.stringify(message)}`,
|
||||
"",
|
||||
@@ -296,12 +308,12 @@ export class ClientGameRunner {
|
||||
"You are desynced from other players. What you see might differ from other players.",
|
||||
);
|
||||
}
|
||||
if (message.type == "turn") {
|
||||
if (message.type === "turn") {
|
||||
if (!this.hasJoined) {
|
||||
this.transport.joinGame(0);
|
||||
return;
|
||||
}
|
||||
if (this.turnsSeen != message.turn.turnNumber) {
|
||||
if (this.turnsSeen !== message.turn.turnNumber) {
|
||||
consolex.error(
|
||||
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
|
||||
);
|
||||
@@ -348,17 +360,17 @@ export class ClientGameRunner {
|
||||
if (this.gameView.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
if (this.myPlayer == null) {
|
||||
this.myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
if (this.myPlayer == null) {
|
||||
return;
|
||||
}
|
||||
if (this.myPlayer === null) {
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
this.myPlayer.actions(tile).then((actions) => {
|
||||
if (this.myPlayer === null) return;
|
||||
const bu = actions.buildableUnits.find(
|
||||
(bu) => bu.type == UnitType.TransportShip,
|
||||
(bu) => bu.type === UnitType.TransportShip,
|
||||
);
|
||||
if (bu == null) {
|
||||
if (bu === undefined) {
|
||||
console.warn(`no transport ship buildable units`);
|
||||
return;
|
||||
}
|
||||
@@ -377,7 +389,8 @@ export class ClientGameRunner {
|
||||
this.myPlayer
|
||||
.bestTransportShipSpawn(this.gameView.ref(cell.x, cell.y))
|
||||
.then((spawn: number | false) => {
|
||||
let spawnCell = null;
|
||||
if (this.myPlayer === null) throw new Error("not initialized");
|
||||
let spawnCell: Cell | null = null;
|
||||
if (spawn !== false) {
|
||||
spawnCell = new Cell(
|
||||
this.gameView.x(spawn),
|
||||
|
||||
@@ -26,7 +26,7 @@ export class FlagInput extends LitElement {
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
if (flag == "xx") {
|
||||
if (flag === "xx") {
|
||||
flag = "";
|
||||
}
|
||||
this.flag = flag;
|
||||
|
||||
@@ -87,7 +87,7 @@ export class GoogleAdElement extends LitElement {
|
||||
const isElectron = () => {
|
||||
// Renderer process
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window !== undefined &&
|
||||
typeof window.process === "object" &&
|
||||
// @ts-expect-error hidden
|
||||
window.process.type === "renderer"
|
||||
@@ -97,7 +97,7 @@ const isElectron = () => {
|
||||
|
||||
// Main process
|
||||
if (
|
||||
typeof process !== "undefined" &&
|
||||
process !== undefined &&
|
||||
typeof process.versions === "object" &&
|
||||
!!process.versions.electron
|
||||
) {
|
||||
|
||||
@@ -31,7 +31,6 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private disableNPCs = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: number | typeof Duos = 2;
|
||||
@state() private disableNukes: boolean = false;
|
||||
@state() private bots: number = 400;
|
||||
@state() private infiniteGold: boolean = false;
|
||||
@state() private infiniteTroops: boolean = false;
|
||||
@@ -40,9 +39,9 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private copySuccess = false;
|
||||
@state() private players: string[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private disabledUnits: string[] = [];
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
|
||||
private playersInterval = null;
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
// Add a new timer for debouncing bot changes
|
||||
private botsUpdateTimer: number | null = null;
|
||||
|
||||
@@ -106,7 +105,7 @@ export class HostLobbyModal extends LitElement {
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === mapValue}
|
||||
.translation=${translateText(
|
||||
`map.${mapKey.toLowerCase()}`,
|
||||
`map.${mapKey?.toLowerCase()}`,
|
||||
)}
|
||||
></map-display>
|
||||
</div>
|
||||
@@ -233,7 +232,7 @@ export class HostLobbyModal extends LitElement {
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
<span>${translateText("host_modal.bots")}</span>${
|
||||
this.bots == 0
|
||||
this.bots === 0
|
||||
? translateText("host_modal.bots_disabled")
|
||||
: this.bots
|
||||
}
|
||||
@@ -326,7 +325,7 @@ export class HostLobbyModal extends LitElement {
|
||||
[UnitType.HydrogenBomb, "unit_type.hydrogen_bomb"],
|
||||
[UnitType.MIRV, "unit_type.mirv"],
|
||||
].map(
|
||||
([unitType, translationKey]) => html`
|
||||
([unitType, translationKey]: [UnitType, string]) => html`
|
||||
<label
|
||||
class="option-card ${this.disabledUnits.includes(
|
||||
unitType,
|
||||
@@ -405,7 +404,7 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</o-modal>
|
||||
`;
|
||||
@@ -503,10 +502,6 @@ export class HostLobbyModal extends LitElement {
|
||||
this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked);
|
||||
this.putGameConfig();
|
||||
}
|
||||
private handleDisableNukesChange(e: Event) {
|
||||
this.disableNukes = Boolean((e.target as HTMLInputElement).checked);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleDisableNPCsChange(e: Event) {
|
||||
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
|
||||
@@ -537,16 +532,14 @@ export class HostLobbyModal extends LitElement {
|
||||
gameMap: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
disableNukes: this.disableNukes,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
gameMode: this.gameMode,
|
||||
numPlayerTeams: this.teamCount,
|
||||
disabledUnits: this.disabledUnits,
|
||||
playerTeams: this.teamCount,
|
||||
} as GameConfig),
|
||||
} satisfies Partial<GameConfig>),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
@@ -607,7 +600,7 @@ export class HostLobbyModal extends LitElement {
|
||||
.then((response) => response.json())
|
||||
.then((data: GameInfo) => {
|
||||
console.log(`got game info response: ${JSON.stringify(data)}`);
|
||||
this.players = data.clients.map((p) => p.username);
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export class InputHandler {
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
private moveInterval: NodeJS.Timeout = null;
|
||||
private moveInterval: NodeJS.Timeout | null = null;
|
||||
private activeKeys = new Set<string>();
|
||||
|
||||
private readonly PAN_SPEED = 5;
|
||||
@@ -300,7 +300,7 @@ export class InputHandler {
|
||||
Math.abs(event.x - this.lastPointerDownX) +
|
||||
Math.abs(event.y - this.lastPointerDownY);
|
||||
if (dist < 10) {
|
||||
if (event.pointerType == "touch") {
|
||||
if (event.pointerType === "touch") {
|
||||
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -385,7 +385,9 @@ export class InputHandler {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.moveInterval);
|
||||
if (this.moveInterval !== null) {
|
||||
clearInterval(this.moveInterval);
|
||||
}
|
||||
this.activeKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +26,8 @@ function save(stats: LocalStatsData) {
|
||||
|
||||
// The user can quit the game anytime so better save the lobby as soon as the
|
||||
// game starts.
|
||||
export function startGame(id: GameID, lobby: GameConfig) {
|
||||
if (typeof localStorage === "undefined") {
|
||||
export function startGame(id: GameID, lobby: Partial<GameConfig>) {
|
||||
if (localStorage === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function startTime() {
|
||||
}
|
||||
|
||||
export function endGame(gameRecord: GameRecord) {
|
||||
if (typeof localStorage === "undefined") {
|
||||
if (localStorage === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class LocalServer {
|
||||
|
||||
private paused = false;
|
||||
|
||||
private winner: ClientSendWinnerMessage = null;
|
||||
private winner: ClientSendWinnerMessage | null = null;
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
|
||||
private turnsExecuted = 0;
|
||||
@@ -43,7 +43,7 @@ export class LocalServer {
|
||||
|
||||
start() {
|
||||
this.turnCheckInterval = setInterval(() => {
|
||||
if (this.turnsExecuted == this.turns.length) {
|
||||
if (this.turnsExecuted === this.turns.length) {
|
||||
if (
|
||||
this.isReplay ||
|
||||
Date.now() >
|
||||
@@ -62,6 +62,9 @@ export class LocalServer {
|
||||
this.lobbyConfig.gameRecord,
|
||||
).turns;
|
||||
}
|
||||
if (this.lobbyConfig.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
this.clientMessage(
|
||||
ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
@@ -84,13 +87,13 @@ export class LocalServer {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(
|
||||
JSON.parse(message),
|
||||
);
|
||||
if (clientMsg.type == "intent") {
|
||||
if (clientMsg.type === "intent") {
|
||||
if (this.lobbyConfig.gameRecord) {
|
||||
// If we are replaying a game, we don't want to process intents
|
||||
return;
|
||||
}
|
||||
if (this.paused) {
|
||||
if (clientMsg.intent.type == "troop_ratio") {
|
||||
if (clientMsg.intent.type === "troop_ratio") {
|
||||
// Store troop change events because otherwise they are
|
||||
// not registered when game is paused.
|
||||
this.intents.push(clientMsg.intent);
|
||||
@@ -99,7 +102,7 @@ export class LocalServer {
|
||||
}
|
||||
this.intents.push(clientMsg.intent);
|
||||
}
|
||||
if (clientMsg.type == "hash") {
|
||||
if (clientMsg.type === "hash") {
|
||||
if (!this.lobbyConfig.gameRecord) {
|
||||
// If we are playing a singleplayer then store hash.
|
||||
this.turns[clientMsg.turnNumber].hash = clientMsg.hash;
|
||||
@@ -113,7 +116,7 @@ export class LocalServer {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (archivedHash != clientMsg.hash) {
|
||||
if (archivedHash !== clientMsg.hash) {
|
||||
console.error(
|
||||
`desync detected on turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}, server hash: ${archivedHash}`,
|
||||
);
|
||||
@@ -131,7 +134,7 @@ export class LocalServer {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (clientMsg.type == "winner") {
|
||||
if (clientMsg.type === "winner") {
|
||||
this.winner = clientMsg;
|
||||
this.allPlayersStats = clientMsg.allPlayersStats;
|
||||
}
|
||||
@@ -172,6 +175,9 @@ export class LocalServer {
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
},
|
||||
];
|
||||
if (this.lobbyConfig.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
}
|
||||
const record = createGameRecord(
|
||||
this.lobbyConfig.gameStartInfo.gameID,
|
||||
this.lobbyConfig.gameStartInfo,
|
||||
@@ -179,8 +185,8 @@ export class LocalServer {
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
Date.now(),
|
||||
this.winner?.winner,
|
||||
this.winner?.winnerType,
|
||||
this.winner?.winner ?? null,
|
||||
this.winner?.winnerType ?? null,
|
||||
this.allPlayersStats,
|
||||
);
|
||||
if (!saveFullGame) {
|
||||
|
||||
+38
-29
@@ -46,7 +46,7 @@ export interface JoinLobbyEvent {
|
||||
}
|
||||
|
||||
class Client {
|
||||
private gameStop: () => void;
|
||||
private gameStop: (() => void) | null = null;
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
@@ -123,7 +123,7 @@ class Client {
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
consolex.log("Browser is closing");
|
||||
if (this.gameStop != null) {
|
||||
if (this.gameStop !== null) {
|
||||
this.gameStop();
|
||||
}
|
||||
});
|
||||
@@ -136,8 +136,10 @@ class Client {
|
||||
"single-player-modal",
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
document.getElementById("single-player").addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
const singlePlayer = document.getElementById("single-player");
|
||||
if (singlePlayer === null) throw new Error("Missing single-player");
|
||||
singlePlayer.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
spModal.open();
|
||||
}
|
||||
});
|
||||
@@ -150,7 +152,9 @@ class Client {
|
||||
|
||||
const hlpModal = document.querySelector("help-modal") as HelpModal;
|
||||
hlpModal instanceof HelpModal;
|
||||
document.getElementById("help-button").addEventListener("click", () => {
|
||||
const helpButton = document.getElementById("help-button");
|
||||
if (helpButton === null) throw new Error("Missing help-button");
|
||||
helpButton.addEventListener("click", () => {
|
||||
hlpModal.open();
|
||||
});
|
||||
|
||||
@@ -193,34 +197,39 @@ class Client {
|
||||
"user-setting",
|
||||
) as UserSettingModal;
|
||||
settingsModal instanceof UserSettingModal;
|
||||
document.getElementById("settings-button").addEventListener("click", () => {
|
||||
settingsModal.open();
|
||||
});
|
||||
document
|
||||
.getElementById("settings-button")
|
||||
?.addEventListener("click", () => {
|
||||
settingsModal.open();
|
||||
});
|
||||
|
||||
const hostModal = document.querySelector(
|
||||
"host-lobby-modal",
|
||||
) as HostPrivateLobbyModal;
|
||||
hostModal instanceof HostPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("host-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
hostModal.open();
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
});
|
||||
const hostLobbyButton = document.getElementById("host-lobby-button");
|
||||
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
|
||||
hostLobbyButton.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
hostModal.open();
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
});
|
||||
|
||||
this.joinModal = document.querySelector(
|
||||
"join-private-lobby-modal",
|
||||
) as JoinPrivateLobbyModal;
|
||||
this.joinModal instanceof JoinPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("join-private-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
this.joinModal.open();
|
||||
}
|
||||
});
|
||||
const joinPrivateLobbyButton = document.getElementById(
|
||||
"join-private-lobby-button",
|
||||
);
|
||||
if (joinPrivateLobbyButton === null)
|
||||
throw new Error("Missing join-private-lobby-button");
|
||||
joinPrivateLobbyButton.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
this.joinModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.userSettings.darkMode()) {
|
||||
document.documentElement.classList.add("dark");
|
||||
@@ -258,7 +267,7 @@ class Client {
|
||||
private async handleJoinLobby(event: CustomEvent) {
|
||||
const lobby = event.detail as JoinLobbyEvent;
|
||||
consolex.log(`joining lobby ${lobby.gameID}`);
|
||||
if (this.gameStop != null) {
|
||||
if (this.gameStop !== null) {
|
||||
consolex.log("joining lobby, stopping existing game");
|
||||
this.gameStop();
|
||||
}
|
||||
@@ -269,10 +278,10 @@ class Client {
|
||||
gameID: lobby.gameID,
|
||||
serverConfig: config,
|
||||
flag:
|
||||
this.flagInput.getCurrentFlag() == "xx"
|
||||
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
|
||||
? ""
|
||||
: this.flagInput.getCurrentFlag(),
|
||||
playerName: this.usernameInput.getCurrentUsername(),
|
||||
playerName: this.usernameInput?.getCurrentUsername() ?? "",
|
||||
token: localStorage.getItem("token") ?? getPersistentIDFromCookie(),
|
||||
clientID: lobby.clientID,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo,
|
||||
@@ -280,7 +289,7 @@ class Client {
|
||||
},
|
||||
() => {
|
||||
console.log("Closing modals");
|
||||
document.getElementById("settings-button").classList.add("hidden");
|
||||
document.getElementById("settings-button")?.classList.add("hidden");
|
||||
[
|
||||
"single-player-modal",
|
||||
"host-lobby-modal",
|
||||
@@ -319,7 +328,7 @@ class Client {
|
||||
(ad as HTMLElement).style.display = "none";
|
||||
});
|
||||
|
||||
if (event.detail.gameConfig?.gameType != GameType.Singleplayer) {
|
||||
if (event.detail.gameConfig?.gameType !== GameType.Singleplayer) {
|
||||
window.history.pushState({}, "", `/join/${lobby.gameID}`);
|
||||
sessionStorage.setItem("inLobby", "true");
|
||||
}
|
||||
@@ -328,7 +337,7 @@ class Client {
|
||||
}
|
||||
|
||||
private async handleLeaveLobby(/* event: CustomEvent */) {
|
||||
if (this.gameStop == null) {
|
||||
if (this.gameStop === null) {
|
||||
return;
|
||||
}
|
||||
consolex.log("leaving lobby, cancelling game");
|
||||
|
||||
@@ -14,7 +14,7 @@ export class PublicLobby extends LitElement {
|
||||
@state() public isLobbyHighlighted: boolean = false;
|
||||
@state() private isButtonDebounced: boolean = false;
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: GameInfo = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
private debounceDelay: number = 750;
|
||||
private lobbyIDToStart = new Map<GameID, number>();
|
||||
|
||||
@@ -46,7 +46,8 @@ export class PublicLobby extends LitElement {
|
||||
// Store the start time on first fetch because endpoint is cached, causing
|
||||
// the time to appear irregular.
|
||||
if (!this.lobbyIDToStart.has(l.gameID)) {
|
||||
this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now());
|
||||
const msUntilStart = l.msUntilStart ?? 0;
|
||||
this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now());
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -82,17 +83,13 @@ export class PublicLobby extends LitElement {
|
||||
if (!lobby?.gameConfig) {
|
||||
return;
|
||||
}
|
||||
const timeRemaining = Math.max(
|
||||
0,
|
||||
Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000),
|
||||
);
|
||||
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
|
||||
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
|
||||
|
||||
// Format time to show minutes and seconds
|
||||
const minutes = Math.floor(timeRemaining / 60);
|
||||
const seconds = timeRemaining % 60;
|
||||
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
||||
const playersRemainingBeforeMax =
|
||||
lobby.gameConfig.maxPlayers - lobby.numClients;
|
||||
|
||||
const teamCount =
|
||||
lobby.gameConfig.gameMode === GameMode.Team
|
||||
@@ -131,8 +128,8 @@ export class PublicLobby extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="text-md font-medium text-blue-100">
|
||||
${lobby.gameConfig.gameMode == GameMode.Team
|
||||
? translateText("public_lobby.teams", { num: teamCount })
|
||||
${lobby.gameConfig.gameMode === GameMode.Team
|
||||
? translateText("public_lobby.teams", { num: teamCount ?? 0 })
|
||||
: translateText("game_mode.ffa")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,7 +165,7 @@ export class PublicLobby extends LitElement {
|
||||
this.isButtonDebounced = false;
|
||||
}, this.debounceDelay);
|
||||
|
||||
if (this.currLobby == null) {
|
||||
if (this.currLobby === null) {
|
||||
this.isLobbyHighlighted = true;
|
||||
this.currLobby = lobby;
|
||||
this.dispatchEvent(
|
||||
|
||||
@@ -73,7 +73,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === mapValue}
|
||||
.translation=${translateText(
|
||||
`map.${mapKey.toLowerCase()}`,
|
||||
`map.${mapKey?.toLowerCase()}`,
|
||||
)}
|
||||
></map-display>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
<span>${translateText("single_modal.bots")}</span>${this
|
||||
.bots == 0
|
||||
.bots === 0
|
||||
? translateText("single_modal.bots_disabled")
|
||||
: this.bots}
|
||||
</div>
|
||||
@@ -444,7 +444,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
clientID,
|
||||
username: usernameInput.getCurrentUsername(),
|
||||
flag:
|
||||
flagInput.getCurrentFlag() == "xx"
|
||||
flagInput.getCurrentFlag() === "xx"
|
||||
? ""
|
||||
: flagInput.getCurrentFlag(),
|
||||
},
|
||||
|
||||
+29
-20
@@ -60,14 +60,14 @@ export class SendSpawnIntentEvent implements GameEvent {
|
||||
|
||||
export class SendAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly targetID: PlayerID | null,
|
||||
public readonly troops: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendBoatAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly targetID: PlayerID | null,
|
||||
public readonly dst: Cell,
|
||||
public readonly troops: number,
|
||||
public readonly src: Cell | null = null,
|
||||
@@ -158,7 +158,7 @@ export class MoveWarshipIntentEvent implements GameEvent {
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private socket: WebSocket;
|
||||
private socket: WebSocket | null = null;
|
||||
|
||||
private localServer: LocalServer;
|
||||
|
||||
@@ -176,8 +176,8 @@ export class Transport {
|
||||
// If gameRecord is not null, we are replaying an archived game.
|
||||
// For multiplayer games, GameConfig is not known until game starts.
|
||||
this.isLocal =
|
||||
lobbyConfig.gameRecord != null ||
|
||||
lobbyConfig.gameStartInfo?.config.gameType == GameType.Singleplayer;
|
||||
lobbyConfig.gameRecord !== undefined ||
|
||||
lobbyConfig.gameStartInfo?.config.gameType === GameType.Singleplayer;
|
||||
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) =>
|
||||
this.onSendAllianceRequest(e),
|
||||
@@ -228,9 +228,9 @@ export class Transport {
|
||||
|
||||
private startPing() {
|
||||
if (this.isLocal || this.pingInterval) return;
|
||||
if (this.pingInterval == null) {
|
||||
if (this.pingInterval === null) {
|
||||
this.pingInterval = window.setInterval(() => {
|
||||
if (this.socket != null && this.socket.readyState === WebSocket.OPEN) {
|
||||
if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg(
|
||||
JSON.stringify({
|
||||
type: "ping",
|
||||
@@ -267,7 +267,7 @@ export class Transport {
|
||||
this.lobbyConfig,
|
||||
onconnect,
|
||||
onmessage,
|
||||
this.lobbyConfig.gameRecord != null,
|
||||
this.lobbyConfig.gameRecord !== undefined,
|
||||
);
|
||||
this.localServer.start();
|
||||
}
|
||||
@@ -290,7 +290,12 @@ export class Transport {
|
||||
console.log("Connected to game server!");
|
||||
while (this.buffer.length > 0) {
|
||||
console.log("sending dropped message");
|
||||
this.sendMsg(this.buffer.pop());
|
||||
const msg = this.buffer.pop();
|
||||
if (msg === undefined) {
|
||||
console.warn("msg is undefined");
|
||||
continue;
|
||||
}
|
||||
this.sendMsg(msg);
|
||||
}
|
||||
onconnect();
|
||||
};
|
||||
@@ -306,13 +311,14 @@ export class Transport {
|
||||
};
|
||||
this.socket.onerror = (err) => {
|
||||
console.error("Socket encountered error: ", err, "Closing socket");
|
||||
if (this.socket === null) return;
|
||||
this.socket.close();
|
||||
};
|
||||
this.socket.onclose = (event: CloseEvent) => {
|
||||
console.log(
|
||||
`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
|
||||
);
|
||||
if (event.code != 1000) {
|
||||
if (event.code !== 1000) {
|
||||
console.log(`reconnecting`);
|
||||
this.reconnect();
|
||||
}
|
||||
@@ -359,6 +365,7 @@ export class Transport {
|
||||
return;
|
||||
}
|
||||
this.stopPing();
|
||||
if (this.socket === null) return;
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
console.log("on stop: leaving game");
|
||||
this.socket.close();
|
||||
@@ -426,8 +433,8 @@ export class Transport {
|
||||
troops: event.troops,
|
||||
dstX: event.dst.x,
|
||||
dstY: event.dst.y,
|
||||
srcX: event.src?.x,
|
||||
srcY: event.src?.y,
|
||||
srcX: event.src?.x ?? null,
|
||||
srcY: event.src?.y ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -444,7 +451,7 @@ export class Transport {
|
||||
type: "emoji",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient:
|
||||
event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
|
||||
event.recipient === AllPlayers ? AllPlayers : event.recipient.id(),
|
||||
emoji: event.emoji,
|
||||
});
|
||||
}
|
||||
@@ -517,7 +524,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private onSendWinnerEvent(event: SendWinnerEvent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
||||
const msg = {
|
||||
type: "winner",
|
||||
winner: event.winner,
|
||||
@@ -528,13 +535,14 @@ export class Transport {
|
||||
} else {
|
||||
console.log(
|
||||
"WebSocket is not open. Current state:",
|
||||
this.socket.readyState,
|
||||
this.socket?.readyState,
|
||||
);
|
||||
console.log("attempting reconnect");
|
||||
}
|
||||
}
|
||||
|
||||
private onSendHashEvent(event: SendHashEvent) {
|
||||
if (this.socket === null) return;
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg(
|
||||
JSON.stringify({
|
||||
@@ -570,7 +578,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
||||
const msg = {
|
||||
type: "intent",
|
||||
intent: intent,
|
||||
@@ -579,7 +587,7 @@ export class Transport {
|
||||
} else {
|
||||
console.log(
|
||||
"WebSocket is not open. Current state:",
|
||||
this.socket.readyState,
|
||||
this.socket?.readyState,
|
||||
);
|
||||
console.log("attempting reconnect");
|
||||
}
|
||||
@@ -589,9 +597,10 @@ export class Transport {
|
||||
if (this.isLocal) {
|
||||
this.localServer.onMessage(msg);
|
||||
} else {
|
||||
if (this.socket === null) return;
|
||||
if (
|
||||
this.socket.readyState == WebSocket.CLOSED ||
|
||||
this.socket.readyState == WebSocket.CLOSED
|
||||
this.socket.readyState === WebSocket.CLOSED ||
|
||||
this.socket.readyState === WebSocket.CLOSED
|
||||
) {
|
||||
console.warn("socket not ready, closing and trying later");
|
||||
this.socket.close();
|
||||
@@ -605,7 +614,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private killExistingSocket(): void {
|
||||
if (this.socket == null) {
|
||||
if (this.socket === null) {
|
||||
return;
|
||||
}
|
||||
// Remove all event listeners
|
||||
|
||||
@@ -281,7 +281,7 @@ export class UserSettingModal extends LitElement {
|
||||
easter="true"
|
||||
@change=${(e: CustomEvent) => {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value !== "undefined") {
|
||||
if (value !== undefined) {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
@@ -300,7 +300,7 @@ export class UserSettingModal extends LitElement {
|
||||
easter="true"
|
||||
@change=${(e: CustomEvent) => {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value !== "undefined") {
|
||||
if (value !== undefined) {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
|
||||
@@ -64,7 +64,7 @@ export class UsernameInput extends LitElement {
|
||||
this.storeUsername(this.username);
|
||||
this.validationError = "";
|
||||
} else {
|
||||
this.validationError = result.error;
|
||||
this.validationError = result.error ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -45,12 +45,12 @@ export function createCanvas(): HTMLCanvasElement {
|
||||
*/
|
||||
export function generateCryptoRandomUUID(): string {
|
||||
// Type guard to check if randomUUID is available
|
||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||
if (crypto !== undefined && "randomUUID" in crypto) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// Fallback using crypto.getRandomValues
|
||||
if (typeof crypto !== "undefined" && "getRandomValues" in crypto) {
|
||||
if (crypto !== undefined && "getRandomValues" in crypto) {
|
||||
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
(c: number): string =>
|
||||
|
||||
@@ -30,7 +30,7 @@ export class SettingSlider extends LitElement {
|
||||
|
||||
private handleSliderChange(e: Event) {
|
||||
const detail = (e as CustomEvent)?.detail;
|
||||
if (!detail || typeof detail.value === "undefined") {
|
||||
if (!detail || detail.value === undefined) {
|
||||
console.warn("Invalid slider change event", e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,9 @@ export class GameRenderer {
|
||||
public uiState: UIState,
|
||||
private layers: Layer[],
|
||||
) {
|
||||
this.context = canvas.getContext("2d");
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
|
||||
@@ -90,6 +90,9 @@ export const getColoredSprite = (
|
||||
}
|
||||
|
||||
const sprite = getSpriteForUnit(unit.type());
|
||||
if (sprite === null) {
|
||||
throw new Error(`Failed to load sprite for ${unit.type()}`);
|
||||
}
|
||||
|
||||
const territoryRgb = territoryColor.toRgb();
|
||||
const borderRgb = borderColor.toRgb();
|
||||
|
||||
@@ -9,8 +9,8 @@ export class TransformHandler {
|
||||
private offsetX: number = -350;
|
||||
private offsetY: number = -200;
|
||||
|
||||
private target: Cell;
|
||||
private intervalID = null;
|
||||
private target: Cell | null;
|
||||
private intervalID: NodeJS.Timeout | null = null;
|
||||
private changed = false;
|
||||
|
||||
constructor(
|
||||
@@ -170,6 +170,8 @@ export class TransformHandler {
|
||||
const { screenX, screenY } = this.screenCenter();
|
||||
const screenMapCenter = new Cell(screenX, screenY);
|
||||
|
||||
if (this.target === null) throw new Error("null target");
|
||||
|
||||
if (
|
||||
this.game.manhattanDist(
|
||||
this.game.ref(screenX, screenY),
|
||||
@@ -234,7 +236,7 @@ export class TransformHandler {
|
||||
}
|
||||
|
||||
private clearTarget() {
|
||||
if (this.intervalID != null) {
|
||||
if (this.intervalID !== null) {
|
||||
clearInterval(this.intervalID);
|
||||
this.intervalID = null;
|
||||
}
|
||||
|
||||
@@ -303,13 +303,12 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
private _hidden = true;
|
||||
|
||||
private canBuild(item: BuildItemDisplay): boolean {
|
||||
if (this.game?.myPlayer() == null || this.playerActions == null) {
|
||||
if (this.game?.myPlayer() === null || this.playerActions === null) {
|
||||
return false;
|
||||
}
|
||||
const unit = this.playerActions.buildableUnits.filter(
|
||||
(u) => u.type == item.unitType,
|
||||
);
|
||||
if (!unit) {
|
||||
const buildableUnits = this.playerActions?.buildableUnits ?? [];
|
||||
const unit = buildableUnits.filter((u) => u.type === item.unitType);
|
||||
if (unit.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return unit[0].canBuild !== false;
|
||||
@@ -317,7 +316,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
|
||||
private cost(item: BuildItemDisplay): number {
|
||||
for (const bu of this.playerActions?.buildableUnits ?? []) {
|
||||
if (bu.type == item.unitType) {
|
||||
if (bu.type === item.unitType) {
|
||||
return bu.cost;
|
||||
}
|
||||
}
|
||||
@@ -368,9 +367,12 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
width="40"
|
||||
height="40"
|
||||
/>
|
||||
<span class="build-name">${translateText(item.key)}</span>
|
||||
<span class="build-name"
|
||||
>${item.key && translateText(item.key)}</span
|
||||
>
|
||||
<span class="build-description"
|
||||
>${translateText(item.description)}</span
|
||||
>${item.description &&
|
||||
translateText(item.description)}</span
|
||||
>
|
||||
<span class="build-cost" translate="no">
|
||||
${renderNumber(
|
||||
@@ -413,7 +415,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
private refresh() {
|
||||
this.game
|
||||
.myPlayer()
|
||||
.actions(this.clickedTile)
|
||||
?.actions(this.clickedTile)
|
||||
.then((actions) => {
|
||||
this.playerActions = actions;
|
||||
this.requestUpdate();
|
||||
|
||||
@@ -63,7 +63,7 @@ export class ChatDisplay extends LitElement implements Layer {
|
||||
if (event.messageType !== MessageType.CHAT) return;
|
||||
const myPlayer = this.game.playerByClientID(this.clientID);
|
||||
if (
|
||||
event.playerID != null &&
|
||||
event.playerID !== null &&
|
||||
(!myPlayer || myPlayer.smallID() !== event.playerID)
|
||||
) {
|
||||
return;
|
||||
@@ -82,6 +82,7 @@ export class ChatDisplay extends LitElement implements Layer {
|
||||
tick() {
|
||||
// this.active = true;
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates === null) throw new Error("null updates");
|
||||
const messages = updates[GameUpdateType.DisplayEvent] as
|
||||
| DisplayMessageUpdate[]
|
||||
| undefined;
|
||||
@@ -91,7 +92,7 @@ export class ChatDisplay extends LitElement implements Layer {
|
||||
if (msg.messageType === MessageType.CHAT) {
|
||||
const myPlayer = this.game.playerByClientID(this.clientID);
|
||||
if (
|
||||
msg.playerID != null &&
|
||||
msg.playerID !== null &&
|
||||
(!myPlayer || myPlayer.smallID() !== msg.playerID)
|
||||
) {
|
||||
continue;
|
||||
|
||||
@@ -214,7 +214,8 @@ export class ChatModal extends LitElement {
|
||||
|
||||
private selectPlayer(player: string) {
|
||||
if (this.previewText) {
|
||||
this.previewText = this.selectedPhraseTemplate.replace("[P1]", player);
|
||||
this.previewText =
|
||||
this.selectedPhraseTemplate?.replace("[P1]", player) ?? null;
|
||||
this.selectedPlayer = player;
|
||||
this.requiresPlayerSelection = false;
|
||||
this.requestUpdate();
|
||||
@@ -228,7 +229,9 @@ export class ChatModal extends LitElement {
|
||||
console.log("Key:", this.selectedQuickChatKey);
|
||||
|
||||
if (this.sender && this.recipient && this.selectedQuickChatKey) {
|
||||
const variables = this.selectedPlayer ? { P1: this.selectedPlayer } : {};
|
||||
const variables: Record<string, string> = this.selectedPlayer
|
||||
? { P1: this.selectedPlayer }
|
||||
: {};
|
||||
|
||||
this.eventBus.emit(
|
||||
new SendQuickChatEvent(
|
||||
|
||||
@@ -85,7 +85,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
newAttackRatio = 1;
|
||||
}
|
||||
|
||||
if (newAttackRatio == 0.11 && this.attackRatio == 0.01) {
|
||||
if (newAttackRatio === 0.11 && this.attackRatio === 0.01) {
|
||||
// If we're changing the ratio from 1%, then set it to 10% instead of 11% to keep a consistency
|
||||
newAttackRatio = 0.1;
|
||||
}
|
||||
@@ -108,13 +108,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
const player = this.game.myPlayer();
|
||||
if (player == null || !player.isAlive()) {
|
||||
if (player === null || !player.isAlive()) {
|
||||
this.setVisibile(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
@@ -275,7 +275,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
>${translateText("control_panel.attack_ratio")}:
|
||||
${(this.attackRatio * 100).toFixed(0)}%
|
||||
(${renderTroops(
|
||||
this.game?.myPlayer()?.troops() * this.attackRatio,
|
||||
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
|
||||
)})</label
|
||||
>
|
||||
<div class="relative h-8">
|
||||
|
||||
@@ -114,7 +114,7 @@ export class EmojiTable extends LitElement {
|
||||
|
||||
this.showTable((emoji) => {
|
||||
const recipient =
|
||||
targetPlayer == this.game.myPlayer()
|
||||
targetPlayer === this.game.myPlayer()
|
||||
? AllPlayers
|
||||
: (targetPlayer as PlayerView);
|
||||
this.eventBus.emit(
|
||||
|
||||
@@ -107,8 +107,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
tick() {
|
||||
this.active = true;
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
for (const [ut, fn] of this.updateMap) {
|
||||
updates[ut]?.forEach((u) => fn(u));
|
||||
if (updates) {
|
||||
for (const [ut, fn] of this.updateMap) {
|
||||
updates[ut]?.forEach(fn);
|
||||
}
|
||||
}
|
||||
|
||||
let remainingEvents = this.events.filter((event) => {
|
||||
@@ -137,16 +139,16 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
// Update attacks
|
||||
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
|
||||
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
|
||||
return t != PlayerType.Bot;
|
||||
return t !== PlayerType.Bot;
|
||||
});
|
||||
|
||||
this.outgoingAttacks = myPlayer
|
||||
.outgoingAttacks()
|
||||
.filter((a) => a.targetID != 0);
|
||||
.filter((a) => a.targetID !== 0);
|
||||
|
||||
this.outgoingLandAttacks = myPlayer
|
||||
.outgoingAttacks()
|
||||
.filter((a) => a.targetID == 0);
|
||||
.filter((a) => a.targetID === 0);
|
||||
|
||||
this.outgoingBoats = myPlayer
|
||||
.units()
|
||||
@@ -157,7 +159,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
private addEvent(event: Event) {
|
||||
this.events = [...this.events, event];
|
||||
if (this._hidden == true) {
|
||||
if (this._hidden === true) {
|
||||
this.newEvents++;
|
||||
}
|
||||
this.requestUpdate();
|
||||
@@ -179,7 +181,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
onDisplayMessageEvent(event: DisplayMessageUpdate) {
|
||||
const myPlayer = this.game.playerByClientID(this.clientID);
|
||||
if (
|
||||
event.playerID != null &&
|
||||
event.playerID !== null &&
|
||||
(!myPlayer || myPlayer.smallID() !== event.playerID)
|
||||
) {
|
||||
return;
|
||||
@@ -345,6 +347,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
: update.player2ID === myPlayer.smallID()
|
||||
? update.player1ID
|
||||
: null;
|
||||
if (otherID === null) return;
|
||||
const other = this.game.playerBySmallID(otherID) as PlayerView;
|
||||
if (!other || !myPlayer.isAlive() || !other.isAlive()) return;
|
||||
|
||||
@@ -394,14 +397,14 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
if (!myPlayer) return;
|
||||
|
||||
const recipient =
|
||||
update.emoji.recipientID == AllPlayers
|
||||
update.emoji.recipientID === AllPlayers
|
||||
? AllPlayers
|
||||
: this.game.playerBySmallID(update.emoji.recipientID);
|
||||
const sender = this.game.playerBySmallID(
|
||||
update.emoji.senderID,
|
||||
) as PlayerView;
|
||||
|
||||
if (recipient == myPlayer) {
|
||||
if (recipient === myPlayer) {
|
||||
this.addEvent({
|
||||
description: `${sender.displayName()}:${update.emoji.message}`,
|
||||
unsafeDescription: true,
|
||||
@@ -427,10 +430,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
onUnitIncomingEvent(event: UnitIncomingUpdate) {
|
||||
const myPlayer = this.game.playerByClientID(this.clientID);
|
||||
|
||||
if (
|
||||
event.playerID != null &&
|
||||
(!myPlayer || myPlayer.smallID() !== event.playerID)
|
||||
) {
|
||||
if (!myPlayer || myPlayer.smallID() !== event.playerID) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -482,8 +482,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
<button
|
||||
translate="no"
|
||||
class="ml-2"
|
||||
@click=${() =>
|
||||
this.emitGoToPlayerEvent(attack.attackerID)}
|
||||
@click=${() => {
|
||||
attack.attackerID &&
|
||||
this.emitGoToPlayerEvent(attack.attackerID);
|
||||
}}
|
||||
>
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
@@ -608,7 +610,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.events.sort((a, b) => {
|
||||
const aPrior = a.priority ?? 100000;
|
||||
const bPrior = b.priority ?? 100000;
|
||||
if (aPrior == bPrior) {
|
||||
if (aPrior === bPrior) {
|
||||
return a.createdAt - b.createdAt;
|
||||
}
|
||||
return bPrior - aPrior;
|
||||
@@ -665,7 +667,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
${event.focusID
|
||||
? html`<button
|
||||
@click=${() => {
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
event.focusID &&
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
}}
|
||||
>
|
||||
${this.getEventDescription(event)}
|
||||
@@ -673,7 +676,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
: event.unitView
|
||||
? html`<button
|
||||
@click=${() => {
|
||||
this.emitGoToUnitEvent(event.unitView);
|
||||
event.unitView &&
|
||||
this.emitGoToUnitEvent(event.unitView);
|
||||
}}
|
||||
>
|
||||
${this.getEventDescription(event)}
|
||||
|
||||
@@ -28,9 +28,9 @@ export class GoToUnitEvent implements GameEvent {
|
||||
|
||||
@customElement("leader-board")
|
||||
export class Leaderboard extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public clientID: ClientID;
|
||||
public eventBus: EventBus;
|
||||
public game: GameView | null = null;
|
||||
public clientID: ClientID | null = null;
|
||||
public eventBus: EventBus | null = null;
|
||||
|
||||
players: Entry[] = [];
|
||||
|
||||
@@ -42,6 +42,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
init() {}
|
||||
|
||||
tick() {
|
||||
if (this.game === null) throw new Error("Not initialized");
|
||||
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
|
||||
this._shownOnInit = true;
|
||||
this.showLeaderboard();
|
||||
@@ -51,18 +52,19 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.game.ticks() % 10 == 0) {
|
||||
if (this.game.ticks() % 10 === 0) {
|
||||
this.updateLeaderboard();
|
||||
}
|
||||
}
|
||||
|
||||
private updateLeaderboard() {
|
||||
if (this.clientID == null) {
|
||||
if (this.game === null) throw new Error("Not initialized");
|
||||
if (this.clientID === null) {
|
||||
return;
|
||||
}
|
||||
const myPlayer = this.game
|
||||
.playerViews()
|
||||
.find((p) => p.clientID() == this.clientID);
|
||||
const myPlayer =
|
||||
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
|
||||
null;
|
||||
|
||||
const sorted = this.game
|
||||
.playerViews()
|
||||
@@ -86,16 +88,19 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
),
|
||||
gold: renderNumber(player.gold()),
|
||||
troops: renderNumber(troops),
|
||||
isMyPlayer: player == myPlayer,
|
||||
isMyPlayer: player === myPlayer,
|
||||
player: player,
|
||||
};
|
||||
});
|
||||
|
||||
if (myPlayer != null && this.players.find((p) => p.isMyPlayer) == null) {
|
||||
if (
|
||||
myPlayer !== null &&
|
||||
this.players.find((p) => p.isMyPlayer) === undefined
|
||||
) {
|
||||
let place = 0;
|
||||
for (const p of sorted) {
|
||||
place++;
|
||||
if (p == myPlayer) {
|
||||
if (p === myPlayer) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -122,6 +127,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private handleRowClickPlayer(player: PlayerView) {
|
||||
if (this.eventBus === null) return;
|
||||
this.eventBus.emit(new GoToPlayerEvent(player));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export class MultiTabModal extends LitElement implements Layer {
|
||||
tick() {
|
||||
if (
|
||||
this.game.inSpawnPhase() ||
|
||||
this.game.config().gameConfig().gameType == GameType.Singleplayer ||
|
||||
this.game.config().serverConfig().env() == GameEnv.Dev
|
||||
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
|
||||
this.game.config().serverConfig().env() === GameEnv.Dev
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class RenderInfo {
|
||||
constructor(
|
||||
public player: PlayerView,
|
||||
public lastRenderCalc: number,
|
||||
public location: Cell,
|
||||
public location: Cell | null,
|
||||
public fontSize: number,
|
||||
public fontColor: string,
|
||||
public element: HTMLElement,
|
||||
@@ -104,7 +104,7 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
public tick() {
|
||||
if (this.game.ticks() % 10 != 0) {
|
||||
if (this.game.ticks() % 10 !== 0) {
|
||||
return;
|
||||
}
|
||||
const sorted = this.game
|
||||
@@ -238,7 +238,7 @@ export class NameLayer implements Layer {
|
||||
|
||||
renderPlayerInfo(render: RenderInfo) {
|
||||
if (!render.player.nameLocation() || !render.player.isAlive()) {
|
||||
this.renders = this.renders.filter((r) => r != render);
|
||||
this.renders = this.renders.filter((r) => r !== render);
|
||||
render.element.remove();
|
||||
return;
|
||||
}
|
||||
@@ -355,7 +355,7 @@ export class NameLayer implements Layer {
|
||||
|
||||
// Alliance icon
|
||||
const existingAlliance = iconsDiv.querySelector('[data-icon="alliance"]');
|
||||
if (myPlayer != null && myPlayer.isAlliedWith(render.player)) {
|
||||
if (myPlayer !== null && myPlayer.isAlliedWith(render.player)) {
|
||||
if (!existingAlliance) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(
|
||||
@@ -372,7 +372,7 @@ export class NameLayer implements Layer {
|
||||
// Alliance request icon
|
||||
const data = '[data-icon="alliance-request"]';
|
||||
const existingRequestAlliance = iconsDiv.querySelector(data);
|
||||
if (myPlayer != null && render.player.isRequestingAllianceWith(myPlayer)) {
|
||||
if (myPlayer !== null && render.player.isRequestingAllianceWith(myPlayer)) {
|
||||
if (!existingRequestAlliance) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(
|
||||
@@ -389,7 +389,7 @@ export class NameLayer implements Layer {
|
||||
// Target icon
|
||||
const existingTarget = iconsDiv.querySelector('[data-icon="target"]');
|
||||
if (
|
||||
myPlayer != null &&
|
||||
myPlayer !== null &&
|
||||
new Set(myPlayer.transitiveTargets()).has(render.player)
|
||||
) {
|
||||
if (!existingTarget) {
|
||||
@@ -412,11 +412,11 @@ export class NameLayer implements Layer {
|
||||
.outgoingEmojis()
|
||||
.filter(
|
||||
(emoji) =>
|
||||
emoji.recipientID == AllPlayers ||
|
||||
emoji.recipientID == myPlayer?.smallID(),
|
||||
emoji.recipientID === AllPlayers ||
|
||||
emoji.recipientID === myPlayer?.smallID(),
|
||||
);
|
||||
|
||||
if (this.game.config().userSettings().emojis() && emojis.length > 0) {
|
||||
if (this.game.config().userSettings()?.emojis() && emojis.length > 0) {
|
||||
if (!existingEmoji) {
|
||||
const emojiDiv = document.createElement("div");
|
||||
emojiDiv.setAttribute("data-icon", "emoji");
|
||||
@@ -451,8 +451,8 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
const nukesSentByOtherPlayer = this.game.units().filter((unit) => {
|
||||
const isSendingNuke = render.player.id() == unit.owner().id();
|
||||
const notMyPlayer = !myPlayer || unit.owner().id() != myPlayer.id();
|
||||
const isSendingNuke = render.player.id() === unit.owner().id();
|
||||
const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
|
||||
return (
|
||||
nukeTypes.includes(unit.type()) &&
|
||||
isSendingNuke &&
|
||||
@@ -462,24 +462,25 @@ export class NameLayer implements Layer {
|
||||
});
|
||||
const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
|
||||
const detonationDst = unit.detonationDst();
|
||||
if (detonationDst === undefined) return false;
|
||||
const targetId = this.game.owner(detonationDst).id();
|
||||
return myPlayer && targetId == this.myPlayer.id();
|
||||
return myPlayer && targetId === myPlayer.id();
|
||||
});
|
||||
const existingNuke = iconsDiv.querySelector(
|
||||
'[data-icon="nuke"]',
|
||||
) as HTMLImageElement;
|
||||
|
||||
if (existingNuke) {
|
||||
if (nukesSentByOtherPlayer.length == 0) {
|
||||
if (nukesSentByOtherPlayer.length === 0) {
|
||||
existingNuke.remove();
|
||||
} else if (
|
||||
isMyPlayerTarget &&
|
||||
existingNuke.src != this.nukeRedIconImage.src
|
||||
existingNuke.src !== this.nukeRedIconImage.src
|
||||
) {
|
||||
existingNuke.src = this.nukeRedIconImage.src;
|
||||
} else if (
|
||||
!isMyPlayerTarget &&
|
||||
existingNuke.src != this.nukeWhiteIconImage.src
|
||||
existingNuke.src !== this.nukeWhiteIconImage.src
|
||||
) {
|
||||
existingNuke.src = this.nukeWhiteIconImage.src;
|
||||
}
|
||||
@@ -499,7 +500,7 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
// Position element with scale
|
||||
if (render.location && render.location != oldLocation) {
|
||||
if (render.location && render.location !== oldLocation) {
|
||||
const scale = Math.min(baseSize * 0.25, 3);
|
||||
render.element.style.transform = `translate(${render.location.x}px, ${render.location.y}px) translate(-50%, -50%) scale(${scale})`;
|
||||
}
|
||||
@@ -525,12 +526,12 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
private getPlayer(): PlayerView | null {
|
||||
if (this.myPlayer != null) {
|
||||
if (this.myPlayer !== null) {
|
||||
return this.myPlayer;
|
||||
}
|
||||
this.myPlayer = this.game
|
||||
.playerViews()
|
||||
.find((p) => p.clientID() == this.clientID);
|
||||
this.myPlayer =
|
||||
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
|
||||
null;
|
||||
return this.myPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,19 +122,20 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
init() {
|
||||
console.log("init called from OptionsMenu");
|
||||
this.showPauseButton =
|
||||
this.game.config().gameConfig().gameType == GameType.Singleplayer ||
|
||||
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
|
||||
this.game.config().isReplay();
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.hasWinner =
|
||||
this.hasWinner ||
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].length > 0;
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = 0;
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
|
||||
this.timer++;
|
||||
}
|
||||
this.isVisible = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
@@ -177,13 +177,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
private renderPlayerInfo(player: PlayerView) {
|
||||
const myPlayer = this.myPlayer();
|
||||
const isFriendly = myPlayer?.isFriendly(player);
|
||||
let relationHtml = null;
|
||||
let relationHtml: TemplateResult | null = null;
|
||||
const attackingTroops = player
|
||||
.outgoingAttacks()
|
||||
.map((a) => a.troops)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (player.type() == PlayerType.FakeHuman && myPlayer != null) {
|
||||
if (player.type() === PlayerType.FakeHuman && myPlayer !== null) {
|
||||
const relation =
|
||||
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
|
||||
const relationClass = this.getRelationClass(relation);
|
||||
@@ -224,7 +224,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
: ""}
|
||||
${player.name()}
|
||||
</div>
|
||||
${player.team() != null
|
||||
${player.team() !== null
|
||||
? html`<div class="text-sm opacity-80">
|
||||
${translateText("player_info_overlay.team")}: ${player.team()}
|
||||
</div>`
|
||||
@@ -271,7 +271,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
private renderUnitInfo(unit: UnitView) {
|
||||
const isAlly =
|
||||
(unit.owner() == this.myPlayer() ||
|
||||
(unit.owner() === this.myPlayer() ||
|
||||
this.myPlayer()?.isFriendly(unit.owner())) ??
|
||||
false;
|
||||
|
||||
@@ -312,8 +312,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
<div
|
||||
class="bg-opacity-60 bg-gray-900 rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
>
|
||||
${this.player != null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit != null ? this.renderUnitInfo(this.unit) : ""}
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -39,8 +39,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
public emojiTable: EmojiTable;
|
||||
|
||||
private actions: PlayerActions = null;
|
||||
private tile: TileRef = null;
|
||||
private actions: PlayerActions | null = null;
|
||||
private tile: TileRef | null = null;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
@@ -125,7 +125,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) {
|
||||
e.stopPropagation();
|
||||
this.emojiTable.showTable((emoji: string) => {
|
||||
if (myPlayer == other) {
|
||||
if (myPlayer === other) {
|
||||
this.eventBus.emit(
|
||||
new SendEmojiIntentEvent(
|
||||
AllPlayers,
|
||||
@@ -181,12 +181,16 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return 0;
|
||||
}
|
||||
let sum = 0;
|
||||
const nukes = stats.sentNukes[this.g.myPlayer().id()];
|
||||
const player = this.g.myPlayer();
|
||||
if (player === null) {
|
||||
return 0;
|
||||
}
|
||||
const nukes = stats.sentNukes[player.id()];
|
||||
if (!nukes) {
|
||||
return 0;
|
||||
}
|
||||
for (const nukeType in nukes) {
|
||||
if (nukeType != UnitType.MIRVWarhead) {
|
||||
if (nukeType !== UnitType.MIRVWarhead) {
|
||||
sum += nukes[nukeType];
|
||||
}
|
||||
}
|
||||
@@ -198,10 +202,8 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return html``;
|
||||
}
|
||||
const myPlayer = this.g.myPlayer();
|
||||
if (myPlayer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (myPlayer === null) return;
|
||||
if (this.tile === null) return;
|
||||
let other = this.g.owner(this.tile);
|
||||
if (!other.isPlayer()) {
|
||||
this.hide();
|
||||
@@ -210,16 +212,16 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
}
|
||||
other = other as PlayerView;
|
||||
|
||||
const canDonate = this.actions.interaction?.canDonate;
|
||||
const canDonate = this.actions?.interaction?.canDonate;
|
||||
const canSendAllianceRequest =
|
||||
this.actions.interaction?.canSendAllianceRequest;
|
||||
this.actions?.interaction?.canSendAllianceRequest;
|
||||
const canSendEmoji =
|
||||
other == myPlayer
|
||||
? this.actions.canSendEmojiAllPlayers
|
||||
: this.actions.interaction?.canSendEmoji;
|
||||
const canBreakAlliance = this.actions.interaction?.canBreakAlliance;
|
||||
const canTarget = this.actions.interaction?.canTarget;
|
||||
const canEmbargo = this.actions.interaction?.canEmbargo;
|
||||
other === myPlayer
|
||||
? this.actions?.canSendEmojiAllPlayers
|
||||
: this.actions?.interaction?.canSendEmoji;
|
||||
const canBreakAlliance = this.actions?.interaction?.canBreakAlliance;
|
||||
const canTarget = this.actions?.interaction?.canTarget;
|
||||
const canEmbargo = this.actions?.interaction?.canEmbargo;
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -392,7 +394,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</button>`
|
||||
: ""}
|
||||
</div>
|
||||
${canEmbargo && other != myPlayer
|
||||
${canEmbargo && other !== myPlayer
|
||||
? html`<button
|
||||
@click=${(e) => this.handleEmbargoClick(e, myPlayer, other)}
|
||||
class="w-100 h-10 flex items-center justify-center
|
||||
@@ -402,7 +404,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
${translateText("player_panel.stop_trade")}
|
||||
</button>`
|
||||
: ""}
|
||||
${!canEmbargo && other != myPlayer
|
||||
${!canEmbargo && other !== myPlayer
|
||||
? html`<button
|
||||
@click=${(e) =>
|
||||
this.handleStopEmbargoClick(e, myPlayer, other)}
|
||||
|
||||
@@ -52,7 +52,16 @@ export class RadialMenu implements Layer {
|
||||
private originalTileOwner: PlayerView | TerraNullius;
|
||||
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
|
||||
private isVisible: boolean = false;
|
||||
private readonly menuItems = new Map([
|
||||
private readonly menuItems: Map<
|
||||
Slot,
|
||||
{
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
action: () => void;
|
||||
color?: string | null;
|
||||
icon?: string | null;
|
||||
}
|
||||
> = new Map([
|
||||
[
|
||||
Slot.Boat,
|
||||
{
|
||||
@@ -105,7 +114,7 @@ export class RadialMenu implements Layer {
|
||||
e.x,
|
||||
e.y,
|
||||
);
|
||||
if (clickedCell == null) {
|
||||
if (clickedCell === null) {
|
||||
return;
|
||||
}
|
||||
if (!this.g.isValidCoord(clickedCell.x, clickedCell.y)) {
|
||||
@@ -113,7 +122,7 @@ export class RadialMenu implements Layer {
|
||||
}
|
||||
const tile = this.g.ref(clickedCell.x, clickedCell.y);
|
||||
const p = this.g.playerByClientID(this.clientID);
|
||||
if (p == null) {
|
||||
if (p === null) {
|
||||
return;
|
||||
}
|
||||
this.buildMenu.showMenu(tile);
|
||||
@@ -280,12 +289,12 @@ export class RadialMenu implements Layer {
|
||||
if (myPlayer === null || !myPlayer.isAlive()) return;
|
||||
const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y);
|
||||
if (this.originalTileOwner.isPlayer()) {
|
||||
if (this.g.owner(tile) != this.originalTileOwner) {
|
||||
if (this.g.owner(tile) !== this.originalTileOwner) {
|
||||
this.closeMenu();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this.g.owner(tile).isPlayer() || this.g.owner(tile) == myPlayer) {
|
||||
if (this.g.owner(tile).isPlayer() || this.g.owner(tile) === myPlayer) {
|
||||
this.closeMenu();
|
||||
return;
|
||||
}
|
||||
@@ -383,7 +392,7 @@ export class RadialMenu implements Layer {
|
||||
});
|
||||
}
|
||||
if (
|
||||
actions.buildableUnits.find((bu) => bu.type == UnitType.TransportShip)
|
||||
actions.buildableUnits.find((bu) => bu.type === UnitType.TransportShip)
|
||||
?.canBuild
|
||||
) {
|
||||
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
|
||||
@@ -395,6 +404,7 @@ export class RadialMenu implements Layer {
|
||||
spawnTile = new Cell(this.g.x(spawn), this.g.y(spawn));
|
||||
}
|
||||
|
||||
if (this.clickedCell === null) return;
|
||||
this.eventBus.emit(
|
||||
new SendBoatAttackIntentEvent(
|
||||
this.g.owner(tile).id(),
|
||||
@@ -446,12 +456,13 @@ export class RadialMenu implements Layer {
|
||||
return;
|
||||
}
|
||||
consolex.log("Center button clicked");
|
||||
if (this.clickedCell === null) return;
|
||||
const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y);
|
||||
if (this.g.inSpawnPhase()) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell));
|
||||
} else {
|
||||
const myPlayer = this.g.myPlayer();
|
||||
if (myPlayer != null && this.g.owner(clicked) != myPlayer) {
|
||||
if (myPlayer !== null && this.g.owner(clicked) !== myPlayer) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.g.owner(clicked).id(),
|
||||
@@ -478,6 +489,7 @@ export class RadialMenu implements Layer {
|
||||
action: () => void,
|
||||
) {
|
||||
const menuItem = this.menuItems.get(slot);
|
||||
if (menuItem === undefined) return;
|
||||
menuItem.action = action;
|
||||
menuItem.disabled = false;
|
||||
menuItem.color = color;
|
||||
|
||||
@@ -24,13 +24,14 @@ export class SpawnTimer implements Layer {
|
||||
this.ratios = [];
|
||||
this.colors = [];
|
||||
|
||||
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
|
||||
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
|
||||
return;
|
||||
}
|
||||
|
||||
const teamTiles: Map<Team, number> = new Map();
|
||||
for (const player of this.game.players()) {
|
||||
const team = player.team();
|
||||
if (team === null) throw new Error("Team is null");
|
||||
const tiles = teamTiles.get(team) ?? 0;
|
||||
const sum = tiles + player.numTilesOwned();
|
||||
teamTiles.set(team, sum);
|
||||
|
||||
@@ -43,7 +43,7 @@ export class StructureLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private unitIcons: Map<string, ImageData> = new Map();
|
||||
private theme: Theme = null;
|
||||
private theme: Theme;
|
||||
|
||||
// Configuration for supported unit types only
|
||||
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
|
||||
@@ -106,6 +106,7 @@ export class StructureLayer implements Layer {
|
||||
// Create temporary canvas for icon processing
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const tempContext = tempCanvas.getContext("2d");
|
||||
if (tempContext === null) throw new Error("2d context not supported");
|
||||
tempCanvas.width = image.width;
|
||||
tempCanvas.height = image.height;
|
||||
|
||||
@@ -135,11 +136,13 @@ export class StructureLayer implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
[
|
||||
GameUpdateType.Unit
|
||||
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
||||
for (const u of unitUpdates) {
|
||||
const unit = this.game.unit(u.id);
|
||||
if (unit === undefined) continue;
|
||||
this.handleUnitRendering(unit);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -149,7 +152,9 @@ export class StructureLayer implements Layer {
|
||||
redraw() {
|
||||
console.log("structure layer redrawing");
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d", { alpha: true });
|
||||
const context = this.canvas.getContext("2d", { alpha: true });
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.game.units().forEach((u) => this.handleUnitRendering(u));
|
||||
@@ -193,7 +198,7 @@ export class StructureLayer implements Layer {
|
||||
)) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
unit.type() == UnitType.Construction
|
||||
unit.type() === UnitType.Construction
|
||||
? underConstructionColor
|
||||
: this.theme.territoryColor(unit.owner()),
|
||||
130,
|
||||
@@ -220,15 +225,15 @@ export class StructureLayer implements Layer {
|
||||
if (!this.isUnitTypeSupported(unitType)) return;
|
||||
|
||||
const config = this.unitConfigs[unitType];
|
||||
let icon: ImageData;
|
||||
let icon: ImageData | undefined;
|
||||
|
||||
if (unitType == UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
icon = this.unitIcons.get("reloadingSam");
|
||||
} else {
|
||||
icon = this.unitIcons.get(iconType);
|
||||
}
|
||||
|
||||
if (unitType == UnitType.MissileSilo && unit.isCooldown()) {
|
||||
if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
|
||||
icon = this.unitIcons.get("reloadingSilo");
|
||||
} else {
|
||||
icon = this.unitIcons.get(iconType);
|
||||
@@ -248,15 +253,15 @@ export class StructureLayer implements Layer {
|
||||
if (!unit.isActive()) return;
|
||||
|
||||
let borderColor = this.theme.borderColor(unit.owner());
|
||||
if (unitType == UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
|
||||
borderColor = reloadingColor;
|
||||
} else if (unit.type() == UnitType.Construction) {
|
||||
} else if (unit.type() === UnitType.Construction) {
|
||||
borderColor = underConstructionColor;
|
||||
}
|
||||
|
||||
if (unitType == UnitType.MissileSilo && unit.isCooldown()) {
|
||||
if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
|
||||
borderColor = reloadingColor;
|
||||
} else if (unit.type() == UnitType.Construction) {
|
||||
} else if (unit.type() === UnitType.Construction) {
|
||||
borderColor = underConstructionColor;
|
||||
}
|
||||
|
||||
@@ -277,7 +282,7 @@ export class StructureLayer implements Layer {
|
||||
unit: UnitView,
|
||||
) {
|
||||
let color = this.theme.borderColor(unit.owner());
|
||||
if (unit.type() == UnitType.Construction) {
|
||||
if (unit.type() === UnitType.Construction) {
|
||||
color = underConstructionColor;
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
init() {}
|
||||
|
||||
tick() {
|
||||
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
|
||||
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
const grouped: Record<number, PlayerView[]> = {};
|
||||
for (const player of players) {
|
||||
const team = player.team();
|
||||
if (team === null) continue;
|
||||
if (!grouped[team]) grouped[team] = [];
|
||||
grouped[team].push(player);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Theme } from "../../../core/configuration/Config";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
|
||||
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
|
||||
import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { AlternateViewEvent, DragEvent } from "../../InputHandler";
|
||||
@@ -22,7 +22,7 @@ export class TerritoryLayer implements Layer {
|
||||
return a.lastUpdate - b.lastUpdate;
|
||||
});
|
||||
private random = new PseudoRandom(123);
|
||||
private theme: Theme = null;
|
||||
private theme: Theme;
|
||||
|
||||
// Used for spawn highlighting
|
||||
private highlightCanvas: HTMLCanvasElement;
|
||||
@@ -58,17 +58,18 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
tick() {
|
||||
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Unit].forEach((u) => {
|
||||
const update = u as UnitUpdate;
|
||||
if (update.unitType == UnitType.DefensePost) {
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
||||
unitUpdates.forEach((update) => {
|
||||
if (update.unitType === UnitType.DefensePost) {
|
||||
const tile = update.pos;
|
||||
this.game
|
||||
.bfs(tile, euclDistFN(tile, this.game.config().defensePostRange()))
|
||||
.forEach((t) => {
|
||||
if (
|
||||
this.game.isBorder(t) &&
|
||||
(this.game.ownerID(t) == update.ownerID ||
|
||||
this.game.ownerID(t) == update.lastOwnerID)
|
||||
(this.game.ownerID(t) === update.ownerID ||
|
||||
this.game.ownerID(t) === update.lastOwnerID)
|
||||
) {
|
||||
this.enqueueTile(t);
|
||||
}
|
||||
@@ -90,7 +91,7 @@ export class TerritoryLayer implements Layer {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ export class TerritoryLayer implements Layer {
|
||||
);
|
||||
const humans = this.game
|
||||
.playerViews()
|
||||
.filter((p) => p.type() == PlayerType.Human);
|
||||
.filter((p) => p.type() === PlayerType.Human);
|
||||
|
||||
for (const human of humans) {
|
||||
const center = human.nameLocation();
|
||||
@@ -114,10 +115,11 @@ export class TerritoryLayer implements Layer {
|
||||
continue;
|
||||
}
|
||||
let color = this.theme.spawnHighlightColor();
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (
|
||||
this.game.myPlayer() != null &&
|
||||
this.game.myPlayer() != human &&
|
||||
this.game.myPlayer().isFriendly(human)
|
||||
myPlayer !== null &&
|
||||
myPlayer !== human &&
|
||||
myPlayer.isFriendly(human)
|
||||
) {
|
||||
color = this.theme.selfColor();
|
||||
}
|
||||
@@ -150,7 +152,9 @@ export class TerritoryLayer implements Layer {
|
||||
redraw() {
|
||||
console.log("redrew territory layer");
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
|
||||
this.imageData = this.context.getImageData(
|
||||
0,
|
||||
@@ -165,9 +169,11 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
// Add a second canvas for highlights
|
||||
this.highlightCanvas = document.createElement("canvas");
|
||||
this.highlightContext = this.highlightCanvas.getContext("2d", {
|
||||
const highlightContext = this.highlightCanvas.getContext("2d", {
|
||||
alpha: true,
|
||||
});
|
||||
if (highlightContext === null) throw new Error("2d context not supported");
|
||||
this.highlightContext = highlightContext;
|
||||
this.highlightCanvas.width = this.game.width();
|
||||
this.highlightCanvas.height = this.game.height();
|
||||
|
||||
@@ -219,7 +225,7 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
renderTerritory() {
|
||||
let numToRender = Math.floor(this.tileToRenderQueue.size() / 10);
|
||||
if (numToRender == 0 || this.game.inSpawnPhase()) {
|
||||
if (numToRender === 0 || this.game.inSpawnPhase()) {
|
||||
numToRender = this.tileToRenderQueue.size();
|
||||
}
|
||||
|
||||
@@ -252,7 +258,7 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
const owner = this.game.owner(tile) as PlayerView;
|
||||
if (this.game.isBorder(tile)) {
|
||||
const playerIsFocused = owner && this.game.focusedPlayer() == owner;
|
||||
const playerIsFocused = owner && this.game.focusedPlayer() === owner;
|
||||
if (
|
||||
this.game.hasUnitNearby(
|
||||
tile,
|
||||
@@ -265,7 +271,7 @@ export class TerritoryLayer implements Layer {
|
||||
const x = this.game.x(tile);
|
||||
const y = this.game.y(tile);
|
||||
const lightTile =
|
||||
(x % 2 == 0 && y % 2 == 0) || (y % 2 == 1 && x % 2 == 1);
|
||||
(x % 2 === 0 && y % 2 === 0) || (y % 2 === 1 && x % 2 === 1);
|
||||
const borderColor = lightTile ? borderColors.light : borderColors.dark;
|
||||
this.paintCell(x, y, borderColor, 255);
|
||||
} else {
|
||||
|
||||
@@ -23,18 +23,21 @@ export class TopBar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.game?.myPlayer() !== null) {
|
||||
const popIncreaseRate =
|
||||
this.game.myPlayer().population() - this._population;
|
||||
if (this.game.ticks() % 5 == 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
this.updatePopulationIncrease();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class UnitLayer implements Layer {
|
||||
|
||||
private unitToTrail = new Map<UnitView, TileRef[]>();
|
||||
|
||||
private theme: Theme = null;
|
||||
private theme: Theme;
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
@@ -67,7 +67,7 @@ export class UnitLayer implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.myPlayer == null) {
|
||||
if (this.myPlayer === null) {
|
||||
this.myPlayer = this.game.playerByClientID(this.clientID);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class UnitLayer implements Layer {
|
||||
const clickRef = this.game.ref(cell.x, cell.y);
|
||||
|
||||
// Make sure we have the current player
|
||||
if (this.myPlayer == null) {
|
||||
if (this.myPlayer === null) {
|
||||
this.myPlayer = this.game.playerByClientID(this.clientID);
|
||||
}
|
||||
|
||||
@@ -189,9 +189,13 @@ export class UnitLayer implements Layer {
|
||||
|
||||
redraw() {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (context === null) throw new Error("2d context not supported");
|
||||
this.context = context;
|
||||
this.transportShipTrailCanvas = document.createElement("canvas");
|
||||
this.unitTrailContext = this.transportShipTrailCanvas.getContext("2d");
|
||||
const trailContext = this.transportShipTrailCanvas.getContext("2d");
|
||||
if (trailContext === null) throw new Error("2d context not supported");
|
||||
this.unitTrailContext = trailContext;
|
||||
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
@@ -217,15 +221,13 @@ export class UnitLayer implements Layer {
|
||||
private updateUnitsSprites() {
|
||||
const unitsToUpdate = this.game
|
||||
.updatesSinceLastTick()
|
||||
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id));
|
||||
unitsToUpdate
|
||||
?.filter((UnitView) => isSpriteReady(UnitView.type()))
|
||||
.forEach((unitView) => {
|
||||
this.clearUnitCells(unitView);
|
||||
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
|
||||
?.forEach((unitView) => {
|
||||
if (unitView === undefined) return;
|
||||
const ready = isSpriteReady(unitView.type());
|
||||
if (ready) this.clearUnitCells(unitView);
|
||||
this.onUnitEvent(unitView);
|
||||
});
|
||||
unitsToUpdate?.forEach((unitView) => {
|
||||
this.onUnitEvent(unitView);
|
||||
});
|
||||
}
|
||||
|
||||
private clearUnitCells(unit: UnitView) {
|
||||
@@ -243,10 +245,10 @@ export class UnitLayer implements Layer {
|
||||
}
|
||||
|
||||
private relationship(unit: UnitView): Relationship {
|
||||
if (this.myPlayer == null) {
|
||||
if (this.myPlayer === null) {
|
||||
return Relationship.Enemy;
|
||||
}
|
||||
if (this.myPlayer == unit.owner()) {
|
||||
if (this.myPlayer === unit.owner()) {
|
||||
return Relationship.Self;
|
||||
}
|
||||
if (this.myPlayer.isFriendly(unit.owner())) {
|
||||
@@ -301,8 +303,8 @@ export class UnitLayer implements Layer {
|
||||
|
||||
// Clear current and previous positions
|
||||
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
|
||||
if (this.oldShellTile.has(unit)) {
|
||||
const oldTile = this.oldShellTile.get(unit);
|
||||
const oldTile = this.oldShellTile.get(unit);
|
||||
if (oldTile !== undefined) {
|
||||
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
|
||||
}
|
||||
|
||||
@@ -348,7 +350,7 @@ export class UnitLayer implements Layer {
|
||||
}
|
||||
|
||||
private clearTrail(unit: UnitView) {
|
||||
const trail = this.unitToTrail.get(unit);
|
||||
const trail = this.unitToTrail.get(unit) ?? [];
|
||||
const rel = this.relationship(unit);
|
||||
for (const t of trail) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t), this.unitTrailContext);
|
||||
@@ -381,7 +383,7 @@ export class UnitLayer implements Layer {
|
||||
}
|
||||
|
||||
let newTrailSize = 1;
|
||||
const trail = this.unitToTrail.get(unit);
|
||||
const trail = this.unitToTrail.get(unit) ?? [];
|
||||
// It can move faster than 1 pixel, draw a line for the trail or else it will be dotted
|
||||
if (trail.length >= 1) {
|
||||
const cur = {
|
||||
@@ -441,7 +443,7 @@ export class UnitLayer implements Layer {
|
||||
if (!this.unitToTrail.has(unit)) {
|
||||
this.unitToTrail.set(unit, []);
|
||||
}
|
||||
const trail = this.unitToTrail.get(unit);
|
||||
const trail = this.unitToTrail.get(unit) ?? [];
|
||||
trail.push(unit.lastTile());
|
||||
|
||||
// Paint trail
|
||||
@@ -496,15 +498,16 @@ export class UnitLayer implements Layer {
|
||||
const x = this.game.x(unit.tile());
|
||||
const y = this.game.y(unit.tile());
|
||||
|
||||
let alternateViewColor = null;
|
||||
let alternateViewColor: Colord | null = null;
|
||||
|
||||
if (this.alternateView) {
|
||||
let rel = this.relationship(unit);
|
||||
if (unit.type() == UnitType.TradeShip && unit.dstPortId() != null) {
|
||||
const target = this.game.unit(unit.dstPortId())?.owner();
|
||||
const dstPortId = unit.dstPortId();
|
||||
if (unit.type() === UnitType.TradeShip && dstPortId !== undefined) {
|
||||
const target = this.game.unit(dstPortId)?.owner();
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer != null && target != null) {
|
||||
if (myPlayer == target) {
|
||||
if (myPlayer !== null && target !== undefined) {
|
||||
if (myPlayer === target) {
|
||||
rel = Relationship.Self;
|
||||
} else if (myPlayer.isFriendly(target)) {
|
||||
rel = Relationship.Ally;
|
||||
@@ -528,7 +531,7 @@ export class UnitLayer implements Layer {
|
||||
unit,
|
||||
this.theme,
|
||||
alternateViewColor ?? customTerritoryColor,
|
||||
alternateViewColor,
|
||||
alternateViewColor ?? undefined,
|
||||
);
|
||||
|
||||
if (unit.isActive()) {
|
||||
|
||||
@@ -182,12 +182,14 @@ export class WinModal extends LitElement implements Layer {
|
||||
this._title = translateText("win_modal.died");
|
||||
this.show();
|
||||
}
|
||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
|
||||
winUpdates.forEach((wu) => {
|
||||
if (wu.winnerType === "team") {
|
||||
this.eventBus.emit(
|
||||
new SendWinnerEvent(wu.winner as Team, wu.allPlayersStats, "team"),
|
||||
);
|
||||
if (wu.winner == this.game.myPlayer()?.team()) {
|
||||
if (wu.winner === this.game.myPlayer()?.team()) {
|
||||
this._title = translateText("win_modal.your_team");
|
||||
} else {
|
||||
this._title = translateText("win_modal.other_team", {
|
||||
@@ -199,10 +201,13 @@ export class WinModal extends LitElement implements Layer {
|
||||
const winner = this.game.playerBySmallID(
|
||||
wu.winner as number,
|
||||
) as PlayerView;
|
||||
this.eventBus.emit(
|
||||
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"),
|
||||
);
|
||||
if (winner == this.game.myPlayer()) {
|
||||
const winnerClient = winner.clientID();
|
||||
if (winnerClient !== null) {
|
||||
this.eventBus.emit(
|
||||
new SendWinnerEvent(winnerClient, wu.allPlayersStats, "player"),
|
||||
);
|
||||
}
|
||||
if (winner === this.game.myPlayer()) {
|
||||
this._title = translateText("win_modal.you_won");
|
||||
} else {
|
||||
this._title = translateText("win_modal.you_won", {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const TokenPayloadSchema = z.object({
|
||||
.refine(
|
||||
(val) => {
|
||||
const uuid = base64urlToUuid(val);
|
||||
return uuid != null;
|
||||
return !!uuid;
|
||||
},
|
||||
{
|
||||
message: "Invalid base64-encoded UUID",
|
||||
@@ -31,7 +31,7 @@ export const TokenPayloadSchema = z.object({
|
||||
rol: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val.split(",")),
|
||||
.transform((val) => (val ?? "").split(",")),
|
||||
});
|
||||
export type TokenPayload = z.infer<typeof TokenPayloadSchema>;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function createGameRunner(
|
||||
(p) =>
|
||||
new PlayerInfo(
|
||||
p.flag,
|
||||
p.clientID == clientID
|
||||
p.clientID === clientID
|
||||
? sanitize(p.username)
|
||||
: fixProfaneUsername(sanitize(p.username)),
|
||||
PlayerType.Human,
|
||||
@@ -140,23 +140,25 @@ 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) {
|
||||
if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) {
|
||||
this.game
|
||||
.players()
|
||||
.filter(
|
||||
(p) =>
|
||||
p.type() == PlayerType.Human || p.type() == PlayerType.FakeHuman,
|
||||
p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman,
|
||||
)
|
||||
.forEach(
|
||||
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.game.ticks() < 3 || this.game.ticks() % 30 == 0) {
|
||||
if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) {
|
||||
this.game.players().forEach((p) => {
|
||||
this.playerViewData[p.id()] = placeName(this.game, p);
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export class PseudoRandom {
|
||||
* Selects a random element from an array.
|
||||
*/
|
||||
randElement<T>(arr: T[]): T {
|
||||
if (arr.length == 0) {
|
||||
if (arr.length === 0) {
|
||||
throw new Error("array must not be empty");
|
||||
}
|
||||
return arr[this.nextInt(0, arr.length)];
|
||||
@@ -101,7 +101,7 @@ export class PseudoRandom {
|
||||
* Returns true with probability 1/odds.
|
||||
*/
|
||||
chance(odds: number): boolean {
|
||||
return this.nextInt(0, odds) == 0;
|
||||
return this.nextInt(0, odds) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-5
@@ -124,7 +124,6 @@ const GameConfigSchema = z.object({
|
||||
infiniteTroops: z.boolean(),
|
||||
instantBuild: z.boolean(),
|
||||
maxPlayers: z.number().optional(),
|
||||
numPlayerTeams: z.number().optional(),
|
||||
disabledUnits: z.array(z.nativeEnum(UnitType)).optional(),
|
||||
playerTeams: z.union([z.number().optional(), z.literal(Duos)]),
|
||||
});
|
||||
@@ -226,11 +225,11 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("boat"),
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
troops: z.number(),
|
||||
dstX: z.number(),
|
||||
dstY: z.number(),
|
||||
srcX: z.number().nullable().optional(),
|
||||
srcY: z.number().nullable().optional(),
|
||||
srcX: z.number().nullable(),
|
||||
srcY: z.number().nullable(),
|
||||
});
|
||||
|
||||
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
|
||||
@@ -435,7 +434,7 @@ export const ClientJoinMessageSchema = z.object({
|
||||
gameID: ID,
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: SafeString,
|
||||
flag: SafeString.nullable().optional(),
|
||||
flag: SafeString.nullable(),
|
||||
});
|
||||
|
||||
export const ClientMessageSchema = z.union([
|
||||
|
||||
+11
-14
@@ -199,21 +199,26 @@ 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) {
|
||||
if (turn.intents.length != 0 || turn.hash != undefined) {
|
||||
if (turn.intents.length !== 0 || turn.hash !== undefined) {
|
||||
record.turns.push(turn);
|
||||
for (const intent of turn.intents) {
|
||||
if (intent.type == "spawn") {
|
||||
if (intent.type === "spawn") {
|
||||
for (const playerRecord of players) {
|
||||
if (playerRecord.clientID == intent.clientID) {
|
||||
if (playerRecord.clientID === intent.clientID) {
|
||||
playerRecord.username = intent.name;
|
||||
}
|
||||
}
|
||||
@@ -221,24 +226,17 @@ 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) {
|
||||
lastTurnNum++;
|
||||
turns.push({
|
||||
gameID: gameRecord.id,
|
||||
turnNumber: lastTurnNum,
|
||||
intents: [],
|
||||
});
|
||||
@@ -249,7 +247,6 @@ export function decompressGameRecord(gameRecord: GameRecord) {
|
||||
const turnLength = turns.length;
|
||||
for (let i = turnLength; i < gameRecord.num_turns; i++) {
|
||||
turns.push({
|
||||
gameID: gameRecord.id,
|
||||
turnNumber: i,
|
||||
intents: [],
|
||||
});
|
||||
@@ -296,7 +293,7 @@ export function createRandomName(
|
||||
name: string,
|
||||
playerType: string,
|
||||
): string | null {
|
||||
let randomName = null;
|
||||
let randomName: string | null = null;
|
||||
if (playerType === "HUMAN") {
|
||||
const hash = simpleHash(name);
|
||||
const prefixIndex = hash % BOT_NAME_PREFIXES.length;
|
||||
@@ -322,4 +319,4 @@ export const emojiTable: string[][] = [
|
||||
["💰", "⚓", "⛵", "🏡", "🛡️"],
|
||||
];
|
||||
// 2d to 1d array
|
||||
export const flattenedEmojiTable: string[] = [].concat(...emojiTable);
|
||||
export const flattenedEmojiTable: string[] = emojiTable.flat();
|
||||
|
||||
@@ -7,11 +7,11 @@ 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,
|
||||
userSettings: UserSettings | null = null,
|
||||
userSettings: UserSettings | null,
|
||||
isReplay: boolean = false,
|
||||
): Promise<Config> {
|
||||
const sc = await getServerConfigFromClient();
|
||||
@@ -45,7 +45,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) {
|
||||
|
||||
@@ -50,6 +50,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
async jwkPublicKey(): Promise<JWK> {
|
||||
if (this.publicKey) return this.publicKey;
|
||||
const jwksUrl = this.jwtIssuer() + "/.well-known/jwks.json";
|
||||
console.log(`Fetching JWKS from ${jwksUrl}`);
|
||||
const response = await fetch(jwksUrl);
|
||||
const jwks = JwksSchema.parse(await response.json());
|
||||
this.publicKey = jwks.keys[0];
|
||||
@@ -61,42 +62,42 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
);
|
||||
}
|
||||
otelEndpoint(): string {
|
||||
return process.env.OTEL_ENDPOINT;
|
||||
return process.env.OTEL_ENDPOINT ?? "undefined";
|
||||
}
|
||||
otelUsername(): string {
|
||||
return process.env.OTEL_USERNAME;
|
||||
return process.env.OTEL_USERNAME ?? "undefined";
|
||||
}
|
||||
otelPassword(): string {
|
||||
return process.env.OTEL_PASSWORD;
|
||||
return process.env.OTEL_PASSWORD ?? "undefined";
|
||||
}
|
||||
region(): string {
|
||||
if (this.env() == GameEnv.Dev) {
|
||||
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 `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
r2Bucket(): string {
|
||||
return process.env.R2_BUCKET;
|
||||
return process.env.R2_BUCKET ?? "undefined";
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -159,13 +160,13 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
return Math.random() < 0.5 ? 30 : 15;
|
||||
}
|
||||
// world belongs with the ~2 mils, but these amounts never made sense so I assume the insanity is intended.
|
||||
if (map == GameMapType.World) {
|
||||
if (map === GameMapType.World) {
|
||||
return Math.random() < 0.2 ? 150 : 50;
|
||||
}
|
||||
// default return for non specified map
|
||||
return Math.random() < 0.2 ? 50 : 20;
|
||||
};
|
||||
return Math.min(150, numPlayers() * (mode == GameMode.Team ? 2 : 1));
|
||||
return Math.min(150, numPlayers() * (mode === GameMode.Team ? 2 : 1));
|
||||
}
|
||||
|
||||
workerIndex(gameID: GameID): number {
|
||||
@@ -186,7 +187,7 @@ export class DefaultConfig implements Config {
|
||||
constructor(
|
||||
private _serverConfig: ServerConfig,
|
||||
private _gameConfig: GameConfig,
|
||||
private _userSettings: UserSettings,
|
||||
private _userSettings: UserSettings | null,
|
||||
private _isReplay: boolean,
|
||||
) {}
|
||||
isReplay(): boolean {
|
||||
@@ -219,7 +220,10 @@ export class DefaultConfig implements Config {
|
||||
return this._serverConfig;
|
||||
}
|
||||
|
||||
userSettings(): UserSettings | null {
|
||||
userSettings(): UserSettings {
|
||||
if (this._userSettings === null) {
|
||||
throw new Error("userSettings is null");
|
||||
}
|
||||
return this._userSettings;
|
||||
}
|
||||
|
||||
@@ -299,7 +303,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.Warship:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: Math.min(
|
||||
1_000_000,
|
||||
@@ -323,7 +327,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: Math.min(
|
||||
1_000_000,
|
||||
@@ -338,19 +342,21 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 750_000,
|
||||
p.type() === PlayerType.Human && this.infiniteGold() ? 0 : 750_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 5_000_000,
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: 5_000_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MIRV:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: 25_000_000,
|
||||
territoryBound: false,
|
||||
@@ -368,14 +374,16 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold() ? 0 : 1_000_000,
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: 1_000_000,
|
||||
territoryBound: true,
|
||||
constructionDuration: this.instantBuild() ? 0 : 10 * 10,
|
||||
};
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: Math.min(
|
||||
250_000,
|
||||
@@ -389,7 +397,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.SAMLauncher:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: Math.min(
|
||||
3_000_000,
|
||||
@@ -403,7 +411,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.infiniteGold()
|
||||
p.type() === PlayerType.Human && this.infiniteGold()
|
||||
? 0
|
||||
: Math.min(
|
||||
1_000_000,
|
||||
@@ -453,7 +461,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
percentageTilesOwnedToWin(): number {
|
||||
if (this._gameConfig.gameMode == GameMode.Team) {
|
||||
if (this._gameConfig.gameMode === GameMode.Team) {
|
||||
return 95;
|
||||
}
|
||||
return 80;
|
||||
@@ -462,13 +470,13 @@ export class DefaultConfig implements Config {
|
||||
return 3;
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300;
|
||||
return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300;
|
||||
}
|
||||
numBots(): number {
|
||||
return this.bots();
|
||||
}
|
||||
theme(): Theme {
|
||||
return this.userSettings().darkMode() ? pastelThemeDark : pastelTheme;
|
||||
return this.userSettings()?.darkMode() ? pastelThemeDark : pastelTheme;
|
||||
}
|
||||
|
||||
attackLogic(
|
||||
@@ -507,7 +515,7 @@ export class DefaultConfig implements Config {
|
||||
gm.config().defensePostRange(),
|
||||
UnitType.DefensePost,
|
||||
)) {
|
||||
if (dp.unit.owner() == defender) {
|
||||
if (dp.unit.owner() === defender) {
|
||||
mag *= this.defensePostDefenseBonus();
|
||||
speed *= this.defensePostDefenseBonus();
|
||||
break;
|
||||
@@ -523,14 +531,14 @@ export class DefaultConfig implements Config {
|
||||
|
||||
if (attacker.isPlayer() && defender.isPlayer()) {
|
||||
if (
|
||||
attacker.type() == PlayerType.Human &&
|
||||
defender.type() == PlayerType.Bot
|
||||
attacker.type() === PlayerType.Human &&
|
||||
defender.type() === PlayerType.Bot
|
||||
) {
|
||||
mag *= 0.8;
|
||||
}
|
||||
if (
|
||||
attacker.type() == PlayerType.FakeHuman &&
|
||||
defender.type() == PlayerType.Bot
|
||||
attacker.type() === PlayerType.FakeHuman &&
|
||||
defender.type() === PlayerType.Bot
|
||||
) {
|
||||
mag *= 0.8;
|
||||
}
|
||||
@@ -563,7 +571,7 @@ export class DefaultConfig implements Config {
|
||||
} else {
|
||||
return {
|
||||
attackerTroopLoss:
|
||||
attacker.type() == PlayerType.Bot ? mag / 10 : mag / 5,
|
||||
attacker.type() === PlayerType.Bot ? mag / 10 : mag / 5,
|
||||
defenderTroopLoss: 0,
|
||||
tilesPerTickUsed: within(
|
||||
(2000 * Math.max(10, speed)) / attackTroops,
|
||||
@@ -608,7 +616,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
attackAmount(attacker: Player, defender: Player | TerraNullius) {
|
||||
if (attacker.type() == PlayerType.Bot) {
|
||||
if (attacker.type() === PlayerType.Bot) {
|
||||
return attacker.troops() / 20;
|
||||
} else {
|
||||
return attacker.troops() / 5;
|
||||
@@ -616,10 +624,10 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number {
|
||||
if (playerInfo.playerType == PlayerType.Bot) {
|
||||
if (playerInfo.playerType === PlayerType.Bot) {
|
||||
return 10_000;
|
||||
}
|
||||
if (playerInfo.playerType == PlayerType.FakeHuman) {
|
||||
if (playerInfo.playerType === PlayerType.FakeHuman) {
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return 2_500 * (playerInfo?.nation?.strength ?? 1);
|
||||
@@ -636,16 +644,16 @@ export class DefaultConfig implements Config {
|
||||
|
||||
maxPopulation(player: Player | PlayerView): number {
|
||||
const maxPop =
|
||||
player.type() == PlayerType.Human && this.infiniteTroops()
|
||||
player.type() === PlayerType.Human && this.infiniteTroops()
|
||||
? 1_000_000_000
|
||||
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease();
|
||||
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
return maxPop / 2;
|
||||
}
|
||||
|
||||
if (player.type() == PlayerType.Human) {
|
||||
if (player.type() === PlayerType.Human) {
|
||||
return maxPop;
|
||||
}
|
||||
|
||||
@@ -669,11 +677,11 @@ export class DefaultConfig implements Config {
|
||||
const ratio = 1 - player.population() / max;
|
||||
toAdd *= ratio;
|
||||
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
toAdd *= 0.7;
|
||||
}
|
||||
|
||||
if (player.type() == PlayerType.FakeHuman) {
|
||||
if (player.type() === PlayerType.FakeHuman) {
|
||||
switch (this._gameConfig.difficulty) {
|
||||
case Difficulty.Easy:
|
||||
toAdd *= 0.9;
|
||||
@@ -721,6 +729,7 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.HydrogenBomb:
|
||||
return { inner: 80, outer: 100 };
|
||||
}
|
||||
throw new Error(`Unknown nuke type: ${unitType}`);
|
||||
}
|
||||
|
||||
defaultNukeSpeed(): number {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DevConfig extends DefaultConfig {
|
||||
constructor(
|
||||
sc: ServerConfig,
|
||||
gc: GameConfig,
|
||||
us: UserSettings,
|
||||
us: UserSettings | null,
|
||||
isReplay: boolean,
|
||||
) {
|
||||
super(sc, gc, us, isReplay);
|
||||
|
||||
@@ -65,20 +65,23 @@ export const pastelTheme = new (class implements Theme {
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
if (player.team() !== null) {
|
||||
return this.teamColor(player.team());
|
||||
const team = player.team();
|
||||
if (team !== null) {
|
||||
return this.teamColor(team);
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Human) {
|
||||
if (player.info().playerType === PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Bot) {
|
||||
if (player.info().playerType === PlayerType.Bot) {
|
||||
return botColors[simpleHash(player.id()) % botColors.length];
|
||||
}
|
||||
return territoryColors[simpleHash(player.id()) % territoryColors.length];
|
||||
}
|
||||
|
||||
textColor(player: PlayerView): string {
|
||||
return player.info().playerType == PlayerType.Human ? "#000000" : "#4D4D4D";
|
||||
return player.info().playerType === PlayerType.Human
|
||||
? "#000000"
|
||||
: "#4D4D4D";
|
||||
}
|
||||
|
||||
specialBuildingColor(player: PlayerView): Colord {
|
||||
|
||||
@@ -65,20 +65,23 @@ export const pastelThemeDark = new (class implements Theme {
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
if (player.team() !== null) {
|
||||
return this.teamColor(player.team());
|
||||
const team = player.team();
|
||||
if (team !== null) {
|
||||
return this.teamColor(team);
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Human) {
|
||||
if (player.info().playerType === PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Bot) {
|
||||
if (player.info().playerType === PlayerType.Bot) {
|
||||
return botColors[simpleHash(player.id()) % botColors.length];
|
||||
}
|
||||
return territoryColors[simpleHash(player.id()) % territoryColors.length];
|
||||
}
|
||||
|
||||
textColor(player: PlayerView): string {
|
||||
return player.info().playerType == PlayerType.Human ? "#ffffff" : "#e6e6e6";
|
||||
return player.info().playerType === PlayerType.Human
|
||||
? "#ffffff"
|
||||
: "#e6e6e6";
|
||||
}
|
||||
|
||||
specialBuildingColor(player: PlayerView): Colord {
|
||||
|
||||
@@ -32,7 +32,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,
|
||||
@@ -42,7 +42,7 @@ export class AttackExecution implements Execution {
|
||||
private removeTroops: boolean = true,
|
||||
) {}
|
||||
|
||||
public targetID(): PlayerID {
|
||||
public targetID(): PlayerID | null {
|
||||
return this._targetID;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class AttackExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this._targetID != null && !mg.hasPlayer(this._targetID)) {
|
||||
if (this._targetID !== null && !mg.hasPlayer(this._targetID)) {
|
||||
console.warn(`target ${this._targetID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -69,22 +69,22 @@ export class AttackExecution implements Execution {
|
||||
|
||||
this._owner = mg.player(this._ownerID);
|
||||
this.target =
|
||||
this._targetID == this.mg.terraNullius().id()
|
||||
this._targetID === this.mg.terraNullius().id()
|
||||
? mg.terraNullius()
|
||||
: mg.player(this._targetID);
|
||||
|
||||
if (this.target && this.target.isPlayer()) {
|
||||
const targetPlayer = this.target as Player;
|
||||
if (
|
||||
targetPlayer.type() != PlayerType.Bot &&
|
||||
this._owner.type() != PlayerType.Bot
|
||||
targetPlayer.type() !== PlayerType.Bot &&
|
||||
this._owner.type() !== PlayerType.Bot
|
||||
) {
|
||||
// Don't let bots embargo since they can't trade anyway.
|
||||
targetPlayer.addEmbargo(this._owner.id(), true);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._owner == this.target) {
|
||||
if (this._owner === this.target) {
|
||||
console.error(`Player ${this._owner} cannot attack itself`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -101,7 +101,7 @@ export class AttackExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.startTroops == null) {
|
||||
if (this.startTroops === null) {
|
||||
this.startTroops = this.mg
|
||||
.config()
|
||||
.attackAmount(this._owner, this.target);
|
||||
@@ -117,7 +117,7 @@ export class AttackExecution implements Execution {
|
||||
);
|
||||
|
||||
for (const incoming of this._owner.incomingAttacks()) {
|
||||
if (incoming.attacker() == this.target) {
|
||||
if (incoming.attacker() === this.target) {
|
||||
// Target has opposing attack, cancel them out
|
||||
if (incoming.troops() > this.attack.troops()) {
|
||||
incoming.setTroops(incoming.troops() - this.attack.troops());
|
||||
@@ -132,9 +132,9 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
for (const outgoing of this._owner.outgoingAttacks()) {
|
||||
if (
|
||||
outgoing != this.attack &&
|
||||
outgoing.target() == this.attack.target() &&
|
||||
outgoing.sourceTile() == this.attack.sourceTile()
|
||||
outgoing !== this.attack &&
|
||||
outgoing.target() === this.attack.target() &&
|
||||
outgoing.sourceTile() === this.attack.sourceTile()
|
||||
) {
|
||||
// Existing attack on same target, add troops
|
||||
outgoing.setTroops(outgoing.troops() + this.attack.troops());
|
||||
@@ -144,7 +144,7 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sourceTile != null) {
|
||||
if (this.sourceTile !== null) {
|
||||
this.addNeighbors(this.sourceTile);
|
||||
} else {
|
||||
this.refreshToConquer();
|
||||
@@ -168,6 +168,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(
|
||||
@@ -182,6 +186,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);
|
||||
@@ -202,7 +210,7 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
|
||||
const alliance = this._owner.allianceWith(this.target as Player);
|
||||
if (this.breakAlliance && alliance != null) {
|
||||
if (this.breakAlliance && alliance !== null) {
|
||||
this.breakAlliance = false;
|
||||
this._owner.breakAlliance(alliance);
|
||||
}
|
||||
@@ -228,7 +236,7 @@ export class AttackExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.toConquer.size() == 0) {
|
||||
if (this.toConquer.size() === 0) {
|
||||
this.refreshToConquer();
|
||||
this.retreat();
|
||||
return;
|
||||
@@ -240,8 +248,8 @@ export class AttackExecution implements Execution {
|
||||
const onBorder =
|
||||
this.mg
|
||||
.neighbors(tileToConquer)
|
||||
.filter((t) => this.mg.owner(t) == this._owner).length > 0;
|
||||
if (this.mg.owner(tileToConquer) != this.target || !onBorder) {
|
||||
.filter((t) => this.mg.owner(t) === this._owner).length > 0;
|
||||
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
|
||||
continue;
|
||||
}
|
||||
this.addNeighbors(tileToConquer);
|
||||
@@ -266,13 +274,16 @@ export class AttackExecution implements Execution {
|
||||
|
||||
private addNeighbors(tile: TileRef) {
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) {
|
||||
if (
|
||||
this.mg.isWater(neighbor) ||
|
||||
this.mg.owner(neighbor) !== this.target
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
this.border.add(neighbor);
|
||||
const numOwnedByMe = this.mg
|
||||
.neighbors(neighbor)
|
||||
.filter((t) => this.mg.owner(t) == this._owner).length;
|
||||
.filter((t) => this.mg.owner(t) === this._owner).length;
|
||||
let mag = 0;
|
||||
switch (this.mg.terrainType(tile)) {
|
||||
case TerrainType.Plains:
|
||||
@@ -314,13 +325,13 @@ export class AttackExecution implements Execution {
|
||||
for (const tile of this.target.tiles()) {
|
||||
const borders = this.mg
|
||||
.neighbors(tile)
|
||||
.some((t) => this.mg.owner(t) == this._owner);
|
||||
.some((t) => this.mg.owner(t) === this._owner);
|
||||
if (borders) {
|
||||
this._owner.conquer(tile);
|
||||
} else {
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
const no = this.mg.owner(neighbor);
|
||||
if (no.isPlayer() && no != this.target) {
|
||||
if (no.isPlayer() && no !== this.target) {
|
||||
this.mg.player(no.id()).conquer(tile);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class BotExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate != this.attackTick) return;
|
||||
if (ticks % this.attackRate !== this.attackTick) return;
|
||||
|
||||
if (!this.bot.isAlive()) {
|
||||
this.active = false;
|
||||
@@ -55,6 +55,9 @@ export class BotExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeAttack() {
|
||||
if (this.behavior === null) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
const traitors = this.bot
|
||||
.neighbors()
|
||||
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
|
||||
|
||||
@@ -27,7 +27,7 @@ export class BotSpawner {
|
||||
}
|
||||
const botName = this.randomBotName();
|
||||
const spawn = this.spawnBot(botName);
|
||||
if (spawn != null) {
|
||||
if (spawn !== null) {
|
||||
this.bots.push(spawn);
|
||||
} else {
|
||||
tries++;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TileRef } from "../game/GameMap";
|
||||
export class CityExecution implements Execution {
|
||||
private player: Player;
|
||||
private mg: Game;
|
||||
private city: Unit;
|
||||
private city: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
|
||||
constructor(
|
||||
@@ -31,9 +31,9 @@ export class CityExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.city == null) {
|
||||
if (this.city === null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.City, this.tile);
|
||||
if (spawnTile == false) {
|
||||
if (spawnTile === false) {
|
||||
consolex.warn("cannot build city");
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -45,7 +45,7 @@ export class CityExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player != this.city.owner()) {
|
||||
if (this.player !== this.city.owner()) {
|
||||
this.player = this.city.owner();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { WarshipExecution } from "./WarshipExecution";
|
||||
|
||||
export class ConstructionExecution implements Execution {
|
||||
private player: Player;
|
||||
private construction: Unit;
|
||||
private construction: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private mg: Game;
|
||||
|
||||
@@ -45,15 +45,15 @@ export class ConstructionExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.construction == null) {
|
||||
if (this.construction === null) {
|
||||
const info = this.mg.unitInfo(this.constructionType);
|
||||
if (info.constructionDuration == null) {
|
||||
if (info.constructionDuration === undefined) {
|
||||
this.completeConstruction();
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
const spawnTile = this.player.canBuild(this.constructionType, this.tile);
|
||||
if (spawnTile == false) {
|
||||
if (spawnTile === false) {
|
||||
consolex.warn(`cannot build ${this.constructionType}`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -75,11 +75,11 @@ export class ConstructionExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player != this.construction.owner()) {
|
||||
if (this.player !== this.construction.owner()) {
|
||||
this.player = this.construction.owner();
|
||||
}
|
||||
|
||||
if (this.ticksUntilComplete == 0) {
|
||||
if (this.ticksUntilComplete === 0) {
|
||||
this.player = this.construction.owner();
|
||||
this.construction.delete(false);
|
||||
// refund the cost so player has the gold to build the unit
|
||||
|
||||
@@ -13,10 +13,10 @@ import { ShellExecution } from "./ShellExecution";
|
||||
export class DefensePostExecution implements Execution {
|
||||
private player: Player;
|
||||
private mg: Game;
|
||||
private post: Unit;
|
||||
private post: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
|
||||
private target: Unit = null;
|
||||
private target: Unit | null = null;
|
||||
private lastShellAttack = 0;
|
||||
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
@@ -37,6 +37,8 @@ export class DefensePostExecution implements Execution {
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
if (this.post === null) return;
|
||||
if (this.target === null) return;
|
||||
const shellAttackRate = this.mg.config().defensePostShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
@@ -58,9 +60,9 @@ export class DefensePostExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.post == null) {
|
||||
if (this.post === null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile);
|
||||
if (spawnTile == false) {
|
||||
if (spawnTile === false) {
|
||||
consolex.warn("cannot build Defense Post");
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -72,58 +74,57 @@ export class DefensePostExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player != this.post.owner()) {
|
||||
if (this.player !== this.post.owner()) {
|
||||
this.player = this.post.owner();
|
||||
}
|
||||
|
||||
if (this.target != null && !this.target.isActive()) {
|
||||
if (this.target !== null && !this.target.isActive()) {
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
// TODO: Reconsider how/if defense posts target ships.
|
||||
return;
|
||||
|
||||
const ships = this.mg
|
||||
.nearbyUnits(
|
||||
this.post.tile(),
|
||||
this.mg.config().defensePostTargettingRange(),
|
||||
[UnitType.TransportShip, UnitType.Warship],
|
||||
)
|
||||
.filter(
|
||||
({ unit }) =>
|
||||
unit.owner() !== this.post.owner() &&
|
||||
!unit.owner().isFriendly(this.post.owner()) &&
|
||||
!this.alreadySentShell.has(unit),
|
||||
);
|
||||
|
||||
this.target =
|
||||
ships.sort((a, b) => {
|
||||
const { unit: unitA, distSquared: distA } = a;
|
||||
const { unit: unitB, distSquared: distB } = b;
|
||||
|
||||
// Prioritize TransportShip
|
||||
if (
|
||||
unitA.type() === UnitType.TransportShip &&
|
||||
unitB.type() !== UnitType.TransportShip
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
unitA.type() !== UnitType.TransportShip &&
|
||||
unitB.type() === UnitType.TransportShip
|
||||
)
|
||||
return 1;
|
||||
|
||||
// If both are the same type, sort by distance (lower `distSquared` means closer)
|
||||
return distA - distB;
|
||||
})[0]?.unit ?? null;
|
||||
|
||||
if (this.target == null || !this.target.isActive()) {
|
||||
this.target = null;
|
||||
return;
|
||||
} else {
|
||||
this.shoot();
|
||||
return;
|
||||
}
|
||||
// const ships = this.mg
|
||||
// .nearbyUnits(
|
||||
// this.post.tile(),
|
||||
// this.mg.config().defensePostTargettingRange(),
|
||||
// [UnitType.TransportShip, UnitType.Warship],
|
||||
// )
|
||||
// .filter(
|
||||
// ({ unit }) =>
|
||||
// this.post !== null &&
|
||||
// unit.owner() !== this.post.owner() &&
|
||||
// !unit.owner().isFriendly(this.post.owner()) &&
|
||||
// !this.alreadySentShell.has(unit),
|
||||
// );
|
||||
//
|
||||
// this.target =
|
||||
// ships.sort((a, b) => {
|
||||
// const { unit: unitA, distSquared: distA } = a;
|
||||
// const { unit: unitB, distSquared: distB } = b;
|
||||
//
|
||||
// // Prioritize TransportShip
|
||||
// if (
|
||||
// unitA.type() === UnitType.TransportShip &&
|
||||
// unitB.type() !== UnitType.TransportShip
|
||||
// )
|
||||
// return -1;
|
||||
// if (
|
||||
// unitA.type() !== UnitType.TransportShip &&
|
||||
// unitB.type() === UnitType.TransportShip
|
||||
// )
|
||||
// return 1;
|
||||
//
|
||||
// // If both are the same type, sort by distance (lower `distSquared` means closer)
|
||||
// return distA - distB;
|
||||
// })[0]?.unit ?? null;
|
||||
//
|
||||
// if (this.target === null || !this.target.isActive()) {
|
||||
// this.target = null;
|
||||
// return;
|
||||
// } else {
|
||||
// this.shoot();
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
|
||||
@@ -27,12 +27,13 @@ export class DonateGoldExecution implements Execution {
|
||||
|
||||
this.sender = mg.player(this.senderID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
if (this.gold == null) {
|
||||
if (this.gold === null) {
|
||||
this.gold = Math.round(this.sender.gold() / 3);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ export class DonateTroopsExecution implements Execution {
|
||||
|
||||
this.sender = mg.player(this.senderID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
if (this.troops == null) {
|
||||
if (this.troops === null) {
|
||||
this.troops = mg.config().defaultDonationAmount(this.sender);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -23,16 +23,12 @@ export class EmbargoExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(_: number): void {
|
||||
if (this.action == "start") this.player.addEmbargo(this.targetID, false);
|
||||
if (this.action === "start") this.player.addEmbargo(this.targetID, false);
|
||||
else this.player.stopEmbargo(this.targetID);
|
||||
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export class EmojiExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.recipientID != AllPlayers && !mg.hasPlayer(this.recipientID)) {
|
||||
if (this.recipientID !== AllPlayers && !mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(`EmojiExecution: recipient ${this.recipientID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -35,18 +35,23 @@ export class EmojiExecution implements Execution {
|
||||
|
||||
this.requestor = mg.player(this.senderID);
|
||||
this.recipient =
|
||||
this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID);
|
||||
this.recipientID === AllPlayers
|
||||
? AllPlayers
|
||||
: mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
const emojiString = flattenedEmojiTable.at(this.emoji);
|
||||
|
||||
if (this.requestor.canSendEmoji(this.recipient)) {
|
||||
if (emojiString === undefined) {
|
||||
consolex.warn(
|
||||
`cannot send emoji ${this.emoji} from ${this.requestor} to ${this.recipient}`,
|
||||
);
|
||||
} else if (this.requestor.canSendEmoji(this.recipient)) {
|
||||
this.requestor.sendEmoji(this.recipient, emojiString);
|
||||
if (
|
||||
emojiString == "🖕" &&
|
||||
this.recipient != AllPlayers &&
|
||||
this.recipient.type() == PlayerType.FakeHuman
|
||||
emojiString === "🖕" &&
|
||||
this.recipient !== AllPlayers &&
|
||||
this.recipient.type() === PlayerType.FakeHuman
|
||||
) {
|
||||
this.recipient.updateRelation(this.requestor, -100);
|
||||
}
|
||||
@@ -58,10 +63,6 @@ export class EmojiExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Execution, Game } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ClientID, GameID, Intent, Turn } from "../Schemas";
|
||||
import { simpleHash } from "../Util";
|
||||
@@ -24,7 +25,7 @@ import { TransportShipExecution } from "./TransportShipExecution";
|
||||
|
||||
export class Executor {
|
||||
// private random = new PseudoRandom(999)
|
||||
private random: PseudoRandom = null;
|
||||
private random: PseudoRandom;
|
||||
|
||||
constructor(
|
||||
private mg: Game,
|
||||
@@ -66,8 +67,8 @@ export class Executor {
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case "boat":
|
||||
let src = null;
|
||||
if (intent.srcX != null || intent.srcY != null) {
|
||||
let src: TileRef | null = null;
|
||||
if (intent.srcX !== null && intent.srcY !== null) {
|
||||
src = this.mg.ref(intent.srcX, intent.srcY);
|
||||
}
|
||||
return new TransportShipExecution(
|
||||
@@ -126,7 +127,7 @@ export class Executor {
|
||||
}
|
||||
|
||||
fakeHumanExecutions(): Execution[] {
|
||||
const execs = [];
|
||||
const execs: Execution[] = [];
|
||||
for (const nation of this.mg.nations()) {
|
||||
execs.push(new FakeHumanExecution(this.gameID, nation));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class FakeHumanExecution implements Execution {
|
||||
private random: PseudoRandom;
|
||||
private behavior: BotBehavior | null = null;
|
||||
private mg: Game;
|
||||
private player: Player = null;
|
||||
private player: Player | null = null;
|
||||
|
||||
private attackRate: number;
|
||||
private attackTick: number;
|
||||
@@ -67,51 +67,55 @@ 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(), false);
|
||||
player.addEmbargo(other.id(), false);
|
||||
} 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % this.attackRate != this.attackTick) return;
|
||||
if (ticks % this.attackRate !== this.attackTick) return;
|
||||
|
||||
if (this.mg.inSpawnPhase()) {
|
||||
const rl = this.randomLand();
|
||||
if (rl == null) {
|
||||
if (rl === null) {
|
||||
consolex.warn(`cannot spawn ${this.nation.playerInfo.name}`);
|
||||
return;
|
||||
}
|
||||
@@ -119,11 +123,11 @@ export class FakeHumanExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player == null) {
|
||||
this.player = this.mg
|
||||
.players()
|
||||
.find((p) => p.id() == this.nation.playerInfo.id);
|
||||
if (this.player == null) {
|
||||
if (this.player === null) {
|
||||
this.player =
|
||||
this.mg.players().find((p) => p.id() === this.nation.playerInfo.id) ??
|
||||
null;
|
||||
if (this.player === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -166,13 +170,17 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeAttack() {
|
||||
if (this.player === null || this.behavior === null) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
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) {
|
||||
if (enemyborder.length === 0) {
|
||||
if (this.random.chance(10)) {
|
||||
this.sendBoatRandomly();
|
||||
}
|
||||
@@ -214,6 +222,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;
|
||||
}
|
||||
@@ -235,10 +244,13 @@ export class FakeHumanExecution implements Execution {
|
||||
return false;
|
||||
}
|
||||
const difficulty = this.mg.config().gameConfig().difficulty;
|
||||
if (difficulty == Difficulty.Hard || difficulty == Difficulty.Impossible) {
|
||||
if (
|
||||
difficulty === Difficulty.Hard ||
|
||||
difficulty === Difficulty.Impossible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (other.type() != PlayerType.Human) {
|
||||
if (other.type() !== PlayerType.Human) {
|
||||
return false;
|
||||
}
|
||||
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
|
||||
@@ -246,6 +258,9 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
handleEnemies() {
|
||||
if (this.player === null || this.behavior === null) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.checkIncomingAttacks();
|
||||
this.behavior.assistAllies();
|
||||
@@ -261,7 +276,8 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSendEmoji(enemy: Player) {
|
||||
if (enemy.type() != PlayerType.Human) return;
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (enemy.type() !== PlayerType.Human) return;
|
||||
const lastSent = this.lastEmojiSent.get(enemy) ?? -300;
|
||||
if (this.mg.ticks() - lastSent <= 300) return;
|
||||
this.lastEmojiSent.set(enemy, this.mg.ticks());
|
||||
@@ -275,11 +291,12 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSendNuke(other: Player) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const silos = this.player.units(UnitType.MissileSilo);
|
||||
if (
|
||||
silos.length == 0 ||
|
||||
silos.length === 0 ||
|
||||
this.player.gold() < this.cost(UnitType.AtomBomb) ||
|
||||
other.type() == PlayerType.Bot ||
|
||||
other.type() === PlayerType.Bot ||
|
||||
this.player.isOnSameTeam(other)
|
||||
) {
|
||||
return;
|
||||
@@ -293,20 +310,20 @@ export class FakeHumanExecution implements Execution {
|
||||
UnitType.SAMLauncher,
|
||||
);
|
||||
const structureTiles = structures.map((u) => u.tile());
|
||||
const randomTiles: TileRef[] = new Array(10);
|
||||
const randomTiles: (TileRef | null)[] = new Array(10);
|
||||
for (let i = 0; i < randomTiles.length; i++) {
|
||||
randomTiles[i] = this.randTerritoryTile(other);
|
||||
}
|
||||
const allTiles = randomTiles.concat(structureTiles);
|
||||
|
||||
let bestTile = null;
|
||||
let bestTile: TileRef | null = null;
|
||||
let bestValue = 0;
|
||||
this.removeOldNukeEvents();
|
||||
outer: for (const tile of new Set(allTiles)) {
|
||||
if (tile == null) continue;
|
||||
if (tile === null) continue;
|
||||
for (const t of this.mg.bfs(tile, manhattanDistFN(tile, 15))) {
|
||||
// Make sure we nuke at least 15 tiles in border
|
||||
if (this.mg.owner(t) != other) {
|
||||
if (this.mg.owner(t) !== other) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
@@ -317,7 +334,7 @@ export class FakeHumanExecution implements Execution {
|
||||
bestValue = value;
|
||||
}
|
||||
}
|
||||
if (bestTile != null) {
|
||||
if (bestTile !== null) {
|
||||
this.sendNuke(bestTile);
|
||||
}
|
||||
}
|
||||
@@ -334,6 +351,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private sendNuke(tile: TileRef) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const tick = this.mg.ticks();
|
||||
this.lastNukeSent.push([tick, tile]);
|
||||
this.mg.addExecution(
|
||||
@@ -366,7 +384,9 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
// Prefer tiles that are closer to a silo
|
||||
const siloTiles = silos.map((u) => u.tile());
|
||||
const { x: closestSilo } = closestTwoTiles(this.mg, siloTiles, [tile]);
|
||||
const result = closestTwoTiles(this.mg, siloTiles, [tile]);
|
||||
if (result === null) throw new Error("Missing result");
|
||||
const { x: closestSilo } = result;
|
||||
const distanceSquared = this.mg.euclideanDistSquared(tile, closestSilo);
|
||||
const distanceToClosestSilo = Math.sqrt(distanceSquared);
|
||||
tileValue -= distanceToClosestSilo * 30;
|
||||
@@ -381,6 +401,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,
|
||||
@@ -389,7 +410,7 @@ export class FakeHumanExecution implements Execution {
|
||||
),
|
||||
Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t)),
|
||||
);
|
||||
if (closest == null) {
|
||||
if (closest === null) {
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(
|
||||
@@ -404,15 +425,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;
|
||||
@@ -425,6 +448,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private maybeSpawnStructure(type: UnitType, maxNum: number) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const units = this.player.units(type);
|
||||
if (units.length >= maxNum) {
|
||||
return;
|
||||
@@ -433,11 +457,11 @@ export class FakeHumanExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
const tile = this.randTerritoryTile(this.player);
|
||||
if (tile == null) {
|
||||
if (tile === null) {
|
||||
return;
|
||||
}
|
||||
const canBuild = this.player.canBuild(type, tile);
|
||||
if (canBuild == false) {
|
||||
if (canBuild === false) {
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(
|
||||
@@ -446,6 +470,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;
|
||||
}
|
||||
@@ -453,16 +478,16 @@ export class FakeHumanExecution implements Execution {
|
||||
const ships = this.player.units(UnitType.Warship);
|
||||
if (
|
||||
ports.length > 0 &&
|
||||
ships.length == 0 &&
|
||||
ships.length === 0 &&
|
||||
this.player.gold() > this.cost(UnitType.Warship)
|
||||
) {
|
||||
const port = this.random.randElement(ports);
|
||||
const targetTile = this.warshipSpawnTile(port.tile());
|
||||
if (targetTile == null) {
|
||||
if (targetTile === null) {
|
||||
return false;
|
||||
}
|
||||
const canBuild = this.player.canBuild(UnitType.Warship, targetTile);
|
||||
if (canBuild == false) {
|
||||
if (canBuild === false) {
|
||||
consolex.warn("cannot spawn destroyer");
|
||||
return false;
|
||||
}
|
||||
@@ -488,7 +513,7 @@ export class FakeHumanExecution implements Execution {
|
||||
continue;
|
||||
}
|
||||
const randTile = this.mg.ref(randX, randY);
|
||||
if (this.mg.owner(randTile) == p) {
|
||||
if (this.mg.owner(randTile) === p) {
|
||||
return randTile;
|
||||
}
|
||||
}
|
||||
@@ -520,21 +545,23 @@ 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);
|
||||
}
|
||||
|
||||
sendBoatRandomly() {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t),
|
||||
);
|
||||
if (oceanShore.length == 0) {
|
||||
if (oceanShore.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const src = this.random.randElement(oceanShore);
|
||||
|
||||
const dst = this.randOceanShoreTile(src, 150);
|
||||
if (dst == null) {
|
||||
if (dst === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -564,7 +591,7 @@ export class FakeHumanExecution implements Execution {
|
||||
const tile = this.mg.ref(x, y);
|
||||
if (this.mg.isLand(tile) && !this.mg.hasOwner(tile)) {
|
||||
if (
|
||||
this.mg.terrainType(tile) == TerrainType.Mountain &&
|
||||
this.mg.terrainType(tile) === TerrainType.Mountain &&
|
||||
this.random.chance(2)
|
||||
) {
|
||||
continue;
|
||||
@@ -576,6 +603,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++) {
|
||||
@@ -599,10 +627,6 @@ export class FakeHumanExecution implements Execution {
|
||||
return null;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private mg: Game;
|
||||
|
||||
private nuke: Unit;
|
||||
private nuke: Unit | null = null;
|
||||
|
||||
private mirvRange = 1500;
|
||||
private warheadCount = 350;
|
||||
@@ -66,9 +66,9 @@ export class MirvExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.nuke == null) {
|
||||
if (this.nuke === null) {
|
||||
const spawn = this.player.canBuild(UnitType.MIRV, this.dst);
|
||||
if (spawn == false) {
|
||||
if (spawn === false) {
|
||||
consolex.warn(`cannot build MIRV`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -100,12 +100,13 @@ export class MirvExecution implements Execution {
|
||||
}
|
||||
|
||||
private separate() {
|
||||
if (this.nuke === null) throw new Error("uninitialized");
|
||||
const dsts: TileRef[] = [this.dst];
|
||||
let attempts = 1000;
|
||||
while (attempts > 0 && dsts.length < this.warheadCount) {
|
||||
attempts--;
|
||||
const potential = this.randomLand(this.dst, dsts);
|
||||
if (potential == null) {
|
||||
if (potential === null) {
|
||||
continue;
|
||||
}
|
||||
dsts.push(potential);
|
||||
@@ -132,10 +133,10 @@ export class MirvExecution implements Execution {
|
||||
}
|
||||
if (this.targetPlayer.isPlayer()) {
|
||||
const alliance = this.player.allianceWith(this.targetPlayer);
|
||||
if (alliance != null) {
|
||||
if (alliance !== null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
if (this.targetPlayer != this.player) {
|
||||
if (this.targetPlayer !== this.player) {
|
||||
this.targetPlayer.updateRelation(this.player, -100);
|
||||
}
|
||||
}
|
||||
@@ -165,7 +166,7 @@ export class MirvExecution implements Execution {
|
||||
if (this.mg.euclideanDistSquared(tile, ref) > mirvRange2) {
|
||||
continue;
|
||||
}
|
||||
if (this.mg.owner(tile) != this.targetPlayer) {
|
||||
if (this.mg.owner(tile) !== this.targetPlayer) {
|
||||
continue;
|
||||
}
|
||||
for (const t of taken) {
|
||||
|
||||
@@ -11,9 +11,9 @@ import { TileRef } from "../game/GameMap";
|
||||
|
||||
export class MissileSiloExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private player: Player;
|
||||
private silo: Unit;
|
||||
private mg: Game | null = null;
|
||||
private player: Player | null = null;
|
||||
private silo: Unit | null = null;
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
@@ -32,7 +32,10 @@ export class MissileSiloExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.silo == null) {
|
||||
if (this.player === null || this.mg === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.silo === null) {
|
||||
const spawn = this.player.canBuild(UnitType.MissileSilo, this.tile);
|
||||
if (spawn === false) {
|
||||
consolex.warn(
|
||||
@@ -45,14 +48,14 @@ export class MissileSiloExecution implements Execution {
|
||||
cooldownDuration: this.mg.config().SiloCooldown(),
|
||||
});
|
||||
|
||||
if (this.player != this.silo.owner()) {
|
||||
if (this.player !== this.silo.owner()) {
|
||||
this.player = this.silo.owner();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.silo.isCooldown() &&
|
||||
this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) == 0
|
||||
this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) === 0
|
||||
) {
|
||||
this.silo.setCooldown(false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const cancelDelay = 2;
|
||||
|
||||
export class MoveWarshipExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private mg: Game | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly unitId: number,
|
||||
@@ -16,7 +16,10 @@ export class MoveWarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
const warship = this.mg.units().find((u) => u.id() == this.unitId);
|
||||
if (this.mg === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const warship = this.mg.units().find((u) => u.id() === this.unitId);
|
||||
if (!warship) {
|
||||
console.log("MoveWarshipExecution: warship is already dead");
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
|
||||
export class NukeExecution implements Execution {
|
||||
private player: Player;
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private nuke: Unit;
|
||||
private player: Player | null = null;
|
||||
private mg: Game | null = null;
|
||||
private nuke: Unit | null = null;
|
||||
|
||||
private random: PseudoRandom;
|
||||
private pathFinder: ParabolaPathFinder;
|
||||
@@ -27,7 +27,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,
|
||||
) {}
|
||||
@@ -42,28 +42,37 @@ export class NukeExecution implements Execution {
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this.senderID);
|
||||
this.random = new PseudoRandom(ticks);
|
||||
if (this.speed == -1) {
|
||||
if (this.speed === -1) {
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
}
|
||||
this.pathFinder = new ParabolaPathFinder(mg);
|
||||
}
|
||||
|
||||
public target(): Player | TerraNullius {
|
||||
if (this.mg === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
return this.mg.owner(this.dst);
|
||||
}
|
||||
|
||||
private tilesToDestroy(): Set<TileRef> {
|
||||
if (this.mg === null || this.nuke === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const rand = new PseudoRandom(this.mg.ticks());
|
||||
const inner2 = magnitude.inner * magnitude.inner;
|
||||
const outer2 = magnitude.outer * magnitude.outer;
|
||||
return this.mg.bfs(this.dst, (_, n: TileRef) => {
|
||||
const d2 = this.mg.euclideanDistSquared(this.dst, n);
|
||||
const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0;
|
||||
return d2 <= outer2 && (d2 <= inner2 || rand.chance(2));
|
||||
});
|
||||
}
|
||||
|
||||
private breakAlliances(toDestroy: Set<TileRef>) {
|
||||
if (this.mg === null || this.player === null || this.nuke === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
@@ -74,13 +83,13 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
for (const [other, tilesDestroyed] of attacked) {
|
||||
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
|
||||
if (tilesDestroyed > 100 && this.nuke.type() !== UnitType.MIRVWarhead) {
|
||||
// Mirv warheads shouldn't break alliances
|
||||
const alliance = this.player.allianceWith(other);
|
||||
if (alliance != null) {
|
||||
if (alliance !== null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
if (other != this.player) {
|
||||
if (other !== this.player) {
|
||||
other.updateRelation(this.player, -100);
|
||||
}
|
||||
}
|
||||
@@ -88,9 +97,13 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.nuke == null) {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
|
||||
if (this.nuke === null) {
|
||||
const spawn = this.src ?? this.player.canBuild(this.type, this.dst);
|
||||
if (spawn == false) {
|
||||
if (spawn === false) {
|
||||
consolex.warn(`cannot build Nuke`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -98,14 +111,14 @@ export class NukeExecution implements Execution {
|
||||
this.pathFinder.computeControlPoints(
|
||||
spawn,
|
||||
this.dst,
|
||||
this.type != UnitType.MIRVWarhead,
|
||||
this.type !== UnitType.MIRVWarhead,
|
||||
);
|
||||
this.nuke = this.player.buildUnit(this.type, spawn, {
|
||||
detonationDst: this.dst,
|
||||
});
|
||||
if (this.mg.hasOwner(this.dst)) {
|
||||
const target = this.mg.owner(this.dst) as Player;
|
||||
if (this.type == UnitType.AtomBomb) {
|
||||
if (this.type === UnitType.AtomBomb) {
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
`${this.player.name()} - atom bomb inbound`,
|
||||
@@ -113,7 +126,7 @@ export class NukeExecution implements Execution {
|
||||
target.id(),
|
||||
);
|
||||
}
|
||||
if (this.type == UnitType.HydrogenBomb) {
|
||||
if (this.type === UnitType.HydrogenBomb) {
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
`${this.player.name()} - hydrogen bomb inbound`,
|
||||
@@ -164,6 +177,9 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
private detonate() {
|
||||
if (this.mg === null || this.nuke === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
this.breakAlliances(toDestroy);
|
||||
@@ -183,15 +199,17 @@ export class NukeExecution implements Execution {
|
||||
.nukeDeathFactor(owner.workers(), owner.numTilesOwned()),
|
||||
);
|
||||
owner.outgoingAttacks().forEach((attack) => {
|
||||
const deaths = this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(attack.troops(), owner.numTilesOwned());
|
||||
const deaths =
|
||||
this.mg
|
||||
?.config()
|
||||
.nukeDeathFactor(attack.troops(), owner.numTilesOwned()) ?? 0;
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
owner.units(UnitType.TransportShip).forEach((attack) => {
|
||||
const deaths = this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(attack.troops(), owner.numTilesOwned());
|
||||
const deaths =
|
||||
this.mg
|
||||
?.config()
|
||||
.nukeDeathFactor(attack.troops(), owner.numTilesOwned()) ?? 0;
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
}
|
||||
@@ -204,10 +222,10 @@ export class NukeExecution implements Execution {
|
||||
const outer2 = magnitude.outer * magnitude.outer;
|
||||
for (const unit of this.mg.units()) {
|
||||
if (
|
||||
unit.type() != UnitType.AtomBomb &&
|
||||
unit.type() != UnitType.HydrogenBomb &&
|
||||
unit.type() != UnitType.MIRVWarhead &&
|
||||
unit.type() != UnitType.MIRV
|
||||
unit.type() !== UnitType.AtomBomb &&
|
||||
unit.type() !== UnitType.HydrogenBomb &&
|
||||
unit.type() !== UnitType.MIRVWarhead &&
|
||||
unit.type() !== UnitType.MIRV
|
||||
) {
|
||||
if (this.mg.euclideanDistSquared(this.dst, unit.tile()) < outer2) {
|
||||
unit.delete();
|
||||
@@ -219,6 +237,9 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
if (this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
return this.player;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
||||
export class PlayerExecution implements Execution {
|
||||
private readonly ticksPerClusterCalc = 20;
|
||||
|
||||
private player: Player;
|
||||
private config: Config;
|
||||
private player: Player | null = null;
|
||||
private config: Config | null = null;
|
||||
private lastCalc = 0;
|
||||
private mg: Game;
|
||||
private mg: Game | null = null;
|
||||
private active = true;
|
||||
|
||||
constructor(private playerID: PlayerID) {}
|
||||
@@ -42,6 +42,9 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (this.mg === null || this.config === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
this.player.decayRelations();
|
||||
const hasPort = this.player.units(UnitType.Port).length > 0;
|
||||
this.player.units().forEach((u) => {
|
||||
@@ -49,13 +52,14 @@ export class PlayerExecution implements Execution {
|
||||
u.delete();
|
||||
return;
|
||||
}
|
||||
if (hasPort && u.type() == UnitType.Warship) {
|
||||
if (hasPort && u.type() === UnitType.Warship) {
|
||||
u.modifyHealth(1);
|
||||
}
|
||||
if (this.mg === null) return;
|
||||
const tileOwner = this.mg.owner(u.tile());
|
||||
if (u.info().territoryBound) {
|
||||
if (tileOwner.isPlayer()) {
|
||||
if (tileOwner != this.player) {
|
||||
if (tileOwner !== this.player) {
|
||||
this.mg.player(tileOwner.id()).captureUnit(u);
|
||||
}
|
||||
} else {
|
||||
@@ -67,10 +71,10 @@ export class PlayerExecution implements Execution {
|
||||
if (!this.player.isAlive()) {
|
||||
this.player.units().forEach((u) => {
|
||||
if (
|
||||
u.type() != UnitType.AtomBomb &&
|
||||
u.type() != UnitType.HydrogenBomb &&
|
||||
u.type() != UnitType.MIRVWarhead &&
|
||||
u.type() != UnitType.MIRV
|
||||
u.type() !== UnitType.AtomBomb &&
|
||||
u.type() !== UnitType.HydrogenBomb &&
|
||||
u.type() !== UnitType.MIRVWarhead &&
|
||||
u.type() !== UnitType.MIRV
|
||||
) {
|
||||
u.delete();
|
||||
}
|
||||
@@ -122,10 +126,14 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
private removeClusters() {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const clusters = this.calculateClusters();
|
||||
clusters.sort((a, b) => b.size - a.size);
|
||||
|
||||
const main = clusters.shift();
|
||||
if (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)) {
|
||||
@@ -140,6 +148,9 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const enemies = new Set<number>();
|
||||
for (const tile of cluster) {
|
||||
const isOceanShore = this.mg.isOceanShore(tile);
|
||||
@@ -149,19 +160,19 @@ export class PlayerExecution implements Execution {
|
||||
if (
|
||||
isOceanShore ||
|
||||
this.mg.isOnEdgeOfMap(tile) ||
|
||||
this.mg.neighbors(tile).some((n) => !this.mg.hasOwner(n))
|
||||
this.mg.neighbors(tile).some((n) => !this.mg?.hasOwner(n))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.mg
|
||||
.neighbors(tile)
|
||||
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
|
||||
.forEach((p) => enemies.add(this.mg.ownerID(p)));
|
||||
if (enemies.size != 1) {
|
||||
.filter((n) => this.mg?.ownerID(n) !== this.player?.smallID())
|
||||
.forEach((p) => this.mg && enemies.add(this.mg.ownerID(p)));
|
||||
if (enemies.size !== 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (enemies.size != 1) {
|
||||
if (enemies.size !== 1) {
|
||||
return false;
|
||||
}
|
||||
const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
|
||||
@@ -174,6 +185,9 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
private isSurrounded(cluster: Set<TileRef>): boolean {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const enemyTiles = new Set<TileRef>();
|
||||
for (const tr of cluster) {
|
||||
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
|
||||
@@ -181,10 +195,10 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
this.mg
|
||||
.neighbors(tr)
|
||||
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
|
||||
.filter((n) => this.mg?.ownerID(n) !== this.player?.smallID())
|
||||
.forEach((n) => enemyTiles.add(n));
|
||||
}
|
||||
if (enemyTiles.size == 0) {
|
||||
if (enemyTiles.size === 0) {
|
||||
return false;
|
||||
}
|
||||
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
|
||||
@@ -193,9 +207,12 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
private removeCluster(cluster: Set<TileRef>) {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (
|
||||
Array.from(cluster).some(
|
||||
(t) => this.mg.ownerID(t) != this.player.smallID(),
|
||||
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
|
||||
)
|
||||
) {
|
||||
// Other removeCluster operations could change tile owners,
|
||||
@@ -204,16 +221,16 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
const capturing = this.getCapturingPlayer(cluster);
|
||||
if (capturing == null) {
|
||||
if (capturing === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstTile = cluster.values().next().value;
|
||||
const filter = (_, t: TileRef): boolean =>
|
||||
this.mg.ownerID(t) == this.player.smallID();
|
||||
this.mg?.ownerID(t) === this.player?.smallID();
|
||||
const tiles = this.mg.bfs(firstTile, filter);
|
||||
|
||||
if (this.player.numTilesOwned() == tiles.size) {
|
||||
if (this.player.numTilesOwned() === tiles.size) {
|
||||
const gold = this.player.gold();
|
||||
this.mg.displayMessage(
|
||||
`Conquered ${this.player.displayName()} received ${renderNumber(
|
||||
@@ -232,10 +249,13 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const neighborsIDs = new Set<number>();
|
||||
for (const t of cluster) {
|
||||
for (const neighbor of this.mg.neighbors(t)) {
|
||||
if (this.mg.ownerID(neighbor) != this.player.smallID()) {
|
||||
if (this.mg.ownerID(neighbor) !== this.player.smallID()) {
|
||||
neighborsIDs.add(this.mg.ownerID(neighbor));
|
||||
}
|
||||
}
|
||||
@@ -249,7 +269,7 @@ export class PlayerExecution implements Execution {
|
||||
continue;
|
||||
}
|
||||
for (const attack of neighbor.outgoingAttacks()) {
|
||||
if (attack.target() == this.player) {
|
||||
if (attack.target() === this.player) {
|
||||
if (attack.troops() > largestTroopCount) {
|
||||
largestTroopCount = attack.troops();
|
||||
largestNeighborAttack = neighbor;
|
||||
@@ -257,7 +277,7 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (largestNeighborAttack != null) {
|
||||
if (largestNeighborAttack !== null) {
|
||||
return largestNeighborAttack;
|
||||
}
|
||||
|
||||
@@ -270,10 +290,13 @@ export class PlayerExecution implements Execution {
|
||||
if (!capturing.isPlayer()) {
|
||||
return null;
|
||||
}
|
||||
return capturing as Player;
|
||||
return capturing;
|
||||
}
|
||||
|
||||
private calculateClusters(): Set<TileRef>[] {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const seen = new Set<TileRef>();
|
||||
const border = this.player.borderTiles();
|
||||
const clusters: Set<TileRef>[] = [];
|
||||
@@ -287,6 +310,7 @@ export class PlayerExecution implements Execution {
|
||||
seen.add(tile);
|
||||
while (queue.length > 0) {
|
||||
const curr = queue.shift();
|
||||
if (curr === undefined) throw new Error("curr is undefined");
|
||||
cluster.add(curr);
|
||||
|
||||
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
|
||||
@@ -303,6 +327,9 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
if (this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
return this.player;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ import { TradeShipExecution } from "./TradeShipExecution";
|
||||
|
||||
export class PortExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private port: Unit;
|
||||
private random: PseudoRandom;
|
||||
private checkOffset: number;
|
||||
private mg: Game | null = null;
|
||||
private port: Unit | null = null;
|
||||
private random: PseudoRandom | null = null;
|
||||
private checkOffset: number | null = null;
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
@@ -36,7 +36,10 @@ export class PortExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.port == null) {
|
||||
if (this.mg === null || this.random === null || this.checkOffset === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.port === null) {
|
||||
const tile = this.tile;
|
||||
const player = this.mg.player(this._owner);
|
||||
const spawn = player.canBuild(UnitType.Port, tile);
|
||||
@@ -53,12 +56,12 @@ export class PortExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._owner != this.port.owner().id()) {
|
||||
if (this._owner !== this.port.owner().id()) {
|
||||
this._owner = this.port.owner().id();
|
||||
}
|
||||
|
||||
// Only check every 10 ticks for performance.
|
||||
if ((this.mg.ticks() + this.checkOffset) % 10 != 0) {
|
||||
if ((this.mg.ticks() + this.checkOffset) % 10 !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +74,7 @@ export class PortExecution implements Execution {
|
||||
|
||||
const ports = this.player().tradingPorts(this.port);
|
||||
|
||||
if (ports.length == 0) {
|
||||
if (ports.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,6 +94,9 @@ export class PortExecution implements Execution {
|
||||
}
|
||||
|
||||
player(): Player {
|
||||
if (this.port === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
return this.port.owner();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ export class SAMLauncherExecution implements Execution {
|
||||
private MIRVWarheadSearchRadius = 400;
|
||||
private MIRVWarheadProtectionRadius = 50;
|
||||
|
||||
private pseudoRandom: PseudoRandom;
|
||||
private pseudoRandom: PseudoRandom | undefined;
|
||||
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
private tile: TileRef | null,
|
||||
private sam: Unit | null = null,
|
||||
) {
|
||||
if (sam != null) {
|
||||
if (sam !== null) {
|
||||
this.tile = sam.tile();
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
}
|
||||
|
||||
private getSingleTarget(): Unit | null {
|
||||
if (this.sam === null) return null;
|
||||
const nukes = this.mg
|
||||
.nearbyUnits(this.sam.tile(), this.searchRangeRadius, [
|
||||
UnitType.AtomBomb,
|
||||
@@ -80,11 +81,11 @@ export class SAMLauncherExecution implements Execution {
|
||||
}
|
||||
|
||||
private isHit(type: UnitType, random: number): boolean {
|
||||
if (type == UnitType.AtomBomb) {
|
||||
if (type === UnitType.AtomBomb) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == UnitType.MIRVWarhead) {
|
||||
if (type === UnitType.MIRVWarhead) {
|
||||
return random < this.mg.config().samWarheadHittingChance();
|
||||
}
|
||||
|
||||
@@ -92,9 +93,15 @@ export class SAMLauncherExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.sam == null) {
|
||||
if (this.mg === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.sam === null) {
|
||||
if (this.tile === null) {
|
||||
throw new Error("tile is null");
|
||||
}
|
||||
const spawnTile = this.player.canBuild(UnitType.SAMLauncher, this.tile);
|
||||
if (spawnTile == false) {
|
||||
if (spawnTile === false) {
|
||||
consolex.warn("cannot build SAM Launcher");
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -108,11 +115,11 @@ export class SAMLauncherExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player != this.sam.owner()) {
|
||||
if (this.player !== this.sam.owner()) {
|
||||
this.player = this.sam.owner();
|
||||
}
|
||||
|
||||
if (!this.pseudoRandom) {
|
||||
if (this.pseudoRandom === undefined) {
|
||||
this.pseudoRandom = new PseudoRandom(this.sam.id());
|
||||
}
|
||||
|
||||
@@ -127,20 +134,24 @@ export class SAMLauncherExecution implements Execution {
|
||||
(unit) =>
|
||||
unit.owner() !== this.player && !this.player.isFriendly(unit.owner()),
|
||||
)
|
||||
.filter(
|
||||
(unit) =>
|
||||
this.mg.manhattanDist(unit.detonationDst(), this.sam.tile()) <
|
||||
this.MIRVWarheadProtectionRadius,
|
||||
);
|
||||
.filter((unit) => {
|
||||
const dst = unit.detonationDst();
|
||||
return (
|
||||
this.sam !== null &&
|
||||
dst !== null &&
|
||||
this.mg.manhattanDist(dst, this.sam.tile()) <
|
||||
this.MIRVWarheadProtectionRadius
|
||||
);
|
||||
});
|
||||
|
||||
let target: Unit | null = null;
|
||||
if (mirvWarheadTargets.length == 0) {
|
||||
if (mirvWarheadTargets.length === 0) {
|
||||
target = this.getSingleTarget();
|
||||
}
|
||||
|
||||
if (
|
||||
this.sam.isCooldown() &&
|
||||
this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) == 0
|
||||
this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) === 0
|
||||
) {
|
||||
this.sam.setCooldown(false);
|
||||
}
|
||||
@@ -152,7 +163,8 @@ export class SAMLauncherExecution implements Execution {
|
||||
) {
|
||||
this.sam.setCooldown(true);
|
||||
const type =
|
||||
mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target.type();
|
||||
mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target?.type();
|
||||
if (type === undefined) throw new Error("Unknown unit type");
|
||||
const random = this.pseudoRandom.next();
|
||||
const hit = this.isHit(type, random);
|
||||
if (!hit) {
|
||||
@@ -171,7 +183,7 @@ export class SAMLauncherExecution implements Execution {
|
||||
);
|
||||
// Delete warheads
|
||||
mirvWarheadTargets.forEach((u) => u.delete());
|
||||
} else {
|
||||
} else if (target !== null) {
|
||||
target.setTargetedBySAM(true);
|
||||
this.mg.addExecution(
|
||||
new SAMMissileExecution(
|
||||
@@ -181,6 +193,8 @@ export class SAMLauncherExecution implements Execution {
|
||||
target,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw new Error("target is null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { PseudoRandom } from "../PseudoRandom";
|
||||
export class SAMMissileExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: AirPathFinder;
|
||||
private SAMMissile: Unit;
|
||||
private SAMMissile: Unit | undefined;
|
||||
private mg: Game;
|
||||
|
||||
constructor(
|
||||
@@ -30,7 +30,7 @@ export class SAMMissileExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.SAMMissile == null) {
|
||||
if (this.SAMMissile === undefined) {
|
||||
this.SAMMissile = this._owner.buildUnit(
|
||||
UnitType.SAMMissile,
|
||||
this.spawn,
|
||||
@@ -46,7 +46,7 @@ export class SAMMissileExecution implements Execution {
|
||||
if (
|
||||
!this.target.isActive() ||
|
||||
!this.ownerUnit.isActive() ||
|
||||
this.target.owner() == this.SAMMissile.owner() ||
|
||||
this.target.owner() === this.SAMMissile.owner() ||
|
||||
!nukesWhitelist.includes(this.target.type())
|
||||
) {
|
||||
this.SAMMissile.delete(false);
|
||||
|
||||
@@ -31,10 +31,6 @@ export class SetTargetTroopRatioExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PseudoRandom } from "../PseudoRandom";
|
||||
export class ShellExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: AirPathFinder;
|
||||
private shell: Unit;
|
||||
private shell: Unit | undefined;
|
||||
private mg: Game;
|
||||
private destroyAtTick: number = -1;
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.shell == null) {
|
||||
if (this.shell === undefined) {
|
||||
this.shell = this._owner.buildUnit(UnitType.Shell, this.spawn, {});
|
||||
}
|
||||
if (!this.shell.isActive()) {
|
||||
@@ -32,15 +32,15 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
if (
|
||||
!this.target.isActive() ||
|
||||
this.target.owner() == this.shell.owner() ||
|
||||
(this.destroyAtTick != -1 && this.mg.ticks() >= this.destroyAtTick)
|
||||
this.target.owner() === this.shell.owner() ||
|
||||
(this.destroyAtTick !== -1 && this.mg.ticks() >= this.destroyAtTick)
|
||||
) {
|
||||
this.shell.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyAtTick == -1 && !this.ownerUnit.isActive()) {
|
||||
if (this.destroyAtTick === -1 && !this.ownerUnit.isActive()) {
|
||||
this.destroyAtTick = this.mg.ticks() + this.mg.config().shellLifetime();
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
|
||||
private effectOnTarget(): number {
|
||||
const baseDamage: number = this.mg.config().unitInfo(UnitType.Shell).damage;
|
||||
return baseDamage;
|
||||
const { damage } = this.mg.config().unitInfo(UnitType.Shell);
|
||||
return damage ?? 0;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
|
||||
@@ -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 {
|
||||
@@ -39,16 +39,13 @@ export class SpawnExecution implements Execution {
|
||||
|
||||
if (!player.hasSpawned()) {
|
||||
this.mg.addExecution(new PlayerExecution(player.id()));
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
this.mg.addExecution(new BotExecution(player));
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ import { distSortUnit } from "../Util";
|
||||
|
||||
export class TradeShipExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private origOwner: Player;
|
||||
private tradeShip: Unit;
|
||||
private mg: Game | null = null;
|
||||
private origOwner: Player | null = null;
|
||||
private tradeShip: Unit | null = null;
|
||||
private index = 0;
|
||||
private wasCaptured = false;
|
||||
private tilesTraveled = 0;
|
||||
@@ -36,12 +36,15 @@ export class TradeShipExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.tradeShip == null) {
|
||||
if (this.mg === null || this.origOwner === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.tradeShip === null) {
|
||||
const spawn = this.origOwner.canBuild(
|
||||
UnitType.TradeShip,
|
||||
this.srcPort.tile(),
|
||||
);
|
||||
if (spawn == false) {
|
||||
if (spawn === false) {
|
||||
consolex.warn(`cannot build trade ship`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -57,14 +60,14 @@ export class TradeShipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.origOwner != this.tradeShip.owner()) {
|
||||
if (this.origOwner !== this.tradeShip.owner()) {
|
||||
// Store as variable in case ship is recaptured by previous owner
|
||||
this.wasCaptured = true;
|
||||
}
|
||||
|
||||
// If a player captures another player's port while trading we should delete
|
||||
// the ship.
|
||||
if (this._dstPort.owner().id() == this.srcPort.owner().id()) {
|
||||
if (this._dstPort.owner().id() === this.srcPort.owner().id()) {
|
||||
this.tradeShip.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -85,7 +88,7 @@ export class TradeShipExecution implements Execution {
|
||||
.owner()
|
||||
.units(UnitType.Port)
|
||||
.sort(distSortUnit(this.mg, this.tradeShip));
|
||||
if (ports.length == 0) {
|
||||
if (ports.length === 0) {
|
||||
this.tradeShip.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -127,6 +130,10 @@ export class TradeShipExecution implements Execution {
|
||||
}
|
||||
|
||||
private complete() {
|
||||
if (this.mg === null || this.origOwner === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.tradeShip === null) return;
|
||||
this.active = false;
|
||||
this.tradeShip.delete(false);
|
||||
const gold = this.mg.config().tradeShipGold(this.tilesTraveled);
|
||||
|
||||
@@ -39,7 +39,7 @@ export class TransportShipExecution implements Execution {
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private ref: TileRef,
|
||||
private troops: number | null,
|
||||
private troops: number,
|
||||
private src: TileRef | null,
|
||||
) {}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class TransportShipExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.targetID != null && !mg.hasPlayer(this.targetID)) {
|
||||
if (this.targetID !== null && !mg.hasPlayer(this.targetID)) {
|
||||
console.warn(`TransportShipExecution: target ${this.targetID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -81,13 +81,16 @@ export class TransportShipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
|
||||
if (
|
||||
this.targetID === null ||
|
||||
this.targetID === this.mg.terraNullius().id()
|
||||
) {
|
||||
this.target = mg.terraNullius();
|
||||
} else {
|
||||
this.target = mg.player(this.targetID);
|
||||
}
|
||||
|
||||
if (this.troops == null) {
|
||||
if (this.troops === null) {
|
||||
this.troops = this.mg
|
||||
.config()
|
||||
.boatAttackAmount(this.attacker, this.target);
|
||||
@@ -96,7 +99,7 @@ export class TransportShipExecution implements Execution {
|
||||
this.troops = Math.min(this.troops, this.attacker.troops());
|
||||
|
||||
this.dst = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dst == null) {
|
||||
if (this.dst === null) {
|
||||
consolex.warn(
|
||||
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
|
||||
);
|
||||
@@ -108,19 +111,19 @@ export class TransportShipExecution implements Execution {
|
||||
UnitType.TransportShip,
|
||||
this.dst,
|
||||
);
|
||||
if (closestTileSrc == false) {
|
||||
if (closestTileSrc === false) {
|
||||
consolex.warn(`can't build transport ship`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.src == null) {
|
||||
if (this.src === null) {
|
||||
// Only update the src if it's not already set
|
||||
// because we assume that the src is set to the best spawn tile
|
||||
this.src = closestTileSrc;
|
||||
} else {
|
||||
if (
|
||||
this.mg.owner(this.src) != this.attacker ||
|
||||
this.mg.owner(this.src) !== this.attacker ||
|
||||
!this.mg.isShore(this.src)
|
||||
) {
|
||||
console.warn(
|
||||
@@ -146,6 +149,10 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (this.dst === null) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +168,7 @@ export class TransportShipExecution implements Execution {
|
||||
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
if (this.mg.owner(this.dst) == this.attacker) {
|
||||
if (this.mg.owner(this.dst) === this.attacker) {
|
||||
this.attacker.addTroops(this.troops);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
@@ -10,11 +10,11 @@ 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));
|
||||
|
||||
if (xSorted.length == 0 || ySorted.length == 0) {
|
||||
if (xSorted.length === 0 || ySorted.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ 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 pathfinder: PathFinder;
|
||||
private target: Unit | null = null;
|
||||
private pathfinder: PathFinder | null = null;
|
||||
|
||||
private patrolTile: TileRef;
|
||||
private patrolTile: TileRef | null = null;
|
||||
|
||||
private lastShellAttack = 0;
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
@@ -48,7 +48,10 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
// Only for warships with "moveTarget" set
|
||||
goToMoveTarget(target: TileRef): boolean {
|
||||
goToMoveTarget(target: TileRef) {
|
||||
if (this.warship === null || this.pathfinder === 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) {
|
||||
@@ -69,6 +72,9 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
if (this.mg === null || this.warship === null || this.target === null) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
@@ -90,8 +96,12 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private patrol() {
|
||||
if (this.warship === null || this.pathfinder === null) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
if (this.patrolTile === null) return;
|
||||
this.warship.setWarshipTarget(this.target);
|
||||
if (this.target == null || this.target.type() != UnitType.TradeShip) {
|
||||
if (this.target === null || this.target.type() !== UnitType.TradeShip) {
|
||||
// Patrol unless we are hunting down a tradeship
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
@@ -117,9 +127,11 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.warship == null) {
|
||||
if (this.pathfinder === null) throw new Error("Warship not initialized");
|
||||
if (this.warship === null) {
|
||||
if (this.patrolTile === null) return;
|
||||
const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile);
|
||||
if (spawn == false) {
|
||||
if (spawn === false) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
@@ -130,10 +142,13 @@ export class WarshipExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.target != null && !this.target.isActive()) {
|
||||
if (this.target !== null && !this.target.isActive()) {
|
||||
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(),
|
||||
@@ -142,12 +157,13 @@ export class WarshipExecution implements Execution {
|
||||
)
|
||||
.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 &&
|
||||
this.warship !== null &&
|
||||
unit.dstPort()?.owner() !== this.warship.owner() &&
|
||||
!unit.dstPort()?.owner().isFriendly(this.warship.owner()) &&
|
||||
unit.isSafeFromPirates() !== true)),
|
||||
@@ -186,22 +202,23 @@ 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) {
|
||||
if (this.target && this.target.type() === UnitType.TradeShip) {
|
||||
this.target = null;
|
||||
}
|
||||
} else if (!this.target || this.target.type() != UnitType.TradeShip) {
|
||||
} else if (!this.target || this.target.type() !== UnitType.TradeShip) {
|
||||
this.patrol();
|
||||
}
|
||||
|
||||
if (
|
||||
this.target == null ||
|
||||
this.target === null ||
|
||||
!this.target.isActive() ||
|
||||
this.target.owner() == this._owner ||
|
||||
this.target.isSafeFromPirates() == true
|
||||
this.target.owner() === this._owner ||
|
||||
this.target.isSafeFromPirates() === true
|
||||
) {
|
||||
// In case another warship captured or destroyed target, or the target escaped into safe waters
|
||||
this.target = null;
|
||||
@@ -215,7 +232,7 @@ export class WarshipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.target.type() != UnitType.TradeShip) {
|
||||
if (this.target.type() !== UnitType.TradeShip) {
|
||||
this.shoot();
|
||||
return;
|
||||
}
|
||||
@@ -255,6 +272,9 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
if (this.mg === null) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
|
||||
const maxAttemptBeforeExpand: number = warshipPatrolRange * 2;
|
||||
let attemptCount: number = 0;
|
||||
@@ -282,5 +302,6 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export class WinEvent implements GameEvent {
|
||||
export class WinCheckExecution implements Execution {
|
||||
private active = true;
|
||||
|
||||
private mg: Game;
|
||||
private mg: Game | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -24,10 +24,11 @@ export class WinCheckExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (ticks % 10 != 0) {
|
||||
if (ticks % 10 !== 0) {
|
||||
return;
|
||||
}
|
||||
if (this.mg.config().gameConfig().gameMode == GameMode.FFA) {
|
||||
if (this.mg === null) throw new Error("Not initialized");
|
||||
if (this.mg.config().gameConfig().gameMode === GameMode.FFA) {
|
||||
this.checkWinnerFFA();
|
||||
} else {
|
||||
this.checkWinnerTeam();
|
||||
@@ -35,10 +36,11 @@ export class WinCheckExecution implements Execution {
|
||||
}
|
||||
|
||||
checkWinnerFFA(): void {
|
||||
if (this.mg === null) throw new Error("Not initialized");
|
||||
const sorted = this.mg
|
||||
.players()
|
||||
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
||||
if (sorted.length == 0) {
|
||||
if (sorted.length === 0) {
|
||||
return;
|
||||
}
|
||||
const max = sorted[0];
|
||||
@@ -55,17 +57,21 @@ export class WinCheckExecution implements Execution {
|
||||
}
|
||||
|
||||
checkWinnerTeam(): void {
|
||||
if (this.mg === null) throw new Error("Not initialized");
|
||||
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(
|
||||
(a, b) => b[1] - a[1],
|
||||
);
|
||||
if (sorted.length == 0) {
|
||||
if (sorted.length === 0) {
|
||||
return;
|
||||
}
|
||||
const max = sorted[0];
|
||||
@@ -73,15 +79,12 @@ export class WinCheckExecution implements Execution {
|
||||
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
|
||||
const percentage = (max[1] / numTilesWithoutFallout) * 100;
|
||||
if (percentage > this.mg.config().percentageTilesOwnedToWin()) {
|
||||
if (max[0] == ColoredTeams.Bot) return;
|
||||
if (max[0] === ColoredTeams.Bot) return;
|
||||
this.mg.setWinner(max[0], this.mg.stats().stats());
|
||||
console.log(`${max[0]} has won the game`);
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
|
||||
@@ -3,9 +3,8 @@ 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;
|
||||
private requestor: Player | null = null;
|
||||
private recipient: Player | null = null;
|
||||
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
@@ -28,12 +27,14 @@ export class AllianceRequestExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor === null || this.recipient === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.requestor.isFriendly(this.recipient)) {
|
||||
consolex.warn("already allied");
|
||||
} else if (!this.requestor.canSendAllianceRequest(this.recipient)) {
|
||||
@@ -44,10 +45,6 @@ export class AllianceRequestExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ 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;
|
||||
private requestor: Player | null = null;
|
||||
private recipient: Player | null = null;
|
||||
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
@@ -28,19 +27,21 @@ export class AllianceRequestReplyExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.requestor === null || this.recipient === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.requestor.isFriendly(this.recipient)) {
|
||||
consolex.warn("already allied");
|
||||
} else {
|
||||
const request = this.requestor
|
||||
.outgoingAllianceRequests()
|
||||
.find((ar) => ar.recipient() == this.recipient);
|
||||
if (request == null) {
|
||||
.find((ar) => ar.recipient() === this.recipient);
|
||||
if (request === undefined) {
|
||||
consolex.warn("no alliance request found");
|
||||
} else {
|
||||
if (this.accept) {
|
||||
@@ -55,10 +56,6 @@ export class AllianceRequestReplyExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
|
||||
|
||||
export class BreakAllianceExecution implements Execution {
|
||||
private active = true;
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
private mg: Game;
|
||||
private requestor: Player | null = null;
|
||||
private recipient: Player | null = null;
|
||||
private mg: Game | null = null;
|
||||
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
@@ -33,14 +33,21 @@ export class BreakAllianceExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (
|
||||
this.mg === null ||
|
||||
this.requestor === null ||
|
||||
this.recipient === null
|
||||
) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
const alliance = this.requestor.allianceWith(this.recipient);
|
||||
if (alliance == null) {
|
||||
if (alliance === null) {
|
||||
consolex.warn("cant break alliance, not allied");
|
||||
} else {
|
||||
this.requestor.breakAlliance(alliance);
|
||||
this.recipient.updateRelation(this.requestor, -200);
|
||||
for (const player of this.mg.players()) {
|
||||
if (player != this.requestor) {
|
||||
if (player !== this.requestor) {
|
||||
player.updateRelation(this.requestor, -40);
|
||||
}
|
||||
}
|
||||
@@ -48,10 +55,6 @@ export class BreakAllianceExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ export class BotBehavior {
|
||||
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
const mostHated = this.player.allRelationsSorted()[0] ?? null;
|
||||
if (mostHated != null && mostHated.relation === Relation.Hostile) {
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (mostHated !== undefined && mostHated.relation === Relation.Hostile) {
|
||||
this.enemy = mostHated.player;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export class BotBehavior {
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
if (neighbor.type() == PlayerType.FakeHuman) {
|
||||
if (neighbor.type() === PlayerType.FakeHuman) {
|
||||
if (this.random.chance(2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { MutableAlliance, Player, Tick } from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { Game, MutableAlliance, Player, Tick } from "./Game";
|
||||
|
||||
export class AllianceImpl implements MutableAlliance {
|
||||
constructor(
|
||||
private readonly mg: GameImpl,
|
||||
readonly requestor_: PlayerImpl,
|
||||
readonly recipient_: PlayerImpl,
|
||||
private readonly mg: Game,
|
||||
readonly requestor_: Player,
|
||||
readonly recipient_: Player,
|
||||
readonly createdAtTick_: Tick,
|
||||
) {}
|
||||
|
||||
other(player: Player): PlayerImpl {
|
||||
if (this.requestor_ == player) {
|
||||
other(player: Player): Player {
|
||||
if (this.requestor_ === player) {
|
||||
return this.recipient_;
|
||||
}
|
||||
return this.requestor_;
|
||||
|
||||
@@ -44,12 +44,12 @@ export class AttackImpl implements Attack {
|
||||
if (this._target.isPlayer()) {
|
||||
(this._target as PlayerImpl)._incomingAttacks = (
|
||||
this._target as PlayerImpl
|
||||
)._incomingAttacks.filter((a) => a != this);
|
||||
)._incomingAttacks.filter((a) => a !== this);
|
||||
}
|
||||
|
||||
(this._attacker as PlayerImpl)._outgoingAttacks = (
|
||||
this._attacker as PlayerImpl
|
||||
)._outgoingAttacks.filter((a) => a != this);
|
||||
)._outgoingAttacks.filter((a) => a !== this);
|
||||
|
||||
this._isActive = false;
|
||||
}
|
||||
|
||||
+12
-11
@@ -302,7 +302,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.
|
||||
@@ -341,20 +341,20 @@ 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;
|
||||
|
||||
setOwner(owner: Player): void;
|
||||
setCooldown(triggerCooldown: boolean): void;
|
||||
ticksLeftInCooldown(cooldownDuration: number): Tick;
|
||||
isCooldown(): boolean;
|
||||
setDstPort(dstPort: Unit): void;
|
||||
dstPort(): Unit; // Only for trade ships
|
||||
dstPort(): Unit | null; // Only for trade ships
|
||||
setSafeFromPirates(): void; // Only for trade ships
|
||||
isSafeFromPirates(): boolean; // Only for trade ships
|
||||
detonationDst(): TileRef; // Only for nukes
|
||||
detonationDst(): TileRef | null; // Only for nukes
|
||||
|
||||
setMoveTarget(cell: TileRef): void;
|
||||
setMoveTarget(cell: TileRef | null): void;
|
||||
moveTarget(): TileRef | null;
|
||||
|
||||
setTargetedBySAM(targeted: boolean): void;
|
||||
@@ -374,7 +374,7 @@ export interface Unit {
|
||||
|
||||
export interface TerraNullius {
|
||||
isPlayer(): false;
|
||||
id(): PlayerID; // always zero, maybe make it TerraNulliusID?
|
||||
id(): null;
|
||||
clientID(): ClientID;
|
||||
smallID(): number;
|
||||
}
|
||||
@@ -391,7 +391,7 @@ export interface Player {
|
||||
info(): PlayerInfo;
|
||||
name(): string;
|
||||
displayName(): string;
|
||||
clientID(): ClientID;
|
||||
clientID(): ClientID | null;
|
||||
id(): PlayerID;
|
||||
type(): PlayerType;
|
||||
isPlayer(): this is Player;
|
||||
@@ -462,7 +462,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;
|
||||
@@ -494,7 +494,7 @@ export interface Player {
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef,
|
||||
sourceTile: TileRef | null,
|
||||
): Attack;
|
||||
outgoingAttacks(): Attack[];
|
||||
incomingAttacks(): Attack[];
|
||||
@@ -510,6 +510,7 @@ export interface Player {
|
||||
}
|
||||
|
||||
export interface Game extends GameMap {
|
||||
expireAlliance(alliance: Alliance);
|
||||
// Map & Dimensions
|
||||
isOnMap(cell: Cell): boolean;
|
||||
width(): number;
|
||||
@@ -557,7 +558,7 @@ export interface Game extends GameMap {
|
||||
unitID: number,
|
||||
message: string,
|
||||
type: MessageType,
|
||||
playerID: PlayerID,
|
||||
playerID: PlayerID | null,
|
||||
): void;
|
||||
|
||||
displayChat(
|
||||
|
||||
+44
-37
@@ -36,7 +36,6 @@ import { StatsImpl } from "./StatsImpl";
|
||||
import { assignTeams } from "./TeamAssignment";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { UnitGrid } from "./UnitGrid";
|
||||
import { UnitImpl } from "./UnitImpl";
|
||||
|
||||
export function createGame(
|
||||
humans: PlayerInfo[],
|
||||
@@ -56,7 +55,7 @@ export class GameImpl implements Game {
|
||||
private unInitExecs: Execution[] = [];
|
||||
|
||||
_players: Map<PlayerID, PlayerImpl> = new Map<PlayerID, PlayerImpl>();
|
||||
_playersBySmallID = [];
|
||||
_playersBySmallID: Player[] = [];
|
||||
|
||||
private execs: Execution[] = [];
|
||||
private _width: number;
|
||||
@@ -119,7 +118,7 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
private addPlayers() {
|
||||
if (this.config().gameConfig().gameMode != GameMode.Team) {
|
||||
if (this.config().gameConfig().gameMode !== GameMode.Team) {
|
||||
this._humans.forEach((p) => this.addPlayer(p));
|
||||
this._nations.forEach((n) => this.addPlayer(n.playerInfo));
|
||||
return;
|
||||
@@ -130,7 +129,7 @@ export class GameImpl implements Game {
|
||||
];
|
||||
const playerToTeam = assignTeams(allPlayers, this.playerTeams);
|
||||
for (const [playerInfo, team] of playerToTeam.entries()) {
|
||||
if (team == "kicked") {
|
||||
if (team === "kicked") {
|
||||
console.warn(`Player ${playerInfo.name} was kicked from team`);
|
||||
continue;
|
||||
}
|
||||
@@ -146,7 +145,7 @@ export class GameImpl implements Game {
|
||||
return this.playerBySmallID(this.ownerID(ref));
|
||||
}
|
||||
playerBySmallID(id: number): Player | TerraNullius {
|
||||
if (id == 0) {
|
||||
if (id === 0) {
|
||||
return this.terraNullius();
|
||||
}
|
||||
return this._playersBySmallID[id - 1];
|
||||
@@ -182,7 +181,7 @@ export class GameImpl implements Game {
|
||||
});
|
||||
}
|
||||
|
||||
units(...types: UnitType[]): UnitImpl[] {
|
||||
units(...types: UnitType[]): Unit[] {
|
||||
return Array.from(this._players.values()).flatMap((p) => p.units(...types));
|
||||
}
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
@@ -192,26 +191,29 @@ 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
|
||||
.incomingAllianceRequests()
|
||||
.find((ar) => ar.requestor() == requestor) != null
|
||||
.find((ar) => ar.requestor() === requestor) !== undefined
|
||||
) {
|
||||
consolex.log(`duplicate alliance request from ${requestor.name()}`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const correspondingReq = requestor
|
||||
.incomingAllianceRequests()
|
||||
.find((ar) => ar.requestor() == recipient);
|
||||
if (correspondingReq != null) {
|
||||
.find((ar) => ar.requestor() === recipient);
|
||||
if (correspondingReq !== undefined) {
|
||||
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);
|
||||
@@ -220,7 +222,9 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
acceptAllianceRequest(request: AllianceRequestImpl) {
|
||||
this.allianceRequests = this.allianceRequests.filter((ar) => ar != request);
|
||||
this.allianceRequests = this.allianceRequests.filter(
|
||||
(ar) => ar !== request,
|
||||
);
|
||||
|
||||
const requestor = request.requestor();
|
||||
const recipient = request.recipient();
|
||||
@@ -250,7 +254,9 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
rejectAllianceRequest(request: AllianceRequestImpl) {
|
||||
this.allianceRequests = this.allianceRequests.filter((ar) => ar != request);
|
||||
this.allianceRequests = this.allianceRequests.filter(
|
||||
(ar) => ar !== request,
|
||||
);
|
||||
(request.requestor() as PlayerImpl).pastOutgoingAllianceRequests.push(
|
||||
request,
|
||||
);
|
||||
@@ -305,7 +311,7 @@ export class GameImpl implements Game {
|
||||
// Players change each to so always add them
|
||||
this.addUpdate(player.toUpdate());
|
||||
}
|
||||
if (this.ticks() % 10 == 0) {
|
||||
if (this.ticks() % 10 === 0) {
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Hash,
|
||||
tick: this.ticks(),
|
||||
@@ -375,7 +381,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,
|
||||
@@ -390,26 +396,27 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
private maybeAssignTeam(player: PlayerInfo): Team | null {
|
||||
if (this._config.gameConfig().gameMode != GameMode.Team) {
|
||||
if (this._config.gameConfig().gameMode !== GameMode.Team) {
|
||||
return null;
|
||||
}
|
||||
if (player.playerType == PlayerType.Bot) {
|
||||
if (player.playerType === PlayerType.Bot) {
|
||||
return this.botTeam;
|
||||
}
|
||||
const rand = simpleHash(player.id);
|
||||
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 (player === undefined) {
|
||||
throw new Error(`Player with id ${id} not found`);
|
||||
}
|
||||
return this._players.get(id);
|
||||
return player;
|
||||
}
|
||||
|
||||
playerByClientID(id: ClientID): Player | null {
|
||||
for (const [, player] of this._players) {
|
||||
if (player.clientID() == id) {
|
||||
if (player.clientID() === id) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
@@ -511,7 +518,7 @@ export class GameImpl implements Game {
|
||||
return false;
|
||||
}
|
||||
for (const neighbor of this.neighbors(tile)) {
|
||||
const bordersEnemy = this.owner(tile) != this.owner(neighbor);
|
||||
const bordersEnemy = this.owner(tile) !== this.owner(neighbor);
|
||||
if (bordersEnemy) {
|
||||
return true;
|
||||
}
|
||||
@@ -528,8 +535,8 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
public breakAlliance(breaker: Player, alliance: Alliance) {
|
||||
let other: Player = null;
|
||||
if (alliance.requestor() == breaker) {
|
||||
let other: Player;
|
||||
if (alliance.requestor() === breaker) {
|
||||
other = alliance.recipient();
|
||||
} else {
|
||||
other = alliance.requestor();
|
||||
@@ -545,12 +552,12 @@ export class GameImpl implements Game {
|
||||
|
||||
const breakerSet = new Set(breaker.alliances());
|
||||
const alliances = other.alliances().filter((a) => breakerSet.has(a));
|
||||
if (alliances.length != 1) {
|
||||
if (alliances.length !== 1) {
|
||||
throw new Error(
|
||||
`must have exactly one alliance, have ${alliances.length}`,
|
||||
);
|
||||
}
|
||||
this.alliances_ = this.alliances_.filter((a) => a != alliances[0]);
|
||||
this.alliances_ = this.alliances_.filter((a) => a !== alliances[0]);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.BrokeAlliance,
|
||||
traitorID: breaker.smallID(),
|
||||
@@ -564,12 +571,12 @@ export class GameImpl implements Game {
|
||||
.requestor()
|
||||
.alliances()
|
||||
.filter((a) => p1Set.has(a));
|
||||
if (alliances.length != 1) {
|
||||
if (alliances.length !== 1) {
|
||||
throw new Error(
|
||||
`cannot expire alliance: must have exactly one alliance, have ${alliances.length}`,
|
||||
);
|
||||
}
|
||||
this.alliances_ = this.alliances_.filter((a) => a != alliances[0]);
|
||||
this.alliances_ = this.alliances_.filter((a) => a !== alliances[0]);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.AllianceExpired,
|
||||
player1ID: alliance.requestor().smallID(),
|
||||
@@ -594,7 +601,7 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
teams(): Team[] {
|
||||
if (this._config.gameConfig().gameMode != GameMode.Team) {
|
||||
if (this._config.gameConfig().gameMode !== GameMode.Team) {
|
||||
return [];
|
||||
}
|
||||
return [this.botTeam, ...this.playerTeams];
|
||||
@@ -605,8 +612,8 @@ export class GameImpl implements Game {
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null,
|
||||
): void {
|
||||
let id = null;
|
||||
if (playerID != null) {
|
||||
let id: number | null = null;
|
||||
if (playerID !== null) {
|
||||
id = this.player(playerID).smallID();
|
||||
}
|
||||
this.addUpdate({
|
||||
@@ -622,11 +629,11 @@ export class GameImpl implements Game {
|
||||
category: string,
|
||||
variables: Record<string, string> = {},
|
||||
playerID: PlayerID | null,
|
||||
isFrom: boolean | null = null,
|
||||
isFrom: boolean,
|
||||
recipient: string,
|
||||
): void {
|
||||
let id = null;
|
||||
if (playerID != null) {
|
||||
let id: number | null = null;
|
||||
if (playerID !== null) {
|
||||
id = this.player(playerID).smallID();
|
||||
}
|
||||
this.addUpdate({
|
||||
@@ -635,7 +642,7 @@ export class GameImpl implements Game {
|
||||
category: category,
|
||||
variables: variables,
|
||||
playerID: id,
|
||||
isFrom: isFrom,
|
||||
isFrom,
|
||||
recipient: recipient,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class GameMapImpl implements GameMap {
|
||||
}
|
||||
|
||||
hasOwner(ref: TileRef): boolean {
|
||||
return this.ownerID(ref) != 0;
|
||||
return this.ownerID(ref) !== 0;
|
||||
}
|
||||
|
||||
setOwnerID(ref: TileRef, playerId: number): void {
|
||||
@@ -207,12 +207,14 @@ export class GameMapImpl implements GameMap {
|
||||
isOnEdgeOfMap(ref: TileRef): boolean {
|
||||
const x = this.x(ref);
|
||||
const y = this.y(ref);
|
||||
return x == 0 || x == this.width() - 1 || y == 0 || y == this.height() - 1;
|
||||
return (
|
||||
x === 0 || x === this.width() - 1 || y === 0 || y === this.height() - 1
|
||||
);
|
||||
}
|
||||
|
||||
isBorder(ref: TileRef): boolean {
|
||||
return this.neighbors(ref).some(
|
||||
(tr) => this.ownerID(tr) != this.ownerID(ref),
|
||||
(tr) => this.ownerID(tr) !== this.ownerID(ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,6 +299,7 @@ export class GameMapImpl implements GameMap {
|
||||
|
||||
while (q.length > 0) {
|
||||
const curr = q.pop();
|
||||
if (curr === undefined) continue;
|
||||
for (const n of this.neighbors(curr)) {
|
||||
if (!seen.has(n) && filter(this, n)) {
|
||||
seen.add(n);
|
||||
|
||||
@@ -92,8 +92,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;
|
||||
|
||||
+47
-52
@@ -57,7 +57,7 @@ export class UnitView {
|
||||
}
|
||||
|
||||
lastTile(): TileRef {
|
||||
if (this.lastPos.length == 0) {
|
||||
if (this.lastPos.length === 0) {
|
||||
return this.data.pos;
|
||||
}
|
||||
return this.lastPos[0];
|
||||
@@ -89,7 +89,7 @@ export class UnitView {
|
||||
return this.data.isActive;
|
||||
}
|
||||
hasHealth(): boolean {
|
||||
return this.data.health != undefined;
|
||||
return this.data.health !== undefined;
|
||||
}
|
||||
health(): number {
|
||||
return this.data.health ?? 0;
|
||||
@@ -97,28 +97,26 @@ export class UnitView {
|
||||
constructionType(): UnitType | undefined {
|
||||
return this.data.constructionType;
|
||||
}
|
||||
dstPortId(): number {
|
||||
if (this.type() != UnitType.TradeShip) {
|
||||
throw Error("Must be a trade ship");
|
||||
}
|
||||
dstPortId(): number | undefined {
|
||||
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 {
|
||||
if (this.type() != UnitType.Warship) {
|
||||
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 (this.data.ticksLeftInCooldown === undefined) return false;
|
||||
return this.data.ticksLeftInCooldown > 0;
|
||||
}
|
||||
}
|
||||
@@ -131,13 +129,11 @@ export class PlayerView {
|
||||
public data: PlayerUpdate,
|
||||
public nameData: NameViewData,
|
||||
) {
|
||||
if (data.clientID == game.myClientID()) {
|
||||
if (data.clientID === game.myClientID()) {
|
||||
this.anonymousName = this.data.name;
|
||||
} else {
|
||||
this.anonymousName = createRandomName(
|
||||
this.data.name,
|
||||
this.data.playerType,
|
||||
);
|
||||
this.anonymousName =
|
||||
createRandomName(this.data.name, this.data.playerType) ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +160,7 @@ export class PlayerView {
|
||||
units(...types: UnitType[]): UnitView[] {
|
||||
return this.game
|
||||
.units(...types)
|
||||
.filter((u) => u.owner().smallID() == this.smallID());
|
||||
.filter((u) => u.owner().smallID() === this.smallID());
|
||||
}
|
||||
|
||||
nameLocation(): NameViewData {
|
||||
@@ -174,7 +170,7 @@ export class PlayerView {
|
||||
smallID(): number {
|
||||
return this.data.smallID;
|
||||
}
|
||||
flag(): string {
|
||||
flag(): string | undefined {
|
||||
return this.data.flag;
|
||||
}
|
||||
name(): string {
|
||||
@@ -188,7 +184,7 @@ export class PlayerView {
|
||||
: this.data.name;
|
||||
}
|
||||
|
||||
clientID(): ClientID {
|
||||
clientID(): ClientID | null {
|
||||
return this.data.clientID;
|
||||
}
|
||||
id(): PlayerID {
|
||||
@@ -236,11 +232,11 @@ export class PlayerView {
|
||||
}
|
||||
|
||||
isAlliedWith(other: PlayerView): boolean {
|
||||
return this.data.allies.some((n) => other.smallID() == n);
|
||||
return this.data.allies.some((n) => other.smallID() === n);
|
||||
}
|
||||
|
||||
isOnSameTeam(other: PlayerView): boolean {
|
||||
return this.data.team != null && this.data.team == other.data.team;
|
||||
return this.data.team !== undefined && this.data.team === other.data.team;
|
||||
}
|
||||
|
||||
isFriendly(other: PlayerView): boolean {
|
||||
@@ -248,7 +244,7 @@ export class PlayerView {
|
||||
}
|
||||
|
||||
isRequestingAllianceWith(other: PlayerView) {
|
||||
return this.data.outgoingAllianceRequests.some((id) => other.id() == id);
|
||||
return this.data.outgoingAllianceRequests.some((id) => other.id() === id);
|
||||
}
|
||||
|
||||
hasEmbargoAgainst(other: PlayerView): boolean {
|
||||
@@ -291,7 +287,7 @@ export class PlayerView {
|
||||
}
|
||||
|
||||
export class GameView implements GameMap {
|
||||
private lastUpdate: GameUpdateViewData;
|
||||
private lastUpdate: GameUpdateViewData | null;
|
||||
private smallIDToID = new Map<number, PlayerID>();
|
||||
private _players = new Map<PlayerID, PlayerView>();
|
||||
private _units = new Map<number, UnitView>();
|
||||
@@ -311,21 +307,15 @@ export class GameView implements GameMap {
|
||||
private _myClientID: ClientID,
|
||||
private _gameID: GameID,
|
||||
) {
|
||||
this.lastUpdate = {
|
||||
tick: 0,
|
||||
packedTileUpdates: new BigUint64Array([]),
|
||||
// TODO: make this empty map instead of null?
|
||||
updates: null,
|
||||
playerNameViewData: {},
|
||||
};
|
||||
this.lastUpdate = null;
|
||||
this.unitGrid = new UnitGrid(_map);
|
||||
}
|
||||
isOnEdgeOfMap(ref: TileRef): boolean {
|
||||
return this._map.isOnEdgeOfMap(ref);
|
||||
}
|
||||
|
||||
public updatesSinceLastTick(): GameUpdates {
|
||||
return this.lastUpdate.updates;
|
||||
public updatesSinceLastTick(): GameUpdates | null {
|
||||
return this.lastUpdate?.updates ?? null;
|
||||
}
|
||||
|
||||
public update(gu: GameUpdateViewData) {
|
||||
@@ -339,11 +329,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 (player !== undefined) {
|
||||
player.data = pu;
|
||||
player.nameData = gu.playerNameViewData[pu.id];
|
||||
} else {
|
||||
this._players.set(
|
||||
pu.id,
|
||||
@@ -356,9 +350,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 (unit !== undefined) {
|
||||
unit.update(update);
|
||||
} else {
|
||||
unit = new UnitView(this, update);
|
||||
@@ -405,17 +398,18 @@ export class GameView implements GameMap {
|
||||
}
|
||||
|
||||
myPlayer(): PlayerView | null {
|
||||
if (this._myPlayer == null) {
|
||||
if (this._myPlayer === null) {
|
||||
this._myPlayer = this.playerByClientID(this._myClientID);
|
||||
}
|
||||
return this._myPlayer;
|
||||
}
|
||||
|
||||
player(id: PlayerID): PlayerView {
|
||||
if (this._players.has(id)) {
|
||||
return this._players.get(id);
|
||||
const player = this._players.get(id);
|
||||
if (player === undefined) {
|
||||
throw Error(`player id ${id} not found`);
|
||||
}
|
||||
throw Error(`player id ${id} not found`);
|
||||
return player;
|
||||
}
|
||||
|
||||
players(): PlayerView[] {
|
||||
@@ -423,20 +417,22 @@ export class GameView implements GameMap {
|
||||
}
|
||||
|
||||
playerBySmallID(id: number): PlayerView | TerraNullius {
|
||||
if (id == 0) {
|
||||
if (id === 0) {
|
||||
return new TerraNulliusImpl();
|
||||
}
|
||||
if (!this.smallIDToID.has(id)) {
|
||||
const playerId = this.smallIDToID.get(id);
|
||||
if (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 {
|
||||
const player =
|
||||
Array.from(this._players.values()).filter((p) => p.clientID() == id)[0] ??
|
||||
null;
|
||||
if (player == null) {
|
||||
Array.from(this._players.values()).filter(
|
||||
(p) => p.clientID() === id,
|
||||
)[0] ?? null;
|
||||
if (player === null) {
|
||||
return null;
|
||||
}
|
||||
return player;
|
||||
@@ -453,23 +449,24 @@ export class GameView implements GameMap {
|
||||
}
|
||||
|
||||
ticks(): Tick {
|
||||
if (this.lastUpdate === null) return 0;
|
||||
return this.lastUpdate.tick;
|
||||
}
|
||||
inSpawnPhase(): boolean {
|
||||
return this.lastUpdate.tick <= this._config.numSpawnPhaseTurns();
|
||||
return this.ticks() <= this._config.numSpawnPhaseTurns();
|
||||
}
|
||||
config(): Config {
|
||||
return this._config;
|
||||
}
|
||||
units(...types: UnitType[]): UnitView[] {
|
||||
if (types.length == 0) {
|
||||
if (types.length === 0) {
|
||||
return Array.from(this._units.values()).filter((u) => u.isActive());
|
||||
}
|
||||
return Array.from(this._units.values()).filter(
|
||||
(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 {
|
||||
@@ -582,8 +579,6 @@ export class GameView implements GameMap {
|
||||
focusedPlayer(): PlayerView | null {
|
||||
// TODO: renable when performance issues are fixed.
|
||||
return this.myPlayer();
|
||||
if (userSettings.focusLocked()) return this.myPlayer();
|
||||
return this._focusedPlayer;
|
||||
}
|
||||
setFocusedPlayer(player: PlayerView | null): void {
|
||||
this._focusedPlayer = player;
|
||||
|
||||
+69
-77
@@ -42,7 +42,6 @@ import {
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { andFN, manhattanDistFN, TileRef } from "./GameMap";
|
||||
import { AttackUpdate, GameUpdateType, PlayerUpdate } from "./GameUpdates";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import {
|
||||
bestShoreDeploymentSource,
|
||||
canBuildTransportShip,
|
||||
@@ -78,10 +77,10 @@ export class PlayerImpl implements Player {
|
||||
|
||||
public _borderTiles: Set<TileRef> = new Set();
|
||||
|
||||
public _units: UnitImpl[] = [];
|
||||
public _units: Unit[] = [];
|
||||
public _tiles: Set<TileRef> = new Set();
|
||||
|
||||
private _flag: string;
|
||||
private _flag: string | undefined;
|
||||
private _name: string;
|
||||
private _displayName: string;
|
||||
|
||||
@@ -132,7 +131,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(),
|
||||
@@ -177,7 +176,7 @@ export class PlayerImpl implements Player {
|
||||
return this._smallID;
|
||||
}
|
||||
|
||||
flag(): string {
|
||||
flag(): string | undefined {
|
||||
return this._flag;
|
||||
}
|
||||
|
||||
@@ -188,7 +187,7 @@ export class PlayerImpl implements Player {
|
||||
return this._displayName;
|
||||
}
|
||||
|
||||
clientID(): ClientID {
|
||||
clientID(): ClientID | null {
|
||||
return this.playerInfo.clientID;
|
||||
}
|
||||
|
||||
@@ -204,8 +203,8 @@ export class PlayerImpl implements Player {
|
||||
return this.playerInfo.clan;
|
||||
}
|
||||
|
||||
units(...types: UnitType[]): UnitImpl[] {
|
||||
if (types.length == 0) {
|
||||
units(...types: UnitType[]): Unit[] {
|
||||
if (types.length === 0) {
|
||||
return this._units;
|
||||
}
|
||||
const ts = new Set(types);
|
||||
@@ -216,7 +215,7 @@ export class PlayerImpl implements Player {
|
||||
const units = this.units(type);
|
||||
units.push(
|
||||
...this.units(UnitType.Construction).filter(
|
||||
(u) => u.constructionType() == type,
|
||||
(u) => u.constructionType() === type,
|
||||
),
|
||||
);
|
||||
return units;
|
||||
@@ -225,7 +224,7 @@ export class PlayerImpl implements Player {
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean {
|
||||
for (const border of this._borderTiles) {
|
||||
for (const neighbor of this.mg.map().neighbors(border)) {
|
||||
if (this.mg.map().ownerID(neighbor) == other.smallID()) {
|
||||
if (this.mg.map().ownerID(neighbor) === other.smallID()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -250,10 +249,8 @@ export class PlayerImpl implements Player {
|
||||
for (const neighbor of this.mg.map().neighbors(border)) {
|
||||
if (this.mg.map().isLand(neighbor)) {
|
||||
const owner = this.mg.map().ownerID(neighbor);
|
||||
if (owner != this.smallID()) {
|
||||
ns.add(
|
||||
this.mg.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl,
|
||||
);
|
||||
if (owner !== this.smallID()) {
|
||||
ns.add(this.mg.playerBySmallID(owner) as Player | TerraNullius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +268,7 @@ export class PlayerImpl implements Player {
|
||||
this.mg.conquer(this, tile);
|
||||
}
|
||||
orderRetreat(id: string) {
|
||||
const attack = this._outgoingAttacks.filter((attack) => attack.id() == id);
|
||||
const attack = this._outgoingAttacks.filter((attack) => attack.id() === id);
|
||||
if (!attack || !attack[0]) {
|
||||
consolex.warn(`Didn't find outgoing attack with id ${id}`);
|
||||
return;
|
||||
@@ -279,7 +276,7 @@ export class PlayerImpl implements Player {
|
||||
attack[0].orderRetreat();
|
||||
}
|
||||
executeRetreat(id: string): void {
|
||||
const attack = this._outgoingAttacks.filter((attack) => attack.id() == id);
|
||||
const attack = this._outgoingAttacks.filter((attack) => attack.id() === id);
|
||||
// Execution is delayed so it's not an error that the attack does not exist.
|
||||
if (!attack || !attack[0]) {
|
||||
return;
|
||||
@@ -287,7 +284,7 @@ export class PlayerImpl implements Player {
|
||||
attack[0].executeRetreat();
|
||||
}
|
||||
relinquish(tile: TileRef) {
|
||||
if (this.mg.owner(tile) != this) {
|
||||
if (this.mg.owner(tile) !== this) {
|
||||
throw new Error(`Cannot relinquish tile not owned by this player`);
|
||||
}
|
||||
this.mg.relinquish(tile);
|
||||
@@ -308,16 +305,16 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
incomingAllianceRequests(): AllianceRequest[] {
|
||||
return this.mg.allianceRequests.filter((ar) => ar.recipient() == this);
|
||||
return this.mg.allianceRequests.filter((ar) => ar.recipient() === this);
|
||||
}
|
||||
|
||||
outgoingAllianceRequests(): AllianceRequest[] {
|
||||
return this.mg.allianceRequests.filter((ar) => ar.requestor() == this);
|
||||
return this.mg.allianceRequests.filter((ar) => ar.requestor() === this);
|
||||
}
|
||||
|
||||
alliances(): MutableAlliance[] {
|
||||
return this.mg.alliances_.filter(
|
||||
(a) => a.requestor() == this || a.recipient() == this,
|
||||
(a) => a.requestor() === this || a.recipient() === this,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -326,23 +323,25 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
isAlliedWith(other: Player): boolean {
|
||||
if (other == this) {
|
||||
if (other === this) {
|
||||
return false;
|
||||
}
|
||||
return this.allianceWith(other) != null;
|
||||
return this.allianceWith(other) !== null;
|
||||
}
|
||||
|
||||
allianceWith(other: Player): MutableAlliance | null {
|
||||
if (other == this) {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
canSendAllianceRequest(other: Player): boolean {
|
||||
if (other == this) {
|
||||
if (other === this) {
|
||||
return false;
|
||||
}
|
||||
if (this.isFriendly(other)) {
|
||||
@@ -350,20 +349,18 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
const hasPending =
|
||||
this.incomingAllianceRequests().find((ar) => ar.requestor() == other) !=
|
||||
null ||
|
||||
this.outgoingAllianceRequests().find((ar) => ar.recipient() == other) !=
|
||||
null;
|
||||
this.incomingAllianceRequests().some((ar) => ar.requestor() === other) ||
|
||||
this.outgoingAllianceRequests().some((ar) => ar.recipient() === other);
|
||||
|
||||
if (hasPending) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const recent = this.pastOutgoingAllianceRequests
|
||||
.filter((ar) => ar.recipient() == other)
|
||||
.filter((ar) => ar.recipient() === other)
|
||||
.sort((a, b) => b.createdAt() - a.createdAt());
|
||||
|
||||
if (recent.length == 0) {
|
||||
if (recent.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -387,7 +384,7 @@ export class PlayerImpl implements Player {
|
||||
this.markedTraitorTick = this.mg.ticks();
|
||||
}
|
||||
|
||||
createAllianceRequest(recipient: Player): AllianceRequest {
|
||||
createAllianceRequest(recipient: Player): AllianceRequest | null {
|
||||
if (this.isAlliedWith(recipient)) {
|
||||
throw new Error(`cannot create alliance request, already allies`);
|
||||
}
|
||||
@@ -395,13 +392,11 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
relation(other: Player): Relation {
|
||||
if (other == this) {
|
||||
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 {
|
||||
@@ -427,13 +422,10 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
updateRelation(other: Player, delta: number): void {
|
||||
if (other == this) {
|
||||
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);
|
||||
}
|
||||
@@ -451,7 +443,7 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
canTarget(other: Player): boolean {
|
||||
if (this == other) {
|
||||
if (this === other) {
|
||||
return false;
|
||||
}
|
||||
if (this.isFriendly(other)) {
|
||||
@@ -487,13 +479,13 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void {
|
||||
if (recipient == this) {
|
||||
if (recipient === this) {
|
||||
throw Error(`Cannot send emoji to oneself: ${this}`);
|
||||
}
|
||||
const msg: EmojiMessage = {
|
||||
message: emoji,
|
||||
senderID: this.smallID(),
|
||||
recipientID: recipient == AllPlayers ? recipient : recipient.smallID(),
|
||||
recipientID: recipient === AllPlayers ? recipient : recipient.smallID(),
|
||||
createdAt: this.mg.ticks(),
|
||||
};
|
||||
this.outgoingEmojis_.push(msg);
|
||||
@@ -512,9 +504,9 @@ export class PlayerImpl implements Player {
|
||||
|
||||
canSendEmoji(recipient: Player | typeof AllPlayers): boolean {
|
||||
const recipientID =
|
||||
recipient == AllPlayers ? AllPlayers : recipient.smallID();
|
||||
recipient === AllPlayers ? AllPlayers : recipient.smallID();
|
||||
const prevMsgs = this.outgoingEmojis_.filter(
|
||||
(msg) => msg.recipientID == recipientID,
|
||||
(msg) => msg.recipientID === recipientID,
|
||||
);
|
||||
for (const msg of prevMsgs) {
|
||||
if (
|
||||
@@ -532,7 +524,7 @@ export class PlayerImpl implements Player {
|
||||
return false;
|
||||
}
|
||||
for (const donation of this.sentDonations) {
|
||||
if (donation.recipient == recipient) {
|
||||
if (donation.recipient === recipient) {
|
||||
if (
|
||||
this.mg.ticks() - donation.tick <
|
||||
this.mg.config().donateCooldown()
|
||||
@@ -580,12 +572,12 @@ export class PlayerImpl implements Player {
|
||||
canTrade(other: Player): boolean {
|
||||
const embargo =
|
||||
other.hasEmbargoAgainst(this) || this.hasEmbargoAgainst(other);
|
||||
return !embargo && other.id() != this.id();
|
||||
return !embargo && other.id() !== this.id();
|
||||
}
|
||||
|
||||
addEmbargo(other: PlayerID, isTemporary: boolean): void {
|
||||
if (this.embargoes.has(other) && !this.embargoes.get(other).isTemporary)
|
||||
return;
|
||||
const embargo = this.embargoes.get(other);
|
||||
if (embargo !== undefined && !embargo.isTemporary) return;
|
||||
|
||||
this.embargoes.set(other, {
|
||||
createdAt: this.mg.ticks(),
|
||||
@@ -603,8 +595,8 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
endTemporaryEmbargo(other: PlayerID): void {
|
||||
if (this.embargoes.has(other) && !this.embargoes.get(other).isTemporary)
|
||||
return;
|
||||
const embargo = this.embargoes.get(other);
|
||||
if (embargo !== undefined && !embargo.isTemporary) return;
|
||||
|
||||
this.stopEmbargo(other);
|
||||
}
|
||||
@@ -612,7 +604,7 @@ export class PlayerImpl implements Player {
|
||||
tradingPartners(): Player[] {
|
||||
return this.mg
|
||||
.players()
|
||||
.filter((other) => other != this && this.canTrade(other));
|
||||
.filter((other) => other !== this && this.canTrade(other));
|
||||
}
|
||||
|
||||
team(): Team | null {
|
||||
@@ -620,16 +612,16 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
isOnSameTeam(other: Player): boolean {
|
||||
if (other == this) {
|
||||
if (other === this) {
|
||||
return false;
|
||||
}
|
||||
if (this.team() == null || other.team() == null) {
|
||||
if (this.team() === null || other.team() === null) {
|
||||
return false;
|
||||
}
|
||||
if (this.team() == ColoredTeams.Bot || other.team() == ColoredTeams.Bot) {
|
||||
if (this.team() === ColoredTeams.Bot || other.team() === ColoredTeams.Bot) {
|
||||
return false;
|
||||
}
|
||||
return this._team == other.team();
|
||||
return this._team === other.team();
|
||||
}
|
||||
|
||||
isFriendly(other: Player): boolean {
|
||||
@@ -700,7 +692,7 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
captureUnit(unit: Unit): void {
|
||||
if (unit.owner() == this) {
|
||||
if (unit.owner() === this) {
|
||||
throw new Error(`Cannot capture unit, ${this} already owns ${unit}`);
|
||||
}
|
||||
unit.setOwner(this);
|
||||
@@ -710,7 +702,7 @@ export class PlayerImpl implements Player {
|
||||
type: T,
|
||||
spawnTile: TileRef,
|
||||
params: UnitParams<T>,
|
||||
): UnitImpl {
|
||||
): Unit {
|
||||
if (this.mg.config().isUnitDisabled(type)) {
|
||||
throw new Error(
|
||||
`Attempted to build disabled unit ${type} at tile ${spawnTile} by player ${this.name()}`,
|
||||
@@ -728,7 +720,7 @@ export class PlayerImpl implements Player {
|
||||
);
|
||||
this._units.push(b);
|
||||
this.removeGold(cost);
|
||||
this.removeTroops("troops" in params ? params.troops : 0);
|
||||
this.removeTroops("troops" in params ? (params.troops ?? 0) : 0);
|
||||
this.mg.addUpdate(b.toUpdate());
|
||||
this.mg.addUnit(b);
|
||||
|
||||
@@ -807,20 +799,20 @@ export class PlayerImpl implements Player {
|
||||
return !silo.isCooldown();
|
||||
})
|
||||
.sort(distSortUnit(this.mg, tile));
|
||||
if (spawns.length == 0) {
|
||||
if (spawns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return spawns[0].tile();
|
||||
}
|
||||
|
||||
portSpawn(tile: TileRef, validTiles: TileRef[]): TileRef | false {
|
||||
portSpawn(tile: TileRef, validTiles: TileRef[] | null): TileRef | false {
|
||||
const spawns = Array.from(
|
||||
this.mg.bfs(
|
||||
tile,
|
||||
manhattanDistFN(tile, this.mg.config().radiusPortSpawn()),
|
||||
),
|
||||
)
|
||||
.filter((t) => this.mg.owner(t) == this && this.mg.isOceanShore(t))
|
||||
.filter((t) => this.mg.owner(t) === this && this.mg.isOceanShore(t))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
|
||||
@@ -845,7 +837,7 @@ export class PlayerImpl implements Player {
|
||||
this.mg.manhattanDist(a.tile(), tile) -
|
||||
this.mg.manhattanDist(b.tile(), tile),
|
||||
);
|
||||
if (spawns.length == 0) {
|
||||
if (spawns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return spawns[0].tile();
|
||||
@@ -856,14 +848,14 @@ export class PlayerImpl implements Player {
|
||||
validTiles: TileRef[] | null = null,
|
||||
): TileRef | false {
|
||||
const tiles = validTiles ?? this.validStructureSpawnTiles(tile);
|
||||
if (tiles.length == 0) {
|
||||
if (tiles.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return tiles[0];
|
||||
}
|
||||
|
||||
private validStructureSpawnTiles(tile: TileRef): TileRef[] {
|
||||
if (this.mg.owner(tile) != this) {
|
||||
if (this.mg.owner(tile) !== this) {
|
||||
return [];
|
||||
}
|
||||
const searchRadius = 15;
|
||||
@@ -878,7 +870,7 @@ export class PlayerImpl implements Player {
|
||||
const nearbyTiles = this.mg.bfs(tile, (gm, t) => {
|
||||
return (
|
||||
this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared &&
|
||||
gm.ownerID(t) == this.smallID()
|
||||
gm.ownerID(t) === this.smallID()
|
||||
);
|
||||
});
|
||||
const validSet: Set<TileRef> = new Set(nearbyTiles);
|
||||
@@ -903,9 +895,9 @@ export class PlayerImpl implements Player {
|
||||
|
||||
tradeShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
const spawns = this.units(UnitType.Port).filter(
|
||||
(u) => u.tile() == targetTile,
|
||||
(u) => u.tile() === targetTile,
|
||||
);
|
||||
if (spawns.length == 0) {
|
||||
if (spawns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return spawns[0].tile();
|
||||
@@ -917,7 +909,7 @@ export class PlayerImpl implements Player {
|
||||
hash(): number {
|
||||
return (
|
||||
simpleHash(this.id()) * (this.population() + this.numTilesOwned()) +
|
||||
this._units.reduce((acc, unit) => acc + unit.hash(), 0)
|
||||
this._units.reduce((acc, unit) => acc + (unit as UnitImpl).hash(), 0)
|
||||
);
|
||||
}
|
||||
toString(): string {
|
||||
@@ -944,7 +936,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(),
|
||||
@@ -976,7 +968,7 @@ export class PlayerImpl implements Player {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.mg.owner(tile) == this) {
|
||||
if (this.mg.owner(tile) === this) {
|
||||
return false;
|
||||
}
|
||||
if (this.mg.hasOwner(tile)) {
|
||||
@@ -1000,7 +992,7 @@ export class PlayerImpl implements Player {
|
||||
),
|
||||
)) {
|
||||
for (const n of this.mg.neighbors(t)) {
|
||||
if (this.mg.owner(n) == this) {
|
||||
if (this.mg.owner(n) === this) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1018,7 +1010,7 @@ export class PlayerImpl implements Player {
|
||||
tradingPorts(port: Unit): Unit[] {
|
||||
const ports = this.mg
|
||||
.players()
|
||||
.filter((p) => p != port.owner() && p.canTrade(port.owner()))
|
||||
.filter((p) => p !== port.owner() && p.canTrade(port.owner()))
|
||||
.flatMap((p) => p.units(UnitType.Port))
|
||||
.sort((p1, p2) => {
|
||||
return (
|
||||
@@ -1039,7 +1031,7 @@ export class PlayerImpl implements Player {
|
||||
// Make ally ports twice more likely by putting them again
|
||||
this.mg
|
||||
.players()
|
||||
.filter((p) => p != port.owner() && p.canTrade(port.owner()))
|
||||
.filter((p) => p !== port.owner() && p.canTrade(port.owner()))
|
||||
.filter((p) => p.isAlliedWith(port.owner()))
|
||||
.flatMap((p) => p.units(UnitType.Port))
|
||||
.forEach((p) => ports.push(p));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export function assignTeams(
|
||||
team = t;
|
||||
}
|
||||
|
||||
if (team === null) continue;
|
||||
|
||||
for (const player of clanPlayers) {
|
||||
if (teamSize < maxTeamSize) {
|
||||
teamSize++;
|
||||
@@ -63,6 +65,7 @@ export function assignTeams(
|
||||
teamSize = p;
|
||||
team = t;
|
||||
}
|
||||
if (team === null) continue;
|
||||
teamPlayerCount.set(team, teamSize + 1);
|
||||
result.set(player, team);
|
||||
}
|
||||
|
||||
@@ -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 (cached !== undefined) return cached;
|
||||
const mapFiles = await terrainMapFileLoader.getMapData(map);
|
||||
|
||||
const gameMap = await genTerrainFromBin(mapFiles.mapBin);
|
||||
@@ -45,7 +44,7 @@ export async function genTerrainFromBin(data: string): Promise<GameMap> {
|
||||
const width = (data.charCodeAt(1) << 8) | data.charCodeAt(0);
|
||||
const height = (data.charCodeAt(3) << 8) | data.charCodeAt(2);
|
||||
|
||||
if (data.length != width * height + 4) {
|
||||
if (data.length !== width * height + 4) {
|
||||
throw new Error(
|
||||
`Invalid data: buffer size ${data.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`,
|
||||
);
|
||||
|
||||
@@ -15,12 +15,12 @@ export function canBuildTransportShip(
|
||||
}
|
||||
|
||||
const dst = targetTransportTile(game, tile);
|
||||
if (dst == null) {
|
||||
if (dst === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const other = game.owner(tile);
|
||||
if (other == player) {
|
||||
if (other === player) {
|
||||
return false;
|
||||
}
|
||||
if (other.isPlayer() && player.isFriendly(other)) {
|
||||
@@ -71,7 +71,7 @@ export function canBuildTransportShip(
|
||||
);
|
||||
|
||||
for (const t of sorted) {
|
||||
if (game.owner(t) == player) {
|
||||
if (game.owner(t) === player) {
|
||||
return transportShipSpawn(game, player, t);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ function transportShipSpawn(
|
||||
return false;
|
||||
}
|
||||
const spawn = closestShoreFromPlayer(game, player, targetTile);
|
||||
if (spawn == null) {
|
||||
if (spawn === null) {
|
||||
return false;
|
||||
}
|
||||
return spawn;
|
||||
@@ -128,7 +128,7 @@ export function closestShoreFromPlayer(
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
||||
gm.isShore(t),
|
||||
);
|
||||
if (shoreTiles.length == 0) {
|
||||
if (shoreTiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -144,20 +144,18 @@ export function bestShoreDeploymentSource(
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | false {
|
||||
target = targetTransportTile(gm, target);
|
||||
if (target == null) {
|
||||
return false;
|
||||
}
|
||||
const t = targetTransportTile(gm, target);
|
||||
if (t === null) return false;
|
||||
|
||||
const candidates = candidateShoreTiles(gm, player, target);
|
||||
const aStar = new MiniAStar(gm, gm.miniMap(), candidates, target, 500_000, 1);
|
||||
const candidates = candidateShoreTiles(gm, player, t);
|
||||
const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 500_000, 1);
|
||||
const result = aStar.compute();
|
||||
if (result != PathFindResultType.Completed) {
|
||||
if (result !== PathFindResultType.Completed) {
|
||||
console.warn(`bestShoreDeploymentSource: path not found: ${result}`);
|
||||
return false;
|
||||
}
|
||||
const path = aStar.reconstructPath();
|
||||
if (path.length == 0) {
|
||||
if (path.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const potential = path[0];
|
||||
@@ -165,8 +163,8 @@ export function bestShoreDeploymentSource(
|
||||
// of the potential tile to find a valid deployment point
|
||||
const neighbors = gm
|
||||
.neighbors(potential)
|
||||
.filter((n) => gm.isShore(n) && gm.owner(n) == player);
|
||||
if (neighbors.length == 0) {
|
||||
.filter((n) => gm.isShore(n) && gm.owner(n) === player);
|
||||
if (neighbors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return neighbors[0];
|
||||
@@ -183,8 +181,8 @@ export function candidateShoreTiles(
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
let bestByManhattan: TileRef = null;
|
||||
const extremumTiles: Record<string, TileRef> = {
|
||||
let bestByManhattan: TileRef | null = null;
|
||||
const extremumTiles: Record<string, TileRef | null> = {
|
||||
minX: null,
|
||||
minY: null,
|
||||
maxX: null,
|
||||
@@ -237,7 +235,7 @@ export function candidateShoreTiles(
|
||||
extremumTiles.maxX,
|
||||
extremumTiles.maxY,
|
||||
...sampledTiles,
|
||||
].filter(Boolean);
|
||||
].filter(Boolean) as number[];
|
||||
|
||||
return candidates;
|
||||
}
|
||||
@@ -246,7 +244,7 @@ function closestShoreTN(
|
||||
gm: GameMap,
|
||||
tile: TileRef,
|
||||
searchDist: number,
|
||||
): TileRef {
|
||||
): TileRef | null {
|
||||
const tn = Array.from(
|
||||
gm.bfs(
|
||||
tile,
|
||||
@@ -255,7 +253,7 @@ function closestShoreTN(
|
||||
)
|
||||
.filter((t) => gm.isShore(t))
|
||||
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
||||
if (tn.length == 0) {
|
||||
if (tn.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return tn[0];
|
||||
|
||||
@@ -135,8 +135,8 @@ export class UnitGrid {
|
||||
for (let cx = startGridX; cx <= endGridX; cx++) {
|
||||
for (const unit of this.grid[cy][cx]) {
|
||||
if (
|
||||
unit.type() == type &&
|
||||
unit.owner().id() == playerId &&
|
||||
unit.type() === type &&
|
||||
unit.owner().id() === playerId &&
|
||||
unit.isActive()
|
||||
) {
|
||||
const distSquared = this.squaredDistanceFromTile(unit, tile);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user