Skip to content

Conversation

@evanpelle
Copy link
Collaborator

@evanpelle evanpelle commented Jan 14, 2026

Description:

  • Better support for crazygame links and username
  • Hide all components that require login until we implement crazy games accounts
  • Move all matchmaking button related logic into Matchmaking.ts so the button can be hidden on crazygames.

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:

evan

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Walkthrough

This PR integrates CrazyGamesSDK for user authentication, advertising, and game invitations; refactors boat attack intents with simplified constructors; restructures conquests tracking from single values to per-player-type arrays; migrates ad rendering from Fuse to Playwire Ramp; and expands localization across multiple languages while updating game ranking categories and transport ship execution logic.

Changes

Cohort / File(s) Summary
CrazyGamesSDK Core
src/client/CrazyGamesSDK.ts
Introduces user API (getUser, addAuthListener), ad API (requestMidgameAd), and invite link generation. Adds readiness mechanism with ready() promise race and timeout. Public methods gate on readiness.
Client Initialization & Integration
src/client/Main.ts, src/client/SinglePlayerModal.ts, src/client/UsernameInput.ts, src/client/PatternInput.ts
Integrates CrazyGamesSDK for username fetching, auth listeners, instant multiplayer checks, and ad requests. Exports genAnonUsername() function. Hides CrazyGames-incompatible UI elements.
Boat Attack Intent Refactor
src/client/ClientGameRunner.ts, src/client/Transport.ts, src/client/graphics/layers/PlayerActionHandler.ts, src/core/Schemas.ts, src/core/execution/ExecutionManager.ts
Simplifies SendBoatAttackIntentEvent constructor from (targetID, dst, troops, src) to (dst, troops). Removes targetID and src parameters across event and execution chain.
Transport Ship Dynamic Targeting
src/core/execution/TransportShipExecution.ts, src/core/execution/utils/AiAttackBehavior.ts, tests/Attack.test.ts, tests/Disconnected.test.ts
Refactors constructor from (player, targetID, ref, startTroops, src) to (player, ref, troops). Introduces dynamic target resolution, pathFinder, and source tile derivation from canBuild results. Updates AI boat attack strategy with max-boat check and DonateTroopsExecution integration.
Conquests Array Restructuring
src/core/StatsSchemas.ts, src/core/game/StatsImpl.ts, src/client/components/baseComponents/ranking/GameInfoRanking.ts, src/client/components/baseComponents/stats/PlayerStatsTree.ts, tests/GameInfoRanking.test.ts, tests/Stats.test.ts
Changes conquests from single bigint to bigint[] array. Splits RankType.Conquests into ConquestHumans and ConquestBots with new player-type indices (PLAYER_INDEX_HUMAN, PLAYER_INDEX_NATION, PLAYER_INDEX_BOT). Updates merge and clone logic for array handling.
Ranking UI Components
src/client/components/baseComponents/ranking/PlayerRow.ts, src/client/components/baseComponents/ranking/RankingControls.ts, src/client/components/baseComponents/ranking/RankingHeader.ts
Updates switch cases from RankType.Conquests to ConquestHumans and ConquestBots. Refactors RankingHeader to render two-button layout for conquest ranks. Introduces isConquestRanking helper and conquest-specific ranking set.
Ad System Migration (Fuse to Ramp)
src/client/GutterAds.ts, src/client/graphics/GameRenderer.ts, src/client/graphics/layers/AdTimer.ts, src/client/graphics/layers/InGameHeaderAd.ts, index.html
Removes AdTimer class. Introduces InGameHeaderAd web component with Playwire Ramp integration, viewport checks, ad loading queue, and auto-hide after threshold. Replaces Fuse script with Ramp initialization in HTML.
Gameplay & UI Integration
src/client/graphics/layers/GameRightSidebar.ts, src/client/graphics/layers/SettingsModal.ts, src/client/graphics/layers/WinModal.ts, src/client/graphics/layers/MainRadialMenu.ts, src/client/graphics/layers/StructureIconsLayer.ts
Integrates crazyGamesSDK.gameplayStop/Start calls into pause/resume/exit flows. Adds disconnected-target filtering to center button and nuke targeting logic. Introduces ShowSettingsModalEvent with pause coordination.
Navigation & Matchmaking
src/client/Matchmaking.ts, src/client/PlayPage.ts, src/client/HostLobbyModal.ts, src/client/components/DesktopNavBar.ts, src/client/components/PatternButton.ts
Replaces dual login buttons with single component. Adds login-state awareness with handleLoggedInClick/handleLoggedOutClick. Adds no-crazygames class to store/account buttons. In HostLobbyModal, uses createInviteLink when on CrazyGames.
Copy Button & Cosmetics
src/client/components/CopyButton.ts, src/client/Cosmetics.ts
Extends resolveCopyText() to return CrazyGames invite link. Introduces module-level cosmetics cache to prevent repeated network requests.
Localization Expansion
resources/lang/bg.json, resources/lang/en.json, resources/lang/ja.json, resources/lang/nl.json, resources/lang/ru.json, resources/lang/tr.json, resources/lang/uk.json, resources/lang/zh-CN.json, resources/lang/fr.json, resources/lang/id.json, resources/lang/metadata.json
Adds UI keys (sign_in, discord_avatar_alt, user_avatar_alt), game labels (game_info, settings), new event messages, build menu modifiers, and map regions across all language files. Adds complete Indonesian localization (id.json). Renames solo, options→settings, updates account modal flow.
Game Core & Stats
src/core/game/Game.ts, src/core/game/GameImpl.ts, src/core/game/FetchGameMapLoader.ts, src/core/execution/nation/NationAllianceBehavior.ts, src/core/execution/nation/NationEmojiBehavior.ts
Adds public getWinner() method and _winner field to track game winner. Adds constructor timing instrumentation. Updates NationAllianceBehavior to use probabilistic difficulty responses. Refactors NationEmojiBehavior to use game.getWinner() instead of tile-count logic.
Server & Pathfinding
src/server/MapPlaylist.ts, src/core/pathfinding/algorithms/AStar.WaterHierarchical.ts, src/core/pathfinding/transformers/SmoothingWaterTransformer.ts, src/server/Worker.ts
Updates MapPlaylist with TEAM_WEIGHTS weighted selection and Didier map. Sets disableNations=true and increases spawnImmunity for 1v1. Increases game start delay from 5s to 7s. Updates A* short-path bounding from 140 to 260. Refactors LOS smoothing with dual-pass magnitude thresholds.
Utility & Testing
src/core/worker/WorkerClient.ts, src/client/LangSelector.ts, src/client/graphics/layers/DynamicUILayer.ts, src/client/graphics/layers/RadialMenuElements.ts, src/client/NewsModal.ts, tests/InputHandler.test.ts, src/core/execution/SpawnExecution.ts
Extends WorkerClient timeout from 5s to 20s. Adds matchmaking-button to LangSelector. Inlines isOwnedByPlayer logic in DynamicUILayer. Adds isDisconnectedTarget filtering and random-spawn disable check to RadialMenuElements. Adds ul margin styling to NewsModal. Adds spawn re-roll guard and test cleanup.
CSS & Responsive
src/client/graphics/layers/UnitDisplay.ts
Replaces fixed breakpoint utilities (lg:, xl:, 2xl:) with min-width utilities (min-[1200px]:). Updates z-index from z-1100 to z-[1100] syntax.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CrazyGamesSDK
    participant CrazyGames External
    participant GameEngine
    participant AdSystem

    Note over Client,AdSystem: CrazyGames + Ad System Integration Flow

    Client->>CrazyGamesSDK: new CrazyGamesSDK()
    CrazyGamesSDK->>CrazyGamesSDK: Initialize readyPromise
    
    Client->>CrazyGamesSDK: ready()
    CrazyGamesSDK->>CrazyGamesSDK: Race with 3s timeout
    CrazyGamesSDK-->>Client: Promise<boolean>
    
    alt On CrazyGames Platform
        Client->>CrazyGamesSDK: getUsername()
        CrazyGamesSDK->>CrazyGames External: window.CrazyGames.SDK.user.getUser()
        CrazyGames External-->>CrazyGamesSDK: {username} | null
        CrazyGamesSDK-->>Client: Promise<string | null>
        
        Client->>CrazyGamesSDK: addAuthListener(callback)
        CrazyGames External-->>Client: Auth state changes
        Client->>GameEngine: Update player name
    end
    
    GameEngine->>GameEngine: Game tick loop
    GameEngine->>Client: Request ad during gameplay
    Client->>CrazyGamesSDK: requestMidgameAd()
    CrazyGamesSDK->>CrazyGames External: window.CrazyGames.SDK.ad.requestAd()
    CrazyGames External-->>Client: adStarted callback
    Client->>AdSystem: Pause game, show ad
    CrazyGames External-->>Client: adFinished callback
    Client->>AdSystem: Resume game
    
    alt Instant Multiplayer
        Client->>CrazyGamesSDK: isInstantMultiplayer()
        CrazyGamesSDK-->>Client: Promise<boolean>
        alt true
            Client->>GameEngine: Open host modal
            Client->>CrazyGamesSDK: createInviteLink(gameId)
            CrazyGamesSDK->>CrazyGames External: window.CrazyGames.SDK.game.inviteLink()
            CrazyGames External-->>CrazyGamesSDK: string
            CrazyGamesSDK-->>Client: string | null
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Pathfinding refinements #2959: Shares the transport ship refactoring and boat attack simplification, including SendBoatAttackIntentEvent constructor changes and A\* pathfinding tuning.
  • mls (v4.9) #2487: Modifies shared localization files (bg.json, etc.) with similar translation key additions and restructuring.
  • mls (v4.13) #2907: Overlaps on localization file updates and adds similar UI label keys across multiple language files.

Suggested reviewers

  • Duwibi

Poem

🎮 CrazyGames Unite!

The SDK springs to life, with auth and invite link,
Boats sail simpler now, from destination and troops so sleek,
Conquests split in two—humans rise, bots compete,
Ads load smooth via Ramp, while rankings dance complete,
A dozen languages dance, one game made whole! 🌍

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: improving CrazyGames integration support. It is concise, specific, and directly relates to the primary objective of the changeset.
Description check ✅ Passed The description is directly related to the changeset. It clearly explains the three main objectives: better CrazyGames link/username support, hiding login-required components, and moving matchmaking logic. All claims are substantiated by the file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/UsernameInput.ts (1)

165-178: Several issues with async username fetch pattern.

  1. Dead code: Line 171 return null; inside .then() does nothing useful.
  2. Missing error handling: If crazyGamesSDK.getUsername() rejects, the error is unhandled. Add .catch().
  3. Bypasses parsing: Setting baseUsername directly skips parseAndSetUsername(), so SDK usernames with clan tags won't be parsed correctly.
  4. Not persisted: SDK username is not stored to localStorage, so it won't survive page refresh.
Proposed fix
 private getUsername(): string {
-  crazyGamesSDK.getUsername().then((username) => {
-    if (username) {
-      this.baseUsername = username;
-      this.requestUpdate();
-    }
-    return null;
-  });
+  crazyGamesSDK
+    .getUsername()
+    .then((username) => {
+      if (username) {
+        this.parseAndSetUsername(username);
+        this.storeUsername(this.constructFullUsername());
+        this.requestUpdate();
+      }
+    })
+    .catch((error) => {
+      console.error("Failed to get CrazyGames username:", error);
+    });
   const storedUsername = localStorage.getItem(usernameKey);
   if (storedUsername) {
     return storedUsername;
   }
   return this.generateNewUsername();
 }
🤖 Fix all issues with AI agents
In `@src/client/CrazyGamesSDK.ts`:
- Around line 115-121: The getUsername method calls
window.CrazyGames!.SDK.user.getUser() without protecting against exceptions;
wrap the SDK call in a try-catch inside getUsername so any thrown error is
caught, log or handle the error consistently with other methods (e.g., use
existing logger or error handling pattern in this class), and return null on
failure; specifically update the async getUsername() implementation to await the
SDK call inside a try block and return the username or null, and return null
from the catch block after logging the error.
- Around line 123-129: The isInstantMultiplayer method currently accesses
window.CrazyGames!.SDK.game.isInstantMultiplayer without error handling; update
isInstantMultiplayer to keep the initial await this.ready() call but wrap the
SDK property access in a try-catch (similar to other SDK methods and the
getUsername pattern), returning false if an exception is thrown and optionally
logging the error; reference the isInstantMultiplayer function and this.ready()
when making the change.

In `@src/client/Main.ts`:
- Around line 645-652: The instant-multiplayer path calls
crazyGamesSDK.isInstantMultiplayer() and directly opens the host modal via
this.hostModal.open() without performing the same username validation used by
the hostLobbyButton handler; update the instant-multiplayer branch to run the
same validation logic (reuse the hostLobbyButton's validation routine or the
exact check it uses) and only call this.hostModal.open() when the username check
passes, otherwise abort and surface the same validation error/feedback as
hostLobbyButton.

In `@startup.sh`:
- Around line 88-92: The if/else in startup.sh around the DOMAIN/SUBDOMAIN check
is redundant because both branches call exec /usr/bin/supervisord -c
/etc/supervisor/conf.d/supervisord.conf; remove the entire if [...] fi block and
replace it with a single direct exec /usr/bin/supervisord -c
/etc/supervisor/conf.d/supervisord.conf line (or, if there was intended
different behavior, restore the original differing command in the branch that
should differ instead of duplicating the same exec).
🧹 Nitpick comments (2)
src/client/graphics/layers/UnitDisplay.ts (1)

133-133: Redundant breakpoint classes can be simplified.

The CSS mixes arbitrary breakpoint min-[1200px]:* with standard Tailwind breakpoints xl:*. Since xl is 1280px and min-[1200px] is 1200px, the xl:flex-col and xl:gap-2 classes are redundant—they're already covered by min-[1200px]:flex-col and min-[1200px]:gap-2.

Consider removing the xl:* classes to keep the responsive logic clear and consistent:

♻️ Suggested cleanup
-        class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col min-[1200px]:flex-col 2xl:gap-5 xl:gap-2 min-[1200px]:gap-2 justify-center items-center"
+        class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row min-[1200px]:flex-col 2xl:gap-5 min-[1200px]:gap-2 justify-center items-center"
src/client/graphics/layers/SettingsModal.ts (1)

3-4: Consider using relative imports for consistency.

Other files in the codebase use relative paths (e.g., ../../CrazyGamesSDK) for these imports. Using absolute-style paths like src/client/... might work depending on build config, but mixing styles can cause confusion.

🔧 Suggested change
-import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
-import { PauseGameIntentEvent } from "src/client/Transport";
+import { crazyGamesSDK } from "../../CrazyGamesSDK";
+import { PauseGameIntentEvent } from "../../Transport";
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1d31ef and dfc3b35.

📒 Files selected for processing (9)
  • src/client/CrazyGamesSDK.ts
  • src/client/Main.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/graphics/layers/GameRightSidebar.ts
  • src/client/Main.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/graphics/layers/WinModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 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/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/Main.ts
  • src/client/UsernameInput.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/graphics/layers/WinModal.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 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/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/Main.ts
🧬 Code graph analysis (6)
src/client/graphics/layers/GameRightSidebar.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (284-284)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/client/Main.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (284-284)
src/client/UsernameInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (284-284)
src/client/graphics/layers/SettingsModal.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (284-284)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/client/SinglePlayerModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (284-284)
src/client/graphics/layers/WinModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (284-284)
🔇 Additional comments (10)
src/client/graphics/layers/GameRightSidebar.ts (2)

106-114: LGTM! Pause/resume gameplay tracking is correct.

When the game pauses, gameplayStop() signals the SDK; when resuming, gameplayStart() signals active play. The toggle logic is sound.


116-128: LGTM! Exit flow with midgame ad is well-structured.

The async/await pattern ensures the ad completes before stopping gameplay and redirecting. Clean and readable.

src/client/graphics/layers/WinModal.ts (1)

252-257: LGTM! Gameplay stop on modal show is appropriate.

Signaling the SDK that active gameplay has ended when showing the win/death modal is the right approach. The call is placed before async content loading, which is correct.

src/client/SinglePlayerModal.ts (1)

843-844: LGTM! Midgame ad placement before game start is correct.

Requesting the ad after validation completes but before dispatching the join-lobby event ensures users see the ad without blocking invalid submissions.

src/client/graphics/layers/SettingsModal.ts (1)

108-117: LGTM! Pause logic with SDK integration is correct.

The guard shouldPause && !wasPausedWhenOpened ensures SDK calls and pause events only fire when needed - not when the game was already paused before opening settings. Clean logic.

src/client/Main.ts (2)

217-217: LGTM! Field extraction for reuse is clean.

Moving hostModal to a class field enables access from handleUrl(). Good refactor.


526-529: LGTM! Type guard and assignment are correct.

Consistent with other modal initialization patterns in this file.

src/client/CrazyGamesSDK.ts (3)

44-61: Clean deferred promise pattern with timeout.

The ready mechanism is well designed: the 3-second timeout prevents indefinite waiting if SDK init never completes. Using Promise.race here is the right approach.


256-280: Good defensive implementation.

The requestMidgameAd method handles all error paths gracefully and always resolves the promise, preventing callers from hanging. The callback pattern matches the SDK API correctly.


6-21: Use literal union type for adType parameter in ad.requestAd().

The user.getUser() signature is correct and matches the official CrazyGames SDK documentation. However, ad.requestAd() should constrain adType to "midgame" | "rewarded" instead of string. The documentation specifies these two values, and using a literal union type provides better type safety and editor autocomplete:

ad: {
  requestAd: (
    adType: "midgame" | "rewarded",
    callbacks: {
      adStarted: () => void;
      adFinished: () => void;
      adError: (error: any) => void;
    },
  ) => void;
};
⛔ Skipped due to learnings
Learnt from: Dimitrije-V
Repo: openfrontio/OpenFrontIO PR: 1814
File: src/client/graphics/layers/PlayerActionHandler.ts:0-0
Timestamp: 2025-08-14T14:05:00.867Z
Learning: In TypeScript, when dealing with union types like NukeType (derived from UnitType), it's better to accept the broader type (UnitType) as a parameter and use runtime type checking rather than trying to narrow the parameter type at compile time. The approach of keeping flexible input parameters with runtime validation followed by properly typed state/storage is more practical than over-constraining function signatures.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/client/UsernameInput.ts (1)

165-178: Async result is ignored - username fetch has no effect on return value.

The getUsername() method fires an async call to crazyGamesSDK.getUsername() but does not await it. The method returns the localStorage value or generates a new username before the SDK call completes. The SDK username is only applied as a side effect later via this.baseUsername = username.

This creates a race: the initial render uses localStorage/generated username, then potentially flickers to the SDK username after the promise resolves.

