mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:50:42 +00:00
Fixes lobby team preview: clan players aren't assigned a team + add nation count + other small fixes (#2536)
## Description: Fixes for v28. In https://github.com/openfrontio/OpenFrontIO/pull/2444, lobby team preview was added. But for players with clan tags, this doesn't work correctly. They don't get assigned to the same team in the preview, while they do get assigned to the same team in the actual game. Also added some small fixes and QoL improvements like showing the number of Nations next to the number of Players. Since we needed that info anyway. Did not choose to show and assign Nations to teams (just the numbers), see why under CONSIDERED OPTIONS THAT I DIDN'T WENT WITH. **BEFORE:** https://youtu.be/AV_aDJ4PgOk <img width="767" height="117" alt="Malformed argument because of double accolades for remove_player" src="https://github.com/user-attachments/assets/7de1114e-7ce1-4a8f-95cc-6b0528a61e3b" /> **AFTER:** https://youtu.be/aDCKkwedqes Cause of bug: maxTeamSize is a number in assignTeams, only used to assign clan players. It uses the length of the players array as input. At actual game start, the Nations are also in the players array. But at lobby preview the Nations aren't yet fetched. So when 2 players want to do a 3 Teams private lobby with them using clan tags to be in the same team. maxTeamCount would do Math.ceil(2/3)=1. So only 1 clan player per team as a result. While actually there could be 10 Nations which would result in maxTeamCount Math.ceil(12/3)=4 so in the game they would actually be assigned to the same team. Fix for bug: fetch Nations count in HostLobbyModal and pass on to LobbyTeamView. Add it to the number of players for maxTeamCount that assignTeamsLobbyPreview uses for its calculation. Also added nation count to the similar teamMaxSize in LobbyTeamView itself, to display the same and correct number of max players. For random maps, we now need to know the random map before the game starts to get its Nation count. So made some changes for that too in HostLobbyModal. Also fixed: - willUpdate ran comptuteTeamPreview every second, now checks if properties like 'client' actually changed. PollPlayers in HostLobbyModal 'changes' the clients property every second even if there are no actual changes. Checks if the other properties are actually changed too, to make it more future proof. - cache teamsList so it is only fetched once instead of first in computeTeamPreview and then again for showTeamColors. - don't show the "Empty Teams" header if there are no empty teams. - console error ICU format error SyntaxError: MALFORMED ARGUMENT. Because of double accolades around remove_player in translation value. - remove fallback for comparing clients on clientID, which used client name. Players may have the same names so it's not safe to compare based on name. Also show number of Nations next to number of Players: now we now the nationCount since it is needed for the fix, show number of Nations next to number of Players. It's handy and it prevents confusion as to why it says 2/32 for two teams if there are only 2 players; it is because there are 61 Nations as well on the World map for example. Also determine number of teams based on Players + Nations: now we now the nationCount since it is needed for the fix, use it to determine the number of teams. Just like populateTeams in GameImpl does. This means for Duos on the World map, a minimum of 31 teams will be shown since there are 61 Nations. This is better than just show two teams based on 1 or 2 humans in the lobby. Also it makes more clear how many teams there'll be the game and how the players and nations are divided over the teams. Choose to not show the Nations' team assignments though. That could be for another PR. See explanation under CONSIDERED OPTIONS THAT I DIDN'T WENT WITH. Also show Nations team as pre-filled for HumansVSNations: now we now the nationCount since it is needed for the fix, for HumansVSNations, show the Nations team as fully assigned and non-empty. For example for World map it shows Nations 61/61. Don't show them as Empty Team but as Assigned Team. Although i choose not to show the actual Nations (see CONSIDERED OPTIONS THAT I DIDN'T WENT WITH), this makes it clear their team is pre-filled and how many Nations you're actually up against. Whereas for other Team game types like 7 Teams or Duos, it will display the teams that the Nations will fill up as empty. CONSIDERED OPTIONS THAT I DIDN'T WENT WITH - Use an optional param 'nationsCount' to assignTeams with default = 0. And simply add nationsCount to the players.length count for maxTeamSize. This would be error prone; 'nationsCount' should then never be assigned a value except when called from LobbyTeamView. But in the future someone might assign it a value even when called from somewhere else. Then you could say, check if there are Nations in the players array and if so, do not use Nationscount because we know they are already counted from players.length. But if Disable Nations is enabled, there would be no Nations in the players array but if nationsCount was somehow given a value we still should not use it. So again, too developer error prone. - Not only fetch the number of Nations, but actually get the Nations themselves to show them in the team assign preview as well. They are shuffled on team assignment but of course deterministicly (nation player ID assigned based on Pseudorandom seeded with GameID in GameRunner, then shuffled in TeamAssignment with Pseudorandom seeded with map's first Nation's playerID), so we could replicate it. But then, how to distinguish humans and Nations in the UI? This feels like something for a follow-up PR. FOR A FUTURE PR - change the way Clan team overflow is handled. Now they're "kicked" as in not assigned to a team without their knowing, are loaded into the game but cannot spawn. This UX could need some improvement. And the logic can be improved too, ie. don't "kick" too soon, check the number of Clans in the lobby and the number of teams to decide if we can assign the 'overflowing' Clan player to another team that doesn't have rivalling Clan players. Far out of scope for this PR. ## 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: tryout33
This commit is contained in:
@@ -275,6 +275,8 @@
|
||||
"enables_title": "Enable Settings",
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
"nation_players": "Nations",
|
||||
"nation_player": "Nation",
|
||||
"waiting": "Waiting for players...",
|
||||
"random_spawn": "Random spawn",
|
||||
"start": "Start Game",
|
||||
@@ -282,7 +284,7 @@
|
||||
"assigned_teams": "Assigned Teams",
|
||||
"empty_teams": "Empty Teams",
|
||||
"empty_team": "Empty",
|
||||
"remove_player": "Remove {{username}}"
|
||||
"remove_player": "Remove {username}"
|
||||
},
|
||||
"team_colors": {
|
||||
"red": "Red",
|
||||
|
||||
@@ -28,8 +28,8 @@ import "./components/Difficulties";
|
||||
import "./components/LobbyTeamView";
|
||||
import "./components/Maps";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
|
||||
|
||||
@customElement("host-lobby-modal")
|
||||
export class HostLobbyModal extends LitElement {
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
@@ -58,11 +58,13 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
@state() private lobbyCreatorClientID: string = "";
|
||||
@state() private lobbyIdVisible: boolean = true;
|
||||
@state() private nationCount: number = 0;
|
||||
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
// Add a new timer for debouncing bot changes
|
||||
private botsUpdateTimer: number | null = null;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
private mapLoader = terrainMapFileLoader;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -553,6 +555,13 @@ export class HostLobbyModal extends LitElement {
|
||||
? translateText("host_modal.player")
|
||||
: translateText("host_modal.players")
|
||||
}
|
||||
<span style="margin: 0 8px;">•</span>
|
||||
${this.nationCount}
|
||||
${
|
||||
this.nationCount === 1
|
||||
? translateText("host_modal.nation_player")
|
||||
: translateText("host_modal.nation_players")
|
||||
}
|
||||
</div>
|
||||
|
||||
<lobby-team-view
|
||||
@@ -560,6 +569,7 @@ export class HostLobbyModal extends LitElement {
|
||||
.clients=${this.clients}
|
||||
.lobbyCreatorClientID=${this.lobbyCreatorClientID}
|
||||
.teamCount=${this.teamCount}
|
||||
.nationCount=${this.disableNPCs ? 0 : this.nationCount}
|
||||
.onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)}
|
||||
></lobby-team-view>
|
||||
</div>
|
||||
@@ -613,6 +623,7 @@ export class HostLobbyModal extends LitElement {
|
||||
});
|
||||
this.modalEl?.open();
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
this.loadNationCount();
|
||||
}
|
||||
|
||||
public close() {
|
||||
@@ -631,12 +642,15 @@ export class HostLobbyModal extends LitElement {
|
||||
|
||||
private async handleRandomMapToggle() {
|
||||
this.useRandomMap = true;
|
||||
this.selectedMap = this.getRandomMap();
|
||||
await this.loadNationCount();
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleMapSelection(value: GameMapType) {
|
||||
this.selectedMap = value;
|
||||
this.useRandomMap = false;
|
||||
await this.loadNationCount();
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
@@ -794,10 +808,6 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
private async startGame() {
|
||||
if (this.useRandomMap) {
|
||||
this.selectedMap = this.getRandomMap();
|
||||
}
|
||||
|
||||
await this.putGameConfig();
|
||||
console.log(
|
||||
`Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
|
||||
@@ -857,6 +867,17 @@ export class HostLobbyModal extends LitElement {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async loadNationCount() {
|
||||
try {
|
||||
const mapData = this.mapLoader.getMapData(this.selectedMap);
|
||||
const manifest = await mapData.manifest();
|
||||
this.nationCount = manifest.nations.length;
|
||||
} catch (error) {
|
||||
console.warn("Failed to load nation count", error);
|
||||
this.nationCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(creatorClientID: string): Promise<GameInfo> {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Team,
|
||||
Trios,
|
||||
} from "../../core/game/Game";
|
||||
import { assignTeams } from "../../core/game/TeamAssignment";
|
||||
import { assignTeamsLobbyPreview } from "../../core/game/TeamAssignment";
|
||||
import { ClientInfo, TeamCountConfig } from "../../core/Schemas";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
@@ -31,19 +31,23 @@ export class LobbyTeamView extends LitElement {
|
||||
@property({ type: String }) lobbyCreatorClientID: string = "";
|
||||
@property({ attribute: "team-count" }) teamCount: TeamCountConfig = 2;
|
||||
@property({ type: Function }) onKickPlayer?: (clientID: string) => void;
|
||||
@property({ type: Number }) nationCount: number = 0;
|
||||
|
||||
private theme: PastelTheme = new PastelTheme();
|
||||
@state() private showTeamColors: boolean = false;
|
||||
|
||||
willUpdate(changedProperties: Map<string, any>) {
|
||||
// Recompute team preview when relevant properties change
|
||||
// clients is 'changed' every 1s from pollPlayers, chose to not compare for actual change
|
||||
if (
|
||||
changedProperties.has("gameMode") ||
|
||||
changedProperties.has("clients") ||
|
||||
changedProperties.has("teamCount")
|
||||
changedProperties.has("teamCount") ||
|
||||
changedProperties.has("nationCount")
|
||||
) {
|
||||
this.computeTeamPreview();
|
||||
this.showTeamColors = this.getTeamList().length <= 7;
|
||||
const teamsList = this.getTeamList();
|
||||
this.computeTeamPreview(teamsList);
|
||||
this.showTeamColors = teamsList.length <= 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +64,12 @@ export class LobbyTeamView extends LitElement {
|
||||
}
|
||||
|
||||
private renderTeamMode() {
|
||||
const active = this.teamPreview.filter((t) => t.players.length > 0);
|
||||
const empty = this.teamPreview.filter((t) => t.players.length === 0);
|
||||
const active = this.teamPreview.filter(
|
||||
(t) => t.players.length > 0 || t.team === ColoredTeams.Nations,
|
||||
);
|
||||
const empty = this.teamPreview.filter(
|
||||
(t) => t.players.length === 0 && t.team !== ColoredTeams.Nations,
|
||||
);
|
||||
return html` <div
|
||||
class="flex flex-col md:flex-row gap-3 md:gap-4 items-stretch max-h-[65vh]"
|
||||
>
|
||||
@@ -96,9 +104,11 @@ export class LobbyTeamView extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-200 mb-1 text-sm">
|
||||
${translateText("host_modal.empty_teams")}
|
||||
</div>
|
||||
${empty.length > 0
|
||||
? html`<div class="font-semibold text-gray-200 mb-1 text-sm">
|
||||
${translateText("host_modal.empty_teams")}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="w-full grid grid-cols-1 sm:grid-cols-2 gap-2 md:gap-3">
|
||||
${repeat(
|
||||
empty,
|
||||
@@ -136,6 +146,16 @@ export class LobbyTeamView extends LitElement {
|
||||
}
|
||||
|
||||
private renderTeamCard(preview: TeamPreviewData, isEmpty: boolean = false) {
|
||||
const displayCount =
|
||||
preview.team === ColoredTeams.Nations
|
||||
? this.nationCount
|
||||
: preview.players.length;
|
||||
|
||||
const maxTeamSize =
|
||||
preview.team === ColoredTeams.Nations
|
||||
? this.nationCount
|
||||
: this.teamMaxSize;
|
||||
|
||||
return html`
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-xl flex flex-col">
|
||||
<div
|
||||
@@ -148,9 +168,7 @@ export class LobbyTeamView extends LitElement {
|
||||
></span>`
|
||||
: null}
|
||||
<span class="truncate">${preview.team}</span>
|
||||
<span class="text-white/90"
|
||||
>${preview.players.length}/${this.teamMaxSize}</span
|
||||
>
|
||||
<span class="text-white/90">${displayCount}/${maxTeamSize}</span>
|
||||
</div>
|
||||
<div class="p-2 ${isEmpty ? "" : "flex flex-col gap-1.5"}">
|
||||
${isEmpty
|
||||
@@ -190,7 +208,7 @@ export class LobbyTeamView extends LitElement {
|
||||
|
||||
private getTeamList(): Team[] {
|
||||
if (this.gameMode !== GameMode.Team) return [];
|
||||
const playerCount = this.clients.length;
|
||||
const playerCount = this.clients.length + this.nationCount;
|
||||
const config = this.teamCount;
|
||||
|
||||
if (config === HumansVsNations) {
|
||||
@@ -230,13 +248,12 @@ export class LobbyTeamView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private computeTeamPreview() {
|
||||
private computeTeamPreview(teams: Team[] = []) {
|
||||
if (this.gameMode !== GameMode.Team) {
|
||||
this.teamPreview = [];
|
||||
this.teamMaxSize = 0;
|
||||
return;
|
||||
}
|
||||
const teams = this.getTeamList();
|
||||
|
||||
// HumansVsNations: show all clients under Humans initially
|
||||
if (this.teamCount === HumansVsNations) {
|
||||
@@ -252,7 +269,11 @@ export class LobbyTeamView extends LitElement {
|
||||
(c) =>
|
||||
new PlayerInfo(c.username, PlayerType.Human, c.clientID, c.clientID),
|
||||
);
|
||||
const assignment = assignTeams(players, teams);
|
||||
const assignment = assignTeamsLobbyPreview(
|
||||
players,
|
||||
teams,
|
||||
this.nationCount,
|
||||
);
|
||||
const buckets = new Map<Team, ClientInfo[]>();
|
||||
for (const t of teams) buckets.set(t, []);
|
||||
|
||||
@@ -260,9 +281,7 @@ export class LobbyTeamView extends LitElement {
|
||||
if (team === "kicked") continue;
|
||||
const bucket = buckets.get(team);
|
||||
if (!bucket) continue;
|
||||
const client =
|
||||
this.clients.find((c) => c.clientID === p.clientID) ??
|
||||
this.clients.find((c) => c.username === p.name);
|
||||
const client = this.clients.find((c) => c.clientID === p.clientID);
|
||||
if (client) bucket.push(client);
|
||||
}
|
||||
|
||||
@@ -277,7 +296,7 @@ export class LobbyTeamView extends LitElement {
|
||||
// Fallback: divide players across teams; guard against 0 and empty lobbies
|
||||
this.teamMaxSize = Math.max(
|
||||
1,
|
||||
Math.ceil(this.clients.length / teams.length),
|
||||
Math.ceil((this.clients.length + this.nationCount) / teams.length),
|
||||
);
|
||||
}
|
||||
this.teamPreview = teams.map((t) => ({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PlayerInfo, PlayerType, Team } from "./Game";
|
||||
export function assignTeams(
|
||||
players: PlayerInfo[],
|
||||
teams: Team[],
|
||||
maxTeamSize: number = getMaxTeamSize(players.length, teams.length),
|
||||
): Map<PlayerInfo, Team | "kicked"> {
|
||||
const result = new Map<PlayerInfo, Team | "kicked">();
|
||||
const teamPlayerCount = new Map<Team, number>();
|
||||
@@ -25,8 +26,6 @@ export function assignTeams(
|
||||
}
|
||||
}
|
||||
|
||||
const maxTeamSize = Math.ceil(players.length / teams.length);
|
||||
|
||||
// Sort clans by size (largest first)
|
||||
const sortedClans = Array.from(clanGroups.entries()).sort(
|
||||
(a, b) => b[1].length - a[1].length,
|
||||
@@ -87,3 +86,19 @@ export function assignTeams(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function assignTeamsLobbyPreview(
|
||||
players: PlayerInfo[],
|
||||
teams: Team[],
|
||||
nationCount: number,
|
||||
): Map<PlayerInfo, Team | "kicked"> {
|
||||
const maxTeamSize = getMaxTeamSize(
|
||||
players.length + nationCount,
|
||||
teams.length,
|
||||
);
|
||||
return assignTeams(players, teams, maxTeamSize);
|
||||
}
|
||||
|
||||
export function getMaxTeamSize(numPlayers: number, numTeams: number): number {
|
||||
return Math.ceil(numPlayers / numTeams);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user