Browse Source

Up to hunger system

Simon Watson 1 year ago
parent
commit
ed0b250e6a
9 changed files with 212 additions and 28 deletions
  1. 5 0
      src/components.rs
  2. 24 16
      src/damage_system.rs
  3. 1 1
      src/gui.rs
  4. 20 3
      src/inventory_system.rs
  5. 9 0
      src/main.rs
  6. 55 3
      src/map.rs
  7. 20 3
      src/melee_combat_system.rs
  8. 74 0
      src/particle_system.rs
  9. 4 2
      src/saveload_system.rs

+ 5 - 0
src/components.rs

@@ -6,6 +6,11 @@ use specs::saveload::{Marker, ConvertSaveload};
 use specs::error::NoError;
 
 // COMPONENTS
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct ParticleLifetime {
+    pub lifetime_ms : f32
+}
+
 #[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct WantsToRemoveItem {
     pub item : Entity

+ 24 - 16
src/damage_system.rs

@@ -1,9 +1,31 @@
 use specs::prelude::*;
-use super::{CombatStats, SufferDamage, Player, GameLog, Name, RunState};
-use rltk::{console};
+use super::{CombatStats, SufferDamage, Player, GameLog, Name, RunState, Position, Map};
 
 pub struct DamageSystem {}
 
+impl<'a> System<'a> for DamageSystem {
+    type SystemData = ( WriteStorage<'a, CombatStats>,
+                        WriteStorage<'a, SufferDamage>,
+                        ReadStorage<'a, Position>,
+                        WriteExpect<'a, Map>,
+                        Entities<'a> );
+
+    fn run(&mut self, data : Self::SystemData) {
+        let (mut stats, mut damage, positions, mut map, entities) = data;
+
+        for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() {
+            stats.hp -= damage.amount.iter().sum::<i32>();
+            let pos = positions.get(entity);
+            if let Some(pos) = pos {
+                let idx = map.xy_idx(pos.x, pos.y);
+                map.bloodstains.insert(idx);
+            }
+        }
+
+        damage.clear();
+    }
+}
+
 pub fn delete_the_dead(ecs : &mut World) {
     let mut dead : Vec<Entity> = Vec::new();
     // Using a scope to make the borrow checker happy
@@ -39,17 +61,3 @@ pub fn delete_the_dead(ecs : &mut World) {
     }    
 }
 
-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();
-    }
-}

+ 1 - 1
src/gui.rs

@@ -29,7 +29,7 @@ pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
     let save_exists = super::saveload_system::does_save_exist();
     let runstate = gs.ecs.fetch::<RunState>();
 
-    ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");
+    ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "St. Antony's Fire");
 
     if let RunState::MainMenu{ menu_selection : selection } = *runstate {
         if selection == MainMenuSelection::NewGame {

+ 20 - 3
src/inventory_system.rs

@@ -5,7 +5,8 @@ use super::{WantsToPickupItem, Name, InBackpack,
             Position, GameLog, WantsToUseItem,
             CombatStats, Item, WantsToDropItem,
             Map, SufferDamage, AreaOfEffect,
-            Equippable,Equipped,WantsToRemoveItem};
+            Equippable,Equipped,WantsToRemoveItem,
+            ParticleBuilder};
 
 pub struct ItemCollectionSystem {}
 
@@ -77,10 +78,13 @@ impl<'a> System<'a> for ItemUseSystem {
                         ReadStorage<'a, AreaOfEffect>,
                         ReadStorage<'a, Equippable>,
                         WriteStorage<'a, Equipped>,
-                        WriteStorage<'a, InBackpack>
+                        WriteStorage<'a, InBackpack>,
+                        WriteExpect<'a, ParticleBuilder>,
+                        ReadStorage<'a, Position>
 
                       );
 
+    #[allow(clippy::cognitive_complexity)]
     fn run(&mut self, data : Self::SystemData) {
         let (player_entity,
             mut gamelog,
@@ -97,7 +101,9 @@ impl<'a> System<'a> for ItemUseSystem {
             aoe,
             equippable,
             mut equipped,
-            mut backpack) = data;
+            mut backpack,
+            mut particle_builder,
+            positions) = data;
 
         for (entity, useitem) in (&entities, &wants_use).join() {
 
@@ -124,6 +130,8 @@ 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('░'), 200.0);
                             }
                         }
                     }
