mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-21 16:28:43 +00:00
feat: replay archived games, gamestate hash verification (#195)
create endpoint to load archived game. when joining game client first checks if the game is active, if not it requests the game archive from the server. the archive is sent to LocalServer to replay the game locally. Every 10 ticks a hash is stored on the archive, and during replay the LocalServer verifies this hash.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
GameID,
|
||||
ServerMessage,
|
||||
PlayerRecord,
|
||||
GameRecord,
|
||||
} from "../core/Schemas";
|
||||
import { loadTerrainMap } from "../core/game/TerrainMapLoader";
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ import { GameView, PlayerView } from "../core/game/GameView";
|
||||
import { GameUpdateViewData } from "../core/game/GameUpdates";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { LocalPersistantStats } from "./LocalPersistantStats";
|
||||
import { CreateGameRecord } from "../core/Util";
|
||||
import { createGameRecord } from "../core/Util";
|
||||
import { getPersistentIDFromCookie } from "./Main";
|
||||
|
||||
export interface LobbyConfig {
|
||||
@@ -40,15 +41,11 @@ export interface LobbyConfig {
|
||||
clientID: ClientID;
|
||||
playerID: PlayerID;
|
||||
persistentID: string;
|
||||
gameType: GameType;
|
||||
gameID: GameID;
|
||||
map: GameMapType | null;
|
||||
difficulty: Difficulty | null;
|
||||
infiniteGold: boolean | null;
|
||||
infiniteTroops: boolean | null;
|
||||
instantBuild: boolean | null;
|
||||
bots: number | null;
|
||||
disableNPCs: boolean | null;
|
||||
// GameConfig only exists when playing a singleplayer game.
|
||||
gameConfig?: GameConfig;
|
||||
// GameRecord exists when replaying an archived game.
|
||||
gameRecord?: GameRecord;
|
||||
}
|
||||
|
||||
export function joinLobby(
|
||||
@@ -63,28 +60,13 @@ export function joinLobby(
|
||||
);
|
||||
|
||||
const userSettings: UserSettings = new UserSettings();
|
||||
const gameConfig: GameConfig = {
|
||||
gameType: lobbyConfig.gameType,
|
||||
gameMap: lobbyConfig.map,
|
||||
difficulty: lobbyConfig.difficulty,
|
||||
disableNPCs: lobbyConfig.disableNPCs,
|
||||
bots: lobbyConfig.bots,
|
||||
infiniteGold: lobbyConfig.infiniteGold,
|
||||
infiniteTroops: lobbyConfig.infiniteTroops,
|
||||
instantBuild: lobbyConfig.instantBuild,
|
||||
};
|
||||
LocalPersistantStats.startGame(
|
||||
lobbyConfig.gameID,
|
||||
lobbyConfig.playerID,
|
||||
gameConfig,
|
||||
lobbyConfig.gameConfig,
|
||||
);
|
||||
|
||||
const transport = new Transport(
|
||||
lobbyConfig,
|
||||
gameConfig,
|
||||
eventBus,
|
||||
lobbyConfig.serverConfig,
|
||||
);
|
||||
const transport = new Transport(lobbyConfig, eventBus);
|
||||
|
||||
const onconnect = () => {
|
||||
consolex.log(`Joined game lobby ${lobbyConfig.gameID}`);
|
||||
@@ -94,13 +76,11 @@ export function joinLobby(
|
||||
if (message.type == "start") {
|
||||
consolex.log("lobby: game started");
|
||||
onjoin();
|
||||
createClientGame(
|
||||
lobbyConfig,
|
||||
message.config,
|
||||
eventBus,
|
||||
transport,
|
||||
userSettings,
|
||||
).then((r) => r.start());
|
||||
// For multiplayer games, GameConfig is not known until game starts.
|
||||
lobbyConfig.gameConfig = message.config;
|
||||
createClientGame(lobbyConfig, eventBus, transport, userSettings).then(
|
||||
(r) => r.start(),
|
||||
);
|
||||
}
|
||||
};
|
||||
transport.connect(onconnect, onmessage);
|
||||
@@ -112,17 +92,16 @@ export function joinLobby(
|
||||
|
||||
export async function createClientGame(
|
||||
lobbyConfig: LobbyConfig,
|
||||
gameConfig: GameConfig,
|
||||
eventBus: EventBus,
|
||||
transport: Transport,
|
||||
userSettings: UserSettings,
|
||||
): Promise<ClientGameRunner> {
|
||||
const config = await getConfig(gameConfig, userSettings);
|
||||
const config = await getConfig(lobbyConfig.gameConfig, userSettings);
|
||||
|
||||
const gameMap = await loadTerrainMap(gameConfig.gameMap);
|
||||
const gameMap = await loadTerrainMap(lobbyConfig.gameConfig.gameMap);
|
||||
const worker = new WorkerClient(
|
||||
lobbyConfig.gameID,
|
||||
gameConfig,
|
||||
lobbyConfig.gameConfig,
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
await worker.initialize();
|
||||
@@ -145,11 +124,10 @@ export async function createClientGame(
|
||||
);
|
||||
|
||||
consolex.log(
|
||||
`creating private game got difficulty: ${gameConfig.difficulty}`,
|
||||
`creating private game got difficulty: ${lobbyConfig.gameConfig.difficulty}`,
|
||||
);
|
||||
|
||||
return new ClientGameRunner(
|
||||
gameConfig,
|
||||
lobbyConfig,
|
||||
eventBus,
|
||||
gameRenderer,
|
||||
@@ -168,7 +146,6 @@ export class ClientGameRunner {
|
||||
private hasJoined = false;
|
||||
|
||||
constructor(
|
||||
private gameConfig: GameConfig,
|
||||
private lobby: LobbyConfig,
|
||||
private eventBus: EventBus,
|
||||
private renderer: GameRenderer,
|
||||
@@ -187,9 +164,9 @@ export class ClientGameRunner {
|
||||
clientID: this.lobby.clientID,
|
||||
},
|
||||
];
|
||||
const record = CreateGameRecord(
|
||||
const record = createGameRecord(
|
||||
this.lobby.gameID,
|
||||
this.gameConfig,
|
||||
this.lobby.gameConfig,
|
||||
players,
|
||||
// Not saving turns locally
|
||||
[],
|
||||
@@ -211,6 +188,7 @@ export class ClientGameRunner {
|
||||
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
|
||||
if ("errMsg" in gu) {
|
||||
showErrorModal(gu.errMsg, gu.stack, this.lobby.clientID);
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
|
||||
@@ -256,10 +234,12 @@ export class ClientGameRunner {
|
||||
}
|
||||
if (message.type == "desync") {
|
||||
showErrorModal(
|
||||
`desync from server: ${JSON.stringify(message)}`,
|
||||
`game: ${this.lobby.gameID}, clientID: ${this.lobby.clientID}, desync from server: ${JSON.stringify(message)}`,
|
||||
"",
|
||||
this.lobby.clientID,
|
||||
);
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
if (message.type == "turn") {
|
||||
if (!this.hasJoined) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getConfig,
|
||||
getServerConfigFromClient,
|
||||
} from "../core/configuration/Config";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
|
||||
@customElement("host-lobby-modal")
|
||||
export class HostLobbyModal extends LitElement {
|
||||
@@ -548,18 +549,8 @@ export class HostLobbyModal extends LitElement {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameType: GameType.Private,
|
||||
lobby: {
|
||||
gameID: this.lobbyId,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
},
|
||||
gameID: this.lobbyId,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
|
||||
@@ -2,8 +2,9 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import { GameMapType, GameType } from "../core/game/Game";
|
||||
import { GameInfo } from "../core/Schemas";
|
||||
import { GameInfo, GameRecord } from "../core/Schemas";
|
||||
import { getServerConfigFromClient } from "../core/configuration/Config";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
|
||||
@customElement("join-private-lobby-modal")
|
||||
export class JoinPrivateLobbyModal extends LitElement {
|
||||
@@ -370,39 +371,56 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
|
||||
const config = await getServerConfigFromClient();
|
||||
const url = `/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.exists) {
|
||||
this.message = "Joined successfully! Waiting for game to start...";
|
||||
this.hasJoined = true;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const gameInfo = await response.json();
|
||||
if (gameInfo.exists) {
|
||||
this.message = "Joined successfully! Waiting for game to start...";
|
||||
this.hasJoined = true;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
} else {
|
||||
const archive_url = `/${config.workerPath(lobbyId)}/api/archived_game/${lobbyId}`;
|
||||
const archive_response = await fetch(archive_url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const archive_data = await archive_response.json();
|
||||
if (archive_data.exists) {
|
||||
const gr = archive_data.gameRecord as GameRecord;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
lobby: { gameID: lobbyId },
|
||||
gameType: GameType.Private,
|
||||
map: GameMapType.World,
|
||||
},
|
||||
gameID: lobbyId,
|
||||
gameRecord: gr,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
} else {
|
||||
this.message = "Lobby not found. Please check the ID and try again.";
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
consolex.error("Error checking lobby existence:", error);
|
||||
this.message = "An error occurred. Please try again.";
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
consolex.error("Error checking lobby existence:", error);
|
||||
this.message = "An error occurred. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
private async pollPlayers() {
|
||||
|
||||
+49
-12
@@ -16,7 +16,11 @@ import {
|
||||
ServerTurnMessageSchema,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import { CreateGameRecord, generateID } from "../core/Util";
|
||||
import {
|
||||
createGameRecord,
|
||||
decompressGameRecord,
|
||||
generateID,
|
||||
} from "../core/Util";
|
||||
import { LobbyConfig } from "./ClientGameRunner";
|
||||
import { getPersistentIDFromCookie } from "./Main";
|
||||
|
||||
@@ -33,8 +37,6 @@ export class LocalServer {
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
|
||||
constructor(
|
||||
private serverConfig: ServerConfig,
|
||||
private gameConfig: GameConfig,
|
||||
private lobbyConfig: LobbyConfig,
|
||||
private clientConnect: () => void,
|
||||
private clientMessage: (message: ServerMessage) => void,
|
||||
@@ -42,16 +44,21 @@ export class LocalServer {
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now();
|
||||
this.endTurnIntervalID = setInterval(
|
||||
() => this.endTurn(),
|
||||
this.serverConfig.turnIntervalMs(),
|
||||
);
|
||||
if (!this.lobbyConfig.gameRecord) {
|
||||
this.endTurnIntervalID = setInterval(
|
||||
() => this.endTurn(),
|
||||
this.lobbyConfig.serverConfig.turnIntervalMs(),
|
||||
);
|
||||
}
|
||||
this.clientConnect();
|
||||
if (this.lobbyConfig.gameRecord) {
|
||||
this.turns = decompressGameRecord(this.lobbyConfig.gameRecord).turns;
|
||||
}
|
||||
this.clientMessage(
|
||||
ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
config: this.gameConfig,
|
||||
turns: [],
|
||||
config: this.lobbyConfig.gameConfig,
|
||||
turns: this.turns,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -69,6 +76,10 @@ export class LocalServer {
|
||||
JSON.parse(message),
|
||||
);
|
||||
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") {
|
||||
// Store troop change events because otherwise they are
|
||||
@@ -79,6 +90,30 @@ export class LocalServer {
|
||||
}
|
||||
this.intents.push(clientMsg.intent);
|
||||
}
|
||||
if (clientMsg.type == "hash") {
|
||||
if (!this.lobbyConfig.gameRecord) {
|
||||
// Don't do hash verification on singleplayer games.
|
||||
return;
|
||||
}
|
||||
const archivedHash = this.turns[clientMsg.turnNumber].hash;
|
||||
if (archivedHash != clientMsg.hash) {
|
||||
console.warn(
|
||||
`desync detected on turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}, server hash: ${archivedHash}`,
|
||||
);
|
||||
this.clientMessage({
|
||||
type: "desync",
|
||||
turn: clientMsg.turnNumber,
|
||||
correctHash: archivedHash,
|
||||
clientsWithCorrectHash: 0,
|
||||
totalActiveClients: 1,
|
||||
yourHash: clientMsg.hash,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
`hash verified on turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}, server hash: ${archivedHash}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (clientMsg.type == "winner") {
|
||||
this.winner = clientMsg.winner;
|
||||
this.allPlayersStats = clientMsg.allPlayersStats;
|
||||
@@ -113,9 +148,9 @@ export class LocalServer {
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
},
|
||||
];
|
||||
const record = CreateGameRecord(
|
||||
const record = createGameRecord(
|
||||
this.lobbyConfig.gameID,
|
||||
this.gameConfig,
|
||||
this.lobbyConfig.gameConfig,
|
||||
players,
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
@@ -129,7 +164,9 @@ export class LocalServer {
|
||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||
type: "application/json",
|
||||
});
|
||||
const workerPath = this.serverConfig.workerPath(this.lobbyConfig.gameID);
|
||||
const workerPath = this.lobbyConfig.serverConfig.workerPath(
|
||||
this.lobbyConfig.gameID,
|
||||
);
|
||||
navigator.sendBeacon(`/${workerPath}/api/archive_singleplayer_game`, blob);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-12
@@ -23,6 +23,16 @@ import { HelpModal } from "./HelpModal";
|
||||
import { GameType } from "../core/game/Game";
|
||||
import { getServerConfigFromClient } from "../core/configuration/Config";
|
||||
import GoogleAdElement from "./GoogleAdElement";
|
||||
import { GameConfig, GameInfo, GameRecord } from "../core/Schemas";
|
||||
|
||||
export interface JoinLobbyEvent {
|
||||
// Multiplayer games only have gameID, gameConfig is not known until game starts.
|
||||
gameID: string;
|
||||
// GameConfig only exists when playing a singleplayer game.
|
||||
gameConfig?: GameConfig;
|
||||
// GameRecord exists when replaying an archived game.
|
||||
gameRecord?: GameRecord;
|
||||
}
|
||||
|
||||
class Client {
|
||||
private gameStop: () => void;
|
||||
@@ -137,18 +147,16 @@ class Client {
|
||||
}
|
||||
|
||||
private async handleJoinLobby(event: CustomEvent) {
|
||||
const lobby = event.detail.lobby;
|
||||
consolex.log(`joining lobby ${lobby.id}`);
|
||||
const lobby = event.detail as JoinLobbyEvent;
|
||||
consolex.log(`joining lobby ${lobby.gameID}`);
|
||||
if (this.gameStop != null) {
|
||||
consolex.log("joining lobby, stopping existing game");
|
||||
this.gameStop();
|
||||
}
|
||||
const config = await getServerConfigFromClient();
|
||||
const gameType = event.detail.gameType;
|
||||
this.gameStop = joinLobby(
|
||||
{
|
||||
serverConfig: config,
|
||||
gameType: gameType,
|
||||
flag: (): string =>
|
||||
this.flagInput.getCurrentFlag() == "xx"
|
||||
? ""
|
||||
@@ -158,13 +166,8 @@ class Client {
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
infiniteGold: event.detail.infiniteGold,
|
||||
infiniteTroops: event.detail.infiniteTroops,
|
||||
instantBuild: event.detail.instantBuild,
|
||||
bots: event.detail.bots,
|
||||
disableNPCs: event.detail.disableNPCs,
|
||||
gameConfig: lobby.gameConfig ?? lobby.gameRecord?.gameConfig,
|
||||
gameRecord: lobby.gameRecord,
|
||||
},
|
||||
() => {
|
||||
this.joinModal.close();
|
||||
@@ -180,7 +183,7 @@ class Client {
|
||||
startingModal instanceof GameStartingModal;
|
||||
startingModal.show();
|
||||
|
||||
if (gameType != GameType.Singleplayer) {
|
||||
if (event.detail.gameConfig?.gameType != GameType.Singleplayer) {
|
||||
window.history.pushState({}, "", `/join/${lobby.gameID}`);
|
||||
sessionStorage.setItem("inLobby", "true");
|
||||
}
|
||||
|
||||
@@ -158,12 +158,7 @@ export class PublicLobby extends LitElement {
|
||||
this.currLobby = lobby;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
lobby,
|
||||
gameType: GameType.Public,
|
||||
map: GameMapType.World,
|
||||
difficulty: Difficulty.Medium,
|
||||
},
|
||||
detail: lobby,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,8 @@ import "./components/Difficulties";
|
||||
import { DifficultyDescription } from "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
import randomMap from "../../resources/images/RandomMap.png";
|
||||
import { GameInfo } from "../core/Schemas";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
|
||||
@customElement("single-player-modal")
|
||||
export class SinglePlayerModal extends LitElement {
|
||||
@@ -482,18 +484,18 @@ export class SinglePlayerModal extends LitElement {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameType: GameType.Singleplayer,
|
||||
lobby: {
|
||||
gameID: generateID(),
|
||||
gameID: generateID(),
|
||||
gameConfig: {
|
||||
gameMap: this.selectedMap,
|
||||
gameType: GameType.Singleplayer,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
},
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
|
||||
+11
-14
@@ -150,12 +150,13 @@ export class Transport {
|
||||
|
||||
constructor(
|
||||
private lobbyConfig: LobbyConfig,
|
||||
// gameConfig only set on private games
|
||||
private gameConfig: GameConfig | null,
|
||||
private eventBus: EventBus,
|
||||
private serverConfig: ServerConfig,
|
||||
) {
|
||||
this.isLocal = lobbyConfig.gameType == GameType.Singleplayer;
|
||||
// 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.gameConfig?.gameType == GameType.Singleplayer;
|
||||
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) =>
|
||||
this.onSendAllianceRequest(e),
|
||||
@@ -237,13 +238,7 @@ export class Transport {
|
||||
onconnect: () => void,
|
||||
onmessage: (message: ServerMessage) => void,
|
||||
) {
|
||||
this.localServer = new LocalServer(
|
||||
this.serverConfig,
|
||||
this.gameConfig,
|
||||
this.lobbyConfig,
|
||||
onconnect,
|
||||
onmessage,
|
||||
);
|
||||
this.localServer = new LocalServer(this.lobbyConfig, onconnect, onmessage);
|
||||
this.localServer.start();
|
||||
}
|
||||
|
||||
@@ -255,7 +250,9 @@ export class Transport {
|
||||
this.maybeKillSocket();
|
||||
const wsHost = window.location.host;
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const workerPath = this.serverConfig.workerPath(this.lobbyConfig.gameID);
|
||||
const workerPath = this.lobbyConfig.serverConfig.workerPath(
|
||||
this.lobbyConfig.gameID,
|
||||
);
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`);
|
||||
this.onconnect = onconnect;
|
||||
this.onmessage = onmessage;
|
||||
@@ -273,7 +270,7 @@ export class Transport {
|
||||
this.onmessage(serverMsg);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to process server message ${event.data}: ${error}`,
|
||||
`Failed to process server message ${event.data}: ${error}, ${error.stack}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -503,7 +500,7 @@ export class Transport {
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
tick: event.tick,
|
||||
turnNumber: event.tick,
|
||||
hash: event.hash,
|
||||
});
|
||||
this.sendMsg(JSON.stringify(msg));
|
||||
|
||||
@@ -334,7 +334,7 @@ export class UnitLayer implements Layer {
|
||||
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), range),
|
||||
euclDistFN(unit.lastTile(), range, false),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
@@ -342,7 +342,7 @@ export class UnitLayer implements Layer {
|
||||
if (unit.isActive()) {
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), range),
|
||||
euclDistFN(unit.tile(), range, false),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
|
||||
Reference in New Issue
Block a user