Proposed fix - make connectedCallback async
-  connectedCallback() {
+  async connectedCallback() {
     super.connectedCallback();
-    const stored = this.getUsername();
-    this.parseAndSetUsername(stored);
+    // Try CrazyGames username first
+    const cgUsername = await crazyGamesSDK.getUsername();
+    if (cgUsername) {
+      this.baseUsername = cgUsername;
+      this.requestUpdate();
+      return;
+    }
+    // Fall back to localStorage or generate new
+    const stored = this.getStoredUsername();
+    this.parseAndSetUsername(stored);
   }

-  private getUsername(): string {
-    crazyGamesSDK.getUsername().then((username) => {
-      if (username) {
-        this.baseUsername = username;
-        this.requestUpdate();
-      }
-      return null;
-    });
+  private getStoredUsername(): string {
     const storedUsername = localStorage.getItem(usernameKey);
     if (storedUsername) {
       return storedUsername;
     }
     return this.generateNewUsername();
   }
src/client/CrazyGamesSDK.ts (1)

63-77: Debug code left in - return true short-circuits the real check.

Line 64 has return true; which makes isOnCrazyGames() always return true, regardless of the actual environment. The actual detection logic below (lines 65-76) is now unreachable dead code.

This will cause CrazyGames-specific behavior to run everywhere, including hiding matchmaking buttons and triggering SDK calls that will fail outside CrazyGames.

Proposed fix - remove debug line
   isOnCrazyGames(): boolean {
-    return true;
     try {
       // Check if we're in an iframe
       if (window.self !== window.top) {
🤖 Fix all issues with AI agents
In `@src/client/Matchmaking.ts`:
- Around line 252-259: The event listener for "userMeResponse" added in
connectedCallback causes a leak because it's never removed; in
disconnectedCallback remove the exact listener by defining the handler as a
named method or a bound property (e.g., this.onUserMeResponse) and use
document.removeEventListener("userMeResponse", this.onUserMeResponse). Update
Matchmaking.connectedCallback to attach the handler via that property and
implement Matchmaking.disconnectedCallback to call document.removeEventListener
with the same function reference so listeners don't accumulate when the
component is detached.
♻️ Duplicate comments (3)
startup.sh (1)

88-92: Redundant conditional - both branches do the same thing.

After removing the timeout wrapper, both the if and else branches run the exact same command. The conditional no longer serves a purpose and can be simplified.

🧹 Suggested fix: remove the dead conditional
 # Start supervisord
-if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
-    exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
-else
-    exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
-fi
+exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
src/client/Main.ts (1)

597-604: Add username validation before opening the host modal.

The instant multiplayer path calls this.hostModal.open() without validating the username first. The hostLobbyButton handler (lines 486-501) validates username before proceeding. This path should do the same.

src/client/CrazyGamesSDK.ts (1)

116-130: Missing try-catch around SDK calls.

Both getUsername() and isInstantMultiplayer() access SDK methods without error handling. Other methods in this class consistently wrap SDK calls in try-catch blocks. These should follow the same pattern.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dfc3b35 and 517b9e1.

📒 Files selected for processing (15)
  • src/client/CrazyGamesSDK.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
✅ Files skipped from review due to trivial changes (1)
  • src/client/components/PatternButton.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
🧰 Additional context used
🧠 Learnings (13)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/PlayPage.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/components/PlayPage.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/UsernameInput.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/PatternInput.ts
  • src/client/Matchmaking.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-07-23T12:36:35.354Z
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 1534
File: src/client/LangSelector.ts:97-106
Timestamp: 2025-07-23T12:36:35.354Z
Learning: In OpenFrontIO's LangSelector.ts, the getClosestSupportedLang method always joins language code parts with underscores ("_") because all keys in the languageMap use underscore format (e.g., pt_BR, sv_SE, zh_CN). This normalization ensures consistency regardless of whether the input language code uses hyphens or underscores as delimiters.

Applied to files:

  • src/client/LangSelector.ts
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
Repo: openfrontio/OpenFrontIO PR: 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:

  • src/client/LangSelector.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 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/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/Main.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In the OpenFrontIO codebase, `playerBySmallID(id: number)` in Game, GameImpl, and GameView explicitly returns `Player | TerraNullius` (or `PlayerView | TerraNullius`), never undefined. The method returns TerraNullius for ID 0 and Player otherwise. Code should check `.isPlayer()` directly without undefined guards or optional chaining.

Applied to files:

  • src/client/CrazyGamesSDK.ts
🧬 Code graph analysis (5)
src/client/UsernameInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (285-285)
src/client/Main.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (285-285)
src/client/PatternInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (285-285)
src/client/Matchmaking.ts (3)
src/core/ApiSchemas.ts (1)
  • UserMeResponse (78-78)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (285-285)
src/client/graphics/layers/GameRightSidebar.ts (4)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (285-285)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/core/game/GameView.ts (1)
  • isAlive (470-472)
src/core/game/PlayerImpl.ts (1)
  • isAlive (347-349)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (13)
src/client/LangSelector.ts (1)

231-231: LGTM!

Adding "matchmaking-button" to the components list ensures translation updates reach the new custom element. This aligns with the refactor in PlayPage.ts that replaces explicit button markup with the <matchmaking-button> component.

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

140-142: LGTM!

Clean refactor replacing two explicit button variants with a single declarative <matchmaking-button> component. This moves the login-state conditional logic into the component itself, keeping the page template simpler.

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

107-111: LGTM!

Adding no-crazygames class to the store button enables the global hideCrazyGamesElements logic to hide it when running inside the CrazyGames environment.


128-141: LGTM!

The no-crazygames class on the account button and avatar image follows the same pattern. This ensures account-related UI is hidden in the CrazyGames context where external authentication is not supported.

src/client/graphics/layers/GameRightSidebar.ts (2)

106-114: LGTM!

Correct integration of CrazyGames SDK gameplay state. Calling gameplayStop() when pausing and gameplayStart() when resuming ensures the SDK knows when the game is interruptible (for ads, overlays, etc.).


116-128: Error handling is already in place — no changes needed.

The requestMidgameAd() method never rejects the promise. All code paths (SDK not ready, ad finished, ad error, caught exceptions) call resolve(), so the async flow is safe. The method cannot throw and block the redirect. Additionally, gameplayStop() is already awaited on line 125 and handles errors internally.

Likely an incorrect or invalid review comment.

src/client/PatternInput.ts (2)

8-8: LGTM!

Import added for the SDK singleton.


90-94: Pattern input is intentionally hidden on CrazyGames, consistent with codebase approach. Other UI components like Matchmaking also return empty templates on CrazyGames, confirming this is the standard pattern for the platform. No changes needed.

src/client/Matchmaking.ts (2)

267-304: LGTM!

The CrazyGames guard and login-based rendering split are clean. Both branches include the modal component, which is consistent.


307-329: LGTM!

The handlers follow existing patterns in the codebase. Username validation before opening the modal and the navigation to account page are both correct.

src/client/Main.ts (1)

915-935: LGTM!

The hideCrazyGamesElements function with delayed calls handles late-rendered Lit components. This is a practical workaround for components that render after initial DOM load.

src/client/CrazyGamesSDK.ts (2)

44-61: LGTM!

The readiness mechanism with Promise.race and a 3-second timeout is a clean pattern. It prevents indefinite waits while allowing async SDK initialization.


257-282: LGTM!

The requestMidgameAd method handles all cases well: SDK not ready, ad finished, ad error, and SDK call failure. The Promise always resolves, which is appropriate for optional ad functionality that should not block the game.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/CrazyGamesSDK.ts (1)

69-83: Debug code left in - return true makes platform detection dead code.

Line 70 unconditionally returns true, making lines 71-82 unreachable. This looks like debug code that was not removed before commit.

This will cause the SDK to always behave as if running on CrazyGames, which will break functionality on other deployments:

  • SDK initialization will be attempted everywhere
  • UI elements may be incorrectly hidden
  • Username fetching and ad requests will fail unexpectedly
Proposed fix - remove debug line
 isOnCrazyGames(): boolean {
-  return true;
   try {
     // Check if we're in an iframe
     if (window.self !== window.top) {
🤖 Fix all issues with AI agents
In `@src/client/CrazyGamesSDK.ts`:
- Around line 130-148: The addAuthListener method uses a synchronous isReady()
check whereas other APIs (getUsername, isInstantMultiplayer) use the async
ready() with timeout; change addAuthListener to the same async pattern: make
addAuthListener return a Promise<void> (or be async), call this.ready(timeout)
and await it before invoking
window.CrazyGames!.SDK.user.addAuthListener(listener), log and throw or handle
timeout errors consistently, and keep the same try/catch around
window.CrazyGames!.SDK.user.addAuthListener to surface any failures.

In `@src/client/graphics/layers/GameRightSidebar.ts`:
- Around line 116-128: The onExitButtonClick handler can throw from
crazyGamesSDK.requestMidgameAd() or crazyGamesSDK.gameplayStop(), preventing the
final redirect; wrap those SDK calls in a try-catch around requestMidgameAd()
and gameplayStop() inside onExitButtonClick, log or console.error any caught
error for diagnostics, and ensure window.location.href = "/" runs in a finally
block (or after the catch) so the redirect always occurs even if the SDK calls
fail.

In `@src/client/PatternInput.ts`:
- Around line 91-94: Remove the premature hard-coded return in the
isOnCrazyGames() method in CrazyGamesSDK: delete the `return true;` early return
so the function proceeds to the existing iframe/platform detection logic (the
code that checks window.top, document.referrer, or iframe identifiers) and
correctly returns true only when actually running on CrazyGames; ensure the
method name isOnCrazyGames() is left intact and its detection branch executes
normally.
♻️ Duplicate comments (3)
src/client/Matchmaking.ts (1)

252-259: Missing cleanup for event listener - memory leak.

This was flagged in a previous review. The userMeResponse listener is added in connectedCallback but never removed. Extract the handler to a class property and add disconnectedCallback to remove it.

src/client/CrazyGamesSDK.ts (2)

122-128: Missing try-catch around SDK call.

The SDK call at line 127 can throw, but there's no error handling. Other methods in this class wrap SDK calls in try-catch.

Proposed fix
 async getUsername(): Promise<string | null> {
   const isReady = await this.ready();
   if (!isReady) {
     return null;
   }
-  return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
+  try {
+    return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
+  } catch (error) {
+    console.error("Failed to get user:", error);
+    return null;
+  }
 }

150-156: Missing try-catch around SDK access.

The SDK property access at line 155 could throw, but there's no error handling. Other methods wrap SDK calls in try-catch.

Proposed fix
 async isInstantMultiplayer(): Promise<boolean> {
   const isReady = await this.ready();
   if (!isReady) {
     return false;
   }
-  return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
+  try {
+    return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
+  } catch (error) {
+    console.error("Failed to check instant multiplayer:", error);
+    return false;
+  }
 }
🧹 Nitpick comments (2)
src/client/components/DesktopNavBar.ts (1)

128-141: Redundant no-crazygames class on nested element.

The avatar image (line 137) is inside the account button (line 130). Since the parent button already has no-crazygames, the class on the child image is redundant—when the parent hides, so does everything inside it.

Consider removing the class from the avatar:

Suggested change
          <img
            id="nav-account-avatar"
-           class="no-crazygames hidden w-8 h-8 rounded-full object-cover"
+           class="hidden w-8 h-8 rounded-full object-cover"
            alt=""
src/client/Matchmaking.ts (1)

307-325: Consider adding type safety for DOM queries.

Casting usernameInput to any loses type safety. If username-input exposes isValid() and validationError, define an interface for it.

Suggested approach
// Define interface for the expected shape
interface UsernameInputElement extends HTMLElement {
  isValid(): boolean;
  validationError?: string;
}

// Use in handler
private handleLoggedInClick() {
  const usernameInput = document.querySelector("username-input") as UsernameInputElement | null;
  const publicLobby = document.querySelector("public-lobby") as { leaveLobby?: () => void } | null;

  if (usernameInput?.isValid()) {
    this.open();
    publicLobby?.leaveLobby?.();
  } else {
    // ...
  }
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 517b9e1 and 8fa73a9.

📒 Files selected for processing (15)
  • src/client/CrazyGamesSDK.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/UsernameInput.ts
  • startup.sh
  • src/client/Main.ts
  • src/client/SinglePlayerModal.ts
  • src/client/LangSelector.ts
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/PatternButton.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/Matchmaking.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/components/PatternButton.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/PatternInput.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/Matchmaking.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/WinModal.ts
📚 Learning: 2025-07-14T20:41:57.645Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1432
File: src/client/graphics/layers/NameLayer.ts:143-143
Timestamp: 2025-07-14T20:41:57.645Z
Learning: In the OpenFrontIO project, the Layer system (like NameLayer) doesn't use Lit framework and has no disconnectedCallback() or unmount mechanism available. This creates challenges for proper cleanup of event listeners and timeouts, leading to potential memory leaks. This is a known architectural limitation that should be addressed in future work.

Applied to files:

  • src/client/Matchmaking.ts
📚 Learning: 2026-01-08T13:52:08.203Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:08.203Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/Matchmaking.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-13T20:16:20.098Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:20.098Z
Learning: In the OpenFrontIO codebase, `playerBySmallID(id: number)` in Game, GameImpl, and GameView explicitly returns `Player | TerraNullius` (or `PlayerView | TerraNullius`), never undefined. The method returns TerraNullius for ID 0 and Player otherwise. Code should check `.isPlayer()` directly without undefined guards or optional chaining.

Applied to files:

  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
🧬 Code graph analysis (5)
src/client/graphics/layers/WinModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (311-311)
src/client/PatternInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (311-311)
src/client/graphics/layers/SettingsModal.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (311-311)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/client/Matchmaking.ts (5)
src/core/ApiSchemas.ts (1)
  • UserMeResponse (78-78)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (311-311)
src/client/LangSelector.ts (1)
  • translateText (277-301)
src/client/Utils.ts (1)
  • translateText (165-224)
src/client/graphics/layers/GameRightSidebar.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (311-311)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (17)
src/client/components/PatternButton.ts (1)

74-79: LGTM!

Adding no-crazygames to the wrapper div is consistent with how other UI components (like DesktopNavBar and PatternInput) handle CrazyGames-specific visibility. This allows the hiding logic in Main.ts to target this element correctly.

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

107-111: LGTM!

Hiding the store button on CrazyGames platform makes sense. The class follows the established pattern for conditional visibility.

src/client/graphics/layers/SettingsModal.ts (2)

3-4: Imports look correct.

The crazyGamesSDK singleton and PauseGameIntentEvent are imported from their correct modules, matching the exports shown in the relevant code snippets.


108-117: SDK methods already handle errors gracefully—code is correct as written.

The gameplayStop() and gameplayStart() methods in CrazyGamesSDK.ts already include try-catch blocks that catch and log errors to console. Errors fail silently without propagating, so wrapping the calls in SettingsModal would be redundant. The pattern of not awaiting these methods is intentional and consistent throughout the codebase. The logic correctly gates both SDK calls and the pause event emission under the same condition, preventing unpausing a game that was already paused before the modal opened.

src/client/graphics/layers/WinModal.ts (1)

252-261: LGTM! Fire-and-forget call is fine here.

The gameplayStop() call signals the CrazyGames SDK that gameplay has paused when the win/death modal appears. The fire-and-forget pattern (no await) matches the existing usage of happytime() on lines 299 and 329, so this is consistent.

src/client/graphics/layers/GameRightSidebar.ts (1)

106-114: LGTM! Pause toggle with SDK integration.

The fire-and-forget calls to gameplayStop() and gameplayStart() are appropriate here. The user expects immediate UI feedback when toggling pause, so blocking on SDK responses would degrade the experience.

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

139-142: Clean refactor to a single component.

Moving login-state awareness into <matchmaking-button> simplifies PlayPage and follows composition over inheritance. The parent no longer needs to track auth state for this UI.

src/client/Matchmaking.ts (2)

267-270: LGTM! Consistent platform check.

This matches the pattern used in PatternInput.ts for hiding features on CrazyGames.


272-305: Both render paths include <matchmaking-modal> - good.

The modal component is rendered regardless of login state, which ensures it is available when needed. The button text and click handlers correctly differ based on isLoggedIn.

src/client/CrazyGamesSDK.ts (8)

1-45: Type declarations look good.

The SDK interface declarations are clear and match the expected CrazyGames API surface.


47-57: Readiness promise pattern is clean.

The deferred promise pattern with captured resolver is a good approach for coordinating initialization state.


59-67: Timeout-based readiness check is solid.

The Promise.race pattern with a 3-second timeout is a good safeguard against indefinite hangs.


85-87: Synchronous readiness check is correct.

Simple guard combining platform check with initialization state.


89-120: Initialization logic is well structured.

Good polling mechanism for SDK availability with proper error handling. The resolveReady() call on success correctly signals the readiness promise.


158-281: Existing SDK methods are well structured.

All existing methods follow a consistent pattern with isReady() guards and try-catch error handling.


283-308: Ad request implementation is robust.

Good handling of all completion paths (success, error, not-ready). The Promise always resolves rather than rejects, which is appropriate for optional ad functionality that shouldn't break the main flow.


311-311: Singleton export is appropriate.

SDK wrapper with global state benefits from singleton pattern.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +116 to 128
private async onExitButtonClick() {
const isAlive = this.game.myPlayer()?.isAlive();
if (isAlive) {
const isConfirmed = confirm(
translateText("help_modal.exit_confirmation"),
);
if (!isConfirmed) return;
}
crazyGamesSDK.gameplayStop().then(() => {
// redirect to the home page
window.location.href = "/";
});
await crazyGamesSDK.requestMidgameAd();
await crazyGamesSDK.gameplayStop();
// redirect to the home page
window.location.href = "/";
}
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 | 🟡 Minor

Add error handling to prevent users getting stuck on exit.

If requestMidgameAd() or gameplayStop() throws an error, the redirect will never happen and the user will be stuck. Wrap in try-catch to ensure the redirect always occurs.

Proposed fix
   private async onExitButtonClick() {
     const isAlive = this.game.myPlayer()?.isAlive();
     if (isAlive) {
       const isConfirmed = confirm(
         translateText("help_modal.exit_confirmation"),
       );
       if (!isConfirmed) return;
     }
-    await crazyGamesSDK.requestMidgameAd();
-    await crazyGamesSDK.gameplayStop();
-    // redirect to the home page
-    window.location.href = "/";
+    try {
+      await crazyGamesSDK.requestMidgameAd();
+      await crazyGamesSDK.gameplayStop();
+    } catch (err) {
+      console.error("SDK error on exit:", err);
+    }
+    window.location.href = "/";
   }
🤖 Prompt for AI Agents
In `@src/client/graphics/layers/GameRightSidebar.ts` around lines 116 - 128, The
onExitButtonClick handler can throw from crazyGamesSDK.requestMidgameAd() or
crazyGamesSDK.gameplayStop(), preventing the final redirect; wrap those SDK
calls in a try-catch around requestMidgameAd() and gameplayStop() inside
onExitButtonClick, log or console.error any caught error for diagnostics, and
ensure window.location.href = "/" runs in a finally block (or after the catch)
so the redirect always occurs even if the SDK calls fail.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/CrazyGamesSDK.ts (1)

70-84: Critical: Hardcoded return true makes all code unreachable.

Line 71 returns true unconditionally, making the entire detection logic (lines 72-83) unreachable. This appears to be debug code left in accidentally.

This will cause all environments to behave as if running on CrazyGames, potentially breaking non-CrazyGames deployments.

Proposed fix - remove debug return
 isOnCrazyGames(): boolean {
-  return true;
   try {
     // Check if we're in an iframe
     if (window.self !== window.top) {
🤖 Fix all issues with AI agents
In `@src/client/Main.ts`:
- Line 589: The call to crazyGamesSDK.createInviteLink() is missing the required
gameId argument; replace it with crazyGamesSDK.getInviteGameId() if you intend
to retrieve an existing invite (e.g., const lobbyId =
crazyGamesSDK.getInviteGameId()), or if you truly want to create a new invite
call crazyGamesSDK.createInviteLink(gameId) and supply the appropriate gameId
value; update the code path using lobbyId accordingly.

In `@src/client/SinglePlayerModal.ts`:
- Around line 843-844: The call to crazyGamesSDK.requestMidgameAd() can throw or
hang and currently blocks game start; wrap that call in a try/catch and guard it
with a short timeout (e.g., Promise.race with a 3–5s timeout) so failures or
hangs are ignored and the flow continues to start the game; catch and log any
error (or timeout) and ensure the subsequent game-start logic (where
requestMidgameAd() is currently awaited) runs regardless.
♻️ Duplicate comments (6)
src/client/PatternInput.ts (1)

91-94: Verify that isOnCrazyGames() returns the correct value.

This early return hides the pattern input on CrazyGames. The logic here looks fine, but a past review flagged that isOnCrazyGames() in CrazyGamesSDK.ts has a hard-coded return true; that makes it always return true. If that bug exists, this will hide the pattern selector for ALL users, not just CrazyGames users.

Please confirm that isOnCrazyGames() has proper platform detection before merging.

#!/bin/bash
# Check the implementation of isOnCrazyGames in CrazyGamesSDK.ts
rg -n "isOnCrazyGames" -A 15 src/client/CrazyGamesSDK.ts
src/client/graphics/layers/GameRightSidebar.ts (1)

116-128: Add error handling to prevent users getting stuck on exit.

If requestMidgameAd() or gameplayStop() throws, the redirect never happens and user is stuck. Wrap in try-catch to ensure redirect always occurs.

Proposed fix
   private async onExitButtonClick() {
     const isAlive = this.game.myPlayer()?.isAlive();
     if (isAlive) {
       const isConfirmed = confirm(
         translateText("help_modal.exit_confirmation"),
       );
       if (!isConfirmed) return;
     }
-    await crazyGamesSDK.requestMidgameAd();
-    await crazyGamesSDK.gameplayStop();
-    // redirect to the home page
-    window.location.href = "/";
+    try {
+      await crazyGamesSDK.requestMidgameAd();
+      await crazyGamesSDK.gameplayStop();
+    } catch (err) {
+      console.error("SDK error on exit:", err);
+    }
+    window.location.href = "/";
   }
src/client/CrazyGamesSDK.ts (3)

131-149: Inconsistent readiness check - uses sync isReady() while others use async ready().

Methods like getUsername() and isInstantMultiplayer() use async ready() with timeout. This method uses synchronous isReady(), which may fail if called before SDK initialization completes.

Consider using the async pattern:

Proposed fix
-addAuthListener(
+async addAuthListener(
   listener: (
     user: {
       username: string;
     } | null,
   ) => void,
-): void {
-  if (!this.isReady()) {
+): Promise<void> {
+  const isReady = await this.ready();
+  if (!isReady) {
     console.warn("CrazyGames SDK not ready, auth listener will not be added");
     return;
   }

151-157: Add try-catch for SDK property access.

For consistency with other methods, wrap the SDK access in try-catch:

Proposed fix
 async isInstantMultiplayer(): Promise<boolean> {
   const isReady = await this.ready();
   if (!isReady) {
     return false;
   }
-  return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
+  try {
+    return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
+  } catch (error) {
+    console.error("Failed to check instant multiplayer:", error);
+    return false;
+  }
 }

123-129: Add try-catch for SDK call consistency.

Other SDK methods wrap calls in try-catch. This method should follow the same pattern to handle potential SDK errors gracefully.

Proposed fix
 async getUsername(): Promise<string | null> {
   const isReady = await this.ready();
   if (!isReady) {
     return null;
   }
-  return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
+  try {
+    return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
+  } catch (error) {
+    console.error("Failed to get user:", error);
+    return null;
+  }
 }
src/client/Main.ts (1)

597-604: Add username validation before opening host modal.

The hostLobbyButton handler (lines 486-501) validates the username before proceeding. The instant multiplayer path bypasses this check. Add validation for consistency:

Proposed fix
     crazyGamesSDK.isInstantMultiplayer().then((isInstant) => {
       if (isInstant) {
         console.log(
           `CrazyGames: joining instant multiplayer lobby from CrazyGames`,
         );
-        this.hostModal.open();
+        if (this.usernameInput?.isValid()) {
+          this.hostModal.open();
+        } else {
+          window.dispatchEvent(
+            new CustomEvent("show-message", {
+              detail: {
+                message: this.usernameInput?.validationError || "Invalid username",
+                color: "red",
+                duration: 3000,
+              },
+            }),
+          );
+        }
       }
     });
🧹 Nitpick comments (6)
src/client/components/DesktopNavBar.ts (1)

128-141: Consider removing redundant class from child element.

The no-crazygames class on nav-account-avatar (line 137) seems redundant since its parent nav-account-button (line 130) already has this class. When the parent button is hidden, the avatar inside will be hidden too.

Unless there is a use case where the avatar needs to be hidden independently while the button remains visible, you can simplify by removing the class from the avatar.

Suggested simplification
           <img
             id="nav-account-avatar"
-            class="no-crazygames hidden w-8 h-8 rounded-full object-cover"
+            class="hidden w-8 h-8 rounded-full object-cover"
             alt=""
src/client/graphics/layers/UnitDisplay.ts (1)

131-134: Looks good - responsive layout update is correct.

The Tailwind classes are well-formed:

  • z-[1100] uses proper arbitrary value syntax
  • min-[1200px]:flex is valid custom breakpoint syntax

Small note: min-[1200px]:flex-col and xl:flex-col both set the same value (same for gap-2). Since xl (1280px) is higher than 1200px, the xl: variants are redundant when the values match. You could simplify by removing xl:flex-col and xl:gap-2 if the intent is just flex-col from 1200px until 2xl kicks in.

This is purely a minor readability thing - the code works correctly as written.

Optional: Remove redundant xl: variants
-        class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col min-[1200px]:flex-col 2xl:gap-5 xl:gap-2 min-[1200px]:gap-2 justify-center items-center"
+        class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row min-[1200px]:flex-col 2xl:gap-5 min-[1200px]:gap-2 justify-center items-center"
src/client/graphics/layers/WinModal.ts (1)

253-253: Consider adding error handling for gameplayStop().

If gameplayStop() throws, the modal content won't load and the modal won't show properly. Since this is a fire-and-forget call, wrapping it in try-catch ensures the win modal always appears.

Proposed fix
   async show() {
-    crazyGamesSDK.gameplayStop();
+    try {
+      crazyGamesSDK.gameplayStop();
+    } catch (err) {
+      console.error("gameplayStop failed:", err);
+    }
     await this.loadPatternContent();
src/client/HostLobbyModal.ts (1)

116-121: Add error handling for createInviteLink() to ensure fallback works.

If createInviteLink() throws instead of returning null, the method will fail and no URL will be built. Wrap in try-catch to guarantee fallback to standard URL construction.

Proposed fix
   private async buildLobbyUrl(): Promise<string> {
     if (crazyGamesSDK.isOnCrazyGames()) {
-      const link = crazyGamesSDK.createInviteLink(this.lobbyId);
-      if (link !== null) {
-        return link;
+      try {
+        const link = crazyGamesSDK.createInviteLink(this.lobbyId);
+        if (link !== null) {
+          return link;
+        }
+      } catch (err) {
+        console.error("CrazyGames invite link failed:", err);
       }
     }
     const config = await getServerConfigFromClient();
src/client/UsernameInput.ts (2)

43-49: Debug log will show "[object Object]" - consider JSON.stringify.

Line 44 logs the user object directly in a template literal, which will display as [object Object]. For useful debug output:

-      console.log(`got user: ${user}`);
+      console.log(`got user:`, user);

Also, the optional chaining on line 46 (user?.username) is redundant since user is already truthy inside the if (user) block.


172-185: Fire-and-forget async call creates confusing control flow.

This method fires an async promise (lines 173-179) but does not await it. The method immediately continues to the synchronous fallback path (lines 180-184) and returns. The async callback later overwrites baseUsername, causing the username to change after initial render.

The return null on line 178 has no effect since nothing awaits this promise.

If this "eventual update" behavior is intended, consider extracting it for clarity:

Suggested refactor
 private getUsername(): string {
-  crazyGamesSDK.getUsername().then((username) => {
-    if (username) {
-      this.baseUsername = username;
-      this.requestUpdate();
-    }
-    return null;
-  });
+  // Async fetch from SDK - will update UI if successful
+  this.fetchUsernameFromSDK();
+
   const storedUsername = localStorage.getItem(usernameKey);
   if (storedUsername) {
     return storedUsername;
   }
   return this.generateNewUsername();
 }

+private fetchUsernameFromSDK(): void {
+  crazyGamesSDK.getUsername().then((username) => {
+    if (username) {
+      this.baseUsername = username;
+      this.requestUpdate();
+    }
+  });
+}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa73a9 and 1af8694.

📒 Files selected for processing (16)
  • src/client/CrazyGamesSDK.ts
  • src/client/HostLobbyModal.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
✅ Files skipped from review due to trivial changes (1)
  • src/client/components/PatternButton.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • startup.sh
  • src/client/Matchmaking.ts
  • src/client/LangSelector.ts
  • src/client/components/PlayPage.ts
🧰 Additional context used
🧠 Learnings (13)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/graphics/layers/SettingsModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/Main.ts
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/graphics/layers/SettingsModal.ts
  • src/client/PatternInput.ts
  • src/client/HostLobbyModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/SinglePlayerModal.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/Main.ts
  • src/client/UsernameInput.ts
📚 Learning: 2026-01-13T20:16:20.098Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:20.098Z
Learning: In the OpenFrontIO codebase, `playerBySmallID(id: number)` in Game, GameImpl, and GameView explicitly returns `Player | TerraNullius` (or `PlayerView | TerraNullius`), never undefined. The method returns TerraNullius for ID 0 and Player otherwise. Code should check `.isPlayer()` directly without undefined guards or optional chaining.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/Main.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/Main.ts
📚 Learning: 2026-01-02T18:11:06.832Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:06.832Z
Learning: In src/client/HostLobbyModal.ts, the ?s=xxxxx URL suffix used in lobby URLs is purely for cache-busting platform previews (e.g., Discord, WhatsApp, x.com) and is not used by the join logic. The join flow ignores the suffix value, so regenerating it via updateUrlWithSuffix() on configuration changes will not break existing shared URLs; it only prompts platforms to refresh preview metadata. Treat the suffix as non-functional for join behavior and ensure any related changes preserve that invariant.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 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/Main.ts
📚 Learning: 2025-12-29T23:33:25.733Z
Learnt from: wraith4081
Repo: openfrontio/OpenFrontIO PR: 2735
File: index.html:390-391
Timestamp: 2025-12-29T23:33:25.733Z
Learning: In Tailwind CSS v4, blur utilities were renamed: `backdrop-blur-sm` from v3 is now `backdrop-blur-xs` in v4, and `backdrop-blur` (bare) becomes `backdrop-blur-sm`. Similarly, other blur utilities like `blur-sm` are renamed to `blur-xs`.

Applied to files:

  • src/client/graphics/layers/UnitDisplay.ts
📚 Learning: 2026-01-08T13:52:08.203Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:08.203Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/WinModal.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
🧬 Code graph analysis (7)
src/client/graphics/layers/SettingsModal.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/client/PatternInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/HostLobbyModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/SinglePlayerModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/graphics/layers/GameRightSidebar.ts (4)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/core/game/GameView.ts (1)
  • isAlive (470-472)
src/core/game/PlayerImpl.ts (1)
  • isAlive (347-349)
src/client/graphics/layers/WinModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Main.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (236-236)
🪛 GitHub Actions: 🧪 CI
src/client/Main.ts

[error] 589-589: TS2554: Expected 1 arguments, but got 0.

🪛 GitHub Actions: 🚀 Deploy
src/client/Main.ts

[error] 589-589: TS2554: Expected 1 arguments, but got 0. (Command: npm run build-prod)

🔇 Additional comments (7)
src/client/components/DesktopNavBar.ts (1)

107-111: LGTM!

Adding no-crazygames to the store button is a clean way to hide platform-specific UI. This follows the same pattern seen across other files in this PR.

src/client/SinglePlayerModal.ts (1)

30-30: LGTM!

Import of the singleton is correct and consistent with other files in this PR.

src/client/PatternInput.ts (1)

8-8: LGTM!

Import is correct and consistent with other files.

src/client/graphics/layers/GameRightSidebar.ts (1)

106-114: LGTM!

Good integration of CrazyGames gameplay lifecycle. Stopping gameplay when paused and starting when resumed is the correct pattern for the SDK.

src/client/graphics/layers/SettingsModal.ts (1)

108-117: Looks good - SDK pause/resume integration.

The logic correctly calls gameplayStop() when pausing and gameplayStart() when resuming. The conditional guard ensures we only affect gameplay state when the modal should control pause and the game was not already paused.

src/client/Main.ts (1)

915-936: Acceptable workaround for late-rendered components.

The multiple setTimeout calls (100ms, 500ms) handle components that render after initial page load. This is a pragmatic approach.

Consider adding a comment explaining why multiple delays are needed:

+  // Hide elements immediately
   hideCrazyGamesElements();

+  // Also hide elements after a short delay to catch late-rendered components
+  // (some Lit components may render asynchronously)
   setTimeout(hideCrazyGamesElements, 100);
   setTimeout(hideCrazyGamesElements, 500);
src/client/CrazyGamesSDK.ts (1)

300-325: Good callback-to-promise conversion with proper error handling.

The method correctly wraps the callback-based SDK API in a Promise, handles all completion paths (adFinished, adError, catch), and always resolves to avoid breaking calling code when ads fail.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/CrazyGamesSDK.ts (1)

70-84: Critical: return true; makes all detection logic unreachable.

Line 71 returns true unconditionally. The iframe detection logic (lines 72-83) is dead code and will never execute. This means isOnCrazyGames() always returns true even when running outside the CrazyGames platform.

This causes:

  • CrazyGames SDK initialization on all platforms
  • Platform-specific UI hiding everywhere
  • Potential runtime errors when SDK methods are called on non-CrazyGames hosts
Fix: Remove the hardcoded return
   isOnCrazyGames(): boolean {
-    return true;
     try {
       // Check if we're in an iframe
       if (window.self !== window.top) {
         // Try to access parent URL
         return window?.top?.location?.hostname.includes("crazygames") ?? false;
       }
       return false;
     } catch (e) {
       // If we get a cross-origin error, we're definitely iframed
       // Check our own referrer as fallback
       return document.referrer.includes("crazygames");
     }
   }
🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/SettingsModal.ts`:
- Around line 3-4: Replace the absolute-style imports in SettingsModal.ts for
crazyGamesSDK and PauseGameIntentEvent with relative paths; change import from
"src/client/CrazyGamesSDK" to "../../CrazyGamesSDK" and from
"src/client/Transport" to "../../Transport" so they match the file's existing
relative import style and project conventions, updating the import statements
that reference crazyGamesSDK and PauseGameIntentEvent accordingly.
♻️ Duplicate comments (7)
src/client/SinglePlayerModal.ts (1)

843-844: Add error handling to prevent blocking game start if ad fails.

If requestMidgameAd() throws or hangs, users cannot start their game. Wrap this in try-catch so the game starts regardless of ad outcome.

Proposed fix
-    await crazyGamesSDK.requestMidgameAd();
-
+    try {
+      await crazyGamesSDK.requestMidgameAd();
+    } catch (err) {
+      console.error("Midgame ad request failed:", err);
+    }
src/client/graphics/layers/GameRightSidebar.ts (1)

116-128: Add error handling to prevent users getting stuck on exit.

If requestMidgameAd() or gameplayStop() throws, the redirect never happens and users are stuck. Wrap SDK calls in try-catch so redirect always runs.

Proposed fix
   private async onExitButtonClick() {
     const isAlive = this.game.myPlayer()?.isAlive();
     if (isAlive) {
       const isConfirmed = confirm(
         translateText("help_modal.exit_confirmation"),
       );
       if (!isConfirmed) return;
     }
-    await crazyGamesSDK.requestMidgameAd();
-    await crazyGamesSDK.gameplayStop();
-    // redirect to the home page
-    window.location.href = "/";
+    try {
+      await crazyGamesSDK.requestMidgameAd();
+      await crazyGamesSDK.gameplayStop();
+    } catch (err) {
+      console.error("SDK error on exit:", err);
+    }
+    window.location.href = "/";
   }
src/client/Matchmaking.ts (1)

252-259: Missing cleanup for event listener causes memory leak.

The userMeResponse event listener is added in connectedCallback but never removed in disconnectedCallback. If this component is removed and re-added to the DOM, listeners stack up.

Proposed fix
+  private handleUserMeResponse = (event: Event) => {
+    const customEvent = event as CustomEvent;
+    if (customEvent.detail) {
+      const userMeResponse = customEvent.detail as UserMeResponse | false;
+      this.isLoggedIn = hasLinkedAccount(userMeResponse);
+    }
+  };

   async connectedCallback() {
     super.connectedCallback();
-    // Listen for user authentication changes
-    document.addEventListener("userMeResponse", (event: Event) => {
-      const customEvent = event as CustomEvent;
-      if (customEvent.detail) {
-        const userMeResponse = customEvent.detail as UserMeResponse | false;
-        this.isLoggedIn = hasLinkedAccount(userMeResponse);
-      }
-    });
+    document.addEventListener("userMeResponse", this.handleUserMeResponse);
   }

+  disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener("userMeResponse", this.handleUserMeResponse);
+  }
src/client/Main.ts (1)

597-604: Username validation still missing for instant multiplayer flow.

The hostLobbyButton handler (lines 486-501) validates the username before proceeding. This instant multiplayer path calls this.hostModal.open() directly without the same check, allowing users to create a lobby with an invalid or empty username.

Suggested fix
     crazyGamesSDK.isInstantMultiplayer().then((isInstant) => {
       if (isInstant) {
         console.log(
           `CrazyGames: joining instant multiplayer lobby from CrazyGames`,
         );
-        this.hostModal.open();
+        if (this.usernameInput?.isValid()) {
+          this.hostModal.open();
+        } else {
+          window.dispatchEvent(
+            new CustomEvent("show-message", {
+              detail: {
+                message: this.usernameInput?.validationError,
+                color: "red",
+                duration: 3000,
+              },
+            }),
+          );
+        }
       }
     });
src/client/CrazyGamesSDK.ts (3)

123-129: Missing try-catch around SDK call.

If window.CrazyGames!.SDK.user.getUser() throws, the error propagates unhandled. Other methods in this class wrap SDK calls in try-catch blocks for consistency.

Suggested fix
   async getUsername(): Promise<string | null> {
     const isReady = await this.ready();
     if (!isReady) {
       return null;
     }
-    return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
+    try {
+      return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
+    } catch (error) {
+      console.error("Failed to get user:", error);
+      return null;
+    }
   }

131-149: Inconsistent readiness check.

This method uses synchronous isReady() (line 138) while getUsername() and isInstantMultiplayer() use async ready() with timeout. If addAuthListener is called during initialization, it silently fails even though the SDK might become ready shortly after.

Suggested fix for consistency
-  addAuthListener(
+  async addAuthListener(
     listener: (
       user: {
         username: string;
       } | null,
     ) => void,
-  ): void {
-    if (!this.isReady()) {
+  ): Promise<void> {
+    const isReady = await this.ready();
+    if (!isReady) {
       console.warn("CrazyGames SDK not ready, auth listener will not be added");
       return;
     }

151-157: Missing try-catch for consistency.

Same pattern issue as getUsername(). Wrap the SDK access in try-catch to match other methods.

Suggested fix
   async isInstantMultiplayer(): Promise<boolean> {
     const isReady = await this.ready();
     if (!isReady) {
       return false;
     }
-    return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
+    try {
+      return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
+    } catch (error) {
+      console.error("Failed to check instant multiplayer:", error);
+      return false;
+    }
   }
🧹 Nitpick comments (2)
src/client/UsernameInput.ts (1)

172-185: Simplify async username retrieval pattern.

The fire-and-forget .then() returning null is confusing. The return value is ignored anyway. Consider using void operator or just omitting the return.

Also, user?.username on line 46 is redundant since user is already truthy from the if (user) check.

Proposed fix
   private getUsername(): string {
-    crazyGamesSDK.getUsername().then((username) => {
+    void crazyGamesSDK.getUsername().then((username) => {
       if (username) {
         this.baseUsername = username;
         this.requestUpdate();
       }
-      return null;
     });
     const storedUsername = localStorage.getItem(usernameKey);
     if (storedUsername) {
       return storedUsername;
     }
     return this.generateNewUsername();
   }
src/client/Matchmaking.ts (1)

307-325: Use proper types instead of any.

Using any for usernameInput and publicLobby bypasses type safety. Use the actual types or create a minimal interface.

Proposed fix
+import { UsernameInput } from "./UsernameInput";
+
+// If PublicLobby type is not exported, create a minimal interface:
+interface LobbyComponent {
+  leaveLobby(): void;
+}

   private handleLoggedInClick() {
-    const usernameInput = document.querySelector("username-input") as any;
-    const publicLobby = document.querySelector("public-lobby") as any;
+    const usernameInput = document.querySelector("username-input") as UsernameInput | null;
+    const publicLobby = document.querySelector("public-lobby") as LobbyComponent | null;

     if (usernameInput?.isValid()) {
       this.open();
       publicLobby?.leaveLobby();
     } else {
       window.dispatchEvent(
         new CustomEvent("show-message", {
           detail: {
             message: usernameInput?.validationError,
             color: "red",
             duration: 3000,
           },
         }),
       );
     }
   }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1af8694 and b032426.

📒 Files selected for processing (16)
  • src/client/CrazyGamesSDK.ts
  • src/client/HostLobbyModal.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/client/PatternInput.ts
  • src/client/components/PatternButton.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • startup.sh
  • src/client/LangSelector.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PlayPage.ts
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/UsernameInput.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/Main.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/Matchmaking.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2026-01-08T13:52:08.203Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:08.203Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/Matchmaking.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/graphics/layers/GameRightSidebar.ts
  • src/client/Main.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/Matchmaking.ts
  • src/client/SinglePlayerModal.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 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/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/Main.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-13T20:16:20.098Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:20.098Z
Learning: In the OpenFrontIO codebase, `playerBySmallID(id: number)` in Game, GameImpl, and GameView explicitly returns `Player | TerraNullius` (or `PlayerView | TerraNullius`), never undefined. The method returns TerraNullius for ID 0 and Player otherwise. Code should check `.isPlayer()` directly without undefined guards or optional chaining.

Applied to files:

  • src/client/Main.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-07-14T20:41:57.645Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1432
File: src/client/graphics/layers/NameLayer.ts:143-143
Timestamp: 2025-07-14T20:41:57.645Z
Learning: In the OpenFrontIO project, the Layer system (like NameLayer) doesn't use Lit framework and has no disconnectedCallback() or unmount mechanism available. This creates challenges for proper cleanup of event listeners and timeouts, leading to potential memory leaks. This is a known architectural limitation that should be addressed in future work.

Applied to files:

  • src/client/Matchmaking.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/CrazyGamesSDK.ts
🧬 Code graph analysis (4)
src/client/UsernameInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/graphics/layers/SettingsModal.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/client/Matchmaking.ts (4)
src/core/ApiSchemas.ts (1)
  • UserMeResponse (78-78)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/LangSelector.ts (1)
  • translateText (277-301)
src/client/SinglePlayerModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (10)
src/client/SinglePlayerModal.ts (1)

30-30: LGTM!

Import of crazyGamesSDK is clean and matches the pattern used across other files in this PR.

src/client/graphics/layers/GameRightSidebar.ts (1)

108-112: LGTM!

Clean toggle logic for gameplay start/stop tied to pause state.

src/client/graphics/layers/SettingsModal.ts (1)

109-116: LGTM!

The conditional logic correctly avoids double-pausing when the game was already paused before the modal opened. Clean approach using composition of SDK calls with event emission.

src/client/Matchmaking.ts (2)

267-270: LGTM!

Clean early return guard for CrazyGames platform.


327-329: LGTM!

Clean navigation handler with defensive optional chaining on window.showPage.

src/client/Main.ts (2)

217-217: Field declaration looks correct now.

The hostModal field is assigned at line 478 before use. TypeScript's strict mode should catch any issues if the DOM query fails. The check at line 481 logs a warning if the element is missing.


915-936: Reasonable approach for hiding platform-specific elements.

The delayed calls at 100ms and 500ms help catch late-rendered components. This pattern works for most cases.

One edge case: components rendered after 500ms (lazy-loaded routes, dynamic modals) might slip through. If that becomes a problem later, a MutationObserver watching for new .no-crazygames elements would be more robust. For now, this is fine.

src/client/CrazyGamesSDK.ts (3)

51-68: Clean readiness pattern.

The promise-based readiness with timeout is a good approach. The 3-second timeout balances waiting for slow SDK loads against hanging forever. The resolveReady! definite assignment works because the constructor always sets it.


269-283: Well-structured invite link creation.

Good error handling with try-catch, proper null return on failure, and helpful logging. Follows the same pattern as other methods.


300-325: Clean ad request implementation.

The promise-based approach properly resolves in all cases: success (adFinished), failure (adError), not ready, and exceptions. This prevents hanging promises and lets callers continue regardless of ad outcome.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +3 to +4
import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
import { PauseGameIntentEvent } from "src/client/Transport";
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 | 🟡 Minor

Use relative import paths for consistency.

These imports use absolute-style paths (src/client/...) while the rest of the file uses relative paths (../../../core/...). Use relative paths for consistency.

Proposed fix
-import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
-import { PauseGameIntentEvent } from "src/client/Transport";
+import { crazyGamesSDK } from "../../CrazyGamesSDK";
+import { PauseGameIntentEvent } from "../../Transport";
📝 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
import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
import { PauseGameIntentEvent } from "src/client/Transport";
import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { PauseGameIntentEvent } from "../../Transport";
🤖 Prompt for AI Agents
In `@src/client/graphics/layers/SettingsModal.ts` around lines 3 - 4, Replace the
absolute-style imports in SettingsModal.ts for crazyGamesSDK and
PauseGameIntentEvent with relative paths; change import from
"src/client/CrazyGamesSDK" to "../../CrazyGamesSDK" and from
"src/client/Transport" to "../../Transport" so they match the file's existing
relative import style and project conventions, updating the import statements
that reference crazyGamesSDK and PauseGameIntentEvent accordingly.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/client/components/DesktopNavBar.ts (1)

128-141: Remove no-crazygames from the avatar image (line 137).

The avatar already has the hidden class, which always sets display: none. The no-crazygames class is for JavaScript hiding on CrazyGames platform, but it has no effect on an element that's already hidden. Remove it to keep the code clean:

Suggested change
          <img
            id="nav-account-avatar"
-           class="no-crazygames hidden w-8 h-8 rounded-full object-cover"
+           class="hidden w-8 h-8 rounded-full object-cover"
            alt=""
            data-i18n-alt="main.discord_avatar_alt"
            referrerpolicy="no-referrer"
          />
src/client/CrazyGamesSDK.ts (1)

70-84: Critical: return true makes rest of function unreachable.

Line 71 unconditionally returns true, so the iframe detection logic (lines 72-84) never runs. This looks like debug code that was accidentally left in.

🐛 Proposed fix: remove the debug return statement
 isOnCrazyGames(): boolean {
-  return true;
   try {
     // Check if we're in an iframe
     if (window.self !== window.top) {
src/client/UsernameInput.ts (1)

172-185: Fire-and-forget async call creates race condition.

The getUsername() method starts an async fetch but immediately returns the localStorage value. The async result arrives later and updates baseUsername, potentially overwriting what the user already typed.

Issues:

  1. return null; inside .then() does nothing useful
  2. No error handling for the promise
  3. Race condition between SDK response and user input
Proposed fix using proper async flow
- private getUsername(): string {
-   crazyGamesSDK.getUsername().then((username) => {
-     if (username) {
-       this.baseUsername = username;
-       this.requestUpdate();
-     }
-     return null;
-   });
-   const storedUsername = localStorage.getItem(usernameKey);
-   if (storedUsername) {
-     return storedUsername;
-   }
-   return this.generateNewUsername();
- }
+ private getUsername(): string {
+   // Async fetch from SDK - will update UI if successful
+   crazyGamesSDK.getUsername().then((username) => {
+     if (username && this.baseUsername === this.getStoredOrGeneratedUsername()) {
+       // Only override if user hasn't modified the username
+       this.parseAndSetUsername(username);
+       this.storeUsername(username);
+     }
+   }).catch((err) => {
+     console.warn("Failed to fetch CrazyGames username:", err);
+   });
+
+   return this.getStoredOrGeneratedUsername();
+ }
+
+ private getStoredOrGeneratedUsername(): string {
+   const storedUsername = localStorage.getItem(usernameKey);
+   if (storedUsername) {
+     return storedUsername;
+   }
+   return this.generateNewUsername();
+ }

This fix:

  1. Adds error handling
  2. Only updates if user hasn't changed the default username
  3. Properly stores the SDK username
♻️ Duplicate comments (9)
startup.sh (1)

88-92: Redundant conditional - both branches are identical.

This conditional no longer serves a purpose since both branches execute the same command.

src/client/graphics/layers/SettingsModal.ts (1)

3-4: Use relative import paths for consistency.

These imports use absolute-style paths while the rest of the file uses relative paths.

src/client/CrazyGamesSDK.ts (2)

123-129: Missing try-catch around SDK call.

If window.CrazyGames!.SDK.user.getUser() throws, the error propagates unhandled. Other methods in this class wrap SDK calls in try-catch blocks.


151-157: Same missing try-catch pattern.

For consistency with other SDK methods, wrap the SDK access in try-catch.

src/client/Main.ts (2)

217-217: Use definite assignment assertion for hostModal.

The field is assigned in initialize() but TypeScript doesn't know this. Add ! to indicate definite assignment.

Proposed fix
-  private hostModal: HostPrivateLobbyModal;
+  private hostModal!: HostPrivateLobbyModal;

597-604: Missing username validation before opening host modal.

This issue was already flagged. The hostLobbyButton handler (lines 486-501) validates the username before proceeding, but the instant multiplayer path skips this check. Add the same validation here.

src/client/Matchmaking.ts (1)

252-259: Memory leak: Event listener never removed.

This issue was already flagged. The userMeResponse listener added here is never cleaned up in disconnectedCallback. Store the handler as a class property and remove it when the component disconnects.

src/client/UsernameInput.ts (1)

43-49: Memory leak: Auth listener cannot be removed.

This was already flagged. The CrazyGames SDK lacks a removeAuthListener method, so this listener will stack if the component remounts.

Also, line 46 has redundant optional chaining: user?.username should be user.username since user is already checked for truthiness on line 45.

src/client/PatternInput.ts (1)

91-94: Remove the hardcoded return true; from isOnCrazyGames() in CrazyGamesSDK.ts.

The isOnCrazyGames() method has a hardcoded return true; at the start that makes all platform detection code unreachable. This causes the pattern input to hide on ALL platforms, not just CrazyGames. Delete the hardcoded return statement and let the platform detection logic execute properly.

Current buggy code:
isOnCrazyGames(): boolean {
  return true;
  try {
    // Check if we're in an iframe
    if (window.self !== window.top) {
      // Try to access parent URL
      return window?.top?.location?.hostname.includes("crazygames") ?? false;
    }
    return false;
  } catch (e) {
    // If we get a cross-origin error, we're definitely iframed
🧹 Nitpick comments (3)
src/client/graphics/layers/UnitDisplay.ts (1)

131-134: CSS changes look valid, but there is slight redundancy.

The Tailwind v4 syntax is correct:

  • z-[1100] uses proper arbitrary value brackets.
  • min-[1200px]:flex is valid arbitrary breakpoint syntax.

However, xl:flex-col (1280px) and min-[1200px]:flex-col overlap since 1280px > 1200px. Same goes for xl:gap-2 and min-[1200px]:gap-2. You can simplify by removing the xl: variants if the only goal is to show the component at 1200px+.

♻️ Suggested simplification
-        class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col min-[1200px]:flex-col 2xl:gap-5 xl:gap-2 min-[1200px]:gap-2 justify-center items-center"
+        class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row min-[1200px]:flex-col 2xl:gap-5 min-[1200px]:gap-2 justify-center items-center"
src/client/Matchmaking.ts (1)

307-325: Avoid as any casts; use proper types instead.

Casting DOM queries to any bypasses TypeScript's type checking. Define an interface or use the actual component types for better safety.

Proposed fix
 private handleLoggedInClick() {
-  const usernameInput = document.querySelector("username-input") as any;
-  const publicLobby = document.querySelector("public-lobby") as any;
+  const usernameInput = document.querySelector("username-input") as UsernameInput | null;
+  const publicLobby = document.querySelector("public-lobby") as PublicLobby | null;

-  if (usernameInput?.isValid()) {
+  if (usernameInput && usernameInput.isValid()) {
     this.open();
     publicLobby?.leaveLobby();
   } else {
     window.dispatchEvent(
       new CustomEvent("show-message", {
         detail: {
-          message: usernameInput?.validationError,
+          message: usernameInput?.validationError ?? "",
           color: "red",
           duration: 3000,
         },
       }),
     );
   }
 }

You'll need to import UsernameInput and PublicLobby at the top of the file.

src/client/Main.ts (1)

929-935: Multiple setTimeout calls are fragile; consider a single observer.

Using hardcoded delays (100ms, 500ms) to catch "late-rendered components" is unreliable. Components might render at different times depending on network or device speed. A MutationObserver would be more robust.

Alternative using MutationObserver
// In bootstrap or initialize
if (crazyGamesSDK.isOnCrazyGames()) {
  hideCrazyGamesElements();
  
  const observer = new MutationObserver(() => {
    hideCrazyGamesElements();
  });
  
  observer.observe(document.body, { 
    childList: true, 
    subtree: true 
  });
  
  // Stop observing after reasonable time
  setTimeout(() => observer.disconnect(), 5000);
}

The current approach works for now but may miss components that render after 500ms.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b032426 and 7e6ad63.

📒 Files selected for processing (16)
  • src/client/CrazyGamesSDK.ts
  • src/client/HostLobbyModal.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/client/components/PatternButton.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/LangSelector.ts
  • src/client/graphics/layers/GameRightSidebar.ts
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2026-01-13T20:16:20.098Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:20.098Z
Learning: In the OpenFrontIO codebase, `playerBySmallID(id: number)` in Game, GameImpl, and GameView explicitly returns `Player | TerraNullius` (or `PlayerView | TerraNullius`), never undefined. The method returns TerraNullius for ID 0 and Player otherwise. Code should check `.isPlayer()` directly without undefined guards or optional chaining.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/Main.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/UsernameInput.ts
  • src/client/Main.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/HostLobbyModal.ts
  • src/client/CrazyGamesSDK.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/SettingsModal.ts
📚 Learning: 2025-07-14T20:41:57.645Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1432
File: src/client/graphics/layers/NameLayer.ts:143-143
Timestamp: 2025-07-14T20:41:57.645Z
Learning: In the OpenFrontIO project, the Layer system (like NameLayer) doesn't use Lit framework and has no disconnectedCallback() or unmount mechanism available. This creates challenges for proper cleanup of event listeners and timeouts, leading to potential memory leaks. This is a known architectural limitation that should be addressed in future work.

Applied to files:

  • src/client/UsernameInput.ts
  • src/client/Matchmaking.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/DesktopNavBar.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/HostLobbyModal.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/SettingsModal.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 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/Main.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2026-01-08T13:52:08.203Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:08.203Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/Matchmaking.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2026-01-02T18:11:06.832Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:06.832Z
Learning: In src/client/HostLobbyModal.ts, the ?s=xxxxx URL suffix used in lobby URLs is purely for cache-busting platform previews (e.g., Discord, WhatsApp, x.com) and is not used by the join logic. The join flow ignores the suffix value, so regenerating it via updateUrlWithSuffix() on configuration changes will not break existing shared URLs; it only prompts platforms to refresh preview metadata. Treat the suffix as non-functional for join behavior and ensure any related changes preserve that invariant.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-07T13:15:55.439Z
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 786
File: src/core/Util.ts:4-4
Timestamp: 2025-06-07T13:15:55.439Z
Learning: In the OpenFrontIO codebase, JSON files should be imported using standard import syntax without import attributes, as the TypeScript configuration supports resolveJsonModule and the codebase already uses this pattern successfully in files like src/client/Cosmetic.ts.

Applied to files:

  • src/client/graphics/layers/SettingsModal.ts
🧬 Code graph analysis (6)
src/client/PatternInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/UsernameInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Main.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Matchmaking.ts (4)
src/core/ApiSchemas.ts (1)
  • UserMeResponse (78-78)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Utils.ts (1)
  • translateText (165-224)
src/client/HostLobbyModal.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/graphics/layers/SettingsModal.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (328-328)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (14)
src/client/components/DesktopNavBar.ts (1)

107-111: LGTM!

Clean addition of the no-crazygames marker class to hide the store button when running on CrazyGames. This follows the pattern seen in other components.

src/client/graphics/layers/SettingsModal.ts (1)

108-117: Logic looks correct for conditional pause handling.

The SDK calls (gameplayStop/gameplayStart) are properly guarded by the shouldPause && !wasPausedWhenOpened condition. The event emission follows the SDK state change - good ordering.

src/client/CrazyGamesSDK.ts (2)

48-68: Readiness pattern is clean and well-structured.

The Promise.race approach with a 3-second timeout is a solid pattern. The constructor initializes the deferred promise correctly, and ready() provides a safe way for consumers to wait for SDK initialization.


300-325: requestMidgameAd handles all callback paths correctly.

The promise resolves on adFinished, adError, and synchronous exceptions - good defensive coding. No hanging promises.

src/client/HostLobbyModal.ts (2)

115-124: CrazyGames invite link path looks good.

The fallback logic is correct: try SDK invite link first, fall back to standard URL construction if null. Note this depends on the isOnCrazyGames() fix in CrazyGamesSDK.ts - once that debug return true is removed, this will correctly gate the SDK path.


131-135: Correct: skip history changes when embedded in CrazyGames.

When running inside the CrazyGames iframe, the parent controls navigation. Skipping history.replaceState avoids conflicts with the host platform.

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

139-142: Clean component extraction.

Replacing the button blocks with a single <matchmaking-button> component keeps the template lean and moves state management (login checks, modal handling) to the right place. The component is properly registered and handles both logged-in and logged-out states with appropriate callbacks.

src/client/Matchmaking.ts (3)

6-14: Imports look good.

The new imports for hasLinkedAccount and crazyGamesSDK are correctly added and used throughout the component.


267-270: Early return for CrazyGames is appropriate.

This pattern matches the approach used in other components (e.g., PatternInput.ts). Returning an empty template when on CrazyGames keeps the UI clean.


327-329: Consider null check for window.showPage.

window.showPage is optional (showPage?: ...). The optional chaining is correct, but the function silently does nothing if showPage is undefined. This is fine if intended, just noting for awareness.

src/client/Main.ts (2)

478-483: Initialization looks correct.

The host modal is properly queried and assigned. The warning for missing element is helpful for debugging.


915-922: Simple and effective hide function.

The function clearly hides elements marked with .no-crazygames class when on the CrazyGames platform.

src/client/PatternInput.ts (1)

8-8: Import is correctly added.

The crazyGamesSDK singleton import follows the established pattern.

src/client/UsernameInput.ts (1)

11-11: Import is correctly added.

The SDK singleton import follows the project pattern.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/UsernameInput.ts (1)

172-185: Logic issue: SDK username bypasses parseAndSetUsername.

The async SDK username update (line 175) sets baseUsername directly, but the connectedCallback uses parseAndSetUsername (line 51) which handles clan tags. This means if the SDK returns a username with a clan tag like [TAG] Player, it won't be parsed correctly.

Also, return null on line 178 is dead code since the return value is never used.

Proposed fix
   private getUsername(): string {
     crazyGamesSDK.getUsername().then((username) => {
       if (username) {
-        this.baseUsername = username;
+        this.parseAndSetUsername(username);
-        this.requestUpdate();
       }
-      return null;
     });
     const storedUsername = localStorage.getItem(usernameKey);
     if (storedUsername) {
       return storedUsername;
     }
     return this.generateNewUsername();
   }
🤖 Fix all issues with AI agents
In `@src/client/UsernameInput.ts`:
- Around line 41-52: The async SDK path, localStorage path and auth-listener
path set baseUsername inconsistently causing skipped clan-tag parsing and
validation; consolidate into a single setter method (e.g., create/rename to
setParsedUsername or normalizeUsername) and update all call sites
(connectedCallback’s stored flow, crazyGamesSDK.getCurrentUser()/async callback,
and the auth listener) to call that method instead of assigning baseUsername
directly or calling inconsistent helpers; ensure this central method runs
clan-tag extraction, validation, sets baseUsername, updates any derived fields,
and calls requestUpdate() to avoid multiple renders/flicker.
♻️ Duplicate comments (4)
src/client/graphics/layers/GameRightSidebar.ts (1)

116-128: Add error handling to prevent users getting stuck on exit.

If requestMidgameAd() or gameplayStop() throws, the redirect never happens and the user is stuck. Wrap in try-catch to ensure the redirect always runs.

Proposed fix
 private async onExitButtonClick() {
   const isAlive = this.game.myPlayer()?.isAlive();
   if (isAlive) {
     const isConfirmed = confirm(
       translateText("help_modal.exit_confirmation"),
     );
     if (!isConfirmed) return;
   }
-  await crazyGamesSDK.requestMidgameAd();
-  await crazyGamesSDK.gameplayStop();
-  // redirect to the home page
-  window.location.href = "/";
+  try {
+    await crazyGamesSDK.requestMidgameAd();
+    await crazyGamesSDK.gameplayStop();
+  } catch (err) {
+    console.error("SDK error on exit:", err);
+  }
+  window.location.href = "/";
 }
src/client/Matchmaking.ts (1)

252-259: Missing cleanup for event listener causes memory leak.

This event listener is added in connectedCallback but never removed. If the component is removed and re-added to the DOM, listeners will stack up.

Proposed fix
+  private boundUserMeHandler = (event: Event) => {
+    const customEvent = event as CustomEvent;
+    if (customEvent.detail) {
+      const userMeResponse = customEvent.detail as UserMeResponse | false;
+      this.isLoggedIn = hasLinkedAccount(userMeResponse);
+    }
+  };

   async connectedCallback() {
     super.connectedCallback();
-    // Listen for user authentication changes
-    document.addEventListener("userMeResponse", (event: Event) => {
-      const customEvent = event as CustomEvent;
-      if (customEvent.detail) {
-        const userMeResponse = customEvent.detail as UserMeResponse | false;
-        this.isLoggedIn = hasLinkedAccount(userMeResponse);
-      }
-    });
+    document.addEventListener("userMeResponse", this.boundUserMeHandler);
   }

+  disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener("userMeResponse", this.boundUserMeHandler);
+  }
src/client/Main.ts (1)

597-604: Add username validation before opening the host modal.

The hostLobbyButton handler (lines 486-501) validates the username before showing the host page. The instant multiplayer path calls this.hostModal.open() without this check, which could allow creating a lobby with an invalid username.

Proposed fix
     crazyGamesSDK.isInstantMultiplayer().then((isInstant) => {
       if (isInstant) {
         console.log(
           `CrazyGames: joining instant multiplayer lobby from CrazyGames`,
         );
-        this.hostModal.open();
+        if (this.usernameInput?.isValid()) {
+          this.hostModal.open();
+        } else {
+          window.dispatchEvent(
+            new CustomEvent("show-message", {
+              detail: {
+                message: this.usernameInput?.validationError,
+                color: "red",
+                duration: 3000,
+              },
+            }),
+          );
+        }
       }
     });
src/client/UsernameInput.ts (1)

43-49: Memory leak: Auth listener cannot be removed due to SDK limitations.

The auth listener added here is never cleaned up. The CrazyGames SDK does not expose a removeAuthListener method.

Also, on line 46, the optional chaining user?.username is redundant since user is already checked for truthiness on line 45.

Fix redundant optional chaining
     crazyGamesSDK.addAuthListener((user) => {
       console.log(`got user: ${user}`);
       if (user) {
-        this.parseAndSetUsername(user?.username);
+        this.parseAndSetUsername(user.username);
       }
       this.requestUpdate();
     });
🧹 Nitpick comments (3)
src/client/CrazyGamesSDK.ts (1)

277-291: Consider using async ready() for consistency.

createInviteLink() uses sync isReady() while getUsername() and isInstantMultiplayer() use async ready(). If called early during initialization, this method will return null even though the SDK might become ready shortly after.

If this is intentional (because invite links are only created after game starts), this is fine. Otherwise, consider making it async.

src/client/Matchmaking.ts (1)

307-325: Consider using proper types instead of as any casts.

The as any casts work but lose type safety. If UsernameInput and PublicLobby are exported types, import them for better type checking.

Suggested improvement
+import { UsernameInput } from "./UsernameInput";
+import { PublicLobby } from "./PublicLobby";

   private handleLoggedInClick() {
-    const usernameInput = document.querySelector("username-input") as any;
-    const publicLobby = document.querySelector("public-lobby") as any;
+    const usernameInput = document.querySelector("username-input") as UsernameInput | null;
+    const publicLobby = document.querySelector("public-lobby") as PublicLobby | null;

     if (usernameInput?.isValid()) {
src/client/Main.ts (1)

915-936: Timeout-based hiding is a reasonable workaround.

The multiple setTimeout calls handle late-rendered components. This works but is fragile if components render after 500ms. Consider using a MutationObserver for a more robust solution if this becomes unreliable.

Alternative with MutationObserver
const hideCrazyGamesElements = () => {
  if (!crazyGamesSDK.isOnCrazyGames()) return;
  
  document.querySelectorAll(".no-crazygames").forEach((el) => {
    (el as HTMLElement).style.display = "none";
  });
};

// Run once immediately
hideCrazyGamesElements();

// Watch for new elements
if (crazyGamesSDK.isOnCrazyGames()) {
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      mutation.addedNodes.forEach((node) => {
        if (node instanceof HTMLElement) {
          if (node.classList.contains("no-crazygames")) {
            node.style.display = "none";
          }
          node.querySelectorAll(".no-crazygames").forEach((el) => {
            (el as HTMLElement).style.display = "none";
          });
        }
      });
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e6ad63 and 680eb46.

📒 Files selected for processing (16)
  • src/client/CrazyGamesSDK.ts
  • src/client/HostLobbyModal.ts
  • src/client/LangSelector.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/SinglePlayerModal.ts
  • src/client/UsernameInput.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PlayPage.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/WinModal.ts
  • startup.sh
🚧 Files skipped from review as they are similar to previous changes (8)
  • startup.sh
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/components/PlayPage.ts
  • src/client/HostLobbyModal.ts
  • src/client/components/PatternButton.ts
  • src/client/graphics/layers/WinModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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-07-23T12:36:35.354Z
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 1534
File: src/client/LangSelector.ts:97-106
Timestamp: 2025-07-23T12:36:35.354Z
Learning: In OpenFrontIO's LangSelector.ts, the getClosestSupportedLang method always joins language code parts with underscores ("_") because all keys in the languageMap use underscore format (e.g., pt_BR, sv_SE, zh_CN). This normalization ensures consistency regardless of whether the input language code uses hyphens or underscores as delimiters.

Applied to files:

  • src/client/LangSelector.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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/LangSelector.ts
  • src/client/Main.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/Matchmaking.ts
📚 Learning: 2026-01-12T21:37:01.156Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2874
File: src/server/MapLandTiles.ts:7-11
Timestamp: 2026-01-12T21:37:01.156Z
Learning: In this repository's OpenFrontIO deployment, inter-service HTTP calls to the master should target http://localhost:3000 (master at port 3000) as the canonical address. Apply this as the standard for all server-side TypeScript code that communicates with the master. Avoid hardcoding non-master URLs; centralize the master address (e.g., via config or env) when possible, and ensure internal service communication uses localhost:3000 in this architecture.

Applied to files:

  • src/client/LangSelector.ts
  • src/client/PatternInput.ts
  • src/client/Main.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/components/DesktopNavBar.ts
  • src/client/UsernameInput.ts
  • src/client/Matchmaking.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-13T20:16:20.098Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:20.098Z
Learning: In the OpenFrontIO codebase, `playerBySmallID(id: number)` in Game, GameImpl, and GameView explicitly returns `Player | TerraNullius` (or `PlayerView | TerraNullius`), never undefined. The method returns TerraNullius for ID 0 and Player otherwise. Code should check `.isPlayer()` directly without undefined guards or optional chaining.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/Main.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/PatternInput.ts
  • src/client/Main.ts
  • src/client/UsernameInput.ts
  • src/client/CrazyGamesSDK.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 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/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2026-01-08T13:52:08.203Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:08.203Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/Matchmaking.ts
📚 Learning: 2026-01-13T20:16:05.535Z
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2887
File: src/core/execution/NukeExecution.ts:118-122
Timestamp: 2026-01-13T20:16:05.535Z
Learning: In code paths that return a Player-like object, prefer returning a union type (Player | TerraNullius) instead of undefined. When a function may fail to find a player, return TerraNullius for the 'not found' case and a Player for valid IDs, and check .isPlayer() (or equivalent) directly on the result instead of guarding with undefined or optional chaining. This should be enforced in Game, GameImpl, and GameView (and similar accessors) to avoid undefined checks and simplify null-safety handling.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-07-14T20:41:57.645Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1432
File: src/client/graphics/layers/NameLayer.ts:143-143
Timestamp: 2025-07-14T20:41:57.645Z
Learning: In the OpenFrontIO project, the Layer system (like NameLayer) doesn't use Lit framework and has no disconnectedCallback() or unmount mechanism available. This creates challenges for proper cleanup of event listeners and timeouts, leading to potential memory leaks. This is a known architectural limitation that should be addressed in future work.

Applied to files:

  • src/client/UsernameInput.ts
  • src/client/Matchmaking.ts
🧬 Code graph analysis (5)
src/client/PatternInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (336-336)
src/client/Main.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (336-336)
src/client/graphics/layers/GameRightSidebar.ts (2)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (336-336)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (33-35)
src/client/UsernameInput.ts (1)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (336-336)
src/client/Matchmaking.ts (4)
src/core/ApiSchemas.ts (1)
  • UserMeResponse (78-78)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/client/CrazyGamesSDK.ts (1)
  • crazyGamesSDK (336-336)
src/client/LangSelector.ts (1)
  • translateText (278-302)
🔇 Additional comments (14)
src/client/components/DesktopNavBar.ts (1)

108-108: LGTM! Clean styling hooks for platform-specific UI.

The no-crazygames class additions provide a consistent way to hide store and account elements when running on the CrazyGames platform. This approach keeps the render logic simple and delegates visibility to CSS.

Also applies to: 130-130, 137-137

src/client/LangSelector.ts (1)

231-231: LGTM! Correct addition for translation support.

Adding "matchmaking-button" to the components list ensures it receives requestUpdate() calls when the language changes. This keeps translation behavior consistent with other components.

src/client/graphics/layers/GameRightSidebar.ts (1)

106-114: LGTM! SDK integration for pause/resume is correct.

The gameplay state tracking via crazyGamesSDK.gameplayStop() and gameplayStart() aligns with the pause toggle. Fire-and-forget calls are acceptable here since the pause event emission should not wait for SDK acknowledgment.

src/client/CrazyGamesSDK.ts (3)

48-68: LGTM! Clean readiness mechanism with timeout.

The readyPromise pattern with a 3-second timeout in ready() is a good approach. It prevents indefinite waits while giving the SDK time to initialize.


122-165: LGTM! Async methods with proper error handling.

getUsername(), addAuthListener(), and isInstantMultiplayer() all use the async ready() pattern with try-catch blocks. This addresses the previous review feedback.


308-333: Good callback-to-Promise wrapper.

The requestMidgameAd() implementation correctly wraps the callback-based SDK API in a Promise. It resolves on both adFinished and adError, ensuring callers are never left waiting.

One note: this uses sync isReady() like createInviteLink(). Same optional consideration applies for consistency.

src/client/PatternInput.ts (2)

91-94: LGTM! Clean platform check for conditional rendering.

The early return when on CrazyGames hides the pattern customization UI on that platform. The isOnCrazyGames() method properly checks iframe context and referrer.


8-8: Verify the import is used correctly.

The import adds a dependency on the SDK singleton. This is fine since the component needs to check the platform.

src/client/Matchmaking.ts (3)

6-6: Imports look good.

The hasLinkedAccount utility and crazyGamesSDK singleton are correctly imported for the new authentication-aware rendering logic.

Also applies to: 12-12


266-304: Render logic is clear and well-structured.

The conditional rendering follows a clean pattern: first check for CrazyGames platform, then branch based on login state. Both paths include the matchmaking-modal component which is correct.


327-329: Handler is simple and correct.

The handleLoggedOutClick method correctly navigates to the account page using the global showPage function.

src/client/Main.ts (2)

217-217: Field declaration looks good.

The hostModal field type is correctly declared. Based on past review, this was addressed.


478-483: Host modal initialization is correct.

The initialization properly queries the DOM and validates the element type. The warning helps with debugging if the element is missing.

src/client/UsernameInput.ts (1)

11-11: Import is correct.

The crazyGamesSDK singleton is properly imported.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@evanpelle evanpelle changed the title crazygames Better crazygames support Jan 15, 2026
@evanpelle evanpelle marked this pull request as ready for review January 15, 2026 03:42
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/Matchmaking.ts (1)

24-36: Remove the userMeResponse listener on disconnect (and reset ELO on logout).

The listener is added but never removed, so it can stack up if the element is re-created. Also, when the event detail is false, the ELO stays stale.

✅ Suggested fix
 export class MatchmakingModal extends BaseModal {
+  private readonly onUserMeResponse = (event: Event) => {
+    const customEvent = event as CustomEvent;
+    const userMeResponse = customEvent.detail as UserMeResponse | false | undefined;
+    if (!userMeResponse) {
+      this.elo = "unknown";
+    } else {
+      this.elo =
+        userMeResponse.player?.leaderboard?.oneVone?.elo?.toString() ?? "unknown";
+    }
+    this.requestUpdate();
+  };
+
   constructor() {
     super();
     this.id = "page-matchmaking";
-    document.addEventListener("userMeResponse", (event: Event) => {
-      const customEvent = event as CustomEvent;
-      if (customEvent.detail) {
-        const userMeResponse = customEvent.detail as UserMeResponse;
-        this.elo =
-          userMeResponse.player?.leaderboard?.oneVone?.elo?.toString() ??
-          "unknown";
-        this.requestUpdate();
-      }
-    });
+    document.addEventListener("userMeResponse", this.onUserMeResponse);
   }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener("userMeResponse", this.onUserMeResponse);
+  }
🧹 Nitpick comments (4)
src/client/PatternInput.ts (1)

49-58: Skip cosmetics fetch on CrazyGames to avoid wasted work.

Right now the component still fetches cosmetics and registers listeners even when the UI is hidden. Consider an early return to save network and setup work.

♻️ Suggested change
  async connectedCallback() {
    super.connectedCallback();
+   if (crazyGamesSDK.isOnCrazyGames()) {
+     this.isLoading = false;
+     return;
+   }
    this._abortController = new AbortController();
    this.isLoading = true;
    const cosmetics = await fetchCosmetics();
src/client/Main.ts (2)

612-631: Why the 2 second delay before opening the join modal?

Line 625 has a hardcoded 2000ms delay. This seems arbitrary. Consider:

  1. Adding a comment explaining why this delay is needed
  2. Using a smaller delay if it's just for UI rendering
  3. Waiting for a specific condition instead of a fixed timeout
       if (lobbyId && GAME_ID_REGEX.test(lobbyId)) {
         console.log("game parsed successfully");
-        await new Promise((resolve) => setTimeout(resolve, 2000));
+        // Wait for UI to be ready before showing modal
+        await new Promise((resolve) => setTimeout(resolve, 500));
         window.showPage?.("page-join-private-lobby");

957-978: Consider using MutationObserver instead of multiple timeouts.

The delayed calls at 100ms and 500ms are a workaround for late-rendered components. A MutationObserver would be more robust and react immediately when new elements are added:

const hideCrazyGamesElements = () => {
  if (!crazyGamesSDK.isOnCrazyGames()) return;
  
  const hideAll = () => {
    document.querySelectorAll(".no-crazygames").forEach((el) => {
      (el as HTMLElement).style.display = "none";
    });
  };
  
  hideAll();
  
  // Watch for dynamically added elements
  const observer = new MutationObserver(hideAll);
  observer.observe(document.body, { childList: true, subtree: true });
};

That said, the current approach is simple and works. This is optional.

src/client/Matchmaking.ts (1)

167-170: Use hasLinkedAccount for the login check.

This avoids duplicated logic and keeps login rules consistent across the UI.

♻️ Suggested change
-    const isLoggedIn =
-      userMe &&
-      userMe.user &&
-      (userMe.user.discord !== undefined || userMe.user.email !== undefined);
+    const isLoggedIn = hasLinkedAccount(userMe);

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

🤖 Fix all issues with AI agents
In `@resources/lang/id.json`:
- Around line 1-953: This PR incorrectly includes non‑English locale changes
(the top-level JSON keys like "lang" and "common" plus many translated strings
such as "close","join_discord","title" etc. inside the Indonesian locale
object); revert this file back to its previous state (remove all edits to the
Indonesian locale JSON so only changes to resources/lang/en.json remain in this
PR) by restoring the original content for the Indonesian locale (undo edits to
the "lang", "common", "main", and other top-level sections in that JSON) and
leave translation updates to the dedicated translation PRs.

In `@src/client/GutterAds.ts`:
- Around line 25-35: The connectedCallback in GutterAds adds a "userMeResponse"
listener but never removes it; create a named/bound handler property (e.g.,
this._onUserMeResponse or this.handleUserMeResponse) when attaching the listener
in connectedCallback and then remove the same handler in disconnectedCallback
using document.removeEventListener("userMeResponse", this._onUserMeResponse);
ensure the handler calls the existing logic (checking window.adsEnabled and
calling this.show()) so the listener can be cleanly detached when the component
is removed.

In `@src/client/Main.ts`:
- Line 434: Call to updateMatchmakingButton(isLinked) is failing because
updateMatchmakingButton is not defined or imported; either add a definition in
Main.ts that mirrors the pattern used by updateAccountNavButton (e.g., a
function that accepts isLinked and updates the matchmaking UI/button state) or
import it from the module that provides matchmaking UI helpers and ensure it's
exported there. Locate the call site (updateMatchmakingButton) and implement a
function named updateMatchmakingButton(isLinked: boolean) or import the existing
implementation, wiring it to the same DOM elements and state logic style used by
updateAccountNavButton so the call resolves and the matchmaking button updates
correctly.
🧹 Nitpick comments (10)
src/client/Matchmaking.ts (3)

266-269: Clarify or remove the commented-out CrazyGames guard.

This commented-out code suggests future intent but lacks explanation. If CrazyGames support is planned, add a TODO with a tracking issue. If not needed, remove it to reduce noise.

The button still has the no-crazygames CSS class (line 275, 295) which may hide it via CSS, making this guard redundant anyway.


307-308: Avoid any type - use proper interface or type assertion.

Casting to any loses type safety. Define a minimal interface for the methods you need, or use a more specific type.

Example interface
interface UsernameInputElement extends HTMLElement {
  isValid(): boolean;
  validationError: string | undefined;
}

interface PublicLobbyElement extends HTMLElement {
  leaveLobby(): void;
}

// Then use:
const usernameInput = document.querySelector("username-input") as UsernameInputElement | null;
const publicLobby = document.querySelector("public-lobby") as PublicLobbyElement | null;

326-328: Silent failure if window.showPage is undefined.

If showPage is not defined on window, the button click does nothing with no feedback. Consider logging a warning or showing a message to the user.

Suggested fix
   private handleLoggedOutClick() {
-    window.showPage?.("page-account");
+    if (window.showPage) {
+      window.showPage("page-account");
+    } else {
+      console.warn("showPage not available on window");
+    }
   }
src/core/execution/utils/AiAttackBehavior.ts (2)

840-849: Consider excluding self from teammates list.

The filter this.player.isOnSameTeam(p) may include the current player (a player is typically on the same team as itself). While canDonateTroops at line 863 likely rejects self-donation, explicitly excluding self avoids unnecessary iteration.

Optional: Exclude self from teammates
     const teammates = this.game
       .players()
-      .filter((p) => this.player.isOnSameTeam(p))
+      .filter((p) => p !== this.player && this.player.isOnSameTeam(p))
       .filter(
         (p) => p.incomingAttacks().length > 0 || p.outgoingAttacks().length > 0,
       );

103-108: Minor duplication of max boat check.

The same max boat check appears here and in findNearestIslandEnemy (lines 539-544). If this check is needed in more places, consider extracting it to a small helper like hasReachedBoatLimit(). For now, the duplication is acceptable.

src/client/graphics/layers/InGameHeaderAd.ts (1)

71-75: Consider destroying the ad unit when hiding.

The hideHeaderAd() method sets shouldShow = false but does not call window.ramp.destroyUnits(HEADER_AD_TYPE). This may leave orphaned ad slots. The GutterAds component in the codebase does not explicitly destroy either, but for a timed ad that disappears mid-game, cleanup may be warranted.

Possible cleanup addition
 private hideHeaderAd(): void {
   this.shouldShow = false;
   this.adLoaded = false;
+  if (window.ramp?.destroyUnits) {
+    try {
+      window.ramp.destroyUnits(HEADER_AD_TYPE);
+    } catch (e) {
+      console.warn("Failed to destroy header ad unit:", e);
+    }
+  }
   this.requestUpdate();
 }
src/client/Main.ts (1)

939-959: Consider using MutationObserver instead of multiple setTimeout calls.

The hideCrazyGamesElements function is called three times with delays (0ms, 100ms, 500ms) to catch late-rendered components. A MutationObserver would be more reliable and efficient for handling dynamically added elements.

Alternative approach with MutationObserver
const hideCrazyGamesElements = () => {
  if (!crazyGamesSDK.isOnCrazyGames()) return;
  
  const hideElements = () => {
    document.querySelectorAll(".no-crazygames").forEach((el) => {
      (el as HTMLElement).style.display = "none";
    });
  };
  
  hideElements();
  
  const observer = new MutationObserver(hideElements);
  observer.observe(document.body, { childList: true, subtree: true });
  
  // Stop observing after 2 seconds
  setTimeout(() => observer.disconnect(), 2000);
};
index.html (1)

365-371: Avoid semantic <footer> just to wrap a script.

If global styles target footer, this wrapper can cause unexpected layout or spacing. A plain script tag is safer.

♻️ Suggested change
-    <footer>
-      <script
-        data-cfasync="false"
-        async
-        src="//cdn.intergient.com/1025558/75940/ramp.js"
-      ></script>
-    </footer>
+    <script
+      data-cfasync="false"
+      async
+      src="//cdn.intergient.com/1025558/75940/ramp.js"
+    ></script>
src/client/GutterAds.ts (2)

23-23: Remove unused css import.

The css import is added but static styles is empty. Either use it or remove the import.

Proposed fix
-import { LitElement, css, html } from "lit";
+import { LitElement, html } from "lit";

And remove line 23:

-  static styles = css``;

93-94: Use console.error for error logging.

For consistency with other error handling in this file (line 98), use console.error instead of console.log for caught exceptions.

Proposed fix
         } catch (e) {
-          console.log(e);
+          console.error("Failed to add ads via Ramp:", e);
         }

Comment on lines +1 to +953
{
"lang": {
"en": "Indonesian",
"native": "Bahasa Indonesia",
"svg": "id",
"lang_code": "id"
},
"common": {
"close": "Keluar",
"back": "Kembali",
"available": "Tersedia",
"preset_max": "Maks",
"summary_send": "Kirim",
"summary_keep": "Simpan",
"cancel": "Batalkan",
"send": "Kirim",
"cap_label": "Batas maksimal",
"cap_tooltip": "Kapasitas penerima yang tersisa",
"target_dead": "Target dieliminasi",
"target_dead_note": "Anda tidak dapat mengirim sumber daya ke pemain yang telah tereliminasi.",
"none": "Tidak Satupun",
"copied": "Tersalin",
"click_to_copy": "Klik untuk salin"
},
"main": {
"title": "OpenFront (ALPHA)",
"join_discord": "Discord",
"login_discord": "Masuk dengan Discord",
"sign_in": "Masuk",
"discord_avatar_alt": "Avatar profil Discord",
"user_avatar_alt": "Avatar {username}",
"checking_login": "Memeriksa login...",
"logged_in": "Berhasil masuk!",
"log_out": "Keluar",
"create": "Buat Lobi",
"join": "Bergabung ke Lobi",
"solo": "Sendiri",
"instructions": "Petunjuk",
"game_info": "Informasi Permainan",
"wiki": "Wiki",
"privacy_policy": "Kebijakan Privasi",
"terms_of_service": "Ketentuan Layanan",
"copyright": "© OpenFront™ dan para kontributor",
"reddit": "Reddit",
"play": "Main",
"news": "Berita",
"store": "Toko",
"settings": "Pengaturan",
"keys": "Tombol",
"stats": "Statistik",
"account": "Akun",
"help": "Bantuan",
"menu": "Menu",
"pick_pattern": "Pilih pola!"
},
"news": {
"github_link": "di GitHub",
"title": "Catatan Rilis"
},
"help_modal": {
"hotkeys": "Tombol pintas",
"table_key": "Kunci",
"table_action": "Tindakan",
"action_alt_view": "Ganti Tampilan (Medan / Negara)",
"action_attack_altclick": "Serang (saat klik kiri diatur untuk membuka menu)",
"action_build": "Buka menu Pembangunan",
"action_emote": "Buka menu Ekspresi",
"action_center": "Pusatkan kamera pada pemain",
"action_zoom": "Perkecil / Perbesar tampilan",
"action_move_camera": "Pindahkan kamera",
"action_ratio_change": "Kurangi / Tingkatkan rasio serangan",
"action_reset_gfx": "Atur ulang grafis",
"action_auto_upgrade": "Tingkatkan bangunan terdekat secara otomatis",
"ui_section": "UI Permainan",
"ui_leaderboard": "Papan Peringkat",
"ui_your_team": "Tim anda:",
"ui_leaderboard_desc": "Menampilkan pemain teratas dalam permainan beserta nama mereka, persentase wilayah yang dikuasai, jumlah emas, dan pasukan. Opsi Tampilkan Semua akan menampilkan seluruh pemain dalam permainan. Jika tidak ingin melihat papan peringkat, klik Sembunyikan.",
"ui_control": "Panel kendali",
"ui_control_desc": "Panel kontrol berisi elemen berikut:",
"ui_pop": "Populasi - Jumlah unit yang kamu miliki, batas populasi maksimum, serta laju pertambahannya.",
"ui_gold": "Emas - Jumlah emas yang kamu miliki dan laju perolehannya.",
"ui_attack_ratio": "Rasio Serangan - Jumlah pasukan yang akan digunakan saat kamu menyerang. Kamu dapat menyesuaikan rasio serangan menggunakan penggeser. Memiliki pasukan penyerang lebih banyak daripada pasukan bertahan akan mengurangi jumlah pasukan yang hilang saat menyerang, sedangkan jumlah pasukan yang lebih sedikit akan meningkatkan kerusakan yang diterima pasukan penyerang. Efek ini tidak berlaku di atas rasio 2:1.",
"ui_events": "Panel Event",
"ui_events_desc": "Panel Event menampilkan peristiwa, permintaan, dan pesan Obrolan Cepat terbaru. Beberapa contohnya adalah:",
"ui_events_alliance": "Aliansi - Permintaan aliansi dapat diterima atau ditolak. Sekutu dapat berbagi sumber daya dan pasukan, tetapi tidak dapat saling menyerang. Menekan Fokus akan memusatkan tampilan ke pemain yang mengirim permintaan.",
"ui_events_attack": "Serangan - Menampilkan serangan yang masuk dan serangan yang kamu lakukan. Klik pesan untuk memusatkan tampilan ke lokasi serangan, nuklir, atau Kapal (kapal pengangkut). Kamu dapat menarik mundur pasukan dengan menekan tombol X merah. Tindakan ini akan mengorbankan 25% dari pasukan penyerang.\nJika serangan Kapal ditarik kembali, kapal akan kembali ke titik awal dan akan menyerang kembali di sana jika wilayah tersebut telah dikuasai sejak saat itu. Serangan nuklir tidak dapat ditarik kembali setelah diluncurkan.",
"ui_events_quickchat": "Obrolan Cepat - Di sini kamu dapat melihat pesan obrolan yang dikirim dan diterima. Untuk mengirim pesan ke pemain, klik ikon Obrolan Cepat di menu Info pemain tersebut.",
"ui_options": "Pilihan",
"ui_options_desc": "Elemen-elemen berikut dapat ditemukan di dalamnya:",
"ui_playeroverlay": "Overlay Info Pemain",
"ui_playeroverlay_desc": "Saat kamu mengarahkan kursor ke suatu negara, overlay Info Pemain akan ditampilkan di bawah menu Opsi. Overlay ini menampilkan jenis pemain: Manusia, Negara (bot pintar), atau Bot; sikap suatu Negara terhadapmu, mulai dari Bermusuhan hingga Ramah; serta jumlah pasukan bertahan, emas, jumlah Kapal Perang, dan berbagai bangunan yang dimiliki pemain tersebut.",
"ui_wilderness": "Alam Liar",
"option_pause": "Jeda / Lanjutkan permainan – Hanya tersedia dalam mode single-player.",
"option_timer": "Timer – Waktu yang telah berlalu sejak permainan dimulai.",
"option_exit": "Tombol keluar.",
"option_settings": "Pengaturan – Membuka menu pengaturan. Di dalamnya kamu dapat mengaktifkan atau menonaktifkan Tampilan Alternatif, Emoji, Mode Gelap, Ninja (mode anonim / nama acak), serta aksi pada klik kiri.",
"radial_title": "Menu Radial",
"radial_desc": "Klik kanan (atau sentuhan di perangkat seluler) akan membuka Menu Radial. Klik kanan di luar menu untuk menutupnya. Dari menu ini kamu dapat:",
"radial_build": "Buka menu Pembangunan.",
"radial_attack": "Buka menu Serangan.",
"radial_info": "Buka menu informasi.",
"radial_boat": "Kirim Kapal (kapal pengangkut) untuk menyerang lokasi yang dipilih. Hanya tersedia jika kamu memiliki akses ke perairan.",
"radial_donate_troops": "Donasikan pasukan kepada sekutu sesuai dengan persentase pada penggeser rasio serangan yang sedang kamu gunakan pada menu radial tersebut.",
"radial_donate_gold": "Membuka menu penggeser donasi emas sehingga kamu dapat dengan cepat mengirim emas kepada sekutu.",
"radial_close": "Tutup menu.",
"info_title": "Menu Informasi",
"info_enemy_desc": "Berisi informasi seperti nama pemain yang dipilih, jumlah emas, pasukan, status berhenti berdagang dengan kamu, nuklir yang dikirim ke arahmu, serta apakah pemain tersebut adalah pengkhianat.\nStatus Berhenti Berdagang berarti kamu tidak akan menerima emas dari pemain tersebut dan mereka juga tidak akan mengirimkan emas kepadamu melalui kapal dagang. Status ini dapat terjadi secara manual (jika pemain menekan tombol “Hentikan Perdagangan”, yang akan berlangsung sampai kalian berdua menekan “Mulai Perdagangan”) atau secara otomatis (jika kamu mengkhianati aliansi, yang akan berlangsung sampai kalian kembali menjadi sekutu atau setelah 5 menit).\nStatus Pengkhianat akan menampilkan “Ya” selama 30 detik ketika pemain tersebut mengkhianati dan menyerang pemain yang sebelumnya berada dalam aliansi dengannya.\nIkon-ikon di bawah ini mewakili interaksi berikut:",
"info_chat": "Kirim pesan Obrolan Cepat ke pemain. Pilih Kategori, Frasa, dan jika frasa berisi [P1], pilih nama pemain untuk menggantikannya. Lalu tekan Kirim.",
"info_target": "Pasang tanda target pada pemain, sehingga terlihat oleh semua sekutu. Digunakan untuk mengoordinasikan serangan.",
"info_alliance": "Kirim permintaan aliansi ke pemain. Sekutu dapat berbagi sumber daya dan pasukan, tetapi tidak dapat saling menyerang.",
"info_emoji": "Kirim emoji ke pemainnya.",
"info_trade": "Gunakan “Hentikan Perdagangan” untuk berhenti memberikan emas kepada pemain tersebut dan berhenti menerima emas dari mereka melalui kapal dagang. Jika kalian berdua menekan “Mulai\".",
"info_ally_panel": "Panel Info Sekutu",
"info_ally_desc": "Saat kamu beraliansi dengan seorang pemain, ikon-ikon baru berikut akan tersedia:",
"ally_betray": "Mengkhianati sekutumu akan mengakhiri aliansi, menghentikan perdagangan, dan melemahkan pertahananmu. Perdagangan di antara kalian akan dijeda selama 5 menit (atau sampai kalian kembali menjadi sekutu), dan pemain lain juga dapat menghentikan perdagangan. Kecuali jika pemain lain tersebut memang sudah berstatus pengkhianat, kamu akan ditandai sebagai Pengkhianat selama 30 detik.\nSelama waktu ini, sebuah ikon akan muncul di atas namamu dan kamu akan menerima debuff pertahanan sebesar 50%. Bot akan lebih enggan beraliansi denganmu, dan pemain lain akan berpikir dua kali sebelum melakukannya.",
"ally_donate": "Donasikan sebagian pasukanmu kepada sekutu. Digunakan ketika mereka kekurangan pasukan, sedang diserang, atau membutuhkan kekuatan tambahan untuk menghancurkan musuh.",
"ally_donate_gold": "Donasikan sebagian emasmu kepada sekutu. Digunakan saat mereka kekurangan emas dan membutuhkannya untuk membangun, atau ketika anggota tim sedang menabung untuk MIRV.",
"build_menu_title": "Menu Pembangunan",
"build_menu_desc": "Bangun item berikut atau lihat jumlah yang sudah kamu bangun:",
"build_name": "Judul",
"build_icon": "Ikon",
"build_desc": "Deskripsi",
"build_city": "Kota",
"build_city_desc": "Meningkatkan batas populasi maksimum. Berguna saat kamu tidak dapat memperluas wilayah atau hampir mencapai batas populasi.",
"build_factory": "Pabrik",
"build_factory_desc": "Secara otomatis membangun jalur kereta api ke kota, pelabuhan, dan pabrik lain di sekitarnya, serta dapat terhubung dengan negara tetangga yang bersahabat. Kereta akan muncul secara berkala dan memberimu sejumlah emas tetap untuk setiap bangunan yang dikunjungi sepanjang rute, dengan bonus emas tambahan saat mengunjungi bangunan milik tetanggamu.",
"build_defense": "Pos Pertahanan",
"build_defense_desc": "Meningkatkan pertahanan di sekitar perbatasan terdekat, yang ditandai dengan pola kotak-kotak. Serangan musuh menjadi lebih lambat dan menyebabkan lebih banyak korban.",
"build_port": "Pelabuhan",
"build_port_desc": "Hanya dapat dibangun di dekat air. Memungkinkan pembangunan Kapal Perang. Secara otomatis mengirim kapal dagang antara pelabuhan di negaramu dan negara lain (kecuali saat perdagangan dihentikan), yang memberikan emas bagi kedua pihak.\nPerdagangan dengan seorang pemain akan berhenti secara otomatis ketika kamu menyerang atau diserang oleh pemain tersebut. Perdagangan akan dilanjutkan kembali setelah 5 menit atau jika kalian menjadi sekutu. Kamu juga dapat mengatur perdagangan secara manual dengan memilih “Hentikan Perdagangan” atau “Mulai Perdagangan”.",
"build_warship": "Kapal Perang",
"build_warship_desc": "Berpatroli di suatu area, menangkap kapal dagang musuh serta menghancurkan Kapal (kapal pengangkut) dan Kapal Perang mereka. Unit ini muncul dari Pelabuhan terdekat dan akan berpatroli di area yang pertama kali kamu klik saat membangunnya.\nKamu dapat mengendalikan Kapal Perang dengan klik-serang pada unit tersebut (lihat aksi Serang pada menu Hotkeys), lalu klik-serang area baru yang ingin dituju.",
"build_silo": "Silo Peluncur Rudal",
"build_silo_desc": "Memungkinkan peluncuran rudal.",
"build_sam": "Peluncur Rudal SAM",
"build_sam_desc": "Dapat mencegat rudal musuh dalam jangkauan 100 piksel. Peluncur SAM memiliki waktu jeda cooldown 7,5 detik.",
"build_atom": "Bom Atom",
"build_atom_desc": "Bom kecil berdaya ledak tinggi yang menghancurkan wilayah, bangunan, kapal, dan perahu. Muncul dari Silo Rudal terdekat dan mendarat di area yang pertama kali kamu klik saat membangunnya.",
"build_hydrogen": "Bom Hidrogen",
"build_hydrogen_desc": "Bom berdaya ledak besar. Muncul dari Silo Rudal terdekat dan mendarat di area yang pertama kali kamu klik saat membangunnya.",
"build_mirv": "MIRV",
"build_mirv_desc": "Bom paling kuat di dalam permainan. Akan terpecah menjadi bom-bom yang lebih kecil dan mencakup area wilayah yang sangat luas. Hanya memberikan kerusakan kepada pemain yang pertama kali kamu klik saat membangunnya.\nSenjata ini muncul dari Silo Rudal terdekat dan akan mendarat di area yang pertama kali kamu klik saat membangunnya.",
"player_icons": "Ikon Pemain",
"icon_desc": "Berikut beberapa ikon yang akan kamu temui di dalam permainan beserta artinya:",
"icon_crown": "Mahkota – Peringkat 1. Pemain teratas di papan peringkat.",
"icon_traitor": "Perisai Retak – Pengkhianat. Pemain ini menyerang sekutu.",
"icon_ally": "Jabat Tangan – Sekutu. Pemain ini adalah sekutumu.",
"icon_embargo": "Tanda Dolar Dicoret – Embargo. Pemain ini menghentikan perdagangan denganmu, baik secara otomatis maupun manual.",
"icon_request": "Amplop – Permintaan Aliansi. Pemain ini mengirim permintaan aliansi kepadamu.",
"info_enemy_panel": "Panel Info Musuh",
"exit_confirmation": "Apakah yakin keluar dari game?",
"bomb_direction": "Arah busur bom atom/hidrogen"
},
"single_modal": {
"title": "Sendiri",
"random_spawn": "Kemunculan acak",
"allow_alliances": "Perbolehkan Aliansi",
"toggle_achievements": "Tampilkan / Sembunyikan pencapaian",
"sign_in_for_achievements": "Masuk untuk melihat pencapaian",
"options_title": "Opsi",
"bots": "Bot: ",
"bots_disabled": "Dinonaktifkan",
"nations": "Bangsa-bangsa: ",
"disable_nations": "Nonaktifkan negara",
"instant_build": "Bangun instan",
"infinite_gold": "Emas tak terbatas",
"infinite_troops": "Pasukan tak terbatas",
"compact_map": "Peta Kecil",
"max_timer": "Lama permainan (menit)",
"max_timer_placeholder": "Menit",
"max_timer_invalid": "Silakan masukkan nilai pengatur waktu maksimum yang valid (1-120 menit)",
"disable_nukes": "Nonaktifkan Senjata Nuklir",
"enables_title": "Aktifkan Pengaturan",
"start": "Mulai Permainan"
},
"token_login_modal": {
"title": "Sedang masuk...",
"logging_in": "Sedang masuk...",
"success": "Berhasil masuk sebagai {email}!"
},
"account_modal": {
"title": "Akun",
"connected_as": "Terhubung sebagai",
"stats_overview": "Gambaran Umum Statistik",
"link_discord": "Tautkan Akun Discord",
"log_out": "Keluar",
"sign_in_desc": "Masuk untuk menyimpan statistik dan kemajuan Anda",
"or": "ATAU",
"email_placeholder": "Masukkan alamat email Anda",
"get_magic_link": "Dapatkan Tautan Ajaib",
"linked_account": "Masuk sebagai {account_name}",
"fetching_account": "Mengambil informasi akun...",
"recovery_email_sent": "Pemulihan email dikirim ke {email}",
"not_found": "Tidak Ditemukan",
"clear_session": "Hapus Sesi",
"failed_to_send_recovery_email": "Gagal mengirim pemulihan email",
"enter_email_address": "Silahkan masukan alamat email"
},
"stats_modal": {
"title": "Statistik",
"clan_stats": "Statistik Klan",
"loading": "Loading...",
"error": "Error saat memuat statistik klan",
"no_stats": "Tidak ada klan yang tersedia",
"no_data_yet": "Data belum tersedia",
"clan": "Klan",
"games": "Permainan",
"win_score": "Skor Kemenangan",
"win_score_tooltip": "Kemenangan dihitung berdasarkan bobot partisipasi klan dan tingkat kesulitan pertandingan",
"loss_score": "Skor Kekalahan",
"loss_score_tooltip": "Kerugian dihitung berdasarkan partisipasi klan dan kesulitan pertandingan",
"win_loss_ratio": "Menang/Kalah",
"ratio": "Rasio",
"rank": "Peringkat",
"try_again": "Coba Lagi"
},
"game_info_modal": {
"title": "Informasi Permainan",
"players": "Pemain",
"atoms": "Atom",
"hydros": "Hidro",
"mirv": "MIRV",
"bombs": "Bom",
"total_gold": "Total",
"all_gold": "Semua emas",
"trade": "Perdagangan",
"conquest_gold": "Emas pemain yang ditaklukan",
"stolen_gold": "Dicuri oleh Kapal Perang",
"num_of_conquests": "Jumlah pemain yang ditaklukan",
"duration": "Durasi",
"survival_time": "Menit Bertahan",
"war": "Perang",
"economy": "Ekonomi",
"conquests": "Penaklukan",
"pirate": "Bajak Laut",
"conquered": "Ditaklukan",
"loading_game_info": "Memuat Statistik Permainan",
"no_winner": "Permainan ini berakhir tanpa pemenang (atau Negara menang)"
},
"map": {
"map": "Peta",
"world": "Dunia",
"giantworldmap": "Map Dunia Besar",
"europe": "Eropa",
"mena": "MENA",
"northamerica": "Amerika Utara",
"oceania": "Oseania",
"blacksea": "Laut Hitam",
"africa": "Afrika",
"asia": "Asia",
"mars": "Mars",
"southamerica": "Amerika Selatan",
"britanniaclassic": "Britania (klasik)",
"britannia": "Britania",
"gatewaytotheatlantic": "Pintu masuk menuju Atlantik",
"australia": "Australia",
"random": "Random",
"iceland": "Islandia",
"pangaea": "Pangea",
"eastasia": "Asia Timur",
"betweentwoseas": "Diantara Dua Laut",
"faroeislands": "Kepulauan Faroe",
"deglaciatedantarctica": "Antartika yang telah bebas dari gletser",
"europeclassic": "Eropa (klasik)",
"falklandislands": "Kepulauan Falkland",
"baikal": "Baikal",
"halkidiki": "Kalkidiki",
"straitofgibraltar": "Selat Gibraltar",
"italia": "Italia",
"japan": "Jepang",
"yenisei": "Sungai Yenisei",
"pluto": "Pluto",
"montreal": "Montreal",
"newyorkcity": "Kota New York",
"achiran": "Sungai Akheron",
"baikalnukewars": "Baikal (Perang Nuklir)",
"fourislands": "Empat Pulau",
"gulfofstlawrence": "Teluk St. Lawrence",
"lisbon": "Lisboa",
"svalmel": "Svalmel",
"manicouagan": "Manicouagan",
"lemnos": "Lemnos",
"sierpinski": "Sierpinski",
"twolakes": "Dua Danau",
"straitofhormuz": "Selat Hormuz",
"surrounded": "Surrourded",
"didier": "Didier",
"didierfrance": "Didier (Prancis)",
"amazonriver": "Sungai Amazon"
},
"map_categories": {
"continental": "Kontinental",
"regional": "Regional",
"fantasy": "Lain",
"special": "Spesial",
"arcade": "Arkade"
},
"map_component": {
"loading": "Loading...",
"error": "Kesalahan"
},
"private_lobby": {
"title": "Gabung Lobi Privat",
"enter_id": "Masukan ID Lobi",
"player": "Pemain",
"players": "Pemain",
"join_lobby": "Bergabung ke Lobi",
"checking": "Memeriksa Lobi...",
"not_found": "Lobi tidak ditemukan. Mohon periksa ID dan coba lagi.",
"error": "Beberapa kesalahan terjadi. Silakan coba lagi atau hubungi dukungan.",
"joined_waiting": "Berhasil gabung ke lobi! Menunggu untuk penyelenggara untuk memulai...",
"version_mismatch": "Permainan ini dibuat dengan versi yang berbeda. Tidak dapat gabung.",
"disabled_units": "Nonaktfikan Units"
},
"public_lobby": {
"join": "Gabung ke permainan selanjutnya",
"waiting": "Pemain menunggu",
"teams_Duos": "{team_count} tim berisi 2 pemain (Berdua)",
"teams_Trios": "{team_count} tim berisi 3 pemain (Bertiga)",
"teams_Quads": "{team_count} tim berisi 4 pemain (Berempat)",
"waiting_for_players": "Menunggu pemain",
"starting_game": "Memulai permainan…",
"teams_hvn": "Pemain vs Negara",
"teams_hvn_detailed": "{num} Pemain vs {num} Negara",
"teams": "{num} tim",
"players_per_team": "dari {num}",
"started": "Dimulai"
},
"matchmaking_modal": {
"title": "Pertandingan 1v1 Ranked (ALPHA)",
"connecting": "Menghubungkan ke server pencarian lawan...",
"searching": "Mencari permainan...",
"waiting_for_game": "Menunggu permainan untuk dimulai...",
"elo": "ELO anda: {elo}"
},
"username": {
"enter_username": "Masukkan nama pengguna",
"not_string": "Nama pengguna harus berupa string.",
"too_short": "Nama pengguna harus memiliki panjang minimal {min} karakter.",
"too_long": "Nama pengguna tidak boleh melebihi {max} karakter.",
"invalid_chars": "Nama pengguna hanya boleh berupa huruf, angka, spasi dan garis bawah.",
"tag": "Tag",
"tag_too_short": "Nama klan harus terdiri dari 2-5 karakter alfanumerik.",
"tag_invalid_chars": "Tag klan hanya boleh berisi huruf dan angka"
},
"host_modal": {
"title": "Buat Lobi Tertutup",
"label": "Tertutup",
"mode": "Mode",
"team_count": "Jumlah Tim",
"team_type": "Tipe Tim",
"options_title": "Pilihan",
"bots": "Bot: ",
"bots_disabled": "Nonaktif",
"player_immunity_duration": "Durasi imunitas PVP (menit)",
"nations": "Bangsa-bangsa: ",
"disable_nations": "Nonaktifkan Negara",
"max_timer": "Lama permainan (menit)",
"mins_placeholder": "Menit",
"instant_build": "Bangun instan",
"infinite_gold": "Emas tak terbatas",
"donate_gold": "Donasikan emas",
"infinite_troops": "Pasukan tak terbatas",
"donate_troops": "Donasikan pasukan",
"compact_map": "Peta Kecil",
"enables_title": "Aktifkan Pengaturan",
"player": "Pemain",
"players": "Pemain",
"nation_players": "Bangsa-bangsa",
"nation_player": "Bangsa",
"waiting": "Menunggu pemain...",
"random_spawn": "Kemunculan Acak",
"start": "Mulai Permainan",
"host_badge": "Host",
"assigned_teams": "Tim yang Ditugaskan",
"empty_teams": "Tim Kosong",
"empty_team": "Kosong",
"remove_player": "Hapus {username}",
"teams_Duos": "Berdua (tim yang terdiri dari 2 orang)",
"teams_Trios": "Bertiga (tim yang terdiri dari 3 orang)",
"teams_Quads": "Berempat (tim yang teridri dari 4 orang)",
"teams_Humans Vs Nations": "Pemain vs Negara"
},
"team_colors": {
"red": "Merah",
"blue": "Biru",
"teal": "Hijau Laut",
"purple": "Ungu",
"yellow": "Kuning",
"orange": "Oranye",
"green": "Hijau",
"bot": "Bot"
},
"game_starting_modal": {
"title": "Memulai Permainan...",
"credits": "Kredit",
"code_license": "Kode berlisensi AGPL-3.0 (tanpa garansi)"
},
"difficulty": {
"difficulty": "Kesulitan Negara",
"easy": "Mudah",
"medium": "Sedang",
"hard": "Sulit",
"impossible": "Mustahil"
},
"game_mode": {
"ffa": "Siapapun bisa bergabung",
"teams": "Tim-tim"
},
"public_game_modifier": {
"random_spawn": "Kemunculan Acak",
"compact_map": "Peta Kecil"
},
"select_lang": {
"title": "Pilih Bahasa"
},
"unit_type": {
"city": "Kota",
"defense_post": "Pos Pertahanan",
"port": "Pelabuhan",
"warship": "Kapal Perang",
"missile_silo": "Silo Peluncur Rudal",
"sam_launcher": "Peluncur Rudal SAM",
"atom_bomb": "Bom Atom",
"hydrogen_bomb": "Bom Hidrogen",
"mirv": "MIRV",
"factory": "Pabrik"
},
"user_setting": {
"title": "Pengaturan",
"tab_basic": "Pengaturan Dasasr",
"tab_keybinds": "Tombol pintasan",
"dark_mode_label": "Mode Gelap",
"dark_mode_desc": "Beralih tampilan situs antara tema terang dan gelap",
"emojis_label": "Emoji",
"emojis_desc": "Alihkan tampilan emoji di dalam game",
"alert_frame_label": "Bingkai Peringatan",
"alert_frame_desc": "Aktifkan / Nonaktifkan bingkai peringatan. Saat diaktifkan, bingkai akan ditampilkan ketika kamu dikhianati atau diserang melalui darat.",
"special_effects_label": "Efek Spesial",
"special_effects_desc": "Alihkan efek khusus. Nonaktifkan untuk meningkatkan performa",
"structure_sprites_label": "Sprite Bangunan",
"structure_sprites_desc": "Alihkan tampilan sprite bangunan",
"cursor_cost_label_label": "Biaya Pembangunan Kursor",
"cursor_cost_label_desc": "Tampilkan label biaya di bawah ikon kursor pembangunan",
"anonymous_names_label": "Sembunyikan Nama",
"anonymous_names_desc": "Sembunyikan nama asli pemain dengan nama acak di layar Anda.",
"lobby_id_visibility_label": "Sembunyikan ID Lobby",
"lobby_id_visibility_desc": "Sembunyikan ID Lobby saat membuat lobby pribadi",
"toggle_visibility": "Alihkan Visibilitas",
"left_click_label": "Klik Kiri untuk Membuka Menu",
"left_click_desc": "Saat AKTIF, klik kiri membuka menu dan tombol pedang digunakan untuk menyerang. Saat NONAKTIF, klik kiri langsung melakukan serangan.",
"left_click_menu": "Klik Kiri untuk Menu",
"attack_ratio_label": "⚔️ Rasio Serangan",
"attack_ratio_desc": "Persentase pasukan yang dikirim saat menyerang (1–100%)",
"territory_patterns_label": "🏳️ Skin Wilayah",
"territory_patterns_desc": "Pilih apakah ingin menampilkan desain skin wilayah di dalam game",
"performance_overlay_label": "Tampilan Performa",
"performance_overlay_desc": "Aktifkan / Nonaktifkan overlay performa.\nSaat diaktifkan, overlay performa akan ditampilkan. Tekan Shift + D saat permainan berlangsung untuk mengaktifkan atau menonaktifkannya.",
"easter_writing_speed_label": "Multiplier Kecepatan Menulis",
"easter_writing_speed_desc": "Atur seberapa cepat kamu berpura-pura coding (x1–x100)",
"easter_bug_count_label": "Jumlah Bug",
"easter_bug_count_desc": "Seberapa banyak bug yang masih bisa Anda toleransi (0–1000, secara emosional)",
"press_a_key": "Tekan tombol",
"view_options": "Opsi Tampilan",
"toggle_view": "Alihkan Tampilan",
"toggle_view_desc": "Ganti Tampilan (Medan / Negara)",
"build_controls": "Kontrol Pembangunan",
"build_city": "Membangun Kota",
"build_city_desc": "Bangun Kota di bawah kursor Anda.",
"build_factory": "Bangun Pabrik",
"build_factory_desc": "Bangun Pabrik di bawah kursor Anda.",
"build_defense_post": "Bangun Pos Pertahanan",
"build_defense_post_desc": "Bangun Pos Pertahanan di bawah kursor Anda.",
"build_port": "Membangun Pelabuhan",
"build_port_desc": "Bangun Pelabuhan di bawah kursor Anda.",
"build_warship": "Bangun Kapal Perang",
"build_warship_desc": "Bangun Kapal Perang di bawah kursor Anda.",
"build_missile_silo": "Bangun Silo Peluncur Rudal",
"build_missile_silo_desc": "Bangun Silo Peluncur Rudal di bawah kursor Anda.",
"build_sam_launcher": "Bangun Peluncur Rudal SAM",
"build_sam_launcher_desc": "Bangun Peluncur Rudal SAM di bawah kursor Anda.",
"build_atom_bomb": "Bangun Bom Atom",
"build_atom_bomb_desc": "Bangun Bom Atom di bawah kursor Anda.",
"build_hydrogen_bomb": "Bangun Bom Hidrogen",
"build_hydrogen_bomb_desc": "Membangun Bom Hidrogen di bawah kursor Anda.",
"build_mirv": "Membangun MIRV",
"build_mirv_desc": "Bangun MIRV di bawah kursor Anda.",
"menu_shortcuts": "Menu Pintasan",
"build_menu_modifier": "Pengubah Menu Pembangunan",
"build_menu_modifier_desc": "Tahan tombol ini sambil mengklik untuk membuka menu pembuatan.",
"emoji_menu_modifier": "Pengubah Menu Emoji",
"emoji_menu_modifier_desc": "Tahan tombol ini sambil mengklik untuk membuka menu emoji.",
"attack_ratio_controls": "Kontrol Rasio Serangan",
"attack_ratio_up": "Tingkatkan Rasio Serangan",
"attack_ratio_up_desc": "Tingkatkan Rasio Serangan sebesar 10%",
"attack_ratio_down": "Kurangi Rasio Serangan",
"attack_ratio_down_desc": "Kurangi Rasio Serangan sebesar 10%",
"attack_keybinds": "Tombol pintas untuk Serangan",
"boat_attack": "Serangan Kapal Pengangkut",
"boat_attack_desc": "Kirim serangan kapal ke petak di bawah kursor Anda.",
"ground_attack": "Serangan Darat",
"ground_attack_desc": "Kirim serangan darat ke petak di bawah kursor Anda.",
"swap_direction": "Tukar Arah Roket",
"swap_direction_desc": "Ubah arah peluncuran roket (atas/bawah).",
"zoom_controls": "Kontrol Zoom",
"zoom_out": "Perkecil tampilan",
"zoom_out_desc": "Perkecil tampilan peta",
"zoom_in": "Perbesar",
"zoom_in_desc": "Perbesar tampilan peta",
"camera_movement": "Pergerakan Kamera",
"center_camera": "Sorot kamera ke tengah",
"center_camera_desc": "Pusatkan kamera pada pemain",
"move_up": "Pindahkan kamera ke atas",
"move_up_desc": "Memindahkan kamera ke atas",
"move_left": "Pindahkan kamera ke kiri",
"move_left_desc": "Memindahkan kamera ke kiri",
"move_down": "Pindahkan kamera ke bawah",
"move_down_desc": "Memindahkan kamera ke bawah",
"move_right": "Pindahkan kemara ke kanan",
"move_right_desc": "Memindahkan kamera ke kanan",
"reset": "Reset",
"unbind": "Batalkan pengikatan tombol",
"on": "Hidup",
"off": "Mati",
"toggle_terrain": "Tampilkan / Sembunyikan Medan",
"exit_game_label": "Keluar Game",
"exit_game_info": "Kembali ke menu utama",
"background_music_volume": "Volume latar belakang musik",
"sound_effects_volume": "Volume Efek Suara",
"keybind_conflict_error": "Tombol {key} sudah terikat ke aksi lain."
},
"chat": {
"title": "Obrolan Cepat",
"to": "Dari {user}: {msg}",
"from": "Dari {user}: {msg}",
"category": "Kategori",
"phrase": "Frase",
"player": "Pemain",
"send": "Kirim",
"search": "Cari pemain...",
"build": "Ketik pesanmu...",
"cat": {
"help": "Bantuan",
"attack": "Serang",
"defend": "Bertahan",
"greet": "Salam",
"misc": "Lain-lain",
"warnings": "Peringatan"
},
"help": {
"troops": "Tolong berikan saya tentara!",
"troops_frontlines": "Kirim pasukan ke garis depan!",
"gold": "Tolong berikan saya emas!",
"no_attack": "Tolong jangan serang saya!",
"sorry_attack": "Maaf, Saya tidak bermaksud untuk menyerang Anda.",
"alliance": "Aliansi?",
"help_defend": "Bantu saya bertahan dari [P1]!",
"trade_partners": "Mari menjadi mitra dagang!"
},
"attack": {
"attack": "Serang [P1]!",
"mirv": "Luncurkan MIRV ke [P1]!",
"focus": "Fokus serangan pada [P1]!",
"finish": "Mari selesaikan [P1]!",
"build_warships": "Bangun Kapal-Kapal Perang!"
},
"defend": {
"defend": "Pertahankan [P1]!",
"defend_from": "Bertahan dari [P1]!",
"dont_attack": "Jangan serang [P1]!",
"ally": "[P1] adalah aliansi saya!",
"build_posts": "Bangun Pos Pertahanan!"
},
"greet": {
"hello": "Halo!",
"good_job": "Kerja bagus!",
"good_luck": "Semoga sukses!",
"have_fun": "Selamat bersenang-senang!",
"gg": "GG!",
"nice_to_meet": "Senang bertemu denganmu!",
"well_played": "Bagus Sekali!",
"hi_again": "Halo lagi!",
"bye": "Da!",
"thanks": "Terima kasih!",
"oops": "Ups, salah tombol!",
"trust_me": "Anda bisa percaya saya. Janji!",
"trust_broken": "Aku percaya padamu...",
"ruining_games": "Kamu bikin permainan kita berdua jadi kacau.",
"dont_do_that": "Jangan!",
"same_team": "Saya di pihak Anda!"
},
"misc": {
"go": "Ayo!",
"strategy": "Strategi yang mantap!",
"fun": "Permainan ini seru!",
"team_up": "Mari menyerang [P1] bersama-sama!",
"pr": "Kapan PR-ku akhirnya akan digabungkan...?",
"build_closer": "Bangun lebih dekat agar membuat jalur kereta!",
"coastline": "Tolong izinkan saya mendapatkan garis pantai."
},
"warnings": {
"strong": "[P1] kuat.",
"weak": "[P1] lemah.",
"mirv_soon": "[P1] akan meluncurkan MIRV segera!",
"number1_warning": "Pemain nomor 1 akan segera menang kecuali kita bekerja sama!",
"stalemate": "Mari berdamai. Ini jalan buntu, kita berdua akan kalah.",
"has_allies": "[P1] punya banyak sekutu.",
"no_allies": "[P1] tidak punya sekutu.",
"betrayed": "[P1] menkhianati sekutu dia!",
"betrayed_me": "[P1] menkhianati saya!",
"getting_big": "[P1] berkembang sangat cepat!",
"danger_base": "[P1] tidak terproteksi!",
"saving_for_mirv": "[P1] sedang menabung untuk meluncurkan MIRV.",
"mirv_ready": "[P1] punya cukup emas untuk meluncurkan MIRV!",
"snowballing": "[P1] berkembang terlalu cepat!",
"cheating": "[P1] curang!",
"stop_trading": "Stop berdangan dengan [P1]!"
}
},
"build_menu": {
"desc": {
"atom_bomb": "Ledakan kecil",
"hydrogen_bomb": "Ledakan dahsyat",
"mirv": "Ledakan Dahsyat, hanya menargetkan pemain yang dipilih",
"missile_silo": "Digunakan untuk meluncurkan nuklir",
"sam_launcher": "Penangkalan nuklir yang mendekat",
"warship": "Menangkap kapal dagang, menghancurkan kapal dan perahu.",
"port": "Mengirim kapal untuk mendapatkan emas",
"defense_post": "Meningkatkan pertahanan perbatasan",
"city": "Meningkatkan jumlah maksimal populasi",
"factory": "Membuat rel dan memunculkan kereta"
},
"not_enough_money": "Uang tidak cukup"
},
"win_modal": {
"support_openfront": "Dukung OpenFront!",
"territory_pattern": "Beli skin wilayah untuk bebas iklan!",
"died": "Anda meninggal",
"your_team": "Tim Anda menang!",
"other_team": "tim {team} menang!",
"you_won": "Anda Menang!",
"other_won": "{player} menang!",
"nation_won": "Negara {nation} menang!",
"exit": "Keluar Game",
"keep": "Terus Main",
"spectate": "Menonton",
"wishlist": "Wishlist di Steam!",
"ofm_winter": "Turnamen Musim Dingin OpenFront Masters!",
"ofm_winter_description": "Ikuti turnamen kompetitif dan bersaing melawan pemain terbaik",
"join_tournament": "Ikut Turnamen",
"join_discord": "Gabung Komunitas Discord Kami!",
"discord_description": "Terhubung dengan pemain lain, temukan fitur baru, dan menangkan hadiah!",
"join_server": "Bergabung dengan Server",
"youtube_tutorial": "Butuh bantuan?"
},
"leaderboard": {
"title": "Papan Peringkat",
"hide": "Sembunyikan",
"rank": "Peringkat",
"player": "Pemain",
"team": "Tim",
"owned": "Dimiliki",
"gold": "Emas",
"maxtroops": "Maksimal pasukan",
"launchers": "Peluncur",
"sams": "SAM-SAM",
"warships": "Kapal Perang",
"cities": "Kota-Kota",
"show_control": "Tampilkan Kontrol",
"show_units": "Tampilkan Unit"
},
"player_info_overlay": {
"type": "Jenis",
"bot": "Bot",
"nation": "Bangsa",
"player": "Pemain",
"team": "Tim",
"alliance_timeout": "Aliansi berakhir dalam",
"troops": "Pasukan",
"maxtroops": "Maksimal pasukan",
"a_troops": "Pasukan menyerang",
"gold": "Emas",
"ports": "Pelabuhan-Pelabuhan",
"cities": "Kota-kota",
"factories": "Pabrik-pabrik",
"missile_launchers": "Peluncur rudal",
"sams": "SAM",
"warships": "Kapal Perang",
"health": "Kesehatan",
"attitude": "Sikap",
"levels": "Tingkat",
"wilderness_title": "Alam Liar",
"irradiated_wilderness_title": "Hutan Belantara yang Terkena Radiasi"
},
"events_display": {
"retreating": "mundur",
"retaliate": "Membalas",
"boat": "Perahu",
"alliance_request_status": "{name} {status} permintaan aliansi Anda",
"alliance_accepted": "diterima",
"alliance_rejected": "ditolak",
"duration_second": "1 detik",
"betrayal_description": "Kamu memutus aliansi dengan {name}, menjadikanmu PENGKHIANAT ({malusPercent}% pengurangan pertahanan selama {durationText})",
"duration_seconds_plural": "{seconds} detik",
"betrayed_you": "{name} memutus aliansi dengan Anda",
"about_to_expire": "Aliansi Anda dengan {name} hampir berakhir!",
"alliance_expired": "Aliansi Anda dengan {name} berakhir",
"attack_request": "{name} meminta Anda untuk menyerang {target}",
"sent_emoji": "Dari {name}: {emoji}",
"renew_alliance": "Minta untuk memperpanjang",
"request_alliance": "{name} meminta aliansi!",
"focus": "Fokus",
"accept_alliance": "Setuju",
"reject_alliance": "Tolak",
"alliance_renewed": "Aliansi anda dengan {name} sudah di perpanjang",
"wants_to_renew_alliance": "{name} ingin memperpanjang aliansi",
"ignore": "Abaikan",
"unit_voluntarily_deleted": "Unit dihapus secara sukarela",
"betrayal_debuff_ends": "{time} detik tersisa hingga efek negatif pengkhianatan berakhir",
"attack_cancelled_retreat": "Penyerangan dibatalkan, {troops} pasukan terbunuh saat mundur",
"received_gold_from_captured_ship": "Menerima {gold} emas dari kapal yang di tawan dari {name}",
"received_gold_from_trade": "Menerima {gold} emas dari perdagangan dengan {name}",
"missile_intercepted": "Rudal dicegat {unit}",
"mirv_warheads_intercepted": "{count, plural, one {{count} hulu ledak MIRV berhasil dicegat} other {{count} hulu ledak MIRV berhasil dicegat}}",
"sent_troops_to_player": "Mengirim {troops} pasukan ke {name}",
"received_troops_from_player": "Menerima {troops} pasukan dari {name}",
"sent_gold_to_player": "Mengirim {gold} emas ke {name}",
"received_gold_from_player": "Menerima {gold} emas dari {name}",
"unit_captured_by_enemy": "{unit} Anda ditangkap oleh {name}",
"captured_enemy_unit": "Menangkap {unit} dari {name}",
"unit_destroyed": "{unit} Anda dihancurkan",
"no_boats_available": "Tidak ada kapal yang tersedia, maksmial {max}"
},
"unit_info_modal": {
"structure_info": "Informasi Struktur",
"unit_type_unknown": "Tidak Diketahui",
"close": "Keluar",
"cooldown": "Cooldown",
"type": "Jenis",
"upgrade": "Tingkatkan",
"level": "Tingkat"
},
"player_type": {
"player": "Pemain",
"nation": "Bangsa",
"bot": "Bot"
},
"relation": {
"hostile": "Berseteru",
"distrustful": "Tak dapat dipercaya",
"neutral": "Netral",
"friendly": "Ramah",
"default": "Default"
},
"control_panel": {
"gold": "Emas",
"troops": "Pasukan",
"attack_ratio": "Rasio Serangan"
},
"player_panel": {
"gold": "Emas",
"troops": "Pasukan",
"betrayals": "Pengkhianatan",
"traitor": "Pengkhianat",
"trading": "Perdagangan",
"active": "Aktif",
"stopped": "Berhenti",
"alliance_time_remaining": "Aliansi Berakhir Dalam",
"embargo": "Berhenti berdangan dengan Anda",
"nuke": "Nuklir dikirim oleh mereka kepada Anda",
"start_trade": "Mulai Berdagang",
"stop_trade": "Stop Berdagang",
"stop_trade_all": "Stop Berdagang degnan Semuanya",
"start_trade_all": "Mulai Berdagang dengan Semuanya",
"alliances": "Aliansi",
"flag": "Bendera",
"chat": "Chat",
"target": "Sasaran",
"break_alliance": "Rusak Aliansi",
"alliance": "Aliansi",
"send_alliance": "Kirim Proposal Aliansi",
"send_troops": "Kirim Pasukan",
"send_gold": "Kirim Emas",
"emotes": "Emoji",
"arc_up": "Lengkungan ke Atas",
"arc_down": "Lengkungan ke Bawah",
"flip_rocket_trajectory": "Balikkan lintasan roket"
},
"send_troops_modal": {
"title_with_name": "Kirim Pasukan ke {name}",
"available_tooltip": "Pasukan Anda yang tersedia saat ini",
"min_keep": "Minimal yang ditinggalkan",
"slider_tooltip": "{{percent}}% • {{amount}}",
"aria_slider": "Penggeser pasukan",
"capacity_note": "Penerima hanya dapat menerima {{amount}} saat ini."
},
"send_gold_modal": {
"title_with_name": "Kirim Emas ke {name}",
"available_tooltip": "Emas yang Anda miliki saat ini",
"aria_slider": "Penggeser jumlah",
"slider_tooltip": "{{percent}}% • {{amount}}"
},
"replay_panel": {
"replay_speed": "Kecepatan tanyangan ulang",
"game_speed": "Kecepatan Permainan",
"fastest_game_speed": "Maks"
},
"error_modal": {
"crashed": "Game berhenti / rusak!",
"connection_error": "Kesalahan koneksi!",
"paste_discord": "Silakan tempelkan teks berikut di laporan bug Anda di Discord:",
"copy_clipboard": "Salin ke papan klip",
"copied": "Tersalin!",
"failed_copy": "Gagal menyalin",
"spawn_failed": {
"title": "Kemunculan gagal",
"description": "Pemilihan titik awal otomatis gagal. Anda tidak dapat memainkan game ini."
},
"desync_notice": "Anda tidak tersinkronisasi dengan pemain lain. Apa yang Anda lihat mungkin berbeda dari pemain lain."
},
"performance_overlay": {
"reset": "Set ulang",
"copy_json_title": "Salin metrik kinerja saat ini sebagai JSON.",
"copy_clipboard": "Menyalin JSON",
"copied": "Tersalin!",
"failed_copy": "Gagal menyalin",
"fps": "FPS:",
"avg_60s": "Rata-rata (60d):",
"frame": "Bingkai:",
"tick_exec": "Eksekutif Tick:",
"tick_delay": "Penundaan Detik:",
"layers_header": "Lapisan (rata-rata / maksimum, diurutkan berdasarkan total waktu):"
},
"heads_up_message": {
"choose_spawn": "Pilih lokasi awal",
"random_spawn": "Kemunculan acak diaktifkan. Memilih lokasi awal untuk Anda...",
"singleplayer_game_paused": "Permainan dijeda",
"multiplayer_game_paused": "Permainan di tunda oleh Pembuat Lobi"
},
"territory_patterns": {
"title": "Tampilan",
"colors": "Warna-Warna",
"purchase": "Beli",
"show_only_owned": "Skin Saya",
"all_owned": "Semua skin sudah dimiliki! Silakan periksa kembali nanti untuk item baru.",
"not_logged_in": "Belum masuk",
"blocked": {
"login": "Anda harus login untuk mengakses skin ini.",
"purchase": "Beli skin ini untuk membukanya."
},
"pattern": {
"default": "Default"
},
"select_skin": "Pilih Skin",
"selected": "dipilih"
},
"flag_input": {
"title": "Pilih Bendera",
"button_title": "Pilih bendera!",
"search_flag": "Cari..."
},
"spawn_ad": {
"loading": "Memuat iklan..."
},
"auth": {
"login_required": "Masuk dibutuhkan untuk mengakses website ini.",
"redirecting": "Anda sedang diarahkan...",
"not_authorized": "Anda tidak punya izin untuk mengakses website ini.",
"contact_admin": "Jika Anda yakin melihat pesan ini karena kesalahan, silakan hubungi administrator situs web."
},
"radial_menu": {
"delete_unit_title": "Hapus Unit",
"delete_unit_description": "Klik untuk menghapus unit terdekat"
},
"discord_user_header": {
"avatar_alt": "Avatar"
},
"player_stats_table": {
"building_stats": "Statistik Bangunan",
"ship_arrivals": "Kedatangan Kapal",
"nuke_stats": "Statistik Nuklir",
"player_metrics": "Metrik Pemain",
"building": "Gedung",
"ship_type": "Jenis Kapal",
"weapon": "Senjata",
"built": "Bangun",
"destroyed": "Telah Hancur",
"captured": "Ditangkap",
"lost": "Kalah",
"hits": "Hits",
"launched": "Telah Diluncurkan",
"landed": "Mendarat",
"sent": "Terkirim",
"arrived": "Tiba",
"attack": "Serang",
"received": "Diterima",
"cancelled": "Dibatalkan",
"count": "Hitungan",
"gold": "Emas",
"workers": "Pekerja",
"war": "Perang",
"trade": "Perdagangan",
"steal": "Steal",
"unit": {
"city": "Kota",
"port": "Pelabuhan",
"defp": "Pos Pertahanan",
"saml": "Peluncur Rudal SAM",
"silo": "Silo Peluncur Rudal",
"wshp": "Kapal Perang",
"fact": "Pabrik",
"trade": "Kapal Perdagangan",
"trans": "Kapal Pengangkut",
"abomb": "Bom Atom",
"hbomb": "Bom Hidrogen",
"mirv": "MIRV",
"mirvw": "Hulu ledak MIRV"
}
},
"game_list": {
"recent_games": "Permainan Terbaru",
"game_id": "ID Permainan",
"mode": "Mode",
"mode_ffa": "Siapapun bisa bergabung",
"mode_team": "Tim",
"replay": "Tayangan ulang",
"details": "Detail",
"ranking": "Peringkat",
"started": "Dimulai",
"map": "Peta",
"difficulty": "Tingkat Kesulitan",
"type": "Jenis"
},
"player_stats_tree": {
"public": "Publik",
"private": "Tertutup",
"singleplayer": "Sendiri",
"mode": "Mode",
"stats_wins": "Jumlah Kemenangan",
"stats_losses": "Jumlah Kehilangan",
"stats_wlr": "Menang:Kalah Rasio",
"stats_games_played": "Permainan Dimainkan",
"mode_ffa": "Siapapun bisa bergabung",
"mode_team": "Tim",
"no_stats": "Tidak ada statistik yang tercatat untuk pilihan ini."
},
"matchmaking_button": {
"play_ranked": "Pertandingan 1v1 Ranked",
"description": "(ALPHA)",
"login_required": "Masuk untuk bermain peringkat!",
"must_login": "Anda harus masuk untuk bermain di pertandingan 1v1 Ranked."
}
}
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

Please remove non-English locale changes from this PR.

Project workflow says only resources/lang/en.json should be updated in normal PRs. Other locales (like resources/lang/id.json) are handled by the translation team in dedicated translation PRs. Please revert this file here and let translators update it later. Based on learnings, please keep non‑English locale updates out of regular PRs.

🤖 Prompt for AI Agents
In `@resources/lang/id.json` around lines 1 - 953, This PR incorrectly includes
non‑English locale changes (the top-level JSON keys like "lang" and "common"
plus many translated strings such as "close","join_discord","title" etc. inside
the Indonesian locale object); revert this file back to its previous state
(remove all edits to the Indonesian locale JSON so only changes to
resources/lang/en.json remain in this PR) by restoring the original content for
the Indonesian locale (undo edits to the "lang", "common", "main", and other
top-level sections in that JSON) and leave translation updates to the dedicated
translation PRs.

Comment on lines 25 to 35
connectedCallback() {
super.connectedCallback();
document.addEventListener(
"userMeResponse",
this.boundUserMeHandler as EventListener,
);
}

private onUserMe(userMeResponse: UserMeResponse | false): void {
const flares =
userMeResponse === false ? [] : (userMeResponse.player.flares ?? []);
const hasFlare = flares.some((flare) => flare.startsWith("pattern:"));
if (hasFlare) {
console.log("No ads because you have patterns");
window.enableAds = false;
} else {
console.log("No flares, showing ads");
this.show();
window.enableAds = true;
}
}

private isScreenLargeEnough(): boolean {
return window.innerWidth >= MIN_SCREEN_WIDTH;
document.addEventListener("userMeResponse", () => {
if (window.adsEnabled) {
console.log("showing gutter ads");
this.show();
} else {
console.log("not showing gutter ads");
}
});
}
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 | 🟡 Minor

Event listener not removed on disconnect.

The userMeResponse event listener added in connectedCallback is never removed in disconnectedCallback. This can cause memory leaks if the component is removed and re-added to the DOM.

Proposed fix
+  private handleUserMeResponse = () => {
+    if (window.adsEnabled) {
+      console.log("showing gutter ads");
+      this.show();
+    } else {
+      console.log("not showing gutter ads");
+    }
+  };

   connectedCallback() {
     super.connectedCallback();
-    document.addEventListener("userMeResponse", () => {
-      if (window.adsEnabled) {
-        console.log("showing gutter ads");
-        this.show();
-      } else {
-        console.log("not showing gutter ads");
-      }
-    });
+    document.addEventListener("userMeResponse", this.handleUserMeResponse);
   }

   disconnectedCallback() {
+    document.removeEventListener("userMeResponse", this.handleUserMeResponse);
     super.disconnectedCallback();
   }
🤖 Prompt for AI Agents
In `@src/client/GutterAds.ts` around lines 25 - 35, The connectedCallback in
GutterAds adds a "userMeResponse" listener but never removes it; create a
named/bound handler property (e.g., this._onUserMeResponse or
this.handleUserMeResponse) when attaching the listener in connectedCallback and
then remove the same handler in disconnectedCallback using
document.removeEventListener("userMeResponse", this._onUserMeResponse); ensure
the handler calls the existing logic (checking window.adsEnabled and calling
this.show()) so the listener can be cleanly detached when the component is
removed.

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

🤖 Fix all issues with AI agents
In `@src/client/components/CopyButton.ts`:
- Around line 88-91: In the CopyButton component update the hardcoded alert when
text is falsy to use translateText(); replace alert("Error copying game id")
with an alert that calls translateText with an appropriate key or message (e.g.,
translateText('error.copyingGameId')) so the error is localized; ensure you
import or reference translateText the same way other components do and use the
existing 'text' check in the function where the alert currently lives.
🧹 Nitpick comments (4)
src/client/UsernameInput.ts (1)

50-52: Redundant optional chaining.

On line 51, user?.username uses optional chaining, but user is already verified as truthy on line 50. Simplify to user.username.

Proposed fix
     crazyGamesSDK.addAuthListener((user) => {
       if (user) {
-        this.parseAndSetUsername(user?.username);
+        this.parseAndSetUsername(user.username);
       }
       this.requestUpdate();
     });
src/client/Main.ts (3)

433-437: Variable name hasLinkedAccount is misleading.

The variable is named hasLinkedAccount but it actually checks if the player has flares AND is not on CrazyGames. Then it's used to compute adsEnabled with inverted logic. This makes the code hard to follow.

Consider renaming for clarity:

Proposed fix
-      const hasLinkedAccount =
-        !crazyGamesSDK.isOnCrazyGames() &&
-        ((userMeResponse || null)?.player?.flares?.length ?? 0) > 0;
-      console.log("ads enabled: ", hasLinkedAccount);
-      window.adsEnabled = !hasLinkedAccount && !crazyGamesSDK.isOnCrazyGames();
+      const hasFlares =
+        ((userMeResponse || null)?.player?.flares?.length ?? 0) > 0;
+      const isOnCrazyGames = crazyGamesSDK.isOnCrazyGames();
+      window.adsEnabled = !hasFlares && !isOnCrazyGames;
+      console.log("ads enabled: ", window.adsEnabled);

622-631: Magic delay of 2000ms needs explanation.

Line 626 adds a 2-second delay before showing the join modal. This seems intentional but lacks a comment explaining why it's needed. Add a brief comment for future maintainers.

Proposed fix
         console.log("game parsed successfully");
+        // Wait for UI components to fully initialize before showing modal
         await new Promise((resolve) => setTimeout(resolve, 2000));
         window.showPage?.("page-join-private-lobby");

936-956: Multiple setTimeout calls for hiding elements is fragile.

Calling hideCrazyGamesElements at 0ms, 100ms, and 500ms is a workaround for timing issues with late-rendered components. Consider using a MutationObserver or component lifecycle hooks for a more robust solution.

Alternative approach using MutationObserver
const hideCrazyGamesElements = () => {
  if (!crazyGamesSDK.isOnCrazyGames()) return;
  
  document.querySelectorAll(".no-crazygames").forEach((el) => {
    (el as HTMLElement).style.display = "none";
  });
};

const bootstrap = () => {
  initLayout();
  new Client().initialize();
  initNavigation();

  hideCrazyGamesElements();

  // Watch for dynamically added elements
  if (crazyGamesSDK.isOnCrazyGames()) {
    const observer = new MutationObserver(() => {
      hideCrazyGamesElements();
    });
    observer.observe(document.body, { childList: true, subtree: true });
    // Stop observing after 2 seconds to avoid performance impact
    setTimeout(() => observer.disconnect(), 2000);
  }
};

Comment on lines +88 to +91
if (!text) {
alert("Error copying game id");
return;
}
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 | 🟡 Minor

Hardcoded error message should use translateText().

The alert message "Error copying game id" is not localized. For consistency with the rest of the UI, this should use translateText().

Proposed fix
   private async handleCopy() {
     const text = await this.resolveCopyText();
     if (!text) {
-      alert("Error copying game id");
+      alert(translateText("common.copy_error") || "Error copying game id");
       return;
     }
📝 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
if (!text) {
alert("Error copying game id");
return;
}
if (!text) {
alert(translateText("common.copy_error") || "Error copying game id");
return;
}
🤖 Prompt for AI Agents
In `@src/client/components/CopyButton.ts` around lines 88 - 91, In the CopyButton
component update the hardcoded alert when text is falsy to use translateText();
replace alert("Error copying game id") with an alert that calls translateText
with an appropriate key or message (e.g., translateText('error.copyingGameId'))
so the error is localized; ensure you import or reference translateText the same
way other components do and use the existing 'text' check in the function where
the alert currently lives.

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

🤖 Fix all issues with AI agents
In `@src/client/Matchmaking.ts`:
- Around line 252-258: The event handler for "userMeResponse" ignores the
explicit false detail (logout) because it uses a truthy check; change the guard
so it accepts false values — e.g., replace `if (customEvent.detail)` with an
explicit undefined/null check like `if (customEvent.detail !== undefined &&
customEvent.detail !== null)` or simply remove the guard and always run `const
userMeResponse = customEvent.detail as UserMeResponse | false; this.isLoggedIn =
hasLinkedAccount(userMeResponse);` so that `hasLinkedAccount` runs for `false`
and updates the logout button; reference: the listener on document, the
`userMeResponse` variable and `hasLinkedAccount` call that sets
`this.isLoggedIn`.
🧹 Nitpick comments (1)
src/client/Main.ts (1)

938-945: Prefer class-based hiding instead of inline styles for consistency.

The repo already hides UI using classList.add("hidden"). Consider following the same pattern here.

♻️ Proposed refactor
-    document.querySelectorAll(".no-crazygames").forEach((el) => {
-      (el as HTMLElement).style.display = "none";
-    });
+    document.querySelectorAll(".no-crazygames").forEach((el) => {
+      (el as HTMLElement).classList.add("hidden");
+    });

Based on learnings: 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.

Also applies to: 952-958

Comment on lines +252 to +258
document.addEventListener("userMeResponse", (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail) {
const userMeResponse = customEvent.detail as UserMeResponse | false;
this.isLoggedIn = hasLinkedAccount(userMeResponse);
}
});
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 | 🟡 Minor

Handle userMeResponse = false so logout updates the button.

if (customEvent.detail) skips the false case, so isLoggedIn can stay true after logout.

✅ Proposed fix
-    document.addEventListener("userMeResponse", (event: Event) => {
-      const customEvent = event as CustomEvent;
-      if (customEvent.detail) {
-        const userMeResponse = customEvent.detail as UserMeResponse | false;
-        this.isLoggedIn = hasLinkedAccount(userMeResponse);
-      }
-    });
+    document.addEventListener("userMeResponse", (event: Event) => {
+      const customEvent = event as CustomEvent;
+      if (customEvent.detail !== undefined) {
+        const userMeResponse = customEvent.detail as UserMeResponse | false;
+        this.isLoggedIn = hasLinkedAccount(userMeResponse);
+      }
+    });
📝 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
document.addEventListener("userMeResponse", (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail) {
const userMeResponse = customEvent.detail as UserMeResponse | false;
this.isLoggedIn = hasLinkedAccount(userMeResponse);
}
});
document.addEventListener("userMeResponse", (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail !== undefined) {
const userMeResponse = customEvent.detail as UserMeResponse | false;
this.isLoggedIn = hasLinkedAccount(userMeResponse);
}
});
🤖 Prompt for AI Agents
In `@src/client/Matchmaking.ts` around lines 252 - 258, The event handler for
"userMeResponse" ignores the explicit false detail (logout) because it uses a
truthy check; change the guard so it accepts false values — e.g., replace `if
(customEvent.detail)` with an explicit undefined/null check like `if
(customEvent.detail !== undefined && customEvent.detail !== null)` or simply
remove the guard and always run `const userMeResponse = customEvent.detail as
UserMeResponse | false; this.isLoggedIn = hasLinkedAccount(userMeResponse);` so
that `hasLinkedAccount` runs for `false` and updates the logout button;
reference: the listener on document, the `userMeResponse` variable and
`hasLinkedAccount` call that sets `this.isLoggedIn`.

@evanpelle evanpelle closed this Jan 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants