Skip to content

Conversation

icslucas
Copy link
Contributor

@icslucas icslucas commented Oct 4, 2025

Description:

add sound effects for bombs
Describe the PR.
sound effects for BOMBS

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:

Lucas

@icslucas icslucas marked this pull request as ready for review October 4, 2025 17:40
@icslucas icslucas requested a review from a team as a code owner October 4, 2025 17:40
@icslucas icslucas added this to the v26 milestone Oct 4, 2025
Copy link
Contributor

coderabbitai bot commented Oct 4, 2025

Walkthrough

Adds a new instance-based sound system. Introduces SoundLayer for nuke launch/hit sounds, expands SoundManager with more effects and volumes, and removes default singleton export. Wires SoundManager and SoundLayer into ClientGameRunner, GameRenderer, and SettingsModal. Adjusts FxLayer to avoid duplicate nuke launches and removes a conquest sound. Fixes a LocalServer hash guard.

Changes

Cohort / File(s) Summary
Sound system core
src/client/sound/SoundManager.ts
Exported SoundManager class; removed default instance export; added sound effect enums (Atom/Hydro/MIRV launch/hit); initialized new effects and background tracks; added volume fields and setters.
Renderer and runner wiring
src/client/ClientGameRunner.ts, src/client/graphics/GameRenderer.ts
Injected SoundManager and SoundLayer into game creation; extended createRenderer and GameRenderer constructor; passed soundManager to SettingsModal; started/stopped music via instance methods.
Settings modal integration
src/client/graphics/layers/SettingsModal.ts
Added public soundManager field; switched from static/global to instance-based volume control; updated imports to named export.
New sound layer
src/client/sound/SoundLayer.ts
Added Layer to play nuke launch/hit sounds based on unit events; tracks seen nukes to avoid duplicates; no visual rendering.
FX layer adjustments
src/client/graphics/layers/FxLayer.ts
Added seenNukes set; played nuke launch via onNukeEvent for Atom/Hydrogen; explicit MIRV case; removed KaChing on conquest and related imports.
Singleplayer hash guard
src/client/LocalServer.ts
Safely sets turn hash only if this.turns[turnNumber] exists to avoid undefined index errors.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant ClientGameRunner
  participant SoundManager
  participant SoundLayer
  participant GameRenderer
  participant SettingsModal

  User->>ClientGameRunner: start()
  ClientGameRunner->>SoundManager: new SoundManager()
  ClientGameRunner->>SoundLayer: new SoundLayer(game, soundManager)
  ClientGameRunner->>GameRenderer: createRenderer(canvas, game, bus, soundManager, soundLayer)
  GameRenderer->>SettingsModal: settingsModal.soundManager = soundManager
  ClientGameRunner->>SoundManager: playBackgroundMusic()
Loading
sequenceDiagram
  autonumber
  participant TickLoop as Game Tick
  participant SoundLayer
  participant GameView
  participant SoundManager

  TickLoop->>SoundLayer: tick()
  SoundLayer->>GameView: get units updated since last tick
  loop For each updated unit
    SoundLayer->>SoundLayer: onUnitEvent(unit)
    alt Nuke launch (owned/targeting player) and not seen
      SoundLayer->>SoundManager: play(AtomLaunch|HydroLaunch|MirvLaunch)
      SoundLayer->>SoundLayer: mark unit as seen
    end
    alt Nuke reached target (relevant)
      SoundLayer->>SoundManager: play(AtomHit|HydroHit|MirvHit)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Feature - New, Feature - Frontend, UI/UX

Suggested reviewers

  • scottanderson

Poem

New songs in the battle’s air,
Nukes whoosh, then thunder’s flare—boom!
A runner hums, a renderer cares,
Sliders tame the sonic room.
Bugs hush with a gentle guard—
Play on, code band, loud and hard. 🎶💥

Pre-merge checks

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The title “sound effects” is too broad and does not specify that this pull request adds bomb and nuclear launch/hit sounds. It lacks context on the type of sound effects implemented and could apply to many features. A more specific title will help teammates understand the main change at a glance. Consider renaming the title to explicitly mention bombs or nuclear weapons to clearly reflect the primary changes; for example, “Add sound effects for nuclear bombs.”
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed The description states that it adds sound effects for bombs and includes checklist items for UI updates, translation, and tests, which directly relates to the implemented changes for bomb sound handling. It clearly describes the purpose of the pull request without being off-topic. This satisfies the lenient requirement that the description be related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d067a1 and 85e271b.

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

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

🧹 Nitpick comments (1)
src/client/sound/SoundManager.ts (1)

55-55: Reusing atom hit sound for MIRV.

