Skip to content

Sound & Effects Settings + Notifications#3951

Open
sardidefcon wants to merge 3 commits into
openfrontio:mainfrom
sardidefcon:feature-sound-and-effects
Open

Sound & Effects Settings + Notifications#3951
sardidefcon wants to merge 3 commits into
openfrontio:mainfrom
sardidefcon:feature-sound-and-effects

Conversation

@sardidefcon
Copy link
Copy Markdown
Contributor

SOUNDS-AND

Description:

It adds specific submenus for sounds and effects, allowing you to adjust the overall volume of background music and sounds, and to toggle each sound effect individually. The effects submenu works similarly, featuring two master toggles to disable all special effects and alerts, but also allowing you to toggle them individually.

Sound settings (new "Sounds" tab / submenu):

  • Volume sliders for background music and sound effects
  • Individual on/off toggles for all sound effects grouped by category:
    • UI & Notifications
    • Weapons & Combat
    • Diplomacy
    • Construction

In addition, 3 NEW SOUNDS have been added:

sounds1 sounds2 sounds3 sounds4 sounds5

Visual effects settings (new "Effects" tab / submenu):

  • Master toggle for Special Effects (gates all fx child toggles)
  • Master toggle for Alert Frame (gates betrayal/attack alert toggles)
  • Individual on/off toggles for all fx effects grouped by category:
    • Environment
    • Combat
    • Nuclear
    • Screen Alerts
  • Child toggles are visually disabled when their master toggle is off
effects1 effects2 effects3 effects4

