Browse Source

Worked up to RangedScrolls/Targeting

Simon Watson 1 year ago
parent
commit
a63ec3d54e
13 changed files with 914 additions and 73 deletions
  1. 19 0
      notes.md
  2. 68 0
      src/components.rs
  3. 52 0
      src/damage_system.rs
  4. 4 0
      src/gamelog.rs
  5. 179 0
      src/gui.rs
  6. 101 0
      src/inventory_system.rs
  7. 120 46
      src/main.rs
  8. 63 9
      src/map.rs
  9. 31 0
      src/map_indexing_system.rs
  10. 38 0
      src/melee_combat_system.rs
  11. 37 10
      src/monster_ai_system.rs
  12. 73 8
      src/player.rs
  13. 129 0
      src/spawner.rs

+ 19 - 0
notes.md

@@ -0,0 +1,19 @@
+# Saint Anthony's Fire
+
+I intend to branch off from the tutorial at some point and have the game be
+about playing as St. Anthony being tested in the desert. I think the core
+gameplay mechanics should be a spellcrafting system and a faith system.
+
+# Ideas
+* Spellcrafting System
+A limited number of discrete spell attributes that can be combined
+to form spells.
+
+* Faith System
+Some kind of 'mana' esque system that's based on player decision
+making. This may be beyond my abilities, but if thematically the player
+is playing as a Saint, there should be come component in the gameplay
+that relates to that.
+* * Sacrifice turns while in combat to gain faith (Pacifism)
+* * Convert HP to Faith (mana) (Flagilation) OR
+* * * No HP/Mana, shared resource pool that is used for HP and casting (Faith)

+ 68 - 0
src/components.rs

@@ -3,6 +3,73 @@ use specs_derive::*;
 use rltk::{RGB};
 
 // COMPONENTS
+#[derive(Component, Debug)]
+pub struct Consumable {}
+
+#[derive(Component, Debug, Clone)]
+pub struct WantsToDropItem {
+    pub item : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct WantsToDrinkPotion {
+    pub potion : Entity
+}
+
+#[derive(Component, Debug, Clone)]
+pub struct WantsToPickupItem {
+    pub collected_by : Entity,
+    pub item : Entity
+}
+
+#[derive(Component, Debug, Clone)]
+pub struct InBackpack {
+    pub owner : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct Item {}
+
+#[derive(Component, Debug)]
+pub struct Potion {
+    pub heal_amount : i32
+}
+
+#[derive(Component, Debug)]
+pub struct SufferDamage {
+    pub amount : Vec<i32>
+}
+impl SufferDamage {
+    pub fn new_damage(store: &mut WriteStorage<SufferDamage>, victim: Entity, amount: i32) {
+        if let Some(suffering) = store.get_mut(victim) {
+            suffering.amount.push(amount);
+        } else {
+            let dmg = SufferDamage { amount : vec![amount] };
+            store.insert(victim, dmg).expect("Unable to insert damage");
+        }
+    }
+}
+
+#[derive(Component, Debug, Clone)]
+pub struct WantsToMelee {
+    pub target : Entity
+}
+
+#[derive(Component, Debug)]
+pub struct CombatStats {
+    pub max_hp : i32,
+    pub hp : i32,
+    pub defense : i32,
+    pub power : i32
+}
+
+#[derive(Component, Debug)]
+pub struct BlocksTile {}
+
+#[derive(Component, Debug)]
+pub struct Name {
+    pub name : String
+}
 
 #[derive(Component, Debug)]
 pub struct Monster {}
@@ -25,6 +92,7 @@ pub struct Renderable {
     pub glyph: rltk::FontCharType,
     pub fg: RGB,
     pub bg: RGB,
+    pub render_order : i32
 }
 
 #[derive(Component, Debug)]

+ 52 - 0
src/damage_system.rs

@@ -0,0 +1,52 @@
+use specs::prelude::*;
+use super::{CombatStats, SufferDamage, Player, GameLog, Name};
+use rltk::{console};
+
+pub struct DamageSystem {}
+
+pub fn delete_the_dead(ecs : &mut World) {
+    let mut dead : Vec<Entity> = Vec::new();
+    // Using a scope to make the borrow checker happy
+    {
+        let combat_stats = ecs.read_storage::<CombatStats>();
+        let players = ecs.read_storage::<Player>();
+        let entities = ecs.entities();
+        let names = ecs.read_storage::<Name>();
+        let mut log = ecs.write_resource::<GameLog>();
+
+        for (entity, stats) in (&entities, &combat_stats).join() {
+            if stats.hp < 1 { 
+                let player = players.get(entity);
+                match player {
+                    None => { 
+                        let victim_name = names.get(entity);
+                        if let Some(victim_name) = victim_name {
+                            log.entries.push(format!("{} is dead", &victim_name.name));
+                        }
+                        dead.push(entity);
+                    },
+                    Some(_) => console::log("You are dead")
+                }
+            }
+        }
+    }
+
+    for victim in dead {
+        ecs.delete_entity(victim).expect("Unable to delete");
+    }    
+}
+
+impl<'a> System<'a> for DamageSystem {
+    type SystemData = ( WriteStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage> );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut stats, mut damage) = data;
+
+        for (mut stats, damage) in (&mut stats, &damage).join() {
+            stats.hp -= damage.amount.iter().sum::<i32>();
+        }
+
+        damage.clear();
+    }
+}

