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:
projectmoon 2021-01-01 23:34:50 +00:00
parent 149b843927
commit 0ca23b46c6
8 changed files with 71 additions and 189 deletions

View File

@ -2,6 +2,24 @@ syntax = "proto3";
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.
message CofdSheet {
message Merit {
@ -72,8 +90,10 @@ message CofdSheet {
repeated Attack attacks = 26;
map<string, string> other_data = 27;
}
message ChangelingSheet {
CofdSheet base = 1;
}
oneof system_fields {
CoreFields core = 28;
MageFields mage = 29;
ChangelingFields changeling = 30;
}
}

View File

@ -6,7 +6,7 @@ pub fn migration() -> String {
println!("Applying migration: {}", file!());
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("user_id", types::integer());
t.add_column("viewable", types::boolean());

View File

@ -1,5 +1,4 @@
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;
@ -46,12 +45,16 @@ pub(crate) trait Visibility {
/// 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.
/// uniqueness. Usually, systems based on the same ruleset will have
/// one character sheet type, with a oneof field for game-specific
/// information.
#[derive(Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString, sqlx::Type)]
#[sqlx(rename_all = "snake_case")]
pub enum CharacterDataType {
/// A character for the core Chronicles of Darkness rules.
ChroniclesOfDarkness,
/// A character for Changeling 2E rules.
Changeling,
}
@ -62,14 +65,8 @@ impl CharacterDataType {
use prost::Message;
use CharacterDataType::*;
let data: BytesMut = match self {
ChroniclesOfDarkness => {
let sheet = CofdSheet::default_sheet();
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
Changeling => {
let sheet = ChangelingSheet::default_sheet();
ChroniclesOfDarkness | Changeling => {
let sheet = CofdSheet::default_sheet(*self);
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
@ -78,6 +75,15 @@ impl CharacterDataType {
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
@ -122,7 +128,7 @@ impl Character {
use CharacterDataType::*;
let decoded: Box<dyn erased_serde::Serialize> = match self.data_type {
ChroniclesOfDarkness => Box::new(self.try_deserialize::<CofdSheet>()?),
Changeling => Box::new(self.try_deserialize::<ChangelingSheet>()?),
Changeling => Box::new(self.try_deserialize::<CofdSheet>()?),
};
Ok(decoded)

View File

@ -2,6 +2,7 @@
//! buffer types, as well as utilities and extensions for working with
//! them.
use crate::models::characters::CharacterDataType;
use std::collections::BTreeMap;
//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"));
}
/// Default mental skill names for a regular Chronicles of Darkness
/// (or derivative system) game.
const MENTAL_SKILLS: &'static [&'static str] = &[
"Academics",
"Computer",
@ -23,6 +26,8 @@ const MENTAL_SKILLS: &'static [&'static str] = &[
"Science",
];
/// Default physical skill names for a regular Chronicles of Darkness
/// (or derivative system) game.
const PHYSICAL_SKILLS: &'static [&'static str] = &[
"Athletics",
"Brawl",
@ -34,6 +39,8 @@ const PHYSICAL_SKILLS: &'static [&'static str] = &[
"Weaponry",
];
/// Default social skill names for a regular Chronicles of Darkness
/// (or derivative system) game.
const SOCIAL_SKILLS: &'static [&'static str] = &[
"Animal Ken",
"Empathy",
@ -56,26 +63,24 @@ fn create_skill_list(skill_names: &[&str]) -> BTreeMap<String, cofd_sheet::Skill
}
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 {
/// Create the default (blank) character sheet for a Chronicles of
/// Darkness-based character. This fills in skills and other
/// information that needs to be pre-populated. System specifics
/// are set based on the given character data type (aka game
/// system).
pub fn default_sheet(system: CharacterDataType) -> 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
use crate::models::proto::cofd::cofd_sheet::SystemFields;
let specifics: SystemFields = match system {
CharacterDataType::Changeling => SystemFields::Changeling(ChangelingFields::default()),
CharacterDataType::ChroniclesOfDarkness => SystemFields::Core(CoreFields::default()),
};
sheet.system_fields = Some(specifics);
sheet
}
}

View File

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

View File

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