Compare commits

..

No commits in common. "b467c32acb970ffd7c5d056c60fdceadcf83ed58" and "2cb547e16ab9760211ba2686a1d5dc2fe47dc1ab" have entirely different histories.

9 changed files with 194 additions and 76 deletions

View File

@ -2,24 +2,6 @@ syntax = "proto3";
package models.proto.cofd; package models.proto.cofd;
//TODO do we want a single "morality" value, or keep it separate
//inside the system-specific fields?
//Information and values specific to the core game.
message CoreFields {
int32 integrity = 1;
}
//Information and values specific to Mage 2E.
message MageFields {
int32 widsom = 1;
}
//Information and values specific to Changeling 2E.
message ChangelingFields {
int32 clarity = 1;
}
//Base sheet for Chronicles of Darkness systems. //Base sheet for Chronicles of Darkness systems.
message CofdSheet { message CofdSheet {
message Merit { message Merit {
@ -90,10 +72,8 @@ message CofdSheet {
repeated Attack attacks = 26; repeated Attack attacks = 26;
map<string, string> other_data = 27; map<string, string> other_data = 27;
}
oneof system_fields {
CoreFields core = 28; message ChangelingSheet {
MageFields mage = 29; CofdSheet base = 1;
ChangelingFields changeling = 30;
}
} }

View File

@ -6,7 +6,7 @@ pub fn migration() -> String {
println!("Applying migration: {}", file!()); println!("Applying migration: {}", file!());
m.create_table("characters", move |t| { m.create_table("characters", move |t| {
let db_enum = r#"CHECK(data_type IN ('chronicles_of_darkness', 'changeling'))"#; let db_enum = r#"CHECK(data_type IN ('chronicles_of_darkness_v1', 'changeling_v1'))"#;
t.add_column("id", types::primary()); t.add_column("id", types::primary());
t.add_column("user_id", types::integer()); t.add_column("user_id", types::integer());
t.add_column("viewable", types::boolean()); t.add_column("viewable", types::boolean());

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;
@ -45,17 +46,13 @@ pub(crate) trait Visibility {
/// games use the same (or similar) character sheets. This is because /// games use the same (or similar) character sheets. This is because
/// of the possibility for slight differences in rules and data /// of the possibility for slight differences in rules and data
/// between even similar systems. It's simpler to err on the side of /// between even similar systems. It's simpler to err on the side of
/// uniqueness. Usually, systems based on the same ruleset will have /// uniqueness. Enum variants are also versioned, in case of drastic
/// one character sheet type, with a oneof field for game-specific /// rewrites or migrations in the future.
/// information.
#[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 {
/// A character for the core Chronicles of Darkness rules. ChroniclesOfDarknessV1,
ChroniclesOfDarkness, ChangelingV1,
/// A character for Changeling 2E rules.
Changeling,
} }
impl CharacterDataType { impl CharacterDataType {
@ -65,8 +62,14 @@ impl CharacterDataType {
use prost::Message; use prost::Message;
use CharacterDataType::*; use CharacterDataType::*;
let data: BytesMut = match self { let data: BytesMut = match self {
ChroniclesOfDarkness | Changeling => { ChroniclesOfDarknessV1 => {
let sheet = CofdSheet::default_sheet(*self); let sheet = CofdSheet::default_sheet();
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
ChangelingV1 => {
let sheet = ChangelingSheet::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
@ -75,15 +78,6 @@ impl CharacterDataType {
Ok(data) Ok(data)
} }
/// Returns whether or not this enum variant represents a Chronicles
/// of Darkness game system.
pub fn is_cofd_system(&self) -> bool {
use CharacterDataType::*;
match self {
ChroniclesOfDarkness | Changeling => true,
}
}
} }
/// An entry that appears in a user's character list. Properties are /// An entry that appears in a user's character list. Properties are
@ -127,8 +121,8 @@ impl Character {
pub fn dyn_deserialize(&self) -> Result<Box<DynCharacterData>, Error> { pub fn dyn_deserialize(&self) -> Result<Box<DynCharacterData>, Error> {
use CharacterDataType::*; use CharacterDataType::*;
let decoded: Box<dyn erased_serde::Serialize> = match self.data_type { let decoded: Box<dyn erased_serde::Serialize> = match self.data_type {
ChroniclesOfDarkness => Box::new(self.try_deserialize::<CofdSheet>()?), ChroniclesOfDarknessV1 => Box::new(self.try_deserialize::<CofdSheet>()?),
Changeling => Box::new(self.try_deserialize::<CofdSheet>()?), ChangelingV1 => Box::new(self.try_deserialize::<ChangelingSheet>()?),
}; };
Ok(decoded) Ok(decoded)

View File

@ -2,7 +2,6 @@
//! buffer types, as well as utilities and extensions for working with //! buffer types, as well as utilities and extensions for working with
//! them. //! them.
use crate::models::characters::CharacterDataType;
use std::collections::BTreeMap; use std::collections::BTreeMap;
//Add the generated protobuf code into this module. //Add the generated protobuf code into this module.
@ -13,8 +12,6 @@ pub mod api {
include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs")); include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs"));
} }
/// Default mental skill names for a regular Chronicles of Darkness
/// (or derivative system) game.
const MENTAL_SKILLS: &'static [&'static str] = &[ const MENTAL_SKILLS: &'static [&'static str] = &[
"Academics", "Academics",
"Computer", "Computer",
@ -26,8 +23,6 @@ const MENTAL_SKILLS: &'static [&'static str] = &[
"Science", "Science",
]; ];
/// Default physical skill names for a regular Chronicles of Darkness
/// (or derivative system) game.
const PHYSICAL_SKILLS: &'static [&'static str] = &[ const PHYSICAL_SKILLS: &'static [&'static str] = &[
"Athletics", "Athletics",
"Brawl", "Brawl",
@ -39,8 +34,6 @@ const PHYSICAL_SKILLS: &'static [&'static str] = &[
"Weaponry", "Weaponry",
]; ];
/// Default social skill names for a regular Chronicles of Darkness
/// (or derivative system) game.
const SOCIAL_SKILLS: &'static [&'static str] = &[ const SOCIAL_SKILLS: &'static [&'static str] = &[
"Animal Ken", "Animal Ken",
"Empathy", "Empathy",
@ -63,24 +56,26 @@ fn create_skill_list(skill_names: &[&str]) -> BTreeMap<String, cofd_sheet::Skill
} }
impl CofdSheet { impl CofdSheet {
/// Create the default (blank) character sheet for a Chronicles of /// Create the default (blank) character sheet for a core
/// Darkness-based character. This fills in skills and other /// Chronicles of Darkness character. This fills in skills and
/// information that needs to be pre-populated. System specifics /// other information that needs to be pre-populated.
/// are set based on the given character data type (aka game pub fn default_sheet() -> CofdSheet {
/// system).
pub fn default_sheet(system: CharacterDataType) -> CofdSheet {
let mut sheet = Self::default(); let mut sheet = Self::default();
sheet.mental_skills = create_skill_list(&MENTAL_SKILLS); sheet.mental_skills = create_skill_list(&MENTAL_SKILLS);
sheet.physical_skills = create_skill_list(&PHYSICAL_SKILLS); sheet.physical_skills = create_skill_list(&PHYSICAL_SKILLS);
sheet.social_skills = create_skill_list(&SOCIAL_SKILLS); sheet.social_skills = create_skill_list(&SOCIAL_SKILLS);
sheet
}
}
use crate::models::proto::cofd::cofd_sheet::SystemFields; impl ChangelingSheet {
let specifics: SystemFields = match system { /// Create the default (blank) character sheet for a Changeling
CharacterDataType::Changeling => SystemFields::Changeling(ChangelingFields::default()), /// character. This fills in skills and other information that
CharacterDataType::ChroniclesOfDarkness => SystemFields::Core(CoreFields::default()), /// needs to be pre-populated.
}; pub fn default_sheet() -> ChangelingSheet {
let mut sheet = Self::default();
sheet.system_fields = Some(specifics); sheet.base = Some(CofdSheet::default_sheet());
//TODO fill in changeling-specific stuff
sheet sheet
} }
} }

View File

@ -37,10 +37,10 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
sheet: character.dyn_deserialize()?, sheet: character.dyn_deserialize()?,
}; };
let template = if character.data_type.is_cofd_system() { use CharacterDataType::*;
Template::render("characters/view_character", context) let template = match character.data_type {
} else { ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
return Err(Error::InvalidInput); ChangelingV1 => Template::render("characters/view_changeling_character", context),
}; };
Ok(template) Ok(template)

View File

@ -33,10 +33,10 @@ fn edit_character_template(user: &User, character: Character) -> Result<Template
}, },
}; };
let template = if character.data_type.is_cofd_system() { use CharacterDataType::*;
Template::render("characters/edit_character", context) let template = match character.data_type {
} else { ChroniclesOfDarknessV1 => Template::render("characters/edit_character", context),
return Err(Error::InvalidInput); ChangelingV1 => Template::render("characters/edit_changeling_character", context),
}; };
Ok(template) Ok(template)

View File

@ -32,7 +32,7 @@ impl NewCharacterContext {
NewCharacterContext { NewCharacterContext {
name: "".to_string(), name: "".to_string(),
error_message: "".to_string(), error_message: "".to_string(),
selected_system: CharacterDataType::ChroniclesOfDarkness, selected_system: CharacterDataType::ChroniclesOfDarknessV1,
systems: CharacterDataType::iter().collect(), systems: CharacterDataType::iter().collect(),
} }
} }
@ -43,7 +43,7 @@ impl NewCharacterContext {
let system: CharacterDataType = *form let system: CharacterDataType = *form
.system .system
.as_ref() .as_ref()
.unwrap_or(&CharacterDataType::ChroniclesOfDarkness); .unwrap_or(&CharacterDataType::ChroniclesOfDarknessV1);
NewCharacterContext { NewCharacterContext {
name: form.name.clone(), name: form.name.clone(),

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

@ -0,0 +1,15 @@
{% extends "base" %}
{% block content %}
<h1>Changeling Sheet</h1>
<div>
<h1>Character {{name}}</h1>
<h3>User: {{username}}</h3>
<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 %}