Files
OpenFrontIO/src/client/graphics/layers/Leaderboard.ts
T
DiesselOne b7ee1caa52 * update leaderboard align (#1189)
## Description:
This fix issue of leaderboard overlaping other elements. 
Also give option to dispaly ads below leaderboard since on desktop they
are next to each other

## Please complete the following:

- [ ] 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
- [ ] 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

![image](https://github.com/user-attachments/assets/28d1a952-b9b1-4990-9a2a-59d221f5c007)

![image](https://github.com/user-attachmen
![Snímek obrazovky 2025-06-17 v 18 28
12](https://github.com/user-attachments/assets/f00bd060-361a-4c2f-98dd-1849f7d705ff)
ts/assets/5886ceb2-2d15-4b0e-9c30-8c61b0255f48)
![Snímek obrazovky 2025-06-17 v 18 28
36](https://github.com/user-attachments/assets/a60943ef-7865-43fa-a400-a7bd381b6ea3)
![Snímek obrazovky 2025-06-17 v 18 28
19](https://github.com/user-attachments/assets/7a328e7a-5a97-4b71-bb7a-40adac2d45c3)

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
2025-06-17 13:32:27 -07:00

291 lines
8.3 KiB
TypeScript

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";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
interface Entry {
name: string;
position: number;
score: string;
gold: string;
troops: string;
isMyPlayer: boolean;
player: PlayerView;
}
export class GoToPlayerEvent implements GameEvent {
constructor(public player: PlayerView) {}
}
export class GoToPositionEvent implements GameEvent {
constructor(
public x: number,
public y: number,
) {}
}
export class GoToUnitEvent implements GameEvent {
constructor(public unit: UnitView) {}
}
@customElement("leader-board")
export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null;
public eventBus: EventBus | null = null;
players: Entry[] = [];
@property({ type: Boolean }) visible = false;
private _shownOnInit = false;
private showTopFive = true;
@state()
private _sortKey: "tiles" | "gold" | "troops" = "tiles";
@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.updateLeaderboard();
}
if (!this.visible) return;
if (this.game.ticks() % 10 === 0) {
this.updateLeaderboard();
}
}
private setSort(key: "tiles" | "gold" | "troops") {
if (this._sortKey === key) {
this._sortOrder = this._sortOrder === "asc" ? "desc" : "asc";
} else {
this._sortKey = key;
this._sortOrder = "desc";
}
this.updateLeaderboard();
}
private updateLeaderboard() {
if (this.game === null) throw new Error("Not initialized");
const myPlayer = this.game.myPlayer();
let sorted = this.game.playerViews();
const compare = (a: number, b: number) =>
this._sortOrder === "asc" ? a - b : b - a;
switch (this._sortKey) {
case "gold":
sorted = sorted.sort((a, b) =>
compare(Number(a.gold()), Number(b.gold())),
);
break;
case "troops":
sorted = sorted.sort((a, b) => compare(a.troops(), b.troops()));
break;
default:
sorted = sorted.sort((a, b) =>
compare(a.numTilesOwned(), b.numTilesOwned()),
);
}
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();
const playersToShow = this.showTopFive ? sorted.slice(0, 5) : sorted;
this.players = playersToShow.map((player, index) => {
let troops = player.troops() / 10;
if (!player.isAlive()) {
troops = 0;
}
return {
name: player.displayName(),
position: index + 1,
score: formatPercentage(
player.numTilesOwned() / numTilesWithoutFallout,
),
gold: renderNumber(player.gold()),
troops: renderNumber(troops),
isMyPlayer: player === myPlayer,
player: player,
};
});
if (
myPlayer !== null &&
this.players.find((p) => p.isMyPlayer) === undefined
) {
let place = 0;
for (const p of sorted) {
place++;
if (p === myPlayer) {
break;
}
}
let myPlayerTroops = myPlayer.troops() / 10;
if (!myPlayer.isAlive()) {
myPlayerTroops = 0;
}
this.players.pop();
this.players.push({
name: myPlayer.displayName(),
position: place,
score: formatPercentage(
myPlayer.numTilesOwned() / this.game.numLandTiles(),
),
gold: renderNumber(myPlayer.gold()),
troops: renderNumber(myPlayerTroops),
isMyPlayer: true,
player: myPlayer,
});
}
this.requestUpdate();
}
private handleRowClickPlayer(player: PlayerView) {
if (this.eventBus === null) return;
this.eventBus.emit(new GoToPlayerEvent(player));
}
renderLayer(context: CanvasRenderingContext2D) {}
shouldTransform(): boolean {
return false;
}
render() {
if (!this.visible) {
return html``;
}
return html`
<div
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-sm md:max-h-[50vh] ${this
.visible
? ""
: "hidden"}"
@contextmenu=${(e: Event) => e.preventDefault()}
>
<button
class="mb-2 px-2 py-1 md:px-2.5 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();
}}
>
${this.showTopFive ? "Show All" : "Show Top 5"}
</button>
<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"
? "⬆️"
: "⬇️"
: ""}
</div>
<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"
? "⬆️"
: "⬇️"
: ""}
</div>
<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"
? "⬆️"
: "⬇️"
: ""}
</div>
</div>
${repeat(
this.players,
(p) => p.player.id(),
(player) => html`
<div
class="contents hover:bg-slate-600/60 ${player.isMyPlayer
? "font-bold"
: ""} cursor-pointer"
@click=${() => this.handleRowClickPlayer(player.player)}
>
<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>
`,
)}
</div>
</div>
`;
}
}
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 (Number.isNaN(perc)) return "0%";
return perc.toPrecision(2) + "%";
}