From 575a051e6efcd8fd3b0a146f49040e543ae8e5b0 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Sun, 14 Jul 2019 16:20:22 -0400 Subject: Implement extremely basic combat There's a gormlak, you can kill it. That's it. --- src/entities/character.rs | 15 +++++--- src/entities/creature.rs | 24 ++++++++++--- src/entities/entity.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++ src/entities/mod.rs | 18 +++------- src/entities/raws.rs | 1 - src/game.rs | 72 ++++++++++++++++++++++++++++++++------- src/main.rs | 5 +-- src/messages.toml | 4 +++ src/types/entity_map.rs | 42 ++++++++++++++++++----- 9 files changed, 220 insertions(+), 48 deletions(-) create mode 100644 src/entities/entity.rs diff --git a/src/entities/character.rs b/src/entities/character.rs index fb5a89591c95..7bcb8b5c87e4 100644 --- a/src/entities/character.rs +++ b/src/entities/character.rs @@ -1,5 +1,5 @@ use crate::display; -use crate::entities::Entity; +use crate::entities::EntityID; use crate::types::{Position, Speed}; use proptest_derive::Arbitrary; use std::io::{self, Write}; @@ -9,6 +9,8 @@ const DEFAULT_SPEED: Speed = Speed(100); #[derive(Debug, PartialEq, Eq, Arbitrary, Clone)] pub struct Character { + pub id: Option, + /// The position of the character, relative to the game pub position: Position, } @@ -16,6 +18,7 @@ pub struct Character { impl Character { pub fn new() -> Character { Character { + id: None, position: Position { x: 0, y: 0 }, } } @@ -23,12 +26,14 @@ impl Character { pub fn speed(&self) -> Speed { Speed(100) } -} -positioned!(Character); -positioned_mut!(Character); + pub fn damage(&self) -> u16 { + // TODO + 1 + } +} -impl Entity for Character {} +entity!(Character); impl display::Draw for Character { fn do_draw(&self, out: &mut Write) -> io::Result<()> { diff --git a/src/entities/creature.rs b/src/entities/creature.rs index 6ddeade21845..55445f951b45 100644 --- a/src/entities/creature.rs +++ b/src/entities/creature.rs @@ -1,11 +1,13 @@ use crate::display; use crate::entities::raws::CreatureType; use crate::entities::raws::EntityRaw; -use crate::entities::{raw, Entity}; +use crate::entities::{raw, EntityID}; use crate::types::Position; use std::io::{self, Write}; +#[derive(Debug)] pub struct Creature { + pub id: Option, pub typ: &'static CreatureType<'static>, pub position: Position, pub hitpoints: u16, @@ -24,17 +26,29 @@ impl Creature { position: Position, ) -> Self { Creature { + id: None, typ, position, hitpoints: typ.max_hitpoints, } } -} -positioned!(Creature); -positioned_mut!(Creature); + /// Damage the given creature by the given amount + pub fn damage(&mut self, amount: u16) { + if self.hitpoints <= amount { + self.hitpoints = 0; + } else { + self.hitpoints -= amount; + } + } + + /// Returns true if this creature has died + pub fn dead(&self) -> bool { + self.hitpoints <= 0 + } +} -impl Entity for Creature {} +entity!(Creature); impl display::Draw for Creature { fn do_draw(&self, out: &mut Write) -> io::Result<()> { diff --git a/src/entities/entity.rs b/src/entities/entity.rs new file mode 100644 index 000000000000..30f7ea9a3dae --- /dev/null +++ b/src/entities/entity.rs @@ -0,0 +1,87 @@ +use crate::display::Draw; +use crate::entities::EntityID; +use crate::types::{Positioned, PositionedMut}; +use downcast_rs::Downcast; +use std::fmt::Debug; +use std::io::{self, Write}; + +pub trait Identified: Debug { + fn opt_id(&self) -> Option; + fn set_id(&mut self, id: ID); + + fn id(&self) -> ID { + self.opt_id() + .expect(format!("Entity ({:?}) is not in the game", self).as_str()) + } +} + +impl<'a, A, ID> Identified for &'a mut A +where + A: Identified, +{ + fn opt_id(&self) -> Option { + (**self).opt_id() + } + fn set_id(&mut self, id: ID) { + (**self).set_id(id); + } +} + +impl> Identified for Box { + fn opt_id(&self) -> Option { + (**self).opt_id() + } + fn set_id(&mut self, id: ID) { + (**self).set_id(id); + } +} + +pub trait Entity: + Positioned + PositionedMut + Identified + Draw + Downcast +{ +} + +impl Identified for Box { + fn opt_id(&self) -> Option { + (**self).opt_id() + } + fn set_id(&mut self, id: EntityID) { + (**self).set_id(id); + } +} + +#[macro_export] +macro_rules! identified { + ($name: ident, $typ: ident) => { + identified!($name, $typ, id); + }; + ($name: ident, $typ: ident, $attr: ident) => { + impl crate::entities::entity::Identified<$typ> for $name { + fn opt_id(&self) -> Option<$typ> { + self.$attr + } + + fn set_id(&mut self, id: $typ) { + self.$attr = Some(id) + } + } + }; +} + +#[macro_export] +macro_rules! entity { + ($name: ident) => { + positioned!($name); + positioned_mut!($name); + identified!($name, EntityID); + impl crate::entities::entity::Entity for $name {} + }; +} + +impl_downcast!(Entity); + +impl Draw for Box { + fn do_draw(&self, out: &mut Write) -> io::Result<()> { + (**self).do_draw(out) + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index c4f46bf4a723..ed83f2f462db 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,3 +1,5 @@ +#[macro_use] +pub mod entity; pub mod character; pub mod creature; pub mod entity_char; @@ -5,20 +7,8 @@ pub mod raws; pub use character::Character; pub use creature::Creature; +pub use entity::{Entity, Identified}; pub use entity_char::EntityChar; pub use raws::raw; -use crate::display::Draw; -use crate::types::{Positioned, PositionedMut}; -use downcast_rs::Downcast; -use std::io::{self, Write}; - -pub trait Entity: Positioned + PositionedMut + Draw + Downcast {} - -impl_downcast!(Entity); - -impl Draw for Box { - fn do_draw(&self, out: &mut Write) -> io::Result<()> { - (**self).do_draw(out) - } -} +pub type EntityID = u32; diff --git a/src/entities/raws.rs b/src/entities/raws.rs index beeb90a40cea..da061d89d8d6 100644 --- a/src/entities/raws.rs +++ b/src/entities/raws.rs @@ -49,7 +49,6 @@ lazy_static! { } pub fn raw(name: &'static str) -> &'static EntityRaw<'static> { - debug!("{:?}", RAWS_BY_NAME.keys().collect::>()); RAWS_BY_NAME .get(name) .map(|e| *e) diff --git a/src/game.rs b/src/game.rs index f86d32d0463c..57c04cfb883f 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,15 +1,12 @@ use crate::display::{self, Viewport}; -use crate::entities::Character; -use crate::entities::{Creature, Entity}; +use crate::entities::{Character, Creature, Entity, EntityID, Identified}; use crate::messages::message; use crate::settings::Settings; use crate::types::command::Command; -use crate::types::entity_map::EntityID; use crate::types::entity_map::EntityMap; -use crate::types::pos; -use crate::types::Ticks; use crate::types::{ - BoundingBox, Collision, Dimensions, Position, Positioned, PositionedMut, + pos, BoundingBox, Collision, Dimensions, Position, Positioned, + PositionedMut, Ticks, }; use rand::rngs::SmallRng; use rand::SeedableRng; @@ -100,21 +97,29 @@ impl<'a> Game<'a> { } } + /// Returns a list of all creature entities at the given position + fn creatures_at<'b>(&'b self, pos: Position) -> Vec<&'b Creature> { + self.entities + .at(pos) + .iter() + .filter_map(|e| e.downcast_ref()) + .collect() + } + /// Returns a collision, if any, at the given Position in the game fn collision_at(&self, pos: Position) -> Option { if !pos.within(self.viewport.inner) { Some(Collision::Stop) } else { - None + if self.creatures_at(pos).len() > 0 { + Some(Collision::Combat) + } else { + None + } } } fn character(&self) -> &Character { - debug!( - "ents: {:?} cid: {:?}", - self.entities.ids().map(|id| *id).collect::>(), - self.character_entity_id - ); (*self.entities.get(self.character_entity_id).unwrap()) .downcast_ref() .unwrap() @@ -128,6 +133,14 @@ impl<'a> Game<'a> { Ok(()) } + /// Remove the given entity from the game, drawing over it if it's visible + fn remove_entity(&mut self, entity_id: EntityID) -> io::Result<()> { + if let Some(entity) = self.entities.remove(entity_id) { + self.viewport.clear(entity.position())?; + } + Ok(()) + } + /// Step the game forward the given number of ticks fn tick(&mut self, ticks: Ticks) {} @@ -153,6 +166,37 @@ impl<'a> Game<'a> { self.viewport.write_message(message) } + fn attack(&mut self, creature_id: EntityID) -> io::Result<()> { + info!("Attacking creature {:?}", creature_id); + self.say("combat.attack")?; + let damage = self.character().damage(); + let creature = self + .entities + .get_mut(creature_id) + .and_then(|e| e.downcast_mut::()) + .expect( + format!("Creature ID went away: {:?}", creature_id).as_str(), + ); + creature.damage(damage); + if creature.dead() { + self.say("combat.killed")?; + info!("Killed creature {:?}", creature_id); + self.remove_entity(creature_id)?; + } + Ok(()) + } + + fn attack_at(&mut self, pos: Position) -> io::Result<()> { + let creatures = self.creatures_at(pos); + if creatures.len() == 1 { + let creature = creatures.get(0).unwrap(); + self.attack(creature.id()) + } else { + // TODO prompt with a menu of creatures to combat + unimplemented!() + } + } + /// Run the game pub fn run(mut self) -> io::Result<()> { info!("Running game"); @@ -180,7 +224,9 @@ impl<'a> Game<'a> { new_pos, ); } - Some(Combat) => unimplemented!(), + Some(Combat) => { + self.attack_at(new_pos)?; + } Some(Stop) => (), } } diff --git a/src/main.rs b/src/main.rs index f7257a889653..636663e05045 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,11 +24,12 @@ extern crate include_dir; #[macro_use] mod util; -mod display; -mod game; #[macro_use] mod types; +#[macro_use] mod entities; +mod display; +mod game; mod messages; mod settings; diff --git a/src/messages.toml b/src/messages.toml index 04746462d580..a6b795d97ec7 100644 --- a/src/messages.toml +++ b/src/messages.toml @@ -1,2 +1,6 @@ [global] welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?" + +[combat] +attack = "You attack the {{creature_name}}." +killed = "You killed the {{creature_name}}." diff --git a/src/types/entity_map.rs b/src/types/entity_map.rs index 1d58873bb0ac..12deaa57a6eb 100644 --- a/src/types/entity_map.rs +++ b/src/types/entity_map.rs @@ -1,3 +1,5 @@ +use crate::entities::entity::Identified; +use crate::entities::EntityID; use crate::types::Position; use crate::types::Positioned; use crate::types::PositionedMut; @@ -5,8 +7,6 @@ use std::collections::hash_map::HashMap; use std::collections::BTreeMap; use std::iter::FromIterator; -pub type EntityID = u32; - #[derive(Debug)] pub struct EntityMap { by_position: BTreeMap>, @@ -83,6 +83,10 @@ impl EntityMap { self.by_id.get(&id) } + pub fn get_mut<'a>(&'a mut self, id: EntityID) -> Option<&'a mut A> { + self.by_id.get_mut(&id) + } + pub fn entities<'a>(&'a self) -> impl Iterator { self.by_id.values() } @@ -101,10 +105,11 @@ impl EntityMap { } } -impl EntityMap { - pub fn insert(&mut self, entity: A) -> EntityID { +impl> EntityMap { + pub fn insert(&mut self, mut entity: A) -> EntityID { let pos = entity.position(); let entity_id = self.next_id(); + entity.set_id(entity_id); self.by_id.entry(entity_id).or_insert(entity); self.by_position .entry(pos) @@ -112,9 +117,19 @@ impl EntityMap { .push(entity_id); entity_id } + + /// Remove the entity with the given ID + pub fn remove(&mut self, id: EntityID) -> Option { + self.by_id.remove(&id).map(|e| { + self.by_position + .get_mut(&e.position()) + .map(|es| es.retain(|e| *e != id)); + e + }) + } } -impl FromIterator for EntityMap { +impl> FromIterator for EntityMap { fn from_iter>(iter: I) -> Self { let mut em = EntityMap::new(); for ent in iter { @@ -160,6 +175,7 @@ mod tests { #[derive(Debug, Arbitrary, PartialEq, Eq, Clone)] struct TestEntity { + _id: Option, position: Position, name: String, } @@ -176,6 +192,16 @@ mod tests { } } + impl Identified for TestEntity { + fn opt_id(&self) -> Option { + self._id + } + + fn set_id(&mut self, id: EntityID) { + self._id = Some(id); + } + } + fn gen_entity_map() -> BoxedStrategy> { any::>() .prop_map(|ents| { @@ -194,7 +220,7 @@ mod tests { let mut map = EntityMap::new(); assert_eq!(map.len(), 0); for ent in &items { - map.insert(ent); + map.insert(ent.clone()); } assert_eq!(map.len(), items.len()); } @@ -205,7 +231,7 @@ mod tests { ent: TestEntity ) { em.insert(ent.clone()); - assert!(em.at(ent.position).iter().any(|e| **e == ent)) + assert!(em.at(ent.position).iter().any(|e| e.name == ent.name)) } #[test] @@ -214,7 +240,7 @@ mod tests { ent: TestEntity ) { em.insert(ent.clone()); - assert!(em.entities().any(|e| *e == ent)) + assert!(em.entities().any(|e| e.name == ent.name)) } #[test] -- cgit 1.4.1