+ 4 - 0
src/gamelog.rs

@@ -0,0 +1,4 @@
+
+pub struct GameLog {
+    pub entries : Vec<String>
+}

+ 179 - 0
src/gui.rs

@@ -0,0 +1,179 @@
+use rltk::{ RGB, Rltk, VirtualKeyCode};
+use specs::prelude::*;
+use super::{CombatStats, Player, GameLog, 
+            MAPHEIGHT, Map, Name, 
+            Position, Point, InBackpack,
+            State};
+
+const GUI_HEIGHT: usize = 50 - MAPHEIGHT - 1;
+
+pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
+    ctx.draw_box(0, 38, 79, GUI_HEIGHT, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+
+    let combat_stats = ecs.read_storage::<CombatStats>();
+    let players = ecs.read_storage::<Player>();
+    for (_player, stats) in (&players, &combat_stats).join() {
+        let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
+        ctx.print_color(12, 38, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health);
+
+        ctx.draw_bar_horizontal(28, 38, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK));
+    }
+
+    let log = ecs.fetch::<GameLog>();
+
+    let mut y = 40;
+    for s in log.entries.iter().rev() {
+        if y < 49 { ctx.print(2, y, s); }
+        y += 1;
+    }
+
+    // Draw mouse cursor
+    let mouse_pos = ctx.mouse_pos();
+    ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::ANTIQUE_WHITE));
+
+    draw_tooltips(ecs, ctx);
+
+}
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum ItemMenuResult { Cancel, NoResponse, Selected }
+
+pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+    let player_entity = gs.ecs.fetch::<Entity>();
+    let names = gs.ecs.read_storage::<Name>();
+    let backpack = gs.ecs.read_storage::<InBackpack>();
+    let entities = gs.ecs.entities();
+
+    let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+    let count = inventory.count();
+
+    let mut y = (25 - (count / 2)) as i32;
+    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Inventory");
+    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+    let mut equippable : Vec<Entity> = Vec::new();
+    let mut j = 0;
+    for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+        ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+        ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as rltk::FontCharType);
+        ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+        ctx.print(21, y, &name.name.to_string());
+        equippable.push(entity);
+        y += 1;
+        j += 1;
+    }
+
+    match ctx.key {
+        None => (ItemMenuResult::NoResponse, None),
+        Some(key) => {
+            match key {
+                VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+                _ => { 
+                    let selection = rltk::letter_to_option(key);
+                    if selection > -1 && selection < count as i32 {
+                        return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+                    }  
+                    (ItemMenuResult::NoResponse, None)
+                }
+            }
+        }
+    }
+}
+
+pub fn drop_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+    let player_entity = gs.ecs.fetch::<Entity>();
+    let names = gs.ecs.read_storage::<Name>();
+    let backpack = gs.ecs.read_storage::<InBackpack>();
+    let entities = gs.ecs.entities();
+
+    let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+    let count = inventory.count();
+
+    let mut y = (25 - (count / 2)) as i32;
+    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Drop Which Item?");
+    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+    let mut equippable : Vec<Entity> = Vec::new();
+    let mut j = 0;
+    for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+        ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+        ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as rltk::FontCharType);
+        ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+        ctx.print(21, y, &name.name.to_string());
+        equippable.push(entity);
+        y += 1;
+        j += 1;
+    }
+
+    match ctx.key {
+        None => (ItemMenuResult::NoResponse, None),
+        Some(key) => {
+            match key {
+                VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+                _ => { 
+                    let selection = rltk::letter_to_option(key);
+                    if selection > -1 && selection < count as i32 {
+                        return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+                    }  
+                    (ItemMenuResult::NoResponse, None)
+                }
+            }
+        }
+    }
+}
+
+fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {
+    let map = ecs.fetch::<Map>();
+    let names = ecs.read_storage::<Name>();
+    let positions = ecs.read_storage::<Position>();
+
+    let mouse_pos = ctx.mouse_pos();
+    if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; }
+    let mut tooltip : Vec<String> = Vec::new();
+    for (name, position) in (&names, &positions).join() {
+        let idx = map.xy_idx(position.x, position.y);
+        if position.x == mouse_pos.0 && position.y == mouse_pos.1 && map.visible_tiles[idx] {
+            tooltip.push(name.name.to_string());
+        }
+    }
+
+    if !tooltip.is_empty() {
+        let mut width :i32 = 0;
+        for s in tooltip.iter() {
+            if width < s.len() as i32 { width = s.len() as i32; }
+        }
+        width += 3;
+
+        if mouse_pos.0 > 40 {
+            let arrow_pos = Point::new(mouse_pos.0 - 2, mouse_pos.1);
+            let left_x = mouse_pos.0 - width;
+            let mut y = mouse_pos.1;
+            for s in tooltip.iter() {
+                ctx.print_color(left_x, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s);
+                let padding = (width - s.len() as i32)-1;
+                for i in 0..padding {
+                    ctx.print_color(arrow_pos.x - i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+                }
+                y += 1;
+            }
+            ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"->".to_string());
+        } else {
+            let arrow_pos = Point::new(mouse_pos.0 + 1, mouse_pos.1);
+            let left_x = mouse_pos.0 +3;
+            let mut y = mouse_pos.1;
+            for s in tooltip.iter() {
+                ctx.print_color(left_x + 1, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s);
+                let padding = (width - s.len() as i32)-1;
+                for i in 0..padding {
+                    ctx.print_color(arrow_pos.x + 1 + i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+                }
+                y += 1;
+            }
+            ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string());
+        }
+    }
+}

