Skip to content

Implement FX sound effects#3394

Draft
camclark wants to merge 6 commits intoopenfrontio:mainfrom
camclark:feature/fx-sound-effects
Draft

Implement FX sound effects#3394
camclark wants to merge 6 commits intoopenfrontio:mainfrom
camclark:feature/fx-sound-effects

Conversation

@camclark
Copy link

@camclark camclark commented Mar 10, 2026

Description:

Adds sound effects for all approved events from the sound asset pack. Extends SoundManager with 15 new SoundEffect enum values and hooks sound triggers into FxLayer, EventsDisplay, and RadialMenu. Sound playback is decoupled from the visual FX toggle — audio plays even when FX layer is disabled. Only approved sounds from the spreadsheet are wired up; sounds marked "Waiting for Approval" are included as assets but not played.

Sound approval status (per spreadsheet)

Approved — wired up in this PR

Event Sound file Trigger location
Message sent/received message.mp3 EventsDisplay
Menu open/select click.mp3 RadialMenu
Atom bomb launch atom-launch.mp3 FxLayer (unit created)
Atom bomb / MIRV hit atom-hit.mp3 FxLayer (reached target)
Hydrogen launch hydrogen-launch.mp3 FxLayer (unit created)
Hydrogen hit hydrogen-hit.mp3 FxLayer (reached target)
MIRV launch mirv-launch.mp3 FxLayer (unit created)
Alliance suggested alliance-suggested.mp3 EventsDisplay
Alliance broken alliance-broken.mp3 EventsDisplay
Port built build-port.mp3 FxLayer (construction complete)
City built build-city.mp3 FxLayer (construction complete)
Defense post built build-defense-post.mp3 FxLayer (construction complete)
Warship built build-warship.mp3 FxLayer (unit created)
SAM built sam-built.mp3 FxLayer (construction complete)

Waiting for Approval — sound files included but NOT wired up

Event Sound file Notes
Missile Silo built silo-built.mp3 Waiting for Approval
SAM shoot sam-shoot.mp3 Waiting for Approval
SAM hit Waiting for Approval, no sound file assigned
Warship sunk Waiting for Approval, no sound file assigned
Warship shoot Waiting for Approval, no sound file assigned

Not done — no sound files exist yet

Event Notes
Looted player "Not sure if needed"
Invaded
Ship invasion incoming
Ship sent
Menu theme song
Ambience "Not sure if needed"

Test plan

  • Start a private game and launch atom/hydrogen/MIRV nukes — verify launch and detonation sounds
  • Build structures (city, port, defense post, SAM) — verify build completion sounds
  • Build a warship — verify warship built sound
  • Receive an alliance request — verify alliance suggested sound
  • Break an alliance — verify alliance broken sound
  • Receive a chat message — verify message sound
  • Open the radial menu and click items — verify click sound
  • Disable visual FX in settings — verify sounds still play
  • Adjust SFX volume slider — verify it affects all new sounds
  • Verify no audio issues with rapid/overlapping events

Checklist

  • 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

Resolves #1893

Please put your Discord username so you can be contacted if a bug or regression is found:

cool_clarky

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b3e1523-19db-4a4b-8d3b-e786cd60b155

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Centralized per-event FX gating in FxLayer and added construction tracking; many new sound assets and enum entries registered; events and UI (chat, alliances, radial menu, unit/structure events) now trigger corresponding sound effects while visual FX remain gated per-event.

Changes

Cohort / File(s) Summary
FX logic & gating
src/client/graphics/layers/FxLayer.ts
Added private fxEnabled(): boolean and private constructionState: Map<number, boolean>; added onUnitCreated and onStructureBuilt; replaced a single upfront fx flag with per-event fxEnabled() checks; preserved key sounds when visuals disabled; expanded structure construction handling and event gating.
Sound assets & registration
src/client/sound/SoundManager.ts
Extended SoundEffect enum (AtomHit, AtomLaunch, HydrogenHit, HydrogenLaunch, MIRVLaunch, AllianceSuggested, AllianceBroken, BuildPort, BuildCity, BuildDefensePost, BuildWarship, SAMBuilt, Message, Click, etc.) and registered/loaded corresponding audio assets during initialization.
Event-driven sounds
src/client/graphics/layers/EventsDisplay.ts
Imported SoundManager/SoundEffect; play SoundEffect.Message on new chat messages; play SoundEffect.AllianceSuggested and SoundEffect.AllianceBroken for alliance events.
UI feedback sound
src/client/graphics/layers/RadialMenu.ts
Play SoundEffect.Click via SoundManager.playSoundEffect when a radial menu item is clicked before performing navigation or actions.

