Skip to content

Conversation

@abdallahbahrawi1
Copy link
Contributor

@abdallahbahrawi1 abdallahbahrawi1 commented Oct 28, 2025

Description:

  • New layout: fewer clicks and much less scrolling; key settings visible at a glance.
  • Added map browser: search, category filters, and random toggle.
  • Added presets
  • Reactor it to shared components/utilities:

Screenshots:

Screenshot 2025-10-28 203850

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • 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:

abodcraft1

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

Walkthrough

Refactors host and single-player modals into component-driven light‑DOM UI, adds preset persistence/manager, many shared Lit components (pickers, sliders, map browser, advanced options), safer map loading, invite ID visibility/copy flows, and expands English translations.

Changes

Cohort / File(s) Summary
Translations
resources/lang/en.json
Added many UI keys/groups (common, game_options, settings_summary, presets, map.browser, map_component.error) and new modal/lobby strings; updated public_lobby team labels.
Host & Single-player modals
src/client/HostLobbyModal.ts, src/client/SinglePlayerModal.ts
Rewrote to componentized, light‑DOM render paths; added presets persistence/apply, rules/unit toggles, invite ID masking/toggle/copy, polling guards, workerPath usage, and new internal helpers/state.
Map components & loading
src/client/components/Maps.ts, src/client/components/shared/MapBrowserPane.ts
MapDisplay now renders into light DOM, normalizes map keys, adds _loadToken to avoid stale loads, loads assets concurrently, and improves loading/error UI; MapBrowserPane adds search, categories, random toggle and emits map-select/toggle-random.
Presets system
src/client/components/shared/PresetsBar.ts, src/client/components/shared/PresetsManager.ts, src/client/types/preset.ts
New generic Preset<T> type; PresetsBar UI and PresetsManager persisted to localStorage with save/update/delete/select, validation/limit enforcement, and emitted events (apply/clear).
Advanced options, rules & units
src/client/components/shared/AdvancedOptions.ts, src/client/utilities/RenderRulesOptions.ts, src/client/utilities/RenderUnitTypeOptions.ts
New AdvancedOptions component and render helpers for rules and unit toggles; card-styled accessible UI, on/off chips, and toggle-rule / toggle-unit event wiring.
Pickers & controls
src/client/components/shared/DifficultyPicker.ts, src/client/components/shared/GameModePicker.ts, src/client/components/shared/TeamCountPicker.ts, src/client/components/shared/DifficultyControls.ts, src/client/components/shared/GameModeControls.ts
Added pickers and wrapper controls (difficulty, game mode, team count) emitting change events with localized segmented/button UIs.
Sliders & small components
src/client/components/shared/BotsSlider.ts, src/client/components/shared/ExpandButton.ts, src/client/components/shared/SettingsSummary.ts, src/client/components/shared/PresetsBar.ts
Added BotsSlider (clamped with optional debounce), ExpandButton (toggle event), SettingsSummary (read-only snapshot), PresetsBar (preset select/name/actions).
Utilities & render helpers
src/client/utilities/RenderRulesOptions.ts, src/client/utilities/RenderUnitTypeOptions.ts
New renderRulesOptions API and richer renderUnitTypeOptions producing accessible card-styled toggles.
Transport logging tweak
src/client/Transport.ts
Safer logging: replaced this.socket!.readyState with this.socket?.readyState in two log branches.
Misc wiring
src/client/...
Wiring across client files to use new components/events, presets helpers, debounced bots handling, and small API path/guard changes.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Modal as Host/Single Modal
  participant Presets as PresetsManager
  participant Storage as localStorage
  participant Server as API
  Note over User,Presets: Save a preset (client-only flow)
  User->>Modal: click "Save preset" (name)
  Modal->>Presets: call getSettings()
  Presets->>Storage: persist preset JSON
  Presets-->>Modal: emit apply-preset (settings)
  Modal->>Modal: applySettings(settings)
  alt lobbyId present
    Modal->>Server: PUT config via config.workerPath(lobbyId)
    Server-->>Modal: ack
  end
Loading
sequenceDiagram
  participant User
  participant InviteUI as Invite Bar
  participant Modal
  participant Clipboard
  Note over User,InviteUI: Reveal and copy lobby invite
  User->>InviteUI: toggle "show id"
  InviteUI->>Modal: toggleInviteVisibility(true)
  Modal->>InviteUI: render full id
  User->>InviteUI: click copy
  InviteUI->>Clipboard: write invite URL
  Clipboard-->>User: copied
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Focus review on:
    • HostLobbyModal & SinglePlayerModal: property vs state exposure, presets apply/update semantics, invite masking/copy, and polling gating.
    • PresetsManager & PresetsBar: localStorage schema validation, limit enforcement, ID/timestamp handling, emitted event shapes.
    • MapDisplay: _loadToken concurrency guard and error handling when concurrent loads race.
    • Event contracts, accessibility attributes, and light‑DOM rendering choices for new components (bots-slider, pickers, advanced-options, team-count-picker).

Possibly related PRs

Suggested labels

Feature - Frontend, UI/UX, Feature - New

Suggested reviewers

  • evanpelle
  • scottanderson
  • Duwibi

Poem

Presets rest in localStorage's care,
Maps load safe — no stale affair,
Sliders hum, pickers choose with grace,
Eye shows the id, copy wins the race,
Light‑DOM modals bring a brighter place. ✨

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Improve host lobby modal" directly relates to the primary change in this PR, which involves a major refactor of the HostLobbyModal component. The title accurately communicates the main objective from the developer's perspective. While the PR also includes parallel improvements to SinglePlayerModal and introduces shared component infrastructure, these serve as supporting infrastructure for the host lobby modal improvements, making the focused title appropriate. The title is concise, clear, and avoids vague language or noise.
Description Check ✅ Passed The description is directly related to the changeset. It clearly outlines the key improvements: a new layout reducing scrolling and clicks, a map browser with search and filters, presets functionality, and refactoring into shared components. These points are all supported by the actual changes shown in the raw summary, including new components (MapBrowserPane, PresetsManager), translation additions for UI elements, and modal refactoring. The description provides meaningful information about what was changed and why, avoiding vague or generic terms.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (12)
src/client/components/shared/DifficultyControls.ts (1)

10-12: Consider simplifying the keyOf lookup.

The Object.keys(Difficulty).find() pattern works but relies on TypeScript enum behavior. For clarity and type safety, consider using Object.entries:

-  private keyOf(value: Difficulty) {
-    return Object.keys(Difficulty).find((k) => Difficulty[k] === value) ?? "";
-  }
+  private keyOf(value: Difficulty) {
+    const entry = Object.entries(Difficulty).find(([_, v]) => v === value);
+    return entry?.[0] ?? "";
+  }

This makes it explicit that you're matching on values and extracting keys.

src/client/components/shared/GameModePicker.ts (1)

43-45: Consider explicit translation mapping for all game modes.

The current logic assumes only FFA and one other mode:

${mode === GameMode.FFA
  ? translateText("game_mode.ffa")
  : translateText("game_mode.teams")}

If additional game modes are added in the future, this will incorrectly label them as "teams". Consider using an explicit mapping:

${translateText(`game_mode.${mode === GameMode.FFA ? "ffa" : "teams"}`)}

Or better, a lookup object if more modes are expected.

src/client/components/shared/SettingsSummary.ts (1)

15-17: Harden enum-to-key lookup for numeric enums

Using Object.keys() on TS enums includes reverse mappings. Safer/simpler: use the reverse map directly for numeric enums.

Apply:

-  private keyOf(enumObj, value) {
-    return Object.keys(enumObj).find((k) => enumObj[k] === value) ?? "";
-  }
+  private enumName<E extends Record<string, string | number>>(e: E, v: number | string): string {
+    return typeof v === "number" ? (e as any)[v] ?? "" : String(v);
+  }

And update usages:

- const mapKey = this.keyOf(GameMapType, this.selectedMap);
- const diffKey = this.keyOf(Difficulty, this.selectedDifficulty);
+ const mapKey = this.enumName(GameMapType, this.selectedMap);
+ const diffKey = this.enumName(Difficulty, this.selectedDifficulty);
src/client/components/Maps.ts (1)

71-91: Optional: cancel in-flight loads on disconnect or new key

Token guard prevents stale state, but network work still continues. Consider AbortController to actually cancel requests.

Sketch:

+ private _abort?: AbortController;
  private async loadMapData() {
    if (!this.mapKey) return;
    const myToken = ++this._loadToken;
+   this._abort?.abort();
+   this._abort = new AbortController();
    this.isLoading = true;
    this.mapWebpPath = null;
    try {
      // ...
-     const [webpPath, manifest] = await Promise.all([data.webpPath(), data.manifest()]);
+     const [webpPath, manifest] = await Promise.all([
+       data.webpPath({ signal: this._abort.signal }),
+       data.manifest({ signal: this._abort.signal }),
+     ]);
      if (myToken !== this._loadToken) return;
      this.mapWebpPath = webpPath;
      this.mapName = manifest.name;
    } catch (error) {
      console.error("Failed to load map data:", error);
    } finally {
      this.isLoading = false;
    }
  }