+ 101 - 0
src/inventory_system.rs

@@ -0,0 +1,101 @@
+use specs::prelude::*;
+use super::{WantsToPickupItem, Name, InBackpack, 
+            Position, GameLog, WantsToDrinkPotion,
+            CombatStats, Potion, WantsToDropItem};
+
+pub struct ItemCollectionSystem {}
+
+impl<'a> System<'a> for ItemCollectionSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        WriteStorage<'a, WantsToPickupItem>,
+                        WriteStorage<'a, Position>,
+                        ReadStorage<'a, Name>,
+                        WriteStorage<'a, InBackpack>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, mut wants_pickup, mut positions, names, mut backpack) = data;
+
+        for pickup in wants_pickup.join() {
+            positions.remove(pickup.item);
+            backpack.insert(pickup.item, InBackpack{ owner: pickup.collected_by }).expect("Unable to insert backpack entry");
+
+            if pickup.collected_by == *player_entity {
+                gamelog.entries.push(format!("You pick up the {}.", names.get(pickup.item).unwrap().name));
+            }
+        }
+
+        wants_pickup.clear();
+    }
+}
+
+pub struct PotionUseSystem {}
+
+impl<'a> System<'a> for PotionUseSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToDrinkPotion>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, Potion>,
+                        WriteStorage<'a, CombatStats>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, entities, mut wants_drink, names, potions, mut combat_stats) = data;
+
+        for (entity, drink, stats) in (&entities, &wants_drink, &mut combat_stats).join() {
+            let potion = potions.get(drink.potion);
+            match potion {
+                None => {}
+                Some(potion) => {
+                    stats.hp = i32::min(stats.max_hp, stats.hp + potion.heal_amount);
+                    if entity == *player_entity {
+                        gamelog.entries.push(format!("You drink the {}, healing {} hp.", names.get(drink.potion).unwrap().name, potion.heal_amount));
+                    }
+                    entities.delete(drink.potion).expect("Delete failed");
+                }
+            }
+        }
+
+        wants_drink.clear();
+    }
+}
+
+pub struct ItemDropSystem {}
+
+impl<'a> System<'a> for ItemDropSystem {
+    #[allow(clippy::type_complexity)]
+    type SystemData = ( ReadExpect<'a, Entity>,
+                        WriteExpect<'a, GameLog>,
+                        Entities<'a>,
+                        WriteStorage<'a, WantsToDropItem>,
+                        ReadStorage<'a, Name>,
+                        WriteStorage<'a, Position>,
+                        WriteStorage<'a, InBackpack>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (player_entity, mut gamelog, entities, mut wants_drop, names, mut positions, mut backpack) = data;
+
+        for (entity, to_drop) in (&entities, &wants_drop).join() {
+            let mut dropper_pos : Position = Position{x:0, y:0};
+            {
+                let dropped_pos = positions.get(entity).unwrap();
+                dropper_pos.x = dropped_pos.x;
+                dropper_pos.y = dropped_pos.y;
+            }
+            positions.insert(to_drop.item, Position{ x : dropper_pos.x, y : dropper_pos.y }).expect("Unable to insert position");
+            backpack.remove(to_drop.item);
+
+            if entity == *player_entity {
+                gamelog.entries.push(format!("You drop the {}.", names.get(to_drop.item).unwrap().name));
+            }
+        }
+
+        wants_drop.clear();
+    }
+}

+ 120 - 46
src/main.rs

@@ -1,4 +1,4 @@
-use rltk::{GameState, Rltk, RGB};
+use rltk::{GameState, Rltk, Point};
 use specs::prelude::*;
 
 mod components;
