Browse Source

Some bugfixes and a restructured chapter 18.

Herbert Wolverson 4 years ago
parent
commit
00c1c14533

BIN
book/src/c18-s1.gif


BIN
book/src/c18-s2.gif


BIN
book/src/c18-s3.gif


+ 1 - 1
book/src/chapter_16.md

@@ -27,7 +27,7 @@ This requires the `wall_glyph` function, so lets write it:
 
 ```rust
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x == 0 || x == map.width || y == 0 || y == MAPHEIGHT as i32 { return 35; }
+    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 222 - 119
book/src/chapter_18.md

@@ -25,106 +25,184 @@ pub struct ParticleLifetime {
 
 We have to register this in all the usual places: `main.rs` and `saveload_system.rs` (twice).
 
-## Spawning particles
+## Grouping particle code together
 
-We'll start by spawning a particle whenever someone attacks. In `melee_combat_system.rs`, we'll expand the list of resources required for melee:
+We'll make a new file, `particle_system.rs`. It won't be a regular system, because we need access to the RLTK `Context` object - but it will have to provide services to other systems.
+
+The first thing to support is making particles vanish after their lifetime. So we start with the following in `particle_system.rs`:
 
 ```rust
-type SystemData = ( Entities<'a>,
-    WriteExpect<'a, GameLog>,
-    WriteStorage<'a, WantsToMelee>,
-    ReadStorage<'a, Name>,
-    ReadStorage<'a, CombatStats>,
-    WriteStorage<'a, SufferDamage>,
-    ReadStorage<'a, MeleePowerBonus>,
-    ReadStorage<'a, DefenseBonus>,
-    ReadStorage<'a, Equipped>,
-    WriteStorage<'a, Position>,
-    WriteStorage<'a, Renderable>,
-    WriteStorage<'a, ParticleLifetime>
-);
+use specs::prelude::*;
+use super::{ Rltk, ParticleLifetime};
+
+pub fn cull_dead_particles(ecs : &mut World, ctx : &Rltk) {
+    let mut dead_particles : Vec<Entity> = Vec::new();
+    {
+        // Age out particles
+        let mut particles = ecs.write_storage::<ParticleLifetime>();
+        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");
+    } 
+}
 ```
 
-Then we'll add some logic to spawn a particle on impact:
+Then we modify the render loop in `main.rs` to call it:
 
 ```rust
-let pos = positions.get(wants_melee.target);
-if let Some(pos) = pos {
-    let particle = entities.create();
-    positions.insert(particle, Position{ x:pos.x, y:pos.y }).expect("Unable to insert position");
-    renderables.insert(particle, 
-        Renderable{ glyph: rltk::to_cp437('░'),
-            fg: RGB::named(rltk::CYAN),
-            bg: RGB::named(rltk::BLACK),
-            render_order: 0 }).expect("Unable to insert renderable");
-    particles.insert(particle, ParticleLifetime{ lifetime_ms : 100.0 }).expect("Unable to insert particle lifetime");
-}
+ctx.cls();        
+particle_system::cull_dead_particles(&mut self.ecs, ctx);
 ```
 
-This gives a borrow warning (FIXME), but works (I'll fix later, promise). If you `cargo run` your project now, when one entity attacks another a cyan ░ pattern renders in the attack space. Unfortunately, it persists forever!
+## Spawning particles via a service
 
-## Vanishing particles
+Let's extend `particle_system.rs` to offer a builder system: you obtain a `ParticleBuilder` and add requests to it, and then create your particles as a batch together. We'll offer the particle system as a *resource* - so it's available anywhere. This avoids having to add much intrusive code into each system, and lets us handle the actual particle spawning as a single (fast) batch.
 
-We want to age each particle by the time since the last frame, each tick. We can do this by modifying the `tick` function in `main.rs`:
+Our basic `ParticleBuilder` looks like this. We haven't done anything to actually *add* any particles yet, but this provides the requestor service:
 
 ```rust