@@ -172,6 +180,11 @@ impl<'a> System<'a> for ItemUseSystem {
                             let mob_name = names.get(*mob).unwrap();
                             let item_name = names.get(useitem.item).unwrap();
                             gamelog.entries.push(format!("You use {} on {}, inflicting {} damage.", item_name.name, mob_name.name, damage.damage));
+                            
+                            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('‼'), 200.0);
+                            }
                         }
 
                     }
@@ -190,6 +203,10 @@ impl<'a> System<'a> for ItemUseSystem {
                             if entity == *player_entity {
                                 gamelog.entries.push(format!("You use the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
                             }
+                            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('♥'), 200.0);
+                            }
                         }
                     }
                 }

+ 9 - 0
src/main.rs

@@ -31,6 +31,8 @@ use inventory_system::*;
 mod saveload_system;
 mod random_table;
 use random_table::*;
+mod particle_system;
+use particle_system::ParticleBuilder;
 
 // ***** //
 // STATE //
@@ -80,6 +82,8 @@ impl State {
         drop_items.run_now(&self.ecs);
         let mut item_remove = ItemRemoveSystem{};
         item_remove.run_now(&self.ecs);
+        let mut particles = particle_system::ParticleSpawnSystem{};
+        particles.run_now(&self.ecs);
         self.ecs.maintain();
     }
 
@@ -230,6 +234,7 @@ impl GameState for State {
         }
 
         ctx.cls();
