use crate::app::sim_events::SimEvent;
use crate::app::{PlacementDenied, Screen, Sim};
use bevy::audio::{SpatialListener, SpatialScale, Volume};
use bevy::input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
use bevy::prelude::*;
use std::time::Duration;
#[derive(Component)]
pub struct Music;
#[derive(Resource)]
pub struct Sounds {
place: Handle<AudioSource>,
remove: Handle<AudioSource>,
bank: Handle<AudioSource>,
eat: Handle<AudioSource>,
raid: Handle<AudioSource>,
win: Handle<AudioSource>,
lose: Handle<AudioSource>,
takeoff: Handle<AudioSource>,
screech: Handle<AudioSource>,
event: Handle<AudioSource>,
golden: Handle<AudioSource>,
tier: Handle<AudioSource>,
surge: Handle<AudioSource>,
horn: Handle<AudioSource>,
denied: Handle<AudioSource>,
}
#[derive(Resource)]
pub struct MusicPlaylist {
tracks: Vec<Handle<AudioSource>>,
next: usize,
}
const PAN_WIDTH: f32 = 0.5;
const EAR_GAP: f32 = 0.3;
const SPATIAL_GAIN: f32 = 1.4;
pub(crate) fn pan_pos(half_width: f32, pos: Vec2) -> Vec3 {
let x = (pos.x / half_width.max(1.0)).clamp(-1.0, 1.0);
Vec3::new(-x * PAN_WIDTH, 0.0, 0.0)
}
pub fn load_sounds(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(SpatialListener::new(EAR_GAP));
commands.insert_resource(MusicPlaylist {
tracks: vec![
assets.load("sounds/theme.wav"),
assets.load("sounds/theme_b.ogg"),
assets.load("sounds/theme_c.ogg"),
assets.load("sounds/theme_d.ogg"),
assets.load("sounds/theme_e.ogg"),
assets.load("sounds/theme_f.ogg"),
assets.load("sounds/theme_g.ogg"),
],
next: 0,
});
commands.insert_resource(Sounds {
place: assets.load("sounds/place.wav"),
remove: assets.load("sounds/remove.wav"),
bank: assets.load("sounds/bank.wav"),
eat: assets.load("sounds/eat.wav"),
raid: assets.load("sounds/raid.wav"),
win: assets.load("sounds/win.wav"),
lose: assets.load("sounds/lose.wav"),
takeoff: assets.load("sounds/takeoff.wav"),
screech: assets.load("sounds/screech.wav"),
event: assets.load("sounds/event.wav"),
golden: assets.load("sounds/golden.wav"),
tier: assets.load("sounds/tier.wav"),
surge: assets.load("sounds/surge.wav"),
horn: assets.load("sounds/horn.wav"),
denied: assets.load("sounds/denied.wav"),
});
}
fn play(commands: &mut Commands, sound: &Handle<AudioSource>, gain: f32) {
if gain <= 0.0 {
return;
}
commands.spawn((
AudioPlayer::new(sound.clone()),
PlaybackSettings {
volume: Volume::Linear(gain),
..PlaybackSettings::DESPAWN
},
));
}
fn play_at(commands: &mut Commands, sound: &Handle<AudioSource>, gain: f32, at: Vec3) {
if gain <= 0.0 {
return;
}
commands.spawn((
AudioPlayer::new(sound.clone()),
PlaybackSettings {
volume: Volume::Linear(gain * SPATIAL_GAIN),
spatial: true,
spatial_scale: Some(SpatialScale::new_2d(1.0)),
..PlaybackSettings::DESPAWN
},
Transform::from_translation(at),
));
}
#[allow(clippy::too_many_arguments)]
pub fn play_events(
mut commands: Commands,
mut events: MessageReader<SimEvent>,
sounds: Res<Sounds>,
screen: Res<State<Screen>>,
sim: Res<Sim>,
settings: Res<crate::app::settings::GameSettings>,
pads: Query<Entity, With<Gamepad>>,
mut rumble: MessageWriter<GamepadRumbleRequest>,
) {
let mut buzz = |ms: u64, strength: f32| {
if !settings.rumble {
return;
}
for gamepad in &pads {
rumble.write(GamepadRumbleRequest::Add {
duration: Duration::from_millis(ms),
intensity: GamepadRumbleIntensity::strong_motor(strength),
gamepad,
});
}
};
let gain = settings.sfx_gain();
let half_width = f32::from(sim.0.width()) * crate::app::layout::TILE / 2.0;
let pan = |pos: &Vec2| pan_pos(half_width, *pos);
for event in events.read() {
match event {
SimEvent::CrabBanked { kind, pos, .. } => {
play_at(&mut commands, &sounds.bank, gain, pan(pos));
if *kind == crate::sim::CrabKind::Golden {
play_at(&mut commands, &sounds.golden, gain, pan(pos));
}
}
SimEvent::CrabEaten { pos } => play_at(&mut commands, &sounds.eat, gain, pan(pos)),
SimEvent::CastleRaided { pos, .. } => {
play_at(&mut commands, &sounds.raid, gain, pan(pos));
buzz(400, 0.7);
}
SimEvent::GullArrived => play(&mut commands, &sounds.screech, gain),
SimEvent::GullTookOff => play(&mut commands, &sounds.takeoff, gain),
SimEvent::SignpostsChanged { delta } => {
play(
&mut commands,
if *delta > 0 {
&sounds.place
} else {
&sounds.remove
},
gain,
);
}
SimEvent::TierUp { .. } => play(&mut commands, &sounds.tier, gain),
SimEvent::TideEventFired { .. } => play(&mut commands, &sounds.event, gain),
SimEvent::SurgeStarted => {
play(&mut commands, &sounds.surge, gain);
buzz(300, 0.4);
}
SimEvent::RoundEnded => {
if *screen.get() == Screen::Versus {
play(&mut commands, &sounds.horn, gain);
buzz(500, 0.5);
}
}
SimEvent::CrabSpawned { .. } | SimEvent::GullLanded { .. } => {}
}
}
}
pub fn play_win(
mut commands: Commands,
sounds: Res<Sounds>,
screen: Res<State<Screen>>,
settings: Res<crate::app::settings::GameSettings>,
) {
if *screen.get() == Screen::Puzzle {
play(&mut commands, &sounds.win, settings.sfx_gain());
}
}
pub fn play_lose(
mut commands: Commands,
sounds: Res<Sounds>,
screen: Res<State<Screen>>,
settings: Res<crate::app::settings::GameSettings>,
) {
if *screen.get() == Screen::Puzzle {
play(&mut commands, &sounds.lose, settings.sfx_gain());
}
}
pub fn play_denied(
mut commands: Commands,
mut denials: MessageReader<PlacementDenied>,
sounds: Option<Res<Sounds>>,
settings: Res<crate::app::settings::GameSettings>,
) {
let any = denials.read().next().is_some();
denials.clear();
if any && let Some(sounds) = sounds {
play(&mut commands, &sounds.denied, settings.sfx_gain());
}
}
pub fn play_chime(commands: &mut Commands, sounds: &Sounds, gain: f32) {
play(commands, &sounds.tier, gain);
}
pub fn rotate_music(
mut commands: Commands,
mut playlist: ResMut<MusicPlaylist>,
settings: Res<crate::app::settings::GameSettings>,
playing: Query<(), With<Music>>,
) {
if !playing.is_empty() {
return;
}
let track = playlist.tracks[playlist.next].clone();
playlist.next = (playlist.next + 1) % playlist.tracks.len();
commands.spawn((
Music,
AudioPlayer::new(track),
PlaybackSettings {
volume: Volume::Linear(settings.music_gain()),
..PlaybackSettings::DESPAWN
},
));
}
pub fn surge_tempo(
sim: Res<crate::app::Sim>,
screen: Res<State<Screen>>,
sinks: Query<&AudioSink, With<Music>>,
) {
let ramp = if *screen.get() == Screen::Versus {
match sim.0.remaining_ticks() {
Some(ticks) if ticks <= 900 => 1.0 + 0.35 * (1.0 - ticks as f32 / 900.0),
_ => 1.0,
}
} else {
1.0
};
for sink in &sinks {
if (sink.speed() - ramp).abs() > 0.005 {
sink.set_speed(ramp);
}
}
}
pub fn toggle_music(keys: Res<ButtonInput<KeyCode>>, sinks: Query<&AudioSink, With<Music>>) {
if keys.just_pressed(KeyCode::KeyM) {
for sink in &sinks {
sink.toggle_playback();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panning_mirrors_the_board_and_stays_inside_the_listener() {
let half = 6.0 * crate::app::layout::TILE;
assert_eq!(pan_pos(half, Vec2::ZERO).x, 0.0, "the middle is centred");
let left = pan_pos(half, Vec2::new(-half, 0.0));
let right = pan_pos(half, Vec2::new(half, 0.0));
assert_eq!(left.x, PAN_WIDTH, "mirrored: left of the beach, left ear");
assert_eq!(right.x, -left.x, "the two edges are symmetric");
assert_eq!(pan_pos(half, Vec2::new(-9.0 * half, 0.0)).x, PAN_WIDTH);
const { assert!(PAN_WIDTH + EAR_GAP / 2.0 < 1.0) };
}
}