Other improvements:

  • Per-effect persistence via UserSettings (localStorage)
  • SoundManager checks per-effect enabled state before playing
  • FxLayer, AlertFrame, DynamicUILayer, NukeFx all respect per-effect flags
  • Volume sliders use step="1" to avoid floating-point display artifacts (I put this bc I reused the slider that was already in use and since it didn't have the step it let me put weird values ​​lmao)
  • SettingToggle supports a disabled prop (opacity + pointer-events-none)
  • All new UI text goes through translateText() with en.json entries

NEW FEATURE: NOTIFICATIONS

A new feature has been added to send browser notifications to users. Currently, there is only one notification that alerts you when a game starts, and, due to standard browser notification logic, it is only sent if you are not in the window where the game is open. The game requests permission the first time a user joins a match. If permission is denied, the user can request it again from the browser's notification settings menu, where they can also disable this notification.

notifications1 notifications2 notifications3

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:

sardidefcon

Adds per-effect toggles for all in-game sound effects and visual effects,
organized by category, in both the main menu Settings and the in-game
Settings modal.

Sound settings (new "Sounds" tab / submenu):
- Volume sliders for background music and sound effects
- Individual on/off toggles for all sound effects grouped by category:
  UI & Notifications, Weapons & Combat, Diplomacy, Construction

Visual effects settings (new "Effects" tab / submenu):
- Master toggle for Special Effects (gates all fx child toggles)
- Master toggle for Alert Frame (gates betrayal/attack alert toggles)
- Individual on/off toggles for all fx effects grouped by category:
  Environment, Combat, Nuclear, Screen Alerts
- Child toggles are visually disabled when their master toggle is off

Other improvements:
- Per-effect persistence via UserSettings (localStorage)
- SoundManager checks per-effect enabled state before playing
- FxLayer, AlertFrame, DynamicUILayer, NukeFx all respect per-effect flags
- Volume sliders use step="1" to avoid floating-point display artifacts
- SettingToggle supports a disabled prop (opacity + pointer-events-none)
- All new UI text goes through translateText() with en.json entries
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Review Change Stack

Walkthrough

This PR adds per-effect sound and FX toggles, expands settings into Sounds/Effects/Notifications pages, integrates a game-start sound and optional browser notification into lobby join, gates in-game FX by per-effect flags, and adds localization and tests for the new settings.

Changes

Sound and FX Settings System

Layer / File(s) Summary
User settings API for per-effect control
src/core/game/UserSettings.ts
UserSettings class gains methods to read/write per-effect sound and FX enablement flags plus game-start notification toggle, persisting via the existing getBool/setBool infrastructure.
Sound effect types and URL mapping
src/client/sound/Sounds.ts
SoundEffect type expanded with "game-start", "upgrade", and "add-ammo" identifiers; soundEffectUrls map includes corresponding mp3 asset paths for each new sound.
SoundManager respects per-effect enablement
src/client/sound/SoundManager.ts
SoundManager constructor captures userSettings and playSoundEffect method checks per-effect enablement before loading or playing, early-returning when disabled.
Setting component disabled state and constraints
src/client/components/baseComponents/setting/SettingToggle.ts, src/client/components/baseComponents/setting/SettingSlider.ts
SettingToggle adds disabled property with conditional CSS styling and input binding. SettingSlider enforces integer step increments via explicit step="1".
FX layer per-effect enablement architecture
src/client/graphics/layers/FxLayer.ts
FxLayer introduces fxEffectEnabled helper and unitLevels map for tracking structure state. Individual FX types (shell impact, dust, conquest, warship sinking, building explosion, SAM, nuke) now gate on per-effect keys instead of a single global toggle. Emits upgrade/add-ammo sound events when unit/structure levels increase.
Alert frame and dynamic UI per-effect gating
src/client/graphics/layers/AlertFrame.ts, src/client/graphics/layers/DynamicUILayer.ts
AlertFrame.activateAlert checks global alert-frame and per-alert-type FX enablement before activating. DynamicUILayer gates nuke telegraph on fx-nuke-telegraph setting.
Nuke FX conditional explosion and debris rendering
src/client/graphics/fx/NukeFx.ts
nukeFxFactory accepts optional showExplosion and showDebris flags defaulting to true; conditionally creates explosion/shockwave and debris sprites based on flags.
Settings modal multi-page navigation and rendering
src/client/graphics/layers/SettingsModal.ts
Introduces activePage state (main, sounds, effects, notifications) and dedicated render methods for each subpage. Sounds page includes volume sliders and per-sound toggles. Effects page includes global toggles plus category-based FX toggles with disabled logic. Notifications page includes browser Notification support detection and permission-request flow.
UserSettingModal tab list and body dispatch
src/client/UserSettingModal.ts
Expands tab list to include sounds, effects, and notifications beyond basic and keybinds. renderBody dispatches to per-tab render methods. toggleAlertFrame and toggleFxLayer now call requestUpdate for immediate UI refresh.
Game-start notification and sound integration
src/client/ClientGameRunner.ts, src/client/Main.ts
ClientGameRunner creates SoundManager during joinLobby and passes it into createClientGame. On lobby start message, emits game-start sound and conditionally displays browser Notification based on permission and tab visibility. Main requests notification permission early in lobby join when API exists and permission is default.
Translation keys for settings UI and game-start notification
resources/lang/en.json
Adds tab labels (sounds, effects, notifications), many per-event sound/FX labels and browser permission strings. Adds fullscreen.game_start_notification title and body translation keys.
UserSettings unit tests
tests/UserSettings.test.ts
Adds Vitest tests mocking localStorage to verify default-enabled behavior, toggling, per-key independence, and persistence for sound, FX, and notification flags.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🎵 Sound effects answer to toggles, bright and clear,
Visual sparks obey the switches we steer,
A modal with pages — sounds, effects, notify —
Game-start chimes and banners that lift to the sky,
Small toggles, big flair, now each choice is near.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: addition of sound and effects settings plus a notification system, matching the substantial changes across UI, persistence, and runtime effects throughout the changeset.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining new sound/effects tabs, volume sliders, individual effect toggles, three new sounds, browser notifications, and technical implementation details that align with the code changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/ClientGameRunner.ts`:
- Around line 138-148: The Notification constructor can throw and currently sits
inside the start-message handling path in ClientGameRunner.ts; wrap the new
Notification(...) call in a try-catch so exceptions don't abort the handler and
block resolveJoin() and createClientGame(); catch the error, log or report it
(e.g., via your existing logger or console.error) including context like
translateText("game_start_notification.*") and
userSettings.gameStartNotificationsEnabled(), but do not rethrow so the rest of
the start flow continues.

In `@src/client/graphics/layers/FxLayer.ts`:
- Line 163: In FxLayer.ts inside the onTrainEvent handler, the impact FX
conditional is inverted: change the check from using !unit.reachedTarget() to
unit.reachedTarget() so the "fx-shell-impact" effect is emitted when the unit
has reached its target; keep the existing fxEffectEnabled("fx-shell-impact")
guard and ensure the same branch emits the impact FX only when
unit.reachedTarget() is true.

In `@src/client/Main.ts`:
- Around line 821-826: Wrap the Notification access and request in a safe
try/catch inside handleJoinLobby: check for "Notification" in window, then try
to read Notification.permission and call Notification.requestPermission(),
awaiting the returned Promise and attaching a .catch to swallow/log errors; on
any thrown error, log it (or ignore) and continue so the function does not exit
early. Specifically modify the block that references Notification.permission and
Notification.requestPermission() to catch synchronous exceptions and promise
rejections so lobby join proceeds even if notification APIs are broken.

In `@src/core/game/UserSettings.ts`:
- Around line 401-427: Add unit tests that cover the new UserSettings
accessors/mutators: isSoundEffectEnabled, setSoundEffectEnabled, isFxEnabled,
setFxEnabled, gameStartNotificationsEnabled, and toggleGameStartNotifications.
Create tests that (1) assert default values returned by
isSoundEffectEnabled/isFxEnabled/gameStartNotificationsEnabled, (2) call
setSoundEffectEnabled/setFxEnabled and verify the corresponding getBool
behaviour changes, and (3) call toggleGameStartNotifications and verify the
boolean flips; use the same test harness/helpers/mocked storage used by other
UserSettings tests so settings persistence is exercised and cleaned up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6c04395-311a-4b7b-8393-1ef54650471e

📥 Commits

Reviewing files that changed from the base of the PR and between 2e17fb5 and f257762.

⛔ Files ignored due to path filters (3)
  • resources/sounds/effects/add-ammo.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/start.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/upgrade.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (14)
  • resources/lang/en.json
  • src/client/ClientGameRunner.ts
  • src/client/Main.ts
  • src/client/UserSettingModal.ts
  • src/client/components/baseComponents/setting/SettingSlider.ts
  • src/client/components/baseComponents/setting/SettingToggle.ts
  • src/client/graphics/fx/NukeFx.ts
  • src/client/graphics/layers/AlertFrame.ts
  • src/client/graphics/layers/DynamicUILayer.ts
  • src/client/graphics/layers/FxLayer.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/sound/SoundManager.ts
  • src/client/sound/Sounds.ts
  • src/core/game/UserSettings.ts

Comment thread src/client/ClientGameRunner.ts
onTrainEvent(unit: UnitView) {
if (!unit.isActive()) {
if (!unit.reachedTarget() && this.fxEnabled()) {
if (!unit.reachedTarget() && this.fxEffectEnabled("fx-shell-impact")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Train impact FX condition looks inverted (Line 163).

onTrainEvent currently emits impact FX when !unit.reachedTarget(). For “impact” behavior, this should usually be unit.reachedTarget().

🔧 Proposed fix
-      if (!unit.reachedTarget() && this.fxEffectEnabled("fx-shell-impact")) {
+      if (unit.reachedTarget() && this.fxEffectEnabled("fx-shell-impact")) {
📝 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 (!unit.reachedTarget() && this.fxEffectEnabled("fx-shell-impact")) {
if (unit.reachedTarget() && this.fxEffectEnabled("fx-shell-impact")) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/graphics/layers/FxLayer.ts` at line 163, In FxLayer.ts inside the
onTrainEvent handler, the impact FX conditional is inverted: change the check
from using !unit.reachedTarget() to unit.reachedTarget() so the
"fx-shell-impact" effect is emitted when the unit has reached its target; keep
the existing fxEffectEnabled("fx-shell-impact") guard and ensure the same branch
emits the impact FX only when unit.reachedTarget() is true.

Comment thread src/client/Main.ts Outdated
Comment thread src/core/game/UserSettings.ts
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 17, 2026
- Wrap game-start Notification creation in try-catch so a throwing
  constructor (quota, enterprise policy, browser bugs) cannot block
  resolveJoin()/createClientGame() and the game start flow
- Guard notification permission request in handleJoinLobby with
  try-catch and a promise .catch() so a broken Notification API
  cannot abort lobby join
- Add unit tests covering the new UserSettings accessors/mutators:
  isSoundEffectEnabled/setSoundEffectEnabled, isFxEnabled/setFxEnabled,
  gameStartNotificationsEnabled/toggleGameStartNotifications
- Apply Prettier formatting
@ryanbarlow97
Copy link
Copy Markdown
Contributor

can you add mobile screenshots too?

@sardidefcon
Copy link
Copy Markdown
Contributor Author

can you add mobile screenshots too?

Of course, I forgot.
I'm adding them to this answer so as not to make the description too long:

ingame1

SOUNDS

ingame2

EFFECTS

ingame3

NOTIFICATIONS

ingame4 ingame5

@sardidefcon
Copy link
Copy Markdown
Contributor Author

You think I'm an idiot, and you're right @ryanbarlow97

I don't know why, but my brain interpreted it as you asking for screenshots of the in-game menu, not the mobile menu hahahahha

I attach them:

mobile1 mobile2 mobile3

@sardidefcon
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/lang/en.json`:
- Around line 1352-1359: The JSON key "game_start_notification" is placed at the
top level but should be nested under the "fullscreen" object; move the entire
"game_start_notification" object so its keys become
fullscreen.game_start_notification.title and
fullscreen.game_start_notification.body to match the expected key path (update
the "fullscreen" object in resources/lang/en.json to include
"game_start_notification" rather than leaving it top-level).

In `@src/client/ClientGameRunner.ts`:
- Around line 103-104: SoundManager is instantiated at SoundManager(eventBus,
userSettings) but if the user leaves the lobby before a game starts the existing
stop() behavior (which checks currentGameRunner === null) doesn't dispose the
SoundManager, leaving audio/event resources alive; to fix, update the
early-lobby-exit path (the handler that runs when a user leaves before start) to
explicitly dispose the pre-start SoundManager instance—either call a disposal
method (e.g., soundManager.dispose() or soundManager.destroy()) or modify
SoundManager.stop() to perform full cleanup when currentGameRunner === null;
reference the SoundManager instance created at the top of ClientGameRunner and
ensure the same variable is cleaned up on the early-exit code path so eventBus
subscriptions and audio resources are released.

In `@src/client/graphics/layers/SettingsModal.ts`:
- Around line 244-267: Replace hardcoded alt attributes on the <img> elements in
SettingsModal.ts (e.g., alt="musicIcon", "soundEffectsIcon", "specialEffects",
"alertFrame", "notificationIcon", "permissionIcon") with calls to
translateText(...) using new localization keys (e.g.,
translateText("icon.music"), translateText("icon.soundEffects"), etc. Update the
corresponding entries in resources/lang/en.json with these keys and English
values, and ensure every img alt in the other mentioned ranges (around lines
434-452 and 553-574) is updated the same way so all user-visible alt text goes
through translateText().

In `@tests/UserSettings.test.ts`:
- Around line 24-114: The test file reimplements a test harness instead of using
the shared setup helper; update the suite to call the shared setup() helper to
create the test environment and replace manual stubbing/initialization.
Specifically, remove the manual vi.stubGlobal("localStorage",
createMockLocalStorage()) and the manual cache clear on UserSettings, and
instead call setup() at the top of the describe block to obtain the testing
context, then instantiate UserSettings via that context or normally if setup
provides globals; ensure all tests still reference the same UserSettings methods
(isSoundEffectEnabled, setSoundEffectEnabled, isFxEnabled, setFxEnabled,
gameStartNotificationsEnabled, toggleGameStartNotifications) and that
localStorage persistence assertions remain valid under the shared setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f949dad3-e389-40cc-83c9-5e5e07bcc12b

📥 Commits

Reviewing files that changed from the base of the PR and between 2e17fb5 and 47813ab.

⛔ Files ignored due to path filters (3)
  • resources/sounds/effects/add-ammo.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/start.mp3 is excluded by !**/*.mp3
  • resources/sounds/effects/upgrade.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (15)
  • resources/lang/en.json
  • src/client/ClientGameRunner.ts
  • src/client/Main.ts
  • src/client/UserSettingModal.ts
  • src/client/components/baseComponents/setting/SettingSlider.ts
  • src/client/components/baseComponents/setting/SettingToggle.ts
  • src/client/graphics/fx/NukeFx.ts
  • src/client/graphics/layers/AlertFrame.ts
  • src/client/graphics/layers/DynamicUILayer.ts
  • src/client/graphics/layers/FxLayer.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/client/sound/SoundManager.ts
  • src/client/sound/Sounds.ts
  • src/core/game/UserSettings.ts
  • tests/UserSettings.test.ts

Comment thread resources/lang/en.json
Comment on lines 1352 to 1359
"fullscreen": {
"enter": "Enter fullscreen",
"exit": "Exit fullscreen"
},
"game_start_notification": {
"title": "The game is starting",
"body": "Choose your starting position"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move game_start_notification under fullscreen to match expected key path.

This block is currently top-level, but the stack contract for this PR expects fullscreen.game_start_notification.title/body. In the current shape, lookups on that path will fail.

Proposed fix
   "fullscreen": {
-    "enter": "Enter fullscreen",
-    "exit": "Exit fullscreen"
-  },
-  "game_start_notification": {
-    "title": "The game is starting",
-    "body": "Choose your starting position"
+    "enter": "Enter fullscreen",
+    "exit": "Exit fullscreen",
+    "game_start_notification": {
+      "title": "The game is starting",
+      "body": "Choose your starting position"
+    }
   }
📝 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
"fullscreen": {
"enter": "Enter fullscreen",
"exit": "Exit fullscreen"
},
"game_start_notification": {
"title": "The game is starting",
"body": "Choose your starting position"
}
"fullscreen": {
"enter": "Enter fullscreen",
"exit": "Exit fullscreen",
"game_start_notification": {
"title": "The game is starting",
"body": "Choose your starting position"
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/lang/en.json` around lines 1352 - 1359, The JSON key
"game_start_notification" is placed at the top level but should be nested under
the "fullscreen" object; move the entire "game_start_notification" object so its
keys become fullscreen.game_start_notification.title and
fullscreen.game_start_notification.body to match the expected key path (update
the "fullscreen" object in resources/lang/en.json to include
"game_start_notification" rather than leaving it top-level).

Comment on lines +103 to 104
const soundManager = new SoundManager(eventBus, userSettings);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dispose pre-start SoundManager on early lobby exit.

SoundManager is created at Line 103, but if the user leaves before game start, stop() takes the currentGameRunner === null path and never disposes it. That leaves audio/event resources alive.

Suggested fix
   return {
     stop: (force: boolean = false) => {
       if (!force && currentGameRunner?.shouldPreventWindowClose()) {
         console.log("Player is active, prevent leaving game");
         return false;
       }
       console.log("leaving game");
       if (currentGameRunner) {
         currentGameRunner.stop();
         currentGameRunner = null;
       } else {
+        soundManager.dispose();
         transport.leaveGame();
       }
       return true;
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/ClientGameRunner.ts` around lines 103 - 104, SoundManager is
instantiated at SoundManager(eventBus, userSettings) but if the user leaves the
lobby before a game starts the existing stop() behavior (which checks
currentGameRunner === null) doesn't dispose the SoundManager, leaving
audio/event resources alive; to fix, update the early-lobby-exit path (the
handler that runs when a user leaves before start) to explicitly dispose the
pre-start SoundManager instance—either call a disposal method (e.g.,
soundManager.dispose() or soundManager.destroy()) or modify SoundManager.stop()
to perform full cleanup when currentGameRunner === null; reference the
SoundManager instance created at the top of ClientGameRunner and ensure the same
variable is cleaned up on the early-exit code path so eventBus subscriptions and
audio resources are released.

Comment thread src/client/graphics/layers/SettingsModal.ts
Comment on lines +24 to +114
describe("UserSettings sound/fx/notification accessors", () => {
let settings: UserSettings;

beforeEach(() => {
vi.stubGlobal("localStorage", createMockLocalStorage());
// The cache is static and shared across instances, so it must be
// cleared between tests to avoid stale reads.
(UserSettings as unknown as { cache: Map<string, unknown> }).cache.clear();
settings = new UserSettings();
});

describe("isSoundEffectEnabled / setSoundEffectEnabled", () => {
it("defaults to enabled when unset", () => {
expect(settings.isSoundEffectEnabled("atom-launch")).toBe(true);
});

it("returns false after being disabled", () => {
settings.setSoundEffectEnabled("atom-launch", false);
expect(settings.isSoundEffectEnabled("atom-launch")).toBe(false);
});

it("returns true after being re-enabled", () => {
settings.setSoundEffectEnabled("atom-launch", false);
settings.setSoundEffectEnabled("atom-launch", true);
expect(settings.isSoundEffectEnabled("atom-launch")).toBe(true);
});

it("persists the value to localStorage", () => {
settings.setSoundEffectEnabled("ka-ching", false);
expect(localStorage.getItem("settings.sound.ka-ching")).toBe("false");
});

it("tracks each effect key independently", () => {
settings.setSoundEffectEnabled("atom-launch", false);
expect(settings.isSoundEffectEnabled("atom-launch")).toBe(false);
expect(settings.isSoundEffectEnabled("mirv-launch")).toBe(true);
});
});

describe("isFxEnabled / setFxEnabled", () => {
it("defaults to enabled when unset", () => {
expect(settings.isFxEnabled("fx-conquest")).toBe(true);
});

it("returns false after being disabled", () => {
settings.setFxEnabled("fx-conquest", false);
expect(settings.isFxEnabled("fx-conquest")).toBe(false);
});

it("returns true after being re-enabled", () => {
settings.setFxEnabled("fx-conquest", false);
settings.setFxEnabled("fx-conquest", true);
expect(settings.isFxEnabled("fx-conquest")).toBe(true);
});

it("persists the value to localStorage", () => {
settings.setFxEnabled("fx-nuke-debris", false);
expect(localStorage.getItem("settings.fx.fx-nuke-debris")).toBe("false");
});

it("tracks each effect key independently", () => {
settings.setFxEnabled("fx-conquest", false);
expect(settings.isFxEnabled("fx-conquest")).toBe(false);
expect(settings.isFxEnabled("fx-dust")).toBe(true);
});
});

describe("gameStartNotificationsEnabled / toggleGameStartNotifications", () => {
it("defaults to enabled when unset", () => {
expect(settings.gameStartNotificationsEnabled()).toBe(true);
});

it("flips to false on first toggle", () => {
settings.toggleGameStartNotifications();
expect(settings.gameStartNotificationsEnabled()).toBe(false);
});

it("flips back to true on second toggle", () => {
settings.toggleGameStartNotifications();
settings.toggleGameStartNotifications();
expect(settings.gameStartNotificationsEnabled()).toBe(true);
});

it("persists the value to localStorage", () => {
settings.toggleGameStartNotifications();
expect(localStorage.getItem("settings.notifications.gameStart")).toBe(
"false",
);
});
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the shared setup() test helper in this test file.

This suite builds its own harness, but tests under tests/**/*.test.ts should use the shared setup path for consistency with repo testing conventions.

As per coding guidelines: "Use the setup() helper from tests/util/Setup.ts to create test game instances and exercise core simulation directly".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/UserSettings.test.ts` around lines 24 - 114, The test file reimplements
a test harness instead of using the shared setup helper; update the suite to
call the shared setup() helper to create the test environment and replace manual
stubbing/initialization. Specifically, remove the manual
vi.stubGlobal("localStorage", createMockLocalStorage()) and the manual cache
clear on UserSettings, and instead call setup() at the top of the describe block
to obtain the testing context, then instantiate UserSettings via that context or
normally if setup provides globals; ensure all tests still reference the same
UserSettings methods (isSoundEffectEnabled, setSoundEffectEnabled, isFxEnabled,
setFxEnabled, gameStartNotificationsEnabled, toggleGameStartNotifications) and
that localStorage persistence assertions remain valid under the shared setup.

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.

2 participants