@@ -13,19 +13,34 @@ mod visibility_system;
 use visibility_system::VisibilitySystem;
 mod monster_ai_system;
 use monster_ai_system::MonsterAI;
+mod map_indexing_system;
+use map_indexing_system::MapIndexingSystem;
+mod melee_combat_system;
+use melee_combat_system::MeleeCombatSystem;
+mod damage_system;
+use damage_system::DamageSystem;
+mod gui;
+mod gamelog;
+use gamelog::GameLog;
+mod spawner;
+mod inventory_system;
+use inventory_system::*;
 
 // ***** //
 // STATE //
 
 #[derive(PartialEq, Copy, Clone)]
 pub enum RunState { 
-    Paused, 
-    Running 
+    AwaitingInput,
+    PreRun,
+    PlayerTurn,
+    MonsterTurn,
+    ShowInventory,
+    ShowDropItem,
 }
 
 pub struct State {
     pub ecs: World,
-    pub runstate: RunState
 }
 
 impl State {
@@ -34,6 +49,18 @@ impl State {
         vis.run_now(&self.ecs);
         let mut mob = MonsterAI{};
         mob.run_now(&self.ecs);
+        let mut mapindex = MapIndexingSystem{};
+        mapindex.run_now(&self.ecs);
+        let mut mcs = MeleeCombatSystem{};
+        mcs.run_now(&self.ecs);
+        let mut dmgs = DamageSystem{};
+        dmgs.run_now(&self.ecs);
+        let mut pickup = ItemCollectionSystem{};
+        pickup.run_now(&self.ecs);
+        let mut potions = PotionUseSystem{};
+        potions.run_now(&self.ecs);
+        let mut drop_items = ItemDropSystem{};
+        drop_items.run_now(&self.ecs);
         self.ecs.maintain();
     }
 }
@@ -41,25 +68,79 @@ impl State {
 impl GameState for State {
     fn tick(&mut self, ctx : &mut Rltk) {
         ctx.cls();
-        
-        if self.runstate == RunState::Running {
-            self.run_systems();
-            self.runstate = RunState::Paused;
-        } else {
-            self.runstate = player_input(self, ctx);
+        let mut newrunstate;
+        {
+            let runstate = self.ecs.fetch::<RunState>();
+            newrunstate = *runstate;
         }
 
+        match newrunstate {
+            RunState::PreRun => {
+                self.run_systems();
+                self.ecs.maintain();
+                newrunstate = RunState::AwaitingInput;
+            }
+            RunState::AwaitingInput => {
+                newrunstate = player_input(self, ctx);
+            }
+            RunState::PlayerTurn => {
+                self.run_systems();
+                self.ecs.maintain();
+                newrunstate = RunState::MonsterTurn;
+            }
+            RunState::MonsterTurn => {
+                self.run_systems();
+                self.ecs.maintain();
+                newrunstate = RunState::AwaitingInput;
+            }
+            RunState::ShowInventory => {
+                let result = gui::show_inventory(self, ctx);
+                match result.0 {
+                    gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+                    gui::ItemMenuResult::NoResponse => {}
+                    gui::ItemMenuResult::Selected => {
+                        let item_entity = result.1.unwrap();
+                        let mut intent = self.ecs.write_storage::<WantsToDrinkPotion>();
+                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToDrinkPotion{ potion: item_entity }).expect("Unable to insert intent");
+                        newrunstate = RunState::PlayerTurn;
+                    }
+                }
+            }
+            RunState::ShowDropItem => {
+                let result = gui::drop_item_menu(self, ctx);
+                match result.0 {
+                    gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+                    gui::ItemMenuResult::NoResponse => {}
+                    gui::ItemMenuResult::Selected => {
+                        let item_entity = result.1.unwrap();
+                        let mut intent = self.ecs.write_storage::<WantsToDropItem>();
+                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem{ item: item_entity }).expect("Unable to insert intent");
+                        newrunstate = RunState::PlayerTurn;
+                    }
+                }
+            }
+        }
+
+        {
+            let mut runwriter = self.ecs.write_resource::<RunState>();
+            *runwriter = newrunstate;
+        }
+        damage_system::delete_the_dead(&mut self.ecs);
+
         draw_map(&self.ecs, ctx);
 
         let positions = self.ecs.read_storage::<Position>();
         let renderables = self.ecs.read_storage::<Renderable>();
         let map = self.ecs.fetch::<Map>();
 
-        for (pos, render) in (&positions, &renderables).join() {
+        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);
     }
 }
 