Line 55 loads atomHitSound for SoundEffect.MirvHit instead of a dedicated MIRV hit sound. If this is intentional (to save on asset size or because they should sound similar), consider adding a comment explaining the reuse.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa9259b and 0fc8645.

⛔ Files ignored due to path filters (6)
  • proprietary/sounds/effects/Atom_Hit.mp3 is excluded by !**/*.mp3
  • proprietary/sounds/effects/atom_launch.mp3 is excluded by !**/*.mp3
  • proprietary/sounds/effects/click.mp3 is excluded by !**/*.mp3
  • proprietary/sounds/effects/hydrogen_hit.mp3 is excluded by !**/*.mp3
  • proprietary/sounds/effects/hydrogen_launch.mp3 is excluded by !**/*.mp3
  • proprietary/sounds/effects/mirv_launch.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (3)
  • src/client/LocalServer.ts (1 hunks)
  • src/client/graphics/layers/FxLayer.ts (3 hunks)
  • src/client/sound/SoundManager.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from UnitType values (specifically bomb-related values like AtomBomb, HydrogenBomb, MIRV, and MIRVWarhead) rather than a separate enum. This means comparing NukeType values against UnitType values in switch statements is valid and intentional.

Applied to files:

  • src/client/graphics/layers/FxLayer.ts
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from specific UnitType enum values (AtomBomb, HydrogenBomb, MIRV, MIRVWarhead). This means that comparisons in switch statements between a NukeType parameter and UnitType enum values are valid and will work correctly at runtime.

Applied to files:

  • src/client/graphics/layers/FxLayer.ts
🪛 GitHub Actions: 🧪 CI
src/client/sound/SoundManager.ts

[error] 2-76: Can't resolve '../../../proprietary/sounds/effects/atom_hit.mp3' in '/home/runner/work/OpenFrontIO/OpenFrontIO/src/client/sound'

🪛 GitHub Actions: 🚀 Deploy
src/client/sound/SoundManager.ts

[error] 2-76: Module not found: Can't resolve '../../../proprietary/sounds/effects/atom_hit.mp3' in '/usr/src/app/src/client/sound' (webpack: Can't resolve the MP3 file; the asset may be missing or path wrong).

🔇 Additional comments (4)
src/client/LocalServer.ts (1)

112-114: Good defensive guard.

The check prevents assigning a hash to an undefined turn, which could happen if the hash message arrives before the turn is created in the array.

src/client/graphics/layers/FxLayer.ts (3)

145-165: Launch sound tracking looks correct.

The per-unit de-duplication via seenNukes ensures each nuclear unit plays its launch sound only once. The distinction between MIRV (launcher) and MIRVWarhead (actual warhead) is intentional - MIRV only plays launch sound while MIRVWarhead triggers the full nuke event.


283-289: Hit sound logic is correct.

Playing specific hit sounds based on the nuclear unit type (AtomHit, HydroHit, MirvHit) provides appropriate audio feedback for each explosion type.


22-22: Verify clearing of seenNukes. The Set on line 22 is never cleared and may grow unbounded if unit IDs stay unique across sessions. Confirm whether IDs reset or if FxLayer is reinitialized on game restart; otherwise clear seenNukes when the game state resets.

Copy link
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

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

Few comments:

  1. I think we should only play nuke effect if you are launching the nuke or the nuke is targeting your territory.

  2. core/ is just a simulation, it should have no dependencies on graphics. So I think it's best to create a SoundLayer that listens for updates. (similar to how EventsDisplay works). And we should probably not have SoundManager be a singleton.

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

♻️ Duplicate comments (1)
src/client/sound/SoundManager.ts (1)

2-6: Critical: Import path case mismatch still blocking build.

The past review comment correctly identified that the import path on line 2 uses atom_hit.mp3 but the actual file is named Atom_Hit.mp3. This case mismatch prevents the module from resolving and causes the build to fail. Please update the import to match the exact filename casing.

Apply this diff to fix the import path:

-import atomHitSound from "../../../proprietary/sounds/effects/atom_hit.mp3";
+import atomHitSound from "../../../proprietary/sounds/effects/Atom_Hit.mp3";
🧹 Nitpick comments (1)
src/client/sound/SoundLayer.ts (1)

31-64: Consider mapping unit types to sound effects with an object.

The launch sound logic is correct but uses multiple if-else statements to map unit types to sound effects. A map-based approach would be cleaner and easier to extend.

Apply this diff to use a typed map:

  onUnitEvent(unit: UnitView) {
    const myPlayer = this.game.myPlayer();
    if (!myPlayer) return;

    const isNuke =
      unit.type() === UnitType.AtomBomb ||
      unit.type() === UnitType.HydrogenBomb ||
      unit.type() === UnitType.MIRV;

    if (isNuke) {
      if (!this.seenNukes.has(unit.id())) {
        const owner = unit.owner();
        const targetTile = unit.targetTile();
        const targetPlayer = targetTile
          ? this.game.playerBySmallID(this.game.ownerID(targetTile))
          : undefined;

        if (owner === myPlayer || targetPlayer === myPlayer) {
-         let soundEffect: SoundEffect | undefined;
-         if (unit.type() === UnitType.AtomBomb) {
-           soundEffect = SoundEffect.AtomLaunch;
-         } else if (unit.type() === UnitType.HydrogenBomb) {
-           soundEffect = SoundEffect.HydroLaunch;
-         } else if (unit.type() === UnitType.MIRV) {
-           soundEffect = SoundEffect.MirvLaunch;
-         }
-
-         if (soundEffect) {
-           this.soundManager.playSoundEffect(soundEffect);
-         }
+         const launchSounds: Record<UnitType, SoundEffect | undefined> = {
+           [UnitType.AtomBomb]: SoundEffect.AtomLaunch,
+           [UnitType.HydrogenBomb]: SoundEffect.HydroLaunch,
+           [UnitType.MIRV]: SoundEffect.MirvLaunch,
+         };
+         const soundEffect = launchSounds[unit.type()];
+         if (soundEffect) {
+           this.soundManager.playSoundEffect(soundEffect);
+         }
        }
        this.seenNukes.add(unit.id());
      }
    }

Apply the same pattern to the hit sound logic at lines 80-91.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fc8645 and 3d067a1.

⛔ Files ignored due to path filters (1)
  • proprietary/sounds/effects/atom_hit.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (7)
  • .husky/pre-commit (1 hunks)
  • src/client/ClientGameRunner.ts (6 hunks)
  • src/client/graphics/GameRenderer.ts (6 hunks)
  • src/client/graphics/layers/FxLayer.ts (3 hunks)
  • src/client/graphics/layers/SettingsModal.ts (4 hunks)
  • src/client/sound/SoundLayer.ts (1 hunks)
  • src/client/sound/SoundManager.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/FxLayer.ts
🧰 Additional context used
🧬 Code graph analysis (4)
src/client/ClientGameRunner.ts (3)
src/client/sound/SoundManager.ts (1)
  • SoundManager (22-124)
src/client/sound/SoundLayer.ts (1)
  • SoundLayer (7-99)
src/client/graphics/GameRenderer.ts (1)
  • createRenderer (45-294)
src/client/graphics/GameRenderer.ts (2)
src/client/sound/SoundManager.ts (1)
  • SoundManager (22-124)
src/client/sound/SoundLayer.ts (1)
  • SoundLayer (7-99)
src/client/sound/SoundLayer.ts (3)
src/client/graphics/layers/Layer.ts (1)
  • Layer (1-7)
src/core/game/GameView.ts (4)
  • GameView (423-754)
  • unit (634-636)
  • UnitView (44-175)
  • myPlayer (568-570)
src/client/sound/SoundManager.ts (1)
  • SoundManager (22-124)
src/client/graphics/layers/SettingsModal.ts (1)
src/client/sound/SoundManager.ts (1)
  • SoundManager (22-124)
⏰ 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 (7)
src/client/sound/SoundManager.ts (1)

22-57: Good refactoring from singleton to exported class.

The change from a default export of an instantiated object to an exported class improves testability and allows multiple instances if needed. The dependency injection pattern used throughout the codebase now has proper control over sound manager lifecycle.

src/client/ClientGameRunner.ts (2)

179-202: LGTM: Clean dependency injection.

The sound management integration follows proper dependency injection. Creating soundManager and soundLayer instances at the client game creation level and passing them through to the renderer ensures clean separation of concerns and testability.


259-259: LGTM: Instance-based sound control.

Background music is now controlled through the injected soundManager instance rather than static calls. This provides better lifecycle management and aligns with the dependency injection pattern used throughout the codebase.

Also applies to: 388-388

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

18-18: LGTM: Instance-based sound management.

The migration from static SoundManager usage to instance-based control is clean. The soundManager field is properly injected (wired in GameRenderer.createRenderer()) and used consistently throughout volume control methods.

Also applies to: 33-33, 51-56, 165-173

src/client/graphics/GameRenderer.ts (1)

45-51: LGTM: Sound subsystem properly wired through renderer.

The sound integration is clean:

  • soundManager and soundLayer are passed as parameters to createRenderer()
  • settingsModal.soundManager is assigned for volume control
  • soundLayer is added to the layers array for sound effects
  • GameRenderer stores the soundManager reference

This architecture allows sound management to participate in the rendering lifecycle while maintaining separation of concerns.

Also applies to: 167-167, 281-281, 292-293, 307-307

src/client/sound/SoundLayer.ts (2)

7-29: LGTM: Clean Layer implementation.

The SoundLayer properly implements the Layer interface. Using shouldTransform(): false is correct since sound logic doesn't need canvas transformations. The tick() method efficiently processes only units updated since the last tick.


66-94: Hit sound logic looks correct.

The hit sound logic properly checks that the unit is inactive and reached its target before playing the sound. Since inactive units are deleted after the update (as shown in GameView.update()), duplicate hit sounds shouldn't occur.

The comparison owner === myPlayer || targetPlayer === myPlayer correctly handles the case where targetPlayer might be TerraNullius (the comparison would be false, which is the desired behavior).

@icslucas
Copy link
Contributor Author

icslucas commented Oct 5, 2025

Few comments:

  1. I think we should only play nuke effect if you are launching the nuke or the nuke is targeting your territory.
  2. core/ is just a simulation, it should have no dependencies on graphics. So I think it's best to create a SoundLayer that listens for updates. (similar to how EventsDisplay works). And we should probably not have SoundManager be a singleton.

FIXED!

Comment on lines +112 to +114
if (this.turns[clientMsg.turnNumber]) {
this.turns[clientMsg.turnNumber].hash = clientMsg.hash;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this change necessary?

import { UnitExplosionFx } from "../fx/UnitExplosionFx";
import { Layer } from "./Layer";
export class FxLayer implements Layer {
private seenNukes: Set<number> = new Set();
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this used?

Comment on lines +140 to +143
this.onNukeEvent(unit, 70);
break;
case UnitType.MIRV:
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this related to sounds?

Comment on lines 12 to 20
export enum SoundEffect {
KaChing = "ka-ching",
AtomLaunch = "atom_launch",
AtomHit = "atom_hit",
HydroLaunch = "hydro_launch",
HydroHit = "hydro_hit",
MirvHit = "mirv_hit",
MirvLaunch = "mirv_launch",
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should try to avoid enums, instead do:

export const SoundEffect = {
KaChing: "ka-ching",
AtomLaunch: "atom_launch",
AtomHit: "atom_hit",
HydroLaunch: "hydro_launch",
HydroHit: "hydro_hit",
MirvHit: "mirv_hit",
MirvLaunch: "mirv_launch",
} as const;

export type SoundEffect = typeof SoundEffect[keyof typeof SoundEffect];

unit.type() === UnitType.AtomBomb ||
unit.type() === UnitType.HydrogenBomb ||
unit.type() === UnitType.MIRV;

Copy link
Collaborator

Choose a reason for hiding this comment

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

return early do:

if(!isNuke) {
return
}

Comment on lines +48 to +60
if (owner === myPlayer || targetPlayer === myPlayer) {
let soundEffect: SoundEffect | undefined;
if (unit.type() === UnitType.AtomBomb) {
soundEffect = SoundEffect.AtomLaunch;
} else if (unit.type() === UnitType.HydrogenBomb) {
soundEffect = SoundEffect.HydroLaunch;
} else if (unit.type() === UnitType.MIRV) {
soundEffect = SoundEffect.MirvLaunch;
}

if (soundEffect) {
this.soundManager.playSoundEffect(soundEffect);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

this can be simplified:

switch(unit.type()) {
case UnitType.AtomBomb:
this.soundManager.playerSoundEffect(SoundEffect.AtomLaunch)

Comment on lines +67 to +70
const isNukeHit =
unit.type() === UnitType.AtomBomb ||
unit.type() === UnitType.HydrogenBomb ||
unit.type() === UnitType.MIRVWarhead;
Copy link
Collaborator

Choose a reason for hiding this comment

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

we already have the isNuke variable, reuse that.

const owner = unit.owner();
const targetTile = unit.lastTile();
const targetPlayer = this.game.playerBySmallID(
this.game.ownerID(targetTile),
Copy link
Collaborator

Choose a reason for hiding this comment

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

this.game.owner(targetTile)

const owner = unit.owner();
const targetTile = unit.targetTile();
const targetPlayer = targetTile
? this.game.playerBySmallID(this.game.ownerID(targetTile))
Copy link
Collaborator

Choose a reason for hiding this comment

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

use this.game.owner(targetTile)

Comment on lines +112 to +114
if (this.turns[clientMsg.turnNumber]) {
this.turns[clientMsg.turnNumber].hash = clientMsg.hash;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this related?

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.

2 participants