From 74c05f97afdd56e6aa3e58f50e06baeeb2492ac5 Mon Sep 17 00:00:00 2001 From: Kasper Juul Hermansen Date: Sat, 29 Jan 2022 00:31:00 +0100 Subject: [PATCH] Add hunger --- src/components.rs | 37 +++++++++++++ src/gui.rs | 23 +++++--- src/healing_system.rs | 22 ++++++++ src/hunger_system.rs | 71 +++++++++++++++++++++++++ src/inventory_system.rs | 22 +++++--- src/main.rs | 13 +++++ src/melee_combat_system.rs | 104 ++++++++++++++++++++----------------- src/player.rs | 20 ++++--- src/save_load_system.rs | 10 +++- src/spawner.rs | 32 ++++++++++-- 10 files changed, 280 insertions(+), 74 deletions(-) create mode 100644 src/healing_system.rs create mode 100644 src/hunger_system.rs diff --git a/src/components.rs b/src/components.rs index 2cd66f0..a15be8a 100644 --- a/src/components.rs +++ b/src/components.rs @@ -109,6 +109,25 @@ pub struct ProvidesHealing { pub heal_amount: i32, } +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct Heals { + pub amount: Vec, +} + +impl Heals { + pub fn new_healing(store: &mut WriteStorage, target: Entity, amount: i32) { + if let Some(healing) = store.get_mut(target) { + healing.amount.push(amount); + } else { + let heal = Heals { + amount: vec![amount], + }; + store.insert(target, heal).expect("Unable to insert heal"); + } + } +} + + #[derive(Component, Debug, Clone, ConvertSaveload)] pub struct Ranged { pub range: i32, @@ -178,3 +197,21 @@ pub struct WantsToRemoveItem { pub struct ParticleLifetime { pub lifetime_ms: f32, } + +#[derive(PartialEq, Serialize, Deserialize, Clone, Copy)] +pub enum HungerState { + WellFed, + Normal, + Hungry, + Starving, +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct HungerClock { + pub state: HungerState, + pub duration: i32, +} + + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ProvidesFood {} diff --git a/src/gui.rs b/src/gui.rs index 5cce72c..8841041 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,14 +1,14 @@ -use rltk::Rltk; -use rltk::RGB; use rltk::{Point, VirtualKeyCode}; +use rltk::RGB; +use rltk::Rltk; use specs::prelude::*; -use crate::gamelog::GameLog; -use crate::Name; -use crate::Player; -use crate::Position; +use crate::{HungerClock, HungerState, HungerSystem, Name}; use crate::{CombatStats, InBackpack, RunState, State, Viewshed}; use crate::{Equipped, Map}; +use crate::gamelog::GameLog; +use crate::Player; +use crate::Position; pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { ctx.draw_box( @@ -22,9 +22,10 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { let combat_stats = ecs.read_storage::(); let players = ecs.read_storage::(); + let hunger = ecs.read_storage::(); let log = ecs.fetch::(); - for (_player, stats) in (&players, &combat_stats).join() { + for (_player, stats, hc) in (&players, &combat_stats, &hunger).join() { let health = format!(" HP: {} / {}", stats.hp, stats.max_hp); ctx.print_color( 12, @@ -43,8 +44,16 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { RGB::named(rltk::RED), RGB::named(rltk::BLACK), ); + + match hc.state { + HungerState::WellFed => ctx.print_color(71, 42, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"), + HungerState::Normal => {} + HungerState::Hungry => ctx.print_color(71, 42, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"), + HungerState::Starving => ctx.print_color(71, 42, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"), + } } + let mut y = 44; for s in log.entries.iter().rev() { if y < 49 { diff --git a/src/healing_system.rs b/src/healing_system.rs new file mode 100644 index 0000000..ab8cf21 --- /dev/null +++ b/src/healing_system.rs @@ -0,0 +1,22 @@ +use specs::prelude::*; + +use crate::{CombatStats, GameLog, Heals}; + +pub struct HealingSystem {} + +impl<'a> System<'a> for HealingSystem { + type SystemData = ( + WriteStorage<'a, Heals>, + WriteStorage<'a, CombatStats>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (mut heals, mut stats) = data; + + for (heal, mut stats) in (&heals, &mut stats).join() { + stats.hp = i32::min(stats.max_hp, stats.hp + heal.amount.iter().sum::()); + } + + heals.clear(); + } +} \ No newline at end of file diff --git a/src/hunger_system.rs b/src/hunger_system.rs new file mode 100644 index 0000000..da5da4a --- /dev/null +++ b/src/hunger_system.rs @@ -0,0 +1,71 @@ +use specs::prelude::*; + +use crate::{GameLog, Heals, HungerClock, HungerState, ProvidesHealing, RunState, SufferDamage}; +use crate::spawner::player; + +pub struct HungerSystem {} + +impl<'a> System<'a> for HungerSystem { + type SystemData = ( + Entities<'a>, + WriteStorage<'a, HungerClock>, + ReadExpect<'a, Entity>, + ReadExpect<'a, RunState>, + WriteStorage<'a, SufferDamage>, + WriteExpect<'a, GameLog>, + WriteStorage<'a, Heals> + ); + + fn run(&mut self, data: Self::SystemData) { + let (entities, mut hunger_clock, player_entity, run_state, mut suffer_damage, mut game_log, mut heals) = data; + for (entity, mut clock) in (&entities, &mut hunger_clock).join() { + let mut proceed = false; + + match *run_state { + RunState::PlayerTurn => { + if entity == *player_entity { proceed = true; } } + RunState::MonsterTurn => { if entity != *player_entity { proceed = true; } } + _ => { proceed = false; } + } + + if !proceed { + continue; + } + + clock.duration -= 1; + if clock.duration > 0 { + continue; + } + + match clock.state { + HungerState::WellFed => { + clock.state = HungerState::Normal; + clock.duration = 200; + if entity == *player_entity { + game_log.entries.push("You are no longer well-feed".to_string()) + } + } + HungerState::Normal => { + clock.state = HungerState::Hungry; + clock.duration = 200; + if entity == *player_entity { + game_log.entries.push("You are hungry".to_string()) + } + } + HungerState::Hungry => { + clock.state = HungerState::Starving; + clock.duration = 200; + if entity == *player_entity { + game_log.entries.push("You are Starving!".to_string()) + } + } + HungerState::Starving => { + if entity == *player_entity { + game_log.entries.push("Your hunger pangs are getting painful".to_string()) + } + SufferDamage::new_damage(&mut suffer_damage, entity, 1); + } + } + } + } +} \ No newline at end of file diff --git a/src/inventory_system.rs b/src/inventory_system.rs index 8528110..552aef6 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,12 +1,8 @@ use specs::prelude::*; +use crate::{AreaOfEffect, CombatStats, Confusion, Consumable, Equippable, Equipped, HungerClock, HungerState, InBackpack, InflictsDamage, Map, Name, Position, ProvidesFood, ProvidesHealing, Ranged, SufferDamage, WantsToDropItem, WantsToPickupItem, WantsToRemoveItem, WantsToUseItem}; use crate::gamelog::GameLog; use crate::particle_system::ParticleBuilder; -use crate::{ - AreaOfEffect, CombatStats, Confusion, Consumable, Equippable, Equipped, InBackpack, - InflictsDamage, Map, Name, Position, ProvidesHealing, Ranged, SufferDamage, WantsToDropItem, - WantsToPickupItem, WantsToRemoveItem, WantsToUseItem, -}; pub struct ItemCollectionSystem {} @@ -72,6 +68,8 @@ impl<'a> System<'a> for ItemUseSystem { WriteStorage<'a, InBackpack>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, + ReadStorage<'a, ProvidesFood>, + WriteStorage<'a, HungerClock>, ); fn run(&mut self, data: Self::SystemData) { @@ -95,6 +93,8 @@ impl<'a> System<'a> for ItemUseSystem { mut backpack, mut particle_builder, positions, + provides_food, + mut hunger_clocks ) = data; for (entity, use_item) in (&entities, &wants_use).join() { @@ -133,11 +133,21 @@ impl<'a> System<'a> for ItemUseSystem { 200., ); } - + } } } } + if let Some(item_edible) = provides_food.get(use_item.item) { + used_item = true; + let target = targets[0]; + if let Some(hc) = hunger_clocks.get_mut(target) { + hc.state = HungerState::WellFed; + hc.duration = 20; + game_log.entries.push(format!("You eat the {}.", names.get(use_item.item).unwrap().name)); + } + } + if let Some(item_equippable) = equippable.get(use_item.item) { let target_slot = item_equippable.slot; let target = targets[0]; diff --git a/src/main.rs b/src/main.rs index 2d5f898..b93b3ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,8 @@ use player::*; use visibility_system::*; use crate::gamelog::GameLog; +use crate::healing_system::HealingSystem; +use crate::hunger_system::HungerSystem; use crate::inventory_system::{ ItemCollectionSystem, ItemDropSystem, ItemRemoveSystem, ItemUseSystem, }; @@ -35,6 +37,8 @@ mod rect; mod save_load_system; mod spawner; mod visibility_system; +mod hunger_system; +mod healing_system; #[derive(PartialEq, Copy, Clone)] pub enum RunState { @@ -213,6 +217,9 @@ impl State { let mut damage_system = DamageSystem {}; damage_system.run_now(&self.ecs); + let mut healing_system = HealingSystem {}; + healing_system.run_now(&self.ecs); + let mut inventory = ItemCollectionSystem {}; inventory.run_now(&self.ecs); @@ -225,6 +232,9 @@ impl State { let mut remove_items = ItemRemoveSystem {}; remove_items.run_now(&self.ecs); + let mut hunger = HungerSystem {}; + hunger.run_now(&self.ecs); + let mut particle_spawn = ParticleSpawnSystem {}; particle_spawn.run_now(&self.ecs); @@ -458,6 +468,9 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.insert(SimpleMarkerAllocator::::new()); diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 7e536f6..65c0831 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,9 +1,9 @@ use specs::prelude::*; +use crate::{DefenseBonus, Equipped, HungerClock, HungerState, MeleePowerBonus, Position}; use crate::components::{CombatStats, Name, SufferDamage, WantsToMelee}; use crate::gamelog::GameLog; use crate::particle_system::ParticleBuilder; -use crate::{DefenseBonus, Equipped, MeleePowerBonus, Position}; pub struct MeleeCombatSystem {} @@ -21,6 +21,7 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, Equipped>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, + ReadStorage<'a, HungerClock> ); fn run(&mut self, data: Self::SystemData) { @@ -36,62 +37,71 @@ impl<'a> System<'a> for MeleeCombatSystem { equipped, mut particle_builder, positions, + hunger_clock ) = data; for (entity, wants_melee, name, stats) in - (&entities, &wants_melee, &names, &combat_stats).join() + (&entities, &wants_melee, &names, &combat_stats).join() { - if stats.hp > 0 { - let mut offensive_bonus = 0; - for (_item_entity, power_bonus, equipped_by) in - (&entities, &melee_bonus, &equipped).join() + if stats.hp <= 0 { + continue; + } + + let mut offensive_bonus = 0; + for (_item_entity, power_bonus, equipped_by) in + (&entities, &melee_bonus, &equipped).join() + { + if equipped_by.owner == entity { + offensive_bonus += power_bonus.power; + } + } + + if let Some(hc) = hunger_clock.get(entity) { + if hc.state == HungerState::WellFed { + offensive_bonus += 1; + } + } + + 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 mut defensive_bonus = 0; + for (_item_entity, defense_bonus, equipped_by) in + (&entities, &defense_bonus, &equipped).join() { - if equipped_by.owner == entity { - offensive_bonus += power_bonus.power; + if equipped_by.owner == wants_melee.target { + defensive_bonus += defense_bonus.defense; } } - 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 mut defensive_bonus = 0; - for (_item_entity, defense_bonus, equipped_by) in - (&entities, &defense_bonus, &equipped).join() - { - if equipped_by.owner == wants_melee.target { - defensive_bonus += defense_bonus.defense; - } - } - - if let Some(pos) = positions.get(wants_melee.target) { - 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 let Some(pos) = positions.get(wants_melee.target) { + particle_builder.request( + pos.x, + pos.y, + rltk::RGB::named(rltk::ORANGE), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('‼'), + 200.0, ); + } - 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) - } + let damage = i32::max( + 0, + (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus), + ); + + 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) } } } diff --git a/src/player.rs b/src/player.rs index 5178a0c..75363bb 100644 --- a/src/player.rs +++ b/src/player.rs @@ -3,13 +3,9 @@ use std::cmp::{max, min}; use rltk::{Point, Rltk, VirtualKeyCode}; use specs::prelude::*; +use crate::{components::{CombatStats, Player, Position, Viewshed, WantsToMelee}, HungerClock, HungerState, Item, map::Map, Monster, RunState, State, TileType, WantsToPickupItem}; use crate::gamelog::GameLog; use crate::spawner::player; -use crate::{ - components::{CombatStats, Player, Position, Viewshed, WantsToMelee}, - map::Map, - Item, Monster, RunState, State, TileType, WantsToPickupItem, -}; pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { let mut positions = ecs.write_storage::(); @@ -21,7 +17,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { let map = ecs.fetch::(); for (entity, _player, pos, viewshed) in - (&entities, &mut players, &mut positions, &mut viewsheds).join() + (&entities, &mut players, &mut positions, &mut viewsheds).join() { if pos.x + delta_x < 1 || pos.x + delta_x > map.width - 1 @@ -139,10 +135,20 @@ fn skip_turn(ecs: &mut World) -> RunState { } } + let hunger_clock = ecs.read_storage::(); + let mut heal_bonus = 0; + if let Some(hc) = hunger_clock.get(*player_entity) { + match hc.state { + HungerState::Hungry | HungerState::Starving => {can_heal = false;} + HungerState::WellFed => {heal_bonus = 1;} + _ => {} + } + } + if can_heal { let mut health_component = ecs.write_storage::(); let player_hp = health_component.get_mut(*player_entity).unwrap(); - player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp); + player_hp.hp = i32::min(player_hp.hp + 1 + heal_bonus, player_hp.max_hp); } RunState::PlayerTurn diff --git a/src/save_load_system.rs b/src/save_load_system.rs index de1f8dc..60512cd 100644 --- a/src/save_load_system.rs +++ b/src/save_load_system.rs @@ -77,7 +77,10 @@ pub fn save_game(ecs: &mut World) { MeleePowerBonus, DefenseBonus, WantsToRemoveItem, - ParticleLifetime + ParticleLifetime, + HungerClock, + Heals, + ProvidesFood ); } @@ -157,7 +160,10 @@ pub fn load_game(ecs: &mut World) { MeleePowerBonus, DefenseBonus, WantsToRemoveItem, - ParticleLifetime + ParticleLifetime, + HungerClock, + Heals, + ProvidesFood ); } diff --git a/src/spawner.rs b/src/spawner.rs index 58fc649..7e4bfc7 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -4,13 +4,9 @@ use rltk::{FontCharType, RandomNumberGenerator, RGB}; use specs::prelude::*; use specs::saveload::{MarkedBuilder, SimpleMarker}; +use crate::{AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, DefenseBonus, EquipmentSlot, Equippable, HungerClock, HungerState, InflictsDamage, Item, MAP_WIDTH, MAX_MONSTER, MeleePowerBonus, Monster, Name, Player, Position, ProvidesFood, ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed}; use crate::random_table::RandomTable; use crate::rect::Rect; -use crate::{ - AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, DefenseBonus, EquipmentSlot, - Equippable, InflictsDamage, Item, MeleePowerBonus, Monster, Name, Player, Position, - ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed, MAP_WIDTH, MAX_MONSTER, -}; pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { ecs.create_entity() @@ -39,6 +35,10 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { defense: 2, power: 5, }) + .with(HungerClock { + duration: 20, + state: HungerState::WellFed, + }) .marked::>() .build() } @@ -125,6 +125,7 @@ pub fn spawn_room(ecs: &mut World, room: &Rect, map_depth: i32) { "Breastplate" => breastplate(ecs, x, y), "Leggings" => leggings(ecs, x, y), "Sabatons" => sabatons(ecs, x, y), + "Rations" => rations(ecs, x, y), _ => {} } } @@ -226,6 +227,7 @@ pub fn room_table(map_depth: i32) -> RandomTable { .add("Breastplate", map_depth - 3) .add("Leggings", map_depth - 4) .add("Sabatons", map_depth - 4) + .add("Rations", 10) } fn dagger(ecs: &mut World, x: i32, y: i32) { @@ -397,3 +399,23 @@ fn sabatons(ecs: &mut World, x: i32, y: i32) { .marked::>() .build(); } + + +fn rations(ecs: &mut World, x: i32, y: i32) { + ecs.create_entity() + .with(Position { x, y }) + .with(Renderable { + glyph: rltk::to_cp437('%'), + render_order: 2, + fg: RGB::named(rltk::GREEN), + bg: RGB::named(rltk::BLACK), + }) + .with(Item {}) + .with(Name { + name: "Rations".to_string(), + }) + .with(Consumable{}) + .with(ProvidesFood{}) + .marked::>() + .build(); +}