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:
parent
149b843927
commit
0ca23b46c6
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
Loading…
Reference in New Issue