Create effects module to handle damage, particles, and death more modularly

This commit is contained in:
Timothy Warren 2022-01-20 11:48:58 -05:00
parent d6c9341569
commit 6f35a4a933
9 changed files with 320 additions and 66 deletions

116
src/effects.rs Normal file
View File

@ -0,0 +1,116 @@
mod damage;
mod particles;
mod targeting;
use std::collections::VecDeque;
use std::sync::Mutex;
use ::rltk::{FontCharType, RGB};
use ::specs::prelude::*;
pub use targeting::*;
use crate::effects::particles::particle_to_tile;
use crate::spatial;
lazy_static! {
pub static ref EFFECT_QUEUE: Mutex<VecDeque<EffectSpawner>> = Mutex::new(VecDeque::new());
}
pub enum EffectType {
Damage {
amount: i32,
},
Bloodstain,
Particle {
glyph: FontCharType,
fg: RGB,
bg: RGB,
lifespan: f32,
},
EntityDeath,
}
#[derive(Clone)]
#[allow(dead_code)]
pub enum Targets {
Single { target: Entity },
TargetList { targets: Vec<Entity> },
Tile { tile_idx: i32 },
Tiles { tiles: Vec<i32> },
}
pub struct EffectSpawner {
pub creator: Option<Entity>,
pub effect_type: EffectType,
pub targets: Targets,
}
pub fn add_effect(creator: Option<Entity>, effect_type: EffectType, targets: Targets) {
EFFECT_QUEUE.lock().unwrap().push_back(EffectSpawner {
creator,
effect_type,
targets,
});
}
pub fn run_effects_queue(ecs: &mut World) {
loop {
let effect = EFFECT_QUEUE.lock().unwrap().pop_front();
if let Some(effect) = effect {
target_applicator(ecs, &effect);
} else {
break;
}
}
}
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)),
}
}
fn tile_effect_hits_entities(effect: &EffectType) -> bool {
match effect {
EffectType::Damage { .. } => true,
_ => false,
}
}
fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx: i32) {
if tile_effect_hits_entities(&effect.effect_type) {
spatial::for_each_tile_content(tile_idx as usize, |entity| {
affect_entity(ecs, effect, entity)
});
}
match &effect.effect_type {
EffectType::Bloodstain => damage::bloodstain(ecs, tile_idx),
EffectType::Particle { .. } => particle_to_tile(ecs, tile_idx, effect),
_ => {}
}
}
fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
match &effect.effect_type {
EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target),
EffectType::EntityDeath => damage::death(ecs, effect, target),
EffectType::Bloodstain { .. } => {
if let Some(pos) = entity_position(ecs, target) {
damage::bloodstain(ecs, pos)
}
}
EffectType::Particle { .. } => {
if let Some(pos) = entity_position(ecs, target) {
particle_to_tile(ecs, pos, effect)
}
}
}
}

109
src/effects/damage.rs Normal file
View File

