mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 01:22:01 +00:00
f6454963b2
## Description: ### Short path for multi-source HPA* Math was not mathing, increased the bounds to 260x260, it is a bit slower but should work better. The short path was breaking when player owned a lot of shores. This is because the bounding box of tiles with less than 120 distance + 10 padding could be as big as 260x260 and the optimized array was set to 140x140. I made mistake of calculating it as `2 * (60 + 10)` instead of `2 * (120 + 10)`. ### LoS path refinement Previously, we ran 2 passes of LoS smoothing on the path. However, since we are effectively tracing the same path, the line of sight is essentially the same. This PR makes second line of sight stop on water tiles with magnitude `n + 1` compared to first path. Practically, this means it'll attempt LoS exactly 1 tile after previous corner. See screenshot. <img width="1299" height="1151" alt="image" src="https://github.com/user-attachments/assets/726be236-1ff8-406c-896a-02902a762ab0" /> ### SendBoatAttackIntentEvent The flow of sending transport ships is currently strange. This PR makes the flow more sane. **Old flow** ``` - Player clicks TARGET tile, it can be deep inland - Client asks Worker for the best START tile to TARGET tile - Worker answers `false`, since the tile is inland - Client sends BoatAttackIntent with START=false and TARGET tiles set - Worker accepts BoatAttackIntent, computes DESTINATION as closest shore to TARGET - Worker re-computes best START to DESTINATION - Worker sends boat from START to DESTINATION ``` **New flow** ``` - Player clicks TARGET tile, it can be deep inland - Client sends BoatAttackIntent with TARGET - Worker accepts BoatAttackIntent, computes DESTINATION as closest shore to TARGET - Worker computes START as the best tile to DESTINATION - Worker sends boat from START to DESTINATION ``` ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: moleole
739 lines
20 KiB
TypeScript
739 lines
20 KiB
TypeScript
import { z } from "zod";
|
|
import { EventBus, GameEvent } from "../core/EventBus";
|
|
import {
|
|
AllPlayers,
|
|
GameType,
|
|
Gold,
|
|
PlayerID,
|
|
Tick,
|
|
UnitType,
|
|
} from "../core/game/Game";
|
|
import { TileRef } from "../core/game/GameMap";
|
|
import { PlayerView } from "../core/game/GameView";
|
|
import {
|
|
AllPlayersStats,
|
|
ClientHashMessage,
|
|
ClientIntentMessage,
|
|
ClientJoinMessage,
|
|
ClientMessage,
|
|
ClientPingMessage,
|
|
ClientRejoinMessage,
|
|
ClientSendWinnerMessage,
|
|
GameConfig,
|
|
Intent,
|
|
ServerMessage,
|
|
ServerMessageSchema,
|
|
Winner,
|
|
} from "../core/Schemas";
|
|
import { replacer } from "../core/Util";
|
|
import { getPlayToken } from "./Auth";
|
|
import { LobbyConfig } from "./ClientGameRunner";
|
|
import { LocalServer } from "./LocalServer";
|
|
|
|
export class PauseGameIntentEvent implements GameEvent {
|
|
constructor(public readonly paused: boolean) {}
|
|
}
|
|
|
|
export class SendAllianceRequestIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly requestor: PlayerView,
|
|
public readonly recipient: PlayerView,
|
|
) {}
|
|
}
|
|
|
|
export class SendBreakAllianceIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly requestor: PlayerView,
|
|
public readonly recipient: PlayerView,
|
|
) {}
|
|
}
|
|
|
|
export class SendUpgradeStructureIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly unitId: number,
|
|
public readonly unitType: UnitType,
|
|
) {}
|
|
}
|
|
|
|
export class SendAllianceReplyIntentEvent implements GameEvent {
|
|
constructor(
|
|
// The original alliance requestor
|
|
public readonly requestor: PlayerView,
|
|
public readonly recipient: PlayerView,
|
|
public readonly accepted: boolean,
|
|
) {}
|
|
}
|
|
|
|
export class SendAllianceExtensionIntentEvent implements GameEvent {
|
|
constructor(public readonly recipient: PlayerView) {}
|
|
}
|
|
|
|
export class SendSpawnIntentEvent implements GameEvent {
|
|
constructor(public readonly tile: TileRef) {}
|
|
}
|
|
|
|
export class SendAttackIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly targetID: PlayerID | null,
|
|
public readonly troops: number,
|
|
) {}
|
|
}
|
|
|
|
export class SendBoatAttackIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly dst: TileRef,
|
|
public readonly troops: number,
|
|
) {}
|
|
}
|
|
|
|
export class BuildUnitIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly unit: UnitType,
|
|
public readonly tile: TileRef,
|
|
public readonly rocketDirectionUp?: boolean,
|
|
) {}
|
|
}
|
|
|
|
export class SendTargetPlayerIntentEvent implements GameEvent {
|
|
constructor(public readonly targetID: PlayerID) {}
|
|
}
|
|
|
|
export class SendEmojiIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly recipient: PlayerView | typeof AllPlayers,
|
|
public readonly emoji: number,
|
|
) {}
|
|
}
|
|
|
|
export class SendDonateGoldIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly recipient: PlayerView,
|
|
public readonly gold: Gold | null,
|
|
) {}
|
|
}
|
|
|
|
export class SendDonateTroopsIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly recipient: PlayerView,
|
|
public readonly troops: number | null,
|
|
) {}
|
|
}
|
|
|
|
export class SendQuickChatEvent implements GameEvent {
|
|
constructor(
|
|
public readonly recipient: PlayerView,
|
|
public readonly quickChatKey: string,
|
|
public readonly target?: PlayerID,
|
|
) {}
|
|
}
|
|
|
|
export class SendEmbargoIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly target: PlayerView,
|
|
public readonly action: "start" | "stop",
|
|
) {}
|
|
}
|
|
|
|
export class SendEmbargoAllIntentEvent implements GameEvent {
|
|
constructor(public readonly action: "start" | "stop") {}
|
|
}
|
|
|
|
export class SendDeleteUnitIntentEvent implements GameEvent {
|
|
constructor(public readonly unitId: number) {}
|
|
}
|
|
|
|
export class CancelAttackIntentEvent implements GameEvent {
|
|
constructor(public readonly attackID: string) {}
|
|
}
|
|
|
|
export class CancelBoatIntentEvent implements GameEvent {
|
|
constructor(public readonly unitID: number) {}
|
|
}
|
|
|
|
export class SendWinnerEvent implements GameEvent {
|
|
constructor(
|
|
public readonly winner: Winner,
|
|
public readonly allPlayersStats: AllPlayersStats,
|
|
) {}
|
|
}
|
|
export class SendHashEvent implements GameEvent {
|
|
constructor(
|
|
public readonly tick: Tick,
|
|
public readonly hash: number,
|
|
) {}
|
|
}
|
|
|
|
export class MoveWarshipIntentEvent implements GameEvent {
|
|
constructor(
|
|
public readonly unitId: number,
|
|
public readonly tile: number,
|
|
) {}
|
|
}
|
|
|
|
export class SendKickPlayerIntentEvent implements GameEvent {
|
|
constructor(public readonly target: string) {}
|
|
}
|
|
|
|
export class SendUpdateGameConfigIntentEvent implements GameEvent {
|
|
constructor(public readonly config: Partial<GameConfig>) {}
|
|
}
|
|
|
|
export class Transport {
|
|
private socket: WebSocket | null = null;
|
|
|
|
private localServer: LocalServer;
|
|
|
|
private buffer: string[] = [];
|
|
|
|
private onconnect: () => void;
|
|
private onmessage: (msg: ServerMessage) => void;
|
|
|
|
private pingInterval: number | null = null;
|
|
public readonly isLocal: boolean;
|
|
|
|
constructor(
|
|
private lobbyConfig: LobbyConfig,
|
|
private eventBus: EventBus,
|
|
) {
|
|
// 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 !== undefined ||
|
|
lobbyConfig.gameStartInfo?.config.gameType === GameType.Singleplayer;
|
|
|
|
this.eventBus.on(SendAllianceRequestIntentEvent, (e) =>
|
|
this.onSendAllianceRequest(e),
|
|
);
|
|
this.eventBus.on(SendAllianceReplyIntentEvent, (e) =>
|
|
this.onAllianceRequestReplyUIEvent(e),
|
|
);
|
|
this.eventBus.on(SendAllianceExtensionIntentEvent, (e) =>
|
|
this.onSendAllianceExtensionIntent(e),
|
|
);
|
|
this.eventBus.on(SendBreakAllianceIntentEvent, (e) =>
|
|
this.onBreakAllianceRequestUIEvent(e),
|
|
);
|
|
this.eventBus.on(SendSpawnIntentEvent, (e) =>
|
|
this.onSendSpawnIntentEvent(e),
|
|
);
|
|
this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e));
|
|
this.eventBus.on(SendUpgradeStructureIntentEvent, (e) =>
|
|
this.onSendUpgradeStructureIntent(e),
|
|
);
|
|
this.eventBus.on(SendBoatAttackIntentEvent, (e) =>
|
|
this.onSendBoatAttackIntent(e),
|
|
);
|
|
this.eventBus.on(SendTargetPlayerIntentEvent, (e) =>
|
|
this.onSendTargetPlayerIntent(e),
|
|
);
|
|
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e));
|
|
this.eventBus.on(SendDonateGoldIntentEvent, (e) =>
|
|
this.onSendDonateGoldIntent(e),
|
|
);
|
|
this.eventBus.on(SendDonateTroopsIntentEvent, (e) =>
|
|
this.onSendDonateTroopIntent(e),
|
|
);
|
|
this.eventBus.on(SendQuickChatEvent, (e) => this.onSendQuickChatIntent(e));
|
|
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
|
|
this.onSendEmbargoIntent(e),
|
|
);
|
|
this.eventBus.on(SendEmbargoAllIntentEvent, (e) =>
|
|
this.onSendEmbargoAllIntent(e),
|
|
);
|
|
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
|
|
|
|
this.eventBus.on(PauseGameIntentEvent, (e) => this.onPauseGameIntent(e));
|
|
this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e));
|
|
this.eventBus.on(SendHashEvent, (e) => this.onSendHashEvent(e));
|
|
this.eventBus.on(CancelAttackIntentEvent, (e) =>
|
|
this.onCancelAttackIntentEvent(e),
|
|
);
|
|
this.eventBus.on(CancelBoatIntentEvent, (e) =>
|
|
this.onCancelBoatIntentEvent(e),
|
|
);
|
|
|
|
this.eventBus.on(MoveWarshipIntentEvent, (e) => {
|
|
this.onMoveWarshipEvent(e);
|
|
});
|
|
|
|
this.eventBus.on(SendDeleteUnitIntentEvent, (e) =>
|
|
this.onSendDeleteUnitIntent(e),
|
|
);
|
|
|
|
this.eventBus.on(SendKickPlayerIntentEvent, (e) =>
|
|
this.onSendKickPlayerIntent(e),
|
|
);
|
|
|
|
this.eventBus.on(SendUpdateGameConfigIntentEvent, (e) =>
|
|
this.onSendUpdateGameConfigIntent(e),
|
|
);
|
|
}
|
|
|
|
private startPing() {
|
|
if (this.isLocal) return;
|
|
this.pingInterval ??= window.setInterval(() => {
|
|
if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) {
|
|
this.sendMsg({
|
|
type: "ping",
|
|
} satisfies ClientPingMessage);
|
|
}
|
|
}, 5 * 1000);
|
|
}
|
|
|
|
private stopPing() {
|
|
if (this.pingInterval) {
|
|
window.clearInterval(this.pingInterval);
|
|
this.pingInterval = null;
|
|
}
|
|
}
|
|
|
|
public connect(
|
|
onconnect: () => void,
|
|
onmessage: (message: ServerMessage) => void,
|
|
) {
|
|
if (this.isLocal) {
|
|
this.connectLocal(onconnect, onmessage);
|
|
} else {
|
|
this.connectRemote(onconnect, onmessage);
|
|
}
|
|
}
|
|
|
|
public updateCallback(
|
|
onconnect: () => void,
|
|
onmessage: (message: ServerMessage) => void,
|
|
) {
|
|
if (this.isLocal) {
|
|
this.localServer.updateCallback(onconnect, onmessage);
|
|
} else {
|
|
this.onconnect = onconnect;
|
|
this.onmessage = onmessage;
|
|
}
|
|
}
|
|
|
|
private connectLocal(
|
|
onconnect: () => void,
|
|
onmessage: (message: ServerMessage) => void,
|
|
) {
|
|
this.localServer = new LocalServer(
|
|
this.lobbyConfig,
|
|
this.lobbyConfig.gameRecord !== undefined,
|
|
this.eventBus,
|
|
);
|
|
this.localServer.updateCallback(onconnect, onmessage);
|
|
this.localServer.start();
|
|
}
|
|
|
|
private connectRemote(
|
|
onconnect: () => void,
|
|
onmessage: (message: ServerMessage) => void,
|
|
) {
|
|
this.startPing();
|
|
this.killExistingSocket();
|
|
const wsHost = window.location.host;
|
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
const workerPath = this.lobbyConfig.serverConfig.workerPath(
|
|
this.lobbyConfig.gameID,
|
|
);
|
|
this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`);
|
|
this.onconnect = onconnect;
|
|
this.onmessage = onmessage;
|
|
this.socket.onopen = () => {
|
|
console.log("Connected to game server!");
|
|
if (this.socket === null) {
|
|
console.error("socket is null");
|
|
return;
|
|
}
|
|
while (this.buffer.length > 0) {
|
|
console.log("sending dropped message");
|
|
const msg = this.buffer.pop();
|
|
if (msg === undefined) {
|
|
console.warn("msg is undefined");
|
|
continue;
|
|
}
|
|
this.socket.send(msg);
|
|
}
|
|
onconnect();
|
|
};
|
|
this.socket.onmessage = (event: MessageEvent) => {
|
|
try {
|
|
const parsed = JSON.parse(event.data);
|
|
const result = ServerMessageSchema.safeParse(parsed);
|
|
if (!result.success) {
|
|
const error = z.prettifyError(result.error);
|
|
console.error("Error parsing server message", error);
|
|
return;
|
|
}
|
|
this.onmessage(result.data);
|
|
} catch (e) {
|
|
console.error("Error in onmessage handler:", e, event.data);
|
|
return;
|
|
}
|
|
};
|
|
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 === 1002) {
|
|
// TODO: make this a modal
|
|
alert(`connection refused: ${event.reason}`);
|
|
} else if (event.code !== 1000) {
|
|
console.log(`received error code ${event.code}, reconnecting`);
|
|
this.reconnect();
|
|
}
|
|
};
|
|
}
|
|
|
|
public reconnect() {
|
|
this.connect(this.onconnect, this.onmessage);
|
|
}
|
|
|
|
public turnComplete() {
|
|
if (this.isLocal) {
|
|
this.localServer.turnComplete();
|
|
}
|
|
}
|
|
|
|
async joinGame() {
|
|
this.sendMsg({
|
|
type: "join",
|
|
gameID: this.lobbyConfig.gameID,
|
|
clientID: this.lobbyConfig.clientID,
|
|
username: this.lobbyConfig.playerName,
|
|
cosmetics: this.lobbyConfig.cosmetics,
|
|
turnstileToken: this.lobbyConfig.turnstileToken,
|
|
token: await getPlayToken(),
|
|
} satisfies ClientJoinMessage);
|
|
}
|
|
|
|
async rejoinGame(lastTurn: number) {
|
|
this.sendMsg({
|
|
type: "rejoin",
|
|
gameID: this.lobbyConfig.gameID,
|
|
clientID: this.lobbyConfig.clientID,
|
|
lastTurn: lastTurn,
|
|
token: await getPlayToken(),
|
|
} satisfies ClientRejoinMessage);
|
|
}
|
|
|
|
leaveGame() {
|
|
if (this.isLocal) {
|
|
this.localServer.endGame();
|
|
return;
|
|
}
|
|
this.stopPing();
|
|
if (this.socket === null) return;
|
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
console.log("on stop: leaving game");
|
|
this.killExistingSocket();
|
|
} else {
|
|
console.log(
|
|
"WebSocket is not open. Current state:",
|
|
this.socket.readyState,
|
|
);
|
|
console.error("attempting reconnect");
|
|
this.killExistingSocket();
|
|
}
|
|
}
|
|
|
|
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
|
|
this.sendIntent({
|
|
type: "allianceRequest",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient: event.recipient.id(),
|
|
});
|
|
}
|
|
|
|
private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) {
|
|
this.sendIntent({
|
|
type: "allianceRequestReply",
|
|
clientID: this.lobbyConfig.clientID,
|
|
requestor: event.requestor.id(),
|
|
accept: event.accepted,
|
|
});
|
|
}
|
|
|
|
private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) {
|
|
this.sendIntent({
|
|
type: "breakAlliance",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient: event.recipient.id(),
|
|
});
|
|
}
|
|
|
|
private onSendAllianceExtensionIntent(
|
|
event: SendAllianceExtensionIntentEvent,
|
|
) {
|
|
this.sendIntent({
|
|
type: "allianceExtension",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient: event.recipient.id(),
|
|
});
|
|
}
|
|
|
|
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
|
|
this.sendIntent({
|
|
type: "spawn",
|
|
clientID: this.lobbyConfig.clientID,
|
|
tile: event.tile,
|
|
});
|
|
}
|
|
|
|
private onSendAttackIntent(event: SendAttackIntentEvent) {
|
|
this.sendIntent({
|
|
type: "attack",
|
|
clientID: this.lobbyConfig.clientID,
|
|
targetID: event.targetID,
|
|
troops: event.troops,
|
|
});
|
|
}
|
|
|
|
private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
|
|
this.sendIntent({
|
|
type: "boat",
|
|
clientID: this.lobbyConfig.clientID,
|
|
troops: event.troops,
|
|
dst: event.dst,
|
|
});
|
|
}
|
|
|
|
private onSendUpgradeStructureIntent(event: SendUpgradeStructureIntentEvent) {
|
|
this.sendIntent({
|
|
type: "upgrade_structure",
|
|
unit: event.unitType,
|
|
clientID: this.lobbyConfig.clientID,
|
|
unitId: event.unitId,
|
|
});
|
|
}
|
|
|
|
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
|
|
this.sendIntent({
|
|
type: "targetPlayer",
|
|
clientID: this.lobbyConfig.clientID,
|
|
target: event.targetID,
|
|
});
|
|
}
|
|
|
|
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
|
|
this.sendIntent({
|
|
type: "emoji",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient:
|
|
event.recipient === AllPlayers ? AllPlayers : event.recipient.id(),
|
|
emoji: event.emoji,
|
|
});
|
|
}
|
|
|
|
private onSendDonateGoldIntent(event: SendDonateGoldIntentEvent) {
|
|
this.sendIntent({
|
|
type: "donate_gold",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient: event.recipient.id(),
|
|
gold: event.gold ? Number(event.gold) : null,
|
|
});
|
|
}
|
|
|
|
private onSendDonateTroopIntent(event: SendDonateTroopsIntentEvent) {
|
|
this.sendIntent({
|
|
type: "donate_troops",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient: event.recipient.id(),
|
|
troops: event.troops,
|
|
});
|
|
}
|
|
|
|
private onSendQuickChatIntent(event: SendQuickChatEvent) {
|
|
this.sendIntent({
|
|
type: "quick_chat",
|
|
clientID: this.lobbyConfig.clientID,
|
|
recipient: event.recipient.id(),
|
|
quickChatKey: event.quickChatKey,
|
|
target: event.target,
|
|
});
|
|
}
|
|
|
|
private onSendEmbargoIntent(event: SendEmbargoIntentEvent) {
|
|
this.sendIntent({
|
|
type: "embargo",
|
|
clientID: this.lobbyConfig.clientID,
|
|
targetID: event.target.id(),
|
|
action: event.action,
|
|
});
|
|
}
|
|
|
|
private onSendEmbargoAllIntent(event: SendEmbargoAllIntentEvent) {
|
|
this.sendIntent({
|
|
type: "embargo_all",
|
|
clientID: this.lobbyConfig.clientID,
|
|
action: event.action,
|
|
});
|
|
}
|
|
|
|
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
|
this.sendIntent({
|
|
type: "build_unit",
|
|
clientID: this.lobbyConfig.clientID,
|
|
unit: event.unit,
|
|
tile: event.tile,
|
|
rocketDirectionUp: event.rocketDirectionUp,
|
|
});
|
|
}
|
|
|
|
private onPauseGameIntent(event: PauseGameIntentEvent) {
|
|
this.sendIntent({
|
|
type: "toggle_pause",
|
|
clientID: this.lobbyConfig.clientID,
|
|
paused: event.paused,
|
|
});
|
|
}
|
|
|
|
private onSendWinnerEvent(event: SendWinnerEvent) {
|
|
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
|
this.sendMsg({
|
|
type: "winner",
|
|
winner: event.winner,
|
|
allPlayersStats: event.allPlayersStats,
|
|
} satisfies ClientSendWinnerMessage);
|
|
} else {
|
|
console.log(
|
|
"WebSocket is not open. Current state:",
|
|
this.socket?.readyState,
|
|
);
|
|
console.log("attempting reconnect");
|
|
}
|
|
}
|
|
|
|
private onSendHashEvent(event: SendHashEvent) {
|
|
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
|
this.sendMsg({
|
|
type: "hash",
|
|
turnNumber: event.tick,
|
|
hash: event.hash,
|
|
} satisfies ClientHashMessage);
|
|
} else {
|
|
console.log(
|
|
"WebSocket is not open. Current state:",
|
|
this.socket?.readyState,
|
|
);
|
|
console.log("attempting reconnect");
|
|
}
|
|
}
|
|
|
|
private onCancelAttackIntentEvent(event: CancelAttackIntentEvent) {
|
|
this.sendIntent({
|
|
type: "cancel_attack",
|
|
clientID: this.lobbyConfig.clientID,
|
|
attackID: event.attackID,
|
|
});
|
|
}
|
|
|
|
private onCancelBoatIntentEvent(event: CancelBoatIntentEvent) {
|
|
this.sendIntent({
|
|
type: "cancel_boat",
|
|
clientID: this.lobbyConfig.clientID,
|
|
unitID: event.unitID,
|
|
});
|
|
}
|
|
|
|
private onMoveWarshipEvent(event: MoveWarshipIntentEvent) {
|
|
this.sendIntent({
|
|
type: "move_warship",
|
|
clientID: this.lobbyConfig.clientID,
|
|
unitId: event.unitId,
|
|
tile: event.tile,
|
|
});
|
|
}
|
|
|
|
private onSendDeleteUnitIntent(event: SendDeleteUnitIntentEvent) {
|
|
this.sendIntent({
|
|
type: "delete_unit",
|
|
clientID: this.lobbyConfig.clientID,
|
|
unitId: event.unitId,
|
|
});
|
|
}
|
|
|
|
private onSendKickPlayerIntent(event: SendKickPlayerIntentEvent) {
|
|
this.sendIntent({
|
|
type: "kick_player",
|
|
clientID: this.lobbyConfig.clientID,
|
|
target: event.target,
|
|
});
|
|
}
|
|
|
|
private onSendUpdateGameConfigIntent(event: SendUpdateGameConfigIntentEvent) {
|
|
this.sendIntent({
|
|
type: "update_game_config",
|
|
clientID: this.lobbyConfig.clientID,
|
|
config: event.config,
|
|
});
|
|
}
|
|
|
|
private sendIntent(intent: Intent) {
|
|
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
|
const msg = {
|
|
type: "intent",
|
|
intent: intent,
|
|
} satisfies ClientIntentMessage;
|
|
this.sendMsg(msg);
|
|
} else {
|
|
console.log(
|
|
"WebSocket is not open. Current state:",
|
|
this.socket?.readyState,
|
|
);
|
|
console.log("attempting reconnect");
|
|
}
|
|
}
|
|
|
|
private sendMsg(msg: ClientMessage) {
|
|
if (this.isLocal) {
|
|
// Forward message to local server
|
|
this.localServer.onMessage(msg);
|
|
return;
|
|
} else if (this.socket === null) {
|
|
// Socket missing, do nothing
|
|
return;
|
|
}
|
|
const str = JSON.stringify(msg, replacer);
|
|
if (this.socket.readyState === WebSocket.CLOSED) {
|
|
// Buffer message
|
|
console.warn("socket not ready, closing and trying later");
|
|
this.socket.close();
|
|
this.socket = null;
|
|
this.connectRemote(this.onconnect, this.onmessage);
|
|
this.buffer.push(str);
|
|
} else {
|
|
// Send the message directly
|
|
this.socket.send(str);
|
|
}
|
|
}
|
|
|
|
private killExistingSocket(): void {
|
|
if (this.socket === null) {
|
|
return;
|
|
}
|
|
// Remove all event listeners
|
|
this.socket.onmessage = null;
|
|
this.socket.onopen = null;
|
|
this.socket.onclose = null;
|
|
this.socket.onerror = null;
|
|
|
|
// Close the connection if it's still open or still connecting
|
|
try {
|
|
if (
|
|
this.socket.readyState === WebSocket.OPEN ||
|
|
this.socket.readyState === WebSocket.CONNECTING
|
|
) {
|
|
this.socket.close();
|
|
}
|
|
} catch (e) {
|
|
console.warn("Error while closing WebSocket:", e);
|
|
}
|
|
|
|
this.socket = null;
|
|
}
|
|
}
|