Skip to content

Place footer ad during spawn phase#3458

Merged
evanpelle merged 1 commit intov30from
footer-ad2
Mar 18, 2026
Merged

Place footer ad during spawn phase#3458
evanpelle merged 1 commit intov30from
footer-ad2

Conversation

@evanpelle
Copy link
Collaborator

@evanpelle evanpelle commented Mar 17, 2026

Description:

Show the "footer_ad" ad type during the spawn phase.

Screenshot 2026-03-17 at 7 01 37 PM

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 Mar 17, 2026

Walkthrough

This pull request consolidates in-game ad components, refactors the lobby lifecycle API from callbacks to promises, updates UI styling for consistency, reworks rate-limiting mechanisms, adjusts game-spawn logic, and modifies annexation behavior with supporting tests.

Changes

Cohort / File(s) Summary
In-Game Ad/Promo System Refactor
index.html, src/client/components/VideoPromo.ts, src/client/graphics/GameRenderer.ts, src/client/graphics/layers/InGameHeaderAd.ts, src/client/graphics/layers/InGamePromo.ts, src/client/graphics/layers/SpawnVideoReward.ts
Removed VideoAd, InGameHeaderAd, and SpawnVideoAd components; introduced new InGamePromo class to unify bottom-rail and corner ad display logic. Updated GameRenderer to wire InGamePromo and adjusted HTML markup to reference single promo component.
UI Styling Consistency Updates
src/client/graphics/layers/AttacksDisplay.ts, src/client/graphics/layers/EventsDisplay.ts, src/client/graphics/layers/GameLeftSidebar.ts, src/client/graphics/layers/GameRightSidebar.ts, src/client/graphics/layers/PlayerInfoOverlay.ts, src/client/graphics/layers/ReplayPanel.ts
Increased background opacity from 70% to 92% and upgraded backdrop blur from xs to sm across HUD layers for visual consistency and improved readability.
Lobby Lifecycle Refactoring
src/client/ClientGameRunner.ts, src/client/Main.ts
Replaced callback-based joinLobby API with promise-based JoinLobbyResult interface exposing stop(), prestart, and join properties. Updated Main.ts to manage lobby lifecycle using lobbyHandle and integrate extended Window.ramp ad API (spaAddAds, destroyUnits, spaAds) with optional selector support.
Server Rate Limiting Overhaul
src/server/ClientMsgRateLimiter.ts, src/server/Worker.ts, tests/server/ClientMsgRateLimiter.test.ts
Transitioned from per-minute byte throttling to cumulative 2MB-per-client tracking. Added per-IP WebSocket pre-join rate limiting (5 tokens/second) with stale-limiter cleanup. Reduced WebSocket maxPayload from 2MB to 1MB. Updated tests to verify cross-client isolation and byte accumulation behavior.
Game Logic & Configuration Adjustments
src/core/configuration/DefaultConfig.ts, src/core/execution/PlayerExecution.ts, src/core/execution/utils/AiAttackBehavior.ts, src/server/MapPlaylist.ts
Modified spawn-phase turn logic to differentiate on random spawn; refined cluster recalculation thresholds and local bounding-box computation for annexation; blocked donations in Public game type; simplified hard-nations modifier handling by removing independent roll logic.
Map Data & Tests
resources/maps/straitofhormuz/manifest.json, tests/core/executions/NoInverseAnnexation.test.ts
Updated Strait of Hormuz manifest with revised height, tile counts, and unicode name normalization. Added comprehensive test suite verifying annexation does not reverse-overwhelm smaller players.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🎬 From many ads, one promo blooms so bright,
Promises replace callbacks in the night,
Rate limits tighten, hexes fatten still,
✨ One small spawn defends against the hill.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 'Place footer ad during spawn phase' directly describes the main change: replacing the previous header/spawn video ads with a single footer ad component displayed during spawn.
Description check ✅ Passed The PR description clearly states the objective: show the footer_ad during spawn phase, with a screenshot provided.

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

📝 Coding Plan
  • Generate coding plan for human review comments

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.

Caution

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

⚠️ Outside diff range comments (1)
tests/server/ClientMsgRateLimiter.test.ts (1)

9-31: ⚠️ Potential issue | 🟡 Minor

Missing test for oversized intent kick.

The implementation kicks when an intent exceeds MAX_INTENT_SIZE (500 bytes), but there is no test for this behavior. Consider adding a test case.