@@ -69,11 +150,13 @@ impl GameState for State {
 fn main() -> rltk::BError {
     use rltk::RltkBuilder;
     let context = RltkBuilder::simple80x50()
-        .with_title("Roguelike Tutorial")
+        .with_title("Saint Antony's Fire")
         .build()?;
+    // Add scanlines
+    //context.with_post_scanlines(true);
+
     let mut gs = State{ 
         ecs: World::new(),
-        runstate: RunState::Running
     };
 
     gs.ecs.register::<Position>();
@@ -81,47 +164,38 @@ fn main() -> rltk::BError {
     gs.ecs.register::<Player>();
     gs.ecs.register::<Viewshed>();
     gs.ecs.register::<Monster>();
+    gs.ecs.register::<Name>();
+    gs.ecs.register::<BlocksTile>();
+    gs.ecs.register::<CombatStats>();
+    gs.ecs.register::<WantsToMelee>();
+    gs.ecs.register::<SufferDamage>();
+    gs.ecs.register::<Item>();
+    gs.ecs.register::<Potion>();
+    gs.ecs.register::<InBackpack>();
+    gs.ecs.register::<WantsToPickupItem>();
+    gs.ecs.register::<WantsToDrinkPotion>();
+    gs.ecs.register::<WantsToDropItem>();
+
 
     let map = Map::new_map_rooms_and_corridors();
     let (player_x, player_y) = map.rooms[0].center();
 
-    let mut rng = rltk::RandomNumberGenerator::new();
-    for room in map.rooms.iter().skip(1) {
-        let (x,y) = room.center();
-    
-        let glyph : rltk::FontCharType;
-        let roll = rng.roll_dice(1, 2);
-        match roll {
-            1 => { glyph = rltk::to_cp437('g') }
-            _ => { glyph = rltk::to_cp437('o') }
-        }
+    let player_entity = spawner::player(&mut gs.ecs, player_x, player_y);
     
-        gs.ecs.create_entity()
-            .with(Position{ x, y })
-            .with(Renderable{
-                glyph: glyph,
-                fg: RGB::named(rltk::RED),
-                bg: RGB::named(rltk::BLACK),
-            })
-            .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
-            .with(Monster{})
-            .build();
+    gs.ecs.insert(player_entity);
+
+    let rng = rltk::RandomNumberGenerator::new();
+    gs.ecs.insert(rng);
+
+    for room in map.rooms.iter().skip(1) {
+        spawner::spawn_room(&mut gs.ecs, room);
     }
 
     gs.ecs.insert(map);
+    gs.ecs.insert(Point::new(player_x, player_y));
+    gs.ecs.insert(RunState::PreRun);
 
-    gs.ecs
-        .create_entity()
-        .with(Position { x: player_x, y: player_y })
-        .with(Renderable {
-            glyph: rltk::to_cp437('@'),
-            fg: RGB::named(rltk::YELLOW),
-            bg: RGB::named(rltk::BLACK),
-    })
-    .with(Player{})
-    .with(Viewshed{ visible_tiles : Vec::new(), range : 8, dirty: true })
-    .build();
-    
+    gs.ecs.insert(gamelog::GameLog{ entries : vec!["Saint Antony casts out...".to_string()] });
 
     rltk::main_loop(context, gs)
 }

+ 63 - 9
src/map.rs

@@ -3,6 +3,10 @@ use super::{Rect};
 use std::cmp::{max, min};
 use specs::prelude::*;
 
+pub const MAPWIDTH : usize = 80;
+pub const MAPHEIGHT : usize = 38;
+pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
+
 #[derive(PartialEq, Copy, Clone)]
 pub enum TileType {
     Wall, 
@@ -15,9 +19,29 @@ pub struct Map {
     pub width: i32,
     pub height: i32,
     pub revealed_tiles : Vec<bool>,
-    pub visible_tiles: Vec<bool>
+    pub visible_tiles: Vec<bool>,
+    pub blocked : Vec<bool>,
+    pub tile_content : Vec<Vec<Entity>>
 }
 impl Map {
+    pub fn clear_content_index(&mut self) {
+        for content in self.tile_content.iter_mut() {
+            content.clear();
+        }
+    }
+
+    pub fn populate_blocked(&mut self) {
+        for (i,tile) in self.tiles.iter_mut().enumerate() {
+            self.blocked[i] = *tile == TileType::Wall;
+        }
+   }
+
+    fn is_exit_valid(&self, x:i32, y:i32) -> bool {
+        if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; }
+        let idx = self.xy_idx(x, y);
+        !self.blocked[idx]
+    }
+
     pub fn xy_idx(&self, x: i32, y: i32) -> usize {
         (y as usize * self.width as usize) + x as usize
     }
@@ -51,12 +75,14 @@ impl Map {
 
     pub fn new_map_rooms_and_corridors() -> Map {
         let mut map = Map{
-            tiles : vec![TileType::Wall; 80*50],
+            tiles : vec![TileType::Wall; MAPCOUNT],
             rooms : Vec::new(),
-            width : 80,
-            height: 50,
-            revealed_tiles : vec![false; 80*50],
-            visible_tiles : vec![false; 80*50],
+            width : MAPWIDTH as i32,
+            height: MAPHEIGHT as i32,
+            revealed_tiles : vec![false; MAPCOUNT],
+            visible_tiles : vec![false; MAPCOUNT],
+            blocked : vec![false; MAPCOUNT],
+            tile_content : vec![Vec::new(); MAPCOUNT]
         };
     
         const MAX_ROOMS : i32 = 30;
@@ -109,12 +135,40 @@ impl BaseMap for Map {
     fn is_opaque(&self, idx:usize) -> bool {
         self.tiles[idx as usize] == TileType::Wall
     }
+
+    fn get_available_exits(&self, idx:usize) -> rltk::SmallVec<[(usize, f32); 10]> {
+        let mut exits = rltk::SmallVec::new();
+        let x = idx as i32 % self.width;
+        let y = idx as i32 / self.width;
+        let w = self.width as usize;
+    
+        // Cardinal directions
+        if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) };
+        if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) };
+        if self.is_exit_valid(x, y-1) { exits.push((idx-w, 1.0)) };
+        if self.is_exit_valid(x, y+1) { exits.push((idx+w, 1.0)) };
+
+        // Diagonals
+        if self.is_exit_valid(x-1, y-1) { exits.push(((idx-w)-1, 1.45)); }
+        if self.is_exit_valid(x+1, y-1) { exits.push(((idx-w)+1, 1.45)); }
+        if self.is_exit_valid(x-1, y+1) { exits.push(((idx+w)-1, 1.45)); }
+        if self.is_exit_valid(x+1, y+1) { exits.push(((idx+w)+1, 1.45)); }
+    
+        exits
+    }
+
+    fn get_pathing_distance(&self, idx1:usize, idx2:usize) -> f32 {
+        let w = self.width as usize;
+        let p1 = Point::new(idx1 % w, idx1 / w);
+        let p2 = Point::new(idx2 % w, idx2 / w);
+        rltk::DistanceAlg::Pythagoras.distance2d(p1, p2)
+    }
 }
 
 // /// Makes a map with solid boundaries and 400 randomly placed walls. No guarantees that it won't
 // /// look awful.
 // pub fn new_map_test() -> Vec<TileType> {
-//     let mut map = vec![TileType::Floor; 80*50];
+//     let mut map = vec![TileType::Floor; MAPCOUNT];
 
 //     // Make the boundaries walls
 //     for x in 0..80 {
@@ -155,11 +209,11 @@ pub fn draw_map(ecs: &World, ctx : &mut Rltk) {
             match tile {
                 TileType::Floor => {
                     glyph = rltk::to_cp437('.');
-                    fg = RGB::from_f32(0.0, 0.5, 0.5);
+                    fg = RGB::from_f32(1.0, 0.5, 0.7);
                 }
                 TileType::Wall => {
                     glyph = rltk::to_cp437('#');
-                    fg = RGB::from_f32(0., 1.0, 0.);
+                    fg = RGB::from_f32(1.0, 0.6, 0.);
                 }
             }
             if !map.visible_tiles[idx] { fg = fg.to_greyscale() }

+ 31 - 0
src/map_indexing_system.rs

@@ -0,0 +1,31 @@
+use specs::prelude::*;
+use super::{Map, Position, BlocksTile};
+
+pub struct MapIndexingSystem {}
+
+impl<'a> System<'a> for MapIndexingSystem {
+    type SystemData = ( WriteExpect<'a, Map>,
+                        ReadStorage<'a, Position>,
+                        ReadStorage<'a, BlocksTile>,
+                        Entities<'a>,);
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut map, position, blockers, entities) = data;
+
+        map.populate_blocked();
+        map.clear_content_index();
+        for (entity, position) in (&entities, &position).join() {
+            let idx = map.xy_idx(position.x, position.y);
+
+            // If they block, update the blocking list
+            let _p : Option<&BlocksTile> = blockers.get(entity);
+            if let Some(_p) = _p {
+                map.blocked[idx] = true;
+            }
+
+            // Push the entity to the appropriate index slot. It's a Copy
+            // type, so we don't need to clone it (we want to avoid moving it out of the ECS!)
+            map.tile_content[idx].push(entity);
+        }
+    }
+}

+ 38 - 0
src/melee_combat_system.rs

@@ -0,0 +1,38 @@
+use specs::prelude::*;
+use super::{CombatStats, WantsToMelee, Name, SufferDamage, GameLog};
+
+pub struct MeleeCombatSystem {}
+
+impl<'a> System<'a> for MeleeCombatSystem {
+    type SystemData = ( Entities<'a>,
+                        WriteExpect<'a, GameLog>,
+                        WriteStorage<'a, WantsToMelee>,
+                        ReadStorage<'a, Name>,
+                        ReadStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage>
+                      );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data;
+
+        for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
+            if stats.hp > 0 {
+                let target_stats = combat_stats.get(wants_melee.target).unwrap();
+                if target_stats.hp > 0 {
+                    let target_name = names.get(wants_melee.target).unwrap();
+
+                    let damage = i32::max(0, stats.power - target_stats.defense);
+
+                    if damage == 0 {
+                        log.entries.push(format!("{} is unable to hurt {}", &name.name, &target_name.name));
+                    } else {
+                        log.entries.push(format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage));
+                        SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage);
+                    }
+                }
+            }
+        }
+
+        wants_melee.clear();
+    }
+}

