Files
OpenFrontIO/src/client/components/baseComponents/ranking/PlayerRow.ts
T
DevelopingTom d758e21351 Restyle game rank modal (#2918)
## Description:
The game rank modal was still using the old style, which clashes
strongly with the new one.

This PR changes changes the modal style to be consistent with the new
one:

### Old

<img width="894" height="451" alt="image"
src="https://github.com/user-attachments/assets/c83177cf-a1ed-4ee5-9e12-7d2a9d8004cf"
/>

### New


![redesign](https://github.com/user-attachments/assets/ecf4f0ae-88f0-433c-90be-f41447e17afe)

Tagged as `v29` to have a consistent style in the same version.

## 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:

IngloriousTom
2026-01-16 10:14:38 -08:00

268 lines
7.7 KiB
TypeScript

import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
GOLD_INDEX_TRADE,
GOLD_INDEX_TRAIN_OTHER,
GOLD_INDEX_TRAIN_SELF,
} from "src/core/StatsSchemas";
import { renderNumber } from "../../../Utils";
import { PlayerInfo, RankType } from "./GameInfoRanking";
@customElement("player-row")
export class PlayerRow extends LitElement {
@property({ type: Object }) player: PlayerInfo;
@property({ type: String }) rankType: RankType;
@property({ type: Number }) bestScore = 1;
@property({ type: Number }) rank = 1;
@property({ type: Number }) score = 0;
@property({ type: Boolean }) currentPlayer = false;
createRenderRoot() {
return this;
}
render() {
if (!this.player) return html``;
const { player } = this;
const visibleBorder = player.winner || this.currentPlayer;
return html`
<li
class="${player.winner ? "bg-black/20" : "bg-black/20"} border-b-1
${player.winner
? "border-yellow-500 border-1 box-content"
: visibleBorder
? "border-white/5"
: "border-transparent"}
relative pt-1 pb-1 pr-2 pl-2 sm:pl-5 sm:pr-5 flex justify-between items-center hover:bg-white/[0.07] transition-colors duration-150 ease-in-out"
>
<div
class="font-bold text-right w-7.5 text-lg text-white absolute -left-10"
>
${this.rank}
</div>
${this.renderPlayerInfo()}
</li>
`;
}
private renderPlayerIcon() {
return html`
${this.renderIcon()} ${this.player.winner ? this.renderCrownIcon() : ""}
`;
}
private renderCrownIcon() {
return html`
<img
src="/images/CrownIcon.svg"
class="absolute -top-0.75 left-4 size-3.75 sm:-top-1.75 sm:left-7.5 sm:size-5"
/>
`;
}
private renderPlayerInfo() {
switch (this.rankType) {
case RankType.Lifetime:
case RankType.Conquests:
return this.renderScoreAsBar();
case RankType.Atoms:
case RankType.Hydros:
case RankType.MIRV:
return this.renderBombScore();
case RankType.TotalGold:
case RankType.ConqueredGold:
case RankType.StolenGold:
return this.renderGoldScore();
case RankType.NavalTrade:
case RankType.TrainTrade:
return this.renderTradeScore();
default:
return html``;
}
}
private renderScoreAsBar() {
return html`
<div class="flex gap-3 items-center w-full">
${this.renderPlayerIcon()}
<div class="flex flex-col sm:flex-row gap-1 text-left w-full">
${this.renderPlayerName()} ${this.renderScoreBar()}
</div>
</div>
<div>
<div
class="font-bold rounded-[50%] size-7.5 leading-[1.6rem] border border-white/10 text-center bg-white/5 text-white/80"
>
${Number(this.score).toFixed(0)}
</div>
</div>
`;
}
private renderScoreBar() {
const bestScore = Math.max(this.bestScore, 1);
const width = Math.min(Math.max((this.score / bestScore) * 100, 0), 100);
return html`
<div class="w-full pr-2.5 m-auto">
<div class="h-1.75 bg-white/10 w-full">
<!-- bar background -->
<div
class="h-1.75 bg-blue-500/50 w-(--width)"
style="--width: ${width}%;"
></div>
</div>
</div>
`;
}
private renderMultiScoreType(value: number, highlight: boolean) {
return html`
<div
class="${highlight
? "font-bold text-[18px] text-white/80"
: "leading-[24px] text-white/40"} min-w-7.5 sm:min-w-15 inline-block text-center"
>
${renderNumber(value)}
</div>
`;
}
private renderAllBombs() {
return html`
<div class="flex justify-between text-sm sm:pr-20">
${this.renderMultiScoreType(
this.player.atoms,
this.rankType === RankType.Atoms,
)}
/
${this.renderMultiScoreType(
this.player.hydros,
this.rankType === RankType.Hydros,
)}
/
${this.renderMultiScoreType(
this.player.mirv,
this.rankType === RankType.MIRV,
)}
</div>
`;
}
private renderAllTrades() {
const navalTrade = this.player.gold[GOLD_INDEX_TRADE] ?? 0n;
const ownTrainTrade = this.player.gold[GOLD_INDEX_TRAIN_SELF] ?? 0n;
const otherTrainTrade = this.player.gold[GOLD_INDEX_TRAIN_OTHER] ?? 0n;
return html`
<div class="flex justify-between text-sm align-baseline">
${this.renderMultiScoreType(
Number(ownTrainTrade + otherTrainTrade),
this.rankType === RankType.TrainTrade,
)}
/
${this.renderMultiScoreType(
Number(navalTrade),
this.rankType === RankType.NavalTrade,
)}
</div>
`;
}
private renderBombScore() {
return html`
<div class="flex gap-3 items-center align-baseline w-full">
${this.renderPlayerIcon()}
<div class="flex flex-col sm:flex-row gap-1 text-left w-full">
${this.renderPlayerName()} ${this.renderAllBombs()}
</div>
</div>
`;
}
private renderGoldScore() {
return html`
<div class="flex gap-3 items-center">
${this.renderPlayerIcon()}
<div class="text-left w-31.25 sm:w-50">${this.renderPlayerName()}</div>
</div>
<div class="flex gap-2">
<div
class="font-bold rounded-md w-15 text-white/80 text-sm sm:w-25 leading-[1.9rem] text-center"
>
${renderNumber(this.score)}
</div>
<img src="/images/GoldCoinIcon.svg" class="size-3.5 sm:size-5 m-auto" />
</div>
`;
}
private renderTradeScore() {
return html`
<div class="flex flex-col sm:flex-row gap-1 text-left w-full">
<div class="flex gap-3 items-center">
${this.renderPlayerIcon()}
<div class="text-left w-31.25 sm:w-50">
${this.renderPlayerName()}
</div>
</div>
<div class="flex gap-2 justify-between items-center w-full">
<div class="rounded-md text-sm leading-[1.9rem] text-center w-full">
${this.renderAllTrades()}
</div>
<img src="/images/GoldCoinIcon.svg" class="w-5 size-3.5 sm:size-5" />
</div>
</div>
`;
}
private renderPlayerName() {
return html`
<div class="flex gap-1 items-center w-50 shrink-0">
${this.player.tag ? this.renderTag(this.player.tag) : ""}
<div
class="text-xs sm:text-sm font-bold tracking-wide text-white/80 text-ellipsis w-37.5 shrink-0 overflow-hidden whitespace-nowrap"
>
${this.player.username}
</div>
</div>
`;
}
private renderTag(tag: string) {
return html`
<div
class="px-2.5 py-1 rounded bg-blue-500/10 border border-blue-500/20 text-blue-300 font-bold text-xs tracking-wide group-hover:bg-blue-500/20 transition-colors"
>
${tag}
</div>
`;
}
private renderIcon() {
if (this.player.killedAt) {
return html` <div
class="size-7.5 leading-1.25 shrink-0 text-lg sm:size-10 pt-3 sm:leading-3.75 sm:rounded-[50%] sm:border sm:border-gray-200 text-center sm:bg-slate-500 sm:text-2xl"
>
💀
</div>`;
} else if (this.player.flag) {
return html`<img
src="/flags/${this.player.flag}.svg"
class="min-w-7.5 h-7.5 sm:min-w-10 sm:h-10 shrink-0"
/>`;
}
return html`
<div
class="size-7.5 leading-1.25 shrink-0 rounded-[50%] sm:size-10 sm:pt-2.5 sm:leading-3.5 border border-gray-200 text-center bg-slate-500"
>
<img
src="/images/ProfileIcon.svg"
class="size-5 mt-0.5 sm:size-6.25 sm:-mt-1.25 m-auto"
/>
</div>
`;
}
}