mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
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:    
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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) + "%";
|
||||
}
|
||||
|
||||
@@ -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) + "%";
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user