+        particle_system::cull_dead_particles(&mut self.ecs, ctx);
 
         match newrunstate {
             RunState::MainMenu{..} => {}
@@ -418,6 +423,10 @@ fn main() -> rltk::BError {
     gs.ecs.register::<MeleePowerBonus>();
     gs.ecs.register::<DefenseBonus>();
     gs.ecs.register::<WantsToRemoveItem>();
+    gs.ecs.register::<ParticleLifetime>();
+
+    // Resource to dispatch particle effects requests
+    gs.ecs.insert(particle_system::ParticleBuilder::new());
 
     gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());
 

+ 55 - 3
src/map.rs

@@ -3,6 +3,7 @@ use super::{Rect};
 use std::cmp::{max, min};
 use specs::prelude::*;
 use serde::{Serialize, Deserialize};
+use std::collections::HashSet;
 
 pub const MAPWIDTH : usize = 80;
 pub const MAPHEIGHT : usize = 38;
@@ -25,6 +26,7 @@ pub struct Map {
     pub visible_tiles: Vec<bool>,
     pub blocked : Vec<bool>,
     pub depth: i32,
+    pub bloodstains : HashSet<usize>,
 
     #[serde(skip_serializing)]
     #[serde(skip_deserializing)]
@@ -90,6 +92,7 @@ impl Map {
             visible_tiles : vec![false; MAPCOUNT],
             blocked : vec![false; MAPCOUNT],
             depth: new_depth,
+            bloodstains: HashSet::new(),
             tile_content : vec![Vec::new(); MAPCOUNT]
         };
     
@@ -208,23 +211,61 @@ impl BaseMap for Map {
 //     return map
 // }
 
+fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
+    let idx = map.xy_idx(x, y);
+    map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx]
+}
+
+fn wall_glyph(map : &Map, x: i32, y:i32) -> rltk::FontCharType {
+    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
+    let mut mask : u8 = 0;
+
+    if is_revealed_and_wall(map, x, y - 1) { mask +=1; }
+    if is_revealed_and_wall(map, x, y + 1) { mask +=2; }
+    if is_revealed_and_wall(map, x - 1, y) { mask +=4; }
+    if is_revealed_and_wall(map, x + 1, y) { mask +=8; }
+
+    match mask {
+        0 => { 9 } // Pillar because we can't see neighbors
+        1 => { 186 } // Wall only to the north
+        2 => { 186 } // Wall only to the south
+        3 => { 186 } // Wall to the north and south
+        4 => { 205 } // Wall only to the west
+        5 => { 188 } // Wall to the north and west
+        6 => { 187 } // Wall to the south and west
+        7 => { 185 } // Wall to the north, south and west
+        8 => { 205 } // Wall only to the east
+        9 => { 200 } // Wall to the north and east
+        10 => { 201 } // Wall to the south and east
+        11 => { 204 } // Wall to the north, south and east
+        12 => { 205 } // Wall to the east and west
+        13 => { 202 } // Wall to the east, west, and south
+        14 => { 203 } // Wall to the east, west, and north
+        15 => { 206 }  // ╬ Wall on all sides
+        _ => { 35 } // We missed one?
+    }
+}
+
 pub fn draw_map(ecs: &World, ctx : &mut Rltk) {
     let map = ecs.fetch::<Map>();
 
     let mut y = 0;
     let mut x = 0;
+
     for (idx,tile) in map.tiles.iter().enumerate() {
         // Render a tile depending upon the tile type
         if map.revealed_tiles[idx] {
             let glyph;
             let mut fg;
+            let mut bg = RGB::from_f32(0., 0., 0.);
+
             match tile {
                 TileType::Floor => {
                     glyph = rltk::to_cp437('.');
                     fg = RGB::from_f32(1.0, 0.5, 0.7);
                 }
                 TileType::Wall => {
-                    glyph = rltk::to_cp437('#');
+                    glyph = wall_glyph(&*map, x, y);
                     fg = RGB::from_f32(1.0, 0.6, 0.);
                 }
                 TileType::DownStairs => {
@@ -232,8 +273,19 @@ pub fn draw_map(ecs: &World, ctx : &mut Rltk) {
                     fg = RGB::from_f32(0.0,1.0,1.0);
                 }
             }
-            if !map.visible_tiles[idx] { fg = fg.to_greyscale() }
-            ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph);
+            // Render bloodstains
+            if map.bloodstains.contains(&idx) { 
+                bg = RGB::from_f32(0.75, 0., 0.); 
+            }
+
+            // Fog of war
+            if !map.visible_tiles[idx] { 
+                fg = fg.to_greyscale();
+                bg = RGB::from_f32(0., 0., 0.); // Don't show blood out of visual range
+            }
+
+
+            ctx.set(x, y, fg, bg, glyph);
         }
 
         // Move the coordinates

+ 20 - 3
src/melee_combat_system.rs

@@ -1,7 +1,8 @@
 use specs::prelude::*;
 use super::{CombatStats, WantsToMelee, Name, 
             SufferDamage, GameLog, MeleePowerBonus,
-            DefenseBonus, Equipped};
+            DefenseBonus, Equipped, particle_system::ParticleBuilder,
+            Position};
 
 pub struct MeleeCombatSystem {}
 
@@ -14,11 +15,23 @@ impl<'a> System<'a> for MeleeCombatSystem {
                         WriteStorage<'a, SufferDamage>,
                         ReadStorage<'a, MeleePowerBonus>,
                         ReadStorage<'a, DefenseBonus>,
-                        ReadStorage<'a, Equipped>
+                        ReadStorage<'a, Equipped>,
+                        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) = data;
+        let (entities, 
+            mut log, 
+            mut wants_melee, 
+            names, 
+            combat_stats, 
+            mut inflict_damage, 
+            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 {
@@ -40,6 +53,10 @@ 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('‼'), 200.0);
+                    }
                     let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus));
 
                     if damage == 0 {

+ 74 - 0
src/particle_system.rs

@@ -0,0 +1,74 @@
+use specs::prelude::*;
+use super::{Rltk, ParticleLifetime, Position, Renderable};
+use rltk::{RGB};
+
+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();
+    }
+}
+
+struct ParticleRequest {
+    x: i32,
+    y: i32,
+    fg: RGB,
+    bg: RGB,
+    glyph: rltk::FontCharType,
+    lifetime: f32
+}
+
+pub struct ParticleBuilder {
+    requests : Vec<ParticleRequest>
+}
+
+impl ParticleBuilder {
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> ParticleBuilder {
+        ParticleBuilder{ requests : Vec::new() }
+    }
+
+    pub fn request(&mut self, x:i32, y:i32, fg: RGB, bg:RGB, glyph: rltk::FontCharType, lifetime: f32) {
+        self.requests.push(
+            ParticleRequest{
+                x, y, fg, bg, glyph, lifetime
+            }
+        );
+    }
+}
+
+pub 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");
+    } 
+}

+ 4 - 2
src/saveload_system.rs

@@ -58,7 +58,8 @@ pub fn save_game(ecs : &mut World) {
         serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster, 
             Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
             AreaOfEffect, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
-            WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem
+            WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem,
+            ParticleLifetime
         );
     }
 
@@ -97,7 +98,8 @@ pub fn load_game(ecs: &mut World) {
         deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, 
             Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
             AreaOfEffect, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
-            WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem
+            WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem,
+            ParticleLifetime
         );
     }