mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 11:36:11 +00:00
Private Lobbies: Add kick player functionality (#1436)
## Description: Added player management features so lobby hosts can kick players from private games. This includes both UI changes and backend work. ### What's new: - Hosts can now kick players from private lobbies with a simple button - Added host badges and remove buttons to the UI - Made sure only hosts can kick people, and hosts can't kick themselves ### How it works: - When someone creates a private game, they automatically become the host - Kicking happens through WebSocket "kick-player" events - Server checks that you're actually the host before letting you kick anyone <img width="1291" height="871" alt="Screenshot 2025-07-15 002114" src="https://github.com/user-attachments/assets/ea575f83-a0f4-45d1-9cfe-7521d373f3d5" /> ### Known Issues: - Kicked player gets general message (same when kicked for multi tab) ### Other Issues: - Host abandoment still existent (host clicks on x; or is closing tab) ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: [UN]nvm --------- Co-authored-by: floriankilian <floriankilian@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,12 @@ import {
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { GameConfig, GameInfo, TeamCountConfig } from "../core/Schemas";
|
||||
import {
|
||||
ClientInfo,
|
||||
GameConfig,
|
||||
GameInfo,
|
||||
TeamCountConfig,
|
||||
} from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import "./components/baseComponents/Modal";
|
||||
import "./components/Difficulties";
|
||||
@@ -40,9 +45,10 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private lobbyId = "";
|
||||
@state() private copySuccess = false;
|
||||
@state() private players: string[] = [];
|
||||
@state() private clients: ClientInfo[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
@state() private lobbyCreatorClientID: string = "";
|
||||
@state() private lobbyIdVisible: boolean = true;
|
||||
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
@@ -395,29 +401,45 @@ export class HostLobbyModal extends LitElement {
|
||||
<!-- Lobby Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
${this.players.length}
|
||||
${this.clients.length}
|
||||
${
|
||||
this.players.length === 1
|
||||
this.clients.length === 1
|
||||
? translateText("host_modal.player")
|
||||
: translateText("host_modal.players")
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="players-list">
|
||||
${this.players.map(
|
||||
(player) => html`<span class="player-tag">${player}</span>`,
|
||||
${this.clients.map(
|
||||
(client) => html`
|
||||
<span class="player-tag">
|
||||
${client.username}
|
||||
${client.clientID === this.lobbyCreatorClientID
|
||||
? html`<span class="host-badge"
|
||||
>(${translateText("host_modal.host_badge")})</span
|
||||
>`
|
||||
: html`
|
||||
<button
|
||||
class="remove-player-btn"
|
||||
@click=${() => this.kickPlayer(client.clientID)}
|
||||
title="Remove ${client.username}"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
`}
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-game-button-container">
|
||||
<button
|
||||
@click=${this.startGame}
|
||||
?disabled=${this.players.length < 2}
|
||||
?disabled=${this.clients.length < 2}
|
||||
class="start-game-button"
|
||||
>
|
||||
${
|
||||
this.players.length === 1
|
||||
this.clients.length === 1
|
||||
? translateText("host_modal.waiting")
|
||||
: translateText("host_modal.start")
|
||||
}
|
||||
@@ -434,12 +456,13 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.lobbyCreatorClientID = generateID();
|
||||
this.lobbyIdVisible = this.userSettings.get(
|
||||
"settings.lobbyIdVisibility",
|
||||
true,
|
||||
);
|
||||
|
||||
createLobby()
|
||||
createLobby(this.lobbyCreatorClientID)
|
||||
.then((lobby) => {
|
||||
this.lobbyId = lobby.gameID;
|
||||
// join lobby
|
||||
@@ -449,7 +472,7 @@ export class HostLobbyModal extends LitElement {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: this.lobbyId,
|
||||
clientID: generateID(),
|
||||
clientID: this.lobbyCreatorClientID,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
@@ -633,17 +656,29 @@ 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.clients = data.clients ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
private kickPlayer(clientID: string) {
|
||||
// Dispatch event to be handled by WebSocket instead of HTTP
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("kick-player", {
|
||||
detail: { target: clientID },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(): Promise<GameInfo> {
|
||||
async function createLobby(creatorClientID: string): Promise<GameInfo> {
|
||||
const config = await getServerConfigFromClient();
|
||||
try {
|
||||
const id = generateID();
|
||||
const response = await fetch(
|
||||
`/${config.workerPath(id)}/api/create_game/${id}`,
|
||||
`/${config.workerPath(id)}/api/create_game/${id}?creatorClientID=${encodeURIComponent(creatorClientID)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -654,6 +689,8 @@ async function createLobby(): Promise<GameInfo> {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Server error response:", errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -663,6 +700,6 @@ async function createLobby(): Promise<GameInfo> {
|
||||
return data as GameInfo;
|
||||
} catch (error) {
|
||||
console.error("Error creating lobby:", error);
|
||||
throw error; // Re-throw the error so the caller can handle it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user