diff --git a/src/entities/character.rs b/src/entities/character.rs index b2da47609..59d4d00a4 100644 --- a/src/entities/character.rs +++ b/src/entities/character.rs @@ -1,22 +1,17 @@ use crate::display; -use crate::entities::EntityID; use crate::types::{Position, Speed}; -use proptest_derive::Arbitrary; use std::io::{self, Write}; -use termion::cursor; 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, - - pub o_name: Option, +entity! { + pub struct Character { + pub o_name: Option, + } } +static_description!(Character, "yourself"); + impl Character { pub fn new() -> Character { Character { @@ -46,8 +41,6 @@ impl Character { } } -entity!(Character); - impl display::Draw for Character { fn do_draw(&self, out: &mut Write) -> io::Result<()> { write!(out, "@") diff --git a/src/entities/creature.rs b/src/entities/creature.rs index 9fd8d23c7..4cf6f60bd 100644 --- a/src/entities/creature.rs +++ b/src/entities/creature.rs @@ -1,7 +1,7 @@ use crate::display; use crate::entities::raws::CreatureType; use crate::entities::raws::EntityRaw; -use crate::entities::{raw, EntityID}; +use crate::entities::{raw, Describe, EntityID}; use crate::types::Position; use std::io::{self, Write}; @@ -50,6 +50,12 @@ impl Creature { entity!(Creature); +impl Describe for Creature { + fn description(&self) -> String { + self.typ.description.to_string() + } +} + impl display::Draw for Creature { fn do_draw(&self, out: &mut Write) -> io::Result<()> { write!(out, "{}", self.typ.chr) diff --git a/src/entities/entity.rs b/src/entities/entity.rs index 7fedb77b2..0043a83ec 100644 --- a/src/entities/entity.rs +++ b/src/entities/entity.rs @@ -1,6 +1,7 @@ use crate::display::DrawWithNeighbors; use crate::entities::EntityID; use crate::types::Neighbors; +use crate::types::Position; use crate::types::{Positioned, PositionedMut}; use downcast_rs::Downcast; use std::fmt::Debug; @@ -37,8 +38,36 @@ impl> Identified for Box { } } +pub trait Describe { + fn description(&self) -> String; +} + +ref_impl! { + impl Describe for &T { + fn description(&self) -> String { + (**self).description() + } + } +} + +#[macro_export] +macro_rules! static_description { + ($name: ident, $description: expr) => { + impl $crate::entities::entity::Describe for $name { + fn description(&self) -> String { + $description.to_string() + } + } + }; +} + pub trait Entity: - Positioned + PositionedMut + Identified + DrawWithNeighbors + Downcast + Positioned + + PositionedMut + + Identified + + DrawWithNeighbors + + Downcast + + Describe { } @@ -80,3 +109,17 @@ impl DrawWithNeighbors for Box { (**self).do_draw_with_neighbors(out, neighbors) } } + +pub type AnEntity = Box; + +impl Positioned for AnEntity { + fn position(&self) -> Position { + (**self).position() + } +} + +impl PositionedMut for AnEntity { + fn set_position(&mut self, pos: Position) { + (**self).set_position(pos) + } +} diff --git a/src/entities/environment.rs b/src/entities/environment.rs index 64366a505..042873ec5 100644 --- a/src/entities/environment.rs +++ b/src/entities/environment.rs @@ -10,6 +10,8 @@ entity! { } } +static_description!(Wall, "a wall"); + impl Wall { pub fn new(position: Position, style: BoxStyle) -> Self { new_entity!(Wall { position, style }) diff --git a/src/entities/item.rs b/src/entities/item.rs index d0ecc090e..6e47a87f5 100644 --- a/src/entities/item.rs +++ b/src/entities/item.rs @@ -1,6 +1,6 @@ use crate::display; use crate::entities::raws::{raw, EntityRaw, ItemType}; -use crate::entities::EntityID; +use crate::entities::{Describe, EntityID}; use crate::types::Position; use std::io::{self, Write}; @@ -37,6 +37,12 @@ impl Item { entity!(Item); +impl Describe for Item { + fn description(&self) -> String { + self.typ.description.to_string() + } +} + impl display::Draw for Item { fn do_draw(&self, out: &mut Write) -> io::Result<()> { write!(out, "{}", self.typ.chr) diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 3fe84c76f..a8c39ed8a 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -12,7 +12,7 @@ pub mod raws; pub use character::Character; pub use creature::Creature; -pub use entity::{Entity, Identified}; +pub use entity::{AnEntity, Describe, Entity, Identified}; pub use entity_char::EntityChar; pub use item::Item; pub use raws::raw; diff --git a/src/entities/raw_types.rs b/src/entities/raw_types.rs index 8f64e60d9..59dd19ed2 100644 --- a/src/entities/raw_types.rs +++ b/src/entities/raw_types.rs @@ -30,9 +30,13 @@ pub struct EdibleItem<'a> { pub struct ItemType<'a> { pub name: &'a str, - /// A description of the item, used by the "look" command + /// A description of the item, used by the "look" command and when walking + /// over the item on the ground pub description: &'a str, + /// A longer description of the item + pub long_description: &'a str, + pub edible_item: Option>, #[serde(rename = "char")] @@ -49,7 +53,8 @@ mod item_type_tests { r#"{ "Item": { "name": "noodles", - "description": "You know exactly what kind of noodles", + "description": "a big bowl o' noodles", + "long_description": "You know exactly what kind of noodles", "char": { "char": "n" }, "edible_item": { "eat_message": "You slurp up the noodles", @@ -67,7 +72,8 @@ mod item_type_tests { let toml_result = toml::from_str( r#"[Item] name = "noodles" -description = "You know exactly what kind of noodles" +description = "a big bowl o' noodles" +long_description = "You know exactly what kind of noodles" char = { char = "🍜" } edible_item = { eat_message = "You slurp up the noodles", hitpoints_healed = 2 } "#, diff --git a/src/entities/raws/noodles.json b/src/entities/raws/noodles.json index 6e2ecded7..dfa2609f5 100644 --- a/src/entities/raws/noodles.json +++ b/src/entities/raws/noodles.json @@ -5,11 +5,11 @@ "char": "n", "color": "yellow" }, - "description": "You know exactly what kind of noodles", + "description": "a big bowl o' noodles", + "long_description": "You know exactly what kind of noodles", "edible_item": { "eat_message": "You slurp up the noodles", "hitpoints_healed": 2 - }, - "display_name": "big bowl o' noodles" + } } } diff --git a/src/game.rs b/src/game.rs index dd45b3009..a42edb553 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,14 +1,14 @@ +use crate::description::list_to_sentence; use crate::display::{self, Viewport}; use crate::entities::{ - Character, Creature, Entity, EntityID, Identified, Item, + AnEntity, Character, Creature, EntityID, Identified, Item, }; use crate::messages::message; use crate::settings::Settings; use crate::types::command::Command; use crate::types::entity_map::EntityMap; use crate::types::{ - pos, BoundingBox, Collision, Dimensions, Position, Positioned, - PositionedMut, Ticks, + pos, BoundingBox, Collision, Dimensions, Position, Positioned, Ticks, }; use crate::util::promise::Cancelled; use crate::util::promise::{promise, Complete, Promise, Promises}; @@ -24,25 +24,30 @@ type Stdout<'a> = RawTerminal>; type Rng = SmallRng; -type AnEntity = Box; - -impl Positioned for AnEntity { - fn position(&self) -> Position { - (**self).position() - } -} - -impl PositionedMut for AnEntity { - fn set_position(&mut self, pos: Position) { - (**self).set_position(pos) - } -} - enum PromptResolution { Uncancellable(Complete), Cancellable(Complete>), } +/// The mode to use when describing entities on a tile to the user +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EntityDescriptionMode { + /// Describe the entities that the user is walking over. + /// + /// This means: + /// - Skip the character themselves + /// - Describe nothing if there are no items other than the character + Walk, + + /// Describe entities that the user is actively asking about. + /// + /// This means: + /// - Describe the character themselves if they've asked to look at the tile + /// they're standing on + /// - Explicitly say there's nothing there if there's nothing there. + Look, +} + impl PromptResolution { fn is_cancellable(&self) -> bool { use PromptResolution::*; @@ -251,6 +256,43 @@ impl<'a> Game<'a> { } } + /// Describe all the entities at a given position to the user. + /// + /// If `force` is not set to `true`, will not do anything if there are no + /// entities + fn describe_entities_at( + &mut self, + pos: Position, + mode: EntityDescriptionMode, + ) -> io::Result<()> { + use EntityDescriptionMode::*; + let mut entities = self.entities.at(pos); + if mode == Walk { + entities.retain(|e| e.id() != self.character_entity_id); + } + + if entities.len() == 0 { + match mode { + Walk => return Ok(()), + Look => { + return self.say( + "global.describe_no_entities", + &template_params!(), + ) + } + } + } + + let descriptions = list_to_sentence( + &entities.iter().map(|e| e.description()).collect(), + ); + + self.say( + "global.describe_entities", + &template_params!({ "descriptions" => &descriptions, }), + ) + } + /// 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) { @@ -446,17 +488,18 @@ impl<'a> Game<'a> { match old_position { Some(old_pos) => { let character = self.character(); - self.viewport.game_cursor_position = - character.position; + let char_pos = character.position.clone(); + self.viewport.game_cursor_position = char_pos; self.viewport.clear(old_pos)?; self.draw_entities_at(old_pos)?; self.draw_entity(self.character_entity_id)?; - self.tick( - self.character().speed().tiles_to_ticks( - (old_pos - self.character().position) - .as_tiles(), - ), - ); + self.describe_entities_at( + char_pos, + EntityDescriptionMode::Walk, + )?; + self.tick(self.character().speed().tiles_to_ticks( + (old_pos - char_pos).as_tiles(), + )); } None => (), } diff --git a/src/messages.toml b/src/messages.toml index 69fd389fa..7c6255142 100644 --- a/src/messages.toml +++ b/src/messages.toml @@ -1,5 +1,7 @@ [global] welcome = "Welcome to Xanthous, {{character.name}}! It's dangerous out there, why not stay inside?" +describe_entities = "You see here {{descriptions}}" +describe_no_entities = "You see nothing here." [combat] attack = "You attack the {{creature.name}}." diff --git a/src/util/mod.rs b/src/util/mod.rs index c55fdfeae..dd5087a55 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -3,3 +3,5 @@ pub mod static_cfg; #[macro_use] pub mod template; pub mod promise; +#[macro_use] +pub mod trait_impls; diff --git a/src/util/trait_impls.rs b/src/util/trait_impls.rs new file mode 100644 index 000000000..ba15f7119 --- /dev/null +++ b/src/util/trait_impls.rs @@ -0,0 +1,17 @@ +macro_rules! ref_impl { + (impl $traiti:ident for &T { + $($body:tt)* + }) => { + impl<'a, T: $traitb $(+ $bound)*> $traiti for &'a T { + $($body)* + } + + impl<'a, T: $traitb $(+ $bound)*> $traiti for &'a mut T { + $($body)* + } + + impl $traiti for ::std::boxed::Box { + $($body)* + } + }; +}