+ 37 - 10
src/monster_ai_system.rs

@@ -1,19 +1,46 @@
 use specs::prelude::*;
-use super::{Viewshed, Position, Map, Monster};
-use rltk::{field_of_view, Point, console};
+use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState};
+use rltk::{Point};
 
 pub struct MonsterAI {}
 
 impl<'a> System<'a> for MonsterAI {
-    type SystemData = ( ReadStorage<'a, Viewshed>, 
-                        ReadStorage<'a, Position>,
-                        ReadStorage<'a, Monster>);
+    #[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>);
 
     fn run(&mut self, data : Self::SystemData) {
-        let (viewshed, pos, monster) = data;
-
-        for (viewshed,pos,_monster) in (&viewshed, &pos, &monster).join() {
-            console::log("Monster considers their own existence");
+        let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee) = 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");
+            }
+            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),
+                    map.xy_idx(player_pos.x, player_pos.y),
+                    &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] as i32 % map.width;
+                    pos.y = path.steps[1] as i32 / map.width;
+                    idx = map.xy_idx(pos.x, pos.y);
+                    map.blocked[idx] = true;
+                    viewshed.dirty = true;
+                }
+            }
         }
     }
-}
+}

+ 73 - 8
src/player.rs

@@ -1,20 +1,64 @@
-use rltk::{VirtualKeyCode, Rltk};
+use rltk::{VirtualKeyCode, Rltk, Point};
 use specs::prelude::*;
