Merge branch 'aweinstock/combat-music' into 'master'

Add combat music transitions based on number of enemies in player radius.

See merge request veloren/veloren!2077
This commit is contained in:
Samuel Keiffer 2021-04-12 06:01:19 +00:00
commit e1020945dd
14 changed files with 464 additions and 180 deletions

View File

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- "Poise" renamed to "Stun resilience"
- Stun resilience stat display
- Villagers and guards now spawn with potions, and know how to use them.
- Combat music in dungeons when within range of enemies.
### Changed

View File

@ -0,0 +1,15 @@
(
combat_nearby_radius: 40.0,
combat_health_factor: 1000,
combat_nearby_high_thresh: 4,
combat_nearby_low_thresh: 1,
fade_timings: {
(TitleMusic, Exploration): (2.0, 12.0),
(TitleMusic, Combat): (2.0, 3.0),
(Exploration, TitleMusic): (2.0, 12.0),
(Exploration, Combat): (5.0, 3.0),
(Combat, Exploration): (5.0, 3.0),
(Combat, TitleMusic): (2.0, 12.0),
},
interrupt_delay: 5.0,
)

View File

@ -6,17 +6,17 @@
(
tracks: [
(
Individual((
title: "Dank Dungeon",
path: "voxygen.audio.soundtrack.dungeon.dank_dungeon",
length: 130.0,
timing: None,
biomes: [],
site: Some(Dungeon),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Calming Hills",
path: "voxygen.audio.soundtrack.overworld.calming_hills",
length: 101.0,
@ -25,10 +25,10 @@
(Mountain, 1)
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Ultimafounding; mixed by Robotnik",
),
(
)),
Individual((
title: "Fiesta Del Pueblo",
path: "voxygen.audio.soundtrack.overworld.fiesta_del_pueblo",
length: 183.0,
@ -37,50 +37,50 @@
(Desert, 1)
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic; mixed by Robotnik",
),
(
)),
Individual((
title: "Ruination",
path: "voxygen.audio.soundtrack.dungeon.ruination",
length: 135.0,
timing: None,
biomes: [],
site: Some(Dungeon),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Dank Hallows",
path: "voxygen.audio.soundtrack.cave.dank_hallows",
length: 227.0,
timing: None,
biomes: [],
site: Some(Cave),
activity: Explore,
music_state: Activity(Explore),
artist: "Flashbang",
),
(
)),
Individual((
title: "Vast Onslaught",
path: "voxygen.audio.soundtrack.dungeon.vast_onslaught",
length: 237.0,
timing: None,
biomes: [],
site: Some(Dungeon),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Sacred Temple",
path: "voxygen.audio.soundtrack.dungeon.sacred_temple",
length: 75.0,
timing: None,
biomes: [],
site: Some(Dungeon),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "True Nature",
path: "voxygen.audio.soundtrack.overworld.true_nature",
length: 169.0,
@ -89,10 +89,10 @@
(Forest, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "DaforLynx",
),
(
)),
Individual((
title: "Jungle Ambient",
path: "voxygen.audio.soundtrack.overworld.jungle_ambient",
length: 218.0,
@ -101,10 +101,10 @@
(Jungle, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
),
(
)),
Individual((
title: "Ethereal Bonds",
path: "voxygen.audio.soundtrack.overworld.ethereal_bonds",
length: 59.0,
@ -113,10 +113,10 @@
(Mountain, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Leap of Faith",
path: "voxygen.audio.soundtrack.overworld.leap_of_faith",
length: 269.0,
@ -126,10 +126,10 @@
(Lake, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Highland of the Hawk",
path: "voxygen.audio.soundtrack.overworld.highland_of_the_hawk",
length: 283.0,
@ -139,10 +139,10 @@
(Mountain, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
),
(
)),
Individual((
title: "Verdant Glades",
path: "voxygen.audio.soundtrack.overworld.verdant_glades",
length: 97.0,
@ -151,10 +151,10 @@
(Grassland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Calling Wild",
path: "voxygen.audio.soundtrack.overworld.calling_wild",
length: 160.0,
@ -163,10 +163,10 @@
(Grassland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Ultimafounding",
),
(
)),
Individual((
title: "Drifting Along",
path: "voxygen.audio.soundtrack.overworld.drifting_along",
length: 164.0,
@ -176,10 +176,10 @@
(Ocean, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "DaforLynx",
),
(
)),
Individual((
title: "Winter Falls",
path: "voxygen.audio.soundtrack.overworld.winter_falls",
length: 215.0,
@ -188,10 +188,10 @@
(Snowland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "DaforLynx",
),
(
)),
Individual((
title: "Short Meandering",
path: "voxygen.audio.soundtrack.overworld.short_meandering",
length: 147.0,
@ -201,10 +201,10 @@
(Mountain, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Ap1evideogame",
),
(
)),
Individual((
title: "Oceania",
path: "voxygen.audio.soundtrack.overworld.oceania",
length: 135.0,
@ -214,10 +214,10 @@
(Ocean, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Eden",
),
(
)),
Individual((
title: "A Solemn Quest",
path: "voxygen.audio.soundtrack.overworld.a_solemn_quest",
length: 206.0,
@ -227,10 +227,10 @@
(Mountain, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Eden",
),
(
)),
Individual((
title: "Into The Dark Forest",
path: "voxygen.audio.soundtrack.overworld.into_the_dark_forest",
length: 184.0,
@ -240,10 +240,10 @@
(Jungle, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Field Grazing",
path: "voxygen.audio.soundtrack.overworld.field_grazing",
length: 154.0,
@ -253,10 +253,10 @@
(Forest, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Wandering Voices",
path: "voxygen.audio.soundtrack.overworld.wandering_voices",
length: 137.0,
@ -265,10 +265,10 @@
(Grassland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Snowtop Volume",
path: "voxygen.audio.soundtrack.overworld.snowtop_volume",
length: 89.0,
@ -277,20 +277,20 @@
(Snowland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Mineral Deposits",
path: "voxygen.audio.soundtrack.cave.mineral_deposits",
length: 148.0,
timing: None,
biomes: [],
site: Some(Cave),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Moonbeams",
path: "voxygen.audio.soundtrack.overworld.moonbeams",
length: 158.0,
@ -299,10 +299,10 @@
(Snowland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Serene Meadows",
path: "voxygen.audio.soundtrack.overworld.serene_meadows",
length: 173.0,
@ -312,10 +312,10 @@
(Desert, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "Aeronic",
),
(
)),
Individual((
title: "Just The Beginning",
path: "voxygen.audio.soundtrack.overworld.just_the_beginning",
length: 188.0,
@ -324,10 +324,10 @@
(Grassland, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
),
(
)),
Individual((
title: "Campfire Stories",
path: "voxygen.audio.soundtrack.overworld.campfire_stories",
length: 100.0,
@ -336,10 +336,10 @@
(Forest, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
),
(
)),
Individual((
title: "Limits",
path: "voxygen.audio.soundtrack.overworld.limits",
length: 203.0,
@ -348,20 +348,20 @@
(Mountain, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
),
(
)),
Individual((
title: "Down The Rabbit Hole",
path: "voxygen.audio.soundtrack.dungeon.down_the_rabbit_hole",
length: 244.0,
timing: None,
biomes: [],
site: Some(Dungeon),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
),
(
)),
Individual((
title: "Between The Fairies",
path: "voxygen.audio.soundtrack.overworld.between_the_fairies",
length: 175.0,
@ -370,8 +370,25 @@
(Forest, 1),
],
site: Some(Void),
activity: Explore,
music_state: Activity(Explore),
artist: "badbbad",
)
)),
Segmented(
title: "Barred Paths",
author: "DaforLynx",
timing: None,
biomes: [],
site: Some(Dungeon),
segments: [
("voxygen.audio.soundtrack.barred_paths.barred_paths-hi-end", 6.0, Transition(Combat(High), Explore), None),
("voxygen.audio.soundtrack.barred_paths.barred_paths-hi-loop", 54.0, Activity(Combat(High)), None),
("voxygen.audio.soundtrack.barred_paths.barred_paths-hi-start", 55.0, Transition(Explore, Combat(High)), Some(Combat(High))),
("voxygen.audio.soundtrack.barred_paths.barred_paths-lo-end", 3.0, Transition(Combat(Low), Explore), None),
("voxygen.audio.soundtrack.barred_paths.barred_paths-lo-loop", 7.0, Activity(Combat(Low)), None),
("voxygen.audio.soundtrack.barred_paths.barred_paths-lo-start", 10.0, Transition(Explore, Combat(Low)), None),
("voxygen.audio.soundtrack.barred_paths.barred_paths-trans-hi-lo", 10.0, Transition(Combat(High), Combat(Low)), None),
("voxygen.audio.soundtrack.barred_paths.barred_paths-trans-lo-hi", 7.0, Transition(Combat(Low), Combat(High)), None),
],
),
]
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -37,10 +37,11 @@ enum ChannelState {
/// transition between `TitleMusic` and `Exploration` when a player enters the
/// world by crossfading over a slow duration. In the future, transitions in the
/// world such as `Exploration` -> `BossBattle` would transition more rapidly.
#[derive(PartialEq, Clone, Copy)]
#[derive(PartialEq, Clone, Copy, Hash, Eq, Deserialize)]
pub enum MusicChannelTag {
TitleMusic,
Exploration,
Combat,
}
/// A MusicChannel uses a non-positional audio sink designed to play music which

View File

@ -9,12 +9,13 @@ pub mod soundcache;
use channel::{AmbientChannel, AmbientChannelTag, MusicChannel, MusicChannelTag, SfxChannel};
use fader::Fader;
use music::MusicTransitionManifest;
use sfx::{SfxEvent, SfxTriggerItem};
use soundcache::{OggSound, WavSound};
use std::time::Duration;
use tracing::{debug, error};
use common::assets::AssetExt;
use common::assets::{AssetExt, AssetHandle};
use rodio::{source::Source, OutputStream, OutputStreamHandle, StreamError};
use vek::*;
@ -45,6 +46,8 @@ pub struct AudioFrontend {
sfx_volume: f32,
music_volume: f32,
listener: Listener,
mtm: AssetHandle<MusicTransitionManifest>,
}
impl AudioFrontend {
@ -90,6 +93,7 @@ impl AudioFrontend {
sfx_volume: 1.0,
music_volume: 1.0,
listener: Listener::default(),
mtm: AssetExt::load_expect("voxygen.audio.music_transition_manifest"),
}
}
@ -108,6 +112,9 @@ impl AudioFrontend {
sfx_volume: 1.0,
music_volume: 1.0,
listener: Listener::default(),
// This expect should be fine, since `<MusicTransitionManifest as Asset>::default_value`
// is specified
mtm: AssetExt::load_expect("voxygen.audio.music_transition_manifest"),
}
}
@ -152,14 +159,19 @@ impl AudioFrontend {
let existing_channel = self.music_channels.last_mut()?;
if existing_channel.get_tag() != next_channel_tag {
let mtm = self.mtm.read();
let (fade_out, fade_in) = mtm
.fade_timings
.get(&(existing_channel.get_tag(), next_channel_tag))
.unwrap_or(&(1.0, 1.0));
let fade_out = Duration::from_secs_f32(*fade_out);
let fade_in = Duration::from_secs_f32(*fade_in);
// Fade the existing channel out. It will be removed when the fade completes.
existing_channel
.set_fader(Fader::fade_out(Duration::from_secs(2), self.music_volume));
existing_channel.set_fader(Fader::fade_out(fade_out, self.music_volume));
let mut next_music_channel = MusicChannel::new(&audio_stream);
next_music_channel
.set_fader(Fader::fade_in(Duration::from_secs(12), self.music_volume));
next_music_channel.set_fader(Fader::fade_in(fade_in, self.music_volume));
self.music_channels.push(next_music_channel);
}

View File

@ -50,20 +50,25 @@ use common::{
terrain::{BiomeKind, SitesKind},
};
use common_sys::state::State;
use hashbrown::HashMap;
use rand::{prelude::SliceRandom, thread_rng, Rng};
use serde::Deserialize;
use std::time::Instant;
use tracing::warn;
use tracing::{debug, warn};
/// Collection of all the tracks
#[derive(Debug, Default, Deserialize)]
struct SoundtrackCollection {
#[derive(Debug, Deserialize)]
struct SoundtrackCollection<T> {
/// List of tracks
tracks: Vec<SoundtrackItem>,
tracks: Vec<T>,
}
impl<T> Default for SoundtrackCollection<T> {
fn default() -> Self { Self { tracks: Vec::new() } }
}
/// Configuration for a single music track in the soundtrack
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct SoundtrackItem {
/// Song title
title: String,
@ -79,17 +84,46 @@ pub struct SoundtrackItem {
site: Option<SitesKind>,
/// What the player is doing when the track is played (i.e. exploring,
/// combat)
activity: MusicActivity,
music_state: MusicState,
/// What activity to override the activity state with, if any (e.g. to make
/// a long combat intro also act like the loop for the purposes of outro
/// transitions)
#[serde(default)]
activity_override: Option<MusicActivity>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize)]
enum RawSoundtrackItem {
Individual(SoundtrackItem),
Segmented {
title: String,
timing: Option<DayPeriod>,
biomes: Vec<(BiomeKind, u8)>,
site: Option<SitesKind>,
segments: Vec<(String, f32, MusicState, Option<MusicActivity>)>,
},
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
enum CombatIntensity {
Low,
High,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
enum MusicActivity {
Explore,
Combat,
Combat(CombatIntensity),
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
enum MusicState {
Activity(MusicActivity),
Transition(MusicActivity, MusicActivity),
}
/// Allows control over when a track should play based on in-game time of day
#[derive(Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
enum DayPeriod {
/// 8:00 AM to 7:30 PM
Day,
@ -109,7 +143,7 @@ enum PlayState {
/// Provides methods to control music playback
pub struct MusicMgr {
/// Collection of all the tracks
soundtrack: AssetHandle<SoundtrackCollection>,
soundtrack: AssetHandle<SoundtrackCollection<SoundtrackItem>>,
/// Instant at which the current track began playing
began_playing: Instant,
/// Time until the next track should be played
@ -117,6 +151,51 @@ pub struct MusicMgr {
/// The title of the last track played. Used to prevent a track
/// being played twice in a row
last_track: String,
/// Time of the last interrupt (to avoid rapid switching)
last_interrupt: Instant,
/// The previous track's activity kind, for transitions
last_activity: MusicState,
}
#[derive(Deserialize)]
pub struct MusicTransitionManifest {
/// Within what radius do enemies count towards combat music?
combat_nearby_radius: f32,
/// Each multiple of this factor that an enemy has health counts as an extra
/// enemy
combat_health_factor: u32,
/// How many nearby enemies trigger High combat music
combat_nearby_high_thresh: u32,
/// How many nearby enemies trigger Low combat music
combat_nearby_low_thresh: u32,
/// Fade in and fade out timings for transitions between channels
pub fade_timings: HashMap<(MusicChannelTag, MusicChannelTag), (f32, f32)>,
/// How many seconds between interrupt checks
pub interrupt_delay: f32,
}
impl Default for MusicTransitionManifest {
fn default() -> MusicTransitionManifest {
MusicTransitionManifest {
combat_nearby_radius: 40.0,
combat_health_factor: 1000,
combat_nearby_high_thresh: 3,
combat_nearby_low_thresh: 1,
fade_timings: HashMap::new(),
interrupt_delay: 5.0,
}
}
}
impl assets::Asset for MusicTransitionManifest {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
fn default_value(id: &str, e: assets::Error) -> Result<MusicTransitionManifest, assets::Error> {
warn!("Error loading MusicTransitionManifest {:?}: {:?}", id, e);
Ok(MusicTransitionManifest::default())
}
}
impl Default for MusicMgr {
@ -126,6 +205,8 @@ impl Default for MusicMgr {
began_playing: Instant::now(),
next_track_change: 0.0,
last_track: String::from("None"),
last_interrupt: Instant::now(),
last_activity: MusicState::Activity(MusicActivity::Explore),
}
}
}
@ -144,75 +225,159 @@ impl MusicMgr {
// player_alt = position.0.z;
//}
if audio.music_enabled()
&& !self.soundtrack.read().tracks.is_empty()
&& self.began_playing.elapsed().as_secs_f32() > self.next_track_change
use common::comp::{group::ENEMY, Group, Health, Pos};
use specs::{Join, WorldExt};
let mut activity_state = MusicActivity::Explore;
let player = client.entity();
let ecs = state.ecs();
let entities = ecs.entities();
let positions = ecs.read_component::<Pos>();
let healths = ecs.read_component::<Health>();
let groups = ecs.read_component::<Group>();
let mtm = audio.mtm.read();
if let Some(player_pos) = positions.get(player) {
// TODO: `group::ENEMY` will eventually be moved server-side with an
// alignment/faction rework, so this will need an alternative way to measure
// "in-combat-ness"
let num_nearby_entities: u32 = (&entities, &positions, &healths, &groups)
.join()
.map(|(entity, pos, health, group)| {
if entity != player
&& group == &ENEMY
&& (player_pos.0 - pos.0).magnitude_squared()
< mtm.combat_nearby_radius.powf(2.0)
{
self.play_random_track(audio, state, client);
(health.maximum() / mtm.combat_health_factor).max(1)
} else {
0
}
})
.sum();
if num_nearby_entities >= mtm.combat_nearby_high_thresh {
activity_state = MusicActivity::Combat(CombatIntensity::High);
} else if num_nearby_entities >= mtm.combat_nearby_low_thresh {
activity_state = MusicActivity::Combat(CombatIntensity::Low);
}
}
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
// Override combat music with explore music if the player is dead
if let Some(health) = healths.get(player) {
if health.is_dead {
activity_state = MusicActivity::Explore;
}
}
let music_state = match self.last_activity {
MusicState::Activity(prev) => {
if prev != activity_state {
MusicState::Transition(prev, activity_state)
} else {
MusicState::Activity(activity_state)
}
},
MusicState::Transition(_, next) => MusicState::Activity(next),
};
let interrupt = matches!(music_state, MusicState::Transition(_, _))
&& self.last_interrupt.elapsed().as_secs_f32() > mtm.interrupt_delay;
if audio.music_enabled()
&& !self.soundtrack.read().tracks.is_empty()
&& (self.began_playing.elapsed().as_secs_f32() > self.next_track_change || interrupt)
{
if interrupt {
self.last_interrupt = Instant::now();
}
debug!(
"pre-play_random_track: {:?} {:?}",
self.last_activity, music_state
);
if let Ok(next_activity) = self.play_random_track(audio, state, client, &music_state) {
self.last_activity = next_activity;
}
}
}
fn play_random_track(
&mut self,
audio: &mut AudioFrontend,
state: &State,
client: &Client,
music_state: &MusicState,
) -> Result<MusicState, ()> {
let mut rng = thread_rng();
// Adds a bit of randomness between plays
let silence_between_tracks_seconds: f32 = rng.gen_range(60.0..120.0);
let silence_between_tracks_seconds: f32 =
if matches!(music_state, MusicState::Activity(MusicActivity::Explore)) {
rng.gen_range(60.0..120.0)
} else {
0.0
};
let is_dark = (state.get_day_period().is_dark()) as bool;
let current_period_of_day = Self::get_current_day_period(is_dark);
let current_biome = client.current_biome();
let current_site = client.current_site();
// Filters out tracks not matching the timing, site, and biome
// Filter the soundtrack in stages, so that we don't overprune it if there are
// too many constraints. Returning Err(()) signals that we couldn't find
// an appropriate track for the current state, and hence the state
// machine for the activity shouldn't be updated.
let soundtrack = self.soundtrack.read();
let maybe_tracks = soundtrack
// First, filter out tracks not matching the timing, site, biome, and current
// activity
let mut maybe_tracks = soundtrack
.tracks
.iter()
.filter(|track| track.activity == MusicActivity::Explore)
.filter(|track| {
!track.title.eq(&self.last_track)
&& match &track.timing {
(match &track.timing {
Some(period_of_day) => period_of_day == &current_period_of_day,
None => true,
}
&& match &track.site {
}) && match &track.site {
Some(site) => site == &current_site,
None => true,
}
})
.filter(|track| {
let mut result = false;
if !track.biomes.is_empty() {
for biome in track.biomes.iter() {
if biome.0 == current_biome {
result = true;
}
}
} else {
result = true;
}
result
track.biomes.is_empty() || track.biomes.iter().any(|b| b.0 == current_biome)
})
.filter(|track| &track.music_state == music_state)
.collect::<Vec<&SoundtrackItem>>();
if maybe_tracks.is_empty() {
return Err(());
}
// Second, prevent playing the last track if possible (though don't return Err
// here, since the combat music is intended to loop)
let filtered_tracks: Vec<_> = maybe_tracks
.iter()
.filter(|track| !track.title.eq(&self.last_track))
.copied()
.collect();
if !filtered_tracks.is_empty() {
maybe_tracks = filtered_tracks;
}
// Randomly selects a track from the remaining tracks weighted based
// on the biome
let new_maybe_track = maybe_tracks.choose_weighted(&mut rng, |track| {
let mut chance = 0;
if !track.biomes.is_empty() {
for biome in track.biomes.iter() {
if biome.0 == current_biome {
chance = biome.1;
}
}
} else {
// If no biome is listed, the song is still added to the
// rotation to allow for site specific songs to play
// in any biome
chance = 1;
}
chance
track
.biomes
.iter()
.find(|b| b.0 == current_biome)
.map_or(1, |b| b.1)
});
debug!(
"selecting new track for {:?}: {:?}",
music_state, new_maybe_track
);
if let Ok(track) = new_maybe_track {
//println!("Now playing {:?}", track.title);
@ -220,7 +385,20 @@ impl MusicMgr {
self.began_playing = Instant::now();
self.next_track_change = track.length + silence_between_tracks_seconds;
audio.play_music(&track.path, MusicChannelTag::Exploration);
let tag = if matches!(music_state, MusicState::Activity(MusicActivity::Explore)) {
MusicChannelTag::Exploration
} else {
MusicChannelTag::Combat
};
audio.play_music(&track.path, tag);
if let Some(state) = track.activity_override {
Ok(MusicState::Activity(state))
} else {
Ok(*music_state)
}
} else {
Err(())
}
}
@ -232,23 +410,59 @@ impl MusicMgr {
}
}
fn load_soundtrack_items() -> AssetHandle<SoundtrackCollection> {
fn load_soundtrack_items() -> AssetHandle<SoundtrackCollection<SoundtrackItem>> {
// Cannot fail: A default value is always provided
SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
}
}
impl assets::Asset for SoundtrackCollection {
impl assets::Asset for SoundtrackCollection<RawSoundtrackItem> {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
fn default_value(_: &str, error: assets::Error) -> Result<Self, assets::Error> {
warn!(
"Error reading music config file, music will not be available: {:#?}",
error
);
impl assets::Compound for SoundtrackCollection<SoundtrackItem> {
fn load<S: assets::source::Source>(
_: &assets::AssetCache<S>,
id: &str,
) -> Result<Self, assets::Error> {
let inner = || -> Result<_, assets::Error> {
let manifest: AssetHandle<assets::Ron<SoundtrackCollection<RawSoundtrackItem>>> =
AssetExt::load(id)?;
let mut soundtracks = SoundtrackCollection::default();
for item in manifest.read().0.tracks.iter().cloned() {
match item {
RawSoundtrackItem::Individual(track) => soundtracks.tracks.push(track),
RawSoundtrackItem::Segmented {
title,
timing,
biomes,
site,
segments,
} => {
for (path, length, music_state, activity_override) in segments.into_iter() {
soundtracks.tracks.push(SoundtrackItem {
title: title.clone(),
path,
length,
timing: timing.clone(),
biomes: biomes.clone(),
site,
music_state,
activity_override,
});
}
},
}
}
Ok(soundtracks)
};
match inner() {
Ok(soundtracks) => Ok(soundtracks),
Err(e) => {
warn!("Error loading soundtracks: {:?}", e);
Ok(SoundtrackCollection::default())
},
}
}
}