diff --git a/src/effects.rs b/src/effects.rs index 3d49283..3f4fae0 100644 --- a/src/effects.rs +++ b/src/effects.rs @@ -1,6 +1,9 @@ mod damage; +mod hunger; +mod movement; mod particles; mod targeting; +mod triggers; use std::collections::VecDeque; use std::sync::Mutex; @@ -9,7 +12,6 @@ use ::rltk::{FontCharType, RGB}; use ::specs::prelude::*; pub use targeting::*; -use crate::effects::particles::particle_to_tile; use crate::spatial; lazy_static! { @@ -28,6 +30,25 @@ pub enum EffectType { lifespan: f32, }, EntityDeath, + ItemUse { + item: Entity, + }, + WellFed, + Healing { + amount: i32, + }, + Confusion { + turns: i32, + }, + TriggerFire { + trigger: Entity, + }, + TeleportTo { + x: i32, + y: i32, + depth: i32, + player_only: bool, + }, } #[derive(Clone)] @@ -65,23 +86,33 @@ pub fn run_effects_queue(ecs: &mut World) { } fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { - match &effect.targets { - Targets::Tile { tile_idx } => affect_tile(ecs, effect, *tile_idx), - Targets::Tiles { tiles } => tiles - .iter() - .for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), - Targets::Single { target } => affect_entity(ecs, effect, *target), - Targets::TargetList { targets } => targets - .iter() - .for_each(|entity| affect_entity(ecs, effect, *entity)), + if let EffectType::ItemUse { item } = effect.effect_type { + triggers::item_trigger(effect.creator, item, &effect.targets, ecs); + } else if let EffectType::TriggerFire { trigger } = effect.effect_type { + triggers::trigger(effect.creator, trigger, &effect.targets, ecs); + } else { + match &effect.targets { + Targets::Tile { tile_idx } => affect_tile(ecs, effect, *tile_idx), + Targets::Tiles { tiles } => tiles + .iter() + .for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), + Targets::Single { target } => affect_entity(ecs, effect, *target), + Targets::TargetList { targets } => targets + .iter() + .for_each(|entity| affect_entity(ecs, effect, *entity)), + } } } fn tile_effect_hits_entities(effect: &EffectType) -> bool { - match effect { - EffectType::Damage { .. } => true, - _ => false, - } + matches!( + effect, + EffectType::Damage { .. } + | EffectType::WellFed + | EffectType::Healing { .. } + | EffectType::Confusion { .. } + | EffectType::TeleportTo { .. } + ) } fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx: i32) { @@ -93,7 +124,7 @@ fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx: i32) { match &effect.effect_type { EffectType::Bloodstain => damage::bloodstain(ecs, tile_idx), - EffectType::Particle { .. } => particle_to_tile(ecs, tile_idx, effect), + EffectType::Particle { .. } => particles::particle_to_tile(ecs, tile_idx, effect), _ => {} } } @@ -109,8 +140,13 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { } EffectType::Particle { .. } => { if let Some(pos) = entity_position(ecs, target) { - particle_to_tile(ecs, pos, effect) + particles::particle_to_tile(ecs, pos, effect) } } + EffectType::WellFed => hunger::well_fed(ecs, effect, target), + EffectType::Healing { .. } => damage::heal_damage(ecs, effect, target), + EffectType::Confusion { .. } => damage::add_confusion(ecs, effect, target), + EffectType::TeleportTo { .. } => movement::apply_teleport(ecs, effect, target), + _ => {} } } diff --git a/src/effects/damage.rs b/src/effects/damage.rs index 77f28cf..67de0c6 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -2,7 +2,7 @@ use ::rltk::Point; use ::specs::prelude::*; use super::{add_effect, entity_position, EffectSpawner, EffectType, Targets}; -use crate::components::{Attributes, Player, Pools}; +use crate::components::{Attributes, Confusion, Player, Pools}; use crate::gamesystem::{mana_at_level, player_hp_at_level}; use crate::{colors, GameLog, Map}; @@ -107,3 +107,31 @@ pub fn death(ecs: &mut World, effect: &EffectSpawner, target: Entity) { } } } + +pub fn heal_damage(ecs: &mut World, heal: &EffectSpawner, target: Entity) { + let mut pools = ecs.write_storage::(); + if let Some(pool) = pools.get_mut(target) { + if let EffectType::Healing { amount } = heal.effect_type { + pool.hit_points.current = + i32::min(pool.hit_points.max, pool.hit_points.current + amount); + add_effect( + None, + EffectType::Particle { + glyph: ::rltk::to_cp437('‼'), + fg: colors::GREEN, + bg: colors::BLACK, + lifespan: 200.0, + }, + Targets::Single { target }, + ); + } + } +} + +pub fn add_confusion(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + if let EffectType::Confusion { turns } = &effect.effect_type { + ecs.write_storage::() + .insert(target, Confusion { turns: *turns }) + .expect("Unable to make target confused"); + } +} diff --git a/src/effects/hunger.rs b/src/effects/hunger.rs new file mode 100644 index 0000000..96440dc --- /dev/null +++ b/src/effects/hunger.rs @@ -0,0 +1,11 @@ +use ::specs::prelude::*; + +use super::EffectSpawner; +use crate::components::{HungerClock, HungerState}; + +pub fn well_fed(ecs: &mut World, _damage: &EffectSpawner, target: Entity) { + if let Some(hc) = ecs.write_storage::().get_mut(target) { + hc.state = HungerState::WellFed; + hc.duration = 20; + } +} diff --git a/src/effects/movement.rs b/src/effects/movement.rs new file mode 100644 index 0000000..dc8389b --- /dev/null +++ b/src/effects/movement.rs @@ -0,0 +1,29 @@ +use ::specs::prelude::*; + +use super::{EffectSpawner, EffectType}; +use crate::components::ApplyTeleport; + +pub fn apply_teleport(ecs: &mut World, destination: &EffectSpawner, target: Entity) { + let player_entity = ecs.fetch::(); + if let EffectType::TeleportTo { + x, + y, + depth, + player_only, + } = &destination.effect_type + { + if !*player_only || target == *player_entity { + let mut apply_teleport = ecs.write_storage::(); + apply_teleport + .insert( + target, + ApplyTeleport { + dest_x: *x, + dest_y: *y, + dest_depth: *depth, + }, + ) + .expect("Unable to insert intent to teleport"); + } + } +} diff --git a/src/effects/targeting.rs b/src/effects/targeting.rs index f046a05..f3cdbf9 100644 --- a/src/effects/targeting.rs +++ b/src/effects/targeting.rs @@ -12,3 +12,13 @@ pub fn entity_position(ecs: &World, target: Entity) -> Option { None } + +pub fn aoe_tiles(map: &Map, target: ::rltk::Point, radius: i32) -> Vec { + let mut blast_tiles = ::rltk::field_of_view(target, radius, &*map); + blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1); + let mut result = Vec::new(); + for t in blast_tiles.iter() { + result.push(map.xy_idx(t.x, t.y) as i32); + } + result +} diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs new file mode 100644 index 0000000..2dc908c --- /dev/null +++ b/src/effects/triggers.rs @@ -0,0 +1,116 @@ +use ::specs::prelude::*; + +use super::{add_effect, EffectType, Targets}; +use crate::components::{ + Confusion, Consumable, Hidden, InflictsDamage, MagicMapper, Name, ProvidesFood, + ProvidesHealing, SingleActivation, TeleportTo, TownPortal, +}; +use crate::{GameLog, Map, RunState}; + +pub fn item_trigger(creator: Option, item: Entity, targets: &Targets, ecs: &mut World) { + // Use the item via the generic system + event_trigger(creator, item, targets, ecs); + + // If it was a consumable, then it gets deleted + if ecs.read_storage::().get(item).is_some() { + ecs.entities() + .delete(item) + .expect("Failed to delete consumable item"); + } +} + +pub fn trigger(creator: Option, trigger: Entity, targets: &Targets, ecs: &mut World) { + // The triggering item is no longer hidden + ecs.write_storage::().remove(trigger); + + // Use the item via the generic system + event_trigger(creator, trigger, targets, ecs); + + // If it was a single activation, then it gets deleted + if ecs + .read_storage::() + .get(trigger) + .is_some() + { + ecs.entities() + .delete(trigger) + .expect("Failed to delete SingleActivation tag"); + } +} + +fn event_trigger(creator: Option, entity: Entity, targets: &Targets, ecs: &mut World) { + let mut gamelog = ecs.fetch_mut::(); + + // Providing food + if ecs.read_storage::().get(entity).is_some() { + add_effect(creator, EffectType::WellFed, targets.clone()); + let names = ecs.read_storage::(); + gamelog.append(format!("You eat the {}.", names.get(entity).unwrap().name)); + } + + // Magic mapper + if ecs.read_storage::().get(entity).is_some() { + let mut runstate = ecs.fetch_mut::(); + gamelog.append("The map is revealed to you!"); + *runstate = RunState::MagicMapReveal { row: 0 }; + } + + // Town Portal + if ecs.read_storage::().get(entity).is_some() { + let map = ecs.fetch::(); + if map.depth == 1 { + gamelog.append("You are already in town, so the scroll does nothing."); + } else { + gamelog.append("You are teleported back to town!"); + let mut runstate = ecs.fetch_mut::(); + *runstate = RunState::TownPortal; + } + } + + // Healing + if let Some(heal) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::Healing { + amount: heal.heal_amount, + }, + targets.clone(), + ); + } + + // Damage + if let Some(damage) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::Damage { + amount: damage.damage, + }, + targets.clone(), + ); + } + + // Confusion + if let Some(confusion) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::Confusion { + turns: confusion.turns, + }, + targets.clone(), + ); + } + + // Teleport + if let Some(teleport) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::TeleportTo { + x: teleport.x, + y: teleport.y, + depth: teleport.depth, + player_only: teleport.player_only, + }, + targets.clone(), + ); + } +} diff --git a/src/inventory_system/use_system.rs b/src/inventory_system/use_system.rs index ec56c7f..43cd927 100644 --- a/src/inventory_system/use_system.rs +++ b/src/inventory_system/use_system.rs @@ -1,13 +1,8 @@ use ::specs::prelude::*; -use crate::components::{ - AreaOfEffect, Confusion, Consumable, EquipmentChanged, HungerClock, HungerState, - IdentifiedItem, InflictsDamage, MagicMapper, Name, Pools, Position, ProvidesFood, - ProvidesHealing, SufferDamage, TownPortal, WantsToUseItem, -}; -use crate::game_log::GameLog; -use crate::particle_system::ParticleBuilder; -use crate::{colors, spatial, Map, RunState}; +use crate::components::{AreaOfEffect, EquipmentChanged, IdentifiedItem, Name, WantsToUseItem}; +use crate::effects::{add_effect, aoe_tiles, EffectType, Targets}; +use crate::Map; pub struct ItemUseSystem {} @@ -15,26 +10,12 @@ impl<'a> System<'a> for ItemUseSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, - WriteExpect<'a, GameLog>, WriteExpect<'a, Map>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, - ReadStorage<'a, Consumable>, - ReadStorage<'a, ProvidesHealing>, - ReadStorage<'a, InflictsDamage>, - WriteStorage<'a, Pools>, - WriteStorage<'a, SufferDamage>, ReadStorage<'a, AreaOfEffect>, - WriteStorage<'a, Confusion>, - WriteExpect<'a, ParticleBuilder>, - ReadStorage<'a, Position>, - ReadStorage<'a, ProvidesFood>, - WriteStorage<'a, HungerClock>, - ReadStorage<'a, MagicMapper>, - WriteExpect<'a, RunState>, WriteStorage<'a, EquipmentChanged>, - ReadStorage<'a, TownPortal>, WriteStorage<'a, IdentifiedItem>, ); @@ -42,26 +23,12 @@ impl<'a> System<'a> for ItemUseSystem { fn run(&mut self, data: Self::SystemData) { let ( player_entity, - mut gamelog, map, entities, mut wants_use, names, - consumables, - healing, - inflict_damage, - mut combat_stats, - mut suffer_damage, aoe, - mut confused, - mut particle_builder, - positions, - provides_food, - mut hunger_clocks, - magic_mapper, - mut runstate, mut dirty, - town_portal, mut identified_item, ) = data; @@ -69,46 +36,6 @@ impl<'a> System<'a> for ItemUseSystem { dirty .insert(entity, EquipmentChanged {}) .expect("Unable to insert equipment change"); - let mut used_item = true; - - // Targeting - let mut targets: Vec = Vec::new(); - match useitem.target { - None => { - targets.push(*player_entity); - } - Some(target) => { - match aoe.get(useitem.item) { - None => { - // Single target in tile - let idx = map.xy_idx(target.x, target.y); - spatial::for_each_tile_content(idx, |mob| targets.push(mob)); - } - Some(area_effect) => { - // AoE - let mut blast_tiles = - rltk::field_of_view(target, area_effect.radius, &*map); - blast_tiles.retain(|p| { - p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1 - }); - - for tile_idx in blast_tiles.iter() { - let idx = map.xy_idx(tile_idx.x, tile_idx.y); - spatial::for_each_tile_content(idx, |mob| targets.push(mob)); - - particle_builder.request( - tile_idx.x, - tile_idx.y, - colors::ORANGE, - colors::BLACK, - rltk::to_cp437('░'), - 200.0, - ); - } - } - } - } - } // Identify if entity == *player_entity { @@ -122,173 +49,27 @@ impl<'a> System<'a> for ItemUseSystem { .expect("Unable to identify item"); } - // If it is edible, eat it! - match provides_food.get(useitem.item) { - None => {} - Some(_) => { - used_item = true; - let target = targets[0]; - - if let Some(hc) = hunger_clocks.get_mut(target) { - hc.state = HungerState::WellFed; - hc.duration = 20; - - gamelog.append(format!( - "You eat the {}.", - names.get(useitem.item).unwrap().name - )); - } - } - } - - // If its a magic mapper... - match magic_mapper.get(useitem.item) { - None => {} - Some(_) => { - used_item = true; - gamelog.append("The map is revealed to you!"); - - *runstate = RunState::MagicMapReveal { row: 0 }; - } - } - - // If it's a town portal... - if town_portal.get(useitem.item).is_some() { - if map.depth == 1 { - gamelog.append("You are already in town, so the scroll does nothing"); - } else { - used_item = true; - gamelog.append("You are teleported back to town!"); - - *runstate = RunState::TownPortal; - } - } - - // If the item heals, apply the healing - match healing.get(useitem.item) { - None => {} - Some(healer) => { - used_item = false; - - for target in targets.iter() { - if let Some(stats) = combat_stats.get_mut(*target) { - stats.hit_points.current = i32::min( - stats.hit_points.max, - stats.hit_points.current + healer.heal_amount, - ); - if entity == *player_entity { - gamelog.append(format!( - "You drink the {}, healing {} hp.", - names.get(useitem.item).unwrap().name, - healer.heal_amount - )); + // Call the effects system + add_effect( + Some(entity), + EffectType::ItemUse { item: useitem.item }, + match useitem.target { + None => Targets::Single { + target: *player_entity, + }, + Some(target) => { + if let Some(aoe) = aoe.get(useitem.item) { + Targets::Tiles { + tiles: aoe_tiles(&*map, target, aoe.radius), } - - used_item = true; - - // Visually show healing - if let Some(pos) = positions.get(*target) { - particle_builder.request( - pos.x, - pos.y, - colors::GREEN, - colors::BLACK, - rltk::to_cp437('♥'), - 200.0, - ); + } else { + Targets::Tile { + tile_idx: map.xy_idx(target.x, target.y) as i32, } } } - } - } - - // If it inflicts damage, apply it to the target cell - match inflict_damage.get(useitem.item) { - None => {} - Some(damage) => { - used_item = false; - - for mob in targets.iter() { - SufferDamage::new_damage(&mut suffer_damage, *mob, damage.damage, true); - if entity == *player_entity { - let mob_name = names.get(*mob).unwrap(); - let item_name = names.get(useitem.item).unwrap(); - - gamelog.append(format!( - "You use {} on {}, inflicting {} hp.", - item_name.name, mob_name.name, damage.damage - )); - - if let Some(pos) = positions.get(*mob) { - particle_builder.request( - pos.x, - pos.y, - colors::RED, - colors::BLACK, - rltk::to_cp437('‼'), - 200.0, - ); - } - } - - used_item = true; - } - } - } - - // Can it pass along confusion? Note the use of scopes - // to escape from the borrow checker! - let mut add_confusion = Vec::new(); - { - match confused.get(useitem.item) { - None => {} - Some(confusion) => { - used_item = false; - - for mob in targets.iter() { - add_confusion.push((*mob, confusion.turns)); - if entity == *player_entity { - let mob_name = names.get(*mob).unwrap(); - let item_name = names.get(useitem.item).unwrap(); - - gamelog.append(format!( - "You use {} on {}, confusing them.", - item_name.name, mob_name.name - )); - - if let Some(pos) = positions.get(*mob) { - particle_builder.request( - pos.x, - pos.y, - colors::MAGENTA, - colors::BLACK, - rltk::to_cp437('?'), - 200.0, - ); - } - } - } - } - } - } - for mob in add_confusion.iter() { - confused - .insert(mob.0, Confusion { turns: mob.1 }) - .expect("Unable to add confused status"); - } - - // If it's a consumable, delete it on use - if used_item { - let consumable = consumables.get(useitem.item); - match consumable { - None => {} - Some(_) => { - entities - .delete(useitem.item) - .expect("Failed to consume item"); - } - } - } + }, + ); } wants_use.clear(); diff --git a/src/trigger_system.rs b/src/trigger_system.rs index 0ef63bf..5cc1144 100644 --- a/src/trigger_system.rs +++ b/src/trigger_system.rs @@ -1,13 +1,9 @@ use ::specs::prelude::*; -use crate::components::{ - ApplyTeleport, EntityMoved, EntryTrigger, Hidden, InflictsDamage, Name, Position, - SingleActivation, TeleportTo, -}; -use crate::effects::{add_effect, EffectType, Targets}; +use crate::components::{AreaOfEffect, EntityMoved, EntryTrigger, Name, Position}; +use crate::effects::{add_effect, aoe_tiles, EffectType, Targets}; use crate::game_log::GameLog; -use crate::particle_system::ParticleBuilder; -use crate::{colors, spatial, Map}; +use crate::{spatial, Map}; pub struct TriggerSystem {} @@ -18,16 +14,10 @@ impl<'a> System<'a> for TriggerSystem { WriteStorage<'a, EntityMoved>, ReadStorage<'a, Position>, ReadStorage<'a, EntryTrigger>, - WriteStorage<'a, Hidden>, ReadStorage<'a, Name>, Entities<'a>, WriteExpect<'a, GameLog>, - ReadStorage<'a, InflictsDamage>, - WriteExpect<'a, ParticleBuilder>, - ReadStorage<'a, SingleActivation>, - ReadStorage<'a, TeleportTo>, - WriteStorage<'a, ApplyTeleport>, - ReadExpect<'a, Entity>, + ReadStorage<'a, AreaOfEffect>, ); fn run(&mut self, data: Self::SystemData) { @@ -36,20 +26,13 @@ impl<'a> System<'a> for TriggerSystem { mut entity_moved, position, entry_trigger, - mut hidden, names, entities, mut log, - inflicts_damage, - mut particle_builder, - single_activation, - teleporters, - mut apply_teleport, - player_entity, + area_of_effect, ) = data; // Iterate the entities that moved and their final position - let mut remove_entities: Vec = Vec::new(); for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() { let idx = map.xy_idx(pos.x, pos.y); @@ -64,62 +47,30 @@ impl<'a> System<'a> for TriggerSystem { log.append(format!("{} triggers!", &name.name)); } - // The trap is no longer hidden - hidden.remove(entity_id); - - // If the trap is damage inflicting, do it - if let Some(damage) = inflicts_damage.get(entity_id) { - particle_builder.request( - pos.x, - pos.y, - colors::ORANGE, - colors::BLACK, - rltk::to_cp437('‼'), - 200.0, - ); - - add_effect( - None, - EffectType::Damage { - amount: damage.damage, - }, - Targets::Single { target: entity }, - ) - } - - // If it's a teleporter, then do that - if let Some(teleport) = teleporters.get(entity_id) { - if !teleport.player_only || entity == *player_entity { - apply_teleport - .insert( - entity, - ApplyTeleport { - dest_x: teleport.x, - dest_y: teleport.y, - dest_depth: teleport.depth, - }, - ) - .expect("Unable to insert intent to teleport"); - } - } - - // If it is single activation, it needs to be removed - if single_activation.get(entity_id).is_some() { - remove_entities.push(entity_id); - } + // Call the effects system + add_effect( + Some(entity), + EffectType::TriggerFire { trigger: entity_id }, + if let Some(aoe) = area_of_effect.get(entity_id) { + Targets::Tiles { + tiles: aoe_tiles( + &*map, + ::rltk::Point::from(*pos), + aoe.radius, + ), + } + } else { + Targets::Tile { + tile_idx: idx as i32, + } + }, + ) } } } }); } - // Remove any single activation traps - for trap in remove_entities.iter() { - entities - .delete(*trap) - .expect("Unable to remove single activation entity"); - } - // Remove all entity movement markers entity_moved.clear(); }