Sequence Diagram(s)

sequenceDiagram
    participant Event as GameEvent
    participant Fx as FxLayer
    participant EventsDisp as EventsDisplay
    participant UI as RadialMenu
    participant Sound as SoundManager
    participant Audio as AudioSystem

    Event->>Fx: deliver event (shell/train/warship/structure/conquest)
    Fx->>Fx: fxEnabled()? (per-event)
    alt fxEnabled == true
        Fx->>Fx: create visual FX (explosion, shockwave, building FX)
    end
    Fx->>Sound: play(launch/hit/build sounds)
    EventsDisp->>Sound: play(Message / AllianceSuggested / AllianceBroken)
    UI->>Sound: play(Click) on item click
    Sound->>Audio: load/decode & play asset
    Audio-->>Sound: playback started
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Sounds queue up where pixels bloom,
Builds and booms now fill the room,
Chats chime, alliances chime back,
Clicks echo down the radial track,
Small jingles guard the victory plume.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement FX sound effects' directly and concisely describes the main change: adding sound effects throughout the codebase.
Linked Issues check ✅ Passed The PR successfully implements all coding objectives from #1893: extends SoundManager with 15 new enum values, loads audio assets, and hooks sound triggers into FxLayer, EventsDisplay, and RadialMenu for approved events.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing FX sound effects: sound asset loading, enum extensions, event-based sound triggers, and FX-independent audio playback. No out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description comprehensively documents the implementation of sound effects for approved game events, including detailed sound approval status, trigger locations, test plan, and a clear link to the resolved issue.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/graphics/layers/FxLayer.ts`:
- Line 106: The visual FX guard in FxLayer.tick (userSettings()?.fxLayer())
currently prevents all SoundManager.playSoundEffect calls (e.g.,
SoundEffect.WarshipShot and the other calls at the locations you noted), which
mutes combat audio when visual FX are off; change this by decoupling sound
playback from the fxLayer gate: either move the SoundManager.playSoundEffect
calls outside the fxLayer conditional or wrap them with a separate sound setting
check (e.g., userSettings()?.sfxEnabled() or userSettings()?.sfxVolume() > 0)
and use that guard before calling SoundManager.playSoundEffect (update all
instances: the call at SoundEffect.WarshipShot plus the calls at the other noted
locations). Ensure you reference FxLayer.tick and SoundManager.playSoundEffect
when making the change so audio respects the SFX setting independently of visual
FX.

In `@src/client/sound/SoundManager.ts`:
- Around line 6-14: Prettier formatting errors are causing CI to fail for
SoundManager.ts; run the project's Prettier formatter (e.g., npx prettier
--write) on this file to fix import spacing/line breaks for the top imports
(atomHitSound, atomLaunchSound, hydrogenHitSound, hydrogenLaunchSound,
mirvLaunchSound, samHitSound, samShootSound, warshipShotSound, warshipLostSound)
and also reformat the block around lines 58-66 referenced in the review so the
file conforms to the repo's Prettier rules before committing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca5aea94-9f3e-4b4a-a425-cd85845e3d8a

📥 Commits

Reviewing files that changed from the base of the PR and between 8d5ec2b and e976d34.

⛔ Files ignored due to path filters (19)
  • resources/sounds/effects/alliance-broken.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/alliance-suggested.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/atom-hit.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/atom-launch.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/build-city.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/build-defense-post.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/build-port.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/build-warship.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/click.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/hydrogen-hit.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/hydrogen-launch.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/message.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/mirv-launch.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/sam-built.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/sam-hit.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/sam-shoot.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/silo-built.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/warship-lost.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/warship-shot.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (2)
  • src/client/graphics/layers/FxLayer.ts
  • src/client/sound/SoundManager.ts

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Mar 10, 2026
@camclark camclark force-pushed the feature/fx-sound-effects branch from e976d34 to 9b4bb9a Compare March 10, 2026 12:31
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 (2)
src/client/graphics/layers/FxLayer.ts (2)

