Compare commits
2 Commits
06b3e0b390
...
2b7f9b8897
Author | SHA1 | Date |
---|---|---|
jeff | 2b7f9b8897 | |
jeff | 06c8289eae |
1
build.rs
1
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
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
|
@ -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<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 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
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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<T> Deref for Proto<T>
|
||||
where
|
||||
T: prost::Message + Default,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -20,6 +20,28 @@ struct FormStateContext<'a> {
|
|||
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")]
|
||||
pub(super) async fn edit_character_page(
|
||||
character_id: i32,
|
||||
|
@ -39,15 +61,6 @@ pub(super) async fn edit_character_page(
|
|||
return Err(Error::NoPermission);
|
||||
}
|
||||
|
||||
let context = EditCharacterContext {
|
||||
name: &character.character_name,
|
||||
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))
|
||||
let template = edit_character_template(logged_in_user, character)?;
|
||||
Ok(template)
|
||||
}
|
||||
|
|
|
@ -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<u8> = system.create_data()?.to_vec();
|
||||
let system: CharacterDataType = *form.system.as_ref().map_err(|_| Error::InvalidInput)?;
|
||||
let sheet: Vec<u8> = system.default_serialized_data()?.to_vec();
|
||||
|
||||
let insert = NewCharacter {
|
||||
user_id: user_id,
|
||||
|
|
|
@ -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 %}
|
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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) }}
|
||||
</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>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -3,4 +3,11 @@
|
|||
<label for="{{name}}">{{name}}:</label>
|
||||
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
|
||||
</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 %}
|
||||
|
|
|
@ -8,4 +8,8 @@
|
|||
<p>System: {{data_type}}</h3>
|
||||
<p>Strength: {{sheet.base.strength}}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
Loading…
Reference in New Issue