Rework leaderboard and team stats (#1164)

## Description:
This PR update the leaderboard and team stats component to be more
responsive on mobile and desktop.
Include new component gameLeftISidebar that manages showing/hiding
elements.
Add icons as components for easy use

## 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 understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Diessel

Closes #1163 

Images:
![Snímek obrazovky 2025-06-13 v 18 45
44](https://github.com/user-attachments/assets/8c80fbfc-ae7d-449d-88a6-5072ede8d0e4)
![Snímek obrazovky 2025-06-13 v 18 45
39](https://github.com/user-attachments/assets/e386e368-3cf5-4cf5-b85b-8380a5785d62)
![Snímek obrazovky 2025-06-13 v 18 45
32](https://github.com/user-attachments/assets/efc39a92-bcd8-4f3c-a281-6570a92e4633)
![Snímek obrazovky 2025-06-13 v 18 45
27](https://github.com/user-attachments/assets/6bcb8462-ee11-4de5-af4c-1763f95fa56a)
This commit is contained in:
DiesselOne
2025-06-13 20:32:50 +02:00
committed by GitHub
parent e68d48c3a8
commit 38b87b7217
9 changed files with 385 additions and 321 deletions
+10
View File
@@ -11,6 +11,7 @@ import { ControlPanel } from "./layers/ControlPanel";
import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay";
import { FxLayer } from "./layers/FxLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
@@ -72,6 +73,14 @@ export function createRenderer(
leaderboard.eventBus = eventBus;
leaderboard.game = game;
const gameLeftSidebar = document.querySelector(
"game-left-sidebar",
) as GameLeftSidebar;
if (!gameLeftSidebar || !(gameLeftSidebar instanceof GameLeftSidebar)) {
console.error("GameLeftSidebar element not found in the DOM");
}
gameLeftSidebar.game = game;
const teamStats = document.querySelector("team-stats") as TeamStats;
if (!emojiTable || !(teamStats instanceof TeamStats)) {
console.error("EmojiTable element not found in the DOM");
@@ -219,6 +228,7 @@ export function createRenderer(
),
new SpawnTimer(game, transformHandler),
leaderboard,
gameLeftSidebar,
controlPanel,
playerInfo,
winModel,
@@ -0,0 +1,35 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("leaderboard-regular-icon")
export class LeaderboardRegularIcon extends LitElement {
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
@property({ type: String }) color = "currentColor";
static styles = css`
:host {
display: inline-block;
vertical-align: middle;
}
svg {
display: block;
}
`;
render() {
return html`
<svg
xmlns="http://www.w3.org/2000/svg"
width="${this.size}"
height="${this.size}"
viewBox="0 0 24 24"
fill="${this.color}"
>
<path
fill="currentColor"
d="M4 19h4v-8H4zm6 0h4V5h-4zm6 0h4v-6h-4zM2 21V9h6V3h8v8h6v10z"
/>
</svg>
`;
}
}
@@ -0,0 +1,35 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("leaderboard-solid-icon")
export class LeaderboardSolidIcon extends LitElement {
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
@property({ type: String }) color = "currentColor";
static styles = css`
:host {
display: inline-block;
vertical-align: middle;
}
svg {
display: block;
}
`;
render() {
return html`
<svg
xmlns="http://www.w3.org/2000/svg"
width="${this.size}"
height="${this.size}"
viewBox="0 0 24 24"
fill="${this.color}"
>
<path
fill="currentColor"
d="M2 21V9h5.5v12zm7.25 0V3h5.5v18zm7.25 0V11H22v10z"
/>
</svg>
`;
}
}
@@ -0,0 +1,35 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("team-regular-icon")
export class TeamRegularIcon extends LitElement {
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
@property({ type: String }) color = "currentColor";
static styles = css`
:host {
display: inline-block;
vertical-align: middle;
}
svg {
display: block;
}
`;
render() {
return html`
<svg
xmlns="http://www.w3.org/2000/svg"
width="${this.size}"
height="${this.size}"
viewBox="0 0 24 24"
fill="${this.color}"
>
<path
fill="currentColor"
d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5M4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5S5.5 6.57 5.5 8.5S7.07 12 9 12m0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7m7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44M15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35c.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35"
/>
</svg>
`;
}
}
@@ -0,0 +1,35 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("team-solid-icon")
export class TeamSolidIcon extends LitElement {
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
@property({ type: String }) color = "currentColor";
static styles = css`
:host {
display: inline-block;
vertical-align: middle;
}
svg {
display: block;
}
`;
render() {
return html`
<svg
xmlns="http://www.w3.org/2000/svg"
width="${this.size}"
height="${this.size}"
viewBox="0 0 24 24"
fill="${this.color}"
>
<path
fill="currentColor"
d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3s1.34 3 3 3m-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5S5 6.34 5 8s1.34 3 3 3m0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5m8 0c-.29 0-.62.02-.97.05c1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5"
/>
</svg>
`;
}
}
@@ -0,0 +1,80 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import "../icons/LeaderboardRegularIcon";
import "../icons/LeaderboardSolidIcon";
import "../icons/TeamRegularIcon";
import "../icons/TeamSolidIcon";
import { Layer } from "./Layer";
@customElement("game-left-sidebar")
export class GameLeftSidebar extends LitElement implements Layer {
@state()
private isLeaderboardShow = false;
@state()
private isTeamLeaderboardShow = false;
private isVisible = false;
public game: GameView;
createRenderRoot() {
return this;
}
init() {
this.isVisible = true;
this.requestUpdate();
}
private toggleLeaderboard(): void {
this.isLeaderboardShow = !this.isLeaderboardShow;
}
private toggleTeamLeaderboard(): void {
this.isTeamLeaderboardShow = !this.isTeamLeaderboardShow;
}
private get isTeamGame(): boolean {
return this.game?.config().gameConfig().gameMode === GameMode.Team;
}
render() {
return html`
<aside
class=${`fixed top-[70px] left-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg transition-transform duration-300 ease-out transform ${
this.isVisible ? "translate-x-0" : "-translate-x-full"
}`}
>
<div class="flex items-center gap-2 space-x-2 text-white mb-2">
<div class="w-6 h-6 cursor-pointer" @click=${this.toggleLeaderboard}>
${this.isLeaderboardShow
? html` <leaderboard-solid-icon></leaderboard-solid-icon>`
: html` <leaderboard-regular-icon></leaderboard-regular-icon>`}
</div>
${this.isTeamGame
? html`
<div
class="w-6 h-6 cursor-pointer"
@click=${this.toggleTeamLeaderboard}
>
${this.isTeamLeaderboardShow
? html` <team-solid-icon></team-solid-icon>`
: html` <team-regular-icon></team-regular-icon>`}
</div>
`
: null}
</div>
<div>
<leader-board
class="block mb-2"
.visible=${this.isLeaderboardShow}
></leader-board>
<team-stats
.visible=${this.isTeamLeaderboardShow && this.isTeamGame}
></team-stats>
</div>
<slot></slot>
</aside>
`;
}
}
+77 -182
View File
@@ -1,5 +1,6 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
@@ -38,8 +39,7 @@ export class Leaderboard extends LitElement implements Layer {
players: Entry[] = [];
@state()
private _leaderboardHidden = true;
@property({ type: Boolean }) visible = false;
private _shownOnInit = false;
private showTopFive = true;
@@ -49,19 +49,19 @@ export class Leaderboard extends LitElement implements Layer {
@state()
private _sortOrder: "asc" | "desc" = "desc";
createRenderRoot() {
return this; // use light DOM for Tailwind support
}
init() {}
tick() {
if (this.game === null) throw new Error("Not initialized");
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
this._shownOnInit = true;
this.showLeaderboard();
this.updateLeaderboard();
}
if (this._leaderboardHidden) {
return;
}
if (!this.visible) return;
if (this.game.ticks() % 10 === 0) {
this.updateLeaderboard();
}
@@ -163,145 +163,25 @@ export class Leaderboard extends LitElement implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {}
shouldTransform(): boolean {
return false;
}
static styles = css`
:host {
display: block;
}
img.emoji {
height: 1em;
width: auto;
}
.leaderboard {
position: fixed;
top: 70px;
left: 10px;
z-index: 9998;
background-color: rgb(31 41 55 / 0.7);
padding: 0 10px 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border-radius: 10px;
max-width: 500px;
max-height: 30vh;
overflow-y: auto;
backdrop-filter: blur(5px);
}
.hidden {
display: none !important;
}
.leaderboard__grid {
display: grid;
grid-template-columns: 40px 100px 85px 65px 65px;
width: 100%;
font-size: 14px;
}
.leaderboard__button {
position: fixed;
left: 10px;
top: 70px;
z-index: 9998;
background-color: rgb(31 41 55 / 0.7);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
.leaderboard__actionButton {
background: none;
border: none;
color: white;
cursor: pointer;
}
.leaderboard__row {
display: contents;
> div {
display: flex;
justify-content: center;
text-align: center;
align-items: center;
padding: 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: white;
}
&:hover {
> div {
background-color: rgba(78, 78, 78, 0.8);
}
}
}
.leaderboard__row--header {
> div {
background-color: rgb(31 41 55 / 0.5);
font-weight: bold;
color: white;
}
}
.myPlayer > div {
font-weight: bold;
}
.player-name {
max-width: 10ch;
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 980px) {
.player-name {
max-width: 14ch;
}
.leaderboard {
top: 10px;
left: 10px;
}
.leaderboard__button {
left: 10px;
top: 10px;
}
}
@media (min-width: 1336px) {
.leaderboard__grid {
grid-template-columns: 60px 120px 105px 85px 85px;
font-size: 16px;
}
}
`;
render() {
if (!this.visible) {
return html``;
}
return html`
<button
@click=${() => this.toggleLeaderboard()}
class="leaderboard__button ${this._shownOnInit &&
this._leaderboardHidden
<div
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-sm md:max-h-[50vh] ${this
.visible
? ""
: "hidden"}"
>
${translateText("leaderboard.title")}
</button>
<div
class="leaderboard ${this._leaderboardHidden ? "hidden" : ""}"
@contextmenu=${(e) => e.preventDefault()}
@contextmenu=${(e: Event) => e.preventDefault()}
>
<button
class="leaderboard__actionButton"
@click=${() => this.hideLeaderboard()}
>
${translateText("leaderboard.hide")}
</button>
<button
class="leaderboard__actionButton"
class="mb-2 px-2 py-1 md:px-3 md:py-1.5 text-xs md:text-sm lg:text-base border border-white/20 hover:bg-white/10"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
@@ -309,11 +189,22 @@ export class Leaderboard extends LitElement implements Layer {
>
${this.showTopFive ? "Show All" : "Show Top 5"}
</button>
<div class="leaderboard__grid">
<div class="leaderboard__row leaderboard__row--header">
<div>#</div>
<div>${translateText("leaderboard.player")}</div>
<div @click=${() => this.setSort("tiles")}>
<div
class="grid bg-slate-800/70 w-full text-xs md:text-sm lg:text-base"
style="grid-template-columns: 35px 100px 85px 65px 65px;"
>
<div class="contents font-bold bg-slate-700/50">
<div class="py-1.5 md:py-2.5 text-center border-b border-slate-500">
#
</div>
<div class="py-1.5 md:py-2.5 text-center border-b border-slate-500">
${translateText("leaderboard.player")}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
@click=${() => this.setSort("tiles")}
>
${translateText("leaderboard.owned")}
${this._sortKey === "tiles"
? this._sortOrder === "asc"
@@ -321,7 +212,10 @@ export class Leaderboard extends LitElement implements Layer {
: "⬇️"
: ""}
</div>
<div @click=${() => this.setSort("gold")}>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
@click=${() => this.setSort("gold")}
>
${translateText("leaderboard.gold")}
${this._sortKey === "gold"
? this._sortOrder === "asc"
@@ -329,7 +223,10 @@ export class Leaderboard extends LitElement implements Layer {
: "⬇️"
: ""}
</div>
<div @click=${() => this.setSort("troops")}>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
@click=${() => this.setSort("troops")}
>
${translateText("leaderboard.troops")}
${this._sortKey === "troops"
? this._sortOrder === "asc"
@@ -338,19 +235,42 @@ export class Leaderboard extends LitElement implements Layer {
: ""}
</div>
</div>
${this.players.map(
${repeat(
this.players,
(p) => p.player.id(),
(player) => html`
<div
class="leaderboard__row ${player.isMyPlayer
? "myPlayer"
: "otherPlayer"}"
class="contents hover:bg-slate-600/60 ${player.isMyPlayer
? "font-bold"
: ""} cursor-pointer"
@click=${() => this.handleRowClickPlayer(player.player)}
>
<div>${player.position}</div>
<div class="player-name">${player.name}</div>
<div>${player.score}</div>
<div>${player.gold}</div>
<div>${player.troops}</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${player.position}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 truncate"
>
${player.name}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${player.score}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${player.gold}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${player.troops}
</div>
</div>
`,
)}
@@ -358,37 +278,12 @@ export class Leaderboard extends LitElement implements Layer {
</div>
`;
}
toggleLeaderboard() {
this._leaderboardHidden = !this._leaderboardHidden;
this.requestUpdate();
}
hideLeaderboard() {
this._leaderboardHidden = true;
this.requestUpdate();
}
showLeaderboard() {
this._leaderboardHidden = false;
this.requestUpdate();
}
get isVisible() {
return !this._leaderboardHidden;
}
}
function formatPercentage(value: number): string {
const perc = value * 100;
if (perc > 99.5) {
return "100%";
}
if (perc < 0.01) {
return "0%";
}
if (perc < 0.1) {
return perc.toPrecision(1) + "%";
}
if (perc > 99.5) return "100%";
if (perc < 0.01) return "0%";
if (perc < 0.1) return perc.toPrecision(1) + "%";
return perc.toPrecision(2) + "%";
}
+76 -137
View File
@@ -1,5 +1,5 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameMode } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
@@ -11,6 +11,7 @@ interface TeamEntry {
totalScoreStr: string;
totalGold: string;
totalTroops: string;
totalScoreSort: number;
players: PlayerView[];
}
@@ -19,26 +20,25 @@ export class TeamStats extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
@property({ type: Boolean }) visible = false;
teams: TeamEntry[] = [];
@state()
private _teamStatsHidden = true;
private _shownOnInit = false;
createRenderRoot() {
return this; // use light DOM for Tailwind
}
init() {}
tick() {
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
return;
}
if (this.game.config().gameConfig().gameMode !== GameMode.Team) return;
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
this._shownOnInit = true;
this._teamStatsHidden = false;
this.updateTeamStats();
}
if (this._teamStatsHidden) return;
if (!this.visible) return;
if (this.game.ticks() % 10 === 0) {
this.updateTeamStats();
@@ -47,8 +47,8 @@ export class TeamStats extends LitElement implements Layer {
private updateTeamStats() {
const players = this.game.playerViews();
const grouped: Record<number, PlayerView[]> = {};
for (const player of players) {
const team = player.team();
if (team === null) continue;
@@ -87,149 +87,88 @@ export class TeamStats extends LitElement implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {}
shouldTransform(): boolean {
return false;
}
static styles = css`
:host {
display: block;
}
.team-stats {
position: fixed;
top: 10px;
left: 450px;
z-index: 9999;
background-color: rgb(31 41 55 / 0.7);
padding: 10px;
padding-top: 0px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border-radius: 10px;
max-width: 250px;
max-height: 30vh;
overflow-y: auto;
width: 400px;
backdrop-filter: blur(5px);
}
.teamStats-close-button {
background: none;
border: none;
color: white;
cursor: pointer;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 5px;
text-align: center;
border-bottom: 1px solid rgba(51, 51, 51, 0.2);
color: var(--text-color, white);
}
th {
background-color: rgb(31 41 55 / 0.5);
color: white;
}
.hidden {
display: none !important;
}
.team-stats-button {
position: fixed;
left: 450px;
top: 10px;
z-index: 9999;
background-color: rgb(31 41 55 / 0.7);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
`;
render() {
if (!this.visible) {
return html``;
}
return html`
<button
@click=${() => this.toggleTeamStats()}
class="team-stats-button ${this._shownOnInit && this._teamStatsHidden
<div
class="max-h-[30vh] overflow-y-auto grid bg-slate-800/70 w-full text-white text-xs md:text-sm ${this
.visible
? ""
: "hidden"}"
>
Team Stats
</button>
<div
class="team-stats ${this._teamStatsHidden ? "hidden" : ""}"
@contextmenu=${(e) => e.preventDefault()}
>
<button
class="teamStats-close-button"
@click=${() => this.hideTeamStats()}
<div
class="grid w-full"
style="grid-template-columns: 1fr 1fr 1fr 1fr;"
>
Hide
</button>
<table>
<thead>
<tr>
<th>Team</th>
<th>Owned</th>
<th>Gold</th>
<th>Troops</th>
</tr>
</thead>
<tbody>
${this.teams.map(
(team) => html`
<tr class="">
<td>${team.teamName}</td>
<td>${team.totalScoreStr}</td>
<td>${team.totalGold}</td>
<td>${team.totalTroops}</td>
</tr>
`,
)}
</tbody>
</table>
<!-- Header row -->
<div class="contents font-bold bg-slate-700/50">
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
Team
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
Owned
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
Gold
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
Troops
</div>
</div>
${this.teams.map(
(team) => html`
<div
class="contents hover:bg-slate-600/60 text-center cursor-pointer"
>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.teamName}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.totalScoreStr}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.totalGold}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.totalTroops}
</div>
</div>
`,
)}
</div>
</div>
`;
}
toggleTeamStats() {
this._teamStatsHidden = !this._teamStatsHidden;
}
hideTeamStats() {
this._teamStatsHidden = true;
this.requestUpdate();
}
showTeamStats() {
this._teamStatsHidden = true;
this.requestUpdate();
}
get isVisible() {
return !this._teamStatsHidden;
}
}
function formatPercentage(value: number): string {
const perc = value * 100;
if (perc > 99.5) {
return "100%";
}
if (perc < 0.01) {
return "0%";
}
if (perc < 0.1) {
return perc.toPrecision(1) + "%";
}
if (perc > 99.5) return "100%";
if (perc < 0.01) return "0%";
if (perc < 0.1) return perc.toPrecision(1) + "%";
return perc.toPrecision(2) + "%";
}
+2 -2
View File
@@ -399,12 +399,11 @@
<host-lobby-modal></host-lobby-modal>
<join-private-lobby-modal></join-private-lobby-modal>
<emoji-table></emoji-table>
<leader-board></leader-board>
<build-menu></build-menu>
<win-modal></win-modal>
<game-starting-modal></game-starting-modal>
<top-bar></top-bar>
<team-stats></team-stats>
<player-panel></player-panel>
<help-modal></help-modal>
<dark-mode-button></dark-mode-button>
@@ -414,6 +413,7 @@
<unit-info-modal></unit-info-modal>
<news-modal></news-modal>
<left-in-game-ad></left-in-game-ad>
<game-left-sidebar></game-left-sidebar>
<div
id="language-modal"
class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex justify-center items-center"