Add player limit feature for private lobbies

This commit is contained in:
abdallahbahrawi1
2025-12-19 13:33:43 +02:00
parent 58c7cdd46f
commit 37d358fbdc
7 changed files with 148 additions and 7 deletions
+5 -2
View File
@@ -127,7 +127,8 @@
"checking": "private_lobby.checking",
"not_found": "private_lobby.not_found",
"error": "private_lobby.error",
"joined_waiting": "private_lobby.joined_waiting"
"joined_waiting": "private_lobby.joined_waiting",
"lobby_full": "private_lobby.lobby_full"
},
"public_lobby": {
"join": "public_lobby.join",
@@ -154,7 +155,9 @@
"player": "host_modal.player",
"players": "host_modal.players",
"waiting": "host_modal.waiting",
"start": "host_modal.start"
"start": "host_modal.start",
"player_limit": "host_modal.player_limit",
"player_limit_warning": "host_modal.player_limit_warning"
},
"game_starting_modal": {
"title": "game_starting_modal.title",
+5 -2
View File
@@ -239,7 +239,8 @@
"not_found": "Lobby not found. Please check the ID and try again.",
"error": "An error occurred. Please try again or contact support.",
"joined_waiting": "Joined successfully! Waiting for game to start...",
"version_mismatch": "This game was created with a different version. Cannot join."
"version_mismatch": "This game was created with a different version. Cannot join.",
"lobby_full": "Lobby is full (limit: {limit})."
},
"public_lobby": {
"join": "Join next Game",
@@ -292,7 +293,9 @@
"assigned_teams": "Assigned Teams",
"empty_teams": "Empty Teams",
"empty_team": "Empty",
"remove_player": "Remove {username}"
"remove_player": "Remove {username}",
"player_limit": "Player limit",
"player_limit_warning": "Limit is below current player count; new players can't join."
},
"team_colors": {
"red": "Red",
+30
View File
@@ -125,6 +125,36 @@ export function joinLobby(
}
if (message.type === "error") {
if (message.error === "full-lobby") {
// Parse the message to get the player limit
let limitInfo = "";
try {
if (message.message) {
const data = JSON.parse(message.message);
if (data.maxPlayers) {
limitInfo = translateText("private_lobby.lobby_full", {
limit: data.maxPlayers,
});
}
}
} catch {
limitInfo = translateText("private_lobby.lobby_full", {
limit: "?",
});
}
// Show the full lobby message
if (limitInfo) {
showErrorModal(
message.error,
limitInfo,
lobbyConfig.gameID,
lobbyConfig.clientID,
true,
false,
"error_modal.connection_error",
);
}
document.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: lobbyConfig.gameID },
+90 -1
View File
@@ -59,6 +59,11 @@ export class HostLobbyModal extends LitElement {
@state() private lobbyCreatorClientID: string = "";
@state() private lobbyIdVisible: boolean = true;
@state() private nationCount: number = 0;
@state() private playerLimit: number | null = null; // null = unlimited
@state() private playerLimitWarning: boolean = false;
private static readonly MIN_PLAYER_LIMIT = 2;
private static readonly MAX_PLAYER_LIMIT = 1000;
private playersInterval: NodeJS.Timeout | null = null;
// Add a new timer for debouncing bot changes
@@ -525,6 +530,56 @@ export class HostLobbyModal extends LitElement {
${translateText("host_modal.max_timer")}
</div>
</label>
<!-- Player Limit -->
<label
for="player-limit"
class="option-card ${this.playerLimit !== null ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="player-limit"
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
if (checked) {
this.playerLimit = 20; // Default value
} else {
this.playerLimit = null;
}
this.updatePlayerLimitWarning();
this.putGameConfig();
}}
.checked=${this.playerLimit !== null}
/>
${
this.playerLimit === null
? ""
: html`<input
type="number"
id="player-limit-value"
min="${HostLobbyModal.MIN_PLAYER_LIMIT}"
max="${HostLobbyModal.MAX_PLAYER_LIMIT}"
.value=${String(this.playerLimit ?? "")}
style="width: 60px; color: black; text-align: right; border-radius: 8px;"
@input=${this.handlePlayerLimitChange}
@keydown=${this.handlePlayerLimitKeyDown}
/>`
}
<div class="option-card-title">
${translateText("host_modal.player_limit")}
</div>
</label>
${
this.playerLimitWarning
? html`<div
class="player-limit-warning"
style="color: #ffa500; font-size: 12px; text-align: center; padding: 4px;"
>
${translateText("host_modal.player_limit_warning")}
</div>`
: ""
}
<hr style="width: 100%; border-top: 1px solid #444; margin: 16px 0;" />
<!-- Individual disables for structures/weapons -->
@@ -549,7 +604,7 @@ export class HostLobbyModal extends LitElement {
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.clients.length}
${this.clients.length}${this.playerLimit !== null ? `/${this.playerLimit}` : ""}
${
this.clients.length === 1
? translateText("host_modal.player")
@@ -735,6 +790,38 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}
private handlePlayerLimitKeyDown(e: KeyboardEvent) {
if (["-", "+", "e"].includes(e.key)) {
e.preventDefault();
}
}
private handlePlayerLimitChange(e: Event) {
const input = e.target as HTMLInputElement;
// Remove invalid characters
input.value = input.value.replace(/[e+-]/gi, "");
const value = parseInt(input.value);
// Validate: must be a number between MIN and MAX
if (
isNaN(value) ||
value < HostLobbyModal.MIN_PLAYER_LIMIT ||
value > HostLobbyModal.MAX_PLAYER_LIMIT
) {
return;
}
this.playerLimit = value;
this.updatePlayerLimitWarning();
this.putGameConfig();
}
private updatePlayerLimitWarning() {
this.playerLimitWarning =
this.playerLimit !== null && this.clients.length > this.playerLimit;
}
private async handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
console.log(`updating disable npcs to ${this.disableNPCs}`);
@@ -786,6 +873,7 @@ export class HostLobbyModal extends LitElement {
}),
maxTimerValue:
this.maxTimer === true ? this.maxTimerValue : undefined,
maxPlayers: this.playerLimit ?? undefined,
} satisfies Partial<GameConfig>),
},
);
@@ -854,6 +942,7 @@ export class HostLobbyModal extends LitElement {
console.log(`got game info response: ${JSON.stringify(data)}`);
this.clients = data.clients ?? [];
this.updatePlayerLimitWarning();
});
}
+7 -1
View File
@@ -18,6 +18,7 @@ export class JoinPrivateLobbyModal extends LitElement {
@state() private message: string = "";
@state() private hasJoined = false;
@state() private players: string[] = [];
@state() private maxPlayers: number | null = null;
private playersInterval: NodeJS.Timeout | null = null;
@@ -75,7 +76,9 @@ export class JoinPrivateLobbyModal extends LitElement {
${this.hasJoined && this.players.length > 0
? html` <div class="options-section">
<div class="option-title">
${this.players.length}
${this.players.length}${this.maxPlayers !== null
? `/${this.maxPlayers}`
: ""}
${this.players.length === 1
? translateText("private_lobby.player")
: translateText("private_lobby.players")}
@@ -127,6 +130,8 @@ export class JoinPrivateLobbyModal extends LitElement {
this.close();
this.hasJoined = false;
this.message = "";
this.maxPlayers = null;
this.players = [];
this.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: this.lobbyIdInput.value },
@@ -315,6 +320,7 @@ export class JoinPrivateLobbyModal extends LitElement {
.then((response) => response.json())
.then((data: GameInfo) => {
this.players = data.clients?.map((p) => p.username) ?? [];
this.maxPlayers = data.gameConfig?.maxPlayers ?? null;
})
.catch((error) => {
console.error("Error polling players:", error);
+1 -1
View File
@@ -170,7 +170,7 @@ export const GameConfigSchema = z.object({
infiniteTroops: z.boolean(),
instantBuild: z.boolean(),
randomSpawn: z.boolean(),
maxPlayers: z.number().optional(),
maxPlayers: z.number().int().min(2).max(1000).optional(),
maxTimerValue: z.number().int().min(1).max(120).optional(),
disabledUnits: z.enum(UnitType).array().optional(),
playerTeams: TeamCountConfigSchema.optional(),
+10
View File
@@ -128,6 +128,10 @@ export class GameServer {
if (gameConfig.playerTeams !== undefined) {
this.gameConfig.playerTeams = gameConfig.playerTeams;
}
if (gameConfig.maxPlayers !== undefined) {
this.gameConfig.maxPlayers = gameConfig.maxPlayers;
}
}
public joinClient(client: Client) {
@@ -152,12 +156,18 @@ export class GameServer {
) {
this.log.warn(`cannot add client, game full`, {
clientID: client.clientID,
currentPlayers: this.activeClients.length,
maxPlayers: this.gameConfig.maxPlayers,
});
client.ws.send(
JSON.stringify({
type: "error",
error: "full-lobby",
message: JSON.stringify({
currentPlayers: this.activeClients.length,
maxPlayers: this.gameConfig.maxPlayers,
}),
} satisfies ServerErrorMessage),
);
return;