@ -0,0 +1,109 @@
use ::rltk::Point;
use ::specs::prelude::*;
use super::{add_effect, entity_position, EffectSpawner, EffectType, Targets};
use crate::components::{Attributes, Player, Pools};
use crate::gamesystem::{mana_at_level, player_hp_at_level};
use crate::{colors, GameLog, Map};
pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) {
let mut pools = ecs.write_storage::<Pools>();
if let Some(pool) = pools.get_mut(target) {
if !pool.god_mode {
if let EffectType::Damage { amount } = damage.effect_type {
pool.hit_points.current -= amount;
add_effect(None, EffectType::Bloodstain, Targets::Single { target });
add_effect(
None,
EffectType::Particle {
glyph: rltk::to_cp437('‼'),
fg: colors::ORANGE,
bg: colors::BLACK,
lifespan: 200.0,
},
Targets::Single { target },
);
if pool.hit_points.current < 1 {
add_effect(
damage.creator,
EffectType::EntityDeath,
Targets::Single { target },
);
}
}
}
}
}
pub fn bloodstain(ecs: &mut World, tile_idx: i32) {
let mut map = ecs.fetch_mut::<Map>();
map.bloodstains.insert(tile_idx as usize);
}
pub fn death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
let mut xp_gain = 0;
let mut gold_gain = 0.0f32;
let mut pools = ecs.write_storage::<Pools>();
let attributes = ecs.read_storage::<Attributes>();
let map = ecs.fetch_mut::<Map>();
if let Some(pos) = entity_position(ecs, target) {
crate::spatial::remove_entity(target, pos as usize);
}
if let Some(source) = effect.creator {
if ecs.read_storage::<Player>().get(source).is_some() {
if let Some(stats) = pools.get(target) {
xp_gain += stats.level * 100;
gold_gain += stats.gold;
}
if xp_gain != 0 || gold_gain != 0.0 {
let mut log = ecs.fetch_mut::<GameLog>();
let mut player_stats = pools.get_mut(source).unwrap();
let player_attributes = attributes.get(source).unwrap();
player_stats.xp += xp_gain;
player_stats.gold += gold_gain;
if player_stats.xp >= player_stats.level * 1000 {
// We've gone up a level!
player_stats.level += 1;
log.append(format!(
"Congratulations, you are now level {}",
player_stats.level
));
player_stats.hit_points.max = player_hp_at_level(
player_attributes.fitness.base + player_attributes.fitness.modifiers,
player_stats.level,
);
player_stats.hit_points.current = player_stats.hit_points.max;
player_stats.mana.max = mana_at_level(
player_attributes.intelligence.base
+ player_attributes.intelligence.modifiers,
player_stats.level,
);
player_stats.mana.current = player_stats.mana.max;
let player_pos = ecs.fetch::<Point>();
for i in 0..10 {
if player_pos.y - i > 1 {
add_effect(
None,
EffectType::Particle {
glyph: ::rltk::to_cp437('░'),
fg: colors::GOLD,
bg: colors::BLACK,
lifespan: 400.0,
},
Targets::Tile {
tile_idx: map.xy_idx(player_pos.x, player_pos.y - i) as i32,
},
);
}
}
}
}
}
}
}

26
src/effects/particles.rs Normal file
View File

@ -0,0 +1,26 @@
use ::specs::prelude::*;
use super::{EffectSpawner, EffectType};
use crate::map::Map;
use crate::particle_system::ParticleBuilder;
pub fn particle_to_tile(ecs: &mut World, tile_idx: i32, effect: &EffectSpawner) {
if let EffectType::Particle {
glyph,
fg,
bg,
lifespan,
} = effect.effect_type
{
let map = ecs.fetch::<Map>();
let mut particle_builder = ecs.fetch_mut::<ParticleBuilder>();
particle_builder.request(
tile_idx % map.width,
tile_idx / map.width,
fg,
bg,
glyph,
lifespan,
);
}
}

14
src/effects/targeting.rs Normal file
View File

@ -0,0 +1,14 @@
use ::specs::prelude::*;
use crate::components::Position;
use crate::map::Map;
pub fn entity_position(ecs: &World, target: Entity) -> Option<i32> {
if let Some(pos) = ecs.read_storage::<Position>().get(target) {
let map = ecs.fetch::<Map>();
return Some(map.xy_idx(pos.x, pos.y) as i32);
}
None
}

View File

@ -2,8 +2,8 @@ use ::specs::prelude::*;
use crate::components::{ use crate::components::{
AreaOfEffect, Confusion, Consumable, EquipmentChanged, Equippable, Equipped, HungerClock, AreaOfEffect, Confusion, Consumable, EquipmentChanged, Equippable, Equipped, HungerClock,
HungerState, IdentifiedItem, InBackpack, InflictsDamage, MagicMapper, Name, Pools, HungerState, IdentifiedItem, InBackpack, InflictsDamage, MagicMapper, Name, Pools, Position,
Position, ProvidesFood, ProvidesHealing, SufferDamage, TownPortal, WantsToUseItem, ProvidesFood, ProvidesHealing, SufferDamage, TownPortal, WantsToUseItem,
}; };
use crate::game_log::GameLog; use crate::game_log::GameLog;
use crate::particle_system::ParticleBuilder; use crate::particle_system::ParticleBuilder;

View File