-match newrunstate {
-    RunState::MainMenu{..} => {}
-    RunState::GameOver{..} => {}
-    _ => {
-        draw_map(&self.ecs, ctx);
+struct ParticleRequest {
+    x: i32,
+    y: i32,
+    fg: RGB,
+    bg: RGB,
+    glyph: u8,
+    lifetime: f32
+}
 
-        let mut dead_particles : Vec<Entity> = Vec::new();
-        {
-            let positions = self.ecs.read_storage::<Position>();
-            let renderables = self.ecs.read_storage::<Renderable>();
-            let map = self.ecs.fetch::<Map>();
+pub struct ParticleBuilder {
+    requests : Vec<ParticleRequest>
+}
 
-            let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
-            data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
-            for (pos, render) in data.iter() {
-                let idx = map.xy_idx(pos.x, pos.y);
-                if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
+impl ParticleBuilder {
+    pub fn new() -> ParticleBuilder {
+        ParticleBuilder{ requests : Vec::new() }
+    }
+
+    pub fn request(&mut self, x:i32, y:i32, fg: RGB, bg:RGB, glyph: u8, lifetime: f32) {
+        self.requests.push(
+            ParticleRequest{
+                x, y, fg, bg, glyph, lifetime
             }
+        );
+    }
+}
+```
+
+In `main.rs`, we'll turn it into a *resource*:
+
+```rust
+gs.ecs.insert(particle_system::ParticleBuilder::new());
+```
+
+Now, we'll return to `particle_systemrs` and build an actual system to spawn particles. The system looks like this:
+
+```rust
+pub struct ParticleSpawnSystem {}
 
-            gui::draw_ui(&self.ecs, ctx);
-
-            // Age out particles
-            let mut particles = self.ecs.write_storage::<ParticleLifetime>();
-            let entities = self.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);
-                }
-            }                    
+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("Unable to inser position");
+            renderables.insert(p, Renderable{ fg: new_particle.fg, bg: new_particle.bg, glyph: new_particle.glyph, render_order: 0 }).expect("Unable to insert renderable");
+            particles.insert(p, ParticleLifetime{ lifetime_ms: new_particle.lifetime }).expect("Unable to insert lifetime");
         }
-        for dead in dead_particles.iter() {
-            self.ecs.delete_entity(*dead).expect("Particle will not die");
-        } 
+
+        particle_builder.requests.clear();
     }
 }
 ```
 
-You can `cargo run` now, and see the hit effect quickly appear and then vanish after you bash a poor goblin (or it bashes you!).
-FIXME: SCREENSHOT
+This is a very simple service: it iterates the requests, and creates an entity for each particle with the component parameters from the request. Then it clears the builder list. The last step is to add it to the system schedule in `main.rs`:
+
+```rust
+let mut gs = State {
+    ecs: World::new(),
+    systems : DispatcherBuilder::new()
+        .with(MapIndexingSystem{}, "map_indexing_system", &[])
+        .with(VisibilitySystem{}, "visibility_system", &[])
+        .with(MonsterAI{}, "monster_ai", &["visibility_system", "map_indexing_system"])
+        .with(MeleeCombatSystem{}, "melee_combat", &["monster_ai"])
+        .with(DamageSystem{}, "damage", &["melee_combat"])
+        .with(ItemCollectionSystem{}, "pickup", &["melee_combat"])
+        .with(ItemUseSystem{}, "potions", &["melee_combat"])
+        .with(ItemDropSystem{}, "drop_items", &["melee_combat"])
+        .with(ItemRemoveSystem{}, "remove_items", &["melee_combat"])
+        .with(particle_system::ParticleSpawnSystem{}, "spawn_particles", &["potions", "melee_combat"])
+        .build(),
+};
+```
 
-## Adding more effects
+We've made it depend upon likely particle spawners. We'll have to be a little careful to avoid accidentally making it concurrent with anything that might add to it.
 
-It would be nice if the various magical items in the game provide some visual feedback. We'll try and add this in a relatively generic fashion to the item use system in `inventory_system.rs`. First, we'll make a new structure to indicate that we'd like a particle:
+## Actually spawning some particles for combat
+
+Lets start by spawning a particle whenever someone attacks. Open up `melee_combat_system.rs`, and we'll add `ParticleBuilder` to the list of requested resources for the system. First, the includes:
 
 ```rust