120-134: ⚠️ Potential issue | 🟠 Major

Missing shell impact sound effect.

Per PR objectives, warship shell impacts should play warship-shot.mp3. Add SoundManager.playSoundEffect(SoundEffect.WarshipShot) when the shell reaches its target.

Proposed fix
   onShellEvent(unit: UnitView) {
     if (!unit.isActive()) {
       if (unit.reachedTarget() && this.fxEnabled()) {
         const x = this.game.x(unit.lastTile());
         const y = this.game.y(unit.lastTile());
         const explosion = new SpriteFx(
           this.animatedSpriteLoader,
           x,
           y,
           FxType.MiniExplosion,
         );
         this.allFx.push(explosion);
       }
+      if (unit.reachedTarget()) {
+        SoundManager.playSoundEffect(SoundEffect.WarshipShot);
+      }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/FxLayer.ts` around lines 120 - 134, The
onShellEvent handler currently creates a MiniExplosion SpriteFx when a UnitView
shell reaches its target but doesn't play the impact sound; update onShellEvent
so that when unit.reachedTarget() && this.fxEnabled() succeeds you also call
SoundManager.playSoundEffect(SoundEffect.WarshipShot) (alongside creating the
SpriteFx and pushing to this.allFx) to trigger the warship shell impact audio;
ensure you reference the existing SoundManager and SoundEffect enums when adding
this call.

185-207: ⚠️ Potential issue | 🟠 Major

Missing warship destruction sound effect.

Per PR objectives, warship destruction should play warship-lost.mp3. Add SoundManager.playSoundEffect(SoundEffect.WarshipLost) outside the fxEnabled() guard.

Proposed fix
   onWarshipEvent(unit: UnitView) {
-    if (!unit.isActive() && this.fxEnabled()) {
+    if (!unit.isActive()) {
+      SoundManager.playSoundEffect(SoundEffect.WarshipLost);
+      if (!this.fxEnabled()) return;
       const x = this.game.x(unit.lastTile());
       const y = this.game.y(unit.lastTile());
       // ... rest of visual FX
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/FxLayer.ts` around lines 185 - 207, onWarshipEvent
currently only plays visual FX when fxEnabled() is true and therefore misses the
required warship destruction audio; move or add a call to
SoundManager.playSoundEffect(SoundEffect.WarshipLost) so it executes whenever a
UnitView is inactive (i.e., inside the outer if (!unit.isActive()) block) but
outside the fxEnabled() guard that surrounds visual effects (refer to the
onWarshipEvent method, UnitView.isActive(), fxEnabled(), and use
SoundManager.playSoundEffect with SoundEffect.WarshipLost).
🧹 Nitpick comments (1)
src/client/graphics/layers/FxLayer.ts (1)

29-29: Consider periodic cleanup of constructionState Map.

This Map tracks construction state per unit ID. Entries are removed when units become inactive (line 222), but if a unit is ever removed from the game without triggering onStructureEvent in inactive state, the entry would persist. For a long-running game, this could accumulate stale entries.

A simple fix: periodically prune entries for unit IDs that no longer exist in the game, or clear the Map on game reset.

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

In `@src/client/graphics/layers/FxLayer.ts` at line 29, The constructionState Map
can accumulate stale entries; add a periodic prune and game-reset clear:
implement a private method (e.g., pruneConstructionState) that iterates
constructionState.keys(), checks unit existence via the game's unit lookup
(e.g., getUnitById or units collection) and deletes entries for missing IDs, and
call this method from a timer (or the main update loop) and from the game reset
handler (e.g., onGameReset or similar); ensure calls are added alongside
existing onStructureEvent and the Map is cleared during reset to prevent
long-term growth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/graphics/layers/EventsDisplay.ts`:
- Around line 553-558: The AllianceBroken sound is triggered before checking
whether the betrayed player is disconnected, which can cause the sound to play
while the UI event is skipped; in EventsDisplay.ts, move the
betrayed.isDisconnected() check to run before calling
SoundManager.playSoundEffect(SoundEffect.AllianceBroken) (and return early if
disconnected) so the sound is only played when the code will also add the
corresponding UI event for update where update.betrayedID or update.traitorID
equals myPlayer.smallID().

In `@src/client/graphics/layers/FxLayer.ts`:
- Around line 209-222: When a structure is detected destroyed in
onStructureEvent (if (!unit.isActive())), add a call to
SoundManager.playSoundEffect(SoundEffect.WarshipShot) so the warship-shot.mp3
plays on destruction; place this call inside the same if-block handling
destruction (near where SpriteFx/FxType.BuildingExplosion is created and before
constructionState.delete(unit.id())) so the sound always plays when a UnitView
becomes inactive.
- Around line 282-296: In handleSAMInterception, play the SAM interception sound
unconditionally by invoking the same sound playback used in handleNukeExplosion
to play SoundEffect.SAMHit (sam-hit.mp3) outside the fxEnabled() guard; locate
handleSAMInterception and add a call to the existing sound playback API (the
method used for nuke sound playback) before or after the fxEnabled() block so
the visual FX remain gated but the SoundEffect.SAMHit is always played.

---

Outside diff comments:
In `@src/client/graphics/layers/FxLayer.ts`:
- Around line 120-134: The onShellEvent handler currently creates a
MiniExplosion SpriteFx when a UnitView shell reaches its target but doesn't play
the impact sound; update onShellEvent so that when unit.reachedTarget() &&
this.fxEnabled() succeeds you also call
SoundManager.playSoundEffect(SoundEffect.WarshipShot) (alongside creating the
SpriteFx and pushing to this.allFx) to trigger the warship shell impact audio;
ensure you reference the existing SoundManager and SoundEffect enums when adding
this call.
- Around line 185-207: onWarshipEvent currently only plays visual FX when
fxEnabled() is true and therefore misses the required warship destruction audio;
move or add a call to SoundManager.playSoundEffect(SoundEffect.WarshipLost) so
it executes whenever a UnitView is inactive (i.e., inside the outer if
(!unit.isActive()) block) but outside the fxEnabled() guard that surrounds
visual effects (refer to the onWarshipEvent method, UnitView.isActive(),
fxEnabled(), and use SoundManager.playSoundEffect with SoundEffect.WarshipLost).

---

Nitpick comments:
In `@src/client/graphics/layers/FxLayer.ts`:
- Line 29: The constructionState Map can accumulate stale entries; add a
periodic prune and game-reset clear: implement a private method (e.g.,
pruneConstructionState) that iterates constructionState.keys(), checks unit
existence via the game's unit lookup (e.g., getUnitById or units collection) and
deletes entries for missing IDs, and call this method from a timer (or the main
update loop) and from the game reset handler (e.g., onGameReset or similar);
ensure calls are added alongside existing onStructureEvent and the Map is
cleared during reset to prevent long-term growth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2973b44-a251-408e-8b2e-f5e9d872492a

📥 Commits

Reviewing files that changed from the base of the PR and between 9b4bb9a and 3d0c1c7.

📒 Files selected for processing (4)
  • src/client/graphics/layers/EventsDisplay.ts
  • src/client/graphics/layers/FxLayer.ts
  • src/client/graphics/layers/RadialMenu.ts
  • src/client/sound/SoundManager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/sound/SoundManager.ts

Comment on lines 209 to +222
onStructureEvent(unit: UnitView) {
if (!unit.isActive()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const explosion = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.BuildingExplosion,
);
this.allFx.push(explosion);
if (this.fxEnabled()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const explosion = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.BuildingExplosion,
);
this.allFx.push(explosion);
}
this.constructionState.delete(unit.id());
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing structure destruction sound effect.

Per PR objectives, structure destruction should play warship-shot.mp3. Add SoundManager.playSoundEffect(SoundEffect.WarshipShot) when a structure is destroyed.

Proposed fix
   onStructureEvent(unit: UnitView) {
     if (!unit.isActive()) {
+      SoundManager.playSoundEffect(SoundEffect.WarshipShot);
       if (this.fxEnabled()) {
         const x = this.game.x(unit.lastTile());
         // ... rest of visual FX
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/FxLayer.ts` around lines 209 - 222, When a
structure is detected destroyed in onStructureEvent (if (!unit.isActive())), add
a call to SoundManager.playSoundEffect(SoundEffect.WarshipShot) so the
warship-shot.mp3 plays on destruction; place this call inside the same if-block
handling destruction (near where SpriteFx/FxType.BuildingExplosion is created
and before constructionState.delete(unit.id())) so the sound always plays when a
UnitView becomes inactive.

Copy link
Author

Choose a reason for hiding this comment

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

warship-shot is "Waiting for Approval" in the sound spreadsheet and doesn't have an approved sound file yet. Will wire up once approved.

Copy link
Contributor

Choose a reason for hiding this comment

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

@camclark, understood! The PR objectives already document this clearly — warship-shot (along with silo-built, sam-shoot, sam-hit, and warship-lost) are intentionally deferred pending sound asset approval. I'll note this for future reviews.


✏️ Learnings added
Learnt from: camclark
Repo: openfrontio/OpenFrontIO PR: 3394
File: src/client/graphics/layers/FxLayer.ts:209-222
Timestamp: 2026-03-10T13:23:16.697Z
Learning: In OpenFrontIO (PR `#3394`, src/client/graphics/layers/FxLayer.ts), the following sound effects are intentionally NOT wired up yet because they are pending approval in the project's sound spreadsheet: silo-built, sam-shoot, sam-hit, warship-lost, warship-shot. Do not flag the absence of these sounds as issues. Wire them up only after they receive approval and have confirmed audio assets.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: wozniakpl
Repo: openfrontio/OpenFrontIO PR: 3317
File: src/client/InputHandler.ts:344-350
Timestamp: 2026-03-07T11:35:24.071Z
Learning: Ensure ghost preservation logic for special units (AtomBomb and HydrogenBomb) is implemented via shouldPreserveGhostAfterBuild(unitType) and that ghost removal is conditional on !shouldPreserveGhostAfterBuild(unitType) within createStructure. Confirm that both mouse (MouseUpEvent) and keyboard (ConfirmGhostStructureEvent) confirmation paths go through requestConfirmStructure → createStructure, and that Enter/NumpadEnter confirmations do not break the locked-nuke multi-placement since the ghost should be preserved by design. Add tests to cover shouldPreserveGhostAfterBuild returning true for AtomBomb/HydrogenBomb and verify that ghost removal only occurs for other units. Applies to files in the repository OpenFrontIO (src/client/InputHandler.ts and src/client/graphics/layers/StructureIconsLayer.ts).

Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2782
File: src/client/graphics/layers/StructureIconsLayer.ts:616-620
Timestamp: 2026-01-03T19:37:30.357Z
Learning: In `src/client/graphics/layers/StructureIconsLayer.ts`, the confirm button in `ensureGhostControls()` for locked ghost nukes (AtomBomb/HydrogenBomb) intentionally does NOT call `removeGhostStructure()` after emitting the build intent. This allows mobile players to rapidly place multiple nukes by "spamming" the confirm button without needing to reopen the radial menu.

Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 0
File: :0-0
Timestamp: 2026-02-25T21:35:31.614Z
Learning: In NationExecution (src/core/execution/NationExecution.ts), even though it's active during spawn phase via activeDuringSpawnPhase() => true, it never calls handleStructures() during that phase - it only handles player spawning. The structure building code path is unreachable during spawn phase, making spawn immunity checks for structure spawning unnecessary.

Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 3337
File: src/client/graphics/layers/UnitLayer.ts:293-300
Timestamp: 2026-03-03T15:02:02.735Z
Learning: In UnitLayer.ts, unit sprites are square by design (sprite.width === sprite.height). Rendering optimizations that assume non-square sprites or attempt to fix aspect ratio issues do not apply to unit sprites in this layer.

Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 3246
File: src/core/execution/nation/NationNukeBehavior.ts:814-832
Timestamp: 2026-02-19T18:07:39.005Z
Learning: In src/core/execution/nation/NationNukeBehavior.ts, the clustering logic in maybeDestroyEnemySam() intentionally checks the arrival time of the first bomb from a silo rather than the last when evaluating if a silo fits within maxTotalArrivalSpread. This slight underestimate is by design: the window is already conservative (half the SAM cooldown = 37 of 75 ticks), staggerInterval is small (1-5 ticks), extra bombs provide margin, and the approach includes borderline silos that work fine in practice. The design should not be "fixed" to use the last bomb's arrival unless SAM timing changes significantly.

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

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.

Learnt from: evanpelle
Repo: openfrontio/OpenFrontIO PR: 3114
File: src/client/graphics/layers/AttacksDisplay.ts:64-77
Timestamp: 2026-02-10T05:02:28.715Z
Learning: In the OpenFrontIO codebase, GameUpdateType maps always have an array value for each key (never undefined). When iterating, you can safely use: for (const event of updates[GameUpdateType.X]) without optional chaining or guards. Treat updates as a Record<GameUpdateType, any[]> and rely on its guaranteed array values. If you modify the type, ensure the guarantee remains (e.g., initialize missing keys to []) to preserve safe iteration.

Comment on lines 282 to 296
handleSAMInterception(unit: UnitView) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const explosion = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.SAMExplosion,
);
this.allFx.push(explosion);
const shockwave = new ShockwaveFx(x, y, 800, 40);
this.allFx.push(shockwave);
if (this.fxEnabled()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const explosion = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.SAMExplosion,
);
this.allFx.push(explosion);
const shockwave = new ShockwaveFx(x, y, 800, 40);
this.allFx.push(shockwave);
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing SAM interception sound effect.

Per PR objectives, SAM interception should play sam-hit.mp3 via SoundEffect.SAMHit. The visual FX is gated behind fxEnabled(), but no sound is played at all. Add sound playback outside the fxEnabled() guard to match the pattern used in handleNukeExplosion.

Proposed fix
   handleSAMInterception(unit: UnitView) {
     if (this.fxEnabled()) {
       const x = this.game.x(unit.lastTile());
       const y = this.game.y(unit.lastTile());
       const explosion = new SpriteFx(
         this.animatedSpriteLoader,
         x,
         y,
         FxType.SAMExplosion,
       );
       this.allFx.push(explosion);
       const shockwave = new ShockwaveFx(x, y, 800, 40);
       this.allFx.push(shockwave);
     }
+    SoundManager.playSoundEffect(SoundEffect.SAMHit);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/FxLayer.ts` around lines 282 - 296, In
handleSAMInterception, play the SAM interception sound unconditionally by
invoking the same sound playback used in handleNukeExplosion to play
SoundEffect.SAMHit (sam-hit.mp3) outside the fxEnabled() guard; locate
handleSAMInterception and add a call to the existing sound playback API (the
method used for nuke sound playback) before or after the fxEnabled() block so
the visual FX remain gated but the SoundEffect.SAMHit is always played.

Copy link
Author

Choose a reason for hiding this comment

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

sam-hit is "Waiting for Approval" in the sound spreadsheet — no approved sound file exists yet. Will wire up once approved.

Copy link
Contributor

Choose a reason for hiding this comment

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

@camclark, understood — thanks for the clarification! Since sam-hit is still waiting approval in the sound spreadsheet, skipping it for now makes total sense. The five unapproved assets (silo-built, sam-shoot, sam-hit, warship-lost, warship-shot) are intentionally excluded until the sounds are approved. I'll disregard this comment.

(´• ω •`) 🐇


🧠 Learnings used
Learnt from: wozniakpl
Repo: openfrontio/OpenFrontIO PR: 3317
File: src/client/InputHandler.ts:344-350
Timestamp: 2026-03-07T11:35:24.071Z
Learning: Ensure ghost preservation logic for special units (AtomBomb and HydrogenBomb) is implemented via shouldPreserveGhostAfterBuild(unitType) and that ghost removal is conditional on !shouldPreserveGhostAfterBuild(unitType) within createStructure. Confirm that both mouse (MouseUpEvent) and keyboard (ConfirmGhostStructureEvent) confirmation paths go through requestConfirmStructure → createStructure, and that Enter/NumpadEnter confirmations do not break the locked-nuke multi-placement since the ghost should be preserved by design. Add tests to cover shouldPreserveGhostAfterBuild returning true for AtomBomb/HydrogenBomb and verify that ghost removal only occurs for other units. Applies to files in the repository OpenFrontIO (src/client/InputHandler.ts and src/client/graphics/layers/StructureIconsLayer.ts).

Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2782
File: src/client/graphics/layers/StructureIconsLayer.ts:616-620
Timestamp: 2026-01-03T19:37:30.357Z
Learning: In `src/client/graphics/layers/StructureIconsLayer.ts`, the confirm button in `ensureGhostControls()` for locked ghost nukes (AtomBomb/HydrogenBomb) intentionally does NOT call `removeGhostStructure()` after emitting the build intent. This allows mobile players to rapidly place multiple nukes by "spamming" the confirm button without needing to reopen the radial menu.

Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 3246
File: src/core/execution/nation/NationNukeBehavior.ts:814-832
Timestamp: 2026-02-19T18:07:39.005Z
Learning: In src/core/execution/nation/NationNukeBehavior.ts, the clustering logic in maybeDestroyEnemySam() intentionally checks the arrival time of the first bomb from a silo rather than the last when evaluating if a silo fits within maxTotalArrivalSpread. This slight underestimate is by design: the window is already conservative (half the SAM cooldown = 37 of 75 ticks), staggerInterval is small (1-5 ticks), extra bombs provide margin, and the approach includes borderline silos that work fine in practice. The design should not be "fixed" to use the last bomb's arrival unless SAM timing changes significantly.

Learnt from: DevelopingTom
Repo: openfrontio/OpenFrontIO PR: 1900
File: src/core/execution/SAMLauncherExecution.ts:103-111
Timestamp: 2025-08-24T11:13:08.495Z
Learning: In SAMLauncherExecution.ts, the cached target bug can only occur if: 1) SAM is not on cooldown when nuke is in range, 2) SAM goes on cooldown right after computing trajectory, 3) SAM becomes available again before nuke explodes. This is not possible with current cooldown values but the fix is still valuable for robustness.

Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 3337
File: src/client/graphics/layers/UnitLayer.ts:293-300
Timestamp: 2026-03-03T15:02:02.735Z
Learning: In UnitLayer.ts, unit sprites are square by design (sprite.width === sprite.height). Rendering optimizations that assume non-square sprites or attempt to fix aspect ratio issues do not apply to unit sprites in this layer.

Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 0
File: :0-0
Timestamp: 2026-02-01T16:36:52.217Z
Learning: In this TypeScript game codebase, UI/layer updates should be wall-clock throttled (independent of simulation speed). Do not use tick-modulus gating like `if (game.ticks()%N===0)` inside `layer.tick()` methods. Instead, layers should expose a `getTickIntervalMs()` method and let the renderer schedule ticks based on wall-clock time.

Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:172-173
Timestamp: 2025-10-18T17:54:01.311Z
Learning: In src/core/execution/FakeHumanExecution.ts, MIRVs and ground attacks should not be mutually exclusive. The considerMIRV() method should not short-circuit maybeAttack() - both actions can occur in the same tick.

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.

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

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

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.

