mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:10:46 +00:00
Add player-stats-table (#2088)
## Description: This PR adds the player-stats-table component, which displays a player’s statistics in a structured table format. It focuses on presenting core stats such as building stats, Player Metrics and more. The component will later be integrated into the player modal so that users can view detailed stats alongside other profile information. It should look like this: <img width="597" height="386" alt="スクリーンショット 2025-09-23 10 16 59" src="https://github.com/user-attachments/assets/9ae07a25-982f-45c4-9bf5-21c8b90d4fae" /> <img width="551" height="644" alt="スクリーンショット 2025-09-23 10 17 13" src="https://github.com/user-attachments/assets/1bdbca32-be2f-4c3a-b172-d11914b9a3cf" /> <img width="525" height="219" alt="スクリーンショット 2025-09-23 10 17 41" src="https://github.com/user-attachments/assets/7da45a98-8602-4b25-b455-c51319752de9" /> ## 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: aotumuri
This commit is contained in:
@@ -646,6 +646,48 @@
|
||||
"delete_unit_title": "Delete Unit",
|
||||
"delete_unit_description": "Click to delete the nearest unit"
|
||||
},
|
||||
"player_stats_table": {
|
||||
"building_stats": "Building Statistics",
|
||||
"ship_arrivals": "Ship Arrivals",
|
||||
"nuke_stats": "Nuke Statistics",
|
||||
"player_metrics": "Player Metrics",
|
||||
"building": "Building",
|
||||
"ship_type": "Ship Type",
|
||||
"weapon": "Weapon",
|
||||
"built": "Built",
|
||||
"destroyed": "Destroyed",
|
||||
"captured": "Captured",
|
||||
"lost": "Lost",
|
||||
"hits": "Hits",
|
||||
"launched": "Launched",
|
||||
"landed": "Landed",
|
||||
"sent": "Sent",
|
||||
"arrived": "Arrived",
|
||||
"attack": "Attack",
|
||||
"received": "Received",
|
||||
"cancelled": "Cancelled",
|
||||
"count": "Count",
|
||||
"gold": "Gold",
|
||||
"workers": "Workers",
|
||||
"war": "War",
|
||||
"trade": "Trade",
|
||||
"steal": "Steal",
|
||||
"unit": {
|
||||
"city": "City",
|
||||
"port": "Port",
|
||||
"defp": "Defense Post",
|
||||
"saml": "SAM Launcher",
|
||||
"silo": "Missile Silo",
|
||||
"wshp": "Warship",
|
||||
"fact": "Factory",
|
||||
"trade": "Trade Ship",
|
||||
"trans": "Transport Ship",
|
||||
"abomb": "Atom Bomb",
|
||||
"hbomb": "Hydrogen Bomb",
|
||||
"mirv": "MIRV",
|
||||
"mirvw": "MIRV Warhead"
|
||||
}
|
||||
},
|
||||
"game_list": {
|
||||
"recent_games": "Recent Games",
|
||||
"game_id": "Game ID",
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import {
|
||||
PlayerStats,
|
||||
boatUnits,
|
||||
bombUnits,
|
||||
otherUnits,
|
||||
} from "../../../../core/StatsSchemas";
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
@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>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.ship_arrivals")}
|
||||
</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>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.nuke_stats")}
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" style="width:40%">
|
||||
${translateText("player_stats_table.weapon")}
|
||||
</th>
|
||||
<th class="text-center" style="width:20%">
|
||||
${translateText("player_stats_table.launched")}
|
||||
</th>
|
||||
<th class="text-center" style="width:20%">
|
||||
${translateText("player_stats_table.landed")}
|
||||
</th>
|
||||
<th class="text-center" style="width:20%">
|
||||
${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>
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="section-title">
|
||||
${translateText("player_stats_table.player_metrics")}
|
||||
</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 style="margin-top: 0.75rem;">
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
+14
-16
@@ -1,12 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { UnitType } from "./game/Game";
|
||||
|
||||
export const BombUnitSchema = z.union([
|
||||
z.literal("abomb"),
|
||||
z.literal("hbomb"),
|
||||
z.literal("mirv"),
|
||||
z.literal("mirvw"),
|
||||
]);
|
||||
export const bombUnits = ["abomb", "hbomb", "mirv", "mirvw"] as const;
|
||||
export const BombUnitSchema = z.enum(bombUnits);
|
||||
export type BombUnit = z.infer<typeof BombUnitSchema>;
|
||||
export type NukeType =
|
||||
| UnitType.AtomBomb
|
||||
@@ -21,7 +17,8 @@ export const unitTypeToBombUnit = {
|
||||
[UnitType.MIRVWarhead]: "mirvw",
|
||||
} as const satisfies Record<NukeType, BombUnit>;
|
||||
|
||||
export const BoatUnitSchema = z.union([z.literal("trade"), z.literal("trans")]);
|
||||
export const boatUnits = ["trade", "trans"] as const;
|
||||
export const BoatUnitSchema = z.enum(boatUnits);
|
||||
export type BoatUnit = z.infer<typeof BoatUnitSchema>;
|
||||
export type BoatUnitType = UnitType.TradeShip | UnitType.TransportShip;
|
||||
|
||||
@@ -30,15 +27,16 @@ export type BoatUnitType = UnitType.TradeShip | UnitType.TransportShip;
|
||||
// [UnitType.TransportShip]: "trans",
|
||||
// } as const satisfies Record<BoatUnitType, BoatUnit>;
|
||||
|
||||
export const OtherUnitSchema = z.union([
|
||||
z.literal("city"),
|
||||
z.literal("defp"),
|
||||
z.literal("port"),
|
||||
z.literal("wshp"),
|
||||
z.literal("silo"),
|
||||
z.literal("saml"),
|
||||
z.literal("fact"),
|
||||
]);
|
||||
export const otherUnits = [
|
||||
"city",
|
||||
"defp",
|
||||
"port",
|
||||
"wshp",
|
||||
"silo",
|
||||
"saml",
|
||||
"fact",
|
||||
] as const;
|
||||
export const OtherUnitSchema = z.enum(otherUnits);
|
||||
export type OtherUnit = z.infer<typeof OtherUnitSchema>;
|
||||
export type OtherUnitType =
|
||||
| UnitType.City
|
||||
|
||||
Reference in New Issue
Block a user