-struct ItemParticleRequest {
-    x: i32,
-    y: i32,
-    fg: RGB,
-    bg: RGB,
-    glyph: u8,
-    lifetime: f32
+use super::{CombatStats, WantsToMelee, Name, SufferDamage, gamelog::GameLog, MeleePowerBonus, DefenseBonus, Equipped,
+    particle_system::ParticleBuilder, Position};
+```
+
+Then, a `WriteExpect` to be able to write to the resource:
+
+```rust
+type SystemData = ( Entities<'a>,
+    WriteExpect<'a, GameLog>,
+    WriteStorage<'a, WantsToMelee>,
+    ReadStorage<'a, Name>,
+    ReadStorage<'a, CombatStats>,
+    WriteStorage<'a, SufferDamage>,
+    ReadStorage<'a, MeleePowerBonus>,
+    ReadStorage<'a, DefenseBonus>,
+    ReadStorage<'a, Equipped>,
+    WriteExpect<'a, ParticleBuilder>,
+    ReadStorage<'a, Position>
+);
+```
+
+And the expanded list of resources for the `run` method itself:
+```rust
+let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage, 
+    melee_power_bonuses, defense_bonuses, equipped, mut particle_builder, positions) = data;
+```
+
+Finally, we'll add the request:
+
+```rust
+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('‼'), 100.0);
 }
+
+let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus));
 ```
 
-Then at the top of the system, we'll request a few more containers and initialize a list of particle requests:
+If you `cargo run` now, you'll see a relatively subtle particle feedback to show that melee combat occurred. This definitely helps with the *feel* of gameplay, and is sufficiently non-intrusive that we aren't making our other systems too confusing.
+
+![Screenshot](./c18-s1.gif)
+
+## Adding effects to item use
+
+It would be great to add similar effects to item use, so lets do it! In `inventory_system.rs`, we'll expand the `ItemUseSystem` introduction to include the `ParticleBuilder`:
 
 ```rust
 impl<'a> System<'a> for ItemUseSystem {
@@ -145,81 +223,106 @@ impl<'a> System<'a> for ItemUseSystem {
                         ReadStorage<'a, Equippable>,
                         WriteStorage<'a, Equipped>,
                         WriteStorage<'a, InBackpack>,
-                        WriteStorage<'a, Position>,
-                        ReadExpect<'a, Point>,
-                        WriteStorage<'a, Renderable>,
-                        WriteStorage<'a, ParticleLifetime>
+                        WriteExpect<'a, ParticleBuilder>,
+                        ReadStorage<'a, Position>
                       );
 
     #[allow(clippy::cognitive_complexity)]
     fn run(&mut self, data : Self::SystemData) {
-        let mut particle_requests : Vec<ItemParticleRequest> = Vec::new();
+        let (player_entity, mut gamelog, map, entities, mut wants_use, names, 
+            consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, 
+            aoe, mut confused, equippable, mut equipped, mut backpack, mut particle_builder, positions) = data;
 ```
 
-At the *bottom* of the system, we'll instantiate our particles in one go:
-
+We'll start by showing a heart when you drink a healing potion. In the *healing* section:
 ```rust
-wants_use.clear();
+stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount);
+if entity == *player_entity {
+    gamelog.entries.insert(0, format!("You use the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
+}
+used_item = true;
 
-for part in particle_requests.iter() {
-    let particle = entities.create();
-    positions.insert(particle, Position{ x : part.x, y: part.y }).expect("Unable to insert position");
-    renderables.insert(particle, Renderable{ fg: part.fg, bg: part.bg, glyph: part.glyph, render_order: 0 }).expect("Unable to insert renderable");
-    particle_life.insert(particle, ParticleLifetime{ lifetime_ms: part.lifetime }).expect("Unable to insert particle lifetime");
+let pos = positions.get(*target);
+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('♥'), 100.0);
 }
 ```
 
-Now we need to add some effects. In the *healing* section:
-
+We can use a similar effect for confusion - only with a magenta question mark. In the *confusion* section:
 ```rust
-used_item = true;                            
-particle_requests.push(ItemParticleRequest{
-    x: player_pos.x,
-    y: player_pos.y,
-    fg: RGB::from_f32(0., 0.75, 0.),
-    bg: RGB::from_f32(0., 0., 0.),
-    glyph: rltk::to_cp437('♥'),
-    lifetime: 200.0
-});
+gamelog.entries.insert(0, format!("You use {} on {}, confusing them.", item_name.name, mob_name.name));
+
+let pos = positions.get(*mob);
+if let Some(pos) = pos {
+    particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'), 100.0);
+}
 ```
 
-And in the *damage* section:
+We should also use a particle to indicate that damage was inflicted. In the *damage* section of the system:
 
 ```rust
 gamelog.entries.insert(0, format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, damage.damage));
 
-let mob_pos = positions.get(*mob);
-if let Some(mob_pos) = mob_pos {
-    particle_requests.push(ItemParticleRequest{
-        x: mob_pos.x,
-        y: mob_pos.y,
-        fg: RGB::named(rltk::ORANGE),
-        bg: RGB::from_f32(0., 0., 0.),
-        glyph: rltk::to_cp437('▒'),
-        lifetime: 200.0
-    });
+let pos = positions.get(*mob);
+if let Some(pos) = pos {
+    particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::RED), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 100.0);
 }
 ```
 
-And in the *confusion* section:
+Lastly, if an effect hits a whole area (for example, a fireball) it would be good to indicate what the area is. In the *targeting* section of the system, add:
 
 ```rust
