diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 17f8bc30f..b778aea1d 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -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, diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts new file mode 100644 index 000000000..9dc45a7f3 --- /dev/null +++ b/src/client/graphics/layers/TeamStats.ts @@ -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 = {}; + 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` + +
e.preventDefault()} + > + + + + + + + + + + + + ${this.teams.map( + (team) => html` + + + + + + + `, + )} + +
TeamOwnedGoldTroops
${team.teamName}${team.totalScoreStr}${team.totalGold}${team.totalTroops}
+
+ `; + } + + 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) + "%"; +} diff --git a/src/client/index.html b/src/client/index.html index 7031f482b..024be0840 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -410,6 +410,7 @@ +