@ -1,8 +1,12 @@
#[macro_use]
extern crate lazy_static;
mod ai; mod ai;
pub mod camera; pub mod camera;
mod colors; mod colors;
mod components; mod components;
mod damage_system; mod damage_system;
mod effects;
mod game_log; mod game_log;
mod gamesystem; mod gamesystem;
mod gui; mod gui;
@ -16,20 +20,17 @@ mod melee_combat_system;
mod movement_system; mod movement_system;
mod particle_system; mod particle_system;
mod player; mod player;
pub mod random_table; mod random_table;
pub mod raws; mod raws;
mod rect; mod rect;
mod rex_assets; mod rex_assets;
pub mod saveload_system; mod saveload_system;
mod spatial; mod spatial;
mod spawner; mod spawner;
mod state; mod state;
mod trigger_system; mod trigger_system;
mod visibility_system; mod visibility_system;
#[macro_use]
extern crate lazy_static;
use ::specs::prelude::*; use ::specs::prelude::*;
use ::specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; use ::specs::saveload::{SimpleMarker, SimpleMarkerAllocator};
use components::*; use components::*;

View File

@ -1,14 +1,14 @@
use ::rltk::RandomNumberGenerator; use ::rltk::RandomNumberGenerator;
use ::specs::prelude::*; use ::specs::prelude::*;
use crate::colors;
use crate::components::{ use crate::components::{
Attributes, Equipped, HungerClock, HungerState, MeleeWeapon, Name, Pools, Skill, Skills, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon, Name,
SufferDamage, WantsToMelee, Wearable, NaturalAttackDefense, Pools, Skill, Skills, WantsToMelee, WeaponAttribute, Wearable,
}; };
use crate::effects::{add_effect, EffectType, Targets};
use crate::game_log::GameLog; use crate::game_log::GameLog;
use crate::gamesystem::skill_bonus; use crate::gamesystem::skill_bonus;
use crate::particle_system::ParticleBuilder;
use crate::{colors, EquipmentSlot, NaturalAttackDefense, Position, WeaponAttribute};
pub struct MeleeCombatSystem {} pub struct MeleeCombatSystem {}
@ -21,9 +21,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
ReadStorage<'a, Attributes>, ReadStorage<'a, Attributes>,
ReadStorage<'a, Skills>, ReadStorage<'a, Skills>,
WriteStorage<'a, SufferDamage>,
WriteExpect<'a, ParticleBuilder>,
ReadStorage<'a, Position>,
ReadStorage<'a, HungerClock>, ReadStorage<'a, HungerClock>,
ReadStorage<'a, Pools>, ReadStorage<'a, Pools>,
WriteExpect<'a, RandomNumberGenerator>, WriteExpect<'a, RandomNumberGenerator>,
@ -31,7 +28,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadStorage<'a, MeleeWeapon>, ReadStorage<'a, MeleeWeapon>,
ReadStorage<'a, Wearable>, ReadStorage<'a, Wearable>,
ReadStorage<'a, NaturalAttackDefense>, ReadStorage<'a, NaturalAttackDefense>,
ReadExpect<'a, Entity>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
@ -42,9 +38,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
names, names,
attributes, attributes,
skills, skills,
mut inflict_damage,
mut particle_builder,
positions,
hunger_clock, hunger_clock,
pools, pools,
mut rng, mut rng,
@ -52,7 +45,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
meleeweapons, meleeweapons,
wearables, wearables,
natural, natural,
player_entity,
) = data; ) = data;
for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in ( for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in (
@ -156,59 +148,53 @@ impl<'a> System<'a> for MeleeCombatSystem {
+ skill_damage_bonus + skill_damage_bonus
+ weapon_damage_bonus, + weapon_damage_bonus,
); );
SufferDamage::new_damage( add_effect(
&mut inflict_damage, Some(entity),
wants_melee.target, EffectType::Damage { amount: damage },
damage, Targets::Single {
entity == *player_entity, target: wants_melee.target,
},
); );
log.append(format!( log.append(format!(
"{} hits {} for {} hp.", "{} hits {} for {} hp.",
&name.name, &target_name.name, damage &name.name, &target_name.name, damage
)); ));
if let Some(pos) = positions.get(wants_melee.target) {
particle_builder.request(
pos.x,
pos.y,
colors::ORANGE,
colors::BLACK,
rltk::to_cp437('‼'),
200.0,
);
}
} else if natural_roll == 1 { } else if natural_roll == 1 {
// Natural 1 miss // Natural 1 miss
log.append(format!( log.append(format!(
"{} considers attacking {}, but misjudges the timing.", "{} considers attacking {}, but misjudges the timing.",
name.name, target_name.name name.name, target_name.name
)); ));
if let Some(pos) = positions.get(wants_melee.target) { add_effect(
particle_builder.request( None,
pos.x, EffectType::Particle {
pos.y, glyph: rltk::to_cp437('‼'),
colors::BLUE, fg: colors::BLUE,
colors::BLACK, bg: colors::BLACK,
rltk::to_cp437('‼'), lifespan: 200.0,
200.0, },
); Targets::Single {
} target: wants_melee.target,
},
);
} else { } else {
// Miss // Miss
log.append(format!( log.append(format!(
"{} attacks {}, but can't connect", "{} attacks {}, but can't connect",
name.name, target_name.name name.name, target_name.name
)); ));
if let Some(pos) = positions.get(wants_melee.target) { add_effect(
particle_builder.request( None,
pos.x, EffectType::Particle {
pos.y, glyph: rltk::to_cp437('‼'),
colors::CYAN, fg: colors::CYAN,
colors::BLACK, bg: colors::BLACK,
rltk::to_cp437('‼'), lifespan: 200.0,
200.0, },
); Targets::Single {
} target: wants_melee.target,
},
);
} }
} }
} }

