Compare commits
No commits in common. "edit-character" and "master" have entirely different histories.
edit-chara
...
master
3
.env
3
.env
|
@ -1 +1,2 @@
|
||||||
DATABASE_URL="./tenebrous.sqlite"
|
DATABASE_URL="sqlite://tenebrous.sqlite"
|
||||||
|
SQLX_OFFLINE="true"
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
/target
|
/target
|
||||||
todo.org
|
todo.org
|
||||||
*.sqlite
|
*.sqlite*
|
||||||
|
*.sqlite.*
|
||||||
|
node_modules
|
||||||
|
static/scripts/dist
|
||||||
|
static/templates/*
|
||||||
|
generated/
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
|
@ -4,32 +4,50 @@ version = "0.1.0"
|
||||||
authors = ["jeff <jeff@agnos.is>"]
|
authors = ["jeff <jeff@agnos.is>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
default-run = "tenebrous"
|
||||||
|
|
||||||
|
[package.metadata.scripts]
|
||||||
|
grpc-proxy = 'docker run -d --rm -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml:ro --network=host envoyproxy/envoy:v1.16.1'
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "tenebrous-migrate"
|
||||||
|
path = "src/migrate.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "tenebrous"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.6"
|
prost-build = "0.6"
|
||||||
|
tonic-build = "0.3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tonic = "0.3"
|
||||||
prost = "0.6"
|
prost = "0.6"
|
||||||
serde = "1.0"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_derive = "1.0"
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
erased-serde = "0.3"
|
erased-serde = "0.3"
|
||||||
diesel = "1.4"
|
|
||||||
diesel-derive-enum = { version = "1", features = ["sqlite"] }
|
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
rust-argon2 = "0.8"
|
rust-argon2 = "0.8"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
tokio = { version = "1.0", features = ["macros"] }
|
||||||
strum = { version = "0.20", features = ["derive"] }
|
strum = { version = "0.20", features = ["derive"] }
|
||||||
|
refinery = { version = "0.5", features = ["rusqlite"]}
|
||||||
|
barrel = { version = "0.6", features = ["sqlite3"] }
|
||||||
|
|
||||||
|
[dependencies.sqlx]
|
||||||
|
version = "0.5"
|
||||||
|
features = [ "offline", "sqlite", "runtime-tokio-native-tls" ]
|
||||||
|
|
||||||
[dependencies.rocket]
|
[dependencies.rocket]
|
||||||
git = "https://github.com/SergioBenitez/Rocket"
|
git = "https://github.com/SergioBenitez/Rocket"
|
||||||
branch = "master"
|
branch = "master"
|
||||||
features = ["secrets"]
|
features = ["secrets", "tls"]
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
git = "https://github.com/SergioBenitez/Rocket"
|
git = "https://github.com/SergioBenitez/Rocket"
|
||||||
branch = "master"
|
branch = "master"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = [ "tera_templates", "diesel_sqlite_pool", "serve" ]
|
features = [ "tera_templates", "serve" ]
|
||||||
|
|
103
README.md
103
README.md
|
@ -4,48 +4,101 @@ An open source character sheet service for tabletop roleplaying games.
|
||||||
|
|
||||||
Currently under heavy development.
|
Currently under heavy development.
|
||||||
|
|
||||||
|
## The Stack
|
||||||
|
|
||||||
|
This project makes use of these technologies:
|
||||||
|
|
||||||
|
- Rust
|
||||||
|
- Rocket Web Framework
|
||||||
|
- Tonic gRPC Framework
|
||||||
|
- Typescript
|
||||||
|
|
||||||
|
Building is backed by: cargo, npm, and webpack.
|
||||||
|
|
||||||
## Build Instructions
|
## Build Instructions
|
||||||
|
|
||||||
These are very basic build instructions. They assume you already have
|
These are very basic build instructions. They assume you already have
|
||||||
cargo set up and installed. Building the application requires **Rust
|
cargo set up and installed.
|
||||||
Nightly!** See [rustup documentation][rustup] for more, particularly
|
|
||||||
the part about using [Rust Nightly][nightly].
|
|
||||||
|
|
||||||
### Install Dependencies
|
|
||||||
|
|
||||||
Install dependencies. The exact method depends on your OS.
|
|
||||||
|
|
||||||
* sqlite3 and development headers (Void Linux: `xbps-install
|
|
||||||
sqlite sqlite-devel`, Ubuntu: `apt install sqlite3 libsqlite3-dev`).
|
|
||||||
* protoc: protocol buffers compiler. There is one baked into the
|
|
||||||
build, so you should not need this unless you are not using
|
|
||||||
Linux/Mac/Windows.
|
|
||||||
|
|
||||||
### Initial Setup
|
### Initial Setup
|
||||||
|
|
||||||
Follow these instructions from the root of the repository. Set up database:
|
Quick initial setup instructions that will set up a development
|
||||||
|
environment.
|
||||||
|
|
||||||
|
First, install dependencies by either using the command below (Void
|
||||||
|
Linux), or reading the "Dependencies Required" section:
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install diesel_cli --no-default-features --features sqlite
|
xbps-install sqlite sqlite-devel protobuf nodejs docker
|
||||||
diesel setup
|
|
||||||
diesel migration run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then run the required `cargo` commands to install management tools and
|
||||||
|
create a development database:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
cargo install --version=0.2.0 sqlx-cli
|
||||||
|
cargo install cargo-run-script
|
||||||
|
cargo run --bin tenebrous-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies Required
|
||||||
|
|
||||||
|
Dependencies required to build the project. The exact installation
|
||||||
|
method depends on your OS.
|
||||||
|
|
||||||
|
* sqlite3 and development headers (Void Linux: `xbps-install
|
||||||
|
sqlite sqlite-devel`, Ubuntu: `apt install sqlite3 libsqlite3-dev`).
|
||||||
|
* protoc: protocol buffers compiler. Needed to compile protobuf files
|
||||||
|
for both the server and web frontends (Void Linux: `xbps-install
|
||||||
|
protobuf`).
|
||||||
|
* Node and npm: Needed to run webpack for compiling and injecting
|
||||||
|
Typescript into various web pages (Void Linux: `xbps-install
|
||||||
|
nodejs`).
|
||||||
|
* Docker: Needed to run the grpc proxy (Void Linux `xbps-install
|
||||||
|
docker`).
|
||||||
|
|
||||||
### Run Application
|
### Run Application
|
||||||
|
|
||||||
If you are using `rustup`, then it should automatically switch to the
|
|
||||||
nightly version of Rust in this repository. This is because of the
|
|
||||||
`rust-toolchain` file.
|
|
||||||
|
|
||||||
Command line "instructions" to build and run the application:
|
Command line "instructions" to build and run the application:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
cargo run-script grpc-proxy # only required if proxy not already running
|
||||||
cargo run
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
The sqlite database is currently always created in the same directory
|
The sqlite database is created in the directory `cargo run` was
|
||||||
that `cargo run` was invoked from, so make sure you invoke it from the
|
invoked from by default. You can also pass a path to a different
|
||||||
same place every time.
|
location as a single argument to the program.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Development instructions.
|
||||||
|
|
||||||
|
To set up a local database, or run migrations, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin tenebrous-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Queries and Migrations
|
||||||
|
|
||||||
|
When adding/updating a compile-checked query or a migration, you need
|
||||||
|
to update the SQLx data JSON file:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo sqlx prepare -- --bin tenebrous
|
||||||
|
```
|
||||||
|
|
||||||
|
### gRPC-Web Proxy
|
||||||
|
|
||||||
|
The frontend web application makes use of the gRPC-Web protocol to
|
||||||
|
call gPRC endpoints from the browser. This requires a proxy to
|
||||||
|
translate the calls from the browser to HTTP2 calls gRPC understands.
|
||||||
|
For development, executing the `cargo run-script grpc-proxy` command
|
||||||
|
will start the envoy proxy recommended by Google's gRPC-Web project.
|
||||||
|
|
||||||
|
The envoy configuration assumes you are on Linux. If you are using Mac
|
||||||
|
OS or Windows, see the note in the envoy configuration.
|
||||||
|
|
||||||
[rustup]: https://rust-lang.github.io/rustup/index.html
|
[rustup]: https://rust-lang.github.io/rustup/index.html
|
||||||
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
[default]
|
||||||
|
template_dir = "generated/templates/"
|
||||||
|
|
||||||
[development]
|
[development]
|
||||||
address = "localhost"
|
address = "localhost"
|
||||||
port = 8000
|
port = 8000
|
||||||
|
|
47
build.rs
47
build.rs
|
@ -1,9 +1,50 @@
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn js_protos() {
|
||||||
|
let output = Command::new("npm")
|
||||||
|
.arg("run")
|
||||||
|
.arg("build:protobuf")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
let err = String::from_utf8(output.stderr).unwrap();
|
||||||
|
panic!("JS Protobuf build failure:\n {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn webpack() {
|
||||||
|
let output = Command::new("npm")
|
||||||
|
.arg("run")
|
||||||
|
.arg("build:webpack")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
let err = String::from_utf8(output.stderr).unwrap();
|
||||||
|
panic!("Webpack build failure:\n {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=static/scripts/webpack.config.js");
|
||||||
|
js_protos();
|
||||||
|
webpack();
|
||||||
|
|
||||||
let mut config = prost_build::Config::new();
|
let mut config = prost_build::Config::new();
|
||||||
config.type_attribute(".", "#[derive(Serialize)]");
|
config.btree_map(&["."]);
|
||||||
|
config.type_attribute(".", "#[derive(::serde::Serialize)]");
|
||||||
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
|
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
|
||||||
config
|
|
||||||
.compile_protos(
|
tonic_build::configure()
|
||||||
|
.build_server(false)
|
||||||
|
.build_client(false)
|
||||||
|
.compile_with_config(
|
||||||
|
config,
|
||||||
&["proto/cofd.proto", "proto/cofd_api.proto"],
|
&["proto/cofd.proto", "proto/cofd_api.proto"],
|
||||||
&["src/", "proto/"],
|
&["src/", "proto/"],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
admin:
|
||||||
|
access_log_path: /tmp/admin_access.log
|
||||||
|
address:
|
||||||
|
socket_address: { address: 0.0.0.0, port_value: 9901 }
|
||||||
|
|
||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
- name: listener_0
|
||||||
|
address:
|
||||||
|
socket_address: { address: 0.0.0.0, port_value: 8080 }
|
||||||
|
filter_chains:
|
||||||
|
- filters:
|
||||||
|
- name: envoy.filters.network.http_connection_manager
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
|
||||||
|
codec_type: auto
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: local_service
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
- match: { prefix: "/" }
|
||||||
|
route:
|
||||||
|
cluster: greeter_service
|
||||||
|
max_grpc_timeout: 0s
|
||||||
|
cors:
|
||||||
|
allow_origin_string_match:
|
||||||
|
- prefix: "*"
|
||||||
|
allow_methods: GET, PUT, DELETE, POST, OPTIONS
|
||||||
|
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
|
||||||
|
max_age: "1728000"
|
||||||
|
expose_headers: custom-header-1,grpc-status,grpc-message
|
||||||
|
http_filters:
|
||||||
|
- name: envoy.filters.http.grpc_web
|
||||||
|
- name: envoy.filters.http.cors
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
clusters:
|
||||||
|
- name: greeter_service
|
||||||
|
connect_timeout: 0.25s
|
||||||
|
type: logical_dns
|
||||||
|
http2_protocol_options: {}
|
||||||
|
lb_policy: round_robin
|
||||||
|
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: cluster_0
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: localhost
|
||||||
|
port_value: 9090
|
|
@ -0,0 +1,3 @@
|
||||||
|
FROM envoyproxy/envoy:v1.16-latest
|
||||||
|
COPY ./envoy.yaml /etc/envoy/envoy.yaml
|
||||||
|
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE characters;
|
|
|
@ -1,9 +0,0 @@
|
||||||
CREATE TABLE characters(
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
viewable BOOLEAN NOT NULL,
|
|
||||||
character_name TEXT NOT NULL,
|
|
||||||
data_type TEXT CHECK(data_type IN ('chronicles_of_darkness_v1', 'changeling_v1')) NOT NULL,
|
|
||||||
data_version INTEGER NOT NULL,
|
|
||||||
data BLOB NOT NULL
|
|
||||||
);
|
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE users;
|
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE TABLE users (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
password TEXT NOT NULL
|
|
||||||
);
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "tenebrous-sheets",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "An open source character sheet service for tabletop roleplaying games.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"webpack-dev": "webpack --watch",
|
||||||
|
"build:protobuf": "mkdir -p src/frontend/_proto && protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts -I ./proto --js_out=import_style=commonjs,binary:./src/frontend/_proto --ts_out=service=grpc-web:./src/frontend/_proto ./proto/*.proto",
|
||||||
|
"build:webpack": "webpack"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@git.agnos.is:22022/projectmoon/tenebrous-sheets.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "AGPL",
|
||||||
|
"dependencies": {
|
||||||
|
"@improbable-eng/grpc-web": "^0.13.0",
|
||||||
|
"google-protobuf": "^3.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/google-protobuf": "^3.7.4",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"compression-webpack-plugin": "^7.1.2",
|
||||||
|
"html-webpack-plugin": "^4.5.0",
|
||||||
|
"ts-loader": "^8.0.13",
|
||||||
|
"ts-protoc-gen": "^0.14.0",
|
||||||
|
"typescript": "^4.1.3",
|
||||||
|
"webpack": "^5.11.1",
|
||||||
|
"webpack-cli": "^4.3.1",
|
||||||
|
"webpack-dev-server": "^3.11.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
@ -39,41 +57,47 @@ message CofdSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
string name = 1;
|
string name = 1;
|
||||||
string player = 2;
|
string gender = 2;
|
||||||
string campaign = 3;
|
string concept = 3;
|
||||||
string description = 4;
|
int32 age = 4;
|
||||||
|
|
||||||
int32 strength = 6;
|
string player = 5;
|
||||||
int32 dexterity = 7;
|
string chronicle = 6;
|
||||||
int32 stamina = 8;
|
string description = 7;
|
||||||
|
|
||||||
int32 intelligence = 9;
|
int32 strength = 8;
|
||||||
int32 wits = 10;
|
int32 dexterity = 9;
|
||||||
int32 resolve = 11;
|
int32 stamina = 10;
|
||||||
|
|
||||||
int32 presence = 12;
|
int32 intelligence = 11;
|
||||||
int32 manipulation = 13;
|
int32 wits = 12;
|
||||||
int32 composure = 14;
|
int32 resolve = 13;
|
||||||
|
|
||||||
map<string, Skill> physical_skills = 16;
|
int32 presence = 14;
|
||||||
map<string, Skill> mental_skills = 17;
|
int32 manipulation = 15;
|
||||||
map<string, Skill> social_skills = 18;
|
int32 composure = 16;
|
||||||
|
|
||||||
repeated Merit merits = 15;
|
map<string, Skill> physical_skills = 17;
|
||||||
repeated Condition conditions = 19;
|
map<string, Skill> mental_skills = 18;
|
||||||
|
map<string, Skill> social_skills = 19;
|
||||||
|
|
||||||
int32 size = 20;
|
repeated Merit merits = 20;
|
||||||
int32 health = 21;
|
repeated Condition conditions = 21;
|
||||||
int32 willpower = 22;
|
|
||||||
int32 experience_points = 23;
|
|
||||||
int32 beats = 24;
|
|
||||||
|
|
||||||
repeated Item items = 25;
|
int32 size = 22;
|
||||||
repeated Attack attacks = 26;
|
int32 health = 23;
|
||||||
|
int32 willpower = 24;
|
||||||
|
int32 experience_points = 25;
|
||||||
|
int32 beats = 26;
|
||||||
|
|
||||||
map<string, string> other_data = 27;
|
repeated Item items = 27;
|
||||||
}
|
repeated Attack attacks = 28;
|
||||||
|
|
||||||
message ChangelingSheet {
|
map<string, string> other_data = 29;
|
||||||
CofdSheet base = 1;
|
|
||||||
|
oneof system_fields {
|
||||||
|
CoreFields core = 30;
|
||||||
|
MageFields mage = 31;
|
||||||
|
ChangelingFields changeling = 32;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,48 +3,84 @@ import "cofd.proto";
|
||||||
|
|
||||||
package models.proto.cofd.api;
|
package models.proto.cofd.api;
|
||||||
|
|
||||||
|
message CharacterIdentifier {
|
||||||
|
string owner = 1;
|
||||||
|
int32 id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
//Update basic information about a Chronicles of Darkness (or
|
//Update basic information about a Chronicles of Darkness (or
|
||||||
//derivative system) character sheet. This is a straight overwrite of
|
//derivative system) character sheet. This is a straight overwrite of
|
||||||
//all basic information on the sheet.
|
//all basic information on the sheet.
|
||||||
message BasicInfo {
|
message UpdateBasicInfoRequest {
|
||||||
|
CharacterIdentifier character = 1;
|
||||||
|
reserved 2;
|
||||||
|
// string owner = 1;
|
||||||
|
// int32 character_id = 2;
|
||||||
|
|
||||||
|
string name = 3;
|
||||||
|
string gender = 4;
|
||||||
|
string concept = 5;
|
||||||
|
string chronicle = 6;
|
||||||
|
int32 age = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generic "did something succeed or not" response.
|
||||||
|
message ApiResult {
|
||||||
|
bool success = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update an attribute's dot amount. TODO rename to AttributesUpdate.
|
||||||
|
message UpdateAttributeRequest {
|
||||||
|
CharacterIdentifier character = 1;
|
||||||
|
string attribute_name = 2;
|
||||||
|
int32 attribute_value = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Full update of a single skill
|
||||||
|
message UpdateSkillRequest {
|
||||||
|
CharacterIdentifier character = 1;
|
||||||
|
CofdSheet.Skill skill = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Partial update of a single skill dot amount.
|
||||||
|
message UpdateSkillValueRequest {
|
||||||
|
CharacterIdentifier character = 1;
|
||||||
|
string skill_name = 2;
|
||||||
|
int32 skill_value = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Partial update of only a skill's specializations. The
|
||||||
|
//specializations will be overwritten with the new values.
|
||||||
|
message UpdateSkillSpecializationsRequest {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
string gender = 2;
|
repeated string specializations = 2;
|
||||||
string concept = 3;
|
|
||||||
string chronicle = 4;
|
|
||||||
int32 age = 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Update all attributes in a Chronicles of Darkness character (or
|
//Update all merits on the character sheet by overwriting them.
|
||||||
//derivative system) character sheet. This is a straight overwrite of
|
//Primarily for the web UI.
|
||||||
//all basic information on the sheet.
|
message UpdateMeritsRequest {
|
||||||
message Attributes {
|
CharacterIdentifier character = 1;
|
||||||
int32 strength = 1;
|
repeated CofdSheet.Merit merits = 2;
|
||||||
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 {
|
//Update all items on the character sheet by overwriting them.
|
||||||
string name = 1;
|
//Primarily for the web UI.
|
||||||
int32 value = 2;
|
message UpdateItemsRequest {
|
||||||
}
|
CharacterIdentifier character = 1;
|
||||||
|
repeated CofdSheet.Item items = 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.
|
//Add a Condition to a Chronicles of Darkness character sheet.
|
||||||
message Condition {
|
message AddConditionRequest {
|
||||||
string name = 1;
|
string character_username = 1;
|
||||||
|
int32 character_id = 2;
|
||||||
|
string condition_name = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove a Condition from a Chronicles of Darkness character sheet.
|
||||||
|
message RemoveConditionRequest {
|
||||||
|
string character_username = 1;
|
||||||
|
int32 character_id = 2;
|
||||||
|
string condition_name = 3;
|
||||||
}
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
{
|
||||||
|
"db": "SQLite",
|
||||||
|
"492e1e087edc6eff4004033227e3f3510f1165c3ec7626e89b695c760bc113d2": {
|
||||||
|
"query": "SELECT id as \"id: _\",\n user_id as \"user_id: _\",\n data_type as \"data_type: _\",\n data_version as \"data_version: _\",\n viewable, character_name\n FROM characters WHERE user_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id: _",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_id: _",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data_type: _",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data_version: _",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "viewable",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "character_name",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7a55ee917cf1ec732b10b2844dd5a3d0089c177b0eb183932fd73e20387a1610": {
|
||||||
|
"query": "SELECT id as \"id: _\", username, password FROM users WHERE username = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id: _",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d7d1fc9ceff3b7659c8f04fd5b574104d1866cb1ea67d9534b6f3f4064699fb6": {
|
||||||
|
"query": "SELECT id as \"id: _\", username, password FROM users WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id: _",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e3c4b224ce8ce70a4d7709af7abe82a6f53bff834460fa7d32bb0b6daea94565": {
|
||||||
|
"query": "SELECT id as \"id: _\",\n user_id as \"user_id: _\",\n viewable, character_name, data,\n data_type as \"data_type: _\",\n data_version as \"data_version: _\"\n FROM characters WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id: _",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_id: _",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "viewable",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "character_name",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Blob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data_type: _",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data_version: _",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Int64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
202
src/db.rs
202
src/db.rs
|
@ -1,119 +1,155 @@
|
||||||
use crate::models::characters::{Character, NewCharacter, StrippedCharacter};
|
use crate::models::characters::{Character, NewCharacter, StrippedCharacter};
|
||||||
use crate::models::users::{NewUser, User};
|
use crate::models::users::{NewUser, User};
|
||||||
use crate::schema::characters;
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||||
use diesel::prelude::*;
|
use sqlx::ConnectOptions;
|
||||||
use rocket_contrib::databases::diesel;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[database("tenebrous_db")]
|
/// Type alias for the Rocket-managed singleton database connection.
|
||||||
pub(crate) struct TenebrousDbConn(SqliteConnection);
|
pub type TenebrousDbConn<'a> = rocket::State<'a, SqlitePool>;
|
||||||
|
|
||||||
|
/// Create a connection pool to the database.
|
||||||
|
pub(crate) async fn create_pool(db_path: &str) -> Result<SqlitePool, crate::errors::Error> {
|
||||||
|
//Create database if missing.
|
||||||
|
let conn = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path))?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
//Return actual conncetion pool.
|
||||||
|
SqlitePoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(db_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
pub(crate) trait Dao {
|
pub(crate) trait Dao {
|
||||||
async fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
|
async fn load_user_by_id(&self, id: i32) -> sqlx::Result<Option<User>>;
|
||||||
|
|
||||||
async fn load_user(&self, for_username: String) -> QueryResult<Option<User>>;
|
async fn load_user(&self, for_username: &str) -> sqlx::Result<Option<User>>;
|
||||||
|
|
||||||
async fn insert_user(&self, new_user: NewUser) -> QueryResult<User>;
|
async fn insert_user(&self, new_user: NewUser<'_>) -> sqlx::Result<User>;
|
||||||
|
|
||||||
async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
|
async fn load_character_list(&self, for_user_id: i32) -> sqlx::Result<Vec<StrippedCharacter>>;
|
||||||
|
|
||||||
async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
|
async fn load_character(&self, character_id: i32) -> sqlx::Result<Option<Character>>;
|
||||||
|
|
||||||
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>;
|
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()>;
|
||||||
|
|
||||||
async fn update_character_sheet(&self, character: Character) -> QueryResult<()>;
|
async fn update_character<'a>(&self, character: &'a Character) -> sqlx::Result<()>;
|
||||||
|
|
||||||
|
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StrippedCharacterColumns = (
|
|
||||||
characters::id,
|
|
||||||
characters::user_id,
|
|
||||||
characters::viewable,
|
|
||||||
characters::character_name,
|
|
||||||
characters::data_type,
|
|
||||||
characters::data_version,
|
|
||||||
);
|
|
||||||
|
|
||||||
const STRIPPED_CHARACTER_COLUMNS: StrippedCharacterColumns = (
|
|
||||||
characters::id,
|
|
||||||
characters::user_id,
|
|
||||||
characters::viewable,
|
|
||||||
characters::character_name,
|
|
||||||
characters::data_type,
|
|
||||||
characters::data_version,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl Dao for TenebrousDbConn {
|
impl Dao for SqlitePool {
|
||||||
async fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
|
async fn load_user_by_id(&self, user_id: i32) -> sqlx::Result<Option<User>> {
|
||||||
use crate::schema::users::dsl::*;
|
sqlx::query_as!(
|
||||||
self.run(move |conn| users.filter(id.eq(user_id)).first(conn).optional())
|
User,
|
||||||
|
r#"SELECT id as "id: _", username, password FROM users WHERE id = ?"#,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_optional(self)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_user(&self, for_username: String) -> QueryResult<Option<User>> {
|
async fn load_user(&self, for_username: &str) -> sqlx::Result<Option<User>> {
|
||||||
use crate::schema::users::dsl::*;
|
sqlx::query_as!(
|
||||||
|
User,
|
||||||
self.run(move |conn| {
|
r#"SELECT id as "id: _", username, password FROM users WHERE username = ?"#,
|
||||||
users
|
for_username
|
||||||
.filter(username.eq(for_username))
|
)
|
||||||
.first(conn)
|
.fetch_optional(self)
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn insert_user(&self, new_user: NewUser) -> QueryResult<User> {
|
async fn insert_user(&self, new_user: NewUser<'_>) -> sqlx::Result<User> {
|
||||||
self.run(move |conn| {
|
sqlx::query("INSERT INTO users (username, password) values (?, ?)")
|
||||||
diesel::insert_into(users).values(&new_user).execute(conn)?;
|
.bind(new_user.username)
|
||||||
|
.bind(new_user.password)
|
||||||
|
.execute(self)
|
||||||
|
.await?;
|
||||||
|
|
||||||
use crate::schema::users::dsl::*;
|
self.load_user(new_user.username)
|
||||||
users.filter(username.eq(new_user.username)).first(conn)
|
.await
|
||||||
})
|
.and_then(|user| user.ok_or(sqlx::Error::RowNotFound))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_character_list(&self, for_user_id: i32) -> sqlx::Result<Vec<StrippedCharacter>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
StrippedCharacter,
|
||||||
|
r#"SELECT id as "id: _",
|
||||||
|
user_id as "user_id: _",
|
||||||
|
data_type as "data_type: _",
|
||||||
|
data_version as "data_version: _",
|
||||||
|
viewable, character_name
|
||||||
|
FROM characters WHERE user_id = ?"#,
|
||||||
|
for_user_id
|
||||||
|
)
|
||||||
|
.fetch_all(self)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
|
async fn load_character(&self, character_id: i32) -> sqlx::Result<Option<Character>> {
|
||||||
use crate::schema::characters::dsl::*;
|
sqlx::query_as!(
|
||||||
|
Character,
|
||||||
self.run(move |conn| {
|
r#"SELECT id as "id: _",
|
||||||
characters
|
user_id as "user_id: _",
|
||||||
.filter(user_id.eq(for_user_id))
|
viewable, character_name, data,
|
||||||
.select(STRIPPED_CHARACTER_COLUMNS)
|
data_type as "data_type: _",
|
||||||
.load(conn)
|
data_version as "data_version: _"
|
||||||
})
|
FROM characters WHERE id = ?"#,
|
||||||
|
character_id
|
||||||
|
)
|
||||||
|
.fetch_optional(self)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
|
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()> {
|
||||||
use crate::schema::characters::dsl::*;
|
sqlx::query(
|
||||||
|
"INSERT INTO characters
|
||||||
self.run(move |conn| {
|
(user_id, viewable, character_name, data_type, data_version, data)
|
||||||
characters
|
values (?, ?, ?, ?, ?, ?)",
|
||||||
.filter(id.eq(character_id))
|
)
|
||||||
.first(conn)
|
.bind(new_character.user_id)
|
||||||
.optional()
|
.bind(new_character.viewable)
|
||||||
})
|
.bind(new_character.character_name)
|
||||||
.await
|
.bind(new_character.data_type)
|
||||||
}
|
.bind(new_character.data_version)
|
||||||
|
.bind(new_character.data)
|
||||||
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> {
|
.execute(self)
|
||||||
self.run(|conn| {
|
|
||||||
diesel::insert_into(characters::table)
|
|
||||||
.values(new_character)
|
|
||||||
.execute(conn)
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_character_sheet(&self, character: Character) -> QueryResult<()> {
|
async fn update_character<'a>(&self, character: &'a Character) -> sqlx::Result<()> {
|
||||||
use crate::schema::characters::dsl::*;
|
sqlx::query(
|
||||||
self.run(move |conn| {
|
"UPDATE characters
|
||||||
diesel::update(&character)
|
set user_id = ?, viewable = ?, character_name = ?,
|
||||||
.set(data.eq(&character.data))
|
data_type = ?, data_version = ?, data = ? where id = ?",
|
||||||
.execute(conn)
|
)
|
||||||
})
|
.bind(character.user_id)
|
||||||
|
.bind(character.viewable)
|
||||||
|
.bind(&character.character_name)
|
||||||
|
.bind(character.data_type)
|
||||||
|
.bind(character.data_version)
|
||||||
|
.bind(&character.data)
|
||||||
|
.bind(character.id)
|
||||||
|
.execute(self)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()> {
|
||||||
|
sqlx::query("UPDATE characters set data = ? where id = ?")
|
||||||
|
.bind(&character.data)
|
||||||
|
.bind(character.id)
|
||||||
|
.execute(self)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -4,6 +4,7 @@ use rocket::response::status;
|
||||||
use rocket::response::{self, Responder};
|
use rocket::response::{self, Responder};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::convert::Into;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -20,8 +21,8 @@ pub enum Error {
|
||||||
#[error("invalid input")]
|
#[error("invalid input")]
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
|
|
||||||
#[error("query error: {0}")]
|
#[error("validation error: {0}")]
|
||||||
QueryError(#[from] diesel::result::Error),
|
ValidationError(#[from] crate::models::convert::ValidationError),
|
||||||
|
|
||||||
#[error("serialization error: {0}")]
|
#[error("serialization error: {0}")]
|
||||||
SerializationError(#[from] prost::EncodeError),
|
SerializationError(#[from] prost::EncodeError),
|
||||||
|
@ -31,6 +32,12 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("i/o error: {0}")]
|
#[error("i/o error: {0}")]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("query error: {0}")]
|
||||||
|
QueryError(#[from] sqlx::Error),
|
||||||
|
|
||||||
|
#[error("rocket error: {0}")]
|
||||||
|
RocketError(#[from] rocket::error::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
@ -42,6 +49,28 @@ impl Error {
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn message(&self) -> String {
|
||||||
|
if self.is_sensitive() {
|
||||||
|
"internal error".to_string()
|
||||||
|
} else {
|
||||||
|
self.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for tonic::Status {
|
||||||
|
fn from(err: Error) -> tonic::Status {
|
||||||
|
use tonic::{Code, Status};
|
||||||
|
use Error::*;
|
||||||
|
match err {
|
||||||
|
NotFound => Status::new(Code::NotFound, err.message()),
|
||||||
|
NotLoggedIn => Status::new(Code::Unauthenticated, err.message()),
|
||||||
|
NoPermission => Status::new(Code::PermissionDenied, err.message()),
|
||||||
|
InvalidInput => Status::new(Code::InvalidArgument, err.message()),
|
||||||
|
_ => Status::new(Code::Internal, err.message()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
// package: models.proto.cofd.api
|
||||||
|
// file: cofd_api.proto
|
||||||
|
|
||||||
|
import * as jspb from "google-protobuf";
|
||||||
|
import * as cofd_pb from "./cofd_pb";
|
||||||
|
|
||||||
|
export class CharacterIdentifier extends jspb.Message {
|
||||||
|
getOwner(): string;
|
||||||
|
setOwner(value: string): void;
|
||||||
|
|
||||||
|
getId(): number;
|
||||||
|
setId(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): CharacterIdentifier.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: CharacterIdentifier): CharacterIdentifier.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: CharacterIdentifier, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): CharacterIdentifier;
|
||||||
|
static deserializeBinaryFromReader(message: CharacterIdentifier, reader: jspb.BinaryReader): CharacterIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CharacterIdentifier {
|
||||||
|
export type AsObject = {
|
||||||
|
owner: string,
|
||||||
|
id: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateBasicInfoRequest extends jspb.Message {
|
||||||
|
hasCharacter(): boolean;
|
||||||
|
clearCharacter(): void;
|
||||||
|
getCharacter(): CharacterIdentifier | undefined;
|
||||||
|
setCharacter(value?: CharacterIdentifier): void;
|
||||||
|
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
getGender(): string;
|
||||||
|
setGender(value: string): void;
|
||||||
|
|
||||||
|
getConcept(): string;
|
||||||
|
setConcept(value: string): void;
|
||||||
|
|
||||||
|
getChronicle(): string;
|
||||||
|
setChronicle(value: string): void;
|
||||||
|
|
||||||
|
getAge(): number;
|
||||||
|
setAge(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateBasicInfoRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateBasicInfoRequest): UpdateBasicInfoRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateBasicInfoRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateBasicInfoRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateBasicInfoRequest, reader: jspb.BinaryReader): UpdateBasicInfoRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateBasicInfoRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
character?: CharacterIdentifier.AsObject,
|
||||||
|
name: string,
|
||||||
|
gender: string,
|
||||||
|
concept: string,
|
||||||
|
chronicle: string,
|
||||||
|
age: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiResult extends jspb.Message {
|
||||||
|
getSuccess(): boolean;
|
||||||
|
setSuccess(value: boolean): void;
|
||||||
|
|
||||||
|
getError(): string;
|
||||||
|
setError(value: string): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): ApiResult.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: ApiResult): ApiResult.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: ApiResult, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): ApiResult;
|
||||||
|
static deserializeBinaryFromReader(message: ApiResult, reader: jspb.BinaryReader): ApiResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ApiResult {
|
||||||
|
export type AsObject = {
|
||||||
|
success: boolean,
|
||||||
|
error: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateAttributeRequest extends jspb.Message {
|
||||||
|
hasCharacter(): boolean;
|
||||||
|
clearCharacter(): void;
|
||||||
|
getCharacter(): CharacterIdentifier | undefined;
|
||||||
|
setCharacter(value?: CharacterIdentifier): void;
|
||||||
|
|
||||||
|
getAttributeName(): string;
|
||||||
|
setAttributeName(value: string): void;
|
||||||
|
|
||||||
|
getAttributeValue(): number;
|
||||||
|
setAttributeValue(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateAttributeRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateAttributeRequest): UpdateAttributeRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateAttributeRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateAttributeRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateAttributeRequest, reader: jspb.BinaryReader): UpdateAttributeRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateAttributeRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
character?: CharacterIdentifier.AsObject,
|
||||||
|
attributeName: string,
|
||||||
|
attributeValue: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateSkillRequest extends jspb.Message {
|
||||||
|
hasCharacter(): boolean;
|
||||||
|
clearCharacter(): void;
|
||||||
|
getCharacter(): CharacterIdentifier | undefined;
|
||||||
|
setCharacter(value?: CharacterIdentifier): void;
|
||||||
|
|
||||||
|
hasSkill(): boolean;
|
||||||
|
clearSkill(): void;
|
||||||
|
getSkill(): cofd_pb.CofdSheet.Skill | undefined;
|
||||||
|
setSkill(value?: cofd_pb.CofdSheet.Skill): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateSkillRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateSkillRequest): UpdateSkillRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateSkillRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateSkillRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateSkillRequest, reader: jspb.BinaryReader): UpdateSkillRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateSkillRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
character?: CharacterIdentifier.AsObject,
|
||||||
|
skill?: cofd_pb.CofdSheet.Skill.AsObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateSkillValueRequest extends jspb.Message {
|
||||||
|
hasCharacter(): boolean;
|
||||||
|
clearCharacter(): void;
|
||||||
|
getCharacter(): CharacterIdentifier | undefined;
|
||||||
|
setCharacter(value?: CharacterIdentifier): void;
|
||||||
|
|
||||||
|
getSkillName(): string;
|
||||||
|
setSkillName(value: string): void;
|
||||||
|
|
||||||
|
getSkillValue(): number;
|
||||||
|
setSkillValue(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateSkillValueRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateSkillValueRequest): UpdateSkillValueRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateSkillValueRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateSkillValueRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateSkillValueRequest, reader: jspb.BinaryReader): UpdateSkillValueRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateSkillValueRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
character?: CharacterIdentifier.AsObject,
|
||||||
|
skillName: string,
|
||||||
|
skillValue: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateSkillSpecializationsRequest extends jspb.Message {
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
clearSpecializationsList(): void;
|
||||||
|
getSpecializationsList(): Array<string>;
|
||||||
|
setSpecializationsList(value: Array<string>): void;
|
||||||
|
addSpecializations(value: string, index?: number): string;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateSkillSpecializationsRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateSkillSpecializationsRequest): UpdateSkillSpecializationsRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateSkillSpecializationsRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateSkillSpecializationsRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateSkillSpecializationsRequest, reader: jspb.BinaryReader): UpdateSkillSpecializationsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateSkillSpecializationsRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
name: string,
|
||||||
|
specializationsList: Array<string>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateMeritsRequest extends jspb.Message {
|
||||||
|
hasCharacter(): boolean;
|
||||||
|
clearCharacter(): void;
|
||||||
|
getCharacter(): CharacterIdentifier | undefined;
|
||||||
|
setCharacter(value?: CharacterIdentifier): void;
|
||||||
|
|
||||||
|
clearMeritsList(): void;
|
||||||
|
getMeritsList(): Array<cofd_pb.CofdSheet.Merit>;
|
||||||
|
setMeritsList(value: Array<cofd_pb.CofdSheet.Merit>): void;
|
||||||
|
addMerits(value?: cofd_pb.CofdSheet.Merit, index?: number): cofd_pb.CofdSheet.Merit;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateMeritsRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateMeritsRequest): UpdateMeritsRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateMeritsRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateMeritsRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateMeritsRequest, reader: jspb.BinaryReader): UpdateMeritsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateMeritsRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
character?: CharacterIdentifier.AsObject,
|
||||||
|
meritsList: Array<cofd_pb.CofdSheet.Merit.AsObject>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateItemsRequest extends jspb.Message {
|
||||||
|
hasCharacter(): boolean;
|
||||||
|
clearCharacter(): void;
|
||||||
|
getCharacter(): CharacterIdentifier | undefined;
|
||||||
|
setCharacter(value?: CharacterIdentifier): void;
|
||||||
|
|
||||||
|
clearItemsList(): void;
|
||||||
|
getItemsList(): Array<cofd_pb.CofdSheet.Item>;
|
||||||
|
setItemsList(value: Array<cofd_pb.CofdSheet.Item>): void;
|
||||||
|
addItems(value?: cofd_pb.CofdSheet.Item, index?: number): cofd_pb.CofdSheet.Item;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): UpdateItemsRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: UpdateItemsRequest): UpdateItemsRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: UpdateItemsRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): UpdateItemsRequest;
|
||||||
|
static deserializeBinaryFromReader(message: UpdateItemsRequest, reader: jspb.BinaryReader): UpdateItemsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UpdateItemsRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
character?: CharacterIdentifier.AsObject,
|
||||||
|
itemsList: Array<cofd_pb.CofdSheet.Item.AsObject>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddConditionRequest extends jspb.Message {
|
||||||
|
getCharacterUsername(): string;
|
||||||
|
setCharacterUsername(value: string): void;
|
||||||
|
|
||||||
|
getCharacterId(): number;
|
||||||
|
setCharacterId(value: number): void;
|
||||||
|
|
||||||
|
getConditionName(): string;
|
||||||
|
setConditionName(value: string): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): AddConditionRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: AddConditionRequest): AddConditionRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: AddConditionRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): AddConditionRequest;
|
||||||
|
static deserializeBinaryFromReader(message: AddConditionRequest, reader: jspb.BinaryReader): AddConditionRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace AddConditionRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
characterUsername: string,
|
||||||
|
characterId: number,
|
||||||
|
conditionName: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoveConditionRequest extends jspb.Message {
|
||||||
|
getCharacterUsername(): string;
|
||||||
|
setCharacterUsername(value: string): void;
|
||||||
|
|
||||||
|
getCharacterId(): number;
|
||||||
|
setCharacterId(value: number): void;
|
||||||
|
|
||||||
|
getConditionName(): string;
|
||||||
|
setConditionName(value: string): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): RemoveConditionRequest.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: RemoveConditionRequest): RemoveConditionRequest.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: RemoveConditionRequest, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): RemoveConditionRequest;
|
||||||
|
static deserializeBinaryFromReader(message: RemoveConditionRequest, reader: jspb.BinaryReader): RemoveConditionRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace RemoveConditionRequest {
|
||||||
|
export type AsObject = {
|
||||||
|
characterUsername: string,
|
||||||
|
characterId: number,
|
||||||
|
conditionName: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
||||||
|
// package: models.proto.cofd.api
|
||||||
|
// file: cofd_api.proto
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// package: models.proto.cofd.api
|
||||||
|
// file: cofd_api.proto
|
||||||
|
|
|
@ -0,0 +1,373 @@
|
||||||
|
// package: models.proto.cofd
|
||||||
|
// file: cofd.proto
|
||||||
|
|
||||||
|
import * as jspb from "google-protobuf";
|
||||||
|
|
||||||
|
export class CoreFields extends jspb.Message {
|
||||||
|
getIntegrity(): number;
|
||||||
|
setIntegrity(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): CoreFields.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: CoreFields): CoreFields.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: CoreFields, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): CoreFields;
|
||||||
|
static deserializeBinaryFromReader(message: CoreFields, reader: jspb.BinaryReader): CoreFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CoreFields {
|
||||||
|
export type AsObject = {
|
||||||
|
integrity: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MageFields extends jspb.Message {
|
||||||
|
getWidsom(): number;
|
||||||
|
setWidsom(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): MageFields.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: MageFields): MageFields.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: MageFields, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): MageFields;
|
||||||
|
static deserializeBinaryFromReader(message: MageFields, reader: jspb.BinaryReader): MageFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace MageFields {
|
||||||
|
export type AsObject = {
|
||||||
|
widsom: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChangelingFields extends jspb.Message {
|
||||||
|
getClarity(): number;
|
||||||
|
setClarity(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): ChangelingFields.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: ChangelingFields): ChangelingFields.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: ChangelingFields, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): ChangelingFields;
|
||||||
|
static deserializeBinaryFromReader(message: ChangelingFields, reader: jspb.BinaryReader): ChangelingFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ChangelingFields {
|
||||||
|
export type AsObject = {
|
||||||
|
clarity: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CofdSheet extends jspb.Message {
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
getGender(): string;
|
||||||
|
setGender(value: string): void;
|
||||||
|
|
||||||
|
getConcept(): string;
|
||||||
|
setConcept(value: string): void;
|
||||||
|
|
||||||
|
getAge(): number;
|
||||||
|
setAge(value: number): void;
|
||||||
|
|
||||||
|
getPlayer(): string;
|
||||||
|
setPlayer(value: string): void;
|
||||||
|
|
||||||
|
getChronicle(): string;
|
||||||
|
setChronicle(value: string): void;
|
||||||
|
|
||||||
|
getDescription(): string;
|
||||||
|
setDescription(value: string): void;
|
||||||
|
|
||||||
|
getStrength(): number;
|
||||||
|
setStrength(value: number): void;
|
||||||
|
|
||||||
|
getDexterity(): number;
|
||||||
|
setDexterity(value: number): void;
|
||||||
|
|
||||||
|
getStamina(): number;
|
||||||
|
setStamina(value: number): void;
|
||||||
|
|
||||||
|
getIntelligence(): number;
|
||||||
|
setIntelligence(value: number): void;
|
||||||
|
|
||||||
|
getWits(): number;
|
||||||
|
setWits(value: number): void;
|
||||||
|
|
||||||
|
getResolve(): number;
|
||||||
|
setResolve(value: number): void;
|
||||||
|
|
||||||
|
getPresence(): number;
|
||||||
|
setPresence(value: number): void;
|
||||||
|
|
||||||
|
getManipulation(): number;
|
||||||
|
setManipulation(value: number): void;
|
||||||
|
|
||||||
|
getComposure(): number;
|
||||||
|
setComposure(value: number): void;
|
||||||
|
|
||||||
|
getPhysicalSkillsMap(): jspb.Map<string, CofdSheet.Skill>;
|
||||||
|
clearPhysicalSkillsMap(): void;
|
||||||
|
getMentalSkillsMap(): jspb.Map<string, CofdSheet.Skill>;
|
||||||
|
clearMentalSkillsMap(): void;
|
||||||
|
getSocialSkillsMap(): jspb.Map<string, CofdSheet.Skill>;
|
||||||
|
clearSocialSkillsMap(): void;
|
||||||
|
clearMeritsList(): void;
|
||||||
|
getMeritsList(): Array<CofdSheet.Merit>;
|
||||||
|
setMeritsList(value: Array<CofdSheet.Merit>): void;
|
||||||
|
addMerits(value?: CofdSheet.Merit, index?: number): CofdSheet.Merit;
|
||||||
|
|
||||||
|
clearConditionsList(): void;
|
||||||
|
getConditionsList(): Array<CofdSheet.Condition>;
|
||||||
|
setConditionsList(value: Array<CofdSheet.Condition>): void;
|
||||||
|
addConditions(value?: CofdSheet.Condition, index?: number): CofdSheet.Condition;
|
||||||
|
|
||||||
|
getSize(): number;
|
||||||
|
setSize(value: number): void;
|
||||||
|
|
||||||
|
getHealth(): number;
|
||||||
|
setHealth(value: number): void;
|
||||||
|
|
||||||
|
getWillpower(): number;
|
||||||
|
setWillpower(value: number): void;
|
||||||
|
|
||||||
|
getExperiencePoints(): number;
|
||||||
|
setExperiencePoints(value: number): void;
|
||||||
|
|
||||||
|
getBeats(): number;
|
||||||
|
setBeats(value: number): void;
|
||||||
|
|
||||||
|
clearItemsList(): void;
|
||||||
|
getItemsList(): Array<CofdSheet.Item>;
|
||||||
|
setItemsList(value: Array<CofdSheet.Item>): void;
|
||||||
|
addItems(value?: CofdSheet.Item, index?: number): CofdSheet.Item;
|
||||||
|
|
||||||
|
clearAttacksList(): void;
|
||||||
|
getAttacksList(): Array<CofdSheet.Attack>;
|
||||||
|
setAttacksList(value: Array<CofdSheet.Attack>): void;
|
||||||
|
addAttacks(value?: CofdSheet.Attack, index?: number): CofdSheet.Attack;
|
||||||
|
|
||||||
|
getOtherDataMap(): jspb.Map<string, string>;
|
||||||
|
clearOtherDataMap(): void;
|
||||||
|
hasCore(): boolean;
|
||||||
|
clearCore(): void;
|
||||||
|
getCore(): CoreFields | undefined;
|
||||||
|
setCore(value?: CoreFields): void;
|
||||||
|
|
||||||
|
hasMage(): boolean;
|
||||||
|
clearMage(): void;
|
||||||
|
getMage(): MageFields | undefined;
|
||||||
|
setMage(value?: MageFields): void;
|
||||||
|
|
||||||
|
hasChangeling(): boolean;
|
||||||
|
clearChangeling(): void;
|
||||||
|
getChangeling(): ChangelingFields | undefined;
|
||||||
|
setChangeling(value?: ChangelingFields): void;
|
||||||
|
|
||||||
|
getSystemFieldsCase(): CofdSheet.SystemFieldsCase;
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): CofdSheet.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: CofdSheet): CofdSheet.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: CofdSheet, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): CofdSheet;
|
||||||
|
static deserializeBinaryFromReader(message: CofdSheet, reader: jspb.BinaryReader): CofdSheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CofdSheet {
|
||||||
|
export type AsObject = {
|
||||||
|
name: string,
|
||||||
|
gender: string,
|
||||||
|
concept: string,
|
||||||
|
age: number,
|
||||||
|
player: string,
|
||||||
|
chronicle: string,
|
||||||
|
description: string,
|
||||||
|
strength: number,
|
||||||
|
dexterity: number,
|
||||||
|
stamina: number,
|
||||||
|
intelligence: number,
|
||||||
|
wits: number,
|
||||||
|
resolve: number,
|
||||||
|
presence: number,
|
||||||
|
manipulation: number,
|
||||||
|
composure: number,
|
||||||
|
physicalSkillsMap: Array<[string, CofdSheet.Skill.AsObject]>,
|
||||||
|
mentalSkillsMap: Array<[string, CofdSheet.Skill.AsObject]>,
|
||||||
|
socialSkillsMap: Array<[string, CofdSheet.Skill.AsObject]>,
|
||||||
|
meritsList: Array<CofdSheet.Merit.AsObject>,
|
||||||
|
conditionsList: Array<CofdSheet.Condition.AsObject>,
|
||||||
|
size: number,
|
||||||
|
health: number,
|
||||||
|
willpower: number,
|
||||||
|
experiencePoints: number,
|
||||||
|
beats: number,
|
||||||
|
itemsList: Array<CofdSheet.Item.AsObject>,
|
||||||
|
attacksList: Array<CofdSheet.Attack.AsObject>,
|
||||||
|
otherDataMap: Array<[string, string]>,
|
||||||
|
core?: CoreFields.AsObject,
|
||||||
|
mage?: MageFields.AsObject,
|
||||||
|
changeling?: ChangelingFields.AsObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Merit extends jspb.Message {
|
||||||
|
getDots(): number;
|
||||||
|
setDots(value: number): void;
|
||||||
|
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): Merit.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: Merit): Merit.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: Merit, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): Merit;
|
||||||
|
static deserializeBinaryFromReader(message: Merit, reader: jspb.BinaryReader): Merit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Merit {
|
||||||
|
export type AsObject = {
|
||||||
|
dots: number,
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Condition extends jspb.Message {
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): Condition.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: Condition): Condition.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: Condition, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): Condition;
|
||||||
|
static deserializeBinaryFromReader(message: Condition, reader: jspb.BinaryReader): Condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Condition {
|
||||||
|
export type AsObject = {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Skill extends jspb.Message {
|
||||||
|
getDots(): number;
|
||||||
|
setDots(value: number): void;
|
||||||
|
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
getUntrainedPenalty(): number;
|
||||||
|
setUntrainedPenalty(value: number): void;
|
||||||
|
|
||||||
|
clearSpecializationsList(): void;
|
||||||
|
getSpecializationsList(): Array<string>;
|
||||||
|
setSpecializationsList(value: Array<string>): void;
|
||||||
|
addSpecializations(value: string, index?: number): string;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): Skill.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: Skill): Skill.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: Skill, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): Skill;
|
||||||
|
static deserializeBinaryFromReader(message: Skill, reader: jspb.BinaryReader): Skill;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Skill {
|
||||||
|
export type AsObject = {
|
||||||
|
dots: number,
|
||||||
|
name: string,
|
||||||
|
untrainedPenalty: number,
|
||||||
|
specializationsList: Array<string>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Item extends jspb.Message {
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
getDescription(): string;
|
||||||
|
setDescription(value: string): void;
|
||||||
|
|
||||||
|
getRules(): string;
|
||||||
|
setRules(value: string): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): Item.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: Item): Item.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: Item, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): Item;
|
||||||
|
static deserializeBinaryFromReader(message: Item, reader: jspb.BinaryReader): Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Item {
|
||||||
|
export type AsObject = {
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
rules: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Attack extends jspb.Message {
|
||||||
|
getName(): string;
|
||||||
|
setName(value: string): void;
|
||||||
|
|
||||||
|
getDicePool(): number;
|
||||||
|
setDicePool(value: number): void;
|
||||||
|
|
||||||
|
getDamage(): number;
|
||||||
|
setDamage(value: number): void;
|
||||||
|
|
||||||
|
getRange(): number;
|
||||||
|
setRange(value: number): void;
|
||||||
|
|
||||||
|
getInitiativeModifier(): number;
|
||||||
|
setInitiativeModifier(value: number): void;
|
||||||
|
|
||||||
|
getSize(): number;
|
||||||
|
setSize(value: number): void;
|
||||||
|
|
||||||
|
serializeBinary(): Uint8Array;
|
||||||
|
toObject(includeInstance?: boolean): Attack.AsObject;
|
||||||
|
static toObject(includeInstance: boolean, msg: Attack): Attack.AsObject;
|
||||||
|
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||||
|
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||||
|
static serializeBinaryToWriter(message: Attack, writer: jspb.BinaryWriter): void;
|
||||||
|
static deserializeBinary(bytes: Uint8Array): Attack;
|
||||||
|
static deserializeBinaryFromReader(message: Attack, reader: jspb.BinaryReader): Attack;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Attack {
|
||||||
|
export type AsObject = {
|
||||||
|
name: string,
|
||||||
|
dicePool: number,
|
||||||
|
damage: number,
|
||||||
|
range: number,
|
||||||
|
initiativeModifier: number,
|
||||||
|
size: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SystemFieldsCase {
|
||||||
|
SYSTEM_FIELDS_NOT_SET = 0,
|
||||||
|
CORE = 30,
|
||||||
|
MAGE = 31,
|
||||||
|
CHANGELING = 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
||||||
|
// package: models.proto.cofd
|
||||||
|
// file: cofd.proto
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// package: models.proto.cofd
|
||||||
|
// file: cofd.proto
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import * as jspb from "google-protobuf";
|
||||||
|
import { ApiResult, UpdateBasicInfoRequest, UpdateAttributeRequest, UpdateSkillValueRequest, UpdateMeritsRequest, UpdateItemsRequest } from "../_proto/cofd_api_pb";
|
||||||
|
|
||||||
|
const PROTOBUF_CONTENT_TYPE = { 'Content-Type': 'application/x-protobuf' };
|
||||||
|
|
||||||
|
async function makeRequest<T extends jspb.Message>(uri: string, params: T): Promise<Uint8Array> {
|
||||||
|
let resp = await fetch(uri, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...PROTOBUF_CONTENT_TYPE },
|
||||||
|
body: params.serializeBinary()
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp.arrayBuffer();
|
||||||
|
return new Uint8Array(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSkillValue(params: UpdateSkillValueRequest): Promise<ApiResult> {
|
||||||
|
let data = await makeRequest('/api/rpc/cofd/update_skill_value', params);
|
||||||
|
return ApiResult.deserializeBinary(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAttributeValue(params: UpdateAttributeRequest): Promise<ApiResult> {
|
||||||
|
let data = await makeRequest('/api/rpc/cofd/update_attribute_value', params);
|
||||||
|
return ApiResult.deserializeBinary(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBasicInfo(params: UpdateBasicInfoRequest): Promise<ApiResult> {
|
||||||
|
let data = await makeRequest('/api/rpc/cofd/update_basic_info', params);
|
||||||
|
return ApiResult.deserializeBinary(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMerits(params: UpdateMeritsRequest): Promise<ApiResult> {
|
||||||
|
let data = await makeRequest('/api/rpc/cofd/update_merits', params);
|
||||||
|
return ApiResult.deserializeBinary(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateItems(params: UpdateItemsRequest): Promise<ApiResult> {
|
||||||
|
let data = await makeRequest('/api/rpc/cofd/update_items', params);
|
||||||
|
return ApiResult.deserializeBinary(data);
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
export function addMeritLine(parent: Element) {
|
||||||
|
const meritName = document.createElement('input');
|
||||||
|
meritName.type = 'text';
|
||||||
|
meritName.value = '';
|
||||||
|
meritName.classList.add('merit-name');
|
||||||
|
|
||||||
|
const meritDots = document.createElement('input');
|
||||||
|
meritDots.type = 'number';
|
||||||
|
meritDots.min = '0';
|
||||||
|
meritDots.value = '0';
|
||||||
|
meritDots.classList.add('merit-dots');
|
||||||
|
|
||||||
|
const removeButton = document.createElement('button');
|
||||||
|
removeButton.innerText = "X";
|
||||||
|
removeButton.setAttribute('data-event-type', 'remove-merit');
|
||||||
|
removeButton.classList.add('remove-merit');
|
||||||
|
|
||||||
|
const dots = document.createElement('span');
|
||||||
|
dots.innerText = 'Dots';
|
||||||
|
|
||||||
|
const merit = document.createElement('div');
|
||||||
|
merit.classList.add('merit');
|
||||||
|
merit.appendChild(meritName);
|
||||||
|
merit.appendChild(meritDots);
|
||||||
|
merit.appendChild(dots);
|
||||||
|
merit.appendChild(removeButton);
|
||||||
|
|
||||||
|
parent.appendChild(merit);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function addItemLine(parent: Element) {
|
||||||
|
const itemName = document.createElement('input');
|
||||||
|
itemName.type = 'text';
|
||||||
|
itemName.value = '';
|
||||||
|
itemName.classList.add('item-name');
|
||||||
|
|
||||||
|
const removeButton = document.createElement('button');
|
||||||
|
removeButton.innerText = "X";
|
||||||
|
removeButton.setAttribute('data-event-type', 'remove-item');
|
||||||
|
removeButton.classList.add('remove-item');
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.classList.add('item');
|
||||||
|
item.appendChild(itemName);
|
||||||
|
item.appendChild(removeButton);
|
||||||
|
|
||||||
|
parent.appendChild(item);
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { CharacterIdentifier, UpdateBasicInfoRequest, UpdateSkillValueRequest, UpdateAttributeRequest, UpdateMeritsRequest, UpdateItemsRequest } from "../../_proto/cofd_api_pb";
|
||||||
|
import { CofdSheet } from "../../_proto/cofd_pb";
|
||||||
|
import * as api from "../api";
|
||||||
|
import * as ui from "./edit-ui";
|
||||||
|
|
||||||
|
// This is the scripting for the edit character page, which submits
|
||||||
|
// changes to the server as the user makes them.
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Useful for making sure elements actually exist in event handler.
|
||||||
|
type Option<T> = T | null | undefined;
|
||||||
|
|
||||||
|
const [, , USERNAME, CHARACTER_ID] = window.location.pathname.split('/');
|
||||||
|
|
||||||
|
function characterId(): CharacterIdentifier {
|
||||||
|
const id = new CharacterIdentifier();
|
||||||
|
id.setId(parseInt(CHARACTER_ID));
|
||||||
|
id.setOwner(USERNAME);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTextValue = (selector: string) =>
|
||||||
|
document.querySelector<HTMLInputElement>(selector)?.value ?? "";
|
||||||
|
|
||||||
|
const getIntValue = (selector: string) =>
|
||||||
|
parseInt(document.querySelector<HTMLInputElement>(selector)?.value ?? "0")
|
||||||
|
|
||||||
|
function setupAttributes() {
|
||||||
|
const attributeInputs = document.querySelectorAll('#attributes input[type="number"]');
|
||||||
|
|
||||||
|
async function attributeHandler(event: Event) {
|
||||||
|
const input = event.target as Option<HTMLInputElement>;
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
console.log("updating attr");
|
||||||
|
const attribute = input.id;
|
||||||
|
const newValue = parseInt(input.value);
|
||||||
|
const params = new UpdateAttributeRequest();
|
||||||
|
params.setCharacter(characterId());
|
||||||
|
params.setAttributeName(attribute);
|
||||||
|
params.setAttributeValue(newValue);
|
||||||
|
let resp = await api.updateAttributeValue(params);
|
||||||
|
console.log("got a response back", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(attributeInputs).forEach(input => {
|
||||||
|
input.addEventListener('change', attributeHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSkills() {
|
||||||
|
const skillInputs = document.querySelectorAll('#skills input[type="number"]');
|
||||||
|
|
||||||
|
async function skillValueHandler(event: Event) {
|
||||||
|
const input = event.target as Option<HTMLInputElement>;
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
console.log("updating skill");
|
||||||
|
const attribute = input.id;
|
||||||
|
const newValue = parseInt(input.value);
|
||||||
|
|
||||||
|
const params = new UpdateSkillValueRequest();
|
||||||
|
params.setCharacter(characterId());
|
||||||
|
params.setSkillName(attribute);
|
||||||
|
params.setSkillValue(newValue);
|
||||||
|
|
||||||
|
let resp = await api.updateSkillValue(params);
|
||||||
|
console.log("got a response back", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(skillInputs).forEach(input => {
|
||||||
|
input.addEventListener('change', skillValueHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupBasicInfo() {
|
||||||
|
async function updateInfo() {
|
||||||
|
const params = new UpdateBasicInfoRequest();
|
||||||
|
|
||||||
|
params.setCharacter(characterId());
|
||||||
|
params.setName(getTextValue("#characterName"));
|
||||||
|
params.setAge(getIntValue("#age"));
|
||||||
|
params.setConcept(getTextValue("#concept"));
|
||||||
|
params.setChronicle(getTextValue("#chronicle"));
|
||||||
|
params.setGender(getTextValue("#gender"));
|
||||||
|
|
||||||
|
let resp = await api.updateBasicInfo(params);
|
||||||
|
console.log("got a response back", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = document.querySelectorAll("#basics input");
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
console.log('got an input', input);
|
||||||
|
input.addEventListener('blur', updateInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMerits() {
|
||||||
|
async function updateMerits() {
|
||||||
|
let meritElements = document.querySelectorAll<HTMLInputElement>('#merits input[class="merit-name"]');
|
||||||
|
let merits: CofdSheet.Merit[] =
|
||||||
|
Array.from(meritElements).map(input => {
|
||||||
|
const dotsInput = input.parentElement?.querySelector<HTMLInputElement>('input[class="merit-dots"]');
|
||||||
|
|
||||||
|
let dotsAmount = dotsInput?.value ?? "0";
|
||||||
|
if (dotsAmount.length == 0) { dotsAmount = "0"; }
|
||||||
|
|
||||||
|
const merit = new CofdSheet.Merit();
|
||||||
|
merit.setName(input.value);
|
||||||
|
merit.setDots(parseInt(dotsAmount));
|
||||||
|
|
||||||
|
return merit;
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = new UpdateMeritsRequest();
|
||||||
|
params.setCharacter(characterId());
|
||||||
|
params.setMeritsList(merits);
|
||||||
|
|
||||||
|
let resp = await api.updateMerits(params);
|
||||||
|
console.log("got a response back", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMerit(event: Event) {
|
||||||
|
const button = event.target as Option<Element>;
|
||||||
|
const meritLine = button?.parentElement;
|
||||||
|
if (meritLine) {
|
||||||
|
document.querySelector("#merit-list")?.removeChild(meritLine);
|
||||||
|
await updateMerits();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processEvent(event: Event) {
|
||||||
|
console.log('processing merit control event');
|
||||||
|
const element = event.target as Element;
|
||||||
|
const eventType = element.getAttribute('data-event-type') ?? '';
|
||||||
|
|
||||||
|
if (eventType == 'remove-merit') {
|
||||||
|
await removeMerit(event);
|
||||||
|
} else if (eventType == 'add-merit') {
|
||||||
|
const meritList = document.querySelector<HTMLDivElement>("#merit-list")!;
|
||||||
|
ui.addMeritLine(meritList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const meritControls = document.querySelector<HTMLDivElement>("#merit-controls");
|
||||||
|
meritControls?.addEventListener('click', processEvent);
|
||||||
|
|
||||||
|
const meritList = document.querySelector<HTMLDivElement>("#merit-list");
|
||||||
|
meritList?.addEventListener('click', processEvent);
|
||||||
|
meritList?.addEventListener('blur', updateMerits);
|
||||||
|
meritList?.addEventListener('change', updateMerits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupItems() {
|
||||||
|
async function updateItems() {
|
||||||
|
let meritElements = document.querySelectorAll<HTMLInputElement>('#items input[class="item-name"]');
|
||||||
|
let items: CofdSheet.Item[] =
|
||||||
|
Array.from(meritElements).map(input => {
|
||||||
|
const item = new CofdSheet.Item();
|
||||||
|
item.setName(input.value);
|
||||||
|
item.setDescription(""); //TODO add description
|
||||||
|
item.setRules(""); //TODO add rules
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("items are", items);
|
||||||
|
const params = new UpdateItemsRequest();
|
||||||
|
params.setCharacter(characterId());
|
||||||
|
params.setItemsList(items);
|
||||||
|
|
||||||
|
let resp = await api.updateItems(params);
|
||||||
|
console.log("got a response back", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeItem(event: Event) {
|
||||||
|
const button = event.target as Option<Element>;
|
||||||
|
const itemLine = button?.parentElement;
|
||||||
|
if (itemLine) {
|
||||||
|
document.querySelector("#item-list")?.removeChild(itemLine);
|
||||||
|
await updateItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processEvent(event: Event) {
|
||||||
|
console.log('processing item control event');
|
||||||
|
const element = event.target as Element;
|
||||||
|
const eventType = element.getAttribute('data-event-type') ?? '';
|
||||||
|
|
||||||
|
if (eventType == 'remove-item') {
|
||||||
|
await removeItem(event);
|
||||||
|
} else if (eventType == 'add-item') {
|
||||||
|
const itemList = document.querySelector<HTMLDivElement>("#item-list")!;
|
||||||
|
ui.addItemLine(itemList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const meritControls = document.querySelector<HTMLDivElement>("#item-controls");
|
||||||
|
meritControls?.addEventListener('click', processEvent);
|
||||||
|
|
||||||
|
const meritList = document.querySelector<HTMLDivElement>("#item-list");
|
||||||
|
meritList?.addEventListener('click', processEvent);
|
||||||
|
meritList?.addEventListener('blur', updateItems);
|
||||||
|
meritList?.addEventListener('change', updateItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAttributes();
|
||||||
|
setupSkills();
|
||||||
|
setupBasicInfo();
|
||||||
|
setupMerits();
|
||||||
|
setupItems();
|
||||||
|
})().catch(e => {
|
||||||
|
alert(e);
|
||||||
|
});
|
|
@ -2,6 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tera Demo</title>
|
<title>Tera Demo</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
|
@ -0,0 +1,285 @@
|
||||||
|
{% import "characters/edit_character_macros" as macros %}
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
font-family: Liberation Sans, Arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#basics .flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-entry {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-entry label {
|
||||||
|
display: inline;
|
||||||
|
width: 5em;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-entry input {
|
||||||
|
display: inline;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attributes .flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attributes .attributes-section {
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute label {
|
||||||
|
width: 10em;
|
||||||
|
display: inline;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute input {
|
||||||
|
max-width: 4em;
|
||||||
|
display: inline;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills .flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills .skills-section {
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill label {
|
||||||
|
width: 10em;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill input {
|
||||||
|
max-width: 4em;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#merits, #items {
|
||||||
|
padding: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#merit-list, #item-list {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit, .item {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit .merit-name, .item .item-name {
|
||||||
|
width: 10em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px;
|
||||||
|
border: none;
|
||||||
|
border: 0px solid gray;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit .merit-dots {
|
||||||
|
max-width: 4em;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit span {
|
||||||
|
width: 3em;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit .remove-merit, .item .remove-item {
|
||||||
|
max-width: 4em;
|
||||||
|
padding: 0;
|
||||||
|
padding: 3px;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right:6px;
|
||||||
|
margin: 3px;
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{# Webpack Templating for API script #}
|
||||||
|
<%= htmlWebpackPlugin.tags.bodyTags %>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div id="basics">
|
||||||
|
<h1>
|
||||||
|
<label for="characterName">Name</label>
|
||||||
|
<input type="text" id="characterName" name="characterName" value="{{name}}" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="links">
|
||||||
|
<a href="/">Home</a> |
|
||||||
|
<a href="/characters/{{username}}/{{id}}">Save Changes</a> |
|
||||||
|
<a href="/characters/{{username}}/{{id}}/delete">Delete Character</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Basics</h2>
|
||||||
|
<div>System: {{data_type}}</div>
|
||||||
|
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="basics-row">
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="gender">Gender:</label>
|
||||||
|
<input type="text" id="gender" name="gender" value="{{sheet.gender}}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="age">Age:</label>
|
||||||
|
<input type="number" id="age" name="age" min="0" value="{{sheet.age}}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basics-row">
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="concept">Concept:</label>
|
||||||
|
<input type="text" id="concept" name="concept" value="{{sheet.concept}}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="chronicle">Chronicle:</label>
|
||||||
|
<input type="text" id="chronicle" name="chronicle" value="{{sheet.chronicle}}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="attributes">
|
||||||
|
<h2>Attributes</h2>
|
||||||
|
<div class="flex-container">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div id="skills">
|
||||||
|
<h2>Skills</h2>
|
||||||
|
|
||||||
|
<div class="flex-container">
|
||||||
|
<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 id="merits">
|
||||||
|
<h2>Merits</h2>
|
||||||
|
<div id="merit-list">
|
||||||
|
{% for merit in sheet.merits %}
|
||||||
|
{{ macros::merit(name=merit.name, value=merit.dots) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="merit-controls">
|
||||||
|
<button data-event-type="add-merit">Add Merit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="items">
|
||||||
|
<h2>Inventory</h2>
|
||||||
|
|
||||||
|
<div id="item-list">
|
||||||
|
{% for item in sheet.items %}
|
||||||
|
{{ macros::item(name=item.name) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div id="item-controls">
|
||||||
|
<button data-event-type="add-item">Add Item</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{% 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 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 %}
|
||||||
|
|
||||||
|
{% macro merit(name, value) %}
|
||||||
|
<div class="merit">
|
||||||
|
<input type="text" class="merit-name" value="{{name}}" />
|
||||||
|
<input type="number" class="merit-dots" min="0" value="{{value}}" /> <span>Dots</span>
|
||||||
|
<button class="remove-merit" data-event-type="remove-merit">X</button>
|
||||||
|
</div>
|
||||||
|
{% endmacro merit %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro item(name) %}
|
||||||
|
<div class="item">
|
||||||
|
<input type="text" class="item-name" value="{{name}}" />
|
||||||
|
<button class="remove-item" data-event-type="remove-item">X</button>
|
||||||
|
</div>
|
||||||
|
{% endmacro item %}
|
|
@ -1,8 +1,6 @@
|
||||||
{% 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.
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
|
||||||
|
{% import "characters/view_character_macros" as macros %}
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
font-family: Liberation Sans, Arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#basics .flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-entry {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-entry label {
|
||||||
|
display: inline;
|
||||||
|
width: 5em;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basics-entry span {
|
||||||
|
width: 15em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attributes .flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attributes .attributes-section {
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute label {
|
||||||
|
width: 10em;
|
||||||
|
display: inline;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute span {
|
||||||
|
width: 2em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills .flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills .skills-section {
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill label {
|
||||||
|
width: 10em;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill span {
|
||||||
|
width: 2em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#merits, #items {
|
||||||
|
padding: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#merit-list, #item-list {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit, .item {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit span, .item span {
|
||||||
|
width: 3em;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit .merit-name, .item .item-name {
|
||||||
|
width: 10em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px;
|
||||||
|
border: none;
|
||||||
|
border: 0px solid gray;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merit .merit-dots {
|
||||||
|
max-width: 4em;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div id="basics">
|
||||||
|
<h1>
|
||||||
|
<span id="characterName" name="characterName">{{name}}</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="links">
|
||||||
|
<a href="/">Home</a> |
|
||||||
|
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a> |
|
||||||
|
<a href="/characters/{{username}}/{{id}}/delete">Delete Character</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Basics</h2>
|
||||||
|
<div>System: {{data_type}}</div>
|
||||||
|
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="basics-row">
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="gender">Gender:</label>
|
||||||
|
<span id="gender" name="gender">{{sheet.gender}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="age">Age:</label>
|
||||||
|
<span id="age" name="age">{{sheet.age}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basics-row">
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="concept">Concept:</label>
|
||||||
|
<span id="concept" name="concept">{{sheet.concept}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basics-entry">
|
||||||
|
<label for="chronicle">Chronicle:</label>
|
||||||
|
<span id="chronicle" name="chronicle">{{sheet.chronicle}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="attributes">
|
||||||
|
<h2>Attributes</h2>
|
||||||
|
<div class="flex-container">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div id="skills">
|
||||||
|
<h2>Skills</h2>
|
||||||
|
<div class="flex-container">
|
||||||
|
<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 id="merits">
|
||||||
|
<h2>Merits</h2>
|
||||||
|
<div id="merit-list">
|
||||||
|
{% for merit in sheet.merits %}
|
||||||
|
{{ macros::merit(name=merit.name, value=merit.dots) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="items">
|
||||||
|
<h2>Items</h2>
|
||||||
|
<div id="item-list">
|
||||||
|
{% for item in sheet.items %}
|
||||||
|
<div class="item">
|
||||||
|
<span class="item-name">{{item.name}}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% macro attribute(name, value) %}
|
||||||
|
<div class="attribute">
|
||||||
|
<label for="{{name}}">{{name}}:</label>
|
||||||
|
<span class="value" id="{{name}}">{{value}}</span>
|
||||||
|
</div>
|
||||||
|
{% endmacro attribute %}
|
||||||
|
|
||||||
|
{% macro skill(name, value) %}
|
||||||
|
<div class="skill">
|
||||||
|
<label for="{{name}}">{{name}}:</label>
|
||||||
|
<span class="value" id="{{name}}">{{value}}</span>
|
||||||
|
</div>
|
||||||
|
{% endmacro skill %}
|
||||||
|
|
||||||
|
{% macro merit(name, value) %}
|
||||||
|
<div class="merit">
|
||||||
|
<span class="merit-name">{{name}}</span>
|
||||||
|
<span class="merit-dots">{{value}}</span> <span>Dots</span>
|
||||||
|
</div>
|
||||||
|
{% endmacro merit %}
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<%= htmlWebpackPlugin.tags.bodyTags %>
|
||||||
<div>
|
<div>
|
||||||
<h1>Tenebrous: Login</h1>
|
<h1>Tenebrous: Login</h1>
|
||||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -4,26 +4,21 @@ extern crate rocket;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket_contrib;
|
extern crate rocket_contrib;
|
||||||
|
|
||||||
#[macro_use]
|
use log::{error, info};
|
||||||
extern crate diesel;
|
|
||||||
|
|
||||||
// Seemingly necessary to get serde::Serialize into scope for Prost
|
|
||||||
// code generation.
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
|
|
||||||
use rocket_contrib::serve::StaticFiles;
|
use rocket_contrib::serve::StaticFiles;
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
|
use std::env;
|
||||||
|
use tonic::transport::Server;
|
||||||
|
|
||||||
pub mod catchers;
|
pub mod catchers;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
pub mod migrator;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod schema;
|
|
||||||
|
|
||||||
#[rocket::main]
|
async fn make_rocket(database: sqlx::SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
async fn main() -> Result<(), rocket::error::Error> {
|
info!("Running Rocket");
|
||||||
let root_routes: Vec<rocket::Route> = {
|
let root_routes: Vec<rocket::Route> = {
|
||||||
routes::root::routes()
|
routes::root::routes()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -37,13 +32,13 @@ async fn main() -> Result<(), rocket::error::Error> {
|
||||||
|
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.attach(db::TenebrousDbConn::fairing())
|
.manage(database)
|
||||||
.mount("/", root_routes)
|
.mount("/", root_routes)
|
||||||
.mount("/characters", character_routes)
|
.mount("/characters", character_routes)
|
||||||
.mount("/api", api_routes)
|
.mount("/api", api_routes)
|
||||||
.mount(
|
.mount(
|
||||||
"/scripts",
|
"/scripts",
|
||||||
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static/scripts")),
|
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/scripts")),
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/protos",
|
"/protos",
|
||||||
|
@ -51,5 +46,25 @@ async fn main() -> Result<(), rocket::error::Error> {
|
||||||
)
|
)
|
||||||
.register(catchers)
|
.register(catchers)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let db_path: &str = match &args[..] {
|
||||||
|
[_, path] => path.as_ref(),
|
||||||
|
[_, _, ..] => panic!("Expected exactly 0 or 1 argument"),
|
||||||
|
_ => "tenebrous.sqlite",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Using database: {}", db_path);
|
||||||
|
|
||||||
|
migrator::migrate(db_path).await?;
|
||||||
|
|
||||||
|
let db = crate::db::create_pool(db_path).await?;
|
||||||
|
|
||||||
|
make_rocket(db.clone()).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
pub mod migrator;
|
||||||
|
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let db_path: &str = match &args[..] {
|
||||||
|
[_, path] => path.as_ref(),
|
||||||
|
[_, _, ..] => panic!("Expected exactly 0 or 1 argument"),
|
||||||
|
_ => "tenebrous.sqlite",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Using database: {}", db_path);
|
||||||
|
|
||||||
|
crate::migrator::migrate(db_path).await
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
use refinery::config::{Config, ConfigDbType};
|
||||||
|
use sqlx::sqlite::SqliteConnectOptions;
|
||||||
|
use sqlx::ConnectOptions;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub mod migrations;
|
||||||
|
|
||||||
|
/// Run database migrations against the sqlite database.
|
||||||
|
pub(crate) async fn migrate(db_path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//Create database if missing.
|
||||||
|
let conn = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path))?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
let mut conn = Config::new(ConfigDbType::Sqlite).set_db_path(db_path);
|
||||||
|
println!("Running migrations");
|
||||||
|
migrations::runner().run(&mut conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
use barrel::backend::Sqlite;
|
||||||
|
use barrel::{types, Migration};
|
||||||
|
|
||||||
|
pub fn migration() -> String {
|
||||||
|
let mut m = Migration::new();
|
||||||
|
println!("Applying migration: {}", file!());
|
||||||
|
|
||||||
|
m.create_table("users", |t| {
|
||||||
|
t.add_column("id", types::primary());
|
||||||
|
t.add_column("username", types::text());
|
||||||
|
t.add_column("password", types::text());
|
||||||
|
});
|
||||||
|
|
||||||
|
m.make::<Sqlite>()
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
use barrel::backend::Sqlite;
|
||||||
|
use barrel::{types, Migration};
|
||||||
|
|
||||||
|
pub fn migration() -> String {
|
||||||
|
let mut m = Migration::new();
|
||||||
|
println!("Applying migration: {}", file!());
|
||||||
|
|
||||||
|
m.create_table("characters", move |t| {
|
||||||
|
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());
|
||||||
|
t.add_column("character_name", types::text());
|
||||||
|
t.add_column("data_type", types::custom(db_enum));
|
||||||
|
t.add_column("data_version", types::integer());
|
||||||
|
t.add_column("data", types::custom("BLOB"));
|
||||||
|
});
|
||||||
|
|
||||||
|
m.make::<Sqlite>()
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
use refinery::include_migration_mods;
|
||||||
|
include_migration_mods!("src/migrator/migrations");
|
|
@ -1,10 +1,8 @@
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::models::proto::cofd::*;
|
use crate::models::proto::cofd::*;
|
||||||
use crate::models::users::User;
|
use crate::models::users::User;
|
||||||
use crate::schema::characters;
|
|
||||||
use diesel_derive_enum::DbEnum;
|
|
||||||
use prost::bytes::BytesMut;
|
use prost::bytes::BytesMut;
|
||||||
use serde_derive::Serialize;
|
use serde::Serialize;
|
||||||
use strum::{EnumIter, EnumString};
|
use strum::{EnumIter, EnumString};
|
||||||
|
|
||||||
/// Dynamic character data is an opaque container type that holds
|
/// Dynamic character data is an opaque container type that holds
|
||||||
|
@ -41,26 +39,34 @@ pub(crate) trait Visibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString)]
|
/// 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. 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 {
|
pub enum CharacterDataType {
|
||||||
ChroniclesOfDarknessV1,
|
/// A character for the core Chronicles of Darkness rules.
|
||||||
ChangelingV1,
|
ChroniclesOfDarkness,
|
||||||
|
|
||||||
|
/// A character for Changeling 2E rules.
|
||||||
|
Changeling,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 => {
|
ChroniclesOfDarkness | Changeling => {
|
||||||
let sheet = CofdSheet::default();
|
let sheet = CofdSheet::default_sheet(*self);
|
||||||
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
|
|
||||||
sheet.encode(&mut buf)?;
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
ChangelingV1 => {
|
|
||||||
let mut sheet = ChangelingSheet::default();
|
|
||||||
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
|
||||||
|
@ -69,11 +75,20 @@ 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
|
||||||
/// in order of table columns.
|
/// in order of table columns.
|
||||||
#[derive(Serialize, Debug, Queryable, Identifiable, AsChangeset)]
|
#[derive(Serialize, Debug, sqlx::FromRow)]
|
||||||
pub struct Character {
|
pub struct Character {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
|
@ -112,8 +127,8 @@ impl Character {
|
||||||
pub fn dyn_deserialize(&self) -> Result<Box<DynCharacterData>, Error> {
|
pub fn dyn_deserialize(&self) -> Result<Box<DynCharacterData>, Error> {
|
||||||
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 {
|
||||||
ChroniclesOfDarknessV1 => Box::new(self.try_deserialize::<CofdSheet>()?),
|
ChroniclesOfDarkness => Box::new(self.try_deserialize::<CofdSheet>()?),
|
||||||
ChangelingV1 => Box::new(self.try_deserialize::<ChangelingSheet>()?),
|
Changeling => Box::new(self.try_deserialize::<CofdSheet>()?),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(decoded)
|
Ok(decoded)
|
||||||
|
@ -131,8 +146,8 @@ impl Character {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the existing character with new serialized protobuf
|
/// Update the existing character with new serialized protobuf
|
||||||
/// data. Consumes the data.
|
/// data.
|
||||||
pub fn update_data<T>(&mut self, data: T) -> Result<(), Error>
|
pub fn update_data<T>(&mut self, data: &T) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
T: prost::Message + std::default::Default,
|
T: prost::Message + std::default::Default,
|
||||||
{
|
{
|
||||||
|
@ -145,7 +160,7 @@ impl Character {
|
||||||
|
|
||||||
/// Same as regular character type, but without the actual protobuf
|
/// Same as regular character type, but without the actual protobuf
|
||||||
/// data loaded into memory.
|
/// data loaded into memory.
|
||||||
#[derive(Serialize, Debug, Queryable)]
|
#[derive(Serialize, Debug, sqlx::FromRow)]
|
||||||
pub struct StrippedCharacter {
|
pub struct StrippedCharacter {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
|
@ -167,13 +182,11 @@ impl Visibility for StrippedCharacter {
|
||||||
|
|
||||||
/// Represents insert of a new character into the database. Property
|
/// Represents insert of a new character into the database. Property
|
||||||
/// names correspond to columns.
|
/// names correspond to columns.
|
||||||
#[derive(Insertable)]
|
pub struct NewCharacter<'a> {
|
||||||
#[table_name = "characters"]
|
|
||||||
pub struct NewCharacter {
|
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub viewable: bool,
|
pub viewable: bool,
|
||||||
pub character_name: String,
|
pub character_name: &'a str,
|
||||||
pub data_type: CharacterDataType,
|
pub data_type: CharacterDataType,
|
||||||
pub data_version: i32,
|
pub data_version: i32,
|
||||||
pub data: Vec<u8>,
|
pub data: &'a [u8],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use super::characters::CharacterDataType;
|
use super::characters::CharacterDataType;
|
||||||
use rocket::http::RawStr;
|
use rocket::http::RawStr;
|
||||||
use rocket::request::FromFormValue;
|
use rocket::request::FromFormValue;
|
||||||
|
use serde::Serialize;
|
||||||
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")]
|
||||||
|
|
|
@ -1,36 +1,21 @@
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
|
use prost::bytes::BytesMut;
|
||||||
use rocket::data::{Data, FromData, Outcome, ToByteUnit};
|
use rocket::data::{Data, FromData, Outcome, ToByteUnit};
|
||||||
|
use rocket::http::{ContentType, Status};
|
||||||
use rocket::request::Request;
|
use rocket::request::Request;
|
||||||
|
use rocket::response::status;
|
||||||
|
use rocket::response::{self, Responder, Response};
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::ops::Deref;
|
use std::io::Cursor;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
/// 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
|
||||||
/// dereferenced into its wrapped type.
|
/// dereferenced into its wrapped type.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Proto<T>(T)
|
pub(crate) struct Proto<T>(pub T)
|
||||||
where
|
where
|
||||||
T: prost::Message + Default;
|
T: prost::Message + Default;
|
||||||
|
|
||||||
|
@ -43,8 +28,17 @@ where
|
||||||
{
|
{
|
||||||
type Error = crate::errors::Error;
|
type Error = crate::errors::Error;
|
||||||
|
|
||||||
async fn from_data(_req: &Request<'_>, data: Data) -> Outcome<Self, Error> {
|
async fn from_data(req: &Request<'_>, data: Data) -> Outcome<Self, Error> {
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
let content_type = req.content_type();
|
||||||
|
|
||||||
|
let is_protobuf = content_type
|
||||||
|
.map(|ct| ct.top() == "application" && ct.sub() == "x-protobuf")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !is_protobuf {
|
||||||
|
return Outcome::Failure((Status::new(422, "invalid protobuf"), Error::InvalidInput));
|
||||||
|
}
|
||||||
|
|
||||||
let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await {
|
let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await {
|
||||||
Ok(read_bytes) => read_bytes,
|
Ok(read_bytes) => read_bytes,
|
||||||
|
@ -58,6 +52,23 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'r, T> Responder<'r, 'static> for Proto<T>
|
||||||
|
where
|
||||||
|
T: prost::Message + Default,
|
||||||
|
{
|
||||||
|
fn respond_to(self, req: &Request) -> response::Result<'static> {
|
||||||
|
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&self.0));
|
||||||
|
match self.0.encode(&mut buf) {
|
||||||
|
Ok(_) => Response::build()
|
||||||
|
.header(ContentType::new("application", "x-protobuf"))
|
||||||
|
.sized_body(buf.len(), Cursor::new(buf))
|
||||||
|
.ok(),
|
||||||
|
Err(e) => status::Custom(Status::InternalServerError, e.to_string()).respond_to(req),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
@ -68,3 +79,12 @@ where
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Proto<T>
|
||||||
|
where
|
||||||
|
T: prost::Message + Default,
|
||||||
|
{
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
//! Contains the generated Chronicles of Darkness-related protocol
|
||||||
|
//! 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.
|
||||||
|
//include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs"));
|
||||||
|
tonic::include_proto!("models.proto.cofd");
|
||||||
|
|
||||||
|
//Add the API protobuf genreated code for the api module.
|
||||||
|
pub mod api {
|
||||||
|
//include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs"));
|
||||||
|
tonic::include_proto!("models.proto.cofd.api");
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// Trait to extract values out of a CharacterIdentifier whose
|
||||||
|
/// instance may or may not exist on an API request.
|
||||||
|
pub trait DefaultCharacterIdentifier {
|
||||||
|
/// Retrieve the specified owner, or a default fallback (empty
|
||||||
|
/// string). Will not allocate if the owner is present in the
|
||||||
|
/// request.
|
||||||
|
fn owner(&self) -> Cow<'_, str>;
|
||||||
|
|
||||||
|
/// Retrieve the character ID specified, or the default i32
|
||||||
|
/// value (0).
|
||||||
|
fn id(&self) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultCharacterIdentifier for Option<CharacterIdentifier> {
|
||||||
|
fn owner(&self) -> Cow<'_, str> {
|
||||||
|
self.as_ref()
|
||||||
|
.map(|ident| Cow::from(&ident.owner))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> i32 {
|
||||||
|
self.as_ref().map(|ident| ident.id).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helpers for the ApiResult class.
|
||||||
|
impl ApiResult {
|
||||||
|
pub fn success() -> Self {
|
||||||
|
ApiResult {
|
||||||
|
success: true,
|
||||||
|
error: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default mental skill names for a regular Chronicles of Darkness
|
||||||
|
/// (or derivative system) game.
|
||||||
|
const MENTAL_SKILLS: &'static [&'static str] = &[
|
||||||
|
"Academics",
|
||||||
|
"Computer",
|
||||||
|
"Crafts",
|
||||||
|
"Investigation",
|
||||||
|
"Medicine",
|
||||||
|
"Occult",
|
||||||
|
"Politics",
|
||||||
|
"Science",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Default physical skill names for a regular Chronicles of Darkness
|
||||||
|
/// (or derivative system) game.
|
||||||
|
const PHYSICAL_SKILLS: &'static [&'static str] = &[
|
||||||
|
"Athletics",
|
||||||
|
"Brawl",
|
||||||
|
"Drive",
|
||||||
|
"Firearms",
|
||||||
|
"Larceny",
|
||||||
|
"Stealth",
|
||||||
|
"Survival",
|
||||||
|
"Weaponry",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Default social skill names for a regular Chronicles of Darkness
|
||||||
|
/// (or derivative system) game.
|
||||||
|
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 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);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::db::{Dao, TenebrousDbConn};
|
use crate::db::{Dao, TenebrousDbConn};
|
||||||
use crate::schema::users;
|
|
||||||
use argon2::{self, Config, Error as ArgonError};
|
use argon2::{self, Config, Error as ArgonError};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rocket::outcome::IntoOutcome;
|
use rocket::outcome::IntoOutcome;
|
||||||
use rocket::request::{self, FromRequest, Request};
|
use rocket::request::{self, FromRequest, Request};
|
||||||
use serde_derive::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
|
pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
|
||||||
let salt = rand::thread_rng().gen::<[u8; 16]>();
|
let salt = rand::thread_rng().gen::<[u8; 16]>();
|
||||||
|
@ -12,7 +11,7 @@ pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
|
||||||
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
|
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Serialize, Debug, Queryable)]
|
#[derive(Eq, PartialEq, Serialize, Debug, sqlx::FromRow)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
@ -25,10 +24,11 @@ impl User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn, id: i32) -> Option<User> {
|
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn<'a>, id: i32) -> Option<User> {
|
||||||
db.load_user_by_id(id).await.ok().flatten()
|
db.load_user_by_id(id).await.ok().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait implementation to get the logged in user.
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
|
impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
@ -53,9 +53,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
pub struct NewUser<'a> {
|
||||||
#[table_name = "users"]
|
pub username: &'a str,
|
||||||
pub struct NewUser {
|
pub password: &'a str,
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +1,43 @@
|
||||||
use crate::db::{Dao, TenebrousDbConn};
|
use crate::db::{Dao, TenebrousDbConn};
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
use crate::models::characters::Character;
|
||||||
use crate::models::proto::{cofd::*, Proto};
|
|
||||||
use crate::models::users::User;
|
use crate::models::users::User;
|
||||||
use rocket_contrib::templates::Template;
|
|
||||||
use serde::Serialize;
|
mod cofd;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub(crate) fn routes() -> Vec<rocket::Route> {
|
pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||||
routes![
|
routes![
|
||||||
cofd::update_basic_info,
|
cofd::update_basic_info,
|
||||||
cofd::update_attributes,
|
cofd::update_attribute_value,
|
||||||
cofd::update_attribute,
|
cofd::update_skill,
|
||||||
cofd::update_skills,
|
cofd::update_skill_value,
|
||||||
|
cofd::update_merits,
|
||||||
|
cofd::update_items,
|
||||||
cofd::add_condition,
|
cofd::add_condition,
|
||||||
cofd::remove_condition
|
cofd::remove_condition
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Protobuf-based REST endpoints for editing a character.
|
/// Load the character belonging to the given user, as long as they're
|
||||||
mod cofd {
|
/// the owner of that character. Returns an error if user is not
|
||||||
use super::*;
|
/// logged in, the owner of the character is not found, or the logged
|
||||||
use crate::models::proto::{cofd::api::*, cofd::*, Proto};
|
/// in user does not have the permission to access this character.
|
||||||
|
async fn load_character(
|
||||||
#[post("/cofd/<owner>/<character_id>/basic-info", data = "<info>")]
|
conn: &TenebrousDbConn<'_>,
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[patch("/cofd/<owner>/<character_id>/attributes", data = "<attr_update>")]
|
|
||||||
pub(super) async fn update_attribute<'a>(
|
|
||||||
owner: String,
|
|
||||||
character_id: i32,
|
|
||||||
attr_update: Proto<Attribute>,
|
|
||||||
conn: TenebrousDbConn,
|
|
||||||
logged_in_user: Option<&User>,
|
logged_in_user: Option<&User>,
|
||||||
) -> Result<&'a str, Error> {
|
owner: &str,
|
||||||
|
character_id: i32,
|
||||||
|
) -> Result<Character, Error> {
|
||||||
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
||||||
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
|
|
||||||
let mut character: Character = conn
|
let character: Character = conn
|
||||||
.load_character(character_id)
|
.load_character(character_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::NotFound)?;
|
.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
if logged_in_user != &owner {
|
if &logged_in_user.username != owner {
|
||||||
return Err(Error::NoPermission);
|
return Err(Error::NoPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sheet: CofdSheet = character.try_deserialize()?;
|
Ok(character)
|
||||||
|
|
||||||
match attr_update.name.to_lowercase().as_ref() {
|
|
||||||
"strength" => Ok(sheet.strength += attr_update.value),
|
|
||||||
"dexterity" => Ok(sheet.dexterity += attr_update.value),
|
|
||||||
"stamina" => Ok(sheet.stamina += attr_update.value),
|
|
||||||
"intelligence" => Ok(sheet.intelligence += attr_update.value),
|
|
||||||
"wits" => Ok(sheet.wits += attr_update.value),
|
|
||||||
"resolve" => Ok(sheet.resolve += attr_update.value),
|
|
||||||
"presence" => Ok(sheet.presence += attr_update.value),
|
|
||||||
"manipulation" => Ok(sheet.manipulation += attr_update.value),
|
|
||||||
"composure" => Ok(sheet.composure += attr_update.value),
|
|
||||||
_ => Err(Error::InvalidInput),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"updated {} attribute {} to {}",
|
|
||||||
character.character_name, attr_update.name, attr_update.value
|
|
||||||
);
|
|
||||||
|
|
||||||
character.update_data(sheet)?;
|
|
||||||
conn.update_character_sheet(character).await?;
|
|
||||||
Ok("lol")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/cofd/<owner>/<character_id>/skills", data = "<info>")]
|
|
||||||
pub(super) fn update_skills<'a>(
|
|
||||||
owner: String,
|
|
||||||
character_id: i32,
|
|
||||||
info: Proto<Skills>,
|
|
||||||
conn: TenebrousDbConn,
|
|
||||||
) -> &'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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
use super::load_character;
|
||||||
|
use crate::db::{Dao, TenebrousDbConn};
|
||||||
|
use crate::errors::Error;
|
||||||
|
use crate::models::characters::Character;
|
||||||
|
use crate::models::proto::cofd::api::DefaultCharacterIdentifier;
|
||||||
|
use crate::models::proto::cofd::cofd_sheet::Skill;
|
||||||
|
use crate::models::proto::cofd::*;
|
||||||
|
use crate::models::proto::{cofd::api::*, cofd::*, Proto};
|
||||||
|
use crate::models::users::User;
|
||||||
|
use std::collections::btree_map::{Entry, OccupiedEntry};
|
||||||
|
|
||||||
|
fn find_skill_entry<'a>(
|
||||||
|
sheet: &'a mut CofdSheet,
|
||||||
|
skill_name: &'a str,
|
||||||
|
) -> Option<OccupiedEntry<'a, String, Skill>> {
|
||||||
|
let all_skills = vec![
|
||||||
|
&mut sheet.mental_skills,
|
||||||
|
&mut sheet.physical_skills,
|
||||||
|
&mut sheet.social_skills,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Search all skill lists for this value using "workaround" to
|
||||||
|
// break value from for loops.
|
||||||
|
let skill: Option<OccupiedEntry<_, _>> = 'l: loop {
|
||||||
|
for skill_map in all_skills {
|
||||||
|
if let Entry::Occupied(entry) = skill_map.entry(skill_name.to_owned()) {
|
||||||
|
break 'l Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break None;
|
||||||
|
};
|
||||||
|
|
||||||
|
skill
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_skill<'a>(sheet: &'a mut CofdSheet, skill_name: &'a str) -> Option<&'a mut Skill> {
|
||||||
|
find_skill_entry(sheet, skill_name).map(|entry| entry.into_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/rpc/cofd/update_basic_info", data = "<req>")]
|
||||||
|
pub(super) async fn update_basic_info<'a>(
|
||||||
|
req: Proto<UpdateBasicInfoRequest>,
|
||||||
|
conn: TenebrousDbConn<'_>,
|
||||||
|
user: Option<&User>,
|
||||||
|
) -> Result<Proto<ApiResult>, Error> {
|
||||||
|
let mut character =
|
||||||
|
load_character(&conn, user, &req.character.owner(), req.character.id()).await?;
|
||||||
|
|
||||||
|
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||||
|
|
||||||
|
println!("name will now be {}", req.name);
|
||||||
|
character.character_name = req.name.clone(); //Should probably remove name from the sheet?
|
||||||
|
sheet.name = req.name.clone();
|
||||||
|
sheet.gender = req.gender.clone();
|
||||||
|
sheet.concept = req.concept.clone();
|
||||||
|
sheet.chronicle = req.chronicle.clone();
|
||||||
|
sheet.age = req.age;
|
||||||
|
|
||||||
|
character.update_data(&sheet)?;
|
||||||
|
conn.update_character(&character).await?;
|
||||||
|
println!("Updated basic info");
|
||||||
|
Ok(Proto(ApiResult::success()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/rpc/cofd/update_attribute_value", data = "<req>")]
|
||||||
|
pub(super) async fn update_attribute_value(
|
||||||
|
req: Proto<UpdateAttributeRequest>,
|
||||||
|
conn: TenebrousDbConn<'_>,
|
||||||
|
logged_in_user: Option<&User>,
|
||||||
|
) -> Result<Proto<ApiResult>, Error> {
|
||||||
|
let mut character = load_character(
|
||||||
|
&conn,
|
||||||
|
logged_in_user,
|
||||||
|
&req.character.owner(),
|
||||||
|
req.character.id(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||||
|
let value = req.attribute_value;
|
||||||
|
match req.attribute_name.to_lowercase().as_ref() {
|
||||||
|
"strength" => Ok(sheet.strength = value),
|
||||||
|
"dexterity" => Ok(sheet.dexterity = value),
|
||||||
|
"stamina" => Ok(sheet.stamina = value),
|
||||||
|
"intelligence" => Ok(sheet.intelligence = value),
|
||||||
|
"wits" => Ok(sheet.wits = value),
|
||||||
|
"resolve" => Ok(sheet.resolve = value),
|
||||||
|
"presence" => Ok(sheet.presence = value),
|
||||||
|
"manipulation" => Ok(sheet.manipulation = value),
|
||||||
|
"composure" => Ok(sheet.composure = value),
|
||||||
|
_ => Err(Error::InvalidInput),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
character.update_data(&sheet)?;
|
||||||
|
conn.update_character_sheet(&character).await?;
|
||||||
|
Ok(Proto(ApiResult {
|
||||||
|
success: true,
|
||||||
|
error: "".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/rpc/cofd/update_skill", data = "<skill_update>")]
|
||||||
|
pub(super) async fn update_skill<'a>(
|
||||||
|
skill_update: Proto<UpdateSkillRequest>,
|
||||||
|
conn: TenebrousDbConn<'_>,
|
||||||
|
logged_in_user: Option<&User>,
|
||||||
|
) -> Result<&'a str, Error> {
|
||||||
|
let mut character = load_character(
|
||||||
|
&conn,
|
||||||
|
logged_in_user,
|
||||||
|
&skill_update.character.owner(),
|
||||||
|
skill_update.character.id(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||||
|
let updated_skill: &Skill = skill_update.skill.as_ref().ok_or(Error::InvalidInput)?;
|
||||||
|
let skill_entry = find_skill_entry(&mut sheet, &updated_skill.name);
|
||||||
|
|
||||||
|
skill_entry
|
||||||
|
.map(|mut entry| entry.insert(updated_skill.clone()))
|
||||||
|
.ok_or(Error::InvalidInput)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"updated skill {} with {:?}",
|
||||||
|
updated_skill.name, skill_update.skill
|
||||||
|
);
|
||||||
|
|
||||||
|
character.update_data(&sheet)?;
|
||||||
|
conn.update_character_sheet(&character).await?;
|
||||||
|
Ok("lol")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/rpc/cofd/update_skill_value", data = "<req>")]
|
||||||
|
pub(super) async fn update_skill_value<'a>(
|
||||||
|
req: Proto<UpdateSkillValueRequest>,
|
||||||
|
conn: TenebrousDbConn<'_>,
|
||||||
|
logged_in_user: Option<&User>,
|
||||||
|
) -> Result<Proto<ApiResult>, Error> {
|
||||||
|
let mut character = load_character(
|
||||||
|
&conn,
|
||||||
|
logged_in_user,
|
||||||
|
&req.character.owner(),
|
||||||
|
req.character.id(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||||
|
|
||||||
|
let mut skill: Option<&mut Skill> = find_skill(&mut sheet, &req.skill_name);
|
||||||
|
if let Some(ref mut s) = skill {
|
||||||
|
s.dots = req.skill_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("updated skill value");
|
||||||
|
|
||||||
|
character.update_data(&sheet)?;
|
||||||
|
conn.update_character_sheet(&character).await?;
|
||||||
|
Ok(Proto(ApiResult {
|
||||||
|
success: true,
|
||||||
|
error: "".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/rpc/cofd/update_merits", data = "<req>")]
|
||||||
|
pub(super) async fn update_merits<'a>(
|
||||||
|
req: Proto<UpdateMeritsRequest>,
|
||||||
|
conn: TenebrousDbConn<'_>,
|
||||||
|
logged_in_user: Option<&User>,
|
||||||
|
) -> Result<Proto<ApiResult>, Error> {
|
||||||
|
let mut character = load_character(
|
||||||
|
&conn,
|
||||||
|
logged_in_user,
|
||||||
|
&req.character.owner(),
|
||||||
|
req.character.id(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||||
|
sheet.merits = req.merits.clone();
|
||||||
|
|
||||||
|
println!("updated merits");
|
||||||
|
|
||||||
|
character.update_data(&sheet)?;
|
||||||
|
conn.update_character_sheet(&character).await?;
|
||||||
|
Ok(Proto(ApiResult::success()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/rpc/cofd/update_items", data = "<req>")]
|
||||||
|
pub(super) async fn update_items<'a>(
|
||||||
|
req: Proto<UpdateItemsRequest>,
|
||||||
|
conn: TenebrousDbConn<'_>,
|
||||||
|
logged_in_user: Option<&User>,
|
||||||
|
) -> Result<Proto<ApiResult>, Error> {
|
||||||
|
let mut character = load_character(
|
||||||
|
&conn,
|
||||||
|
logged_in_user,
|
||||||
|
&req.character.owner(),
|
||||||
|
req.character.id(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||||
|
sheet.items = req.items.clone();
|
||||||
|
|
||||||
|
println!("updated items");
|
||||||
|
|
||||||
|
character.update_data(&sheet)?;
|
||||||
|
conn.update_character_sheet(&character).await?;
|
||||||
|
Ok(Proto(ApiResult::success()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/rpc/cofd/add_condition", data = "<info>")]
|
||||||
|
pub(super) fn add_condition<'a>(info: Proto<AddConditionRequest>) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/rpc/cofd/remove_condition", data = "<info>")]
|
||||||
|
pub(super) fn remove_condition<'a>(info: Proto<RemoveConditionRequest>) -> &'a str {
|
||||||
|
"lol"
|
||||||
|
}
|
|
@ -50,10 +50,10 @@ fn registration_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
|
||||||
async fn login(
|
async fn login(
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
login: Form<Login>,
|
login: Form<Login>,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn<'_>,
|
||||||
) -> Result<Redirect, Flash<Redirect>> {
|
) -> Result<Redirect, Flash<Redirect>> {
|
||||||
let user = conn
|
let user = conn
|
||||||
.load_user(login.username.clone())
|
.load_user(&login.username)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("login - error loading user user: {}", e);
|
error!("login - error loading user user: {}", e);
|
||||||
|
@ -104,12 +104,9 @@ fn register_page(flash: Option<FlashMessage>) -> Template {
|
||||||
async fn register(
|
async fn register(
|
||||||
mut cookies: &CookieJar<'_>,
|
mut cookies: &CookieJar<'_>,
|
||||||
registration: Form<Registration>,
|
registration: Form<Registration>,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn<'_>,
|
||||||
) -> Result<Redirect, Flash<Redirect>> {
|
) -> Result<Redirect, Flash<Redirect>> {
|
||||||
let existing_user = conn
|
let existing_user = conn.load_user(®istration.username).await.map_err(|e| {
|
||||||
.load_user(registration.username.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("registration - error loading existing user: {}", e);
|
error!("registration - error loading existing user: {}", e);
|
||||||
registration_error_redirect("There was an error attempting to register.")
|
registration_error_redirect("There was an error attempting to register.")
|
||||||
})?;
|
})?;
|
||||||
|
@ -127,8 +124,8 @@ async fn register(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let user = NewUser {
|
let user = NewUser {
|
||||||
username: registration.username.clone(),
|
username: ®istration.username,
|
||||||
password: hashed_pw,
|
password: &hashed_pw,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = conn.insert_user(user).await.map_err(|e| {
|
let user = conn.insert_user(user).await.map_err(|e| {
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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 serde::Serialize;
|
||||||
|
|
||||||
mod edit;
|
mod edit;
|
||||||
mod new;
|
mod new;
|
||||||
|
@ -37,10 +38,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)
|
||||||
ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
|
} else {
|
||||||
ChangelingV1 => Template::render("characters/view_changeling_character", context),
|
return Err(Error::InvalidInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(template)
|
Ok(template)
|
||||||
|
@ -50,10 +51,10 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
|
||||||
async fn view_character(
|
async fn view_character(
|
||||||
character_id: i32,
|
character_id: i32,
|
||||||
username: String,
|
username: String,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn<'_>,
|
||||||
logged_in_user: Option<&User>,
|
logged_in_user: Option<&User>,
|
||||||
) -> Result<Template, Error> {
|
) -> Result<Template, Error> {
|
||||||
let user = &conn.load_user(username).await?.ok_or(Error::NotFound)?;
|
let user = &conn.load_user(&username).await?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
let character = conn
|
let character = conn
|
||||||
.load_character(character_id)
|
.load_character(character_id)
|
||||||
|
|
|
@ -10,6 +10,7 @@ use strum::IntoEnumIterator;
|
||||||
struct EditCharacterContext<'a> {
|
struct EditCharacterContext<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub username: &'a str,
|
pub username: &'a str,
|
||||||
|
pub id: i32,
|
||||||
pub data_type: &'a CharacterDataType,
|
pub data_type: &'a CharacterDataType,
|
||||||
pub sheet: Box<DynCharacterData>,
|
pub sheet: Box<DynCharacterData>,
|
||||||
pub state: FormStateContext<'a>,
|
pub state: FormStateContext<'a>,
|
||||||
|
@ -20,14 +21,37 @@ 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,
|
||||||
|
id: character.id,
|
||||||
|
data_type: &character.data_type,
|
||||||
|
sheet: character.dyn_deserialize()?,
|
||||||
|
state: FormStateContext {
|
||||||
|
selected_system: &character.data_type,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let template = if character.data_type.is_cofd_system() {
|
||||||
|
Template::render("characters/edit_character", context)
|
||||||
|
} else {
|
||||||
|
return Err(Error::InvalidInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
owner: String,
|
owner: String,
|
||||||
logged_in_user: Option<&User>,
|
logged_in_user: Option<&User>,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn<'_>,
|
||||||
) -> Result<Template, Error> {
|
) -> Result<Template, Error> {
|
||||||
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
|
let owner = conn.load_user(&owner).await?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
||||||
let character = conn
|
let character = conn
|
||||||
|
@ -39,15 +63,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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::models::{
|
||||||
};
|
};
|
||||||
use rocket::{request::Form, response::Redirect};
|
use rocket::{request::Form, response::Redirect};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
|
use serde::Serialize;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
/// Form submission for creating a new character.
|
/// Form submission for creating a new character.
|
||||||
|
@ -32,7 +33,7 @@ impl NewCharacterContext {
|
||||||
NewCharacterContext {
|
NewCharacterContext {
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
error_message: "".to_string(),
|
error_message: "".to_string(),
|
||||||
selected_system: CharacterDataType::ChroniclesOfDarknessV1,
|
selected_system: CharacterDataType::ChroniclesOfDarkness,
|
||||||
systems: CharacterDataType::iter().collect(),
|
systems: CharacterDataType::iter().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ impl NewCharacterContext {
|
||||||
let system: CharacterDataType = *form
|
let system: CharacterDataType = *form
|
||||||
.system
|
.system
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&CharacterDataType::ChroniclesOfDarknessV1);
|
.unwrap_or(&CharacterDataType::ChroniclesOfDarkness);
|
||||||
|
|
||||||
NewCharacterContext {
|
NewCharacterContext {
|
||||||
name: form.name.clone(),
|
name: form.name.clone(),
|
||||||
|
@ -59,18 +60,18 @@ impl NewCharacterContext {
|
||||||
async fn create_new_character(
|
async fn create_new_character(
|
||||||
form: &Form<NewCharacterForm>,
|
form: &Form<NewCharacterForm>,
|
||||||
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,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
character_name: form.name.clone(),
|
character_name: &form.name,
|
||||||
data_type: system,
|
data_type: system,
|
||||||
data_version: 1,
|
data_version: 1,
|
||||||
data: sheet,
|
data: &sheet,
|
||||||
};
|
};
|
||||||
|
|
||||||
conn.insert_character(insert).await?;
|
conn.insert_character(insert).await?;
|
||||||
|
@ -97,7 +98,7 @@ pub(super) fn new_character_page(_logged_in_user: &User) -> Result<Template, Err
|
||||||
pub(super) async fn new_character_submit(
|
pub(super) async fn new_character_submit(
|
||||||
form: Form<NewCharacterForm>,
|
form: Form<NewCharacterForm>,
|
||||||
logged_in_user: &User,
|
logged_in_user: &User,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn<'_>,
|
||||||
) -> Result<Redirect, Template> {
|
) -> Result<Redirect, Template> {
|
||||||
if let Err(e) = &form.system {
|
if let Err(e) = &form.system {
|
||||||
return Err(render_error(&form, e.to_string().clone()));
|
return Err(render_error(&form, e.to_string().clone()));
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::models::characters::Visibility;
|
||||||
use crate::models::{characters::StrippedCharacter, users::User};
|
use crate::models::{characters::StrippedCharacter, users::User};
|
||||||
use rocket::response::Redirect;
|
use rocket::response::Redirect;
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
use serde_derive::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![index, user_index, proto_test]
|
routes![index, user_index, proto_test]
|
||||||
|
@ -18,7 +18,7 @@ pub struct UserHomeContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
|
async fn user_index(user: &User, conn: TenebrousDbConn<'_>) -> Result<Template, Error> {
|
||||||
let characters: Vec<StrippedCharacter> = conn
|
let characters: Vec<StrippedCharacter> = conn
|
||||||
.load_character_list(user.id)
|
.load_character_list(user.id)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use crate::models::characters::*;
|
|
||||||
|
|
||||||
characters (id) {
|
|
||||||
id -> Integer,
|
|
||||||
user_id -> Integer,
|
|
||||||
viewable -> Bool,
|
|
||||||
character_name -> Text,
|
|
||||||
data_type -> CharacterDataTypeMapping,
|
|
||||||
data_version -> Integer,
|
|
||||||
data -> Binary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use crate::models::characters::*;
|
|
||||||
|
|
||||||
users (id) {
|
|
||||||
id -> Integer,
|
|
||||||
username -> Text,
|
|
||||||
password -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
|
||||||
characters,
|
|
||||||
users,
|
|
||||||
);
|
|
|
@ -1,30 +0,0 @@
|
||||||
function makeAPI(root) {
|
|
||||||
const Attribute = root.lookupType("models.proto.cofd.api.Attribute");
|
|
||||||
|
|
||||||
const attributesResource = (username, characterID) =>
|
|
||||||
'/api/cofd/' + username + '/' + characterID + '/attributes';
|
|
||||||
|
|
||||||
async function updateAttribute(params) {
|
|
||||||
const { username, characterID, attribute, newValue } = params;
|
|
||||||
|
|
||||||
let req = Attribute.create({
|
|
||||||
name: attribute,
|
|
||||||
value: parseInt(newValue)
|
|
||||||
});
|
|
||||||
|
|
||||||
const resource = attributesResource(username, characterID);
|
|
||||||
|
|
||||||
let resp = await fetch(resource, {
|
|
||||||
method: 'PATCH',
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
(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);
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,76 +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;
|
|
||||||
}
|
|
||||||
</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 %}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% 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,11 +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>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,15 +0,0 @@
|
||||||
{% extends "base" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Core Sheet</h1>
|
|
||||||
<div>
|
|
||||||
<h1>Character {{name}}</h1>
|
|
||||||
<h3>User: {{username}}</h3>
|
|
||||||
<p>System: {{data_type}}</h3>
|
|
||||||
<p>Strength: {{sheet.strength}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"target": "es5",
|
||||||
|
"removeComments": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"stripInternal": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"lib": [
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const CompressionPlugin = require("compression-webpack-plugin");
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '.');
|
||||||
|
|
||||||
|
function packPage(page, chunks) {
|
||||||
|
if (!chunks) chunks = [];
|
||||||
|
return new HtmlWebpackPlugin({
|
||||||
|
template: `${root}/src/frontend/templates/${page}`,
|
||||||
|
filename: `${root}/generated/templates/${page}`,
|
||||||
|
publicPath: '/',
|
||||||
|
scriptLoading: 'defer',
|
||||||
|
chunks: chunks,
|
||||||
|
inject: false,
|
||||||
|
minify: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
edit_character: "./src/frontend/scripts/characters/edit.ts",
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
runtimeChunk: "single",
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mode: "development",
|
||||||
|
output: {
|
||||||
|
path: `${root}/generated`,
|
||||||
|
filename: 'scripts/dist/[name].bundle.js'
|
||||||
|
},
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
include: /src|_proto/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: "ts-loader"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js"]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin({
|
||||||
|
cleanOnceBeforeBuildPatterns: ['!templates/**/*'],
|
||||||
|
cleanAfterEveryBuildPatterns: ['!templates/**/*'],
|
||||||
|
dry: false
|
||||||
|
}),
|
||||||
|
new CompressionPlugin(),
|
||||||
|
packPage('login.html.tera'),
|
||||||
|
packPage('base.html.tera'),
|
||||||
|
packPage('error.html.tera'),
|
||||||
|
packPage('index.html.tera'),
|
||||||
|
packPage('registration.html.tera'),
|
||||||
|
packPage('characters/edit_character.html.tera', ['edit_character']),
|
||||||
|
packPage('characters/edit_character_macros.html.tera'),
|
||||||
|
packPage('characters/new_character.html.tera'),
|
||||||
|
packPage('characters/view_character.html.tera'),
|
||||||
|
packPage('characters/view_character_macros.html.tera'),
|
||||||
|
]
|
||||||
|
};
|
Loading…
Reference in New Issue