diff --git a/src/components.rs b/src/components.rs index fee43d3..c8a0012 100644 --- a/src/components.rs +++ b/src/components.rs @@ -97,3 +97,8 @@ pub struct WantsToUseItem { #[derive(Component, Debug)] pub struct Consumable {} + +#[derive(Component, Clone)] +pub struct ParticleLifetime { + pub lifetime_ms: f32, +} diff --git a/src/inventory_system.rs b/src/inventory_system.rs index a78dff1..ce40b12 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,6 +1,6 @@ use super::{ - gamelog::GameLog, CombatStats, Consumable, InBackpack, Name, Position, ProvidesHealing, WantsToDropItem, - WantsToPickupItem, WantsToUseItem, + gamelog::GameLog, CombatStats, Consumable, InBackpack, Name, ParticleBuilder, Position, ProvidesHealing, + WantsToDropItem, WantsToPickupItem, WantsToUseItem, DEFAULT_PARTICLE_LIFETIME, }; use specs::prelude::*; @@ -45,10 +45,23 @@ impl<'a> System<'a> for ItemUseSystem { ReadStorage<'a, Consumable>, ReadStorage<'a, ProvidesHealing>, WriteStorage<'a, CombatStats>, + WriteExpect<'a, ParticleBuilder>, + ReadStorage<'a, Position>, ); fn run(&mut self, data: Self::SystemData) { - let (player_entity, mut gamelog, entities, mut wants_use, names, consumables, healing, mut combat_stats) = data; + let ( + player_entity, + mut gamelog, + entities, + mut wants_use, + names, + consumables, + healing, + mut combat_stats, + mut particle_builder, + positions, + ) = data; for (entity, use_item, stats) in (&entities, &wants_use, &mut combat_stats).join() { let item_heals = healing.get(use_item.item); @@ -56,6 +69,17 @@ impl<'a> System<'a> for ItemUseSystem { None => {} Some(healer) => { stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount); + let pos = positions.get(entity); + if let Some(pos) = pos { + particle_builder.request( + pos.x, + pos.y, + rltk::RGB::named(rltk::GREEN), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('♥'), + DEFAULT_PARTICLE_LIFETIME, + ); + } if entity == *player_entity { gamelog.entries.push(format!( "You quaff the {}, and heal {} hp.", diff --git a/src/main.rs b/src/main.rs index 9d2af59..1ddd2a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,8 @@ mod melee_combat_system; use melee_combat_system::MeleeCombatSystem; mod inventory_system; use inventory_system::*; +mod particle_system; +use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME}; // Embedded resources for use in wasm build rltk::embedded_resource!(TERMINAL8X8, "../resources/terminal8x8.jpg"); @@ -62,6 +64,8 @@ impl State { item_use_system.run_now(&self.ecs); let mut drop_system = ItemDropSystem {}; drop_system.run_now(&self.ecs); + let mut particle_system = particle_system::ParticleSpawnSystem {}; + particle_system.run_now(&self.ecs); self.ecs.maintain(); } } @@ -70,6 +74,7 @@ impl GameState for State { fn tick(&mut self, ctx: &mut Rltk) { // Clear screen ctx.cls(); + particle_system::cull_dead_particles(&mut self.ecs, ctx); // Draw map and ui draw_map(&self.ecs, ctx); @@ -186,6 +191,7 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); let map = Map::new_map_rooms_and_corridors(); let (player_x, player_y) = map.rooms[0].centre(); @@ -202,6 +208,7 @@ fn main() -> rltk::BError { gs.ecs.insert(player_entity); gs.ecs.insert(gamelog::GameLog { entries: vec!["Here's your welcome message.".to_string()] }); gs.ecs.insert(RunState::PreRun); + gs.ecs.insert(particle_system::ParticleBuilder::new()); rltk::main_loop(context, gs) } diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 6b8aa87..5f6431d 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,4 +1,4 @@ -use super::{gamelog::GameLog, CombatStats, Name, SufferDamage, WantsToMelee}; +use super::{gamelog::GameLog, CombatStats, Name, ParticleBuilder, Position, SufferDamage, WantsToMelee}; use specs::prelude::*; pub struct MeleeCombatSystem {} @@ -11,10 +11,21 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, Name>, ReadStorage<'a, CombatStats>, WriteStorage<'a, SufferDamage>, + WriteExpect<'a, ParticleBuilder>, + ReadStorage<'a, Position>, ); fn run(&mut self, data: Self::SystemData) { - let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data; + let ( + entities, + mut log, + mut wants_melee, + names, + combat_stats, + mut inflict_damage, + mut particle_builder, + positions, + ) = data; for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { if stats.hp <= 0 { @@ -26,6 +37,17 @@ impl<'a> System<'a> for MeleeCombatSystem { } let target_name = names.get(wants_melee.target).unwrap(); + let pos = positions.get(wants_melee.target); + if let Some(pos) = pos { + particle_builder.request( + pos.x, + pos.y, + rltk::RGB::named(rltk::ORANGE), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('‼'), + 150.0, + ); + } let damage = i32::max(0, stats.power - target_stats.defence); if damage == 0 { diff --git a/src/monster_ai_system.rs b/src/monster_ai_system.rs index abc60bc..9968e7f 100644 --- a/src/monster_ai_system.rs +++ b/src/monster_ai_system.rs @@ -42,6 +42,10 @@ impl<'a> System<'a> for MonsterAI { .insert(entity, WantsToMelee { target: *player_entity }) .expect("Unable to insert attack."); } else if viewshed.visible_tiles.contains(&*player_pos) { + // If the player is visible, but the path is obstructed, this will currently search + // the entire map (i.e. Will do a huge ASTAR to find an alternate route), and the + // mob will follow that path until it leaves vision, then lose sight of the player + // and stop. let path = rltk::a_star_search(map.xy_idx(pos.x, pos.y), map.xy_idx(player_pos.x, player_pos.y), &*map); if path.success && path.steps.len() > 1 { let mut idx = map.xy_idx(pos.x, pos.y); diff --git a/src/particle_system.rs b/src/particle_system.rs new file mode 100644 index 0000000..4be5e77 --- /dev/null +++ b/src/particle_system.rs @@ -0,0 +1,79 @@ +use super::{ParticleLifetime, Position, Renderable, Rltk}; +use rltk::RGB; +use specs::prelude::*; + +pub const DEFAULT_PARTICLE_LIFETIME: f32 = 150.0; + +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("Particle will not die"); + } +} + +struct ParticleRequest { + x: i32, + y: i32, + fg: RGB, + bg: RGB, + glyph: rltk::FontCharType, + lifetime: f32, +} + +pub struct ParticleBuilder { + requests: Vec, +} + +impl ParticleBuilder { + #[allow(clippy::new_without_default)] + pub fn new() -> ParticleBuilder { + ParticleBuilder { requests: Vec::new() } + } + + 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("Could not insert position"); + renderables + .insert( + p, + Renderable { fg: new_particle.fg, bg: new_particle.bg, glyph: new_particle.glyph, render_order: 0 }, + ) + .expect("Could not insert renderables"); + particles + .insert(p, ParticleLifetime { lifetime_ms: new_particle.lifetime }) + .expect("Could not insert lifetime"); + } + particle_builder.requests.clear(); + } +}