View File

@ -19,7 +19,7 @@ use crate::player::*;
use crate::raws::*; use crate::raws::*;
use crate::trigger_system::TriggerSystem; use crate::trigger_system::TriggerSystem;
use crate::visibility_system::VisibilitySystem; use crate::visibility_system::VisibilitySystem;
use crate::{ai, camera, saveload_system, spawner}; use crate::{ai, camera, effects, saveload_system, spawner};
pub const SHOW_MAPGEN_VISUALIZER: bool = false; pub const SHOW_MAPGEN_VISUALIZER: bool = false;
@ -139,6 +139,8 @@ impl State {
let mut hunger = HungerSystem {}; let mut hunger = HungerSystem {};
hunger.run_now(&self.ecs); hunger.run_now(&self.ecs);
effects::run_effects_queue(&mut self.ecs);
let mut particles = ParticleSpawnSystem {}; let mut particles = ParticleSpawnSystem {};
particles.run_now(&self.ecs); particles.run_now(&self.ecs);

View File

@ -2,8 +2,9 @@ use ::specs::prelude::*;
use crate::components::{ use crate::components::{
ApplyTeleport, EntityMoved, EntryTrigger, Hidden, InflictsDamage, Name, Position, ApplyTeleport, EntityMoved, EntryTrigger, Hidden, InflictsDamage, Name, Position,
SingleActivation, SufferDamage, TeleportTo, 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::particle_system::ParticleBuilder;
use crate::{colors, spatial, Map}; use crate::{colors, spatial, Map};
@ -23,7 +24,6 @@ impl<'a> System<'a> for TriggerSystem {
WriteExpect<'a, GameLog>, WriteExpect<'a, GameLog>,
ReadStorage<'a, InflictsDamage>, ReadStorage<'a, InflictsDamage>,
WriteExpect<'a, ParticleBuilder>, WriteExpect<'a, ParticleBuilder>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, SingleActivation>, ReadStorage<'a, SingleActivation>,
ReadStorage<'a, TeleportTo>, ReadStorage<'a, TeleportTo>,
WriteStorage<'a, ApplyTeleport>, WriteStorage<'a, ApplyTeleport>,
@ -42,7 +42,6 @@ impl<'a> System<'a> for TriggerSystem {
mut log, mut log,
inflicts_damage, inflicts_damage,
mut particle_builder, mut particle_builder,
mut inflict_damage,
single_activation, single_activation,
teleporters, teleporters,
mut apply_teleport, mut apply_teleport,
@ -79,12 +78,13 @@ impl<'a> System<'a> for TriggerSystem {
200.0, 200.0,
); );
SufferDamage::new_damage( add_effect(
&mut inflict_damage, None,
entity, EffectType::Damage {
damage.damage, amount: damage.damage,
false, },
); Targets::Single { target: entity },
)
} }
// If it's a teleporter, then do that // If it's a teleporter, then do that