mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 04:22:04 +00:00
464a4a817a
## Description: The calculation is based on: 50 players per 1_000_000 land tiles, limited at 125 players because of performance Second number is 75% of that, third one 50% That way, the player counts are staying mostly the same Look at the "Dynamic Config" column, these are the new player counts: (The 125 players limit is missing in that column, only relevant for the twolakes map) <img width="930" height="1033" alt="Screenshot_2026-01-12_152758" src="https://github.com/user-attachments/assets/e1791740-e263-47b3-8b27-4f9aa358d381" /> <img width="926" height="324" alt="Screenshot_2026-01-12_152814" src="https://github.com/user-attachments/assets/78d6789b-374f-4f8b-b50f-f6f08395572b" /> This PR also removes `MapDescription` from `Maps.ts` because its unused. And this PR updates the map-generator `README.md` to reflect the changes ## 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: FloPinguin
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { LitElement, html } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
import { Difficulty, GameMapType } from "../../core/game/Game";
|
|
import { terrainMapFileLoader } from "../TerrainMapFileLoader";
|
|
import { translateText } from "../Utils";
|
|
|
|
@customElement("map-display")
|
|
export class MapDisplay extends LitElement {
|
|
@property({ type: String }) mapKey = "";
|
|
@property({ type: Boolean }) selected = false;
|
|
@property({ type: String }) translation: string = "";
|
|
@property({ type: Boolean }) showMedals = false;
|
|
@property({ attribute: false }) wins: Set<Difficulty> = new Set();
|
|
@state() private mapWebpPath: string | null = null;
|
|
@state() private mapName: string | null = null;
|
|
@state() private isLoading = true;
|
|
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.loadMapData();
|
|
}
|
|
|
|
private async loadMapData() {
|
|
if (!this.mapKey) return;
|
|
|
|
try {
|
|
this.isLoading = true;
|
|
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
|
|
const data = terrainMapFileLoader.getMapData(mapValue);
|
|
this.mapWebpPath = await data.webpPath();
|
|
this.mapName = (await data.manifest()).name;
|
|
} catch (error) {
|
|
console.error("Failed to load map data:", error);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
private handleKeydown(event: KeyboardEvent) {
|
|
// Trigger the same activation logic as click when Enter or Space is pressed
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
// Dispatch a click event to maintain compatibility with parent click handlers
|
|
(event.target as HTMLElement).click();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<div
|
|
role="button"
|
|
tabindex="0"
|
|
aria-selected="${this.selected}"
|
|
aria-label="${this.translation ?? this.mapName ?? this.mapKey}"
|
|
@keydown="${this.handleKeydown}"
|
|
class="w-full h-full p-3 flex flex-col items-center justify-between rounded-xl border cursor-pointer transition-all duration-200 gap-3 group ${this
|
|
.selected
|
|
? "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.3)]"
|
|
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 hover:-translate-y-1 active:scale-95"}"
|
|
>
|
|
${this.isLoading
|
|
? html`<div
|
|
class="w-full aspect-[2/1] text-white/40 transition-transform duration-200 rounded-lg bg-black/20 text-xs font-bold uppercase tracking-wider flex items-center justify-center animate-pulse"
|
|
>
|
|
${translateText("map_component.loading")}
|
|
</div>`
|
|
: this.mapWebpPath
|
|
? html`<div
|
|
class="w-full aspect-[2/1] relative overflow-hidden rounded-lg bg-black/20"
|
|
>
|
|
<img
|
|
src="${this.mapWebpPath}"
|
|
alt="${this.translation || this.mapName}"
|
|
class="w-full h-full object-cover ${this.selected
|
|
? "opacity-100"
|
|
: "opacity-80"} group-hover:opacity-100 transition-opacity duration-200"
|
|
/>
|
|
</div>`
|
|
: html`<div
|
|
class="w-full aspect-[2/1] text-red-400 transition-transform duration-200 rounded-lg bg-red-500/10 text-xs font-bold uppercase tracking-wider flex items-center justify-center"
|
|
>
|
|
${translateText("map_component.error")}
|
|
</div>`}
|
|
${this.showMedals
|
|
? html`<div class="flex gap-1 justify-center w-full">
|
|
${this.renderMedals()}
|
|
</div>`
|
|
: null}
|
|
<div
|
|
class="text-xs font-bold text-white uppercase tracking-wider text-center leading-tight break-words hyphens-auto"
|
|
>
|
|
${this.translation || this.mapName}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderMedals() {
|
|
const medalOrder: Difficulty[] = [
|
|
Difficulty.Easy,
|
|
Difficulty.Medium,
|
|
Difficulty.Hard,
|
|
Difficulty.Impossible,
|
|
];
|
|
const colors: Record<Difficulty, string> = {
|
|
[Difficulty.Easy]: "var(--medal-easy)",
|
|
[Difficulty.Medium]: "var(--medal-medium)",
|
|
[Difficulty.Hard]: "var(--medal-hard)",
|
|
[Difficulty.Impossible]: "var(--medal-impossible)",
|
|
};
|
|
const wins = this.readWins();
|
|
return medalOrder.map((medal) => {
|
|
const earned = wins.has(medal);
|
|
const mask =
|
|
"url('/images/MedalIconWhite.svg') no-repeat center / contain";
|
|
return html`<div
|
|
class="w-5 h-5 ${earned ? "opacity-100" : "opacity-25"}"
|
|
style="background-color:${colors[
|
|
medal
|
|
]}; mask: ${mask}; -webkit-mask: ${mask};"
|
|
title=${translateText(`difficulty.${medal.toLowerCase()}`)}
|
|
></div>`;
|
|
});
|
|
}
|
|
|
|
private readWins(): Set<Difficulty> {
|
|
return this.wins ?? new Set();
|
|
}
|
|
}
|