From 06c8289eae9fb20ad3d19790ca77fe295c56500c Mon Sep 17 00:00:00 2001 From: jeff Date: Thu, 31 Dec 2020 22:21:05 +0000 Subject: [PATCH] Add skills to the character editor. --- build.rs | 1 + src/errors.rs | 3 + src/models/characters.rs | 18 +++- src/models/convert.rs | 2 +- src/models/proto.rs | 23 +---- src/models/proto/cofd.rs | 93 +++++++++++++++++++ src/routes/characters/new.rs | 4 +- templates/characters/edit_character.html.tera | 58 ++++++++++++ .../edit_character_macros.html.tera | 9 +- .../view_changeling_character.html.tera | 4 + 10 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 src/models/proto/cofd.rs diff --git a/build.rs b/build.rs index 0695b92..a9bb398 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,6 @@ fn main() { let mut config = prost_build::Config::new(); + config.btree_map(&["."]); config.type_attribute(".", "#[derive(Serialize)]"); config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]"); config diff --git a/src/errors.rs b/src/errors.rs index adce3ac..f631094 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,6 +20,9 @@ pub enum Error { #[error("invalid input")] InvalidInput, + #[error("validation error: {0}")] + ValidationError(#[from] crate::models::convert::ValidationError), + #[error("serialization error: {0}")] SerializationError(#[from] prost::EncodeError), diff --git a/src/models/characters.rs b/src/models/characters.rs index 3742a5c..dfaf31c 100644 --- a/src/models/characters.rs +++ b/src/models/characters.rs @@ -1,4 +1,5 @@ use crate::errors::Error; +use crate::models::proto::cofd::cofd_sheet::*; use crate::models::proto::cofd::*; use crate::models::users::User; use prost::bytes::BytesMut; @@ -39,6 +40,14 @@ pub(crate) trait Visibility { } } +/// Enum representing all game systems supported by the character +/// service. Game systems are kept unique instead of lumping them +/// together under common umbrella systems, even if the different +/// games use the same (or similar) character sheets. This is because +/// of the possibility for slight differences in rules and data +/// between even similar systems. It's simpler to err on the side of +/// uniqueness. Enum variants are also versioned, in case of drastic +/// rewrites or migrations in the future. #[derive(Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString, sqlx::Type)] #[sqlx(rename_all = "snake_case")] pub enum CharacterDataType { @@ -47,19 +56,20 @@ pub enum CharacterDataType { } impl CharacterDataType { - pub fn create_data(&self) -> Result { + /// Create the default serialized protobuf data (character sheet) + /// for the game system represented by the enum variant. + pub fn default_serialized_data(&self) -> Result { use prost::Message; use CharacterDataType::*; let data: BytesMut = match self { ChroniclesOfDarknessV1 => { - let sheet = CofdSheet::default(); + let sheet = CofdSheet::default_sheet(); let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet)); sheet.encode(&mut buf)?; buf } ChangelingV1 => { - let mut sheet = ChangelingSheet::default(); - sheet.base = Some(CofdSheet::default()); + let sheet = ChangelingSheet::default_sheet(); let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet)); sheet.encode(&mut buf)?; buf diff --git a/src/models/convert.rs b/src/models/convert.rs index c108555..ca34ad4 100644 --- a/src/models/convert.rs +++ b/src/models/convert.rs @@ -4,7 +4,7 @@ use rocket::request::FromFormValue; use std::str::FromStr; use thiserror::Error; -/// Validation errors specific to the new character form. +/// Validation errors from form submissions. #[derive(Serialize, Error, Debug, Clone)] pub enum ValidationError { #[error("bad UTF-8 encoding")] diff --git a/src/models/proto.rs b/src/models/proto.rs index fc00eb1..5171ebd 100644 --- a/src/models/proto.rs +++ b/src/models/proto.rs @@ -4,27 +4,7 @@ use rocket::request::Request; use std::default::Default; use std::ops::Deref; -/// Contains the generated Chronicles of Darkness-related protocol -/// buffer types. -pub mod cofd { - include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs")); - - pub mod api { - include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs")); - } - - // TODO these values are not available in tera templates, so how to - // handle? - pub(crate) trait DerivedStats { - fn speed(&self) -> i32; - } - - impl DerivedStats for CofdSheet { - fn speed(&self) -> i32 { - self.size + self.stamina - } - } -} +pub mod cofd; /// A struct wrapping a protobuf that allows it to be used as binary /// data submitted via POST using fetch API. Can automatically be @@ -58,6 +38,7 @@ where } } +/// Enable automatically calling methods on a decoded Proto instance. impl Deref for Proto where T: prost::Message + Default, diff --git a/src/models/proto/cofd.rs b/src/models/proto/cofd.rs new file mode 100644 index 0000000..5b4b75d --- /dev/null +++ b/src/models/proto/cofd.rs @@ -0,0 +1,93 @@ +//! Contains the generated Chronicles of Darkness-related protocol +//! buffer types, as well as utilities and extensions for working with +//! them. + +use std::collections::BTreeMap; + +//Add the generated protobuf code into this module. +include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs")); + +//Add the API protobuf genreated code for the api module. +pub mod api { + include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs")); +} + +const MENTAL_SKILLS: &'static [&'static str] = &[ + "Academics", + "Computer", + "Crafts", + "Investigation", + "Medicine", + "Occult", + "Politics", + "Science", +]; + +const PHYSICAL_SKILLS: &'static [&'static str] = &[ + "Athletics", + "Brawl", + "Drive", + "Firearms", + "Larceny", + "Stealth", + "Survival", + "Weaponry", +]; + +const SOCIAL_SKILLS: &'static [&'static str] = &[ + "Animal Ken", + "Empathy", + "Expression", + "Intimidation", + "Persuasion", + "Socialize", + "Streetwise", + "Subterfuge", +]; + +/// Create a pre-populated skill list based on skill names given to +/// the function. The list of skill names is turned into a sorted Map +/// of skill name to default Skill protobuf instances. +fn create_skill_list(skill_names: &[&str]) -> BTreeMap { + skill_names + .into_iter() + .map(|skill_name| (skill_name.to_string(), cofd_sheet::Skill::default())) + .collect() +} + +impl CofdSheet { + /// Create the default (blank) character sheet for a core + /// Chronicles of Darkness character. This fills in skills and + /// other information that needs to be pre-populated. + pub fn default_sheet() -> CofdSheet { + let mut sheet = Self::default(); + sheet.mental_skills = create_skill_list(&MENTAL_SKILLS); + sheet.physical_skills = create_skill_list(&PHYSICAL_SKILLS); + sheet.social_skills = create_skill_list(&SOCIAL_SKILLS); + sheet + } +} + +impl ChangelingSheet { + /// Create the default (blank) character sheet for a Changeling + /// character. This fills in skills and other information that + /// needs to be pre-populated. + pub fn default_sheet() -> ChangelingSheet { + let mut sheet = Self::default(); + sheet.base = Some(CofdSheet::default_sheet()); + //TODO fill in changeling-specific stuff + sheet + } +} + +// TODO these values are not available in tera templates, so how to +// handle? +pub(crate) trait DerivedStats { + fn speed(&self) -> i32; +} + +impl DerivedStats for CofdSheet { + fn speed(&self) -> i32 { + self.size + self.stamina + } +} diff --git a/src/routes/characters/new.rs b/src/routes/characters/new.rs index 0499915..265c633 100644 --- a/src/routes/characters/new.rs +++ b/src/routes/characters/new.rs @@ -61,8 +61,8 @@ async fn create_new_character( user_id: i32, conn: TenebrousDbConn<'_>, ) -> Result<(), Error> { - let system: CharacterDataType = *form.system.as_ref().unwrap(); - let sheet: Vec = system.create_data()?.to_vec(); + let system: CharacterDataType = *form.system.as_ref().map_err(|_| Error::InvalidInput)?; + let sheet: Vec = system.default_serialized_data()?.to_vec(); let insert = NewCharacter { user_id: user_id, diff --git a/templates/characters/edit_character.html.tera b/templates/characters/edit_character.html.tera index cd2f79a..681b919 100644 --- a/templates/characters/edit_character.html.tera +++ b/templates/characters/edit_character.html.tera @@ -44,6 +44,44 @@ background-color: lightgray; margin: 0; } + + #skills { + padding: 4px; + border-collapse: collapse; + } + + #skills .skills-section { + border: 1px solid gray; + border-collapse: collapse; + display: table-cell; + } + + .skill { + margin: 0; + padding: 0; + display: flex; + } + + .skill label { + display: inline-block; + float: left; + clear: left; + width: 10em; + text-align: right; + vertical-align: text-bottom; + padding: 8px; + margin: 0; + } + + .skill input { + max-width: 4em; + display: inline-block; + float: left; + padding: 8px; + border: none; + background-color: lightgray; + margin: 0; + } @@ -72,5 +110,25 @@ {{ macros::attribute(name="Composure", value=sheet.composure) }} + +
+
+ {% for skill_name, skill in sheet.mentalSkills %} + {{ macros::skill(name=skill_name, value=skill.dots) }} + {% endfor %} +
+ +
+ {% for skill_name, skill in sheet.physicalSkills %} + {{ macros::skill(name=skill_name, value=skill.dots) }} + {% endfor %} +
+ +
+ {% for skill_name, skill in sheet.socialSkills %} + {{ macros::skill(name=skill_name, value=skill.dots) }} + {% endfor %} +
+
{% endblock content %} diff --git a/templates/characters/edit_character_macros.html.tera b/templates/characters/edit_character_macros.html.tera index ba36eef..903af74 100644 --- a/templates/characters/edit_character_macros.html.tera +++ b/templates/characters/edit_character_macros.html.tera @@ -3,4 +3,11 @@ -{% endmacro input %} +{% endmacro attribute %} + +{% macro skill(name, value) %} +
+ + +
+{% endmacro skill %} diff --git a/templates/characters/view_changeling_character.html.tera b/templates/characters/view_changeling_character.html.tera index 030ee93..82cc180 100644 --- a/templates/characters/view_changeling_character.html.tera +++ b/templates/characters/view_changeling_character.html.tera @@ -8,4 +8,8 @@

System: {{data_type}}

Strength: {{sheet.base.strength}}

+ + {% endblock content %}