Simplify ItemUseSystem and TriggerSystem by refactoring into piecs of the effect module

This commit is contained in:
Timothy Warren 2022-01-20 15:57:22 -05:00
parent 0f6755b35f
commit 0f3903e456
8 changed files with 290 additions and 328 deletions

View File

@ -1,6 +1,9 @@
mod damage; mod damage;
mod hunger;
mod movement;
mod particles; mod particles;
mod targeting; mod targeting;
mod triggers;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Mutex; use std::sync::Mutex;
@ -9,7 +12,6 @@ use ::rltk::{FontCharType, RGB};
use ::specs::prelude::*; use ::specs::prelude::*;
pub use targeting::*; pub use targeting::*;
use crate::effects::particles::particle_to_tile;
use crate::spatial; use crate::spatial;
lazy_static! { lazy_static! {
@ -28,6 +30,25 @@ pub enum EffectType {
lifespan: f32, lifespan: f32,
}, },
EntityDeath, 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)] #[derive(Clone)]
@ -65,6 +86,11 @@ pub fn run_effects_queue(ecs: &mut World) {
} }
fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { fn target_applicator(ecs: &mut World, effect: &EffectSpawner) {
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 { match &effect.targets {
Targets::Tile { tile_idx } => affect_tile(ecs, effect, *tile_idx), Targets::Tile { tile_idx } => affect_tile(ecs, effect, *tile_idx),
Targets::Tiles { tiles } => tiles Targets::Tiles { tiles } => tiles
@ -75,13 +101,18 @@ fn target_applicator(ecs: &mut World, effect: &EffectSpawner) {
.iter() .iter()
.for_each(|entity| affect_entity(ecs, effect, *entity)), .for_each(|entity| affect_entity(ecs, effect, *entity)),
} }
}
} }
fn tile_effect_hits_entities(effect: &EffectType) -> bool { fn tile_effect_hits_entities(effect: &EffectType) -> bool {
match effect { matches!(
EffectType::Damage { .. } => true, effect,
_ => false, EffectType::Damage { .. }
} | EffectType::WellFed
| EffectType::Healing { .. }
| EffectType::Confusion { .. }
| EffectType::TeleportTo { .. }
)
} }
fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx: i32) { 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 { match &effect.effect_type {
EffectType::Bloodstain => damage::bloodstain(ecs, tile_idx), 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 { .. } => { EffectType::Particle { .. } => {
if let Some(pos) = entity_position(ecs, target) { 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),
_ => {}
} }
} }

View File

@ -2,7 +2,7 @@ use ::rltk::Point;
use ::specs::prelude::*; use ::specs::prelude::*;
use super::{add_effect, entity_position, EffectSpawner, EffectType, Targets}; 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::gamesystem::{mana_at_level, player_hp_at_level};
use crate::{colors, GameLog, Map}; 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::<Pools>();
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::<Confusion>()
.insert(target, Confusion { turns: *turns })
.expect("Unable to make target confused");
}
}

11
src/effects/hunger.rs Normal file
View File

@ -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::<HungerClock>().get_mut(target) {
hc.state = HungerState::WellFed;
hc.duration = 20;
}
}

29
src/effects/movement.rs Normal file
View File

@ -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::<Entity>();
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::<ApplyTeleport>();
apply_teleport
.insert(
target,
ApplyTeleport {
dest_x: *x,
dest_y: *y,
dest_depth: *depth,
},
)
.expect("Unable to insert intent to teleport");
}
}
}

View File

@ -12,3 +12,13 @@ pub fn entity_position(ecs: &World, target: Entity) -> Option<i32> {
None None
} }
pub fn aoe_tiles(map: &Map, target: ::rltk::Point, radius: i32) -> Vec<i32> {
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
}

116
src/effects/triggers.rs Normal file
View File

