Start of adding protobuf-based editable character sheet.
This commit is contained in:
parent
b95bad440b
commit
c0a48245b1
5
build.rs
5
build.rs
|
@ -3,6 +3,9 @@ fn main() {
|
||||||
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
|
||||||
.compile_protos(&["proto/cofd.proto"], &["src/", "proto/"])
|
.compile_protos(
|
||||||
|
&["proto/cofd.proto", "proto/cofd_api.proto"],
|
||||||
|
&["src/", "proto/"],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
import "cofd.proto";
|
||||||
|
|
||||||
|
package models.proto.cofd.api;
|
||||||
|
|
||||||
|
//Update basic information about a Chronicles of Darkness (or
|
||||||
|
//derivative system) character sheet. This is a straight overwrite of
|
||||||
|
//all basic information on the sheet.
|
||||||
|
message BasicInfo {
|
||||||
|
string name = 1;
|
||||||
|
string gender = 2;
|
||||||
|
string concept = 3;
|
||||||
|
string chronicle = 4;
|
||||||
|
int32 age = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update all attributes in a Chronicles of Darkness character (or
|
||||||
|
//derivative system) character sheet. This is a straight overwrite of
|
||||||
|
//all basic information on the sheet.
|
||||||
|
message Attributes {
|
||||||
|
int32 strength = 1;
|
||||||
|
int32 dexterity = 2;
|
||||||
|
int32 stamina = 3;
|
||||||
|
|
||||||
|
int32 intelligence = 4;
|
||||||
|
int32 wits = 5;
|
||||||
|
int32 resolve = 6;
|
||||||
|
|
||||||
|
int32 presence = 7;
|
||||||
|
int32 manipulation = 8;
|
||||||
|
int32 composure = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Attribute {
|
||||||
|
string name = 1;
|
||||||
|
int32 value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update skill entries in a Chronicles of Darkness character sheet.
|
||||||
|
//This is a straight overwrite of all skills in the sheet.
|
||||||
|
message Skills {
|
||||||
|
repeated CofdSheet.Skill physical_skills = 1;
|
||||||
|
repeated CofdSheet.Skill mental_skills = 2;
|
||||||
|
repeated CofdSheet.Skill social_skills = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add a Condition to a Chronicles of Darkness character sheet.
|
||||||
|
message Condition {
|
||||||
|
string name = 1;
|
||||||
|
}
|
|
@ -25,6 +25,9 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("deserialization error: {0}")]
|
#[error("deserialization error: {0}")]
|
||||||
DeserializationError(#[from] prost::DecodeError),
|
DeserializationError(#[from] prost::DecodeError),
|
||||||
|
|
||||||
|
#[error("i/o error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
|
|
@ -31,6 +31,7 @@ async fn main() -> Result<(), rocket::error::Error> {
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let api_routes = routes::api::routes();
|
||||||
let character_routes = routes::characters::routes();
|
let character_routes = routes::characters::routes();
|
||||||
let catchers = catchers::catchers();
|
let catchers = catchers::catchers();
|
||||||
|
|
||||||
|
@ -39,6 +40,11 @@ async fn main() -> Result<(), rocket::error::Error> {
|
||||||
.attach(db::TenebrousDbConn::fairing())
|
.attach(db::TenebrousDbConn::fairing())
|
||||||
.mount("/", root_routes)
|
.mount("/", root_routes)
|
||||||
.mount("/characters", character_routes)
|
.mount("/characters", character_routes)
|
||||||
|
.mount("/api", api_routes)
|
||||||
|
.mount(
|
||||||
|
"/scripts",
|
||||||
|
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static/scripts")),
|
||||||
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/protos",
|
"/protos",
|
||||||
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
|
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
|
use crate::errors::Error;
|
||||||
|
use rocket::data::{Data, FromData, Outcome, ToByteUnit};
|
||||||
|
use rocket::request::Request;
|
||||||
|
use std::default::Default;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
/// Contains the generated Chronicles of Darkness-related protocol
|
/// Contains the generated Chronicles of Darkness-related protocol
|
||||||
/// buffer types.
|
/// buffer types.
|
||||||
pub mod cofd {
|
pub mod cofd {
|
||||||
include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs"));
|
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 {
|
pub(crate) trait DerivedStats {
|
||||||
fn speed(&self) -> i32;
|
fn speed(&self) -> i32;
|
||||||
}
|
}
|
||||||
|
@ -13,3 +25,46 @@ pub mod cofd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A struct wrapping a protobuf that allows it to be used as binary
|
||||||
|
/// data submitted via POST using fetch API. Can automatically be
|
||||||
|
/// dereferenced into its wrapped type.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Proto<T>(T)
|
||||||
|
where
|
||||||
|
T: prost::Message + Default;
|
||||||
|
|
||||||
|
/// Converts the body of a POST request containing encoded protobuf
|
||||||
|
/// data into the wrapped type.
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<T> FromData for Proto<T>
|
||||||
|
where
|
||||||
|
T: prost::Message + Default,
|
||||||
|
{
|
||||||
|
type Error = crate::errors::Error;
|
||||||
|
|
||||||
|
async fn from_data(_req: &Request<'_>, data: Data) -> Outcome<Self, Error> {
|
||||||
|
use rocket::http::Status;
|
||||||
|
|
||||||
|
let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await {
|
||||||
|
Ok(read_bytes) => read_bytes,
|
||||||
|
Err(e) => return Outcome::Failure((Status::new(422, "invalid protobuf"), e.into())),
|
||||||
|
};
|
||||||
|
|
||||||
|
match T::decode(bytes.as_ref()) {
|
||||||
|
Ok(decoded) => Outcome::Success(Proto(decoded)),
|
||||||
|
Err(e) => Outcome::Failure((Status::new(422, "invalid protobuf"), e.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for Proto<T>
|
||||||
|
where
|
||||||
|
T: prost::Message + Default,
|
||||||
|
{
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod api;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod characters;
|
pub mod characters;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
use crate::db::{Dao, TenebrousDbConn};
|
||||||
|
use crate::errors::Error;
|
||||||
|
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
||||||
|
use crate::models::proto::{cofd::*, Proto};
|
||||||
|
use crate::models::users::User;
|
||||||
|
use rocket_contrib::templates::Template;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||||
|
routes![
|
||||||
|
cofd::update_basic_info,
|
||||||
|
cofd::update_attributes,
|
||||||
|
cofd::update_attribute,
|
||||||
|
cofd::update_skills,
|
||||||
|
cofd::add_condition,
|
||||||
|
cofd::remove_condition
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Protobuf-based REST endpoints for editing a character.
|
||||||
|
mod cofd {
|
||||||
|
use super::*;
|
||||||
|
use crate::models::proto::{cofd::api::*, cofd::*, Proto};
|
||||||
|
|
||||||
|
#[post("/cofd/<owner>/<character_id>/basic-info", data = "<info>")]
|
||||||
|
pub(super) fn update_basic_info<'a>(
|
||||||
|
owner: String,
|
||||||
|
character_id: i32,
|
||||||
|
info: Proto<BasicInfo>,
|
||||||
|
) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/cofd/<owner>/<character_id>/attributes", data = "<info>")]
|
||||||
|
pub(super) fn update_attributes<'a>(
|
||||||
|
owner: String,
|
||||||
|
character_id: i32,
|
||||||
|
info: Proto<Attributes>,
|
||||||
|
) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/cofd/<owner>/<character_id>/attribute/<attribute>", data = "<info>")]
|
||||||
|
pub(super) fn update_attribute<'a>(
|
||||||
|
owner: String,
|
||||||
|
character_id: i32,
|
||||||
|
attribute: String,
|
||||||
|
info: Proto<Attribute>,
|
||||||
|
) -> &'a str {
|
||||||
|
println!("incoming request is {:#?}", info);
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/cofd/<owner>/<character_id>/skills", data = "<info>")]
|
||||||
|
pub(super) fn update_skills<'a>(
|
||||||
|
owner: String,
|
||||||
|
character_id: i32,
|
||||||
|
info: Proto<Skills>,
|
||||||
|
) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
|
||||||
|
pub(super) fn add_condition<'a>(
|
||||||
|
owner: String,
|
||||||
|
character_id: i32,
|
||||||
|
info: Proto<Condition>,
|
||||||
|
) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
|
||||||
|
pub(super) fn remove_condition<'a>(
|
||||||
|
owner: String,
|
||||||
|
character_id: i32,
|
||||||
|
info: Proto<Condition>,
|
||||||
|
) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ use crate::errors::Error;
|
||||||
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
||||||
use crate::models::users::User;
|
use crate::models::users::User;
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
|
mod edit;
|
||||||
mod new;
|
mod new;
|
||||||
|
|
||||||
pub(crate) fn routes() -> Vec<rocket::Route> {
|
pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||||
|
@ -13,7 +13,7 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||||
new::new_character_page,
|
new::new_character_page,
|
||||||
new::new_character_submit,
|
new::new_character_submit,
|
||||||
new::new_character_not_logged_in,
|
new::new_character_not_logged_in,
|
||||||
edit_character
|
edit::edit_character_page
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,28 +62,3 @@ async fn view_character(
|
||||||
let template = view_character_template(user, character)?;
|
let template = view_character_template(user, character)?;
|
||||||
Ok(template)
|
Ok(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<owner>/<character_id>/edit")]
|
|
||||||
async fn edit_character(
|
|
||||||
character_id: i32,
|
|
||||||
owner: String,
|
|
||||||
logged_in_user: Option<&User>,
|
|
||||||
conn: TenebrousDbConn,
|
|
||||||
) -> Result<Template, Error> {
|
|
||||||
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
|
|
||||||
|
|
||||||
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
|
||||||
let character = conn
|
|
||||||
.load_character(character_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
|
|
||||||
if logged_in_user != &owner {
|
|
||||||
return Err(Error::NoPermission);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut context = HashMap::new();
|
|
||||||
context.insert("name", character.character_name);
|
|
||||||
context.insert("username", owner.username);
|
|
||||||
Ok(Template::render("view_character", context))
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
use crate::db::{Dao, TenebrousDbConn};
|
||||||
|
use crate::errors::Error;
|
||||||
|
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
||||||
|
use crate::models::users::User;
|
||||||
|
use rocket_contrib::templates::Template;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct EditCharacterContext<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub username: &'a str,
|
||||||
|
pub data_type: &'a CharacterDataType,
|
||||||
|
pub sheet: Box<DynCharacterData>,
|
||||||
|
pub state: FormStateContext<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct FormStateContext<'a> {
|
||||||
|
pub selected_system: &'a CharacterDataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<owner>/<character_id>/edit")]
|
||||||
|
pub(super) async fn edit_character_page(
|
||||||
|
character_id: i32,
|
||||||
|
owner: String,
|
||||||
|
logged_in_user: Option<&User>,
|
||||||
|
conn: TenebrousDbConn,
|
||||||
|
) -> Result<Template, Error> {
|
||||||
|
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
||||||
|
let character = conn
|
||||||
|
.load_character(character_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
if logged_in_user != &owner {
|
||||||
|
return Err(Error::NoPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = EditCharacterContext {
|
||||||
|
name: &character.character_name,
|
||||||
|
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))
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ use rocket_contrib::templates::Template;
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![index, user_index]
|
routes![index, user_index, proto_test]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information to display to the user on their home page.
|
/// Information to display to the user on their home page.
|
||||||
|
@ -39,3 +39,12 @@ async fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Erro
|
||||||
fn index() -> Redirect {
|
fn index() -> Redirect {
|
||||||
super::common::redirect_to_login()
|
super::common::redirect_to_login()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::models::proto::{cofd::*, Proto};
|
||||||
|
|
||||||
|
#[post("/proto-test", data = "<buf>")]
|
||||||
|
async fn proto_test<'a>(buf: Proto<CofdSheet>) -> &'a str {
|
||||||
|
println!("buf is {:#?}", buf);
|
||||||
|
println!("str is: {}", buf.strength);
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
function makeAPI(root) {
|
||||||
|
const Attribute = root.lookupType("models.proto.cofd.api.Attribute");
|
||||||
|
|
||||||
|
const attributeResource = (username, characterID, attribute) =>
|
||||||
|
'/api/cofd/' + username + '/' + characterID + '/attribute/' + attribute;
|
||||||
|
|
||||||
|
async function updateAttribute(params) {
|
||||||
|
const { username, characterID, attribute, newValue } = params;
|
||||||
|
|
||||||
|
let req = Attribute.create({
|
||||||
|
name: attribute,
|
||||||
|
value: parseInt(newValue)
|
||||||
|
});
|
||||||
|
|
||||||
|
const resource = attributeResource(username, characterID, attribute);
|
||||||
|
|
||||||
|
let resp = await fetch(resource, {
|
||||||
|
method: 'POST',
|
||||||
|
body: Attribute.encode(req).finish()
|
||||||
|
}).then(async resp => {
|
||||||
|
console.log("resp is", await resp.text());
|
||||||
|
}).catch(async err => {
|
||||||
|
console.log("err is", err.text());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAttribute
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
(async () => {
|
||||||
|
//TODO start refactoring these into a separate script, and make API calls
|
||||||
|
//take all necessary info (e.g. username and character ID, plus other stuff)
|
||||||
|
//as object params.
|
||||||
|
const root = await protobuf.load("/protos/cofd_api.proto");
|
||||||
|
|
||||||
|
const [, , USERNAME, CHARACTER_ID] = window.location.pathname.split('/');
|
||||||
|
|
||||||
|
const api = makeAPI(root);
|
||||||
|
console.log("api is", api);
|
||||||
|
|
||||||
|
function setupAttributes() {
|
||||||
|
const attributeInputs = document.querySelectorAll('#attributes input[type="number"]');
|
||||||
|
|
||||||
|
Array.from(attributeInputs).forEach(input => {
|
||||||
|
input.addEventListener('change', async function(event) {
|
||||||
|
console.log("updating attr");
|
||||||
|
const attribute = event.target.id;
|
||||||
|
const newValue = parseInt(event.target.value);
|
||||||
|
const params = { username: USERNAME, characterID: CHARACTER_ID, attribute, newValue };
|
||||||
|
await api.updateAttribute(params);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAttributes();
|
||||||
|
})().catch(e => {
|
||||||
|
alert(e);
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', event => {
|
||||||
|
protobuf.load("/protos/cofd.proto").then(function(root) {
|
||||||
|
console.log("root is", root);
|
||||||
|
let CofdSheet = root.lookupType("models.proto.cofd.CofdSheet");
|
||||||
|
let sheet = CofdSheet.fromObject({ name: 'lol', strength: 100 });
|
||||||
|
let buffer = CofdSheet.encode(sheet).finish();
|
||||||
|
|
||||||
|
fetch('/proto-test', {
|
||||||
|
method: 'POST',
|
||||||
|
body: buffer
|
||||||
|
}).then(async resp => {
|
||||||
|
console.log("resp is", await resp.text());
|
||||||
|
}).catch(async err => {
|
||||||
|
console.log("err is", err.text());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
{% 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;
|
||||||
|
}
|
||||||
|
</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.intelligence) }}
|
||||||
|
{{ macros::attribute(name="Wits", value=sheet.wits) }}
|
||||||
|
{{ macros::attribute(name="Resolve", value=sheet.resolve) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attributes-section" id="physicalAttributes">
|
||||||
|
{{ macros::attribute(name="Strength", value=sheet.strength) }}
|
||||||
|
{{ macros::attribute(name="Dexterity", value=sheet.dexterity) }}
|
||||||
|
{{ macros::attribute(name="Stamina", value=sheet.stamina) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attributes-section" id="socicalAttributes">
|
||||||
|
{{ macros::attribute(name="Presence", value=sheet.presence) }}
|
||||||
|
{{ macros::attribute(name="Manipulation", value=sheet.manipulation) }}
|
||||||
|
{{ macros::attribute(name="Composure", value=sheet.composure) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% macro attribute(name, value) %}
|
||||||
|
<div class="attribute">
|
||||||
|
<label for="{{name}}">{{name}}:</label>
|
||||||
|
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
|
||||||
|
</div>
|
||||||
|
{% endmacro input %}
|
|
@ -1,6 +1,8 @@
|
||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/scripts/characters/new-character.js"></script>
|
||||||
<div>
|
<div>
|
||||||
New character page.
|
New character page.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue