Start game via WebSocket intent (#3794)

## Description:

Replaces the HTTP POST /api/start_game/:id endpoint with a WebSocket
intent, making private game start consistent with how kick_player and
update_game_config already work. Also verifies that only the lobby
creator can start a game.

## 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:

evan
This commit is contained in:
Evan
2026-04-29 13:26:14 -06:00
committed by GitHub
parent 58cb86cb6b
commit 2994a5f848
6 changed files with 59 additions and 34 deletions
+5 -14
View File
@@ -1011,21 +1011,12 @@ export class HostLobbyModal extends BaseModal {
// If the modal closes as part of starting the game, do not leave the lobby
this.leaveLobbyOnClose = false;
const config = await getRuntimeClientServerConfig();
const response = await fetch(
`${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
this.dispatchEvent(
new CustomEvent("start-game", {
bubbles: true,
composed: true,
}),
);
if (!response.ok) {
this.leaveLobbyOnClose = true;
}
return response;
}
private kickPlayer(clientID: string) {
+9
View File
@@ -51,6 +51,7 @@ import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
import { TokenLoginModal } from "./TokenLoginModal";
import {
SendKickPlayerIntentEvent,
SendStartGameEvent,
SendUpdateGameConfigIntentEvent,
} from "./Transport";
import { UserSettingModal } from "./UserSettingModal";
@@ -215,6 +216,7 @@ declare global {
interface DocumentEventMap {
"join-lobby": CustomEvent<JoinLobbyEvent>;
"kick-player": CustomEvent;
"start-game": CustomEvent;
"join-changed": CustomEvent;
"open-matchmaking": CustomEvent<undefined>;
}
@@ -311,6 +313,7 @@ class Client {
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
document.addEventListener("start-game", this.handleStartGame.bind(this));
document.addEventListener(
"update-game-config",
this.handleUpdateGameConfig.bind(this),
@@ -932,6 +935,12 @@ class Client {
}
}
private handleStartGame() {
if (this.eventBus) {
this.eventBus.emit(new SendStartGameEvent());
}
}
private handleUpdateGameConfig(event: CustomEvent) {
const { config } = event.detail;
+8
View File
@@ -173,6 +173,8 @@ export class SendUpdateGameConfigIntentEvent implements GameEvent {
constructor(public readonly config: Partial<GameConfig>) {}
}
export class SendStartGameEvent implements GameEvent {}
export class Transport {
private socket: WebSocket | null = null;
@@ -262,6 +264,8 @@ export class Transport {
this.eventBus.on(SendUpdateGameConfigIntentEvent, (e) =>
this.onSendUpdateGameConfigIntent(e),
);
this.eventBus.on(SendStartGameEvent, () => this.onSendStartGame());
}
private startPing() {
@@ -644,6 +648,10 @@ export class Transport {
});
}
private onSendStartGame() {
this.sendIntent({ type: "start_game" });
}
private sendIntent(intent: Intent) {
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
const msg = {