-use super::{Position, Player, TileType, Map, State, Viewshed, RunState};
+use super::{Position, Player, WantsToMelee, 
+            Map, State, Viewshed, 
+            RunState, CombatStats, WantsToPickupItem,
+            GameLog, Item};
 use std::cmp::{min, max};
 
+fn get_item(ecs: &mut World) {
+    let player_pos = ecs.fetch::<Point>();
+    let player_entity = ecs.fetch::<Entity>();
+    let entities = ecs.entities();
+    let items = ecs.read_storage::<Item>();
+    let positions = ecs.read_storage::<Position>();
+    let mut gamelog = ecs.fetch_mut::<GameLog>();    
+
+    let mut target_item : Option<Entity> = None;
+    for (item_entity, _item, position) in (&entities, &items, &positions).join() {
+        if position.x == player_pos.x && position.y == player_pos.y {
+            target_item = Some(item_entity);
+        }
+    }
+
+    match target_item {
+        None => gamelog.entries.push("There is nothing here to pick up.".to_string()),
+        Some(item) => {
+            let mut pickup = ecs.write_storage::<WantsToPickupItem>();
+            pickup.insert(*player_entity, WantsToPickupItem{ collected_by: *player_entity, item }).expect("Unable to insert want to pickup");
+        }
+    }
+}
+
 pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
     let mut positions = ecs.write_storage::<Position>();
-    let mut players = ecs.write_storage::<Player>();
+    let players = ecs.write_storage::<Player>();
     let mut viewsheds = ecs.write_storage::<Viewshed>();
+    let combat_stats = ecs.read_storage::<CombatStats>();
     let map = ecs.fetch::<Map>();
+    let entities = ecs.entities();
+    let mut wants_to_melee = ecs.write_storage::<WantsToMelee>();
 
-    for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() {
+    for (entity, _player, pos, viewshed) in (&entities, &players, &mut positions, &mut viewsheds).join() {
+        if pos.x + delta_x < 1 || pos.x + delta_x > map.width-1 || pos.y + delta_y < 1 || pos.y + delta_y > map.height-1 { return; }
         let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y);
-        if map.tiles[destination_idx] != TileType::Wall {
+        
+        for potential_target in map.tile_content[destination_idx].iter() {
+            let target = combat_stats.get(*potential_target);
+            if let Some(_target) = target {
+                wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target }).expect("Add target failed");
+                return;
+            }
+        }
+
+        if !map.blocked[destination_idx] {
             pos.x = min(79 , max(0, pos.x + delta_x));
             pos.y = min(49, max(0, pos.y + delta_y));
 
+            let mut ppos = ecs.write_resource::<Point>();
+            ppos.x = pos.x;
+            ppos.y = pos.y;
+
             viewshed.dirty = true;
         }
     }
