Implement extremely basic combat

There's a gormlak, you can kill it.
That's it.
This commit is contained in:
Griffin Smith 2019-07-14 16:20:22 -04:00
parent e7ad87c730
commit 575a051e6e
9 changed files with 220 additions and 48 deletions

View file

@ -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<EntityID>,
/// 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)
}
pub fn damage(&self) -> u16 {
// TODO
1
}
}
positioned!(Character);
positioned_mut!(Character);
impl Entity for Character {}
entity!(Character);
impl display::Draw for Character {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {

View file

@ -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<EntityID>,
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,
}
}
/// 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
}
}
positioned!(Creature);
positioned_mut!(Creature);
impl Entity for Creature {}
entity!(Creature);
impl display::Draw for Creature {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {

87
src/entities/entity.rs Normal file
View file

@ -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<ID>: Debug {
fn opt_id(&self) -> Option<ID>;
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<ID> for &'a mut A
where
A: Identified<ID>,
{
fn opt_id(&self) -> Option<ID> {
(**self).opt_id()
}
fn set_id(&mut self, id: ID) {
(**self).set_id(id);
}
}
impl<ID, A: Identified<ID>> Identified<ID> for Box<A> {
fn opt_id(&self) -> Option<ID> {
(**self).opt_id()
}
fn set_id(&mut self, id: ID) {
(**self).set_id(id);
}
}
pub trait Entity:
Positioned + PositionedMut + Identified<EntityID> + Draw + Downcast
{
}
impl Identified<EntityID> for Box<dyn Entity> {
fn opt_id(&self) -> Option<EntityID> {
(**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<dyn Entity> {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
(**self).do_draw(out)
}
}

View file

@ -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<dyn Entity> {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
(**self).do_draw(out)
}
}
pub type EntityID = u32;

View file

@ -49,7 +49,6 @@ lazy_static! {
}
pub fn raw(name: &'static str) -> &'static EntityRaw<'static> {
debug!("{:?}", RAWS_BY_NAME.keys().collect::<Vec<&&'static str>>());
RAWS_BY_NAME
.get(name)
.map(|e| *e)

View file

@ -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<Collision> {
if !pos.within(self.viewport.inner) {
Some(Collision::Stop)
} else {
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::<Vec<u32>>(),
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::<Creature>())
.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) => (),
}
}

View file

@ -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;

View file

@ -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}}."

View file

@ -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<A> {
by_position: BTreeMap<Position, Vec<EntityID>>,
@ -83,6 +83,10 @@ impl<A> EntityMap<A> {
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<Item = &'a A> {
self.by_id.values()
}
@ -101,10 +105,11 @@ impl<A> EntityMap<A> {
}
}
impl<A: Positioned> EntityMap<A> {
pub fn insert(&mut self, entity: A) -> EntityID {
impl<A: Positioned + Identified<EntityID>> EntityMap<A> {
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<A: Positioned> EntityMap<A> {
.push(entity_id);
entity_id
}
/// Remove the entity with the given ID
pub fn remove(&mut self, id: EntityID) -> Option<A> {
self.by_id.remove(&id).map(|e| {
self.by_position
.get_mut(&e.position())
.map(|es| es.retain(|e| *e != id));
e
})
}
}
impl<A: Positioned> FromIterator<A> for EntityMap<A> {
impl<A: Positioned + Identified<EntityID>> FromIterator<A> for EntityMap<A> {
fn from_iter<I: IntoIterator<Item = A>>(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<EntityID>,
position: Position,
name: String,
}
@ -176,6 +192,16 @@ mod tests {
}
}
impl Identified<EntityID> for TestEntity {
fn opt_id(&self) -> Option<EntityID> {
self._id
}
fn set_id(&mut self, id: EntityID) {
self._id = Some(id);
}
}
fn gen_entity_map() -> BoxedStrategy<EntityMap<TestEntity>> {
any::<Vec<TestEntity>>()
.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]