Switch to oneof field for game-specific information and values.

Having specific protobuf types for different game systems using the
same rule set (e.g. all the Chronicles of Darkness games) is untenable
because protobuf does not have inheritance or mixins.

Instead, we have one generic character sheet type, with a oneof field
for the game specifics. This will be used in conjunction with the
character's game system (stored in db) to render different stuff on
the character templates.

Without this, we'd wind up having duplicate templates, a lot more code
for handling specifics of each game system, and so on.
This commit is contained in:
jeff 2021-01-01 23:34:50 +00:00
parent 14863353f0
commit b467c32acb
8 changed files with 71 additions and 189 deletions

View File

@ -2,6 +2,24 @@ 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 {
@ -72,8 +90,10 @@ message CofdSheet {
repeated Attack attacks = 26; repeated Attack attacks = 26;
map<string, string> other_data = 27; map<string, string> other_data = 27;
}
message ChangelingSheet { oneof system_fields {
CofdSheet base = 1; CoreFields core = 28;
} MageFields mage = 29;
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_v1', 'changeling_v1'))"#; let db_enum = r#"CHECK(data_type IN ('chronicles_of_darkness', 'changeling'))"#;
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,5 +1,4 @@
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;
@ -46,12 +45,16 @@ 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. Enum variants are also versioned, in case of drastic /// uniqueness. Usually, systems based on the same ruleset will have
/// rewrites or migrations in the future. /// one character sheet type, with a oneof field for game-specific
/// 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.
ChroniclesOfDarkness, ChroniclesOfDarkness,
/// A character for Changeling 2E rules.
Changeling, Changeling,
} }
@ -62,14 +65,8 @@ impl CharacterDataType {
use prost::Message; use prost::Message;
use CharacterDataType::*; use CharacterDataType::*;
let data: BytesMut = match self { let data: BytesMut = match self {
ChroniclesOfDarkness => { ChroniclesOfDarkness | Changeling => {
let sheet = CofdSheet::default_sheet(); let sheet = CofdSheet::default_sheet(*self);
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
Changeling => {
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
@ -78,6 +75,15 @@ 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
@ -122,7 +128,7 @@ impl Character {
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>()?), ChroniclesOfDarkness => Box::new(self.try_deserialize::<CofdSheet>()?),
Changeling => Box::new(self.try_deserialize::<ChangelingSheet>()?), Changeling => Box::new(self.try_deserialize::<CofdSheet>()?),
}; };
Ok(decoded) Ok(decoded)

View File

@ -2,6 +2,7 @@
//! 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.
@ -12,6 +13,8 @@ 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",
@ -23,6 +26,8 @@ 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",
@ -34,6 +39,8 @@ 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",
@ -56,26 +63,24 @@ fn create_skill_list(skill_names: &[&str]) -> BTreeMap<String, cofd_sheet::Skill
} }
impl CofdSheet { impl CofdSheet {
/// Create the default (blank) character sheet for a core /// Create the default (blank) character sheet for a Chronicles of
/// Chronicles of Darkness character. This fills in skills and /// Darkness-based character. This fills in skills and other
/// other information that needs to be pre-populated. /// information that needs to be pre-populated. System specifics
pub fn default_sheet() -> CofdSheet { /// are set based on the given character data type (aka game
/// 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
}
}
impl ChangelingSheet { use crate::models::proto::cofd::cofd_sheet::SystemFields;
/// Create the default (blank) character sheet for a Changeling let specifics: SystemFields = match system {
/// character. This fills in skills and other information that CharacterDataType::Changeling => SystemFields::Changeling(ChangelingFields::default()),
/// needs to be pre-populated. CharacterDataType::ChroniclesOfDarkness => SystemFields::Core(CoreFields::default()),
pub fn default_sheet() -> ChangelingSheet { };
let mut sheet = Self::default();
sheet.base = Some(CofdSheet::default_sheet()); sheet.system_fields = Some(specifics);
//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()?,
}; };
use CharacterDataType::*; let template = if character.data_type.is_cofd_system() {
let template = match character.data_type { Template::render("characters/view_character", context)
ChroniclesOfDarkness => Template::render("characters/view_character", context), } else {
Changeling => Template::render("characters/view_changeling_character", context), return Err(Error::InvalidInput);
}; };
Ok(template) Ok(template)

View File

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

View File

@ -1,134 +0,0 @@
{% 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

@ -1,15 +0,0 @@
{% 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 %}