ed0b250e6aa868be23d1e520439a1884779fdb96
commit ed0b250e6aa868be23d1e520439a1884779fdb96
Author: Simon Watson <spw01@protonmail.com>
Date: Fri Oct 7 19:58:31 2022 -0400

Up to hunger system

diff --git a/src/components.rs b/src/components.rs
index 9b37d57..17dd4f4 100644
--- a/src/components.rs
+++ b/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
diff --git a/src/damage_system.rs b/src/damage_system.rs
index 93a617d..e242336 100644
--- a/src/damage_system.rs
+++ b/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();
- }
-}
\ No newline at end of file
diff --git a/src/gui.rs b/src/gui.rs
index d437b36..9dc8f44 100644
--- a/src/gui.rs
+++ b/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 {
diff --git a/src/inventory_system.rs b/src/inventory_system.rs
index ab08aad..ee028aa 100644
--- a/src/inventory_system.rs
+++ b/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);
+ }
}
}
}
diff --git a/src/main.rs b/src/main.rs
index 78f873a..04e40dc 100644
--- a/src/main.rs
+++ b/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());

diff --git a/src/map.rs b/src/map.rs
index 69df5f7..b5faf8a 100644
--- a/src/map.rs
+++ b/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
diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs
index 9f87a27..9f91a73 100644
--- a/src/melee_combat_system.rs
+++ b/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 {
diff --git a/src/particle_system.rs b/src/particle_system.rs
new file mode 100644
index 0000000..19c4d37
--- /dev/null
+++ b/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");
+ }
+}
\ No newline at end of file
diff --git a/src/saveload_system.rs b/src/saveload_system.rs
index a8c3c27..c74fe38 100644
--- a/src/saveload_system.rs
+++ b/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
);
}