From 29c80ac8ba0d733c6c452d8fd39e9561553495b0 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 19 Jul 2019 21:55:09 -0400 Subject: [PATCH] Add the beginning of item entities Add a new Item raw type and entity type, with preliminary, basic support for food. There's a really frustrating toml-rs bug that prevents writing these nicely as toml right now, so I also added support for mixing JSON and TOML in a single config dir --- Cargo.lock | 8 +++ Cargo.toml | 2 + src/display/color.rs | 6 ++ src/entities/entity_char.rs | 2 + src/entities/item.rs | 44 ++++++++++++++ src/entities/mod.rs | 3 + src/entities/raw_types.rs | 104 +++++++++++++++++++++++++++++++++ src/entities/raws.rs | 44 +++++--------- src/entities/raws/noodles.json | 14 +++++ src/game.rs | 7 ++- src/main.rs | 3 + src/messages.rs | 11 +++- src/messages.toml | 3 + src/util/static_cfg.rs | 58 +++++++++++++++++- 14 files changed, 274 insertions(+), 35 deletions(-) create mode 100644 src/entities/item.rs create mode 100644 src/entities/raw_types.rs create mode 100644 src/entities/raws/noodles.json diff --git a/Cargo.lock b/Cargo.lock index 9cffdfec1..cd0809898 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,6 +454,11 @@ name = "maplit" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "memchr" version = "2.2.0" @@ -1218,6 +1223,7 @@ dependencies = [ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "nom 5.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "proptest 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1226,6 +1232,7 @@ dependencies = [ "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1301,6 +1308,7 @@ dependencies = [ "checksum log-mdc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" "checksum log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "100052474df98158c0738a7d3f4249c99978490178b5f9f68cd835ac57adbd1b" "checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" "checksum miniz-sys 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9e3ae51cea1576ceba0dde3d484d30e6e5b86dee0b2d412fe3a16a15c98202" "checksum miniz_oxide 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c468f2369f07d651a5d0bb2c9079f8488a66d5466efe42d0c5c6466edcb7f71e" diff --git a/Cargo.toml b/Cargo.toml index 3de1dbbe8..b290f6b44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ itertools = "*" lazy_static = "*" log = "*" log4rs = "*" +matches = "0.1.8" maplit = "^1.0.1" nom = "^5.0.0" prettytable-rs = "^0.8" @@ -23,6 +24,7 @@ rand = {version = "^0.7.0", features = ["small_rng"]} serde = "^1.0.8" serde_derive = "^1.0.8" serde_json = "*" +serde_yaml = "0.8" termion = "*" toml = "^0.5.1" diff --git a/src/display/color.rs b/src/display/color.rs index 7de1f124b..7d024a960 100644 --- a/src/display/color.rs +++ b/src/display/color.rs @@ -35,6 +35,12 @@ impl<'a> color::Color for &'a Color { } } +impl Default for Color { + fn default() -> Self { + Color::new(color::Reset) + } +} + pub struct ColorVisitor { marker: PhantomData Color>, } diff --git a/src/entities/entity_char.rs b/src/entities/entity_char.rs index 578aaf3da..2f8458200 100644 --- a/src/entities/entity_char.rs +++ b/src/entities/entity_char.rs @@ -4,7 +4,9 @@ use termion::color; #[derive(Debug, Deserialize)] pub struct EntityChar { + #[serde(default)] color: Color, + #[serde(rename = "char")] chr: char, } diff --git a/src/entities/item.rs b/src/entities/item.rs new file mode 100644 index 000000000..d0ecc090e --- /dev/null +++ b/src/entities/item.rs @@ -0,0 +1,44 @@ +use crate::display; +use crate::entities::raws::{raw, EntityRaw, ItemType}; +use crate::entities::EntityID; +use crate::types::Position; +use std::io::{self, Write}; + +#[derive(Debug, Clone)] +pub struct Item { + pub id: Option, + pub typ: &'static ItemType<'static>, + pub position: Position, +} + +impl Item { + pub fn new_from_raw(name: &'static str, position: Position) -> Self { + match raw(name) { + EntityRaw::Item(typ) => Self::new_with_type(typ, position), + _ => panic!("Invalid raw type for {:?}, expected Item", name), + } + } + + pub fn new_with_type( + typ: &'static ItemType<'static>, + position: Position, + ) -> Self { + Item { + id: None, + typ, + position, + } + } + + pub fn is_edible(&self) -> bool { + self.typ.is_edible() + } +} + +entity!(Item); + +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 ed83f2f46..c54a587e6 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -3,12 +3,15 @@ pub mod entity; pub mod character; pub mod creature; pub mod entity_char; +pub mod item; +pub mod raw_types; pub mod raws; pub use character::Character; pub use creature::Creature; pub use entity::{Entity, Identified}; pub use entity_char::EntityChar; +pub use item::Item; pub use raws::raw; pub type EntityID = u32; diff --git a/src/entities/raw_types.rs b/src/entities/raw_types.rs new file mode 100644 index 000000000..8f64e60d9 --- /dev/null +++ b/src/entities/raw_types.rs @@ -0,0 +1,104 @@ +use crate::entities::entity_char::EntityChar; +use crate::messages::Message; +use crate::types::Speed; + +#[derive(Debug, Deserialize)] +pub struct CreatureType<'a> { + /// The name of the creature. Used in raw lookups. + pub name: &'a str, + + /// A description of the entity, used by the "look" command + pub description: &'a str, + + #[serde(rename = "char")] + pub chr: EntityChar, + pub max_hitpoints: u16, + pub speed: Speed, + pub friendly: bool, +} + +#[derive(Debug, Deserialize)] +pub struct EdibleItem<'a> { + #[serde(borrow)] + pub eat_message: Option>, + + /// The number of hitpoints that eating this item heals + pub hitpoints_healed: u16, +} + +#[derive(Debug, Deserialize)] +pub struct ItemType<'a> { + pub name: &'a str, + + /// A description of the item, used by the "look" command + pub description: &'a str, + + pub edible_item: Option>, + + #[serde(rename = "char")] + pub chr: EntityChar, +} + +#[cfg(test)] +mod item_type_tests { + use super::*; + + #[test] + fn test_deserialize_item_type() { + let result = serde_json::from_str( + r#"{ + "Item": { + "name": "noodles", + "description": "You know exactly what kind of noodles", + "char": { "char": "n" }, + "edible_item": { + "eat_message": "You slurp up the noodles", + "hitpoints_healed": 2 + } + } + }"#, + ) + .unwrap(); + assert_matches!(result, EntityRaw::Item(_)); + if let EntityRaw::Item(item) = result { + assert_eq!(item.name, "noodles"); + } + + let toml_result = toml::from_str( + r#"[Item] +name = "noodles" +description = "You know exactly what kind of noodles" +char = { char = "🍜" } +edible_item = { eat_message = "You slurp up the noodles", hitpoints_healed = 2 } +"#, + ) + .unwrap(); + + assert_matches!(toml_result, EntityRaw::Item(_)); + if let EntityRaw::Item(item) = toml_result { + assert_eq!(item.name, "noodles"); + } + } +} + +impl<'a> ItemType<'a> { + pub fn is_edible(&self) -> bool { + self.edible_item.is_some() + } +} + +#[derive(Debug, Deserialize)] +pub enum EntityRaw<'a> { + Creature(#[serde(borrow)] CreatureType<'a>), + Item(#[serde(borrow)] ItemType<'a>), +} + +impl<'a> EntityRaw<'a> { + pub fn name(&self) -> &'a str { + use EntityRaw::*; + match self { + Creature(typ) => typ.name, + Item(typ) => typ.name, + } + } +} diff --git a/src/entities/raws.rs b/src/entities/raws.rs index da061d89d..2c4a8203c 100644 --- a/src/entities/raws.rs +++ b/src/entities/raws.rs @@ -1,37 +1,8 @@ -use crate::entities::entity_char::EntityChar; -use crate::types::Speed; +pub use crate::entities::raw_types::{CreatureType, EntityRaw, ItemType}; use std::collections::HashMap; -#[derive(Debug, Deserialize)] -pub struct CreatureType<'a> { - /// The name of the creature. Used in raw lookups. - pub name: &'a str, - - /// A description of the entity, used by the "look" command - pub description: &'a str, - - #[serde(rename = "char")] - pub chr: EntityChar, - pub max_hitpoints: u16, - pub speed: Speed, - pub friendly: bool, -} - -#[derive(Debug, Deserialize)] -pub enum EntityRaw<'a> { - Creature(#[serde(borrow)] CreatureType<'a>), -} - -impl<'a> EntityRaw<'a> { - pub fn name(&self) -> &'a str { - match self { - EntityRaw::Creature(typ) => typ.name, - } - } -} - static_cfg! { - static ref RAWS: Vec> = toml_dir("src/entities/raws"); + static ref RAWS: Vec> = cfg_dir("src/entities/raws"); } lazy_static! { @@ -54,3 +25,14 @@ pub fn raw(name: &'static str) -> &'static EntityRaw<'static> { .map(|e| *e) .expect(format!("Raw not found: {}", name).as_str()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_raws() { + RAWS_BY_NAME.keys(); + assert_eq!(raw("noodles").name(), "noodles"); + } +} diff --git a/src/entities/raws/noodles.json b/src/entities/raws/noodles.json new file mode 100644 index 000000000..d4b773cac --- /dev/null +++ b/src/entities/raws/noodles.json @@ -0,0 +1,14 @@ +{ + "Item": { + "name": "noodles", + "char": { + "char": "🍜" + }, + "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 af9b0ac93..c4fc6d2be 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,5 +1,7 @@ use crate::display::{self, Viewport}; -use crate::entities::{Character, Creature, Entity, EntityID, Identified}; +use crate::entities::{ + Character, Creature, Entity, EntityID, Identified, Item, +}; use crate::messages::message; use crate::settings::Settings; use crate::types::command::Command; @@ -80,6 +82,9 @@ impl<'a> Game<'a> { "gormlak", pos(10, 0), ))); + + entities + .insert(Box::new(Item::new_from_raw("noodles", pos(0, 10)))); } Game { diff --git a/src/main.rs b/src/main.rs index 8bad5c057..69b7304e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ extern crate serde; extern crate toml; #[macro_use] extern crate serde_derive; +extern crate serde_json; #[macro_use] extern crate clap; #[macro_use] @@ -23,6 +24,8 @@ extern crate backtrace; extern crate include_dir; #[macro_use] extern crate nom; +#[macro_use] +extern crate matches; #[macro_use] mod util; diff --git a/src/messages.rs b/src/messages.rs index 9ca78025e..719389fa6 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; #[derive(Deserialize, Debug, PartialEq, Eq)] #[serde(untagged)] -enum Message<'a> { +pub enum Message<'a> { #[serde(borrow)] Single(Template<'a>), Choice(Vec>), @@ -123,6 +123,13 @@ static_cfg! { static ref MESSAGES: NestedMap<'static> = toml_file("messages.toml"); } +pub fn get( + name: &'static str, + rng: &mut R, +) -> Option<&'static Template<'static>> { + MESSAGES.lookup(name).and_then(|msg| msg.resolve(rng)) +} + /// Look up and format a game message based on the given (dot-separated) name, /// with the given random generator used to select from choice-based messages pub fn message<'a, R: Rng + ?Sized>( @@ -130,7 +137,7 @@ pub fn message<'a, R: Rng + ?Sized>( rng: &mut R, params: &TemplateParams<'a>, ) -> String { - match MESSAGES.lookup(name).and_then(|msg| msg.resolve(rng)) { + match get(name, rng) { Some(msg) => msg.format(params).unwrap_or_else(|e| { error!("Error formatting template: {}", e); "Template Error".to_string() diff --git a/src/messages.toml b/src/messages.toml index d3e0e1de8..e7d097a76 100644 --- a/src/messages.toml +++ b/src/messages.toml @@ -9,3 +9,6 @@ killed = [ "The {{creature.name}} kicks it.", "The {{creature.name}} beefs it." ] + +[defaults.item] +eat = "You eat the {{item.name}}" diff --git a/src/util/static_cfg.rs b/src/util/static_cfg.rs index 1b4864df7..b20456fb3 100644 --- a/src/util/static_cfg.rs +++ b/src/util/static_cfg.rs @@ -14,6 +14,9 @@ macro_rules! __static_cfg_include { (json_dir, $filename:expr) => { include_dir!($filename) }; + (cfg_dir, $filename:expr) => { + include_dir!($filename) + }; } macro_rules! __static_cfg_type { @@ -21,6 +24,7 @@ macro_rules! __static_cfg_type { (json_file) => (&'static str); (toml_dir) => (include_dir::Dir<'static>); (json_dir) => (include_dir::Dir<'static>); + (cfg_dir) => (include_dir::Dir<'static>); } macro_rules! __static_cfg_parse { @@ -39,6 +43,10 @@ macro_rules! __static_cfg_parse { (json_dir, $e:expr) => { crate::util::static_cfg::parse_json_dir($e) }; + + (cfg_dir, $e:expr) => { + crate::util::static_cfg::parse_cfg_dir($e); + }; } macro_rules! __static_cfg_inner { @@ -70,13 +78,61 @@ macro_rules! static_cfg { () => () } +pub fn parse_cfg_dir<'a, T>(d: Dir<'a>) -> Vec +where + T: de::Deserialize<'a>, +{ + d.files() + .iter() + .filter_map(|f| { + let path = f.path(); + let contents = f.contents_utf8().unwrap(); + match path.extension().and_then(|e| e.to_str()) { + Some("toml") => { + Some(toml::from_str(contents).unwrap_or_else(|e| { + panic!( + "Error parsing TOML file {}: {}", + path.display(), + e + ) + })) + } + Some("json") => { + Some(serde_json::from_str(contents).unwrap_or_else(|e| { + panic!( + "Error parsing JSON file {}: {}", + path.display(), + e + ) + })) + } + // > YAML currently does not support zero-copy deserialization + // Some("yaml") => { + // Some(serde_yaml::from_str(contents).unwrap_or_else(|e| { + // panic!( + // "Error parsing YAML file {}: {}", + // path.display(), + // e + // ) + // })) + // } + _ => None, + } + }) + .collect() +} + pub fn parse_toml_dir<'a, T>(d: Dir<'a>) -> Vec where T: de::Deserialize<'a>, { d.files() .iter() - .map(|f| toml::from_str(f.contents_utf8().unwrap()).unwrap()) + .map(|f| { + toml::from_str(f.contents_utf8().unwrap()).unwrap_or_else(|e| { + panic!("Error parsing TOML file {}: {}", f.path, e) + }) + }) .collect() }