Implement extremely basic combat
There's a gormlak, you can kill it. That's it.
This commit is contained in:
parent
e7ad87c730
commit
575a051e6e
9 changed files with 220 additions and 48 deletions
|
@ -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<()> {
|
||||
|
|
|
@ -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
87
src/entities/entity.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
72
src/game.rs
72
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<Collision> {
|
||||
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::<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) => (),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}}."
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue