Compare commits

...

2 Commits

Author SHA1 Message Date
jeff 2b7f9b8897 Add ability to edit changeling characters. 2020-12-31 22:28:00 +00:00
jeff 06c8289eae Add skills to the character editor. 2020-12-31 22:21:05 +00:00
12 changed files with 344 additions and 40 deletions

View File

@ -1,5 +1,6 @@
fn main() { fn main() {
let mut config = prost_build::Config::new(); let mut config = prost_build::Config::new();
config.btree_map(&["."]);
config.type_attribute(".", "#[derive(Serialize)]"); config.type_attribute(".", "#[derive(Serialize)]");
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]"); config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
config config

View File

@ -20,6 +20,9 @@ pub enum Error {
#[error("invalid input")] #[error("invalid input")]
InvalidInput, InvalidInput,
#[error("validation error: {0}")]
ValidationError(#[from] crate::models::convert::ValidationError),
#[error("serialization error: {0}")] #[error("serialization error: {0}")]
SerializationError(#[from] prost::EncodeError), SerializationError(#[from] prost::EncodeError),

View File

@ -1,4 +1,5 @@
use crate::errors::Error; use crate::errors::Error;
use crate::models::proto::cofd::cofd_sheet::*;
use crate::models::proto::cofd::*; use crate::models::proto::cofd::*;
use crate::models::users::User; use crate::models::users::User;
use prost::bytes::BytesMut; 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)] #[derive(Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString, sqlx::Type)]
#[sqlx(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")]
pub enum CharacterDataType { pub enum CharacterDataType {
@ -47,19 +56,20 @@ pub enum CharacterDataType {
} }
impl CharacterDataType { impl CharacterDataType {
pub fn create_data(&self) -> Result<BytesMut, Error> { /// Create the default serialized protobuf data (character sheet)
/// for the game system represented by the enum variant.
pub fn default_serialized_data(&self) -> Result<BytesMut, Error> {
use prost::Message; use prost::Message;
use CharacterDataType::*; use CharacterDataType::*;
let data: BytesMut = match self { let data: BytesMut = match self {
ChroniclesOfDarknessV1 => { ChroniclesOfDarknessV1 => {
let sheet = CofdSheet::default(); let sheet = CofdSheet::default_sheet();
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet)); let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?; sheet.encode(&mut buf)?;
buf buf
} }
ChangelingV1 => { ChangelingV1 => {
let mut sheet = ChangelingSheet::default(); let sheet = ChangelingSheet::default_sheet();
sheet.base = Some(CofdSheet::default());
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet)); let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?; sheet.encode(&mut buf)?;
buf buf

View File

@ -4,7 +4,7 @@ use rocket::request::FromFormValue;
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error; use thiserror::Error;
/// Validation errors specific to the new character form. /// Validation errors from form submissions.
#[derive(Serialize, Error, Debug, Clone)] #[derive(Serialize, Error, Debug, Clone)]
pub enum ValidationError { pub enum ValidationError {
#[error("bad UTF-8 encoding")] #[error("bad UTF-8 encoding")]

View File

@ -4,27 +4,7 @@ use rocket::request::Request;
use std::default::Default; use std::default::Default;
use std::ops::Deref; use std::ops::Deref;
/// Contains the generated Chronicles of Darkness-related protocol pub mod cofd;
/// 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
}
}
}
/// A struct wrapping a protobuf that allows it to be used as binary /// A struct wrapping a protobuf that allows it to be used as binary
/// data submitted via POST using fetch API. Can automatically be /// 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<T> Deref for Proto<T> impl<T> Deref for Proto<T>
where where
T: prost::Message + Default, T: prost::Message + Default,

93
src/models/proto/cofd.rs Normal file
View File

@ -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<String, cofd_sheet::Skill> {
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
}
}

View File

@ -20,6 +20,28 @@ struct FormStateContext<'a> {
pub selected_system: &'a CharacterDataType, pub selected_system: &'a CharacterDataType,
} }
fn edit_character_template(user: &User, character: Character) -> Result<Template, Error> {
let character = character.uprade()?;
let context = EditCharacterContext {
name: &character.character_name,
username: &user.username,
data_type: &character.data_type,
sheet: character.dyn_deserialize()?,
state: FormStateContext {
selected_system: &character.data_type,
},
};
use CharacterDataType::*;
let template = match character.data_type {
ChroniclesOfDarknessV1 => Template::render("characters/edit_character", context),
ChangelingV1 => Template::render("characters/edit_changeling_character", context),
};
Ok(template)
}
#[get("/<owner>/<character_id>/edit")] #[get("/<owner>/<character_id>/edit")]
pub(super) async fn edit_character_page( pub(super) async fn edit_character_page(
character_id: i32, character_id: i32,
@ -39,15 +61,6 @@ pub(super) async fn edit_character_page(
return Err(Error::NoPermission); return Err(Error::NoPermission);
} }
let context = EditCharacterContext { let template = edit_character_template(logged_in_user, character)?;
name: &character.character_name, Ok(template)
username: &owner.username,
data_type: &character.data_type,
sheet: character.dyn_deserialize()?,
state: FormStateContext {
selected_system: &character.data_type,
},
};
Ok(Template::render("characters/edit_character", context))
} }

View File

@ -61,8 +61,8 @@ async fn create_new_character(
user_id: i32, user_id: i32,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn<'_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let system: CharacterDataType = *form.system.as_ref().unwrap(); let system: CharacterDataType = *form.system.as_ref().map_err(|_| Error::InvalidInput)?;
let sheet: Vec<u8> = system.create_data()?.to_vec(); let sheet: Vec<u8> = system.default_serialized_data()?.to_vec();
let insert = NewCharacter { let insert = NewCharacter {
user_id: user_id, user_id: user_id,

View File

@ -0,0 +1,134 @@
{% import "characters/edit_character_macros" as macros %}
{% extends "base" %}
{% block content %}
<style type="text/css">
body {
font-family: Liberation Sans, Arial;
}
#attributes {
padding: 4px;
border-collapse: collapse;
}
#attributes .attributes-section {
border: 1px solid gray;
border-collapse: collapse;
display: table-cell;
}
.attribute {
margin: 0;
padding: 0;
display: flex;
}
.attribute label {
display: inline-block;
float: left;
clear: left;
width: 10em;
text-align: right;
vertical-align: text-bottom;
padding: 8px;
margin: 0;
}
.attribute input {
max-width: 4em;
display: inline-block;
float: left;
padding: 8px;
border: none;
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;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
<script defer type="text/javascript" src="/scripts/api.js"></script>
<script defer type="text/javascript" src="/scripts/characters/edit.js"></script>
<h1>Core Sheet</h1>
<div>
<h1>Name: <input type="text" value="{{name}}" /></h1>
<p>System: {{data_type}}</p>
<div id="attributes">
<div class="attributes-section" id="mentalAttributes">
{{ macros::attribute(name="Intelligence", value=sheet.base.intelligence) }}
{{ macros::attribute(name="Wits", value=sheet.base.wits) }}
{{ macros::attribute(name="Resolve", value=sheet.base.resolve) }}
</div>
<div class="attributes-section" id="physicalAttributes">
{{ macros::attribute(name="Strength", value=sheet.base.strength) }}
{{ macros::attribute(name="Dexterity", value=sheet.base.dexterity) }}
{{ macros::attribute(name="Stamina", value=sheet.base.stamina) }}
</div>
<div class="attributes-section" id="socicalAttributes">
{{ macros::attribute(name="Presence", value=sheet.base.presence) }}
{{ macros::attribute(name="Manipulation", value=sheet.base.manipulation) }}
{{ macros::attribute(name="Composure", value=sheet.base.composure) }}
</div>
</div>
<div id="skills">
<div class="skills-section" id="mentalSkills">
{% for skill_name, skill in sheet.base.mentalSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
<div class="skills-section" id="physicalSkills">
{% for skill_name, skill in sheet.base.physicalSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
<div class="skills-section" id="socialSkills">
{% for skill_name, skill in sheet.base.socialSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
</div>
</div>
{% endblock content %}

View File

@ -44,6 +44,44 @@
background-color: lightgray; background-color: lightgray;
margin: 0; 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;
}
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
@ -72,5 +110,25 @@
{{ macros::attribute(name="Composure", value=sheet.composure) }} {{ macros::attribute(name="Composure", value=sheet.composure) }}
</div> </div>
</div> </div>
<div id="skills">
<div class="skills-section" id="mentalSkills">
{% for skill_name, skill in sheet.mentalSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
<div class="skills-section" id="physicalSkills">
{% for skill_name, skill in sheet.physicalSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
<div class="skills-section" id="socialSkills">
{% for skill_name, skill in sheet.socialSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -3,4 +3,11 @@
<label for="{{name}}">{{name}}:</label> <label for="{{name}}">{{name}}:</label>
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" /> <input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
</div> </div>
{% endmacro input %} {% endmacro attribute %}
{% macro skill(name, value) %}
<div class="skill">
<label for="{{name}}">{{name}}:</label>
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
</div>
{% endmacro skill %}

View File

@ -8,4 +8,8 @@
<p>System: {{data_type}}</h3> <p>System: {{data_type}}</h3>
<p>Strength: {{sheet.base.strength}}</p> <p>Strength: {{sheet.base.strength}}</p>
</div> </div>
<div>
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a>
</div>
{% endblock content %} {% endblock content %}