@ -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<Entity>, 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::<Consumable>().get(item).is_some() {
ecs.entities()
.delete(item)
.expect("Failed to delete consumable item");
}
}
pub fn trigger(creator: Option<Entity>, trigger: Entity, targets: &Targets, ecs: &mut World) {
// The triggering item is no longer hidden
ecs.write_storage::<Hidden>().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::<SingleActivation>()
.get(trigger)
.is_some()
{
ecs.entities()
.delete(trigger)
.expect("Failed to delete SingleActivation tag");
}
}
fn event_trigger(creator: Option<Entity>, entity: Entity, targets: &Targets, ecs: &mut World) {
let mut gamelog = ecs.fetch_mut::<GameLog>();
// Providing food
if ecs.read_storage::<ProvidesFood>().get(entity).is_some() {
add_effect(creator, EffectType::WellFed, targets.clone());
let names = ecs.read_storage::<Name>();
gamelog.append(format!("You eat the {}.", names.get(entity).unwrap().name));
}
// Magic mapper
if ecs.read_storage::<MagicMapper>().get(entity).is_some() {
let mut runstate = ecs.fetch_mut::<RunState>();
gamelog.append("The map is revealed to you!");
*runstate = RunState::MagicMapReveal { row: 0 };
}
// Town Portal
if ecs.read_storage::<TownPortal>().get(entity).is_some() {
let map = ecs.fetch::<Map>();
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 = RunState::TownPortal;
}
}
// Healing
if let Some(heal) = ecs.read_storage::<ProvidesHealing>().get(entity) {
add_effect(
creator,
EffectType::Healing {
amount: heal.heal_amount,
},
targets.clone(),
);
}
// Damage
if let Some(damage) = ecs.read_storage::<InflictsDamage>().get(entity) {
add_effect(
creator,
EffectType::Damage {
amount: damage.damage,
},
targets.clone(),
);
}
// Confusion
if let Some(confusion) = ecs.read_storage::<Confusion>().get(entity) {
add_effect(
creator,
EffectType::Confusion {
turns: confusion.turns,
},
targets.clone(),
);
}
// Teleport
if let Some(teleport) = ecs.read_storage::<TeleportTo>().get(entity) {
add_effect(
creator,
EffectType::TeleportTo {
x: teleport.x,
y: teleport.y,
depth: teleport.depth,
player_only: teleport.player_only,
},
targets.clone(),
);
}
}

View File

