diff --git a/resources/assets/ambient_night.ogg b/resources/assets/ambient_night.ogg new file mode 100644 index 0000000..86fb6ee --- /dev/null +++ b/resources/assets/ambient_night.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396644028ca42bf724b4c45073c4729fde765867be1f4441829f1a8d5376be47 +size 1624764 diff --git a/resources/input.ron b/resources/input.ron index 0876c3d..5886dd2 100644 --- a/resources/input.ron +++ b/resources/input.ron @@ -33,5 +33,14 @@ "CameraMoveBackward": [ [Key(LShift), Key(Down)] ], + "TogglePauseMenu": [ + [Key(Escape)] + ], + "GoodMorning": [ + [Key(LAlt), Key(M)] + ], + "GoodNight": [ + [Key(LAlt), Key(N)] + ], }, ) diff --git a/resources/prefabs/ui/controls.ron b/resources/prefabs/ui/controls.ron index 673a208..cdff68c 100644 --- a/resources/prefabs/ui/controls.ron +++ b/resources/prefabs/ui/controls.ron @@ -41,7 +41,7 @@ Container( id: "done button", x: 0.0, y: 50.0, - z: 10.0, + z: 15.0, width: 100.0, height: 50.0, anchor: BottomMiddle, diff --git a/resources/prefabs/ui/controls_row.ron b/resources/prefabs/ui/controls_row.ron index 9fefb2b..2daa589 100644 --- a/resources/prefabs/ui/controls_row.ron +++ b/resources/prefabs/ui/controls_row.ron @@ -4,13 +4,13 @@ Label( id: "controls_row", anchor: TopMiddle, width: 800.0, - height: 100.0, + height: 70.0, z: 10.0, ), text: ( text: "CONTROLS ROW PLACEHOLDER", font: File("assets/fonts/OpenSans-Regular.ttf", ("TTF", ())), - font_size: 30.0, + font_size: 25.0, color: (0.0, 0.0, 0.0, 1.0), ) ) diff --git a/src/components/combat.rs b/src/components/combat.rs index 2ccfcef..0d01a75 100644 --- a/src/components/combat.rs +++ b/src/components/combat.rs @@ -9,8 +9,7 @@ use amethyst::{ //use amethyst_inspector::Inspect; use log::error; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Duration; +use std::{collections::HashMap, ops::DerefMut, time::Duration}; #[derive(Default, Debug, Clone, Deserialize, Serialize, PrefabData)] #[prefab(Component)] @@ -211,10 +210,15 @@ pub struct Factions(HashMap); // factions as prey that are on top of their definition. For example, 'Plants' cannot define 'Herbivores' as their prey // because 'Herbivores' is defined after 'Plants'. pub fn load_factions(world: &mut World) { - let prefab_handle = world.exec(|loader: PrefabLoader<'_, FactionPrefabData>| { - loader.load("prefabs/factions.ron", RonFormat, ()) - }); - + world + .res + .entry::() + .or_insert(ProgressCounter::default()); + let prefab_handle = world.exec( + |(loader, mut progress): (PrefabLoader, Write)| { + loader.load("prefabs/factions.ron", RonFormat, progress.deref_mut()) + }, + ); world.create_entity().with(prefab_handle.clone()).build(); } diff --git a/src/events/day_night_cycle.rs b/src/events/day_night_cycle.rs new file mode 100644 index 0000000..e114a40 --- /dev/null +++ b/src/events/day_night_cycle.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Copy, Clone)] +pub enum DayNightCycleEvent { + GoodMorning, + GoodNight, +} diff --git a/src/events/mod.rs b/src/events/mod.rs new file mode 100644 index 0000000..87ad1d0 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,6 @@ +// event producers and consumers will both depend on the same event enumeration +// there may be multiple producers; there may be multiple consumers +// that is, there is no clear owner +// thus, events are defined in this directory independent of their producers and consumers + +pub mod day_night_cycle; diff --git a/src/main.rs b/src/main.rs index 264cfb6..5988818 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,10 @@ #[macro_use] extern crate log; -use amethyst::assets::PrefabLoaderSystem; use amethyst::{ - assets::Processor, - audio::{AudioBundle, DjSystem}, - core::frame_limiter::FrameRateLimitStrategy, - core::transform::TransformBundle, + assets::{PrefabLoaderSystem, Processor}, + audio::AudioBundle, + core::{frame_limiter::FrameRateLimitStrategy, transform::TransformBundle}, gltf::GltfSceneLoaderSystem, input::{InputBundle, StringBindings}, prelude::*, @@ -20,16 +18,20 @@ use amethyst::{ }; mod components; +mod events; mod render_graph; mod resources; mod states; mod systems; mod utils; -use crate::components::{combat, creatures}; -use crate::render_graph::RenderGraph; -use crate::resources::audio::Music; -use crate::states::loading::LoadingState; +use crate::{ + components::{combat, creatures}, + events::day_night_cycle, + render_graph::RenderGraph, + states::loading::LoadingState, + systems::music::MusicSystem, +}; fn main() -> amethyst::Result<()> { amethyst::start_logger(Default::default()); @@ -67,11 +69,6 @@ fn main() -> amethyst::Result<()> { "", &[], ) - .with( - DjSystem::new(|music: &mut Music| music.music.next()), - "dj", - &[], - ) .with_bundle(TransformBundle::new())? .with_bundle(AudioBundle::default())? .with_bundle(WindowBundle::from_config(display_config))? @@ -93,7 +90,8 @@ fn main() -> amethyst::Result<()> { ) .with_thread_local(RenderingSystem::::new( RenderGraph::default(), - )); + )) + .with(MusicSystem::default(), "music_system", &[]); // Set up the core application. let mut game: Application = diff --git a/src/resources/audio.rs b/src/resources/audio.rs deleted file mode 100644 index ad79e1c..0000000 --- a/src/resources/audio.rs +++ /dev/null @@ -1,40 +0,0 @@ -use amethyst::{ - assets::Loader, - audio::{AudioSink, OggFormat, SourceHandle}, - ecs::prelude::World, -}; - -use std::iter::Cycle; -use std::vec::IntoIter; - -const BACKGROUND_MUSIC: &'static [&'static str] = &["assets/ambient.ogg"]; - -pub struct Music { - pub music: Cycle>, -} - -fn load_audio_track(loader: &Loader, world: &World, file: &str) -> SourceHandle { - loader.load(file, OggFormat, (), &world.read_resource()) -} - -// Initialise audio in the world. This sets up the background music -pub fn initialise_audio(world: &mut World) { - let music = { - let loader = world.read_resource::(); - - let mut sink = world.write_resource::(); - sink.set_volume(0.25); - - let music = BACKGROUND_MUSIC - .iter() - .map(|file| load_audio_track(&loader, &world, &file)) - .collect::>() - .into_iter() - .cycle(); - - Music { music } - }; - - // Add sounds to the world - world.add_resource(music); -} diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 72848d2..f29523d 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -1,4 +1,3 @@ -pub mod audio; pub mod debug; pub mod prefabs; pub mod world_bounds; diff --git a/src/resources/prefabs.rs b/src/resources/prefabs.rs index a64f72d..2ccb0b0 100644 --- a/src/resources/prefabs.rs +++ b/src/resources/prefabs.rs @@ -1,10 +1,9 @@ -use std::collections::HashMap; -use std::fs::read_dir; +use std::{collections::HashMap, fs::read_dir, ops::DerefMut}; use crate::components::creatures::CreaturePrefabData; use amethyst::{ assets::{AssetStorage, Handle, Prefab, PrefabLoader, ProgressCounter, RonFormat}, - ecs::World, + ecs::{World, Write}, ui::{UiLoader, UiPrefab}, utils::application_root_dir, }; @@ -73,11 +72,18 @@ fn make_name(subdirectory: &str, entry: &std::fs::DirEntry) -> String { // These prefabs are then stored in a resource of type CreaturePrefabs that is used by the spawner system. // At initialization time, we put temporary keys for the prefabs since they're not loaded yet. // When their loading is finished, we read the name of the entity inside to change the keys. This is done in the update_prefabs function. -pub fn initialize_prefabs(world: &mut World) -> ProgressCounter { - let mut progress_counter = ProgressCounter::new(); +pub fn initialize_prefabs(world: &mut World) { + world + .res + .entry::() + .or_insert(ProgressCounter::default()); + // load ui prefabs { - let mut ui_prefab_registry = UiPrefabRegistry::default(); + world + .res + .entry::() + .or_insert(UiPrefabRegistry::default()); let prefab_dir_path = application_root_dir() .unwrap() .into_os_string() @@ -85,17 +91,22 @@ pub fn initialize_prefabs(world: &mut World) -> ProgressCounter { .unwrap() + "/resources/prefabs/ui"; let prefab_iter = read_dir(prefab_dir_path).unwrap(); - ui_prefab_registry.prefabs = prefab_iter - .map(|prefab_dir_entry| { - world.exec(|loader: UiLoader<'_>| { - loader.load( - make_name("prefabs/ui/", &prefab_dir_entry.unwrap()), - &mut progress_counter, - ) - }) - }) - .collect::>>(); - world.add_resource(ui_prefab_registry); + world.exec( + |(mut ui_prefab_registry, loader, mut progress): ( + Write, + UiLoader, + Write, + )| { + ui_prefab_registry.deref_mut().prefabs = prefab_iter + .map(|prefab_dir_entry| { + loader.load( + make_name("prefabs/ui/", &prefab_dir_entry.unwrap()), + progress.deref_mut(), + ) + }) + .collect::>>(); + }, + ); } // load creature prefabs @@ -109,13 +120,18 @@ pub fn initialize_prefabs(world: &mut World) -> ProgressCounter { + "/resources/prefabs/creatures"; let prefab_iter = read_dir(prefab_dir_path).unwrap(); prefab_iter.map(|prefab_dir_entry| { - world.exec(|loader: PrefabLoader<'_, CreaturePrefabData>| { - loader.load( - make_name("prefabs/creatures/", &prefab_dir_entry.unwrap()), - RonFormat, - &mut progress_counter, - ) - }) + world.exec( + |(loader, mut progress): ( + PrefabLoader<'_, CreaturePrefabData>, + Write, + )| { + loader.load( + make_name("prefabs/creatures/", &prefab_dir_entry.unwrap()), + RonFormat, + progress.deref_mut(), + ) + }, + ) }) }; @@ -125,8 +141,6 @@ pub fn initialize_prefabs(world: &mut World) -> ProgressCounter { } world.add_resource(creature_prefabs); } - - progress_counter } // Once the prefabs are loaded, this function is called to update the ekeys in the CreaturePrefabs struct. diff --git a/src/states/controls.rs b/src/states/controls.rs index 130adf9..2236965 100644 --- a/src/states/controls.rs +++ b/src/states/controls.rs @@ -41,7 +41,7 @@ impl ControlsState { bindings }; let y = -150.; // start below our title label - let y_step = -40.; + let y_step = -30.; ( &world.entities(), &mut world.write_storage::(), diff --git a/src/states/loading.rs b/src/states/loading.rs index 96805bb..ab8553f 100644 --- a/src/states/loading.rs +++ b/src/states/loading.rs @@ -1,42 +1,33 @@ use crate::{ + components::combat::load_factions, resources::{ - audio::initialise_audio, prefabs::{initialize_prefabs, update_prefabs}, world_bounds::WorldBounds, }, states::{main_game::MainGameState, menu::MenuState}, }; -use std::env; - -use crate::components::combat::load_factions; use amethyst::{ assets::ProgressCounter, prelude::*, renderer::debug_drawing::{DebugLines, DebugLinesParams}, }; +use std::env; const SKIP_MENU_ARG: &str = "no_menu"; -pub struct LoadingState { - prefab_loading_progress: Option, -} - -impl Default for LoadingState { - fn default() -> Self { - LoadingState { - prefab_loading_progress: None, - } - } -} +#[derive(Default)] +pub struct LoadingState {} impl SimpleState for LoadingState { fn on_start(&mut self, mut data: StateData) { + data.world + .res + .entry::() + .or_insert(ProgressCounter::default()); load_factions(data.world); - self.prefab_loading_progress = Some(initialize_prefabs(&mut data.world)); - initialise_audio(data.world); + initialize_prefabs(&mut data.world); data.world .add_resource(DebugLinesParams { line_width: 1.0 }); - data.world.add_resource(DebugLines::new()); data.world .add_resource(WorldBounds::new(-10.0, 10.0, -10.0, 10.0)); @@ -44,15 +35,14 @@ impl SimpleState for LoadingState { fn update(&mut self, data: &mut StateData) -> SimpleTrans { data.data.update(&data.world); - if let Some(ref counter) = self.prefab_loading_progress.as_ref() { - if counter.is_complete() { - self.prefab_loading_progress = None; - update_prefabs(&mut data.world); - if env::args().any(|arg| arg == SKIP_MENU_ARG) { - return Trans::Switch(Box::new(MainGameState::new(data.world))); - } else { - return Trans::Switch(Box::new(MenuState::default())); - } + if data.world.read_resource::().is_complete() { + info!("loading complete"); + update_prefabs(&mut data.world); + // TODO how to reset the ProgressCounter? data.world.write_resource::>().as_mut() = Some(ProgressCounter::new()); + if env::args().any(|arg| arg == SKIP_MENU_ARG) { + return Trans::Switch(Box::new(MainGameState::new(data.world))); + } else { + return Trans::Switch(Box::new(MenuState::default())); } } diff --git a/src/states/main_game.rs b/src/states/main_game.rs index b21fd57..f216c8c 100644 --- a/src/states/main_game.rs +++ b/src/states/main_game.rs @@ -19,17 +19,18 @@ use amethyst::{ use std::f32; -use crate::systems::behaviors::decision::{ - ClosestSystem, Predator, Prey, QueryPredatorsAndPreySystem, SeekSystem, -}; -use crate::systems::behaviors::obstacle::{ClosestObstacleSystem, Obstacle}; use crate::{ components::creatures::CreatureTag, + day_night_cycle::DayNightCycleEvent, resources::{ debug::DebugConfig, prefabs::UiPrefabRegistry, spatial_grid::SpatialGrid, world_bounds::WorldBounds, }, states::pause_menu::PauseMenuState, + systems::behaviors::{ + decision::{ClosestSystem, Predator, Prey, QueryPredatorsAndPreySystem, SeekSystem}, + obstacle::{ClosestObstacleSystem, Obstacle}, + }, systems::*, }; use rand::{thread_rng, Rng}; @@ -246,10 +247,23 @@ impl MainGameState { } fn handle_action(&mut self, action: &str, world: &mut World) -> SimpleTrans { + info!("MainGameState::handle_action({})", action); if action == "ToggleDebug" { let mut debug_config = world.write_resource::(); debug_config.visible = !debug_config.visible; Trans::None + } else if action == "TogglePauseMenu" { + Trans::Push(Box::new(PauseMenuState::default())) + } else if action == "GoodMorning" { + world + .write_resource::>() + .single_write(DayNightCycleEvent::GoodMorning); + Trans::None + } else if action == "GoodNight" { + world + .write_resource::>() + .single_write(DayNightCycleEvent::GoodNight); + Trans::None } else if action == main_game_ui::PAUSE_BUTTON.action { self.paused = !self.paused; self.update_time_scale(world); diff --git a/src/states/pause_menu.rs b/src/states/pause_menu.rs index 53c8ab6..3e11641 100644 --- a/src/states/pause_menu.rs +++ b/src/states/pause_menu.rs @@ -2,6 +2,7 @@ use crate::resources::prefabs::UiPrefabRegistry; use crate::states::menu::MenuState; use amethyst::{ ecs::Entity, + input::InputEvent, prelude::*, shrev::EventChannel, ui::{UiEvent, UiEventType, UiFinder}, @@ -67,6 +68,17 @@ impl<'a> SimpleState for PauseMenuState { Trans::None } } + StateEvent::Input(input_event) => { + if let InputEvent::ActionPressed(action) = input_event { + if action == "TogglePauseMenu" { + Trans::Pop + } else { + Trans::None + } + } else { + Trans::None + } + } _ => Trans::None, } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index 6faac2d..8a7eb4e 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -7,6 +7,7 @@ pub mod digestion; pub mod health; pub mod main_game_ui; pub mod movement; +pub mod music; pub mod spawner; pub mod swarm_behavior; diff --git a/src/systems/music.rs b/src/systems/music.rs new file mode 100644 index 0000000..76c5030 --- /dev/null +++ b/src/systems/music.rs @@ -0,0 +1,123 @@ +use crate::day_night_cycle::DayNightCycleEvent; +use amethyst::{ + assets::{AssetStorage, Loader, ProgressCounter}, + audio, + audio::{ + output::Output as AudioOutput, AudioSink, OggFormat, Source as AudioSource, + SourceHandle as AudioSourceHandle, + }, + ecs::*, + shrev::{EventChannel, ReaderId}, +}; +use std::ops::DerefMut; + +#[derive(Default)] +pub struct MusicSystem { + day_night_cycle_event_subscription: Option>, + day_music: Option, + night_music: Option, + audio_source: Option, + audio_sink: Option, +} + +type MusicSystemData<'s> = ( + Read<'s, EventChannel>, + Read<'s, AssetStorage>, + Write<'s, AudioOutput>, +); + +impl<'s> System<'s> for MusicSystem { + type SystemData = MusicSystemData<'s>; + + fn setup(&mut self, res: &mut Resources) { + info!("setup Music"); + + if let Some(audio_output) = audio::output::default_output() { + res.entry::().or_insert(audio_output); + } else { + error!("No audio output hardware detected") + } + + // TODO #day_night #date_time as there is no system currently writing to this EventChannel, explicitly register it for now + // once the DateTimeSystem is producing DayNightCycleEvents, this can be removed + // TODO what? this doesn't parse? + // res.insert(EventChannel::new()); + type DayNightEventChannel = EventChannel; + res.entry::() + .or_insert(DayNightEventChannel::new()); + + Self::SystemData::setup(res); + + self.day_night_cycle_event_subscription = Some( + res.fetch_mut::>() + .register_reader(), + ); + + res.entry::() + .or_insert(ProgressCounter::default()); + let mut progress = res.fetch_mut::(); + let loader = res.fetch::(); + let audio_source_storage = res.fetch::>(); + self.day_music = Some(loader.load( + "assets/ambient.ogg", + OggFormat, + progress.deref_mut(), + &audio_source_storage, + )); + // this music courtesy of https://community.amethyst.rs/u/jakob_t_r + // via this post https://community.amethyst.rs/t/evoli-mvp-implementation-tracker/537/6 + self.night_music = Some(loader.load( + "assets/ambient_night.ogg", + OggFormat, + progress.deref_mut(), + &audio_source_storage, + )); + } + + fn run( + &mut self, + (day_night_cycle_event_channel, audio_source_storage, audio_output): Self::SystemData, + ) { + // watch for day_night_cycle events and change music to match time of day + day_night_cycle_event_channel + .read( + self.day_night_cycle_event_subscription + .as_mut() + .expect("not subscribed to DayNightCycleEvents"), + ) + .for_each(|event| { + // stop the current audio + if let Some(audio_sink) = self.audio_sink.as_ref() { + audio_sink.stop(); + } + self.audio_sink = None; + + // choose new audio source appropriate for the event + self.audio_source = match event { + DayNightCycleEvent::GoodMorning => self.day_music.clone(), + DayNightCycleEvent::GoodNight => self.night_music.clone(), + }; + }); + + // we will create and start a new audio_sink whenever the current one becomes invalid, + // either by finishing normally or by being explicitly stopped + let new_sink_needed = if let Some(audio_sink) = self.audio_sink.as_ref() { + audio_sink.empty() + } else { + true + }; + + // if it is deemed that new audio should start, play the current audio source + if new_sink_needed { + self.audio_sink = self + .audio_source + .as_ref() + .and_then(|audio_source| audio_source_storage.get(audio_source)) + .and_then(|audio_source| { + let mut audio_sink = AudioSink::new(&audio_output); + audio_sink.set_volume(0.25); + audio_sink.append(audio_source).ok().and(Some(audio_sink)) + }); + }; + } +}