-let mob_pos = positions.get(*mob);
-if let Some(mob_pos) = mob_pos {
-    particle_requests.push(ItemParticleRequest{
-        x: mob_pos.x,
-        y: mob_pos.y,
-        fg: RGB::from_f32(0., 0., 0.75),
-        bg: RGB::from_f32(0., 0., 0.),
-        glyph: rltk::to_cp437('?'),
-        lifetime: 200.0
-    });
+for mob in map.tile_content[idx].iter() {
+    targets.push(*mob);
 }
+particle_builder.request(tile_idx.x, tile_idx.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('░'), 100.0);
+```
+
+That wasn't too hard, was it? If you `cargo run` your project now, you'll see various visual effects firing.
+
+![Screenshot](./c18-s2.gif)
+
+## Adding an indicator for missing a turn due to confusion
+
+Lastly, we'll repeat the confused effect on monsters when it is their turn and they skip due to being confused. This should make it less confusing as to why they stand around. In `monster_ai_system.rs`, we first modify the system header to request the appropriate helper:
+
+```rust
+impl<'a> System<'a> for MonsterAI {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( WriteExpect<'a, Map>,
+                        ReadExpect<'a, Point>,
+                        ReadExpect<'a, Entity>,
+                        ReadExpect<'a, RunState>,
+                        Entities<'a>,
+                        WriteStorage<'a, Viewshed>, 
+                        ReadStorage<'a, Monster>,
+                        WriteStorage<'a, Position>,
+                        WriteStorage<'a, WantsToMelee>,
+                        WriteStorage<'a, Confusion>,
+                        WriteExpect<'a, ParticleBuilder>);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, 
+            monster, mut position, mut wants_to_melee, mut confused, mut particle_builder) = data;
+```
+
+Then we add in a request at the end of the confusion test:
+
+```rust
+can_act = false;
+
+particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA), 
+                    rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'), 100.0);
 ```
 
-If you `cargo run` the project now, you'll find that healing generates a nice heart over yourself, confusion spams a question mark, and damage-dealing flashes an orange haze. It's subtle, but gives a lot more visceral feel to the game.
+We don't need to worry about getting the `Position` component here, because we already get it as part of the loop. If you `cargo run` your project now, and find a confusion scroll - you have visual feedback as to why a goblin isn't chasing you anymore:
+
+![Screenshot](./c18-s3.gif)
+
+## Wrap Up
+
+That's it for visual effects for now. We've given the game a much more visceral feel, with feedback given for actions. That's a big improvement, and goes a long way to modernizing an ASCII interface!
 
-FIXME: SCREENSHOT
 
 **The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-18-particles)**
 

+ 1 - 1
chapter-16-nicewalls/src/map.rs

@@ -187,7 +187,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x == 0 || x == map.width || y == 0 || y == MAPHEIGHT as i32 { return 35; }
+    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 3 - 2
chapter-16-nicewalls/src/monster_ai_system.rs

@@ -20,7 +20,8 @@ impl<'a> System<'a> for MonsterAI {
                         WriteStorage<'a, Confusion>);
 
     fn run(&mut self, data : Self::SystemData) {
-        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee, mut confused) = data;
+        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, 
+            monster, mut position, mut wants_to_melee, mut confused) = data;
 
         if *runstate != RunState::MonsterTurn { return; }
 
@@ -33,7 +34,7 @@ impl<'a> System<'a> for MonsterAI {
                 if i_am_confused.turns < 1 {
                     confused.remove(entity);
                 }
-                can_act = false;
+                can_act = false;                
             }
 
             if can_act {

+ 1 - 1
chapter-17-blood/src/map.rs

@@ -190,7 +190,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x == 0 || x == map.width || y == 0 || y == MAPHEIGHT as i32 { return 35; }
+    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 20 - 58
chapter-18-particles/src/inventory_system.rs

@@ -2,8 +2,7 @@ extern crate specs;
 use specs::prelude::*;
 use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog, WantsToUseItem, 
     Consumable, ProvidesHealing, CombatStats, WantsToDropItem, InflictsDamage, Map, SufferDamage,
-    AreaOfEffect, Confusion, Equippable, Equipped, WantsToRemoveItem, Renderable, ParticleLifetime};
-use rltk::{RGB, Point};
+    AreaOfEffect, Confusion, Equippable, Equipped, WantsToRemoveItem, particle_system::ParticleBuilder};
 
 pub struct ItemCollectionSystem {}
 
@@ -35,15 +34,6 @@ impl<'a> System<'a> for ItemCollectionSystem {
 
 pub struct ItemUseSystem {}
 
-struct ItemParticleRequest {
-    x: i32,
-    y: i32,
-    fg: RGB,
-    bg: RGB,
-    glyph: u8,
-    lifetime: f32
-}
-
 impl<'a> System<'a> for ItemUseSystem {
     #[allow(clippy::type_complexity)]
     type SystemData = ( ReadExpect<'a, Entity>,
@@ -62,20 +52,15 @@ impl<'a> System<'a> for ItemUseSystem {
                         ReadStorage<'a, Equippable>,
                         WriteStorage<'a, Equipped>,
                         WriteStorage<'a, InBackpack>,
-                        WriteStorage<'a, Position>,
-                        ReadExpect<'a, Point>,
-                        WriteStorage<'a, Renderable>,
-                        WriteStorage<'a, ParticleLifetime>
+                        WriteExpect<'a, ParticleBuilder>,
+                        ReadStorage<'a, Position>
                       );
 
     #[allow(clippy::cognitive_complexity)]
     fn run(&mut self, data : Self::SystemData) {
-        let mut particle_requests : Vec<ItemParticleRequest> = Vec::new();
-
         let (player_entity, mut gamelog, map, entities, mut wants_use, names, 
             consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, 
-            aoe, mut confused, equippable, mut equipped, mut backpack,
-            mut positions, player_pos, mut renderables, mut particle_life) = data;
+            aoe, mut confused, equippable, mut equipped, mut backpack, mut particle_builder, positions) = data;
 
         for (entity, useitem) in (&entities, &wants_use).join() {
             let mut used_item = true;
@@ -102,6 +87,7 @@ impl<'a> System<'a> for ItemUseSystem {
                                 for mob in map.tile_content[idx].iter() {
                                     targets.push(*mob);
                                 }
+                                particle_builder.request(tile_idx.x, tile_idx.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('░'), 100.0);
                             }
                         }
                     }
@@ -149,20 +135,17 @@ impl<'a> System<'a> for ItemUseSystem {
                     for target in targets.iter() {
                         let stats = combat_stats.get_mut(*target);
                         if let Some(stats) = stats {
-                            stats.hp = i32::max(stats.max_hp, stats.hp + healer.heal_amount);
+                            stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount);
                             if entity == *player_entity {
                                 gamelog.entries.insert(0, format!("You use the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
                             }
-                            used_item = true;                            
-                            particle_requests.push(ItemParticleRequest{
-                                x: player_pos.x,
-                                y: player_pos.y,
-                                fg: RGB::from_f32(0., 0.75, 0.),
-                                bg: RGB::from_f32(0., 0., 0.),
-                                glyph: rltk::to_cp437('♥'),
-                                lifetime: 200.0
-                            });
-                        }                        
+                            used_item = true;
+
+                            let pos = positions.get(*target);
+                            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('♥'), 100.0);
+                            }   
+                        }            
                     }
                 }
             }
@@ -180,16 +163,9 @@ impl<'a> System<'a> for ItemUseSystem {
                             let item_name = names.get(useitem.item).unwrap();
                             gamelog.entries.insert(0, format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, damage.damage));
 
-                            let mob_pos = positions.get(*mob);
-                            if let Some(mob_pos) = mob_pos {
-                                particle_requests.push(ItemParticleRequest{
-                                    x: mob_pos.x,
-                                    y: mob_pos.y,
-                                    fg: RGB::named(rltk::ORANGE),
-                                    bg: RGB::from_f32(0., 0., 0.),
-                                    glyph: rltk::to_cp437('▒'),
-                                    lifetime: 200.0
-                                });
+                            let pos = positions.get(*mob);
+                            if let Some(pos) = pos {
+                                particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::RED), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 100.0);
                             }
                         }
 
@@ -213,16 +189,9 @@ impl<'a> System<'a> for ItemUseSystem {
                                 let item_name = names.get(useitem.item).unwrap();
                                 gamelog.entries.insert(0, format!("You use {} on {}, confusing them.", item_name.name, mob_name.name));
 
-                                let mob_pos = positions.get(*mob);
-                                if let Some(mob_pos) = mob_pos {
-                                    particle_requests.push(ItemParticleRequest{
-                                        x: mob_pos.x,
-                                        y: mob_pos.y,
-                                        fg: RGB::from_f32(0., 0., 0.75),
-                                        bg: RGB::from_f32(0., 0., 0.),
-                                        glyph: rltk::to_cp437('?'),
-                                        lifetime: 200.0
-                                    });
+                                let pos = positions.get(*mob);
+                                if let Some(pos) = pos {
+                                    particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'), 100.0);
                                 }
                             }
                         }
@@ -242,17 +211,10 @@ impl<'a> System<'a> for ItemUseSystem {
                         entities.delete(useitem.item).expect("Delete failed");
                     }
                 }
-            }            
+            }
         }
 
         wants_use.clear();
-
-        for part in particle_requests.iter() {
-            let particle = entities.create();
-            positions.insert(particle, Position{ x : part.x, y: part.y }).expect("Unable to insert position");
-            renderables.insert(particle, Renderable{ fg: part.fg, bg: part.bg, glyph: part.glyph, render_order: 0 }).expect("Unable to insert renderable");
-            particle_life.insert(particle, ParticleLifetime{ lifetime_ms: part.lifetime }).expect("Unable to insert particle lifetime");
-        }
     }
 }
 

+ 14 - 28
chapter-18-particles/src/main.rs

@@ -31,6 +31,7 @@ mod inventory_system;
 use inventory_system::{ ItemCollectionSystem, ItemUseSystem, ItemDropSystem, ItemRemoveSystem };
 pub mod saveload_system;
 pub mod random_table;
+pub mod particle_system;
 
 #[derive(PartialEq, Copy, Clone)]
 pub enum RunState { AwaitingInput, 
@@ -61,41 +62,24 @@ impl GameState for State {
         }
 
         ctx.cls();        
+        particle_system::cull_dead_particles(&mut self.ecs, ctx);
 
         match newrunstate {
             RunState::MainMenu{..} => {}
             RunState::GameOver{..} => {}
             _ => {
                 draw_map(&self.ecs, ctx);
-
-                let mut dead_particles : Vec<Entity> = Vec::new();
-                {
-                    let positions = self.ecs.read_storage::<Position>();
-                    let renderables = self.ecs.read_storage::<Renderable>();
-                    let map = self.ecs.fetch::<Map>();
-
-                    let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
-                    data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
-                    for (pos, render) in data.iter() {
-                        let idx = map.xy_idx(pos.x, pos.y);
-                        if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
-                    }
-
-                    gui::draw_ui(&self.ecs, ctx);
-
-                    // Age out particles
-                    let mut particles = self.ecs.write_storage::<ParticleLifetime>();
-                    let entities = self.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);
-                        }
-                    }                    
+                let positions = self.ecs.read_storage::<Position>();
+                let renderables = self.ecs.read_storage::<Renderable>();
+                let map = self.ecs.fetch::<Map>();
+
+                let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
+                data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
+                for (pos, render) in data.iter() {
+                    let idx = map.xy_idx(pos.x, pos.y);
+                    if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
                 }
-                for dead in dead_particles.iter() {
-                    self.ecs.delete_entity(*dead).expect("Particle will not die");
-                } 
+                gui::draw_ui(&self.ecs, ctx);                
             }
         }
         
@@ -373,6 +357,7 @@ fn main() {
             .with(ItemUseSystem{}, "potions", &["melee_combat"])
             .with(ItemDropSystem{}, "drop_items", &["melee_combat"])
             .with(ItemRemoveSystem{}, "remove_items", &["melee_combat"])
+            .with(particle_system::ParticleSpawnSystem{}, "spawn_particles", &["potions", "melee_combat"])
             .build(),
     };
     gs.ecs.register::<Position>();
@@ -422,6 +407,7 @@ fn main() {
     gs.ecs.insert(player_entity);
     gs.ecs.insert(RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame });
     gs.ecs.insert(gamelog::GameLog{ entries : vec!["Welcome to Rusty Roguelike".to_string()] });
+    gs.ecs.insert(particle_system::ParticleBuilder::new());
 
     rltk::main_loop(context, gs);
 }

+ 1 - 1
chapter-18-particles/src/map.rs

@@ -190,7 +190,7 @@ fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
 }
 
 fn wall_glyph(map : &Map, x: i32, y:i32) -> u8 {
-    if x == 0 || x == map.width || y == 0 || y == MAPHEIGHT as i32 { return 35; }
+    if x < 0 || x > map.width-1 || y < 0 || y > map.height-1 as i32 { return 35; }
     let mut mask : u8 = 0;
 
     if is_revealed_and_wall(map, x, y - 1) { mask +=1; }

+ 10 - 19
chapter-18-particles/src/melee_combat_system.rs

@@ -1,8 +1,7 @@
 extern crate specs;
 use specs::prelude::*;
-use super::{CombatStats, WantsToMelee, Name, SufferDamage, gamelog::GameLog, 
-    MeleePowerBonus, DefenseBonus, Equipped, Position, Renderable, ParticleLifetime};
-use rltk::RGB;
+use super::{CombatStats, WantsToMelee, Name, SufferDamage, gamelog::GameLog, MeleePowerBonus, DefenseBonus, Equipped,
+    particle_system::ParticleBuilder, Position};
 
 pub struct MeleeCombatSystem {}
 
@@ -17,14 +16,13 @@ impl<'a> System<'a> for MeleeCombatSystem {
                         ReadStorage<'a, MeleePowerBonus>,
                         ReadStorage<'a, DefenseBonus>,
                         ReadStorage<'a, Equipped>,
-                        WriteStorage<'a, Position>,
-                        WriteStorage<'a, Renderable>,
-                        WriteStorage<'a, ParticleLifetime>
+                        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, 
-            melee_power_bonuses, defense_bonuses, equipped, mut positions, mut renderables, mut particles) = data;
+            melee_power_bonuses, defense_bonuses, equipped, mut particle_builder, positions) = data;
 
         for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
             if stats.hp > 0 {
@@ -46,6 +44,11 @@ impl<'a> System<'a> for MeleeCombatSystem {
                         }
                     }
 
+                    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('‼'), 100.0);
+                    }
+
                     let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus));
 
                     if damage == 0 {
@@ -54,18 +57,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
                         log.entries.insert(0, format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage));
                         inflict_damage.insert(wants_melee.target, SufferDamage{ amount: damage }).expect("Unable to do damage");                        
                     }
-
-                    let pos = positions.get(wants_melee.target);
-                    if let Some(pos) = pos {
-                        let particle = entities.create();
-                        positions.insert(particle, Position{ x:pos.x, y:pos.y }).expect("Unable to insert position");
-                        renderables.insert(particle, 
-                            Renderable{ glyph: rltk::to_cp437('░'),
-                                fg: RGB::named(rltk::CYAN),
-                                bg: RGB::named(rltk::BLACK),
-                                render_order: 0 }).expect("Unable to insert renderable");
-                        particles.insert(particle, ParticleLifetime{ lifetime_ms : 100.0 }).expect("Unable to insert particle lifetime");
-                    }
                 }
             }
         }

+ 8 - 3
chapter-18-particles/src/monster_ai_system.rs

@@ -1,6 +1,6 @@
 extern crate specs;
 use specs::prelude::*;
-use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState, Confusion};
+use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState, Confusion, particle_system::ParticleBuilder};
 extern crate rltk;
 use rltk::{Point};
 
@@ -17,10 +17,12 @@ impl<'a> System<'a> for MonsterAI {
                         ReadStorage<'a, Monster>,
                         WriteStorage<'a, Position>,
                         WriteStorage<'a, WantsToMelee>,
-                        WriteStorage<'a, Confusion>);
+                        WriteStorage<'a, Confusion>,
+                        WriteExpect<'a, ParticleBuilder>);
 
     fn run(&mut self, data : Self::SystemData) {
-        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee, mut confused) = data;
+        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, 
+            monster, mut position, mut wants_to_melee, mut confused, mut particle_builder) = data;
 
         if *runstate != RunState::MonsterTurn { return; }
 
@@ -34,6 +36,9 @@ impl<'a> System<'a> for MonsterAI {
                     confused.remove(entity);
                 }
                 can_act = false;
+
+                particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::MAGENTA), 
+                    rltk::RGB::named(rltk::BLACK), rltk::to_cp437('?'), 100.0);
             }
 
             if can_act {

+ 73 - 0
chapter-18-particles/src/particle_system.rs

@@ -0,0 +1,73 @@
+use specs::prelude::*;
+use super::{ Rltk, ParticleLifetime, Position, Renderable };
+use rltk::RGB;
+
+pub fn cull_dead_particles(ecs : &mut World, ctx : &Rltk) {
+    let mut dead_particles : Vec<Entity> = Vec::new();
+    {
+        // Age out particles
+        let mut particles = ecs.write_storage::<ParticleLifetime>();
+        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: u8,
+    lifetime: f32
+}
+
+pub struct ParticleBuilder {
+    requests : Vec<ParticleRequest>
+}
+
+impl ParticleBuilder {
+    pub fn new() -> ParticleBuilder {
+        ParticleBuilder{ requests : Vec::new() }
+    }
+
+    pub fn request(&mut self, x:i32, y:i32, fg: RGB, bg:RGB, glyph: u8, 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("Unable to inser position");
+            renderables.insert(p, Renderable{ fg: new_particle.fg, bg: new_particle.bg, glyph: new_particle.glyph, render_order: 0 }).expect("Unable to insert renderable");
+            particles.insert(p, ParticleLifetime{ lifetime_ms: new_particle.lifetime }).expect("Unable to insert lifetime");
+        }
+
+        particle_builder.requests.clear();
+    }
+}