Browse Source

Working chapter 10, just needs visuals.

Herbert Wolverson 4 years ago
parent
commit
9ee904503b

+ 120 - 0
book/src/chapter_10.md

@@ -411,6 +411,126 @@ match item_damages {
 }
 ```
 
+If you `cargo run` the project now, you can use magic missile scrolls, fireball scrolls and health potions. 
+
+# Confusion Scrolls
+
+Let's add another item - confusion scrolls. These will target a single entity at range, and make them Confused for a few turns - during which time they will do nothing. We'll start by describing what we want in the item spawning code:
+
+```rust
+fn confusion_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::PINK),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Confusion Scroll".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(Ranged{ range: 6 })
+        .with(Confusion{ turns: 4 })
+        .build();
+}
+```
+
+We'll also add it to the item choices:
+```rust
+fn random_item(ecs: &mut World, x: i32, y: i32) {
+    let roll :i32;
+    {
+        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
+        roll = rng.roll_dice(1, 4);
+    }
+    match roll {
+        1 => { health_potion(ecs, x, y) }
+        2 => { fireball_scroll(ecs, x, y) }
+        3 => { confusion_scroll(ecs, x, y) }
+        _ => { magic_missile_scroll(ecs, x, y) }
+    }
+}
+```
+
+We'll add a new component (and register it!):
+```rust
+#[derive(Component, Debug)]
+pub struct Confusion {
+    pub turns : i32
+}
+```
+
+That's enough to have them appear, be triggerable and cause targeting to happen - but nothing will happen when it is used. We'll add the ability to pass along confusion to the `ItemUseSystem`:
+
+```rust
+// Can it pass along confusion? Note the use of scopes to escape from the borrow checker!
+let mut add_confusion = Vec::new();
+{
+    let causes_confusion = confused.get(useitem.item);
+    match causes_confusion {
+        None => {}
+        Some(confusion) => {
+            used_item = false;
+            for mob in targets.iter() {
+                add_confusion.push((*mob, confusion.turns ));
+                if entity == *player_entity {
+                    let mob_name = names.get(*mob).unwrap();
+                    let item_name = names.get(useitem.item).unwrap();
+                    gamelog.entries.insert(0, format!("You use {} on {}, confusing them.", item_name.name, mob_name.name));
+                }
+            }
+        }
+    }
+}
+for mob in add_confusion.iter() {
+    confused.insert(mob.0, Confusion{ turns: mob.1 }).expect("Unable to insert status");
+}
+```
+
+Alright! Now we can *add* the `Confused` status to anything. We should update the `monster_ai_system` to use it. Replace the loop with:
+
+```rust
+for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() {
+    let mut can_act = true;
+
+    let is_confused = confused.get_mut(entity);
+    if let Some(i_am_confused) = is_confused {
+        i_am_confused.turns -= 1;
+        if i_am_confused.turns < 1 {
+            confused.remove(entity);
+        }
+        can_act = false;
+    }
+
+    if can_act {
+        let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos);
+        if distance < 1.5 {
+            wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack");
+        }
+        else if viewshed.visible_tiles.contains(&*player_pos) {
+            // Path to the player
+            let path = rltk::a_star_search(
+                map.xy_idx(pos.x, pos.y) as i32, 
+                map.xy_idx(player_pos.x, player_pos.y) as i32, 
+                &mut *map
+            );
+            if path.success && path.steps.len()>1 {
+                let mut idx = map.xy_idx(pos.x, pos.y);
+                map.blocked[idx] = false;
+                pos.x = path.steps[1] % map.width;
+                pos.y = path.steps[1] / map.width;
+                idx = map.xy_idx(pos.x, pos.y);
+                map.blocked[idx] = true;
+                viewshed.dirty = true;
+            }
+        }
+    }
+}
+```
+
+If this sees a `Confused` component, it decrements the timer. If the timer hits 0, it removes it. It then returns, making the monster
+
 **The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-10-ranged)**
 
 ---

+ 5 - 0
chapter-10-ranged/src/components.rs

@@ -78,6 +78,11 @@ pub struct AreaOfEffect {
     pub radius : i32
 }
 
+#[derive(Component, Debug)]
+pub struct Confusion {
+    pub turns : i32
+}
+
 #[derive(Component, Debug)]
 pub struct ProvidesHealing {
     pub heal_amount : i32

+ 28 - 3
chapter-10-ranged/src/inventory_system.rs

@@ -2,7 +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};
+    AreaOfEffect, Confusion};
 
 pub struct ItemCollectionSystem {}
 
@@ -47,12 +47,14 @@ impl<'a> System<'a> for ItemUseSystem {
                         ReadStorage<'a, InflictsDamage>,
                         WriteStorage<'a, CombatStats>,
                         WriteStorage<'a, SufferDamage>,
-                        ReadStorage<'a, AreaOfEffect>
+                        ReadStorage<'a, AreaOfEffect>,
+                        WriteStorage<'a, Confusion>
                       );
 
     fn run(&mut self, data : Self::SystemData) {
         let (player_entity, mut gamelog, map, entities, mut wants_use, names, 
-            consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, aoe) = data;
+            consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, 
+            aoe, mut confused) = data;
 
         for (entity, useitem) in (&entities, &wants_use).join() {
             let mut used_item = true;
@@ -123,6 +125,29 @@ impl<'a> System<'a> for ItemUseSystem {
                 }
             }
 
+            // Can it pass along confusion? Note the use of scopes to escape from the borrow checker!
+            let mut add_confusion = Vec::new();
+            {
+                let causes_confusion = confused.get(useitem.item);
+                match causes_confusion {
+                    None => {}
+                    Some(confusion) => {
+                        used_item = false;
+                        for mob in targets.iter() {
+                            add_confusion.push((*mob, confusion.turns ));
+                            if entity == *player_entity {
+                                let mob_name = names.get(*mob).unwrap();
+                                let item_name = names.get(useitem.item).unwrap();
+                                gamelog.entries.insert(0, format!("You use {} on {}, confusing them.", item_name.name, mob_name.name));
+                            }
+                        }
+                    }
+                }
+            }
+            for mob in add_confusion.iter() {
+                confused.insert(mob.0, Confusion{ turns: mob.1 }).expect("Unable to insert status");
+            }
+
             // If its a consumable, we delete it on use
             if used_item {
                 let consumable = consumables.get(useitem.item);

+ 1 - 0
chapter-10-ranged/src/main.rs

@@ -174,6 +174,7 @@ fn main() {
     gs.ecs.register::<WantsToPickupItem>();
     gs.ecs.register::<WantsToUseItem>();
     gs.ecs.register::<WantsToDropItem>();
+    gs.ecs.register::<Confusion>();
 
     let map : Map = Map::new_map_rooms_and_corridors();
     let (player_x, player_y) = map.rooms[0].center();

+ 35 - 21
chapter-10-ranged/src/monster_ai_system.rs

@@ -1,6 +1,6 @@
 extern crate specs;
 use specs::prelude::*;
-use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState};
+use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState, Confusion};
 extern crate rltk;
 use rltk::{Point};
 
@@ -16,33 +16,47 @@ impl<'a> System<'a> for MonsterAI {
                         WriteStorage<'a, Viewshed>, 
                         ReadStorage<'a, Monster>,
                         WriteStorage<'a, Position>,
-                        WriteStorage<'a, WantsToMelee>);
+                        WriteStorage<'a, WantsToMelee>,
+                        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) = 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; }
 
         for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() {
-            let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos);
-            if distance < 1.5 {
-                wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack");
+            let mut can_act = true;
+
+            let is_confused = confused.get_mut(entity);
+            if let Some(i_am_confused) = is_confused {
+                i_am_confused.turns -= 1;
+                if i_am_confused.turns < 1 {
+                    confused.remove(entity);
+                }
+                can_act = false;
             }
-            else if viewshed.visible_tiles.contains(&*player_pos) {
-                // Path to the player
-                let path = rltk::a_star_search(
-                    map.xy_idx(pos.x, pos.y) as i32, 
-                    map.xy_idx(player_pos.x, player_pos.y) as i32, 
-                    &mut *map
-                );
-                if path.success && path.steps.len()>1 {
-                    let mut idx = map.xy_idx(pos.x, pos.y);
-                    map.blocked[idx] = false;
-                    pos.x = path.steps[1] % map.width;
-                    pos.y = path.steps[1] / map.width;
-                    idx = map.xy_idx(pos.x, pos.y);
-                    map.blocked[idx] = true;
-                    viewshed.dirty = true;
+
+            if can_act {
+                let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos);
+                if distance < 1.5 {
+                    wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack");
+                }
+                else if viewshed.visible_tiles.contains(&*player_pos) {
+                    // Path to the player
+                    let path = rltk::a_star_search(
+                        map.xy_idx(pos.x, pos.y) as i32, 
+                        map.xy_idx(player_pos.x, player_pos.y) as i32, 
+                        &mut *map
+                    );
+                    if path.success && path.steps.len()>1 {
+                        let mut idx = map.xy_idx(pos.x, pos.y);
+                        map.blocked[idx] = false;
+                        pos.x = path.steps[1] % map.width;
+                        pos.y = path.steps[1] / map.width;
+                        idx = map.xy_idx(pos.x, pos.y);
+                        map.blocked[idx] = true;
+                        viewshed.dirty = true;
+                    }
                 }
             }
         }

+ 20 - 2
chapter-10-ranged/src/spawner.rs

@@ -3,7 +3,7 @@ use rltk::{ RGB, RandomNumberGenerator };
 extern crate specs;
 use specs::prelude::*;
 use super::{CombatStats, Player, Renderable, Name, Position, Viewshed, Monster, BlocksTile, Rect, Item, 
-    Consumable, Ranged, ProvidesHealing, map::MAPWIDTH, InflictsDamage, AreaOfEffect};
+    Consumable, Ranged, ProvidesHealing, map::MAPWIDTH, InflictsDamage, AreaOfEffect, Confusion};
 
 /// Spawns the player and returns his/her entity object.
 pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
@@ -95,11 +95,12 @@ fn random_item(ecs: &mut World, x: i32, y: i32) {
     let roll :i32;
     {
         let mut rng = ecs.write_resource::<RandomNumberGenerator>();
-        roll = rng.roll_dice(1, 3);
+        roll = rng.roll_dice(1, 4);
     }
     match roll {
         1 => { health_potion(ecs, x, y) }
         2 => { fireball_scroll(ecs, x, y) }
+        3 => { confusion_scroll(ecs, x, y) }
         _ => { magic_missile_scroll(ecs, x, y) }
     }
 }
@@ -173,4 +174,21 @@ fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {
         .with(InflictsDamage{ damage: 20 })
         .with(AreaOfEffect{ radius: 3 })
         .build();
+}
+
+fn confusion_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::PINK),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Confusion Scroll".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(Ranged{ range: 6 })
+        .with(Confusion{ turns: 4 })
+        .build();
 }