Files
OpenFrontIO/src/client/GameInfoModal.ts
T
DevelopingTom d758e21351 Restyle game rank modal (#2918)
## Description:
The game rank modal was still using the old style, which clashes
strongly with the new one.

This PR changes changes the modal style to be consistent with the new
one:

### Old

<img width="894" height="451" alt="image"
src="https://github.com/user-attachments/assets/c83177cf-a1ed-4ee5-9e12-7d2a9d8004cf"
/>

### New


![redesign](https://github.com/user-attachments/assets/ecf4f0ae-88f0-433c-90be-f41447e17afe)

Tagged as `v29` to have a consistent style in the same version.

## 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:

IngloriousTom
2026-01-16 10:14:38 -08:00

217 lines
6.3 KiB
TypeScript

import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { GameEndInfo } from "../core/Schemas";
import { GameMapType, hasUnusualThumbnailSize } from "../core/game/Game";
import { fetchGameById } from "./Api";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { UsernameInput } from "./UsernameInput";
import { renderDuration, translateText } from "./Utils";
import {
PlayerInfo,
Ranking,
RankType,
} from "./components/baseComponents/ranking/GameInfoRanking";
import "./components/baseComponents/ranking/PlayerRow";
import "./components/baseComponents/ranking/RankingControls";
import "./components/baseComponents/ranking/RankingHeader";
@customElement("game-info-modal")
export class GameInfoModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@state() private mapImage: string | null = null;
@state() private gameInfo: GameEndInfo | null = null;
@state() private rankedPlayers: Array<PlayerInfo> = [];
@property({ type: String }) gameId: string | null = null;
@property({ type: String }) rankType = RankType.Lifetime;
@state() private username: string | null = null;
@state() private isLoadingGame: boolean = true;
private ranking: Ranking | null = null;
connectedCallback() {
super.connectedCallback();
this.updateRanking();
}
createRenderRoot() {
return this;
}
render() {
return html`
<o-modal
id="gameInfoModal"
title="${translateText("game_info_modal.title")}"
translationKey="main.game_info"
>
<div
class="h-full flex flex-col items-center px-25 text-center mb-4 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent"
>
<div class="w-75 sm:w-125">
${this.isLoadingGame
? this.renderLoadingAnimation()
: this.renderRanking()}
</div>
</div>
</o-modal>
`;
}
private renderRanking() {
if (this.rankedPlayers.length === 0) {
return html`
<div class="flex flex-col items-center justify-center p-6 text-white">
<p class="mb-2">❌ ${translateText("game_info_modal.no_winner")}</p>
</div>
`;
}
return html`
${this.renderGameInfo()}
<ranking-controls
.rankType=${this.rankType}
@sort=${this.sort}
></ranking-controls>
${this.renderSummaryTable()}
`;
}
private renderLoadingAnimation() {
return html` <div
class="flex flex-col items-center justify-center p-6 text-white"
>
<p class="mb-2">${translateText("game_info_modal.loading_game_info")}</p>
<div
class="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"
></div>
</div>`;
}
private sort(e: CustomEvent<RankType>) {
this.rankType = e.detail;
this.updateRanking();
}
private updateRanking() {
if (this.ranking) {
this.rankedPlayers = this.ranking.sortedBy(this.rankType);
}
}
private renderGameInfo() {
const info = this.gameInfo;
if (!info) {
return html``;
}
const isUnusualThumbnailSize = hasUnusualThumbnailSize(info.config.gameMap);
return html`
<div
class="h-37.5 flex relative justify-between rounded-xl bg-black/20 items-center"
>
${this.mapImage
? html`<img
src="${this.mapImage}"
class="absolute place-self-start col-span-full row-span-full h-full rounded-xl mask-[linear-gradient(to_left,transparent,#fff)] ${isUnusualThumbnailSize
? "object-cover object-center"
: ""}"
/>`
: html`<div
class="place-self-start col-span-full row-span-full h-full rounded-xl bg-gray-300"
></div>`}
<div class="text-right p-3 w-full">
<div class="font-normal pl-1 pr-1">
<span class="bg-white text-blue-800 font-normal pl-1 pr-1"
>${info.config.gameMode}</span
>
<span class="font-bold">${info.config.gameMap}</span>
</div>
<div>${renderDuration(info.duration)}</div>
<div>
${info.players.length} ${translateText("game_info_modal.players")}
</div>
</div>
</div>
`;
}
private renderSummaryTable() {
const bestScore =
this.rankedPlayers.length > 0 ? this.score(this.rankedPlayers[0]) : 0;
return html`
<ul>
<ranking-header
.rankType=${this.rankType}
@sort=${this.sort}
></ranking-header>
${this.rankedPlayers.map(
(player: PlayerInfo, index) => html`
<player-row
.player=${player}
.rank=${index + 1}
.score=${this.ranking?.score(player, this.rankType) ?? 0}
.rankType=${this.rankType}
.bestScore=${bestScore}
.currentPlayer=${this.username === player.rawUsername}
></player-row>
`,
)}
</ul>
`;
}
public open() {
this.modalEl?.open();
}
public close() {
this.modalEl?.close();
}
private score(player: PlayerInfo): number {
if (!this.ranking) return 0;
return this.ranking.score(player, this.rankType);
}
private async loadMapImage(gameMap: string) {
try {
const mapType = gameMap as GameMapType;
const data = terrainMapFileLoader.getMapData(mapType);
this.mapImage = await data.webpPath();
} catch (error) {
console.error("Failed to load map image:", error);
}
}
public loadUserName() {
const usernameInput = document.querySelector(
"username-input",
) as UsernameInput;
if (usernameInput) {
this.username = usernameInput.getCurrentUsername();
}
}
public async loadGame(gameId: string) {
try {
this.isLoadingGame = true;
this.loadUserName();
const session = await fetchGameById(gameId);
if (!session) return;
this.gameInfo = session.info;
this.ranking = new Ranking(session);
this.updateRanking();
this.isLoadingGame = false;
await this.loadMapImage(session.info.config.gameMap);
} catch (err) {
console.error("Failed to load game:", err);
} finally {
this.isLoadingGame = false;
}
}
}