@@ -23,7 +67,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
 pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
     // Player movement
     match ctx.key {
-        None => {} // Nothing happened
+        None => { return RunState::AwaitingInput } // Nothing happened
         Some(key) => match key {
             VirtualKeyCode::Left |
             VirtualKeyCode::Numpad4 |
@@ -45,8 +89,29 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
             VirtualKeyCode::S |
             VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs),
 
-            _ => {}
+            // Diagonals
+            VirtualKeyCode::Numpad9 |
+            VirtualKeyCode::E |
+            VirtualKeyCode::Y => try_move_player(1, -1, &mut gs.ecs),
+
+            VirtualKeyCode::Numpad7 |
+            VirtualKeyCode::Q |
+            VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs),
+
+            VirtualKeyCode::Numpad3 |
+            VirtualKeyCode::C |
+            VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs),
+
+            VirtualKeyCode::Numpad1 |
+            VirtualKeyCode::Z |
+            VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs),
+
+            VirtualKeyCode::G => get_item(&mut gs.ecs),
+            VirtualKeyCode::I => return RunState::ShowInventory,
+            VirtualKeyCode::X => return RunState::ShowDropItem,
+            
+            _ => { return RunState::AwaitingInput }
         },
     }
-    RunState::Running
+    RunState::PlayerTurn
 }

+ 129 - 0
src/spawner.rs

@@ -0,0 +1,129 @@
+use rltk::{ RGB, RandomNumberGenerator };
+use specs::prelude::*;
+use super::{CombatStats, Player, Renderable, 
+            Name, Position, Viewshed, 
+            Monster, BlocksTile, Rect, 
+            MAPWIDTH, Item, Potion};
+
+const MAX_MONSTERS : i32 = 4;
+const MAX_ITEMS : i32 = 2;
+
+fn health_potion(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437('¡'),
+            fg: RGB::named(rltk::MAGENTA),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Health Potion".to_string() })
+        .with(Item{})
+        .with(Potion{ heal_amount: 12 })
+        .build();
+}
+
+/// Fills a room with stuff!
+pub fn spawn_room(ecs: &mut World, room : &Rect) {
+    let mut monster_spawn_points : Vec<usize> = Vec::new();
+    let mut item_spawn_points : Vec<usize> = Vec::new();
+
+    // Scope to keep the borrow checker happy
+    {
+        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
+        let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;
+        let num_items = rng.roll_dice(1, MAX_ITEMS + 2) - 3;
+
+
+        for _i in 0 .. num_monsters {
+            let mut added = false;
+            while !added {
+                let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
+                let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
+                let idx = (y * MAPWIDTH) + x;
+                if !monster_spawn_points.contains(&idx) {
+                    monster_spawn_points.push(idx);
+                    added = true;
+                }
+            }
+        }
+
+        for _i in 0 .. num_items {
+            let mut added = false;
+            while !added {
+                let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
+                let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
+                let idx = (y * MAPWIDTH) + x;
+                if !item_spawn_points.contains(&idx) {
+                    item_spawn_points.push(idx);
+                    added = true;
+                }
+            }
+        }
+    }
+
+    // Actually spawn the monsters
+    for idx in monster_spawn_points.iter() {
+        let x = *idx % MAPWIDTH;
+        let y = *idx / MAPWIDTH;
+        random_monster(ecs, x as i32, y as i32);
+    }
+
+        // Actually spawn the potions
+    for idx in item_spawn_points.iter() {
+        let x = *idx % MAPWIDTH;
+        let y = *idx / MAPWIDTH;
+        health_potion(ecs, x as i32, y as i32);
+    }
+}
+
+/// Spawns the player and returns his/her entity object.
+pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
+    ecs
+        .create_entity()
+        .with(Position { x: player_x, y: player_y })
+        .with(Renderable {
+            glyph: rltk::to_cp437('@'),
+            fg: RGB::named(rltk::YELLOW),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 0
+        })
+        .with(Player{})
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .with(Name{name: "Player".to_string() })
+        .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
+        .build()
+}
+
+/// Spawns a random monster at a given location
+pub fn random_monster(ecs: &mut World, x: i32, y: i32) {
+    let roll :i32;
+    {
+        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
+        roll = rng.roll_dice(1, 2);
+    }
+    match roll {
+        1 => { orc(ecs, x, y) }
+        _ => { goblin(ecs, x, y) }
+    }
+}
+
+fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('o'), "Orc"); }
+fn goblin(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('g'), "Goblin"); }
+
+fn monster<S : ToString>(ecs: &mut World, x: i32, y: i32, glyph : rltk::FontCharType, name : S) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph,
+            fg: RGB::named(rltk::RED),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 1
+        })
+        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
+        .with(Monster{})
+        .with(Name{ name : name.to_string() })
+        .with(BlocksTile{})
+        .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
+        .build();
+}