about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/entities/character.rs19
-rw-r--r--src/entities/creature.rs8
-rw-r--r--src/entities/entity.rs45
-rw-r--r--src/entities/environment.rs2
-rw-r--r--src/entities/item.rs8
-rw-r--r--src/entities/mod.rs2
-rw-r--r--src/entities/raw_types.rs12
-rw-r--r--src/entities/raws/noodles.json6
-rw-r--r--src/game.rs93
-rw-r--r--src/messages.toml2
-rw-r--r--src/util/mod.rs2
-rw-r--r--src/util/trait_impls.rs17
12 files changed, 168 insertions, 48 deletions
diff --git a/src/entities/character.rs b/src/entities/character.rs
index b2da47609670..59d4d00a4b65 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<EntityID>,
-
-    /// The position of the character, relative to the game
-    pub position: Position,
-
-    pub o_name: Option<String>,
+entity! {
+    pub struct Character {
+        pub o_name: Option<String>,
+    }
 }
 
+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 9fd8d23c752e..4cf6f60bdc6c 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 7fedb77b2562..0043a83ecd54 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<ID, A: Identified<ID>> Identified<ID> for Box<A> {
     }
 }
 
+pub trait Describe {
+    fn description(&self) -> String;
+}
+
+ref_impl! {
+    impl<T: Describe> 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<EntityID> + DrawWithNeighbors + Downcast
+    Positioned
+    + PositionedMut
+    + Identified<EntityID>
+    + DrawWithNeighbors
+    + Downcast
+    + Describe
 {
 }
 
@@ -80,3 +109,17 @@ impl DrawWithNeighbors for Box<dyn Entity> {
         (**self).do_draw_with_neighbors(out, neighbors)
     }
 }
+
+pub type AnEntity = Box<dyn Entity>;
+
+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 64366a505496..042873ec5a12 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 d0ecc090e2e4..6e47a87f5b83 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 3fe84c76f8ef..a8c39ed8aa78 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 8f64e60d9cd4..59dd19ed2fa2 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<EdibleItem<'a>>,
 
     #[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 6e2ecded7513..dfa2609f5ecb 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 dd45b3009a37..a42edb553711 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<StdoutLock<'a>>;
 
 type Rng = SmallRng;
 
-type AnEntity = Box<dyn Entity>;
-
-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<String>),
     Cancellable(Complete<Result<String, Cancelled>>),
 }
 
+/// 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 69fd389faccc..7c6255142dd2 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 c55fdfeae2b4..dd5087a55558 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 000000000000..ba15f7119d26
--- /dev/null
+++ b/src/util/trait_impls.rs
@@ -0,0 +1,17 @@
+macro_rules! ref_impl {
+    (impl<T: $traitb: ident $(+ $bound:ident)*> $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<T: $traitb $(+ $bound)*> $traiti for ::std::boxed::Box<T> {
+            $($body)*
+        }
+    };
+}