Add team stats component (#678)

## Description:

Closes #677 

- [x] I have added screenshots for all UI updates
- [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


![TeamStats2](https://github.com/user-attachments/assets/f2bd3f06-8484-476a-8205-97d6e6fc2f1d)

![TeamStats1](https://github.com/user-attachments/assets/4d1f71ef-b6ad-4cee-af82-18bd73e9754a)


Killersoren
This commit is contained in:
Killersoren
2025-05-15 19:31:36 +02:00
committed by GitHub
parent 1b286f5518
commit 2efc7d00fb
3 changed files with 248 additions and 0 deletions
+10
View File
@@ -22,6 +22,7 @@ import { PlayerPanel } from "./layers/PlayerPanel";
import { RadialMenu } from "./layers/RadialMenu";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { TopBar } from "./layers/TopBar";
@@ -70,6 +71,14 @@ export function createRenderer(
leaderboard.eventBus = eventBus;
leaderboard.game = game;
const teamStats = document.querySelector("team-stats") as TeamStats;
if (!emojiTable || !(teamStats instanceof TeamStats)) {
consolex.error("EmojiTable element not found in the DOM");
}
teamStats.clientID = clientID;
teamStats.eventBus = eventBus;
teamStats.game = game;
const controlPanel = document.querySelector("control-panel") as ControlPanel;
if (!(controlPanel instanceof ControlPanel)) {
consolex.error("ControlPanel element not found in the DOM");
@@ -178,6 +187,7 @@ export function createRenderer(
playerInfo,
winModel,
optionsMenu,
teamStats,
topBar,
playerPanel,
multiTabModal,
+237
View File
@@ -0,0 +1,237 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameMode } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
interface TeamEntry {
teamName: string;
totalScoreStr: string;
totalGold: string;
totalTroops: string;
players: PlayerView[];
}
@customElement("team-stats")
export class TeamStats extends LitElement implements Layer {
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
teams: TeamEntry[] = [];
@state()
private _teamStatsHidden = true;
private _shownOnInit = false;
init() {}
tick() {
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
return;
}
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
this._shownOnInit = true;
this._teamStatsHidden = false;
this.updateTeamStats();
}
if (this._teamStatsHidden) return;
if (this.game.ticks() % 10 === 0) {
this.updateTeamStats();
}
}
private updateTeamStats() {
const players = this.game.playerViews();
const grouped: Record<number, PlayerView[]> = {};
for (const player of players) {
const team = player.team();
if (!grouped[team]) grouped[team] = [];
grouped[team].push(player);
}
this.teams = Object.entries(grouped)
.map(([teamStr, teamPlayers]) => {
let totalGold = 0;
let totalTroops = 0;
let totalScoreSort = 0;
for (const p of teamPlayers) {
totalGold += p.gold();
if (p.isAlive()) {
totalTroops += p.troops();
totalGold += p.gold();
totalScoreSort += p.numTilesOwned();
}
}
const totalScorePercent = totalScoreSort / this.game.numLandTiles();
return {
teamName: teamStr,
totalScoreStr: formatPercentage(totalScorePercent),
totalScoreSort,
totalGold: renderNumber(totalGold),
totalTroops: renderNumber(totalTroops / 10),
players: teamPlayers,
};
})
.sort((a, b) => b.totalScoreSort - a.totalScoreSort);
this.requestUpdate();
}
renderLayer(context: CanvasRenderingContext2D) {}
shouldTransform(): boolean {
return false;
}
static styles = css`
:host {
display: block;
}
.team-stats {
position: fixed;
top: 10px;
left: 450px;
z-index: 9999;
background-color: rgb(31 41 55 / 0.7);
padding: 10px;
padding-top: 0px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border-radius: 10px;
max-width: 250px;
max-height: 30vh;
overflow-y: auto;
width: 400px;
backdrop-filter: blur(5px);
}
.teamStats-close-button {
background: none;
border: none;
color: white;
cursor: pointer;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 5px;
text-align: center;
border-bottom: 1px solid rgba(51, 51, 51, 0.2);
color: var(--text-color, white);
}
th {
background-color: rgb(31 41 55 / 0.5);
color: white;
}
.hidden {
display: none !important;
}
.team-stats-button {
position: fixed;
left: 450px;
top: 10px;
z-index: 9999;
background-color: rgb(31 41 55 / 0.7);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
`;
render() {
return html`
<button
@click=${() => this.toggleTeamStats()}
class="team-stats-button ${this._shownOnInit && this._teamStatsHidden
? ""
: "hidden"}"
>
Team Stats
</button>
<div
class="team-stats ${this._teamStatsHidden ? "hidden" : ""}"
@contextmenu=${(e) => e.preventDefault()}
>
<button
class="teamStats-close-button"
@click=${() => this.hideTeamStats()}
>
Hide
</button>
<table>
<thead>
<tr>
<th>Team</th>
<th>Owned</th>
<th>Gold</th>
<th>Troops</th>
</tr>
</thead>
<tbody>
${this.teams.map(
(team) => html`
<tr class="">
<td>${team.teamName}</td>
<td>${team.totalScoreStr}</td>
<td>${team.totalGold}</td>
<td>${team.totalTroops}</td>
</tr>
`,
)}
</tbody>
</table>
</div>
`;
}
toggleTeamStats() {
this._teamStatsHidden = !this._teamStatsHidden;
}
hideTeamStats() {
this._teamStatsHidden = true;
this.requestUpdate();
}
showTeamStats() {
this._teamStatsHidden = true;
this.requestUpdate();
}
get isVisible() {
return !this._teamStatsHidden;
}
}
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) + "%";
}
return perc.toPrecision(2) + "%";
}
+1
View File
@@ -410,6 +410,7 @@
<win-modal></win-modal>
<game-starting-modal></game-starting-modal>
<top-bar></top-bar>
<team-stats></team-stats>
<player-panel></player-panel>
<help-modal></help-modal>
<dark-mode-button></dark-mode-button>