feat(data,engine): persistent Settings + SFX volume hotkeys

- solitaire_data::Settings { sfx_volume, first_run_complete } with
  atomic JSON persistence and clamping sanitizer.
- SettingsPlugin (engine): [ / ] adjust SFX volume by 0.1, clamped;
  persists on change; emits SettingsChangedEvent. No-op at rails.
- AudioPlugin applies sfx_volume to kira's main track at startup
  and on every change so live tweaks take effect without restart.
- Brief "SFX: N%" toast on each change. Help cheat sheet updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-25 23:08:20 -07:00
parent b720588687
commit 9d0f9478b2
9 changed files with 389 additions and 7 deletions
+34
View File
@@ -22,10 +22,12 @@ use bevy::prelude::*;
use kira::manager::backend::DefaultBackend;
use kira::manager::{AudioManager, AudioManagerSettings};
use kira::sound::static_sound::StaticSoundData;
use kira::tween::Tween;
use crate::events::{
DrawRequestEvent, GameWonEvent, MoveRejectedEvent, MoveRequestEvent, NewGameRequestEvent,
};
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
/// Pre-decoded sound effects. Cheap to clone (frames are an `Arc<[Frame]>`),
/// so we hand a fresh handle to `manager.play()` on every event.
@@ -66,6 +68,11 @@ impl Plugin for AudioPlugin {
.add_event::<MoveRejectedEvent>()
.add_event::<NewGameRequestEvent>()
.add_event::<GameWonEvent>()
.add_event::<SettingsChangedEvent>()
.add_systems(
Startup,
apply_initial_volume,
)
.add_systems(
Update,
(
@@ -74,6 +81,7 @@ impl Plugin for AudioPlugin {
play_on_rejected,
play_on_new_game,
play_on_win,
apply_volume_on_change,
),
);
}
@@ -113,6 +121,32 @@ fn play(audio: &mut AudioState, sound: &StaticSoundData) {
}
}
fn set_main_track_volume(audio: &mut AudioState, volume: f32) {
let Some(manager) = audio.manager.as_mut() else {
return;
};
manager
.main_track()
.set_volume(volume.clamp(0.0, 1.0) as f64, Tween::default());
}
fn apply_initial_volume(
mut audio: NonSendMut<AudioState>,
settings: Option<Res<SettingsResource>>,
) {
let volume = settings.map_or(1.0, |s| s.0.sfx_volume);
set_main_track_volume(&mut audio, volume);
}
fn apply_volume_on_change(
mut events: EventReader<SettingsChangedEvent>,
mut audio: NonSendMut<AudioState>,
) {
for ev in events.read() {
set_main_track_volume(&mut audio, ev.0.sfx_volume);
}
}
fn play_on_draw(
mut events: EventReader<DrawRequestEvent>,
mut audio: NonSendMut<AudioState>,