mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 16:16:09 +00:00
Main Menu UI Overhaul (#2829)
## Description: Overhauls the Main Menu UI, visit https://menu.openfront.dev to see everything. ## 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: w.o.n
This commit is contained in:
@@ -1,32 +1,13 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import type { DiscordUser } from "../../../../core/ApiSchemas";
|
||||
import { translateText } from "../../../Utils";
|
||||
|
||||
@customElement("discord-user-header")
|
||||
export class DiscordUserHeader extends LitElement {
|
||||
static styles = css`
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.avatarFrame {
|
||||
padding: 3px;
|
||||
border-radius: 9999px;
|
||||
background: #6b7280; /* bg-gray-500 */
|
||||
}
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 9999px;
|
||||
display: block;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@state() private _data: DiscordUser | null = null;
|
||||
|
||||
@@ -59,19 +40,19 @@ export class DiscordUserHeader extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
${this.avatarUrl
|
||||
? html`
|
||||
<div class="avatarFrame">
|
||||
<div class="p-[3px] rounded-full bg-gray-500">
|
||||
<img
|
||||
class="avatar"
|
||||
class="w-12 h-12 rounded-full block"
|
||||
src="${this.avatarUrl}"
|
||||
alt="${translateText("discord_user_header.avatar_alt")}"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<span class="name">${this.discordDisplayName}</span>
|
||||
<span class="font-semibold text-white">${this.discordDisplayName}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { PlayerGame } from "../../../../core/ApiSchemas";
|
||||
import { GameMode } from "../../../../core/game/Game";
|
||||
@@ -7,52 +7,9 @@ import { translateText } from "../../../Utils";
|
||||
|
||||
@customElement("game-list")
|
||||
export class GameList extends LitElement {
|
||||
static styles = css`
|
||||
.section-title {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
.subtle {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.btn {
|
||||
font-size: 0.875rem;
|
||||
color: #d1d5db;
|
||||
background: #374151;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.btn.secondary {
|
||||
background: #4b5563;
|
||||
}
|
||||
.details {
|
||||
padding: 0 1rem 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #d1d5db;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
`;
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: Array }) games: PlayerGame[] = [];
|
||||
@property({ attribute: false }) onViewGame?: (id: string) => void;
|
||||
@@ -77,91 +34,115 @@ export class GameList extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div class="mt-4 w-full max-w-md">
|
||||
<div class="text-sm text-gray-400 font-semibold mb-1">
|
||||
<div class="section-title">
|
||||
🎮 ${translateText("game_list.recent_games")}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
${this.games.map(
|
||||
(game) => html`
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
return html` <div class="w-full">
|
||||
<div class="flex flex-col gap-3">
|
||||
${this.games.map(
|
||||
(game) => html`
|
||||
<div
|
||||
class="bg-white/5 border border-white/5 rounded-xl overflow-hidden hover:bg-white/10 transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between px-4 py-3 gap-3"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-2 bg-blue-500/20 rounded-lg text-blue-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polygon points="10 8 16 12 10 16 10 8"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">
|
||||
${translateText("game_list.game_id")}: ${game.gameId}
|
||||
<div class="text-sm font-bold text-white tracking-wide">
|
||||
${new Date(game.start).toLocaleDateString()}
|
||||
</div>
|
||||
<div class="subtle">
|
||||
<div
|
||||
class="text-xs text-blue-200/60 font-semibold uppercase tracking-wider"
|
||||
>
|
||||
${translateText("game_list.mode")}:
|
||||
${game.mode === GameMode.FFA
|
||||
? translateText("game_list.mode_ffa")
|
||||
: html`${translateText("game_list.mode_team")}`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => this.onViewGame?.(game.gameId)}
|
||||
>
|
||||
${translateText("game_list.view")}
|
||||
</button>
|
||||
<button
|
||||
class="btn secondary"
|
||||
@click=${() => this.toggle(game.gameId)}
|
||||
>
|
||||
${translateText("game_list.details")}
|
||||
</button>
|
||||
<button
|
||||
class="btn secondary"
|
||||
@click=${() => this.showRanking(game.gameId)}
|
||||
>
|
||||
${translateText("game_list.ranking")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="details max-h-(--max-height) ${this.expandedGameId ===
|
||||
game.gameId
|
||||
? "max-h-50"
|
||||
: "py-0"}"
|
||||
>
|
||||
|
||||
<div class="flex gap-2 self-end sm:self-auto">
|
||||
<button
|
||||
class="text-xs font-bold text-white bg-blue-600 hover:bg-blue-500 px-3 py-1.5 rounded-lg transition-colors shadow-lg shadow-blue-900/20"
|
||||
@click=${() => this.onViewGame?.(game.gameId)}
|
||||
>
|
||||
${translateText("game_list.replay")}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs font-bold text-gray-300 bg-white/10 hover:bg-white/20 px-3 py-1.5 rounded-lg transition-colors border border-white/5"
|
||||
@click=${() => this.toggle(game.gameId)}
|
||||
>
|
||||
${translateText("game_list.details")}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs font-bold text-gray-300 bg-white/10 hover:bg-white/20 px-3 py-1.5 rounded-lg transition-colors border border-white/5"
|
||||
@click=${() => this.showRanking(game.gameId)}
|
||||
>
|
||||
${translateText("game_list.ranking")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-black/20 border-t border-white/5 px-4 text-xs text-gray-400 transition-all duration-300 overflow-hidden"
|
||||
style="max-height:${this.expandedGameId === game.gameId
|
||||
? "200px"
|
||||
: "0"}; opacity:${this.expandedGameId === game.gameId
|
||||
? "1"
|
||||
: "0"}"
|
||||
>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 py-3">
|
||||
<div>
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.started")}:</span
|
||||
<div
|
||||
class="font-bold text-white uppercase tracking-wider text-[10px] mb-1"
|
||||
>
|
||||
${new Date(game.start).toLocaleString()}
|
||||
${translateText("game_list.game_id")}
|
||||
</div>
|
||||
<div class="text-white font-mono">${game.gameId}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.mode")}:</span
|
||||
<div
|
||||
class="font-bold text-white uppercase tracking-wider text-[10px] mb-1"
|
||||
>
|
||||
${game.mode === GameMode.FFA
|
||||
? translateText("game_list.mode_ffa")
|
||||
: translateText("game_list.mode_team")}
|
||||
${translateText("game_list.map")}
|
||||
</div>
|
||||
<div class="text-white">${game.map}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.map")}:</span
|
||||
<div
|
||||
class="font-bold text-white uppercase tracking-wider text-[10px] mb-1"
|
||||
>
|
||||
${game.map}
|
||||
${translateText("game_list.difficulty")}
|
||||
</div>
|
||||
<div class="text-white">${game.difficulty}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.difficulty")}:</span
|
||||
<div
|
||||
class="font-bold text-white uppercase tracking-wider text-[10px] mb-1"
|
||||
>
|
||||
${game.difficulty}
|
||||
</div>
|
||||
<div>
|
||||
<span class="title text-xs"
|
||||
>${translateText("game_list.type")}:</span
|
||||
>
|
||||
${game.type}
|
||||
${translateText("game_list.type")}
|
||||
</div>
|
||||
<div class="text-white">${game.type}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,11 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("player-stats-grid")
|
||||
export class PlayerStatsGrid extends LitElement {
|
||||
static styles = css`
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
.stat {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.stat-title {
|
||||
color: #bbb;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: Array }) titles: string[] = [];
|
||||
@property({ type: Array }) values: Array<string | number> = [];
|
||||
@@ -37,14 +15,22 @@ export class PlayerStatsGrid extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="grid mb-2">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-2">
|
||||
${Array(this.VISIBLE_STATS_COUNT)
|
||||
.fill(0)
|
||||
.map(
|
||||
(_, i) => html`
|
||||
<div class="stat">
|
||||
<div class="stat-value">${this.values[i] ?? ""}</div>
|
||||
<div class="stat-title">${this.titles[i] ?? ""}</div>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-4 rounded-xl bg-white/5 border border-white/5 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div class="text-2xl font-bold text-white mb-1">
|
||||
${this.values[i] ?? ""}
|
||||
</div>
|
||||
<div
|
||||
class="text-blue-200/60 text-xs font-bold uppercase tracking-widest"
|
||||
>
|
||||
${this.titles[i] ?? ""}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import {
|
||||
PlayerStats,
|
||||
@@ -10,186 +10,271 @@ import { renderNumber, translateText } from "../../../Utils";
|
||||
|
||||
@customElement("player-stats-table")
|
||||
export class PlayerStatsTable extends LitElement {
|
||||
static styles = css`
|
||||
.table-container {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
font-size: 0.95rem;
|
||||
color: #ccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 0.25rem 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
th {
|
||||
color: #bbb;
|
||||
font-weight: 600;
|
||||
}
|
||||
.section-title {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
`;
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: Object }) stats: PlayerStats;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.building_stats")}
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">
|
||||
${translateText("player_stats_table.building")}
|
||||
</th>
|
||||
<th>${translateText("player_stats_table.built")}</th>
|
||||
<th>${translateText("player_stats_table.destroyed")}</th>
|
||||
<th>${translateText("player_stats_table.captured")}</th>
|
||||
<th>${translateText("player_stats_table.lost")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${otherUnits.map((key) => {
|
||||
const built = this.stats?.units?.[key]?.[0] ?? 0n;
|
||||
const destroyed = this.stats?.units?.[key]?.[1] ?? 0n;
|
||||
const captured = this.stats?.units?.[key]?.[2] ?? 0n;
|
||||
const lost = this.stats?.units?.[key]?.[3] ?? 0n;
|
||||
return html`
|
||||
<tr>
|
||||
<td>${translateText(`player_stats_table.unit.${key}`)}</td>
|
||||
<td>${renderNumber(built)}</td>
|
||||
<td>${renderNumber(destroyed)}</td>
|
||||
<td>${renderNumber(captured)}</td>
|
||||
<td>${renderNumber(lost)}</td>
|
||||
<div class="grid grid-cols-1 gap-6 w-full">
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="text-gray-400 text-sm font-bold uppercase tracking-wider mb-2"
|
||||
>
|
||||
${translateText("player_stats_table.building_stats")}
|
||||
</div>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg border border-white/5 bg-black/20"
|
||||
>
|
||||
<table class="w-full text-sm text-gray-300">
|
||||
<thead>
|
||||
<tr class="bg-white/5">
|
||||
<th class="px-4 py-2 font-semibold text-left text-gray-400">
|
||||
${translateText("player_stats_table.building")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.built")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.destroyed")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.captured")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.lost")}
|
||||
</th>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.ship_arrivals")}
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
${otherUnits.map((key) => {
|
||||
const built = this.stats?.units?.[key]?.[0] ?? 0n;
|
||||
const destroyed = this.stats?.units?.[key]?.[1] ?? 0n;
|
||||
const captured = this.stats?.units?.[key]?.[2] ?? 0n;
|
||||
const lost = this.stats?.units?.[key]?.[3] ?? 0n;
|
||||
return html`
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-left font-medium text-white/80">
|
||||
${translateText(`player_stats_table.unit.${key}`)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(built)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(destroyed)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(captured)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(lost)}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">
|
||||
${translateText("player_stats_table.ship_type")}
|
||||
</th>
|
||||
<th>${translateText("player_stats_table.sent")}</th>
|
||||
<th>${translateText("player_stats_table.destroyed")}</th>
|
||||
<th>${translateText("player_stats_table.arrived")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${boatUnits.map((key) => {
|
||||
const sent = this.stats?.boats?.[key]?.[0] ?? 0n;
|
||||
const arrived = this.stats?.boats?.[key]?.[1] ?? 0n;
|
||||
const destroyed = this.stats?.boats?.[key]?.[3] ?? 0n;
|
||||
return html`
|
||||
<tr>
|
||||
<td>${translateText(`player_stats_table.unit.${key}`)}</td>
|
||||
<td>${renderNumber(sent)}</td>
|
||||
<td>${renderNumber(destroyed)}</td>
|
||||
<td>${renderNumber(arrived)}</td>
|
||||
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="text-gray-400 text-sm font-bold uppercase tracking-wider mb-2"
|
||||
>
|
||||
${translateText("player_stats_table.ship_arrivals")}
|
||||
</div>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg border border-white/5 bg-black/20"
|
||||
>
|
||||
<table class="w-full text-sm text-gray-300">
|
||||
<thead>
|
||||
<tr class="bg-white/5">
|
||||
<th class="px-4 py-2 font-semibold text-left text-gray-400">
|
||||
${translateText("player_stats_table.ship_type")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.sent")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.destroyed")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.arrived")}
|
||||
</th>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.nuke_stats")}
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
${boatUnits.map((key) => {
|
||||
const sent = this.stats?.boats?.[key]?.[0] ?? 0n;
|
||||
const arrived = this.stats?.boats?.[key]?.[1] ?? 0n;
|
||||
const destroyed = this.stats?.boats?.[key]?.[3] ?? 0n;
|
||||
return html`
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-left font-medium text-white/80">
|
||||
${translateText(`player_stats_table.unit.${key}`)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(sent)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(destroyed)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(arrived)}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left w-2/5">
|
||||
${translateText("player_stats_table.weapon")}
|
||||
</th>
|
||||
<th class="text-center w-1/5">
|
||||
${translateText("player_stats_table.launched")}
|
||||
</th>
|
||||
<th class="text-center w-1/5">
|
||||
${translateText("player_stats_table.landed")}
|
||||
</th>
|
||||
<th class="text-center w-1/5">
|
||||
${translateText("player_stats_table.hits")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${bombUnits.map((bomb) => {
|
||||
const launched = this.stats?.bombs?.[bomb]?.[0] ?? 0n;
|
||||
const landed = this.stats?.bombs?.[bomb]?.[1] ?? 0n;
|
||||
const intercepted = this.stats?.bombs?.[bomb]?.[2] ?? 0n;
|
||||
return html`
|
||||
<tr>
|
||||
<td>${translateText(`player_stats_table.unit.${bomb}`)}</td>
|
||||
<td class="text-center">${renderNumber(launched)}</td>
|
||||
<td class="text-center">${renderNumber(landed)}</td>
|
||||
<td class="text-center">${renderNumber(intercepted)}</td>
|
||||
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="text-gray-400 text-sm font-bold uppercase tracking-wider mb-2"
|
||||
>
|
||||
${translateText("player_stats_table.nuke_stats")}
|
||||
</div>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg border border-white/5 bg-black/20"
|
||||
>
|
||||
<table class="w-full text-sm text-gray-300">
|
||||
<thead>
|
||||
<tr class="bg-white/5">
|
||||
<th class="px-4 py-2 font-semibold text-left text-gray-400">
|
||||
${translateText("player_stats_table.weapon")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.launched")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.landed")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.hits")}
|
||||
</th>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.player_metrics")}
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
${bombUnits.map((bomb) => {
|
||||
const launched = this.stats?.bombs?.[bomb]?.[0] ?? 0n;
|
||||
const landed = this.stats?.bombs?.[bomb]?.[1] ?? 0n;
|
||||
const intercepted = this.stats?.bombs?.[bomb]?.[2] ?? 0n;
|
||||
return html`
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-left font-medium text-white/80">
|
||||
${translateText(`player_stats_table.unit.${bomb}`)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(launched)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(landed)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(intercepted)}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="text-gray-400 text-sm font-bold uppercase tracking-wider mb-2"
|
||||
>
|
||||
${translateText("player_stats_table.player_metrics")}
|
||||
</div>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg border border-white/5 bg-black/20 mb-4"
|
||||
>
|
||||
<table class="w-full text-sm text-gray-300">
|
||||
<thead>
|
||||
<tr class="bg-white/5">
|
||||
<th class="px-4 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.attack")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.sent")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.received")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.cancelled")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-center text-white/60">
|
||||
${translateText("player_stats_table.count")}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.attacks?.[0] ?? 0n)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.attacks?.[1] ?? 0n)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.attacks?.[2] ?? 0n)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg border border-white/5 bg-black/20"
|
||||
>
|
||||
<table class="w-full text-sm text-gray-300">
|
||||
<thead>
|
||||
<tr class="bg-white/5">
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.gold")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.workers")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.war")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.trade")}
|
||||
</th>
|
||||
<th class="px-3 py-2 text-center font-semibold text-gray-400">
|
||||
${translateText("player_stats_table.steal")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.gold?.[0] ?? 0n)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.gold?.[1] ?? 0n)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.gold?.[2] ?? 0n)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.gold?.[3] ?? 0n)}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-white/60">
|
||||
${renderNumber(this.stats?.gold?.[4] ?? 0n)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${translateText("player_stats_table.attack")}</th>
|
||||
<th>${translateText("player_stats_table.sent")}</th>
|
||||
<th>${translateText("player_stats_table.received")}</th>
|
||||
<th>${translateText("player_stats_table.cancelled")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>${translateText("player_stats_table.count")}</td>
|
||||
<td>${renderNumber(this.stats?.attacks?.[0] ?? 0n)}</td>
|
||||
<td>${renderNumber(this.stats?.attacks?.[1] ?? 0n)}</td>
|
||||
<td>${renderNumber(this.stats?.attacks?.[2] ?? 0n)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${translateText("player_stats_table.gold")}</th>
|
||||
<th>${translateText("player_stats_table.workers")}</th>
|
||||
<th>${translateText("player_stats_table.war")}</th>
|
||||
<th>${translateText("player_stats_table.trade")}</th>
|
||||
<th>${translateText("player_stats_table.steal")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>${translateText("player_stats_table.count")}</td>
|
||||
<td>${renderNumber(this.stats?.gold?.[0] ?? 0n)}</td>
|
||||
<td>${renderNumber(this.stats?.gold?.[1] ?? 0n)}</td>
|
||||
<td>${renderNumber(this.stats?.gold?.[2] ?? 0n)}</td>
|
||||
<td>${renderNumber(this.stats?.gold?.[3] ?? 0n)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,86 +117,111 @@ export class PlayerStatsTreeView extends LitElement {
|
||||
: 0;
|
||||
|
||||
return html`
|
||||
<!-- Type selector -->
|
||||
<div class="flex gap-2 mt-2 justify-center">
|
||||
${types.map(
|
||||
(t) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-0.5 rounded-sm border ${this
|
||||
.selectedType === t
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setGameType(t)}
|
||||
>
|
||||
${t === GameType.Public
|
||||
? translateText("player_stats_tree.public")
|
||||
: t === GameType.Private
|
||||
? translateText("player_stats_tree.private")
|
||||
: translateText("player_stats_tree.singleplayer")}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<!-- Mode selector -->
|
||||
${modes.length
|
||||
? html`<div class="flex gap-2 mt-2 justify-center">
|
||||
${modes.map(
|
||||
(m) => html`
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Filters -->
|
||||
<div
|
||||
class="flex flex-wrap gap-2 items-center justify-between p-2 bg-black/20 rounded-lg border border-white/5"
|
||||
>
|
||||
<!-- Type selector -->
|
||||
<div class="flex gap-1">
|
||||
${types.map(
|
||||
(t) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-0.5 rounded-sm border ${this
|
||||
.selectedMode === m
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setMode(m)}
|
||||
title=${translateText("player_stats_tree.mode")}
|
||||
class="text-xs px-3 py-1.5 rounded-md border font-bold uppercase tracking-wider transition-all duration-200 ${this
|
||||
.selectedType === t
|
||||
? "bg-blue-600 border-blue-500 text-white shadow-lg shadow-blue-900/40"
|
||||
: "bg-white/5 border-white/10 text-gray-400 hover:bg-white/10 hover:text-white"}"
|
||||
@click=${() => this.setGameType(t)}
|
||||
>
|
||||
${this.labelForMode(m)}
|
||||
${t === GameType.Public
|
||||
? translateText("player_stats_tree.public")
|
||||
: t === GameType.Private
|
||||
? translateText("player_stats_tree.private")
|
||||
: translateText("player_stats_tree.solo")}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
<!-- Difficulty selector -->
|
||||
${diffs.length
|
||||
? html`<div class="flex gap-2 mt-2 justify-center">
|
||||
${diffs.map(
|
||||
(d) =>
|
||||
html` <button
|
||||
class="text-xs px-2 py-0.5 rounded-sm border ${this
|
||||
.selectedDifficulty === d
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setDifficulty(d)}
|
||||
title=${translateText("difficulty.difficulty")}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<!-- Mode selector -->
|
||||
${modes.length
|
||||
? html`<div
|
||||
class="flex gap-1 bg-black/20 rounded-md p-1 border border-white/5"
|
||||
>
|
||||
${translateText(`difficulty.${d}`)}
|
||||
</button>`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
${leaf
|
||||
? html`
|
||||
<hr class="w-2/3 border-gray-600 my-2" />
|
||||
<player-stats-grid
|
||||
.titles=${[
|
||||
translateText("player_stats_tree.stats_wins"),
|
||||
translateText("player_stats_tree.stats_losses"),
|
||||
translateText("player_stats_tree.stats_wlr"),
|
||||
translateText("player_stats_tree.stats_games_played"),
|
||||
]}
|
||||
.values=${[
|
||||
renderNumber(leaf.wins),
|
||||
renderNumber(leaf.losses),
|
||||
wlr.toFixed(2),
|
||||
renderNumber(leaf.total),
|
||||
]}
|
||||
></player-stats-grid>
|
||||
<hr class="w-2/3 border-gray-600 my-2" />
|
||||
<player-stats-table
|
||||
.stats=${this.getDisplayedStats()}
|
||||
></player-stats-table>
|
||||
`
|
||||
: html``}
|
||||
${modes.map(
|
||||
(m) => html`
|
||||
<button
|
||||
class="text-xs px-3 py-1 rounded-sm transition-colors ${this
|
||||
.selectedMode === m
|
||||
? "bg-white/20 text-white font-bold"
|
||||
: "text-gray-400 hover:text-white"}"
|
||||
@click=${() => this.setMode(m)}
|
||||
title=${translateText("player_stats_tree.mode")}
|
||||
>
|
||||
${this.labelForMode(m)}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
|
||||
<!-- Difficulty selector -->
|
||||
${diffs.length
|
||||
? html`<div
|
||||
class="flex gap-1 bg-black/20 rounded-md p-1 border border-white/5"
|
||||
>
|
||||
${diffs.map(
|
||||
(d) =>
|
||||
html` <button
|
||||
class="text-xs px-3 py-1 rounded-sm transition-colors ${this
|
||||
.selectedDifficulty === d
|
||||
? "bg-white/20 text-white font-bold"
|
||||
: "text-gray-400 hover:text-white"}"
|
||||
@click=${() => this.setDifficulty(d)}
|
||||
title=${translateText("difficulty.difficulty")}
|
||||
>
|
||||
${translateText(`difficulty.${d.toLowerCase()}`)}
|
||||
</button>`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${leaf
|
||||
? html`
|
||||
<div class="space-y-6 mt-2">
|
||||
<player-stats-grid
|
||||
.titles=${[
|
||||
translateText("player_stats_tree.stats_wins"),
|
||||
translateText("player_stats_tree.stats_losses"),
|
||||
translateText("player_stats_tree.stats_wlr"),
|
||||
translateText("player_stats_tree.stats_games_played"),
|
||||
]}
|
||||
.values=${[
|
||||
renderNumber(leaf.wins),
|
||||
renderNumber(leaf.losses),
|
||||
wlr.toFixed(2),
|
||||
renderNumber(leaf.total),
|
||||
]}
|
||||
></player-stats-grid>
|
||||
|
||||
<div class="border-t border-white/10 pt-6">
|
||||
<player-stats-table
|
||||
.stats=${this.getDisplayedStats()}
|
||||
></player-stats-table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div
|
||||
class="py-12 text-center text-white/30 italic border border-white/5 rounded-xl bg-white/5"
|
||||
>
|
||||
${translateText("player_stats_tree.no_stats")}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user