From 6f35a4a933a2eb71b675fca6334fe29f9c9c082c Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Thu, 20 Jan 2022 11:48:58 -0500 Subject: [PATCH] Create effects module to handle damage, particles, and death more modularly --- src/effects.rs | 116 +++++++++++++++++++++++++++++ src/effects/damage.rs | 109 +++++++++++++++++++++++++++ src/effects/particles.rs | 26 +++++++ src/effects/targeting.rs | 14 ++++ src/inventory_system/use_system.rs | 4 +- src/main.rs | 13 ++-- src/melee_combat_system.rs | 82 +++++++++----------- src/state.rs | 4 +- src/trigger_system.rs | 18 ++--- 9 files changed, 320 insertions(+), 66 deletions(-) create mode 100644 src/effects.rs create mode 100644 src/effects/damage.rs create mode 100644 src/effects/particles.rs create mode 100644 src/effects/targeting.rs diff --git a/src/effects.rs b/src/effects.rs new file mode 100644 index 0000000..3d49283 --- /dev/null +++ b/src/effects.rs @@ -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> = 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 }, + Tile { tile_idx: i32 }, + Tiles { tiles: Vec }, +} + +pub struct EffectSpawner { + pub creator: Option, + pub effect_type: EffectType, + pub targets: Targets, +} + +pub fn add_effect(creator: Option, 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) + } + } + } +} diff --git a/src/effects/damage.rs b/src/effects/damage.rs new file mode 100644 index 0000000..77f28cf --- /dev/null +++ b/src/effects/damage.rs @@ -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::(); + 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.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::(); + let attributes = ecs.read_storage::(); + let map = ecs.fetch_mut::(); + + 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::().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::(); + 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::(); + 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, + }, + ); + } + } + } + } + } + } +} diff --git a/src/effects/particles.rs b/src/effects/particles.rs new file mode 100644 index 0000000..25efea1 --- /dev/null +++ b/src/effects/particles.rs @@ -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::(); + let mut particle_builder = ecs.fetch_mut::(); + particle_builder.request( + tile_idx % map.width, + tile_idx / map.width, + fg, + bg, + glyph, + lifespan, + ); + } +} diff --git a/src/effects/targeting.rs b/src/effects/targeting.rs new file mode 100644 index 0000000..f046a05 --- /dev/null +++ b/src/effects/targeting.rs @@ -0,0 +1,14 @@ +use ::specs::prelude::*; + +use crate::components::Position; +use crate::map::Map; + +pub fn entity_position(ecs: &World, target: Entity) -> Option { + if let Some(pos) = ecs.read_storage::().get(target) { + let map = ecs.fetch::(); + + return Some(map.xy_idx(pos.x, pos.y) as i32); + } + + None +} diff --git a/src/inventory_system/use_system.rs b/src/inventory_system/use_system.rs index 3b218de..1bc0b57 100644 --- a/src/inventory_system/use_system.rs +++ b/src/inventory_system/use_system.rs @@ -2,8 +2,8 @@ use ::specs::prelude::*; use crate::components::{ AreaOfEffect, Confusion, Consumable, EquipmentChanged, Equippable, Equipped, HungerClock, - HungerState, IdentifiedItem, InBackpack, InflictsDamage, MagicMapper, Name, Pools, - Position, ProvidesFood, ProvidesHealing, SufferDamage, TownPortal, WantsToUseItem, + HungerState, IdentifiedItem, InBackpack, InflictsDamage, MagicMapper, Name, Pools, Position, + ProvidesFood, ProvidesHealing, SufferDamage, TownPortal, WantsToUseItem, }; use crate::game_log::GameLog; use crate::particle_system::ParticleBuilder; diff --git a/src/main.rs b/src/main.rs index 4c2073a..f7f49dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ +#[macro_use] +extern crate lazy_static; + mod ai; pub mod camera; mod colors; mod components; mod damage_system; +mod effects; mod game_log; mod gamesystem; mod gui; @@ -16,20 +20,17 @@ mod melee_combat_system; mod movement_system; mod particle_system; mod player; -pub mod random_table; -pub mod raws; +mod random_table; +mod raws; mod rect; mod rex_assets; -pub mod saveload_system; +mod saveload_system; mod spatial; mod spawner; mod state; mod trigger_system; mod visibility_system; -#[macro_use] -extern crate lazy_static; - use ::specs::prelude::*; use ::specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; use components::*; diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 86d30ed..e438899 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,14 +1,14 @@ use ::rltk::RandomNumberGenerator; use ::specs::prelude::*; +use crate::colors; use crate::components::{ - Attributes, Equipped, HungerClock, HungerState, MeleeWeapon, Name, Pools, Skill, Skills, - SufferDamage, WantsToMelee, Wearable, + Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon, Name, + NaturalAttackDefense, Pools, Skill, Skills, WantsToMelee, WeaponAttribute, Wearable, }; +use crate::effects::{add_effect, EffectType, Targets}; use crate::game_log::GameLog; use crate::gamesystem::skill_bonus; -use crate::particle_system::ParticleBuilder; -use crate::{colors, EquipmentSlot, NaturalAttackDefense, Position, WeaponAttribute}; pub struct MeleeCombatSystem {} @@ -21,9 +21,6 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, Name>, ReadStorage<'a, Attributes>, ReadStorage<'a, Skills>, - WriteStorage<'a, SufferDamage>, - WriteExpect<'a, ParticleBuilder>, - ReadStorage<'a, Position>, ReadStorage<'a, HungerClock>, ReadStorage<'a, Pools>, WriteExpect<'a, RandomNumberGenerator>, @@ -31,7 +28,6 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, MeleeWeapon>, ReadStorage<'a, Wearable>, ReadStorage<'a, NaturalAttackDefense>, - ReadExpect<'a, Entity>, ); fn run(&mut self, data: Self::SystemData) { @@ -42,9 +38,6 @@ impl<'a> System<'a> for MeleeCombatSystem { names, attributes, skills, - mut inflict_damage, - mut particle_builder, - positions, hunger_clock, pools, mut rng, @@ -52,7 +45,6 @@ impl<'a> System<'a> for MeleeCombatSystem { meleeweapons, wearables, natural, - player_entity, ) = data; 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 + weapon_damage_bonus, ); - SufferDamage::new_damage( - &mut inflict_damage, - wants_melee.target, - damage, - entity == *player_entity, + add_effect( + Some(entity), + EffectType::Damage { amount: damage }, + Targets::Single { + target: wants_melee.target, + }, ); log.append(format!( "{} hits {} for {} hp.", &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 { // Natural 1 miss log.append(format!( "{} considers attacking {}, but misjudges the timing.", name.name, target_name.name )); - if let Some(pos) = positions.get(wants_melee.target) { - particle_builder.request( - pos.x, - pos.y, - colors::BLUE, - colors::BLACK, - rltk::to_cp437('‼'), - 200.0, - ); - } + add_effect( + None, + EffectType::Particle { + glyph: rltk::to_cp437('‼'), + fg: colors::BLUE, + bg: colors::BLACK, + lifespan: 200.0, + }, + Targets::Single { + target: wants_melee.target, + }, + ); } else { // Miss log.append(format!( "{} attacks {}, but can't connect", name.name, target_name.name )); - if let Some(pos) = positions.get(wants_melee.target) { - particle_builder.request( - pos.x, - pos.y, - colors::CYAN, - colors::BLACK, - rltk::to_cp437('‼'), - 200.0, - ); - } + add_effect( + None, + EffectType::Particle { + glyph: rltk::to_cp437('‼'), + fg: colors::CYAN, + bg: colors::BLACK, + lifespan: 200.0, + }, + Targets::Single { + target: wants_melee.target, + }, + ); } } } diff --git a/src/state.rs b/src/state.rs index 17b32d6..0cc62d1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -19,7 +19,7 @@ use crate::player::*; use crate::raws::*; use crate::trigger_system::TriggerSystem; 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; @@ -139,6 +139,8 @@ impl State { let mut hunger = HungerSystem {}; hunger.run_now(&self.ecs); + effects::run_effects_queue(&mut self.ecs); + let mut particles = ParticleSpawnSystem {}; particles.run_now(&self.ecs); diff --git a/src/trigger_system.rs b/src/trigger_system.rs index b9e4c74..0ef63bf 100644 --- a/src/trigger_system.rs +++ b/src/trigger_system.rs @@ -2,8 +2,9 @@ use ::specs::prelude::*; use crate::components::{ 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::particle_system::ParticleBuilder; use crate::{colors, spatial, Map}; @@ -23,7 +24,6 @@ impl<'a> System<'a> for TriggerSystem { WriteExpect<'a, GameLog>, ReadStorage<'a, InflictsDamage>, WriteExpect<'a, ParticleBuilder>, - WriteStorage<'a, SufferDamage>, ReadStorage<'a, SingleActivation>, ReadStorage<'a, TeleportTo>, WriteStorage<'a, ApplyTeleport>, @@ -42,7 +42,6 @@ impl<'a> System<'a> for TriggerSystem { mut log, inflicts_damage, mut particle_builder, - mut inflict_damage, single_activation, teleporters, mut apply_teleport, @@ -79,12 +78,13 @@ impl<'a> System<'a> for TriggerSystem { 200.0, ); - SufferDamage::new_damage( - &mut inflict_damage, - entity, - damage.damage, - false, - ); + add_effect( + None, + EffectType::Damage { + amount: damage.damage, + }, + Targets::Single { target: entity }, + ) } // If it's a teleporter, then do that