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() {
|
fn main() {
|
||||||
let mut config = prost_build::Config::new();
|
let mut config = prost_build::Config::new();
|
||||||
|
config.btree_map(&["."]);
|
||||||
config.type_attribute(".", "#[derive(Serialize)]");
|
config.type_attribute(".", "#[derive(Serialize)]");
|
||||||
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
|
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
|
||||||
config
|
config
|
||||||
|
|
|
@ -20,6 +20,9 @@ pub enum Error {
|
||||||
#[error("invalid input")]
|
#[error("invalid input")]
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
|
|
||||||
|
#[error("validation error: {0}")]
|
||||||
|
ValidationError(#[from] crate::models::convert::ValidationError),
|
||||||
|
|
||||||
#[error("serialization error: {0}")]
|
#[error("serialization error: {0}")]
|
||||||
SerializationError(#[from] prost::EncodeError),
|
SerializationError(#[from] prost::EncodeError),
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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)]
|
#[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 {
|
||||||
|
@ -47,19 +56,20 @@ pub enum CharacterDataType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 prost::Message;
|
||||||
use CharacterDataType::*;
|
use CharacterDataType::*;
|
||||||
let data: BytesMut = match self {
|
let data: BytesMut = match self {
|
||||||
ChroniclesOfDarknessV1 => {
|
ChroniclesOfDarknessV1 => {
|
||||||
let sheet = CofdSheet::default();
|
let sheet = CofdSheet::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
|
||||||
}
|
}
|
||||||
ChangelingV1 => {
|
ChangelingV1 => {
|
||||||
let mut sheet = ChangelingSheet::default();
|
let sheet = ChangelingSheet::default_sheet();
|
||||||
sheet.base = Some(CofdSheet::default());
|
|
||||||
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
|
||||||
|
|
|
@ -4,7 +4,7 @@ use rocket::request::FromFormValue;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Validation errors specific to the new character form.
|
/// Validation errors from form submissions.
|
||||||
#[derive(Serialize, Error, Debug, Clone)]
|
#[derive(Serialize, Error, Debug, Clone)]
|
||||||
pub enum ValidationError {
|
pub enum ValidationError {
|
||||||
#[error("bad UTF-8 encoding")]
|
#[error("bad UTF-8 encoding")]
|
||||||
|
|
|
@ -4,27 +4,7 @@ use rocket::request::Request;
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
/// Contains the generated Chronicles of Darkness-related protocol
|
pub mod cofd;
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A struct wrapping a protobuf that allows it to be used as binary
|
/// A struct wrapping a protobuf that allows it to be used as binary
|
||||||
/// data submitted via POST using fetch API. Can automatically be
|
/// 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>
|
impl<T> Deref for Proto<T>
|
||||||
where
|
where
|
||||||
T: prost::Message + Default,
|
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,
|
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")]
|
#[get("/<owner>/<character_id>/edit")]
|
||||||
pub(super) async fn edit_character_page(
|
pub(super) async fn edit_character_page(
|
||||||
character_id: i32,
|
character_id: i32,
|
||||||
|
@ -39,15 +61,6 @@ pub(super) async fn edit_character_page(
|
||||||
return Err(Error::NoPermission);
|
return Err(Error::NoPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = EditCharacterContext {
|
let template = edit_character_template(logged_in_user, character)?;
|
||||||
name: &character.character_name,
|
Ok(template)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,8 +61,8 @@ async fn create_new_character(
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
conn: TenebrousDbConn<'_>,
|
conn: TenebrousDbConn<'_>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let system: CharacterDataType = *form.system.as_ref().unwrap();
|
let system: CharacterDataType = *form.system.as_ref().map_err(|_| Error::InvalidInput)?;
|
||||||
let sheet: Vec<u8> = system.create_data()?.to_vec();
|
let sheet: Vec<u8> = system.default_serialized_data()?.to_vec();
|
||||||
|
|
||||||
let insert = NewCharacter {
|
let insert = NewCharacter {
|
||||||
user_id: user_id,
|
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;
|
background-color: lightgray;
|
||||||
margin: 0;
|
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>
|
</style>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
|
<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) }}
|
{{ macros::attribute(name="Composure", value=sheet.composure) }}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -3,4 +3,11 @@
|
||||||
<label for="{{name}}">{{name}}:</label>
|
<label for="{{name}}">{{name}}:</label>
|
||||||
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
|
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
|
||||||
</div>
|
</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>System: {{data_type}}</h3>
|
||||||
<p>Strength: {{sheet.base.strength}}</p>
|
<p>Strength: {{sheet.base.strength}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a>
|
||||||
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
Loading…
Reference in New Issue