mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-13 10:33:59 +00:00
Move map metadata into info.json and generate map TypeScript from it (#4227)
**Add approved & assigned issue number here:** N/A — maintainer refactor. ## Description: Makes each map's `info.json` the single source of truth for map metadata — adding a map is now a folder with `image.png` + `info.json`, a `gen-maps` run, and an en.json display name. **info.json / manifest.json carry full map metadata.** Every `map-generator/assets/maps/<map>/info.json` declares `id` (the `GameMapType` enum key), `name` (the enum value — wire format, unchanged for all 94 maps), `translation_key`, `categories`, and `multiplayer_frequency` (the public-playlist weight that used to be the `FREQUENCY` record in MapPlaylist.ts). The generator validates everything and mirrors it into `resources/maps/<map>/manifest.json`. 23 stale info.json `name` values were normalized to the canonical enum value; enum values are byte-identical, so replays and stored game configs are unaffected. **The generator emits the TypeScript and discovers maps itself.** New `map-generator/codegen.go` generates `src/core/game/Maps.gen.ts` (`GameMapType`, `GameMapName`, `mapCategories`, `mapTranslationKeys`, `multiplayerFrequency` — now a full `Record<GameMapName, number>`, killing the old `Partial`) on every run; `Game.ts` re-exports it. The hardcoded map registry in `main.go` is gone — maps are auto-discovered from the `assets/maps` / `assets/test_maps` directories. MapConsistency tests fail with a "run `npm run gen-maps`" message if info.json, manifest.json, and Maps.gen.ts drift. The tracked `map-generator/map-generator` binary is rebuilt to match. **New categories: continents + world/cosmic/tournament/other, multi-category support.** `continental`/`regional`/`fantasy`/`arcade` are replaced by `featured`, `world`, `europe`, `asia`, `north_america`, `africa`, `south_america`, `oceania`, `antarctica`, `cosmic`, `tournament`, and `other`. Maps can list multiple categories, so straddlers (Black Sea, Bosphorus, Caucasus, Between Two Seas, Bering Sea/Strait, Mena, Strait of Gibraltar, Hawaii, Arctic) appear under both regions. Featured is itself a category (same 7 maps as before). MapPlaylist keeps its arcade exclusion via an explicit set. **Map picker UI.** Two tabs: **Featured** (default — featured maps plus a Favorites section when maps are starred) and **All** (one prominent collapsible bar per category with a map count, collapsed by default). The selected map is prepended to the featured grid when it lives elsewhere. `getMapName()` resolves through the generated `mapTranslationKeys`, which also fixes tourney maps never resolving a valid translation key. ## Please complete the following: - [ ] I have added screenshots for all UI updates (maintainer change — picker described above) - [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 ## Please put your Discord username so you can be contacted if a bug or regression is found: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,20 @@ export class MapDisplay extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
// If this element is reused for a different map, reload its data —
|
||||
// otherwise it keeps showing the previous map's thumbnail.
|
||||
const previousMapKey = changedProperties.get("mapKey");
|
||||
if (
|
||||
changedProperties.has("mapKey") &&
|
||||
previousMapKey !== undefined &&
|
||||
previousMapKey !== this.mapKey &&
|
||||
this.dataLoaded
|
||||
) {
|
||||
this.loadMapData();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMapData() {
|
||||
if (!this.mapKey) return;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { assetUrl } from "../../../core/AssetUrls";
|
||||
import {
|
||||
Difficulty,
|
||||
GameMapType,
|
||||
mapCategories,
|
||||
mapTranslationKeys,
|
||||
} from "../../../core/game/Game";
|
||||
import { translateText } from "../../Utils";
|
||||
import "./MapDisplay";
|
||||
@@ -13,16 +15,6 @@ const randomMap = assetUrl("images/RandomMap.webp");
|
||||
|
||||
type MapTab = "featured" | "all" | "favorites";
|
||||
|
||||
const featuredMaps: GameMapType[] = [
|
||||
GameMapType.World,
|
||||
GameMapType.Europe,
|
||||
GameMapType.NorthAmerica,
|
||||
GameMapType.SouthAmerica,
|
||||
GameMapType.Asia,
|
||||
GameMapType.Africa,
|
||||
GameMapType.Japan,
|
||||
];
|
||||
|
||||
@customElement("map-picker")
|
||||
export class MapPicker extends LitElement {
|
||||
@property({ type: String }) selectedMap: GameMapType = GameMapType.World;
|
||||
@@ -34,6 +26,7 @@ export class MapPicker extends LitElement {
|
||||
@property({ attribute: false }) onSelectMap?: (map: GameMapType) => void;
|
||||
@property({ attribute: false }) onSelectRandom?: () => void;
|
||||
@state() private activeTab: MapTab = "featured";
|
||||
@state() private expandedCategories: Set<string> = new Set();
|
||||
@state() private favorites: GameMapType[] = getFavoriteMaps();
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -52,6 +45,16 @@ export class MapPicker extends LitElement {
|
||||
this.onSelectRandom?.();
|
||||
};
|
||||
|
||||
private toggleCategory(categoryKey: string) {
|
||||
const expanded = new Set(this.expandedCategories);
|
||||
if (expanded.has(categoryKey)) {
|
||||
expanded.delete(categoryKey);
|
||||
} else {
|
||||
expanded.add(categoryKey);
|
||||
}
|
||||
this.expandedCategories = expanded;
|
||||
}
|
||||
|
||||
private preventImageDrag(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -76,50 +79,92 @@ export class MapPicker extends LitElement {
|
||||
.wins=${this.getWins(mapValue)}
|
||||
.favorite=${this.favorites.includes(mapValue)}
|
||||
.onToggleFavorite=${() => this.handleToggleFavorite(mapValue)}
|
||||
.translation=${translateText(`map.${mapKey?.toLowerCase()}`)}
|
||||
.translation=${translateText(mapTranslationKeys[mapValue])}
|
||||
></map-display>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAllMaps() {
|
||||
const mapCategoryEntries = Object.entries(mapCategories);
|
||||
return html`<div class="space-y-8">
|
||||
${mapCategoryEntries.map(
|
||||
([categoryKey, maps]) => html`
|
||||
<div class="w-full">
|
||||
<h4
|
||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||
>
|
||||
${translateText(`map_categories.${categoryKey}`)}
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
${maps.map((mapValue) => this.renderMapCard(mapValue))}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
private renderMapGrid(maps: GameMapType[]) {
|
||||
// Keyed by map so cards keep their identity when the list shifts
|
||||
// (e.g. the selected map gets prepended to the featured grid) —
|
||||
// positional reuse would leave stale thumbnails behind.
|
||||
return html`<div
|
||||
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
||||
>
|
||||
${repeat(
|
||||
maps,
|
||||
(mapValue) => mapValue,
|
||||
(mapValue) => this.renderMapCard(mapValue),
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderFeaturedMaps() {
|
||||
let featuredMapList = featuredMaps;
|
||||
if (!this.useRandomMap && !featuredMapList.includes(this.selectedMap)) {
|
||||
featuredMapList = [this.selectedMap, ...featuredMaps];
|
||||
}
|
||||
private renderSectionHeading(label: string) {
|
||||
return html`<h4
|
||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||
>
|
||||
${label}
|
||||
</h4>`;
|
||||
}
|
||||
|
||||
private renderCategoryBar(categoryKey: string, maps: GameMapType[]) {
|
||||
const expanded = this.expandedCategories.has(categoryKey);
|
||||
return html`<div class="w-full">
|
||||
<h4
|
||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded=${expanded}
|
||||
@click=${() => this.toggleCategory(categoryKey)}
|
||||
class="w-full flex items-center justify-between gap-3 px-4 py-3 rounded-xl border transition-all duration-200 active:scale-[0.99] ${expanded
|
||||
? "bg-malibu-blue/20 border-malibu-blue/50"
|
||||
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}"
|
||||
>
|
||||
${translateText("map_categories.featured")}
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
${featuredMapList.map((mapValue) => this.renderMapCard(mapValue))}
|
||||
</div>
|
||||
<span
|
||||
class="flex items-center gap-3 text-sm font-bold text-white uppercase tracking-wider"
|
||||
>
|
||||
<svg
|
||||
class="w-3.5 h-3.5 shrink-0 transition-transform duration-200 ${expanded
|
||||
? "rotate-90"
|
||||
: ""}"
|
||||
viewBox="0 0 12 12"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 2l5 4-5 4z" />
|
||||
</svg>
|
||||
${translateText(`map_categories.${categoryKey}`)}
|
||||
</span>
|
||||
<span class="text-xs font-bold text-white/40">${maps.length}</span>
|
||||
</button>
|
||||
${expanded
|
||||
? html`<div class="mt-4">${this.renderMapGrid(maps)}</div>`
|
||||
: null}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderFavoriteMaps() {
|
||||
private renderFeaturedTab() {
|
||||
const featured = mapCategories.featured ?? [];
|
||||
let featuredMapList = featured;
|
||||
if (!this.useRandomMap && !featured.includes(this.selectedMap)) {
|
||||
featuredMapList = [this.selectedMap, ...featured];
|
||||
}
|
||||
return html`<div class="w-full">
|
||||
${this.renderSectionHeading(translateText("map_categories.featured"))}
|
||||
${this.renderMapGrid(featuredMapList)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderAllTab() {
|
||||
return html`<div class="space-y-3">
|
||||
${Object.entries(mapCategories)
|
||||
.filter(([categoryKey]) => categoryKey !== "featured")
|
||||
.map(([categoryKey, maps]) =>
|
||||
this.renderCategoryBar(categoryKey, maps),
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderFavoritesTab() {
|
||||
if (this.favorites.length === 0) {
|
||||
return html`<div
|
||||
class="w-full flex flex-col items-center justify-center gap-3 py-12 px-4 text-center rounded-xl border border-dashed border-white/10 bg-black/20"
|
||||
@@ -131,25 +176,19 @@ export class MapPicker extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
return html`<div class="w-full">
|
||||
<h4
|
||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||
>
|
||||
${translateText("map_categories.favorites")}
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
${this.favorites.map((mapValue) => this.renderMapCard(mapValue))}
|
||||
</div>
|
||||
${this.renderSectionHeading(translateText("map_categories.favorites"))}
|
||||
${this.renderMapGrid(this.favorites)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderActiveTab() {
|
||||
switch (this.activeTab) {
|
||||
case "all":
|
||||
return this.renderAllMaps();
|
||||
return this.renderAllTab();
|
||||
case "favorites":
|
||||
return this.renderFavoriteMaps();
|
||||
return this.renderFavoritesTab();
|
||||
default:
|
||||
return this.renderFeaturedMaps();
|
||||
return this.renderFeaturedTab();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,11 +227,7 @@ export class MapPicker extends LitElement {
|
||||
? "pt-4 border-t border-white/5"
|
||||
: ""}"
|
||||
>
|
||||
<h4
|
||||
class="text-xs font-bold text-white/40 uppercase tracking-widest mb-4 pl-2"
|
||||
>
|
||||
${translateText("map_categories.special")}
|
||||
</h4>
|
||||
${this.renderSectionHeading(translateText("map_categories.special"))}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user