@ -1,13 +1,8 @@
use ::specs::prelude::*; use ::specs::prelude::*;
use crate::components::{ use crate::components::{AreaOfEffect, EquipmentChanged, IdentifiedItem, Name, WantsToUseItem};
AreaOfEffect, Confusion, Consumable, EquipmentChanged, HungerClock, HungerState, use crate::effects::{add_effect, aoe_tiles, EffectType, Targets};
IdentifiedItem, InflictsDamage, MagicMapper, Name, Pools, Position, ProvidesFood, use crate::Map;
ProvidesHealing, SufferDamage, TownPortal, WantsToUseItem,
};
use crate::game_log::GameLog;
use crate::particle_system::ParticleBuilder;
use crate::{colors, spatial, Map, RunState};
pub struct ItemUseSystem {} pub struct ItemUseSystem {}
@ -15,26 +10,12 @@ impl<'a> System<'a> for ItemUseSystem {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
type SystemData = ( type SystemData = (
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>,
WriteExpect<'a, Map>, WriteExpect<'a, Map>,
Entities<'a>, Entities<'a>,
WriteStorage<'a, WantsToUseItem>, WriteStorage<'a, WantsToUseItem>,
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
ReadStorage<'a, Consumable>,
ReadStorage<'a, ProvidesHealing>,
ReadStorage<'a, InflictsDamage>,
WriteStorage<'a, Pools>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, AreaOfEffect>, 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>, WriteStorage<'a, EquipmentChanged>,
ReadStorage<'a, TownPortal>,
WriteStorage<'a, IdentifiedItem>, WriteStorage<'a, IdentifiedItem>,
); );
@ -42,26 +23,12 @@ impl<'a> System<'a> for ItemUseSystem {
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let ( let (
player_entity, player_entity,
mut gamelog,
map, map,
entities, entities,
mut wants_use, mut wants_use,
names, names,
consumables,
healing,
inflict_damage,
mut combat_stats,
mut suffer_damage,
aoe, aoe,
mut confused,
mut particle_builder,
positions,
provides_food,
mut hunger_clocks,
magic_mapper,
mut runstate,
mut dirty, mut dirty,
town_portal,
mut identified_item, mut identified_item,
) = data; ) = data;
@ -69,46 +36,6 @@ impl<'a> System<'a> for ItemUseSystem {
dirty dirty
.insert(entity, EquipmentChanged {}) .insert(entity, EquipmentChanged {})
.expect("Unable to insert equipment change"); .expect("Unable to insert equipment change");
let mut used_item = true;
// Targeting
let mut targets: Vec<Entity> = 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 // Identify
if entity == *player_entity { if entity == *player_entity {
@ -122,173 +49,27 @@ impl<'a> System<'a> for ItemUseSystem {
.expect("Unable to identify item"); .expect("Unable to identify item");
} }
// If it is edible, eat it! // Call the effects system
match provides_food.get(useitem.item) { add_effect(
None => {} Some(entity),
Some(_) => { EffectType::ItemUse { item: useitem.item },
used_item = true; match useitem.target {
let target = targets[0]; None => Targets::Single {
target: *player_entity,
if let Some(hc) = hunger_clocks.get_mut(target) { },
hc.state = HungerState::WellFed; Some(target) => {
hc.duration = 20; if let Some(aoe) = aoe.get(useitem.item) {
Targets::Tiles {
gamelog.append(format!( tiles: aoe_tiles(&*map, target, aoe.radius),
"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 { } else {
used_item = true; Targets::Tile {
gamelog.append("You are teleported back to town!"); tile_idx: map.xy_idx(target.x, target.y) as i32,
*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
));
}
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,
);
}
}
}
}
}
// 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(); wants_use.clear();

View File

@ -1,13 +1,9 @@
use ::specs::prelude::*; use ::specs::prelude::*;
use crate::components::{ use crate::components::{AreaOfEffect, EntityMoved, EntryTrigger, Name, Position};
ApplyTeleport, EntityMoved, EntryTrigger, Hidden, InflictsDamage, Name, Position, use crate::effects::{add_effect, aoe_tiles, EffectType, Targets};
SingleActivation, TeleportTo,
};
use crate::effects::{add_effect, EffectType, Targets};
use crate::game_log::GameLog; use crate::game_log::GameLog;
use crate::particle_system::ParticleBuilder; use crate::{spatial, Map};
use crate::{colors, spatial, Map};
pub struct TriggerSystem {} pub struct TriggerSystem {}
@ -18,16 +14,10 @@ impl<'a> System<'a> for TriggerSystem {
WriteStorage<'a, EntityMoved>, WriteStorage<'a, EntityMoved>,
ReadStorage<'a, Position>, ReadStorage<'a, Position>,
ReadStorage<'a, EntryTrigger>, ReadStorage<'a, EntryTrigger>,
WriteStorage<'a, Hidden>,
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
Entities<'a>, Entities<'a>,
WriteExpect<'a, GameLog>, WriteExpect<'a, GameLog>,
ReadStorage<'a, InflictsDamage>, ReadStorage<'a, AreaOfEffect>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, SingleActivation>,
ReadStorage<'a, TeleportTo>,
WriteStorage<'a, ApplyTeleport>,
ReadExpect<'a, Entity>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
@ -36,20 +26,13 @@ impl<'a> System<'a> for TriggerSystem {
mut entity_moved, mut entity_moved,
position, position,
entry_trigger, entry_trigger,
mut hidden,
names, names,
entities, entities,
mut log, mut log,
inflicts_damage, area_of_effect,
mut particle_builder,
single_activation,
teleporters,
mut apply_teleport,
player_entity,
) = data; ) = data;
// Iterate the entities that moved and their final position // Iterate the entities that moved and their final position
let mut remove_entities: Vec<Entity> = Vec::new();
for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() { for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() {
let idx = map.xy_idx(pos.x, pos.y); 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)); log.append(format!("{} triggers!", &name.name));
} }
// The trap is no longer hidden // Call the effects system
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( add_effect(
None, Some(entity),
EffectType::Damage { EffectType::TriggerFire { trigger: entity_id },
amount: damage.damage, if let Some(aoe) = area_of_effect.get(entity_id) {
}, Targets::Tiles {
Targets::Single { target: entity }, tiles: aoe_tiles(
) &*map,
::rltk::Point::from(*pos),
aoe.radius,
),
}
} else {
Targets::Tile {
tile_idx: idx as i32,
} }
// 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);
}
} }
} }
} }
}); });
} }
// 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 // Remove all entity movement markers
entity_moved.clear(); entity_moved.clear();
} }