(Adjust loader signatures if needed.)

src/client/components/shared/MapBrowserPane.ts (2)

72-79: Localize and tighten list semantics

Localize the aria-label and declare a listbox container to pair with option roles.

-      <aside
-        aria-label="Map Browser"
+      <aside
+        aria-label=${translateText("map.browser")}
         class="min-h-80 flex flex-col overflow-hidden rounded-xl border border-white/15 bg-zinc-900/40"
       >

And around the maps grid:

-        <div class="grid flex-1 grid-cols-1 gap-4 overflow-auto p-3">
+        <div class="grid flex-1 grid-cols-1 gap-4 overflow-auto p-3" role="listbox" aria-multiselectable="false">

Ensure map items are role="option" as in the previous comment.


133-147: Use lit repeat() for stable keyed rendering

Improves performance and DOM stability during search/filter.

+ import { repeat } from "lit/directives/repeat.js";
  ...
-            ${maps.length
-              ? maps.map(({ value, key, name }) => {
+            ${maps.length
+              ? repeat(
+                  maps,
+                  ({ value }) => value,
+                  ({ value, key, name }) => {
                     const selected =
                       !this.useRandomMap && this.selectedMap === value;
-                  return html` <div
+                  return html` <div
                     @click=${() => this.selectMap(value)}
                     class="w-full h-full cursor-pointer"
                   >
                     <map-display
                       .mapKey=${key}
                       .selected=${selected}
                       .translation=${name}
                     ></map-display>
                   </div>`;
-                })
+                  },
+                )
               : html`<div class="col-span-full text-sm text-zinc-400">
src/client/components/shared/AdvancedOptions.ts (1)

87-97: Optional: unify rule rendering to reduce duplication

You already have renderRulesOptions; consider extending its config to include donate* rules when provided, instead of a separate renderRule method here.

Also applies to: 98-111

src/client/components/shared/BotsSlider.ts (1)

27-33: Type willUpdate with PropertyValues for better TS checks

Aligns with Lit’s typing and catches mistakes.

-import { customElement, property, state } from "lit/decorators.js";
+import { customElement, property, state } from "lit/decorators.js";
+import type { PropertyValues } from "lit";
...
-  willUpdate(changed: Map<string, unknown>) {
+  willUpdate(changed: PropertyValues<this>) {
src/client/components/shared/PresetsBar.ts (1)

33-35: Localize aria-label/title strings for accessibility

Hardcoded English labels (“Select preset”, “Preset name”) bypass i18n. Use translateText to keep accessibility strings localized.

-            aria-label="Select preset"
-            title="Select preset"
+            aria-label=${translateText("single_modal.select_placeholder")}
+            title=${translateText("single_modal.select_placeholder")}
...
-            aria-label="Preset name"
+            aria-label=${translateText("common.name")}

Also applies to: 55-60, 62-71, 74-81, 85-92

src/client/HostLobbyModal.ts (1)

104-105: Prevent overlapping polls; add error handling

A 1s interval can stack fetches if a request lags. Gate with an in‑flight flag and catch errors.

-  private playersInterval: ReturnType<typeof setInterval> | null = null;
+  private playersInterval: ReturnType<typeof setInterval> | null = null;
+  private isPolling = false;
@@
   private async pollPlayers() {
-    if (!this.lobbyId) return;
-    const config = await getServerConfigFromClient();
-    fetch(`/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`, {
-      method: "GET",
-      headers: { "Content-Type": "application/json" },
-    })
-      .then((response) => response.json())
-      .then((data: GameInfo) => {
-        this.clients = data.clients ?? [];
-      });
+    if (!this.lobbyId || this.isPolling) return;
+    this.isPolling = true;
+    try {
+      const config = await getServerConfigFromClient();
+      const res = await fetch(
+        `/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`,
+        { method: "GET", headers: { "Content-Type": "application/json" } },
+      );
+      if (res.ok) {
+        const data: GameInfo = await res.json();
+        this.clients = data.clients ?? [];
+      }
+    } catch (err) {
+      console.warn("pollPlayers failed:", err);
+    } finally {
+      this.isPolling = false;
+    }
   }

Also applies to: 666-679

src/client/SinglePlayerModal.ts (2)

59-71: Avoid attribute reflection on internal stateful props

These are controlled programmatically; reflecting to attributes can coerce types unexpectedly. Mark them non-attribute.

-  @property({ type: Number }) selectedMap: GameMapType = GameMapType.World;
-  @property({ type: Number }) selectedDifficulty: Difficulty =
+  @property({ type: Number, attribute: false }) selectedMap: GameMapType = GameMapType.World;
+  @property({ type: Number, attribute: false }) selectedDifficulty: Difficulty =
     Difficulty.Medium;
-  @property({ type: Boolean }) disableNPCs = false;
-  @property({ type: Number }) bots = 400;
-  @property({ type: Boolean }) infiniteGold = false;
-  @property({ type: Boolean }) infiniteTroops = false;
-  @property({ type: Boolean }) compactMap = false;
-  @property({ type: Boolean }) instantBuild = false;
-  @property({ type: Boolean }) useRandomMap = false;
-  @property({ type: Number }) gameMode: GameMode = GameMode.FFA;
-  @property({ type: Number }) teamCount: TeamCountConfig = 2;
+  @property({ type: Boolean, attribute: false }) disableNPCs = false;
+  @property({ type: Number, attribute: false }) bots = 400;
+  @property({ type: Boolean, attribute: false }) infiniteGold = false;
+  @property({ type: Boolean, attribute: false }) infiniteTroops = false;
+  @property({ type: Boolean, attribute: false }) compactMap = false;
+  @property({ type: Boolean, attribute: false }) instantBuild = false;
+  @property({ type: Boolean, attribute: false }) useRandomMap = false;
+  @property({ type: Number, attribute: false }) gameMode: GameMode = GameMode.FFA;
+  @property({ type: Number, attribute: false }) teamCount: TeamCountConfig = 2;

139-146: Localize the quick start button title

Use translateText for the title tooltip to match the button label.

-            title="Quick Start (Enter)"
+            title=${translateText("single_modal.quick_start")}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dd45e8 and a801e3e.

📒 Files selected for processing (19)
  • resources/lang/en.json (6 hunks)
  • src/client/HostLobbyModal.ts (11 hunks)
  • src/client/SinglePlayerModal.ts (2 hunks)
  • src/client/Transport.ts (1 hunks)
  • src/client/components/Maps.ts (3 hunks)
  • src/client/components/shared/AdvancedOptions.ts (1 hunks)
  • src/client/components/shared/BotsSlider.ts (1 hunks)
  • src/client/components/shared/DifficultyControls.ts (1 hunks)
  • src/client/components/shared/DifficultyPicker.ts (1 hunks)
  • src/client/components/shared/ExpandButton.ts (1 hunks)
  • src/client/components/shared/GameModeControls.ts (1 hunks)
  • src/client/components/shared/GameModePicker.ts (1 hunks)
  • src/client/components/shared/MapBrowserPane.ts (1 hunks)
  • src/client/components/shared/PresetsBar.ts (1 hunks)
  • src/client/components/shared/PresetsManager.ts (1 hunks)
  • src/client/components/shared/SettingsSummary.ts (1 hunks)
  • src/client/components/shared/TeamCountPicker.ts (1 hunks)
  • src/client/utilities/RenderRulesOptions.ts (1 hunks)
  • src/client/utilities/RenderUnitTypeOptions.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (12)
src/client/components/shared/DifficultyControls.ts (3)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-472)
src/client/components/shared/DifficultyPicker.ts (1)
  • customElement (6-44)
src/client/components/shared/AdvancedOptions.ts (4)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-472)
src/client/utilities/RenderRulesOptions.ts (1)
  • renderRulesOptions (25-91)
src/client/utilities/RenderUnitTypeOptions.ts (1)
  • renderUnitTypeOptions (23-87)
src/client/components/shared/GameModePicker.ts (3)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-472)
src/client/components/shared/GameModeControls.ts (1)
  • customElement (6-34)
src/client/components/shared/TeamCountPicker.ts (2)
src/core/Schemas.ts (1)
  • TeamCountConfig (153-153)
src/core/game/Game.ts (3)
  • Duos (52-52)
  • Trios (53-53)
  • Quads (54-54)
src/client/components/shared/MapBrowserPane.ts (4)
src/core/game/Game.ts (1)
  • mapCategories (103-141)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-472)
src/client/components/Maps.ts (1)
  • customElement (42-146)
src/client/components/Maps.ts (2)
src/client/TerrainMapFileLoader.ts (1)
  • terrainMapFileLoader (4-4)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/HostLobbyModal.ts (4)
src/core/Schemas.ts (1)
  • TeamCountConfig (153-153)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-201)
src/client/Utils.ts (1)
  • translateText (92-147)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (28-45)
src/client/components/shared/PresetsManager.ts (4)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-472)
src/client/components/shared/PresetsBar.ts (1)
  • customElement (7-114)
src/core/Util.ts (1)
  • generateID (245-251)
src/client/utilities/RenderUnitTypeOptions.ts (1)
src/client/HostLobbyModal.ts (1)
  • toggleUnit (599-605)
src/client/SinglePlayerModal.ts (3)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/components/Maps.ts (1)
  • customElement (42-146)
src/client/utilities/RenderRulesOptions.ts (1)
  • RuleKey (4-9)
src/client/components/shared/DifficultyPicker.ts (3)
src/client/HostLobbyModal.ts (1)
  • customElement (79-718)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-472)
src/client/components/shared/DifficultyControls.ts (1)
  • customElement (6-43)
src/client/components/shared/GameModeControls.ts (2)
src/client/components/shared/DifficultyControls.ts (1)
  • customElement (6-43)
src/client/components/shared/GameModePicker.ts (1)
  • customElement (6-52)
🔇 Additional comments (7)
src/client/Transport.ts (3)

567-581: Good defensive improvement.

The change from this.socket!.readyState to this.socket?.readyState on line 577 is correct. Since the else branch is reached when socket might be null, using optional chaining prevents a potential crash in logging.


583-597: Good defensive improvement.

Same improvement as in onSendWinnerEvent above. The change from this.socket!.readyState to this.socket?.readyState on line 593 correctly uses optional chaining for safe logging when socket might be null.


336-349: No action needed — z.prettifyError is valid Zod 4 API.

z.prettifyError is part of Zod 4's official API alongside other error helpers like z.flattenError and z.treeifyError. The code at line 340 is correct and requires no changes. This existing code (not modified in this PR) uses valid Zod 4 methods.

Likely an incorrect or invalid review comment.

src/client/components/shared/ExpandButton.ts (1)

9-18: Controlled component pattern relies on parent updating state.

The onClick handler dispatches a toggle event but doesn't update this.expanded internally. This means the button is a controlled component—the parent must listen to the event and update the .expanded property:

@toggle=${(e: CustomEvent<{ value: boolean }>) =>
  (this.rightExpanded = e.detail.value)}

This pattern is fine and matches the usage in HostLobbyModal.ts and SinglePlayerModal.ts. Just ensure all consumers follow this pattern to avoid the button appearing stuck.

src/client/utilities/RenderRulesOptions.ts (1)

1-91: LGTM! Clean implementation of rules options.

This utility follows good practices:

  • Type-safe RuleKey union and RulesRenderContext interface
  • Proper checkbox semantics (checked state matches isOn, handler passes checked directly)
  • Accessibility attributes (aria-label, title)
  • Consistent styling with the unit options
  • Clear on/off visual indicators

The straightforward toggleRule(key, checked) logic is easier to understand than the inverted pattern in RenderUnitTypeOptions.ts.

src/client/components/Maps.ts (1)

58-62: LGTM: reactive reload on mapKey changes

updated() correctly reloads on mapKey change and avoids redundant work.

src/client/components/shared/BotsSlider.ts (1)

14-25: LGTM: debounced slider with proper clamping and cleanup

Event semantics, clamping, and timer cleanup look solid. Light DOM styling is scoped via class selector—nice.

Also applies to: 35-74, 80-158

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 29, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/client/components/shared/MapBrowserPane.ts (1)

106-128: Add type="button" to the random toggle
Line 106 still falls back to the HTML default "submit". In a form, clicking this control would submit the form. Please add type="button" just like the other buttons in this block.

-            <button
+            <button
+              type="button"
               class="h-9 rounded-full border px-3 flex items-center gap-1.5 transition-all ${this
🧹 Nitpick comments (2)
src/client/HostLobbyModal.ts (1)

34-77: Consider extracting shared preset structure to reduce duplication.

The HostLobbyPreset type (lines 34-55) and SinglePlayerPreset (in SinglePlayerModal.ts lines 46-52) share identical structure except for the settings content. You could define a generic Preset<TSettings> type in a shared file to eliminate duplication and ensure consistency.

// In a shared types file
type Preset<TSettings> = {
  id: string;
  name: string;
  createdAt: number;
  updatedAt: number;
  settings: TSettings;
};

// Then use it
type HostLobbyPreset = Preset<{
  selectedMap: GameMapType;
  selectedDifficulty: Difficulty;
  // ... rest of settings
}>;

This would also align with the GenericPreset<T> already used in PresetsManager.ts (line 15).

src/client/SinglePlayerModal.ts (1)

287-299: Simplify static property access.

Lines 288-290 use (this.constructor as typeof SinglePlayerModal).PRESETS_KEY to access static properties. This works but is verbose. Consider storing static values in instance fields during construction or referencing the class directly:

+  private readonly PRESETS_KEY = SinglePlayerModal.PRESETS_KEY;
+  private readonly MAX_PRESETS = SinglePlayerModal.MAX_PRESETS;

   ${html`
     <presets-manager
-      storageKey=${(this.constructor as typeof SinglePlayerModal).PRESETS_KEY}
-      .limit=${(this.constructor as typeof SinglePlayerModal).MAX_PRESETS}
+      storageKey=${this.PRESETS_KEY}
+      .limit=${this.MAX_PRESETS}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a801e3e and 44b099e.

📒 Files selected for processing (7)
  • resources/lang/en.json (7 hunks)
  • src/client/HostLobbyModal.ts (10 hunks)
  • src/client/SinglePlayerModal.ts (4 hunks)
  • src/client/components/shared/AdvancedOptions.ts (1 hunks)
  • src/client/components/shared/MapBrowserPane.ts (1 hunks)
  • src/client/components/shared/PresetsManager.ts (1 hunks)
  • src/client/utilities/RenderUnitTypeOptions.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/utilities/RenderUnitTypeOptions.ts
  • src/client/components/shared/AdvancedOptions.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/components/shared/AdvancedOptions.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
PR: openfrontio/OpenFrontIO#1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • src/client/HostLobbyModal.ts
  • resources/lang/en.json
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
PR: openfrontio/OpenFrontIO#2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-21T21:42:45.277Z
Learnt from: plazmaezio
PR: openfrontio/OpenFrontIO#1883
File: src/client/HostLobbyModal.ts:227-245
Timestamp: 2025-08-21T21:42:45.277Z
Learning: In openfrontio/OpenFrontIO, the en.json file contains difficulty translation keys using the descriptive names ("Relaxed", "Balanced", "Intense", "Impossible") rather than the enum values ("Easy", "Medium", "Hard", "Impossible"). The DifficultyDescription mapping in src/client/components/Difficulties.ts correctly maps enum values to these descriptive translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-05-30T03:53:52.231Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#949
File: resources/lang/en.json:8-10
Timestamp: 2025-05-30T03:53:52.231Z
Learning: For the OpenFrontIO project, do not suggest updating translation files in resources/lang/*.json except for en.json. The project has a dedicated translation team that handles all other locale files.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-27T08:12:19.610Z
Learnt from: mokizzz
PR: openfrontio/OpenFrontIO#1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T08:41:35.101Z
Learnt from: Aotumuri
PR: openfrontio/OpenFrontIO#1357
File: resources/lang/de.json:523-540
Timestamp: 2025-07-12T08:41:35.101Z
Learning: In OpenFrontIO project localization files, always check the en.json source file before flagging potential spelling errors in other language files, as some keys may intentionally use non-standard spellings that need to be consistent across all translations.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-19T11:00:55.422Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#1864
File: resources/maps/arabianpeninsula/manifest.json:13-170
Timestamp: 2025-08-19T11:00:55.422Z
Learning: In OpenFrontIO, nation names in map manifests are displayed directly in the UI without translation. They do not need to be added to resources/lang/en.json or processed through translateText(). This is the established pattern across all existing maps including Europe, World, Asia, Africa, and others.

Applied to files:

  • resources/lang/en.json
🧬 Code graph analysis (6)
src/client/utilities/RenderUnitTypeOptions.ts (1)
src/client/HostLobbyModal.ts (1)
  • toggleUnit (606-615)
src/client/components/shared/PresetsManager.ts (4)
src/client/HostLobbyModal.ts (1)
  • customElement (79-749)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-489)
src/client/components/shared/PresetsBar.ts (1)
  • customElement (7-114)
src/core/Util.ts (1)
  • generateID (245-251)
src/client/components/shared/MapBrowserPane.ts (2)
src/core/game/Game.ts (1)
  • mapCategories (103-141)
src/client/components/Maps.ts (1)
  • customElement (42-146)
src/client/components/shared/AdvancedOptions.ts (4)
src/client/HostLobbyModal.ts (1)
  • customElement (79-749)
src/client/SinglePlayerModal.ts (1)
  • customElement (54-489)
src/client/utilities/RenderRulesOptions.ts (1)
  • renderRulesOptions (25-91)
src/client/utilities/RenderUnitTypeOptions.ts (1)
  • renderUnitTypeOptions (23-88)
src/client/HostLobbyModal.ts (4)
src/core/Schemas.ts (2)
  • TeamCountConfig (153-153)
  • ClientInfo (131-134)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-201)
src/client/Utils.ts (1)
  • translateText (92-147)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (28-45)
src/client/SinglePlayerModal.ts (5)
src/client/HostLobbyModal.ts (1)
  • customElement (79-749)
src/client/components/shared/PresetsManager.ts (1)
  • customElement (16-190)
src/client/components/shared/AdvancedOptions.ts (1)
  • customElement (18-140)
src/client/components/shared/MapBrowserPane.ts (1)
  • customElement (8-170)
src/client/utilities/RenderRulesOptions.ts (1)
  • RuleKey (4-9)
🔇 Additional comments (4)
src/client/HostLobbyModal.ts (3)

516-534: Good defensive polling initialization.

Deferring setInterval until after the lobby is created (line 520) prevents unnecessary API calls with an empty lobbyId. The guard aligns well with the checks in putGameConfig (line 575) and pollPlayers (line 698).


617-626: LGTM: Smart debouncing via event type.

Only calling putGameConfig on "change" events (line 624) avoids flooding the server with updates during slider drag. This pairs well with the debounceMs prop in the bots-slider component.


220-225: Translation keys are present and correctly implemented.

All translation keys used in the code (host_modal.hide_id, host_modal.show_id, host_modal.copy_invite, common.copied) exist in resources/lang/en.json. The code will display proper translated strings instead of raw keys. No changes needed.

src/client/SinglePlayerModal.ts (1)

59-70: Verify @Property vs @State usage for modal configuration.

Lines 59-70 use @property decorator, which creates public, reflected attributes. This differs from HostLobbyModal (lines 81-92) which uses @state for the same fields.

@property makes these accessible externally as HTML attributes/properties. If external code shouldn't modify these settings, use @state instead to keep them internal. If they're meant to be configurable from outside, @property is correct but document this intent.

@evanpelle
Copy link
Collaborator

I like the new UI. I can't seem to scroll past the maps on mobile.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 30, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/client/components/Maps.ts (1)

114-138: Guarantee a real alt string (avoid "null")
When both this.translation and this.mapName are absent, ${this.translation || this.mapName} evaluates to null, so the attribute literal becomes alt="null". That’s confusing for screen-reader users. Please fall back to translateText("common.untitled") (or an empty string if you want it decorative) so the alt text is always meaningful.

-                alt="${this.translation || this.mapName}"
+                alt="${this.translation ??
+                this.mapName ??
+                translateText("common.untitled")}"
src/client/components/shared/PresetsManager.ts (1)

25-35: Past review comment addressed.

The early warning for missing storageKey has been implemented as suggested in the previous review. The guard correctly warns and returns early when storageKey is missing.

src/client/HostLobbyModal.ts (2)

189-194: Past review comment on fallback remains relevant.

The code at line 193 still calls translateText("host_modal.generating") without checking if the key exists. As noted in the previous review, translateText returns the key itself when the translation is missing, so a hardcoded fallback would never apply.

If the key "host_modal.generating" is missing from translation files, users will see the raw key string instead of "Generating…".


624-631: Past review comment addressed.

The random map selection now correctly filters to numeric enum values only, preventing invalid string values from being selected.

🧹 Nitpick comments (2)
src/client/components/shared/PresetsManager.ts (2)

101-130: Consider re-sorting after save for immediate UX consistency.

After saving a new preset, it's appended to the array (line 127). On the next page load, loadFromStorage sorts by updatedAt (most recent first), but within the current session the new preset appears at the end of the dropdown.

To improve UX, consider sorting immediately after adding:

 this.presets = [...this.presets, preset];
+this.presets.sort((a, b) => b.updatedAt - a.updatedAt);
 this.selectedId = preset.id;
 this.persistToStorage();

132-155: Consider re-sorting after update for immediate UX consistency.

Similar to the save flow, updating a preset changes its updatedAt timestamp but doesn't re-sort the array in the current session. The preset won't move to the top of the list until the next page load.

Apply the same sort after updating:

 this.presets = [
   ...this.presets.slice(0, i),
   updated,
   ...this.presets.slice(i + 1),
 ];
+this.presets.sort((a, b) => b.updatedAt - a.updatedAt);
 this.nameInput = updated.name;
 this.persistToStorage();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44b099e and 7b4f626.

📒 Files selected for processing (10)
  • resources/lang/en.json (7 hunks)
  • src/client/HostLobbyModal.ts (10 hunks)
  • src/client/SinglePlayerModal.ts (4 hunks)
  • src/client/components/Maps.ts (3 hunks)
  • src/client/components/shared/BotsSlider.ts (1 hunks)
  • src/client/components/shared/GameModePicker.ts (1 hunks)
  • src/client/components/shared/MapBrowserPane.ts (1 hunks)
  • src/client/components/shared/PresetsBar.ts (1 hunks)
  • src/client/components/shared/PresetsManager.ts (1 hunks)
  • src/client/types/preset.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/client/components/shared/BotsSlider.ts
  • src/client/components/shared/PresetsBar.ts
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/components/shared/MapBrowserPane.ts
  • src/client/components/shared/GameModePicker.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
PR: openfrontio/OpenFrontIO#1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • src/client/HostLobbyModal.ts
  • resources/lang/en.json
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
PR: openfrontio/OpenFrontIO#2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-21T21:42:45.277Z
Learnt from: plazmaezio
PR: openfrontio/OpenFrontIO#1883
File: src/client/HostLobbyModal.ts:227-245
Timestamp: 2025-08-21T21:42:45.277Z
Learning: In openfrontio/OpenFrontIO, the en.json file contains difficulty translation keys using the descriptive names ("Relaxed", "Balanced", "Intense", "Impossible") rather than the enum values ("Easy", "Medium", "Hard", "Impossible"). The DifficultyDescription mapping in src/client/components/Difficulties.ts correctly maps enum values to these descriptive translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-27T08:12:19.610Z
Learnt from: mokizzz
PR: openfrontio/OpenFrontIO#1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-05-30T03:53:52.231Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#949
File: resources/lang/en.json:8-10
Timestamp: 2025-05-30T03:53:52.231Z
Learning: For the OpenFrontIO project, do not suggest updating translation files in resources/lang/*.json except for en.json. The project has a dedicated translation team that handles all other locale files.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-19T11:00:55.422Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#1864
File: resources/maps/arabianpeninsula/manifest.json:13-170
Timestamp: 2025-08-19T11:00:55.422Z
Learning: In OpenFrontIO, nation names in map manifests are displayed directly in the UI without translation. They do not need to be added to resources/lang/en.json or processed through translateText(). This is the established pattern across all existing maps including Europe, World, Asia, Africa, and others.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-06-02T14:27:23.893Z
Learnt from: andrewNiziolek
PR: openfrontio/OpenFrontIO#1007
File: resources/lang/he.json:138-138
Timestamp: 2025-06-02T14:27:23.893Z
Learning: andrewNiziolek's project uses community translation for internationalization. When updating map names or other user-facing text, they update the keys programmatically but wait for community translators to provide accurate translations in each language rather than using machine translations.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T08:41:35.101Z
Learnt from: Aotumuri
PR: openfrontio/OpenFrontIO#1357
File: resources/lang/de.json:523-540
Timestamp: 2025-07-12T08:41:35.101Z
Learning: In OpenFrontIO project localization files, always check the en.json source file before flagging potential spelling errors in other language files, as some keys may intentionally use non-standard spellings that need to be consistent across all translations.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T06:35:10.457Z
Learnt from: Aotumuri
PR: openfrontio/OpenFrontIO#1357
File: resources/lang/ja.json:0-0
Timestamp: 2025-07-12T06:35:10.457Z
Learning: In OpenFrontIO project, "giantworldmap" is the correct localization key name for the giant world map, used consistently across all language files and TypeScript code. Do not suggest renaming this key.

Applied to files:

  • resources/lang/en.json
🧬 Code graph analysis (6)
src/client/components/shared/MapBrowserPane.ts (4)
src/core/game/Game.ts (1)
  • mapCategories (103-141)
src/client/HostLobbyModal.ts (1)
  • customElement (74-753)
src/client/SinglePlayerModal.ts (1)
  • customElement (52-490)
src/client/components/Maps.ts (1)
  • customElement (42-144)
src/client/components/shared/GameModePicker.ts (4)
src/client/HostLobbyModal.ts (1)
  • customElement (74-753)
src/client/SinglePlayerModal.ts (1)
  • customElement (52-490)
src/client/components/shared/GameModeControls.ts (1)
  • customElement (6-34)
src/client/components/shared/SettingsSummary.ts (1)
  • customElement (6-67)
src/client/HostLobbyModal.ts (5)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/core/Schemas.ts (2)
  • TeamCountConfig (153-153)
  • ClientInfo (131-134)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-201)
src/client/Utils.ts (1)
  • translateText (92-147)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (28-45)
src/client/components/shared/PresetsManager.ts (4)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/HostLobbyModal.ts (1)
  • customElement (74-753)
src/client/SinglePlayerModal.ts (1)
  • customElement (52-490)
src/client/components/shared/PresetsBar.ts (1)
  • customElement (7-114)
src/client/components/Maps.ts (2)
src/client/TerrainMapFileLoader.ts (1)
  • terrainMapFileLoader (4-4)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/SinglePlayerModal.ts (9)
src/core/Schemas.ts (1)
  • TeamCountConfig (153-153)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/components/shared/BotsSlider.ts (1)
  • customElement (6-160)
src/client/components/shared/MapBrowserPane.ts (1)
  • customElement (8-175)
src/client/components/shared/PresetsBar.ts (1)
  • customElement (7-114)
src/client/components/shared/PresetsManager.ts (1)
  • customElement (11-185)
src/client/components/shared/AdvancedOptions.ts (1)
  • customElement (18-140)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-201)
src/client/utilities/RenderRulesOptions.ts (1)
  • RuleKey (4-9)
🔇 Additional comments (1)
src/client/components/shared/PresetsManager.ts (1)

37-61: LGTM! Robust validation and error handling.

The storage loading logic correctly validates the preset structure, sorts by most recent, and enforces the limit. Error handling is in place.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
src/client/components/Maps.ts (1)

115-120: Alt fallback still needs fixing (duplicate concern).

The past review comment remains valid. Line 117 uses || which can result in undefined when both values are falsy, while the title at lines 133-139 correctly uses ?? with a translated fallback.

-                alt="${this.translation || this.mapName}"
+                alt="${this.translation ?? this.mapName ?? translateText("common.untitled")}"
src/client/HostLobbyModal.ts (1)

192-196: Fallback for "Generating..." still won't work (duplicate concern).

The past review comment remains valid. When translateText("host_modal.generating") can't find the key, it returns the key string itself, not undefined, so any ?? fallback would never trigger. You need to check if the returned value equals the key.

-    const displayValue = id
-      ? this.lobbyIdVisible
-        ? id
-        : this.mask(id)
-      : translateText("host_modal.generating");
+    const t = translateText("host_modal.generating");
+    const generating = t !== "host_modal.generating" ? t : "Generating…";
+    const displayValue = id
+      ? this.lobbyIdVisible
+        ? id
+        : this.mask(id)
+      : generating;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4f626 and 0e1f645.

📒 Files selected for processing (4)
  • resources/lang/en.json (10 hunks)
  • src/client/HostLobbyModal.ts (10 hunks)
  • src/client/SinglePlayerModal.ts (6 hunks)
  • src/client/components/Maps.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
PR: openfrontio/OpenFrontIO#1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • src/client/HostLobbyModal.ts
  • resources/lang/en.json
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
PR: openfrontio/OpenFrontIO#2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-08-21T21:42:45.277Z
Learnt from: plazmaezio
PR: openfrontio/OpenFrontIO#1883
File: src/client/HostLobbyModal.ts:227-245
Timestamp: 2025-08-21T21:42:45.277Z
Learning: In openfrontio/OpenFrontIO, the en.json file contains difficulty translation keys using the descriptive names ("Relaxed", "Balanced", "Intense", "Impossible") rather than the enum values ("Easy", "Medium", "Hard", "Impossible"). The DifficultyDescription mapping in src/client/components/Difficulties.ts correctly maps enum values to these descriptive translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-27T08:12:19.610Z
Learnt from: mokizzz
PR: openfrontio/OpenFrontIO#1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-05-30T03:53:52.231Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#949
File: resources/lang/en.json:8-10
Timestamp: 2025-05-30T03:53:52.231Z
Learning: For the OpenFrontIO project, do not suggest updating translation files in resources/lang/*.json except for en.json. The project has a dedicated translation team that handles all other locale files.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-06-02T14:27:23.893Z
Learnt from: andrewNiziolek
PR: openfrontio/OpenFrontIO#1007
File: resources/lang/he.json:138-138
Timestamp: 2025-06-02T14:27:23.893Z
Learning: andrewNiziolek's project uses community translation for internationalization. When updating map names or other user-facing text, they update the keys programmatically but wait for community translators to provide accurate translations in each language rather than using machine translations.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-19T11:00:55.422Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#1864
File: resources/maps/arabianpeninsula/manifest.json:13-170
Timestamp: 2025-08-19T11:00:55.422Z
Learning: In OpenFrontIO, nation names in map manifests are displayed directly in the UI without translation. They do not need to be added to resources/lang/en.json or processed through translateText(). This is the established pattern across all existing maps including Europe, World, Asia, Africa, and others.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T08:41:35.101Z
Learnt from: Aotumuri
PR: openfrontio/OpenFrontIO#1357
File: resources/lang/de.json:523-540
Timestamp: 2025-07-12T08:41:35.101Z
Learning: In OpenFrontIO project localization files, always check the en.json source file before flagging potential spelling errors in other language files, as some keys may intentionally use non-standard spellings that need to be consistent across all translations.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T06:35:10.457Z
Learnt from: Aotumuri
PR: openfrontio/OpenFrontIO#1357
File: resources/lang/ja.json:0-0
Timestamp: 2025-07-12T06:35:10.457Z
Learning: In OpenFrontIO project, "giantworldmap" is the correct localization key name for the giant world map, used consistently across all language files and TypeScript code. Do not suggest renaming this key.

Applied to files:

  • resources/lang/en.json
🧬 Code graph analysis (3)
src/client/components/Maps.ts (2)
src/client/TerrainMapFileLoader.ts (1)
  • terrainMapFileLoader (4-4)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/HostLobbyModal.ts (4)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/core/Schemas.ts (2)
  • TeamCountConfig (155-155)
  • ClientInfo (132-135)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-201)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (28-45)
src/client/SinglePlayerModal.ts (3)
src/core/Schemas.ts (1)
  • TeamCountConfig (155-155)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/utilities/RenderRulesOptions.ts (1)
  • RuleKey (4-9)
🪛 Biome (2.1.2)
resources/lang/en.json

[error] 283-283: The key max_timer was already declared.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

🪛 GitHub Actions: 🧪 CI
src/client/HostLobbyModal.ts

[warning] 1-1: Code style issues found in 2 files. Run 'prettier --write' to fix code style issues in this file.

src/client/SinglePlayerModal.ts

[warning] 1-1: Code style issues found in 2 files. Run 'prettier --write' to fix code style issues in this file.

resources/lang/en.json

[error] 231-231: Prettier: SyntaxError: Unexpected token, expected "," (231:5).

🔇 Additional comments (35)
src/client/components/Maps.ts (4)

52-53: Good race condition prevention.

The load token pattern correctly guards against stale async results when mapKey changes rapidly.


59-63: LGTM!

The updated lifecycle correctly reloads when mapKey changes.


65-67: LGTM!

Light DOM rendering aligns with the component-driven refactor across the PR.


72-98: Solid async load flow with race protection.

The token-based guard at line 89 correctly prevents stale results, and the case-insensitive normalization with error throwing is a good safety measure.

src/client/HostLobbyModal.ts (12)

38-75: Clean type definitions.

The preset types and allowed rule keys are well-structured with good type safety.


116-158: Well-structured settings management.

The currentSettings/applySettings pattern is clean. Array spreading for disabledUnits prevents mutation issues.


167-179: LGTM!

Component integration follows the new pattern consistently.


214-273: Translation keys now properly used.

The hardcoded strings from the past review have been replaced with translateText() calls. Ensure the corresponding keys exist in en.json (host_modal.hide_id, host_modal.show_id, host_modal.copy_invite, common.copied).


308-426: Component composition looks solid.

The settings pane integrates multiple shared components cleanly with proper event handling and accessibility attributes.


508-545: Open flow is well-structured.

The polling guard at line 519 (??=) prevents duplicate intervals, and error handling is present.


573-612: LGTM!

The guard at line 574 prevents invalid requests, and the HumansVsNations special case follows the established pattern.


625-634: Solid event handling.

The dual event type handling with clamping is correct and matches the pattern in SinglePlayerModal.


636-643: Random map selection fixed.

The past review concern has been addressed. The numeric filter at lines 637-639 correctly prevents picking invalid string enum values.


666-682: LGTM!

Reset to defaults is straightforward and correctly calls putGameConfig.


684-711: Clean backdrop and clipboard handling.

The backdrop click check and clipboard copy flow are implemented correctly.


728-753: Type-safe rule flag setting.

The explicit switch is safer than dynamic property access and ensures all rule keys are handled.

resources/lang/en.json (8)

8-31: Common translations properly added.

The past review concern about common.no_results has been addressed. All new common keys (no_results, search, copied, etc.) support the new UI components.


32-43: LGTM!

The game_options section properly supports the AdvancedOptions component.


158-161: LGTM!

Settings summary keys support the new SettingsSummary component.


178-183: LGTM!

Single player modal keys properly support the presets UI.


238-241: LGTM!

Map component translations support error and loading states.


257-261: LGTM!

Team mode translations are concise and clear.


298-301: Translation keys properly added (past concern resolved).

The past review concern about missing host_modal keys (generating, hide_id, show_id, copy_invite) has been addressed.


331-339: Presets translations complete (past concern resolved).

The past review concern about missing presets.select_placeholder and other preset keys has been addressed.

src/client/SinglePlayerModal.ts (11)

35-53: Clean preset types.

The type definitions follow the same pattern as HostLobbyModal, maintaining consistency.


57-68: LGTM!

Using @property instead of @state allows parent components to configure the modal externally.


93-123: Settings management is solid.

The currentSettings/applySettings pattern matches HostLobbyModal with proper array spreading.


125-267: Component composition is clean.

The render helpers properly integrate shared components with good event handling patterns.


268-301: LGTM!

Main render structure matches HostLobbyModal with proper presets integration.


332-339: Random map selection is correct.

The numeric filter prevents picking invalid string enum values, matching the pattern in HostLobbyModal.


341-348: Unit toggle logic is sound.

The toggle correctly manages the disabled units array with duplicate prevention.


350-358: Bots event handling is correct.

The pattern matches HostLobbyModal but appropriately omits the server call for single player.


360-379: Type-safe rule flag setting.

The explicit switch is safer than dynamic property access. Note the absence of donate rules is correct for single player.


395-479: Start game flow is complete.

The config assembly with HumansVsNations override (lines 463-470) follows the established pattern. The duplicate disableNPCs at lines 452 and 469 is intentional for the override.


481-501: LGTM!

Reset and backdrop handling are straightforward and correct.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 30, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/client/components/shared/GameModePicker.ts (1)

24-51: LGTM! Enum iteration fix successfully applied.

The previous issue with Object.keys(GameMode) has been correctly resolved. The code now filters to numeric enum values only, preventing spurious buttons and ensuring numeric mode values are dispatched.

Optional: Add explicit button type to prevent implicit form submission.

For safety, consider adding type="button" to prevent these buttons from implicitly submitting forms if this component is ever placed inside a form context:

           return html` <button
+            type="button"
             class=${`h-10 px-4 transition-colors ${
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e1f645 and 829d748.

📒 Files selected for processing (7)
  • resources/lang/en.json (10 hunks)
  • src/client/HostLobbyModal.ts (10 hunks)
  • src/client/SinglePlayerModal.ts (4 hunks)
  • src/client/components/Maps.ts (3 hunks)
  • src/client/components/shared/GameModePicker.ts (1 hunks)
  • src/client/components/shared/MapBrowserPane.ts (1 hunks)
  • src/client/components/shared/PresetsManager.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/components/shared/MapBrowserPane.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
PR: openfrontio/OpenFrontIO#2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-06-10T09:56:44.473Z
Learnt from: Ble4Ch
PR: openfrontio/OpenFrontIO#1063
File: src/core/configuration/PastelThemeDark.ts:53-53
Timestamp: 2025-06-10T09:56:44.473Z
Learning: In ColorAllocator class in src/core/configuration/Colors.ts, the correct method names are assignColor(id: string): Colord for general color assignment and assignTeamColor(team: Team): Colord for team-specific colors. There are no assignPlayerColor() or assignBotColor() methods.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
PR: openfrontio/OpenFrontIO#1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • src/client/HostLobbyModal.ts
  • resources/lang/en.json
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-21T21:42:45.277Z
Learnt from: plazmaezio
PR: openfrontio/OpenFrontIO#1883
File: src/client/HostLobbyModal.ts:227-245
Timestamp: 2025-08-21T21:42:45.277Z
Learning: In openfrontio/OpenFrontIO, the en.json file contains difficulty translation keys using the descriptive names ("Relaxed", "Balanced", "Intense", "Impossible") rather than the enum values ("Easy", "Medium", "Hard", "Impossible"). The DifficultyDescription mapping in src/client/components/Difficulties.ts correctly maps enum values to these descriptive translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-05-30T03:53:52.231Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#949
File: resources/lang/en.json:8-10
Timestamp: 2025-05-30T03:53:52.231Z
Learning: For the OpenFrontIO project, do not suggest updating translation files in resources/lang/*.json except for en.json. The project has a dedicated translation team that handles all other locale files.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-19T11:00:55.422Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#1864
File: resources/maps/arabianpeninsula/manifest.json:13-170
Timestamp: 2025-08-19T11:00:55.422Z
Learning: In OpenFrontIO, nation names in map manifests are displayed directly in the UI without translation. They do not need to be added to resources/lang/en.json or processed through translateText(). This is the established pattern across all existing maps including Europe, World, Asia, Africa, and others.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-27T08:12:19.610Z
Learnt from: mokizzz
PR: openfrontio/OpenFrontIO#1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T06:35:10.457Z
Learnt from: Aotumuri
PR: openfrontio/OpenFrontIO#1357
File: resources/lang/ja.json:0-0
Timestamp: 2025-07-12T06:35:10.457Z
Learning: In OpenFrontIO project, "giantworldmap" is the correct localization key name for the giant world map, used consistently across all language files and TypeScript code. Do not suggest renaming this key.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/SinglePlayerModal.ts
🧬 Code graph analysis (6)
src/client/components/shared/GameModePicker.ts (3)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-502)
src/client/components/shared/GameModeControls.ts (1)
  • customElement (6-34)
src/client/components/Maps.ts (2)
src/client/TerrainMapFileLoader.ts (1)
  • terrainMapFileLoader (4-4)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/components/shared/MapBrowserPane.ts (4)
src/core/game/Game.ts (1)
  • mapCategories (108-147)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-502)
src/client/components/Maps.ts (1)
  • customElement (43-147)
src/client/HostLobbyModal.ts (4)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/core/Schemas.ts (2)
  • TeamCountConfig (155-155)
  • ClientInfo (132-135)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-201)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (28-45)
src/client/SinglePlayerModal.ts (2)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/utilities/RenderRulesOptions.ts (1)
  • RuleKey (4-9)
src/client/components/shared/PresetsManager.ts (5)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-502)
src/client/components/shared/PresetsBar.ts (1)
  • customElement (7-114)
src/core/Util.ts (1)
  • generateID (245-251)
🔇 Additional comments (12)
resources/lang/en.json (1)

8-338: LGTM! All previously flagged translation keys have been added.

Verification confirms:

  • common.no_results added (line 30)
  • host_modal.generating added (line 297)
  • host_modal.hide_id, show_id, copy_invite added (lines 298-300)
  • presets.select_placeholder added (line 337)
  • ✅ JSON syntax errors corrected (comma after map.browser)

All translation keys referenced by the new UI components are now present.

src/client/components/Maps.ts (2)

52-98: Excellent race condition handling with load tokens.

The token-based pattern correctly prevents stale async results from overwriting newer data when mapKey changes rapidly. Early returns when myToken !== this._loadToken ensure only the most recent load updates the component state.


117-141: LGTM! Alt text fallback properly implemented.

The three-level fallback chain (translation ?? mapName ?? translateText("common.untitled")) ensures the image always has meaningful alt text. This addresses the previous accessibility concern and provides a safe final fallback via translation.

src/client/components/shared/MapBrowserPane.ts (1)

133-167: LGTM! Accessibility issues resolved.

All previous concerns have been addressed:

  • ✅ Single role="listbox" on outer container (duplicate removed)
  • ✅ Single role="option" per item (no duplication with map-display)
  • ✅ Keyboard navigation implemented (Enter/Space on items)
  • ✅ Proper ARIA states (aria-selected, aria-pressed, aria-multiselectable)
  • ✅ Explicit type="button" on all buttons

The component is now fully keyboard-accessible and screen-reader friendly.

src/client/components/shared/PresetsManager.ts (2)

25-35: LGTM! StorageKey warning properly implemented.

The early guard now provides clear feedback when storageKey is missing, helping integrators catch configuration errors during development. The warning message is descriptive and the early return prevents silent failures.


37-70: Robust localStorage handling with proper validation.

The implementation correctly:

  • Guards against missing storageKey in both load and save paths
  • Validates preset structure on load (id, name, timestamps, settings)
  • Handles JSON parse/stringify errors gracefully
  • Auto-cleans when exceeding limit
  • Maintains sorted order (most recent first)
src/client/HostLobbyModal.ts (3)

117-159: Well-structured preset support with proper type safety.

The currentSettings() and applySettings() methods correctly capture and restore all lobby configuration. The type HostLobbyPreset["settings"] ensures compile-time verification that all settings are included.


637-644: LGTM! Random map selection correctly handles numeric enums.

The filter to typeof v === "number" prevents selecting string keys from the enum. The defensive fallback to [GameMapType.World] ensures a valid map is always returned. This addresses the previous critical issue.


219-273: LGTM! Button labels properly localized.

All previously hardcoded English strings now use translateText():

  • Hide/Show ID buttons
  • Copy invite button
  • "Copied" confirmation

Translation keys are present in en.json (verified in earlier review).

src/client/SinglePlayerModal.ts (3)

94-124: Well-structured preset system matching HostLobbyModal pattern.

The currentSettings() and applySettings() methods maintain all single-player configuration. Type safety via SinglePlayerPreset["settings"] ensures completeness. Implementation is consistent with the host lobby approach.


333-340: LGTM! Consistent random map selection with HostLobbyModal.

Correctly filters numeric enum values and provides a defensive fallback. Matches the pattern used in HostLobbyModal for consistency across the codebase.


342-380: Clean rule and unit toggling logic.

Both toggleUnit and setRuleFlag correctly translate UI events into state updates:

  • checked=true means enabled (remove from disabledUnits)
  • checked=false means disabled (add to disabledUnits)
  • Switch statement exhaustively covers all RuleKey values

Simpler than HostLobbyModal (no server sync) but follows the same semantic patterns.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
src/client/components/shared/DifficultyPicker.ts (1)

27-40: Filter numeric enum values to avoid rendering string keys.

TypeScript numeric enums include both string keys and numeric values in Object.values(). Without filtering, you'll render buttons for string keys like "Easy", "Medium", etc., in addition to the numeric values.

Apply this diff to filter to numeric values only:

       <div class="flex flex-wrap gap-2">
-        ${(Object.values(Difficulty) as Difficulty[]).map(
+        ${(Object.values(Difficulty).filter(
+            (v) => typeof v === "number",
+          ) as Difficulty[]).map(
           (v) => html`

This matches the pattern used in GameModePicker.ts (lines 30-32).

src/client/components/shared/MapBrowserPane.ts (1)

143-161: Verify no duplicate role="option" in map-display.

This wrapper has role="option" and keyboard handling. If <map-display> also renders role="option" internally, screen readers will announce each tile twice.

Check that map-display does not render its own role="option" or aria-selected:

#!/bin/bash
# Search for role="option" in MapDisplay component
rg -nP 'role=.*option' src/client/components/Maps.ts

If found, remove the role/aria from either the wrapper or map-display to keep a single interactive element per option.

src/client/components/shared/GameModePicker.ts (1)

25-29: Filter enum values before rendering buttons.

Object.values(GameMode) returns both named keys ("FFA", "Team") and numeric values (0, 1) for TypeScript numeric enums. Without filtering, extra buttons render with undefined labels, and pick() dispatches string values instead of numbers.

Apply this diff:

-    const modes = Object.values(GameMode) as GameMode[];
+    const modes = Object.values(GameMode).filter(
+      (m): m is GameMode => typeof m === "number",
+    );
🧹 Nitpick comments (6)
src/client/components/shared/TeamCountPicker.ts (1)

11-12: Simplify array property declarations.

For array properties with complex element types, use attribute: false to skip Lit's built-in attribute-to-array conversion and rely on property binding.

Apply this diff:

-  @property({ type: Array }) numbers: TeamCountConfig[] = [2, 3, 4, 5, 6, 7];
-  @property({ type: Array }) named: TeamCountConfig[] = [Duos, Trios, Quads];
+  @property({ attribute: false }) numbers: TeamCountConfig[] = [2, 3, 4, 5, 6, 7];
+  @property({ attribute: false }) named: TeamCountConfig[] = [Duos, Trios, Quads];

This makes the intent clearer: these properties are set via JavaScript, not HTML attributes.

src/client/components/shared/MapBrowserPane.ts (1)

33-36: Optimize key lookup in map filtering.

The nested find() inside the double loop runs for every map. Pre-build a reverse lookup map once to avoid repeated searches.

Add this helper before getFilteredMaps():

private mapValueToKey = new Map(
  (Object.keys(GameMapType) as Array<keyof typeof GameMapType>).map((k) => [
    GameMapType[k],
    k,
  ]),
);

Then simplify the loop:

-        const key = (
-          Object.keys(GameMapType) as Array<keyof typeof GameMapType>
-        ).find((k) => GameMapType[k] === mapValue) as keyof typeof GameMapType;
+        const key = this.mapValueToKey.get(mapValue)!;
src/client/components/shared/PresetsManager.ts (1)

44-46: Consider stricter preset validation.

The filter checks item && item.settings !== undefined but doesn't validate the full preset structure (id, name, timestamps).

Add a type guard for more robust validation:

const isValidPreset = (item: any): item is GenericPreset<T> =>
  item &&
  typeof item.id === "string" &&
  typeof item.name === "string" &&
  typeof item.createdAt === "number" &&
  typeof item.updatedAt === "number" &&
  item.settings !== undefined;

// Then use it:
const arr = parsed.filter(isValidPreset);

This prevents corrupted presets from breaking the UI.

src/client/SinglePlayerModal.ts (3)

2-2: Consider using @State for internal configuration instead of @Property.

The fields like selectedMap, selectedDifficulty, bots, etc. are internal modal state managed by user interactions, not attributes set by a parent component. Using @property unnecessarily exposes them as public attributes and creates reflected attributes in the DOM. Since this modal is self-contained and manages its own configuration, @state would better encapsulate this internal state.

Apply this pattern:

-@property({ type: String }) selectedMap: GameMapType = GameMapType.World;
-@property({ type: String }) selectedDifficulty: Difficulty = Difficulty.Medium;
-@property({ type: Boolean }) disableNPCs = false;
-@property({ type: Number }) bots = 400;
+@state() private selectedMap: GameMapType = GameMapType.World;
+@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
+@state() private disableNPCs = false;
+@state() private bots = 400;

(Apply the same pattern to the other configuration properties.)

Also applies to: 55-70


343-350: Simplify the toggleUnit logic.

The nested ternary is hard to follow. Here's a clearer approach:

 private toggleUnit = (unit: UnitType, checked: boolean): void => {
-  // checked=true means the unit is enabled, so ensure it's NOT in disabledUnits
-  this.disabledUnits = checked
-    ? this.disabledUnits.filter((u) => u !== unit)
-    : this.disabledUnits.includes(unit)
-      ? this.disabledUnits
-      : [...this.disabledUnits, unit];
+  if (checked) {
+    // Unit enabled: remove from disabled list
+    this.disabledUnits = this.disabledUnits.filter((u) => u !== unit);
+  } else if (!this.disabledUnits.includes(unit)) {
+    // Unit disabled: add to disabled list
+    this.disabledUnits = [...this.disabledUnits, unit];
+  }
 };

363-381: Consider using object lookup instead of switch statement.

The switch statement is verbose. An object lookup would be more concise and easier to maintain:

-private setRuleFlag(key: RuleKey, checked: boolean) {
-  switch (key) {
-    case "disableNPCs":
-      this.disableNPCs = checked;
-      break;
-    case "instantBuild":
-      this.instantBuild = checked;
-      break;
-    case "infiniteGold":
-      this.infiniteGold = checked;
-      break;
-    case "infiniteTroops":
-      this.infiniteTroops = checked;
-      break;
-    case "compactMap":
-      this.compactMap = checked;
-      break;
-  }
-}
+private setRuleFlag(key: RuleKey, checked: boolean) {
+  const ruleMap: Record<RuleKey, keyof this> = {
+    disableNPCs: 'disableNPCs',
+    instantBuild: 'instantBuild',
+    infiniteGold: 'infiniteGold',
+    infiniteTroops: 'infiniteTroops',
+    compactMap: 'compactMap',
+  };
+  const prop = ruleMap[key];
+  if (prop && prop in this) {
+    (this as any)[prop] = checked;
+  }
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 829d748 and afb43d6.

📒 Files selected for processing (9)
  • src/client/SinglePlayerModal.ts (4 hunks)
  • src/client/components/shared/DifficultyControls.ts (1 hunks)
  • src/client/components/shared/DifficultyPicker.ts (1 hunks)
  • src/client/components/shared/GameModeControls.ts (1 hunks)
  • src/client/components/shared/GameModePicker.ts (1 hunks)
  • src/client/components/shared/MapBrowserPane.ts (1 hunks)
  • src/client/components/shared/PresetsManager.ts (1 hunks)
  • src/client/components/shared/SettingsSummary.ts (1 hunks)
  • src/client/components/shared/TeamCountPicker.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/client/components/shared/DifficultyControls.ts
  • src/client/components/shared/SettingsSummary.ts
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
📚 Learning: 2025-08-21T21:42:45.277Z
Learnt from: plazmaezio
PR: openfrontio/OpenFrontIO#1883
File: src/client/HostLobbyModal.ts:227-245
Timestamp: 2025-08-21T21:42:45.277Z
Learning: In openfrontio/OpenFrontIO, the en.json file contains difficulty translation keys using the descriptive names ("Relaxed", "Balanced", "Intense", "Impossible") rather than the enum values ("Easy", "Medium", "Hard", "Impossible"). The DifficultyDescription mapping in src/client/components/Difficulties.ts correctly maps enum values to these descriptive translation keys.

Applied to files:

  • src/client/components/shared/DifficultyPicker.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
PR: openfrontio/OpenFrontIO#2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/client/components/shared/DifficultyPicker.ts
  • src/client/components/shared/GameModePicker.ts
  • src/client/components/shared/TeamCountPicker.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/SinglePlayerModal.ts
  • src/client/components/shared/MapBrowserPane.ts
  • src/client/components/shared/GameModeControls.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
PR: openfrontio/OpenFrontIO#2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/components/shared/GameModePicker.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/SinglePlayerModal.ts
🧬 Code graph analysis (7)
src/client/components/shared/DifficultyPicker.ts (1)
src/client/components/shared/DifficultyControls.ts (1)
  • customElement (6-43)
src/client/components/shared/GameModePicker.ts (3)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-503)
src/client/components/shared/GameModeControls.ts (1)
  • customElement (6-34)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
src/client/components/shared/TeamCountPicker.ts (2)
src/core/Schemas.ts (1)
  • TeamCountConfig (155-155)
src/core/game/Game.ts (3)
  • Duos (53-53)
  • Trios (54-54)
  • Quads (55-55)
src/client/components/shared/PresetsManager.ts (5)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-503)
src/client/components/shared/PresetsBar.ts (1)
  • customElement (7-114)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
src/core/Util.ts (1)
  • generateID (245-251)
src/client/SinglePlayerModal.ts (10)
src/core/Schemas.ts (1)
  • TeamCountConfig (155-155)
src/client/types/preset.ts (1)
  • Preset (2-8)
src/client/components/shared/DifficultyControls.ts (1)
  • customElement (6-43)
src/client/components/shared/DifficultyPicker.ts (1)
  • customElement (6-44)
src/client/components/shared/GameModePicker.ts (1)
  • customElement (6-49)
src/client/components/shared/MapBrowserPane.ts (1)
  • customElement (8-171)
src/client/components/shared/PresetsManager.ts (1)
  • customElement (11-181)
src/client/components/shared/AdvancedOptions.ts (1)
  • customElement (18-140)
src/client/FlagInput.ts (1)
  • customElement (9-116)
src/client/utilities/RenderRulesOptions.ts (1)
  • RuleKey (4-9)
src/client/components/shared/MapBrowserPane.ts (4)
src/core/game/Game.ts (1)
  • mapCategories (108-147)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-503)
src/client/components/Maps.ts (1)
  • customElement (43-147)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
src/client/components/shared/GameModeControls.ts (3)
src/client/SinglePlayerModal.ts (1)
  • customElement (53-503)
src/client/components/shared/GameModePicker.ts (1)
  • customElement (6-49)
src/client/HostLobbyModal.ts (1)
  • customElement (75-766)
🔇 Additional comments (8)
src/client/components/shared/PresetsManager.ts (1)

113-124: LGTM!

Clean preset creation logic with proper timestamp tracking, sorting, and persistence. The limit check prevents unbounded growth.

src/client/SinglePlayerModal.ts (7)

33-51: LGTM!

The preset types and constants are well-defined and align with the PresetsManager integration.


95-125: LGTM!

The preset helper methods correctly capture and restore all configuration state, with proper array cloning to prevent mutation.


270-303: LGTM!

The dialog structure follows accessibility best practices with proper ARIA attributes, and the backdrop-click-to-close pattern is correctly implemented. The PresetsManager integration is properly wired.


305-315: LGTM!

The light DOM rendering and direct style manipulation for modal visibility align with codebase patterns.

Based on learnings.


334-341: LGTM!

The random map selection correctly filters numeric enum values with a safe fallback.


352-360: LGTM!

The dual event handling and value clamping correctly manage bot count input from different sources.


257-268: No issues found. Code structure for scrolling is correct.

The CSS properties for overflow and scrolling are properly configured:

  • Main element has flex-1 min-h-0 overflow-auto (correct flex pattern)
  • Header and settings pane use min-h-0 to allow shrinking
  • MapBrowserPane inner list has overflow-auto for scrolling
  • No height constraints block scrolling

The conditional rendering at line 264 does not affect layout. However, on mobile devices with small viewports, space may be tight due to inset-4 margins and min-h-[560px] minimum height on the section. If users report scrolling issues, verify the actual viewport height available after these constraints on target devices, as the code structure itself is sound.


@customElement("difficulty-picker")
export class DifficultyPicker extends LitElement {
@property({ type: String }) value: Difficulty = Difficulty.Medium;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix property decorator type for numeric enum.

The @property({ type: String }) decorator is incorrect for Difficulty, which is a numeric enum. Lit uses the type option for built-in conversion, and String will coerce the numeric value to a string, breaking comparisons.

Apply this diff:

-  @property({ type: String }) value: Difficulty = Difficulty.Medium;
+  @property({ type: Number }) value: Difficulty = Difficulty.Medium;
🤖 Prompt for AI Agents
In src/client/components/shared/DifficultyPicker.ts around line 8, the @property
decorator uses { type: String } for the numeric Difficulty enum which coerces
values to strings and breaks comparisons; change the decorator to use { type:
Number } (or remove the type option to avoid coercion) so the numeric enum value
is preserved and comparisons work correctly.


@customElement("game-mode-controls")
export class GameModeControls extends LitElement {
@property({ type: String }) value: GameMode = GameMode.FFA;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix property decorator type for numeric enum.

GameMode is a numeric enum, but @property({ type: String }) will coerce values to strings, breaking equality checks in the nested game-mode-picker.

Apply this diff:

-  @property({ type: String }) value: GameMode = GameMode.FFA;
+  @property({ type: Number }) value: GameMode = GameMode.FFA;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@property({ type: String }) value: GameMode = GameMode.FFA;
@property({ type: Number }) value: GameMode = GameMode.FFA;
🤖 Prompt for AI Agents
In src/client/components/shared/GameModeControls.ts around line 8, the @property
decorator is using type: String which coerces the numeric GameMode enum to a
string and breaks comparisons; change the decorator to use type: Number so the
value remains a numeric enum (keep the default GameMode.FFA) and ensure any
attribute/serialization behavior aligns with numeric values.


@customElement("game-mode-picker")
export class GameModePicker extends LitElement {
@property({ type: String }) value: GameMode = GameMode.FFA;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix property decorator type for numeric enum.

GameMode is a numeric enum, but @property({ type: String }) will coerce the numeric value to a string, breaking comparisons in line 37.

Apply this diff:

-  @property({ type: String }) value: GameMode = GameMode.FFA;
+  @property({ type: Number }) value: GameMode = GameMode.FFA;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@property({ type: String }) value: GameMode = GameMode.FFA;
@property({ type: Number }) value: GameMode = GameMode.FFA;
🤖 Prompt for AI Agents
In src/client/components/shared/GameModePicker.ts around line 8, the @property
decorator is using type: String for GameMode which is a numeric enum; this
causes the value to be coerced to a string and breaks enum comparisons (e.g.
around line 37). Change the decorator to use type: Number so the property stays
a numeric enum value (keep the default GameMode.FFA) and ensure any
bindings/reads treat it as a number.

@customElement("team-count-picker")
export class TeamCountPicker extends LitElement {
@property({ type: String }) mode: GameMode = GameMode.FFA;
@property({ type: Number }) value: TeamCountConfig = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix property decorator type for union type.

TeamCountConfig is a union type (number | "Duos" | "Trios" | "Quads"), but @property({ type: Number }) will only convert string attributes to numbers, breaking string values like "Duos".

Apply this diff:

-  @property({ type: Number }) value: TeamCountConfig = 2;
+  @property({ attribute: false }) value: TeamCountConfig = 2;

Using attribute: false prevents automatic conversion and accepts the value directly via property binding (.value=${...}), which is how the component is used in SinglePlayerModal.ts and HostLobbyModal.ts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@property({ type: Number }) value: TeamCountConfig = 2;
@property({ attribute: false }) value: TeamCountConfig = 2;
🤖 Prompt for AI Agents
In src/client/components/shared/TeamCountPicker.ts around line 10, the property
decorator uses type: Number which forces attribute-to-property conversion and
breaks string union values like "Duos"/"Trios"/"Quads"; change the decorator to
use attribute: false so the property is assigned directly via JS property
binding (e.g. .value=...) and will accept the union type TeamCountConfig as-is
while keeping the default value of 2.

@TheGiraffe3 TheGiraffe3 added the UI/UX UI/UX changes including assets, menus, QoL, etc. label Oct 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants