diff --git a/src/effects/damage.rs b/src/effects/damage.rs index 02be7c9..779940c 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -38,56 +38,61 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { }; target_pool.hit_points.current -= ((amount as f32) * mult) as i32; let bleeders = ecs.read_storage::(); - if target_pool.hit_points.current < 1 { - super::DEAD_ENTITIES.lock().unwrap().push_back(target); - add_effect(damage.source, EffectType::EntityDeath, Targets::Entity { target }); - for i in 0..3 { - let sprite = ( - match i { - 0 => "explode1", - 1 => "explode2", - _ => "explode3", - } - ).to_string(); - add_effect( - None, - EffectType::Particle { - glyph: to_cp437('‼'), - sprite, - fg: RGB::named(RED), - lifespan: 75.0, - delay: 75.0 * (i as f32), - }, - Targets::Entity { target } - ); - if let Some(bleeds) = bleeders.get(target) { + // If the target bleeds, handle bloodstains and use the bleed colour for sfx. + if let Some(bleeds) = bleeders.get(target) { + if target_pool.hit_points.current < 1 { + super::DEAD_ENTITIES.lock().unwrap().push_back(target); + add_effect(damage.source, EffectType::EntityDeath, Targets::Entity { + target, + }); + for i in 0..3 { + let sprite = ( + match i { + 0 => "explode1", + 1 => "explode2", + _ => "explode3", + } + ).to_string(); + add_effect( + None, + EffectType::Particle { + glyph: to_cp437('‼'), + sprite, + fg: bleeds.colour, + lifespan: 75.0, + delay: 75.0 * (i as f32), + }, + Targets::Entity { target } + ); add_effect( None, EffectType::Bloodstain { colour: bleeds.colour }, Targets::Entity { target } ); } - } - } else { - // Regular damage taken effect - use damagetype to determine which one to play. - add_effect( - None, - EffectType::Particle { - glyph: to_cp437('‼'), - sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES - fg: RGB::named(ORANGE), - lifespan: DEFAULT_PARTICLE_LIFETIME, - delay: 0.0, - }, - Targets::Entity { target } - ); - if let Some(bleeds) = bleeders.get(target) { + } else { + // Regular damage taken effect - use damagetype to determine which one to play. + add_effect( + None, + EffectType::Particle { + glyph: to_cp437('‼'), + sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES + fg: RGB::named(ORANGE), + lifespan: DEFAULT_PARTICLE_LIFETIME, + delay: 0.0, + }, + Targets::Entity { target } + ); add_effect( None, EffectType::Bloodstain { colour: bleeds.colour }, Targets::Entity { target } ); } + } else { + // TODO: Damage taken particle effects when the target does not bleed. + // Also damage types, etc. + return; } } } diff --git a/src/effects/mod.rs b/src/effects/mod.rs index f7b3d4f..c7a0577 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -2,6 +2,7 @@ use super::BUC; use crate::spatial; use bracket_lib::prelude::*; use specs::prelude::*; +use notan::prelude::*; use std::collections::VecDeque; use std::sync::Mutex; use crate::components::*; @@ -13,6 +14,7 @@ mod targeting; mod triggers; mod attr; mod intrinsics; +pub mod sound; pub use targeting::aoe_tiles; @@ -63,6 +65,9 @@ pub enum EffectType { TriggerFire { trigger: Entity, }, + Sound { + sound: String, + }, } #[derive(Clone)] @@ -95,7 +100,7 @@ pub fn add_effect(source: Option, effect_type: EffectType, target: Targe } /// Iterates through the effects queue, applying each effect to their target. -pub fn run_effects_queue(ecs: &mut World) { +pub fn run_effects_queue(app: &mut App, ecs: &mut World) { // First removes any effect in the EFFECT_QUEUE with a dead entity as its source. loop { let dead_entity: Option = DEAD_ENTITIES.lock().unwrap().pop_front(); @@ -111,7 +116,7 @@ pub fn run_effects_queue(ecs: &mut World) { loop { let effect: Option = EFFECT_QUEUE.lock().unwrap().pop_front(); if let Some(effect) = effect { - target_applicator(ecs, &effect); + target_applicator(app, ecs, &effect); } else { break; } @@ -119,7 +124,7 @@ pub fn run_effects_queue(ecs: &mut World) { } /// Applies an effect to the correct target(s). -fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { +fn target_applicator(app: &mut App, ecs: &mut World, effect: &EffectSpawner) { // Item use is handled differently - it creates other effects with itself // as the source, passing all effects attached to the item into the queue. if let EffectType::ItemUse { item } = effect.effect_type { @@ -131,25 +136,26 @@ fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { } // Otherwise, just match the effect and enact it directly. match &effect.target { - Targets::Tile { target } => affect_tile(ecs, effect, *target), + Targets::Tile { target } => affect_tile(app, ecs, effect, *target), Targets::TileList { targets } => - targets.iter().for_each(|target| affect_tile(ecs, effect, *target)), - Targets::Entity { target } => affect_entity(ecs, effect, *target), + targets.iter().for_each(|target| affect_tile(app, ecs, effect, *target)), + Targets::Entity { target } => affect_entity(app, ecs, effect, *target), Targets::EntityList { targets } => - targets.iter().for_each(|target| affect_entity(ecs, effect, *target)), + targets.iter().for_each(|target| affect_entity(app, ecs, effect, *target)), } } /// Runs an effect on a given tile index -fn affect_tile(ecs: &mut World, effect: &EffectSpawner, target: usize) { +fn affect_tile(app: &mut App, ecs: &mut World, effect: &EffectSpawner, target: usize) { if tile_effect_hits_entities(&effect.effect_type) { spatial::for_each_tile_content(target, |entity| { - affect_entity(ecs, effect, entity); + affect_entity(app, ecs, effect, entity); }); } match &effect.effect_type { EffectType::Particle { .. } => particles::particle_to_tile(ecs, target as i32, &effect), + EffectType::Sound { .. } => sound::play_sound(app, ecs, &effect, target), _ => {} } // Run the effect @@ -168,7 +174,7 @@ fn tile_effect_hits_entities(effect: &EffectType) -> bool { } /// Runs an effect on a given entity -fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { +fn affect_entity(app: &mut App, ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target), EffectType::Healing { .. } => damage::heal_damage(ecs, effect, target), @@ -187,6 +193,11 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { EffectType::EntityDeath => damage::entity_death(ecs, effect, target), EffectType::ModifyNutrition { .. } => hunger::modify_nutrition(ecs, effect, target), EffectType::AddIntrinsic { .. } => intrinsics::add_intrinsic(ecs, effect, target), + EffectType::Sound { .. } => { + if let Some(pos) = targeting::entity_position(ecs, target) { + sound::play_sound(app, ecs, &effect, pos); + } + } _ => {} } } diff --git a/src/effects/sound.rs b/src/effects/sound.rs new file mode 100644 index 0000000..e5f8c74 --- /dev/null +++ b/src/effects/sound.rs @@ -0,0 +1,65 @@ +use bracket_lib::prelude::*; +use notan::prelude::*; +use specs::prelude::*; +use std::sync::Mutex; +use std::collections::HashMap; +use super::{ EffectSpawner, EffectType }; +use crate::Map; + +lazy_static::lazy_static! { + pub static ref SOUNDS: Mutex> = Mutex::new(HashMap::new()); + pub static ref VOLUME: Mutex = Mutex::new(1.0); +} + +#[derive(PartialEq, Copy, Clone)] +pub enum AudioType { + Ambient, + SFX, +} + +pub fn play_sound(app: &mut App, ecs: &mut World, effect: &EffectSpawner, target: usize) { + // Extract sound from the EffectType, or panic if we somehow called this with the wrong effect. + let sound = if let EffectType::Sound { sound } = &effect.effect_type { + sound + } else { + unreachable!("add_intrinsic() called with the wrong EffectType") + }; + // Fetch all the relevant precursors. + let sounds = SOUNDS.lock().unwrap(); + let volume = VOLUME.lock().unwrap(); + let source = sounds.get(sound).unwrap(); + let (vol, repeat) = match source.1 { + AudioType::Ambient => (*volume * 0.5, true), + AudioType::SFX => { + let map = ecs.fetch::(); + let ppos = ecs.fetch::(); + // Calc distance from player to target. + let dist = DistanceAlg::PythagorasSquared.distance2d( + *ppos, + Point::new((target as i32) % map.width, (target as i32) / map.width) + ); + // Play sound at volume proportional to distance. + (*volume * (1.0 - (dist as f32) / 14.0), false) + } + }; + // Play the sound. + app.audio.play_sound(&source.0, vol, repeat); +} + +pub fn init_sounds(app: &mut App) { + let list: Vec<(&str, (&[u8], AudioType))> = vec![ + //key, (bytes, type) - audiotype determines final volume, looping, etc. + ("hit", (include_bytes!("../../resources/sounds/hit.wav"), AudioType::Ambient)), + ("other", (include_bytes!("../../resources/sounds/hit.wav"), AudioType::SFX)), + ("another", (include_bytes!("../../resources/sounds/hit.wav"), AudioType::SFX)) + ]; + let mut sounds = SOUNDS.lock().unwrap(); + for (k, (bytes, audiotype)) in list.iter() { + sounds.insert(k.to_string(), (app.audio.create_source(bytes).unwrap(), *audiotype)); + } +} + +pub fn set_volume(vol: f32) { + let mut volume = VOLUME.lock().unwrap(); + *volume = vol; +} diff --git a/src/main.rs b/src/main.rs index a4814b8..f067a8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,14 +33,8 @@ fn main() -> Result<(), String> { .build() } -fn setup(_app: &mut App, gfx: &mut Graphics) -> State { - /* - let sound = app.audio.create_source(include_bytes!("../resources/sounds/hit.wav")).unwrap(); - let sounds: HashMap = vec![("hit".to_string(), sound)] - .into_iter() - .collect(); - */ - +fn setup(app: &mut App, gfx: &mut Graphics) -> State { + effects::sound::init_sounds(app); let texture = gfx .create_texture() .from_image(include_bytes!("../resources/atlas.png")) diff --git a/src/states/state.rs b/src/states/state.rs index d97f901..716255b 100644 --- a/src/states/state.rs +++ b/src/states/state.rs @@ -85,23 +85,23 @@ impl State { } } - fn run_systems(&mut self) { + fn run_systems(&mut self, ctx: &mut App) { let mut hunger_clock = hunger_system::HungerSystem {}; let mut particle_system = particle_system::ParticleSpawnSystem {}; // Order is *very* important here, to ensure effects take place in the right order, // and that the player/AI are making decisions based on the most up-to-date info. - self.resolve_entity_decisions(); // Push Player messages of intent to effects queue, and run it. + self.resolve_entity_decisions(ctx); // Push Player messages of intent to effects queue, and run it. self.refresh_indexes(); // Get up-to-date map and viewsheds prior to AI turn. self.run_ai(); // Get AI decision-making. - self.resolve_entity_decisions(); // Push AI messages of intent to effects queue, and run it. + self.resolve_entity_decisions(ctx); // Push AI messages of intent to effects queue, and run it. hunger_clock.run_now(&self.ecs); // Tick the hunger clock (on the turn clock's turn) particle_system.run_now(&self.ecs); // Spawn/delete particles (turn independent) self.ecs.maintain(); } - fn resolve_entity_decisions(&mut self) { + fn resolve_entity_decisions(&mut self, ctx: &mut App) { let mut trigger_system = trigger_system::TriggerSystem {}; let mut item_equip_system = inventory::ItemEquipSystem {}; let mut item_use_system = inventory::ItemUseSystem {}; @@ -121,7 +121,7 @@ impl State { key_system.run_now(&self.ecs); melee_system.run_now(&self.ecs); - effects::run_effects_queue(&mut self.ecs); + effects::run_effects_queue(ctx, &mut self.ecs); } fn refresh_indexes(&mut self) { @@ -207,13 +207,13 @@ impl State { particle_system::particle_ticker(&mut self.ecs, ctx); match new_runstate { RunState::PreRun => { - self.run_systems(); + self.run_systems(ctx); self.ecs.maintain(); new_runstate = RunState::AwaitingInput; } RunState::AwaitingInput => { self.refresh_indexes(); - effects::run_effects_queue(&mut self.ecs); + effects::run_effects_queue(ctx, &mut self.ecs); let mut can_act = false; { let player_entity = self.ecs.fetch::(); @@ -231,7 +231,7 @@ impl State { } RunState::Ticking => { while new_runstate == RunState::Ticking && particle_system::check_queue(&self.ecs) { - self.run_systems(); + self.run_systems(ctx); self.ecs.maintain(); try_spawn_interval(&mut self.ecs); maybe_map_message(&mut self.ecs); @@ -585,7 +585,7 @@ impl State { match new_runstate { RunState::PreRun => { - self.run_systems(); + //self.run_systems(); self.ecs.maintain(); new_runstate = RunState::AwaitingInput; } @@ -594,7 +594,7 @@ impl State { // still be in the queue, just to make 100% sure that // there are no lingering effects from the last tick. self.refresh_indexes(); - effects::run_effects_queue(&mut self.ecs); + //effects::run_effects_queue(&mut self.ecs); // Sanity-checking that the player actually *should* // be taking a turn before giving them one. If they // don't have a turn component, go back to ticking. @@ -615,7 +615,7 @@ impl State { } RunState::Ticking => { while new_runstate == RunState::Ticking && particle_system::check_queue(&self.ecs) { - self.run_systems(); + //self.run_systems(); self.ecs.maintain(); try_spawn_interval(&mut self.ecs); maybe_map_message(&mut self.ecs);