From 110a255265e3d151be7254d5c5924af209b4c861 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Fri, 26 Sep 2025 04:49:22 +0900 Subject: [PATCH] Add player-stats-table (#2088) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: スクリーンショット 2025-09-23 10 16 59 スクリーンショット 2025-09-23 10 17 13 スクリーンショット 2025-09-23 10 17 41 ## 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 --- resources/lang/en.json | 42 ++++ .../baseComponents/stats/PlayerStatsTable.ts | 196 ++++++++++++++++++ src/core/StatsSchemas.ts | 30 ++- 3 files changed, 252 insertions(+), 16 deletions(-) create mode 100644 src/client/components/baseComponents/stats/PlayerStatsTable.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index ea078c4fe..fe4ff0fa9 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -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", diff --git a/src/client/components/baseComponents/stats/PlayerStatsTable.ts b/src/client/components/baseComponents/stats/PlayerStatsTable.ts new file mode 100644 index 000000000..027a5a594 --- /dev/null +++ b/src/client/components/baseComponents/stats/PlayerStatsTable.ts @@ -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` +
+
+ ${translateText("player_stats_table.building_stats")} +
+ + + + + + + + + + + + ${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` + + + + + + + + `; + })} + +
+ ${translateText("player_stats_table.building")} + ${translateText("player_stats_table.built")}${translateText("player_stats_table.destroyed")}${translateText("player_stats_table.captured")}${translateText("player_stats_table.lost")}
${translateText(`player_stats_table.unit.${key}`)}${renderNumber(built)}${renderNumber(destroyed)}${renderNumber(captured)}${renderNumber(lost)}
+
+
+
+ ${translateText("player_stats_table.ship_arrivals")} +
+ + + + + + + + + + + ${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` + + + + + + + `; + })} + +
+ ${translateText("player_stats_table.ship_type")} + ${translateText("player_stats_table.sent")}${translateText("player_stats_table.destroyed")}${translateText("player_stats_table.arrived")}
${translateText(`player_stats_table.unit.${key}`)}${renderNumber(sent)}${renderNumber(destroyed)}${renderNumber(arrived)}
+
+
+
+ ${translateText("player_stats_table.nuke_stats")} +
+ + + + + + + + + + + ${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` + + + + + + + `; + })} + +
+ ${translateText("player_stats_table.weapon")} + + ${translateText("player_stats_table.launched")} + + ${translateText("player_stats_table.landed")} + + ${translateText("player_stats_table.hits")} +
${translateText(`player_stats_table.unit.${bomb}`)}${renderNumber(launched)}${renderNumber(landed)}${renderNumber(intercepted)}
+
+
+
+ ${translateText("player_stats_table.player_metrics")} +
+ + + + + + + + + + + + + + + + + +
${translateText("player_stats_table.attack")}${translateText("player_stats_table.sent")}${translateText("player_stats_table.received")}${translateText("player_stats_table.cancelled")}
${translateText("player_stats_table.count")}${renderNumber(this.stats?.attacks?.[0] ?? 0n)}${renderNumber(this.stats?.attacks?.[1] ?? 0n)}${renderNumber(this.stats?.attacks?.[2] ?? 0n)}
+ + + + + + + + + + + + + + + + + + + +
${translateText("player_stats_table.gold")}${translateText("player_stats_table.workers")}${translateText("player_stats_table.war")}${translateText("player_stats_table.trade")}${translateText("player_stats_table.steal")}
${translateText("player_stats_table.count")}${renderNumber(this.stats?.gold?.[0] ?? 0n)}${renderNumber(this.stats?.gold?.[1] ?? 0n)}${renderNumber(this.stats?.gold?.[2] ?? 0n)}${renderNumber(this.stats?.gold?.[3] ?? 0n)}
+
+ `; + } +} diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts index 1a4a949ec..043b5dfd2 100644 --- a/src/core/StatsSchemas.ts +++ b/src/core/StatsSchemas.ts @@ -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; export type NukeType = | UnitType.AtomBomb @@ -21,7 +17,8 @@ export const unitTypeToBombUnit = { [UnitType.MIRVWarhead]: "mirvw", } as const satisfies Record; -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; 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; -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; export type OtherUnitType = | UnitType.City