Learnt from: evanpelle
Repo: openfrontio/OpenFrontIO PR: 3114
File: src/client/graphics/layers/AttacksDisplay.ts:64-77
Timestamp: 2026-02-10T05:02:28.715Z
Learning: In the OpenFrontIO codebase, GameUpdateType maps always have an array value for each key (never undefined). When iterating, you can safely use: for (const event of updates[GameUpdateType.X]) without optional chaining or guards. Treat updates as a Record<GameUpdateType, any[]> and rely on its guaranteed array values. If you modify the type, ensure the guarantee remains (e.g., initialize missing keys to []) to preserve safe iteration.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/graphics/layers/EventsDisplay.ts`:
- Around line 558-563: The code currently calls
SoundManager.playSoundEffect(SoundEffect.AllianceBroken) based solely on
update.betrayedID/update.traitorID matching myPlayer.smallID(), which can play
audio even when no event is added; remove that standalone playback and instead
invoke SoundManager.playSoundEffect(SoundEffect.AllianceBroken) inside the two
branches where addEvent(...) is called (the branches that create the "betrayed"
and "traitor" events) so the sound only plays when addEvent actually runs; keep
references to update.betrayedID, update.traitorID, myPlayer.smallID(), addEvent,
SoundManager.playSoundEffect and SoundEffect.AllianceBroken to locate the
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d7cc377-63a7-456f-b5ad-ca43acee44bf

📥 Commits

Reviewing files that changed from the base of the PR and between 3d0c1c7 and 03685f4.

📒 Files selected for processing (1)
  • src/client/graphics/layers/EventsDisplay.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

Implement FX sound effects

1 participant