From 38b87b72176304dd7046fad8e900b5bf6e256d01 Mon Sep 17 00:00:00 2001 From: DiesselOne <49088589+DiesselOne@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:32:50 +0200 Subject: [PATCH] Rework leaderboard and team stats (#1164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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:     --- src/client/graphics/GameRenderer.ts | 10 + .../graphics/icons/LeaderboardRegularIcon.ts | 35 +++ .../graphics/icons/LeaderboardSolidIcon.ts | 35 +++ src/client/graphics/icons/TeamRegularIcon.ts | 35 +++ src/client/graphics/icons/TeamSolidIcon.ts | 35 +++ src/client/graphics/layers/GameLeftSidebar.ts | 80 ++++++ src/client/graphics/layers/Leaderboard.ts | 259 ++++++------------ src/client/graphics/layers/TeamStats.ts | 213 +++++--------- src/client/index.html | 4 +- 9 files changed, 385 insertions(+), 321 deletions(-) create mode 100644 src/client/graphics/icons/LeaderboardRegularIcon.ts create mode 100644 src/client/graphics/icons/LeaderboardSolidIcon.ts create mode 100644 src/client/graphics/icons/TeamRegularIcon.ts create mode 100644 src/client/graphics/icons/TeamSolidIcon.ts create mode 100644 src/client/graphics/layers/GameLeftSidebar.ts diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 8699c2741..b759a907d 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -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, diff --git a/src/client/graphics/icons/LeaderboardRegularIcon.ts b/src/client/graphics/icons/LeaderboardRegularIcon.ts new file mode 100644 index 000000000..46f455fb0 --- /dev/null +++ b/src/client/graphics/icons/LeaderboardRegularIcon.ts @@ -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` + + + + `; + } +} diff --git a/src/client/graphics/icons/LeaderboardSolidIcon.ts b/src/client/graphics/icons/LeaderboardSolidIcon.ts new file mode 100644 index 000000000..31847cda3 --- /dev/null +++ b/src/client/graphics/icons/LeaderboardSolidIcon.ts @@ -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` + + + + `; + } +} diff --git a/src/client/graphics/icons/TeamRegularIcon.ts b/src/client/graphics/icons/TeamRegularIcon.ts new file mode 100644 index 000000000..cac1c7acf --- /dev/null +++ b/src/client/graphics/icons/TeamRegularIcon.ts @@ -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` + + + + `; + } +} diff --git a/src/client/graphics/icons/TeamSolidIcon.ts b/src/client/graphics/icons/TeamSolidIcon.ts new file mode 100644 index 000000000..d4e021e6a --- /dev/null +++ b/src/client/graphics/icons/TeamSolidIcon.ts @@ -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` + + + + `; + } +} diff --git a/src/client/graphics/layers/GameLeftSidebar.ts b/src/client/graphics/layers/GameLeftSidebar.ts new file mode 100644 index 000000000..0cce032a1 --- /dev/null +++ b/src/client/graphics/layers/GameLeftSidebar.ts @@ -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` + + `; + } +} diff --git a/src/client/graphics/layers/Leaderboard.ts b/src/client/graphics/layers/Leaderboard.ts index be9bafed6..703c011ab 100644 --- a/src/client/graphics/layers/Leaderboard.ts +++ b/src/client/graphics/layers/Leaderboard.ts @@ -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` - this.toggleLeaderboard()} - class="leaderboard__button ${this._shownOnInit && - this._leaderboardHidden + - ${translateText("leaderboard.title")} - - e.preventDefault()} + @contextmenu=${(e: Event) => e.preventDefault()} > this.hideLeaderboard()} - > - ${translateText("leaderboard.hide")} - - { this.showTopFive = !this.showTopFive; this.updateLeaderboard(); @@ -309,11 +189,22 @@ export class Leaderboard extends LitElement implements Layer { > ${this.showTopFive ? "Show All" : "Show Top 5"} - - - # - ${translateText("leaderboard.player")} - this.setSort("tiles")}> + + + + + # + + + ${translateText("leaderboard.player")} + + this.setSort("tiles")} + > ${translateText("leaderboard.owned")} ${this._sortKey === "tiles" ? this._sortOrder === "asc" @@ -321,7 +212,10 @@ export class Leaderboard extends LitElement implements Layer { : "⬇️" : ""} - this.setSort("gold")}> + this.setSort("gold")} + > ${translateText("leaderboard.gold")} ${this._sortKey === "gold" ? this._sortOrder === "asc" @@ -329,7 +223,10 @@ export class Leaderboard extends LitElement implements Layer { : "⬇️" : ""} - this.setSort("troops")}> + this.setSort("troops")} + > ${translateText("leaderboard.troops")} ${this._sortKey === "troops" ? this._sortOrder === "asc" @@ -338,19 +235,42 @@ export class Leaderboard extends LitElement implements Layer { : ""} - ${this.players.map( + + ${repeat( + this.players, + (p) => p.player.id(), (player) => html` this.handleRowClickPlayer(player.player)} > - ${player.position} - ${player.name} - ${player.score} - ${player.gold} - ${player.troops} + + ${player.position} + + + ${player.name} + + + ${player.score} + + + ${player.gold} + + + ${player.troops} + `, )} @@ -358,37 +278,12 @@ export class Leaderboard extends LitElement implements Layer { `; } - - 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) + "%"; } diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts index 42a603a69..459b2ab1d 100644 --- a/src/client/graphics/layers/TeamStats.ts +++ b/src/client/graphics/layers/TeamStats.ts @@ -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 = {}; + 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` - this.toggleTeamStats()} - class="team-stats-button ${this._shownOnInit && this._teamStatsHidden + - Team Stats - - e.preventDefault()} > - this.hideTeamStats()} + - Hide - - - - - Team - Owned - Gold - Troops - - - - ${this.teams.map( - (team) => html` - - ${team.teamName} - ${team.totalScoreStr} - ${team.totalGold} - ${team.totalTroops} - - `, - )} - - + + + + Team + + + Owned + + + Gold + + + Troops + + + ${this.teams.map( + (team) => html` + + + ${team.teamName} + + + ${team.totalScoreStr} + + + ${team.totalGold} + + + ${team.totalTroops} + + + `, + )} + `; } - - 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) + "%"; } diff --git a/src/client/index.html b/src/client/index.html index c22738bdb..5292bbd18 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -399,12 +399,11 @@ - - + @@ -414,6 +413,7 @@ +