🧪 Suggested test case
   describe("intent messages", () => {
     it("allows intents within limits", () => {
       const limiter = new ClientMsgRateLimiter();
       expect(limiter.check(CLIENT_A, "intent", SMALL)).toBe("ok");
     });
 
+    it("kicks when intent exceeds max size", () => {
+      const limiter = new ClientMsgRateLimiter();
+      expect(limiter.check(CLIENT_A, "intent", 501)).toBe("kick");
+    });
+
     it("limits when per-second count exceeded", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/server/ClientMsgRateLimiter.test.ts` around lines 9 - 31, Add a test to
cover the oversized-intent branch: create a ClientMsgRateLimiter instance and
call its check method with message type "intent" and a payload whose size
exceeds MAX_INTENT_SIZE (the constant used by the implementation, e.g., >500
bytes) for CLIENT_A, then assert the result is the expected "kick" outcome;
place this new it-block alongside the other intent tests to ensure oversized
intents trigger the kick path in ClientMsgRateLimiter.check.
🧹 Nitpick comments (9)
src/server/Worker.ts (1)

618-635: Simple cleanup approach is acceptable, but note the trade-off.

Clearing all IP limiters every 10 minutes is straightforward and prevents memory growth. However, active rate-limited IPs get a fresh token budget immediately after cleanup.

If stricter enforcement is needed later, consider tracking last-access time and only removing stale entries. For now, this simple approach is fine since authenticated clients get per-client rate limiting in ClientMsgRateLimiter anyway.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/Worker.ts` around lines 618 - 635, The current cleanup clears the
entire wsIpLimiters map every 10 minutes which resets token budgets for active
IPs; update the implementation to track last access per IP and only remove stale
limiters instead of clearing all: augment the wsIpLimiters Map entries (or
create a parallel Map) to store a timestamp updated inside getWsIpLimiter(ip)
whenever a limiter is retrieved/created, and replace the setInterval(() =>
wsIpLimiters.clear(), ...) task with a sweeper that iterates wsIpLimiters and
deletes entries whose last-access timestamp is older than a configurable stale
threshold (e.g., 10 minutes); keep the RateLimiter creation logic in
getWsIpLimiter and ensure timestamp updates occur on both creation and
retrieval.
src/core/execution/PlayerExecution.ts (1)

101-103: Replace hard-coded tile threshold with a named constant.

The 100 threshold would be easier to tune and reason about if named once near ticksPerClusterCalc.

♻️ Suggested refactor
 export class PlayerExecution implements Execution {
   private readonly ticksPerClusterCalc = 20;
+  private readonly smallPlayerClusterRecalcTileThreshold = 100;
@@
     if (
       ticks - this.lastCalc > this.ticksPerClusterCalc ||
-      this.player.numTilesOwned() < 100
+      this.player.numTilesOwned() < this.smallPlayerClusterRecalcTileThreshold
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/PlayerExecution.ts` around lines 101 - 103, Introduce a
named constant for the hard-coded tile threshold used in the PlayerExecution
cluster recalculation condition: instead of comparing
this.player.numTilesOwned() < 100 inline, declare a constant (e.g.
minTilesForClusterCalc or TILE_THRESHOLD_FOR_CLUSTER) alongside
ticksPerClusterCalc and use that constant in the condition; update the condition
that references this.lastCalc and this.ticksPerClusterCalc to use the new
constant so the threshold is centralized and easier to tune (reference: class
PlayerExecution, properties ticksPerClusterCalc and lastCalc, and method/usage
player.numTilesOwned()).
tests/core/executions/NoInverseAnnexation.test.ts (2)

58-60: Drop the no-op assertion at Line 59.

expect(largePlayer.numTilesOwned()).toBe(initialLargeTiles) immediately after assignment does not add signal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/executions/NoInverseAnnexation.test.ts` around lines 58 - 60,
Remove the redundant assertion that re-checks the same value just assigned:
delete the expect(largePlayer.numTilesOwned()).toBe(initialLargeTiles) line (the
no-op using initialLargeTiles and largePlayer.numTilesOwned()). Keep the
meaningful assertion about smallPlayer.numTilesOwned() or replace the removed
line with a real behavior check if needed; reference the variables
initialLargeTiles, largePlayer.numTilesOwned(), and smallPlayer.numTilesOwned()
to locate and edit the test.

63-70: Make tick advancement less brittle to internal timing changes.

This test depends on fixed values (20, 50) that are tied to cluster recalculation cadence. Consider advancing until the expected state (with a max tick cap) so the test is stable if cadence changes.

♻️ Suggested refactor
-    // Keep ticksPerClusterCalc and lastTileChange in mind
-    executeTicks(game, 20);
+    // Warm up simulation before the trigger actions
+    executeTicks(game, 25);
     largePlayer.conquer(game.ref(49, 49));
     smallPlayer.conquer(game.ref(50, 50));

-    // Annexation happens here
-    executeTicks(game, 50);
+    // Annexation should happen within a bounded window
+    let steps = 0;
+    const maxSteps = 120;
+    while (steps < maxSteps && smallPlayer.numTilesOwned() > 0) {
+      game.executeNextTick();
+      steps++;
+    }
+    expect(steps).toBeLessThan(maxSteps);
     expect(largePlayer.numTilesOwned()).toBeGreaterThan(initialLargeTiles);
     expect(smallPlayer.numTilesOwned()).toBe(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/executions/NoInverseAnnexation.test.ts` around lines 63 - 70,
Replace the hardcoded tick advances with a loop that calls executeTicks until
the expected post-annexation state is observed or a max tick cap is reached:
after calling largePlayer.conquer(game.ref(49, 49)) and
smallPlayer.conquer(game.ref(50, 50)), repeatedly call executeTicks(game, 1) (or
a small step) and check the conditions using largePlayer.numTilesOwned() >
initialLargeTiles and smallPlayer.numTilesOwned() === 0; break when both are
true or throw/assert failure after the cap to avoid flakiness. Use the existing
functions executeTicks, largePlayer.conquer, smallPlayer.conquer,
largePlayer.numTilesOwned, smallPlayer.numTilesOwned and preserve
initialLargeTiles in the check. Ensure the loop enforces a reasonable maxTicks
to prevent infinite loops in CI.
src/core/execution/utils/AiAttackBehavior.ts (1)

850-853: Recommended: centralize the public-game donation policy.
This new guard is good, but keeping the rule only in AiAttackBehavior.donateTroops() can cause policy drift. Prefer one shared eligibility check (e.g., config-level or execution-level) so all troop-donation paths follow the same rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/execution/utils/AiAttackBehavior.ts` around lines 850 - 853, The
guard in AiAttackBehavior.donateTroops() that checks
this.game.config().gameConfig().gameType === GameType.Public should be
centralized: add a single shared eligibility method (e.g.,
GameConfig.canDonateTroops() or ExecutionContext.isDonationAllowed()) that
encapsulates the public-game rule (and any future rules), replace the inline
check in AiAttackBehavior.donateTroops() with a call to that new method, and
update all other troop-donation entry points to call the same method so all
donation paths follow the same policy.
src/core/configuration/DefaultConfig.ts (1)

548-554: Optional cleanup: extract spawn turn literals into named constants.
Using named constants for 100, 150, and 300 would make future balance tuning easier and reduce magic numbers in config logic.

♻️ Suggested refactor
+const SINGLEPLAYER_SPAWN_PHASE_TURNS = 100;
+const RANDOM_SPAWN_PHASE_TURNS = 150;
+const DEFAULT_SPAWN_PHASE_TURNS = 300;
...
  numSpawnPhaseTurns(): number {
    if (this._gameConfig.gameType === GameType.Singleplayer) {
-      return 100;
+      return SINGLEPLAYER_SPAWN_PHASE_TURNS;
    }
    if (this.isRandomSpawn()) {
-      return 150;
+      return RANDOM_SPAWN_PHASE_TURNS;
    }
-    return 300;
+    return DEFAULT_SPAWN_PHASE_TURNS;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/configuration/DefaultConfig.ts` around lines 548 - 554, Replace the
magic literals 100, 150, and 300 in DefaultConfig with named constants to make
tuning clearer: add constants (e.g., SINGLEPLAYER_SPAWN_TURN = 100,
RANDOM_SPAWN_TURN = 150, DEFAULT_SPAWN_TURN = 300) near the top of the
DefaultConfig module/class and update the method using this._gameConfig.gameType
and isRandomSpawn() to return those constants instead of raw numbers; ensure the
constant names are descriptive and used in the return statements so future
balance changes only require editing the constants.
src/client/ClientGameRunner.ts (1)

80-83: Consider adding promise rejection for error cases.

The prestartPromise and joinPromise never reject. If the connection fails or an error occurs before these lifecycle events, the promises remain pending forever. This could leave callers in a stuck state.

One approach: Add rejectPrestart and rejectJoin handlers, then call them in the "error" message handler.

♻️ Possible enhancement for error handling
  let resolvePrestart: () => void;
  let resolveJoin: () => void;
+ let rejectPrestart: (err: Error) => void;
+ let rejectJoin: (err: Error) => void;
- const prestartPromise = new Promise<void>((r) => (resolvePrestart = r));
- const joinPromise = new Promise<void>((r) => (resolveJoin = r));
+ const prestartPromise = new Promise<void>((resolve, reject) => {
+   resolvePrestart = resolve;
+   rejectPrestart = reject;
+ });
+ const joinPromise = new Promise<void>((resolve, reject) => {
+   resolveJoin = resolve;
+   rejectJoin = reject;
+ });

Then in the error handler around line 165:

    if (message.type === "error") {
+     const err = new Error(message.error);
+     rejectPrestart(err);
+     rejectJoin(err);
      if (message.error === "full-lobby") {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/ClientGameRunner.ts` around lines 80 - 83, prestartPromise and
joinPromise are never rejected so they can hang forever; add paired reject
handlers (e.g., rejectPrestart and rejectJoin) when creating the promises
(alongside resolvePrestart/resolveJoin) and store them, then invoke those reject
functions from the connection's "error" handler (and any connection
close/failure handler) so callers receive a rejection on connection failure or
other errors; update any cleanup logic to avoid calling both resolve and reject
for the same promise.
src/client/Main.ts (1)

764-818: Consider extracting the prestart handler to a named method.

This .then() callback spans ~50 lines handling modal closures, UI cleanup, and loading state. Extracting it to a method like onLobbyPrestart() would improve readability and make the flow easier to follow.

♻️ Example extraction
+ private onLobbyPrestart(): void {
+   console.log("Closing modals");
+   document.getElementById("settings-button")?.classList.add("hidden");
+   // ... rest of the prestart logic
+ }

  this.lobbyHandle.prestart.then(() => {
-   console.log("Closing modals");
-   document.getElementById("settings-button")?.classList.add("hidden");
-   // ... 50+ lines
+   this.onLobbyPrestart();
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/Main.ts` around lines 764 - 818, Extract the long anonymous
prestart handler passed to this.lobbyHandle.prestart into a named instance
method (suggested name: onLobbyPrestart) and replace the .then(...) callback
with this.onLobbyPrestart.bind(this) or this.onLobbyPrestart(); move all logic
that manipulates UI elements (settings-button hiding,
usernameInput.validationError reset, username-validation-error hiding,
joinModal.closeWithoutLeaving, the array-forEach that closes modals,
gameModeSelector.stop, ad hiding, crazyGamesSDK.loadingStart and the
startingModal lookup/show) into that new onLobbyPrestart method so the Main
class keeps the same behavior but with clearer structure and a concise prestart
hookup.
src/client/graphics/layers/InGamePromo.ts (1)

138-144: Consider using rem or px instead of cm for positioning.

style="bottom: -0.7cm" uses centimeters which can render differently across screens and DPI settings. Using rem (relative to root font size) or px provides more predictable behavior.

♻️ Suggested change
      <div
        id="${AD_CONTAINER_ID}"
        class="fixed left-0 z-[100] pointer-events-auto"
-       style="bottom: -0.7cm"
+       style="bottom: -0.5rem"
      ></div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/InGamePromo.ts` around lines 138 - 144, The inline
style on the ad container uses physical units ("bottom: -0.7cm") which can vary
across devices; update the InGamePromo component to use a screen-relative unit
(e.g., replace the "bottom" value on the element with a rem or px value) by
changing the style on the element that renders AD_CONTAINER_ID from "bottom:
-0.7cm" to a consistent unit such as "bottom: -0.7rem" or an equivalent "bottom:
-12px" so positioning is predictable across DPIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/server/ClientMsgRateLimiter.test.ts`:
- Around line 9-31: Add a test to cover the oversized-intent branch: create a
ClientMsgRateLimiter instance and call its check method with message type
"intent" and a payload whose size exceeds MAX_INTENT_SIZE (the constant used by
the implementation, e.g., >500 bytes) for CLIENT_A, then assert the result is
the expected "kick" outcome; place this new it-block alongside the other intent
tests to ensure oversized intents trigger the kick path in
ClientMsgRateLimiter.check.

---

Nitpick comments:
In `@src/client/ClientGameRunner.ts`:
- Around line 80-83: prestartPromise and joinPromise are never rejected so they
can hang forever; add paired reject handlers (e.g., rejectPrestart and
rejectJoin) when creating the promises (alongside resolvePrestart/resolveJoin)
and store them, then invoke those reject functions from the connection's "error"
handler (and any connection close/failure handler) so callers receive a
rejection on connection failure or other errors; update any cleanup logic to
avoid calling both resolve and reject for the same promise.

In `@src/client/graphics/layers/InGamePromo.ts`:
- Around line 138-144: The inline style on the ad container uses physical units
("bottom: -0.7cm") which can vary across devices; update the InGamePromo
component to use a screen-relative unit (e.g., replace the "bottom" value on the
element with a rem or px value) by changing the style on the element that
renders AD_CONTAINER_ID from "bottom: -0.7cm" to a consistent unit such as
"bottom: -0.7rem" or an equivalent "bottom: -12px" so positioning is predictable
across DPIs.

In `@src/client/Main.ts`:
- Around line 764-818: Extract the long anonymous prestart handler passed to
this.lobbyHandle.prestart into a named instance method (suggested name:
onLobbyPrestart) and replace the .then(...) callback with
this.onLobbyPrestart.bind(this) or this.onLobbyPrestart(); move all logic that
manipulates UI elements (settings-button hiding, usernameInput.validationError
reset, username-validation-error hiding, joinModal.closeWithoutLeaving, the
array-forEach that closes modals, gameModeSelector.stop, ad hiding,
crazyGamesSDK.loadingStart and the startingModal lookup/show) into that new
onLobbyPrestart method so the Main class keeps the same behavior but with
clearer structure and a concise prestart hookup.

In `@src/core/configuration/DefaultConfig.ts`:
- Around line 548-554: Replace the magic literals 100, 150, and 300 in
DefaultConfig with named constants to make tuning clearer: add constants (e.g.,
SINGLEPLAYER_SPAWN_TURN = 100, RANDOM_SPAWN_TURN = 150, DEFAULT_SPAWN_TURN =
300) near the top of the DefaultConfig module/class and update the method using
this._gameConfig.gameType and isRandomSpawn() to return those constants instead
of raw numbers; ensure the constant names are descriptive and used in the return
statements so future balance changes only require editing the constants.

In `@src/core/execution/PlayerExecution.ts`:
- Around line 101-103: Introduce a named constant for the hard-coded tile
threshold used in the PlayerExecution cluster recalculation condition: instead
of comparing this.player.numTilesOwned() < 100 inline, declare a constant (e.g.
minTilesForClusterCalc or TILE_THRESHOLD_FOR_CLUSTER) alongside
ticksPerClusterCalc and use that constant in the condition; update the condition
that references this.lastCalc and this.ticksPerClusterCalc to use the new
constant so the threshold is centralized and easier to tune (reference: class
PlayerExecution, properties ticksPerClusterCalc and lastCalc, and method/usage
player.numTilesOwned()).

In `@src/core/execution/utils/AiAttackBehavior.ts`:
- Around line 850-853: The guard in AiAttackBehavior.donateTroops() that checks
this.game.config().gameConfig().gameType === GameType.Public should be
centralized: add a single shared eligibility method (e.g.,
GameConfig.canDonateTroops() or ExecutionContext.isDonationAllowed()) that
encapsulates the public-game rule (and any future rules), replace the inline
check in AiAttackBehavior.donateTroops() with a call to that new method, and
update all other troop-donation entry points to call the same method so all
donation paths follow the same policy.

In `@src/server/Worker.ts`:
- Around line 618-635: The current cleanup clears the entire wsIpLimiters map
every 10 minutes which resets token budgets for active IPs; update the
implementation to track last access per IP and only remove stale limiters
instead of clearing all: augment the wsIpLimiters Map entries (or create a
parallel Map) to store a timestamp updated inside getWsIpLimiter(ip) whenever a
limiter is retrieved/created, and replace the setInterval(() =>
wsIpLimiters.clear(), ...) task with a sweeper that iterates wsIpLimiters and
deletes entries whose last-access timestamp is older than a configurable stale
threshold (e.g., 10 minutes); keep the RateLimiter creation logic in
getWsIpLimiter and ensure timestamp updates occur on both creation and
retrieval.

In `@tests/core/executions/NoInverseAnnexation.test.ts`:
- Around line 58-60: Remove the redundant assertion that re-checks the same
value just assigned: delete the
expect(largePlayer.numTilesOwned()).toBe(initialLargeTiles) line (the no-op
using initialLargeTiles and largePlayer.numTilesOwned()). Keep the meaningful
assertion about smallPlayer.numTilesOwned() or replace the removed line with a
real behavior check if needed; reference the variables initialLargeTiles,
largePlayer.numTilesOwned(), and smallPlayer.numTilesOwned() to locate and edit
the test.
- Around line 63-70: Replace the hardcoded tick advances with a loop that calls
executeTicks until the expected post-annexation state is observed or a max tick
cap is reached: after calling largePlayer.conquer(game.ref(49, 49)) and
smallPlayer.conquer(game.ref(50, 50)), repeatedly call executeTicks(game, 1) (or
a small step) and check the conditions using largePlayer.numTilesOwned() >
initialLargeTiles and smallPlayer.numTilesOwned() === 0; break when both are
true or throw/assert failure after the cap to avoid flakiness. Use the existing
functions executeTicks, largePlayer.conquer, smallPlayer.conquer,
largePlayer.numTilesOwned, smallPlayer.numTilesOwned and preserve
initialLargeTiles in the check. Ensure the loop enforces a reasonable maxTicks
to prevent infinite loops in CI.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 36f3b237-cf6e-4711-85ad-0bc17b9d2d81

📥 Commits

Reviewing files that changed from the base of the PR and between 6952550 and 4a7b1e7.

⛔ Files ignored due to path filters (8)
  • map-generator/assets/maps/niledelta/image.png is excluded by !**/*.png
  • map-generator/assets/maps/straitofhormuz/image.png is excluded by !**/*.png
  • resources/maps/niledelta/map.bin is excluded by !**/*.bin
  • resources/maps/niledelta/map16x.bin is excluded by !**/*.bin
  • resources/maps/niledelta/map4x.bin is excluded by !**/*.bin
  • resources/maps/straitofhormuz/map.bin is excluded by !**/*.bin
  • resources/maps/straitofhormuz/map16x.bin is excluded by !**/*.bin
  • resources/maps/straitofhormuz/map4x.bin is excluded by !**/*.bin
📒 Files selected for processing (25)
  • index.html
  • resources/maps/niledelta/thumbnail.webp
  • resources/maps/straitofhormuz/manifest.json
  • resources/maps/straitofhormuz/thumbnail.webp
  • src/client/ClientGameRunner.ts
  • src/client/Main.ts
  • src/client/components/VideoPromo.ts
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/AttacksDisplay.ts
  • src/client/graphics/layers/EventsDisplay.ts
  • src/client/graphics/layers/GameLeftSidebar.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/InGameHeaderAd.ts
  • src/client/graphics/layers/InGamePromo.ts
  • src/client/graphics/layers/PlayerInfoOverlay.ts
  • src/client/graphics/layers/ReplayPanel.ts
  • src/client/graphics/layers/SpawnVideoReward.ts
  • src/core/configuration/DefaultConfig.ts
  • src/core/execution/PlayerExecution.ts
  • src/core/execution/utils/AiAttackBehavior.ts
  • src/server/ClientMsgRateLimiter.ts
  • src/server/MapPlaylist.ts
  • src/server/Worker.ts
  • tests/core/executions/NoInverseAnnexation.test.ts
  • tests/server/ClientMsgRateLimiter.test.ts
💤 Files with no reviewable changes (3)
  • src/client/graphics/layers/SpawnVideoReward.ts
  • src/client/graphics/layers/InGameHeaderAd.ts
  • src/client/components/VideoPromo.ts

@github-project-automation github-project-automation bot moved this from Triage to Final Review in OpenFront Release Management Mar 17, 2026
@evanpelle evanpelle marked this pull request as ready for review March 18, 2026 01:25
@evanpelle evanpelle requested a review from a team as a code owner March 18, 2026 01:25
@evanpelle evanpelle added this to the v30 milestone Mar 18, 2026
@evanpelle evanpelle merged commit cb7164a into v30 Mar 18, 2026
18 of 27 checks passed
@evanpelle evanpelle deleted the footer-ad2 branch March 18, 2026 02:02
@github-project-automation github-project-automation bot moved this from Final Review to Complete in OpenFront Release Management Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant