From 1dd5db42f7f8472a74073186e002ea2c94d2d50e Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Tue, 16 Nov 2021 11:33:58 -0500 Subject: [PATCH] Add particle effects for Melee combat --- src/components.rs | 5 ++ src/main.rs | 7 +++ src/map.rs | 2 +- src/melee_combat_system.rs | 18 +++++- src/particle_system.rs | 118 +++++++++++++++++++++++++++++++++++++ src/player.rs | 24 +++----- src/saveload_system.rs | 6 +- 7 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 src/particle_system.rs diff --git a/src/components.rs b/src/components.rs index ee360d4..24eacdf 100644 --- a/src/components.rs +++ b/src/components.rs @@ -174,6 +174,11 @@ pub struct DefenseBonus { pub defense: i32, } +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct ParticleLifetime { + pub lifetime_ms: f32, +} + // Serialization helper code. We need to implement ConvertSaveLoad for each type that contains an // Entity. diff --git a/src/main.rs b/src/main.rs index 3d49f55..88df956 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod map; mod map_indexing_system; mod melee_combat_system; mod monster_ai_system; +mod particle_system; mod player; pub mod random_table; mod rect; @@ -101,6 +102,9 @@ impl State { let mut item_remove = ItemRemoveSystem {}; item_remove.run_now(&self.ecs); + let mut particles = particle_system::ParticleSpawnSystem {}; + particles.run_now(&self.ecs); + self.ecs.maintain(); } } @@ -114,6 +118,7 @@ impl GameState for State { } ctx.cls(); + particle_system::cull_dead_particles(&mut self.ecs, ctx); match newrunstate { RunState::MainMenu { .. } => {} @@ -485,6 +490,7 @@ fn main() -> rltk::BError { MeleePowerBonus, DefenseBonus, WantsToRemoveItem, + ParticleLifetime, ); gs.ecs.insert(SimpleMarkerAllocator::::new()); @@ -506,6 +512,7 @@ fn main() -> rltk::BError { menu_selection: gui::MainMenuSelection::NewGame, }); gs.ecs.insert(GameLog::new("Welcome to Rusty Roguelike")); + gs.ecs.insert(particle_system::ParticleBuilder::new()); rltk::main_loop(context, gs) } diff --git a/src/map.rs b/src/map.rs index 12efa42..809abe3 100644 --- a/src/map.rs +++ b/src/map.rs @@ -245,7 +245,7 @@ pub fn draw_map(ecs: &World, ctx: &mut Rltk) { if !map.visible_tiles[idx] { fg = fg.to_greyscale(); - bg = RGB::from_f32(0.,0.,0.); + bg = RGB::from_f32(0., 0., 0.); } ctx.set(x, y, fg, bg, glyph); diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index b9679ff..f6d0ca4 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,7 +1,8 @@ use crate::components::{ CombatStats, DefenseBonus, Equipped, MeleePowerBonus, Name, SufferDamage, WantsToMelee, }; -use crate::game_log::GameLog; +use crate::{game_log::GameLog, particle_system::ParticleBuilder, Position}; +use rltk::RGB; use specs::prelude::*; pub struct MeleeCombatSystem {} @@ -18,6 +19,8 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, MeleePowerBonus>, ReadStorage<'a, DefenseBonus>, ReadStorage<'a, Equipped>, + WriteExpect<'a, ParticleBuilder>, + ReadStorage<'a, Position>, ); fn run(&mut self, data: Self::SystemData) { @@ -31,6 +34,8 @@ impl<'a> System<'a> for MeleeCombatSystem { melee_power_bonuses, defense_bonuses, equipped, + mut particle_builder, + positions, ) = data; for (entity, wants_melee, name, stats) in @@ -59,6 +64,17 @@ impl<'a> System<'a> for MeleeCombatSystem { } } + if let Some(pos) = positions.get(wants_melee.target) { + particle_builder.request( + pos.x, + pos.y, + RGB::named(rltk::ORANGE), + RGB::named(rltk::BLACK), + rltk::to_cp437('‼'), + 200.0, + ); + } + let damage = i32::max( 0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus), diff --git a/src/particle_system.rs b/src/particle_system.rs new file mode 100644 index 0000000..01e34e3 --- /dev/null +++ b/src/particle_system.rs @@ -0,0 +1,118 @@ +use crate::components::{ParticleLifetime, Renderable}; +use crate::Position; +use rltk::{Rltk, RGB}; +use specs::prelude::*; + +pub fn cull_dead_particles(ecs: &mut World, ctx: &Rltk) { + let mut dead_particles: Vec = Vec::new(); + { + // Age out particles + let mut particles = ecs.write_storage::(); + let entities = ecs.entities(); + for (entity, mut particle) in (&entities, &mut particles).join() { + particle.lifetime_ms -= ctx.frame_time_ms; + + if particle.lifetime_ms < 0.0 { + dead_particles.push(entity); + } + } + } + + for dead in dead_particles.iter() { + ecs.delete_entity(*dead) + .expect("Failed to delete dead particle entity"); + } +} + +struct ParticleRequest { + x: i32, + y: i32, + fg: RGB, + bg: RGB, + glyph: rltk::FontCharType, + lifetime: f32, +} + +#[derive(Default)] +pub struct ParticleBuilder { + requests: Vec, +} + +impl ParticleBuilder { + pub fn new() -> ParticleBuilder { + ParticleBuilder::default() + } + + pub fn request( + &mut self, + x: i32, + y: i32, + fg: RGB, + bg: RGB, + glyph: rltk::FontCharType, + lifetime: f32, + ) { + self.requests.push(ParticleRequest { + x, + y, + fg, + bg, + glyph, + lifetime, + }) + } +} + +pub struct ParticleSpawnSystem {} + +impl<'a> System<'a> for ParticleSpawnSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteStorage<'a, Position>, + WriteStorage<'a, Renderable>, + WriteStorage<'a, ParticleLifetime>, + WriteExpect<'a, ParticleBuilder>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (entities, mut positions, mut renderables, mut particles, mut particle_builder) = data; + + for new_particle in particle_builder.requests.iter() { + let p = entities.create(); + + positions + .insert( + p, + Position { + x: new_particle.x, + y: new_particle.y, + }, + ) + .expect("Failed to insert Position of new particle"); + + renderables + .insert( + p, + Renderable { + fg: new_particle.fg, + bg: new_particle.bg, + glyph: new_particle.glyph, + render_order: 0, + }, + ) + .expect("Failed to insert Renderable of new particle"); + + particles + .insert( + p, + ParticleLifetime { + lifetime_ms: new_particle.lifetime, + }, + ) + .expect("Failed to insert Lifetime of new particle"); + } + + particle_builder.requests.clear(); + } +} diff --git a/src/player.rs b/src/player.rs index bfc84f5..096ee20 100644 --- a/src/player.rs +++ b/src/player.rs @@ -175,17 +175,8 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { try_move_player(-1, 1, &mut gs.ecs) } - // Pick up item - VirtualKeyCode::G => get_item(&mut gs.ecs), - - // Show inventory - VirtualKeyCode::I => return RunState::ShowInventory, - - // Show item drop screen - VirtualKeyCode::D => return RunState::ShowDropItem, - - // Save and Quit - VirtualKeyCode::Escape => return RunState::SaveGame, + // Skip Turn + VirtualKeyCode::Numpad5 | VirtualKeyCode::Space => return skip_turn(&mut gs.ecs), // Level changes VirtualKeyCode::Period => { @@ -194,12 +185,15 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { } } - // Skip Turn - VirtualKeyCode::Numpad5 | VirtualKeyCode::Space => return skip_turn(&mut gs.ecs), - - // Remove item + // Item management + VirtualKeyCode::G => get_item(&mut gs.ecs), + VirtualKeyCode::I => return RunState::ShowInventory, + VirtualKeyCode::D => return RunState::ShowDropItem, VirtualKeyCode::R => return RunState::ShowRemoveItem, + // Save and Quit + VirtualKeyCode::Escape => return RunState::SaveGame, + _ => return RunState::AwaitingInput, }, } diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 99c33b2..3029c96 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -74,7 +74,8 @@ pub fn save_game(ecs: &mut World) { Equipped, MeleePowerBonus, DefenseBonus, - WantsToRemoveItem + WantsToRemoveItem, + ParticleLifetime ); } @@ -155,7 +156,8 @@ pub fn load_game(ecs: &mut World) { Equipped, MeleePowerBonus, DefenseBonus, - WantsToRemoveItem + WantsToRemoveItem, + ParticleLifetime ); }