i did some stuff, honestly it starts to build pretty well if i might sayy
This commit is contained in:
parent
22319e84a1
commit
9189d9cd88
22 changed files with 689 additions and 176 deletions
7
assets/pf2r/consumable/alcohol.json
Normal file
7
assets/pf2r/consumable/alcohol.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
name: "Alcohol",
|
||||
traits: [ "alchemical", "consumable", "drug", "ingested", "poison" ],
|
||||
price: 1,
|
||||
bulk: 0,
|
||||
desc: "Alcohol! what's more to say? dont forget to make a saving throw if DC 12 Fortitude",
|
||||
}
|
6
assets/pf2r/spell/phase_bolt.json
Normal file
6
assets/pf2r/spell/phase_bolt.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
name: "Phase Bolt",
|
||||
actions: 2,
|
||||
damage: "3d4",
|
||||
damage_type: "piercing"
|
||||
}
|
8
assets/pf2r/weapon/dagger.json
Normal file
8
assets/pf2r/weapon/dagger.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Dagger",
|
||||
"two_handed": false,
|
||||
"one_handed": true,
|
||||
"melee_reach": 1,
|
||||
"ranged_reach": 10,
|
||||
"damage": "1d4"
|
||||
}
|
8
assets/pf2r/weapon/katar.json
Normal file
8
assets/pf2r/weapon/katar.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
name: "Katar",
|
||||
two_handed: false,
|
||||
one_handed: true,
|
||||
melee_reach: 1,
|
||||
ranged_reach: 0,
|
||||
damage: "1d4"
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
Sheet -> Title Definitions;
|
||||
Title -> '$' Name | [a-zA-Z0-9_\ ]+ | epsilon
|
||||
// Title is either a variable whose value will be the title, constant text or none
|
||||
// in the case of none, the first var will be the title
|
||||
Definitions -> Definitions Definition | epsilon;
|
||||
Definition -> Name ':' Type Requirements;
|
||||
Name -> [a-zA-Z]+;
|
||||
Type -> BOOL | INT | TEXT | TEXT '(' [0-9]+ ')' | EXP;
|
||||
// ^^^^^ num of lines
|
||||
EXP -> TERM '+' FACTOR | TERM '-' FACTOR;
|
||||
TERM -> FACTOR '*' FACTOR | FACTOR '/' FACTOR;
|
||||
FACTOR -> '(' EXP ')' | [0-9]+ | '$' Name(of type INT/BOOL);
|
||||
// $Name of type bool will result in True = 1/False = 0
|
||||
Requirements -> '|' Condition Requirements | epsilon;
|
||||
Condition -> EXP RELOP EXP | '$' Name(of type BOOL);
|
||||
RELOP -> (>|<|>=|<=|==|!=);
|
|
@ -1,2 +1,122 @@
|
|||
<button onclick="onClick()", id="login_button">Click me!, also open terminal</button>
|
||||
<script src='socket.js'></script>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="./socket.js"></script>
|
||||
<script>
|
||||
// init - game view (map)
|
||||
var mapScale = 1.0;
|
||||
var mapOffsetX = 0.0;
|
||||
var mapOffsetY = 0.0;
|
||||
function init() {
|
||||
let view = document.getElementById('game-view');
|
||||
view.onwheel = onGameViewScroll;
|
||||
view.onclick = (e) => console.log('click', e);
|
||||
view.onauxclick = (e) => console.log(e);
|
||||
view.onmousemove = onGameMouseMove;
|
||||
view.oncontextmenu = () => false;
|
||||
}
|
||||
|
||||
function onLoginClick() {
|
||||
let username = document.getElementById('login-username').value;
|
||||
let pass = document.getElementById('login-pass').value;
|
||||
tavern.login(username, pass);
|
||||
}
|
||||
tavern.onlogin = (s) => {
|
||||
console.log(s);
|
||||
if(s) {
|
||||
let login = document.getElementById('login-screen');
|
||||
let game = document.getElementById('game');
|
||||
login.style.display = 'none';
|
||||
game.style.display = 'flex';
|
||||
}
|
||||
else {
|
||||
alert("Invalid username or password!");
|
||||
}
|
||||
}
|
||||
function onGameViewScroll(event) {
|
||||
let map = document.getElementById('map');
|
||||
mapScale += (event.wheelDelta / 1800.0);
|
||||
if(mapScale < 0.1) { mapScale = 0.1; }
|
||||
map.style.transform = `scale(${mapScale})`;
|
||||
}
|
||||
function onGameMouseMove(event) {
|
||||
if(event.buttons == 2) {
|
||||
// middle click
|
||||
let map = document.getElementById('map');
|
||||
let mult = event.ctrlKey ? 2.0 : 1.0;
|
||||
mapOffsetX += event.movementX * mult;
|
||||
mapOffsetY += event.movementY * mult;
|
||||
map.style.left = `${mapOffsetX}px`;
|
||||
map.style.top = `${mapOffsetY}px`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
background-color: rgb(32, 35, 35);
|
||||
color: white;
|
||||
}
|
||||
#side-panel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
resize: horizontal;
|
||||
overflow: auto;
|
||||
border: 0px solid black;
|
||||
width: 20%;
|
||||
min-width: 10%;
|
||||
max-width: 50%;
|
||||
background-color: rgb(17, 0, 36);
|
||||
background-image: linear-gradient(135deg, rgb(17, 0, 36) 0px, rgb(17, 0, 36) 98%, rgb(188, 255, 185) 99%);
|
||||
}
|
||||
#chat-history {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
margin-top: 10px;
|
||||
background-color:brown;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body onload="init()">
|
||||
<div id="login-screen" style="display: flex; justify-content: center; width: 100%; height: 100%;">
|
||||
<div style="display: flex; justify-content: center; flex-direction: column;" >
|
||||
<input type="text" id="login-username" placeholder="Username..." >
|
||||
<input type="password" id="login-pass" placeholder="Password..." >
|
||||
<button onclick="onLoginClick()">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="game" style="display: none; width: 100%; height: 100%;" >
|
||||
<div id="side-panel">
|
||||
<div style="display: flex; flex-direction: column; width: calc(100% - 15px); height: 100%;" >
|
||||
<div id="chat-history">
|
||||
Chat history
|
||||
</div>
|
||||
<div id="chat-input" style="width: 100%; flex-grow: 1; background-color: aqua; margin: 10px 0;">
|
||||
new message input
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="game-view" style="display: flex; overflow: hidden; position: relative; top: 0px; left: 0px; flex-grow: 1;" >
|
||||
<div style="position:absolute; top: 10px; left: 5px; background-color: rgb(255, 166, 0); z-index: 1;" >
|
||||
floating<br>stuff
|
||||
</div>
|
||||
<div id="map" style="position:absolute;">
|
||||
<img src="https://rustystriker.dev/molly.jpg" height="200%" >
|
||||
<img src="https://rustystriker.dev/louise.jpg" height="10%" style="position: absolute; left:20px; top: 20px;" >
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,15 +1,24 @@
|
|||
const socket = new WebSocket('ws://localhost:3001/ws');
|
||||
|
||||
socket.addEventListener('open', e => { socket.send('hello server'); });
|
||||
|
||||
socket.addEventListener('message', e => {
|
||||
console.log('message from server', e.data);
|
||||
if (e.data == 'ok') {
|
||||
document.getElementById('login_button').innerText = "logged in!";
|
||||
const tavern = {
|
||||
socket: socket = new WebSocket('ws://localhost:3001/ws'),
|
||||
connected: false,
|
||||
loggedIn: false,
|
||||
call: (f, ...args) => {
|
||||
if(typeof(f) == "function") {
|
||||
f(...args);
|
||||
}
|
||||
});
|
||||
|
||||
function onClick() {
|
||||
socket.send('login admin admin123');
|
||||
}
|
||||
};
|
||||
|
||||
tavern.socket.onopen = () => tavern.connected = true;
|
||||
tavern.socket.onmessage = (m) => {
|
||||
m = JSON.parse(m.data);
|
||||
console.log(m);
|
||||
if(m.login) {
|
||||
tavern.socket.loggedIn = m.login.success;
|
||||
tavern.call(tavern.onlogin, tavern.socket.loggedIn);
|
||||
}
|
||||
}
|
||||
tavern.login = (username, password) => {
|
||||
if(!tavern.connected || tavern.loggedIn) { return false; }
|
||||
tavern.socket.send(JSON.stringify({ login: { username, password }}));
|
||||
}
|
77
readme.md
Normal file
77
readme.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# WTF how do i implement a game???
|
||||
|
||||
## Things you need from a game
|
||||
|
||||
Characters and character sheet:
|
||||
[x] Stats and items: get/set/display
|
||||
[ ] Actions (broken into different tabs, aka: `(&self) -> Vec<InteractionDefinition>`)
|
||||
[ ] Token status: status icons (with tooltip ffs)/light produced/vision distance(in light level)
|
||||
[ ] Apply Action `(&mut self, InteractionResult) -> ()`
|
||||
Spells and items
|
||||
|
||||
[ ]Turn based combat callbakcs:
|
||||
[ ] Start of turn - `(&mut self) -> Vec<Message>`
|
||||
[ ] End of turn
|
||||
|
||||
|
||||
InteractionDef -> Player uses -> Interaction in chat (with actions) -> Rolls some dice -> Interaction
|
||||
|
||||
Shoot arrow def ->
|
||||
player A shoots B (rolls attack) -> I
|
||||
nteraction in chat (with Roll damage button) ->
|
||||
InteractionResult (damage/double/block/heal actions) ->
|
||||
Apply InteractionResult ()
|
||||
|
||||
```rust
|
||||
trait Action {}
|
||||
trait CS<A: Action> {
|
||||
|
||||
fn can_use_action(&self, action: &A) -> bool;
|
||||
}
|
||||
struct Game<'dec, 'dea, C: CS<A> + Serialize + Deserialize<'dec>, A: Action + Serialize + Deserialize<'dea>> {
|
||||
_a: std::marker::PhantomData<&'dea A>,
|
||||
_c: std::marker::PhantomData<&'dec C>,
|
||||
}
|
||||
impl<'dec, 'dea, C: CS<A> + Serialize + Deserialize<'dec>, A: Action + Serialize + Deserialize<'dea>> Game<'dec, 'dea, C, A> {
|
||||
fn read_spell(s: &'dea str) -> A {
|
||||
serde_json::de::from_str::<A>(s).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Spell {
|
||||
pub mana: i32,
|
||||
}
|
||||
impl Action for Spell {}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Sheet;
|
||||
impl CS<Spell> for Sheet {
|
||||
fn can_use_action(&self, action: &Spell) -> bool {
|
||||
action.mana > 10
|
||||
}
|
||||
}
|
||||
fn stupid() {
|
||||
let game = Game::<'_, '_, Sheet, Spell>::read_spell("aaaaaaaa");
|
||||
}
|
||||
```
|
||||
^^^ this looks mad isnt it, but it defines 3 traits, Action, Character sheet and game(which should be a trait actually)
|
||||
|
||||
1. Player connects
|
||||
2. Player gets character data (including all the relevant actions and such)
|
||||
3. Player acts
|
||||
|
||||
1. Player does action
|
||||
2. Action is printed to chat (with rolls and such)
|
||||
3. Action button is pressed (optional rolls)
|
||||
|
||||
fn use_action(&mut self, entry: &Entry, action: &Action) -> ChatMessage
|
||||
|
||||
```rust
|
||||
struct Action {
|
||||
pub name: String,
|
||||
pub roll_result: i32,
|
||||
}
|
||||
|
||||
```
|
||||
|
2
reqs.md
2
reqs.md
|
@ -3,7 +3,7 @@
|
|||
## User initiated
|
||||
|
||||
- Login
|
||||
- get available tabes
|
||||
- get available tables
|
||||
- get table data
|
||||
- connect to table
|
||||
- get map data
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub hashed_password: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LoginData {
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResult {
|
||||
pub success: bool,
|
||||
// TODO: Figure out what the user needs on successful login to reduce traffic
|
||||
}
|
|
@ -4,9 +4,23 @@ pub mod map_actions;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ResultBase<T: Serialize> {
|
||||
pub success: bool,
|
||||
pub fail_reason: Option<String>,
|
||||
pub data: Option<T>
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Request {
|
||||
#[default]
|
||||
Error,
|
||||
Login(login::LoginRequest)
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Response {
|
||||
Error(RequestError),
|
||||
Login(login::LoginResult)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RequestError {
|
||||
InvalidRequest,
|
||||
AlreadyLoggedIn,
|
||||
}
|
33
src/game/action.rs
Normal file
33
src/game/action.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::chat_message::ChatMessage;
|
||||
|
||||
pub trait GameEntry : Serialize + Sized {
|
||||
/// Load a single entry from string
|
||||
fn load(entry: &str) -> Option<Self>;
|
||||
/// Loads multiple items, blanket impl using Self::load
|
||||
fn load_mul(entries: &[&str]) -> Vec<Option<Self>> {
|
||||
let mut r = Vec::with_capacity(entries.len());
|
||||
for &e in entries {
|
||||
r.push(Self::load(e));
|
||||
}
|
||||
r
|
||||
}
|
||||
/// Display name (e.g. `weapon/dagger` -> `Dagger`)
|
||||
fn display_name(&self) -> String;
|
||||
/// Get all entries, with an optional filter (could be `weapon` for example to show only weapons)
|
||||
fn all(filter: Option<&str>) -> Vec<String>;
|
||||
/// returns a chat message to show the entry (with description and all)
|
||||
fn to_chat(&self, source: &str) -> ChatMessage;
|
||||
}
|
||||
|
||||
pub struct ActionDefinition {
|
||||
pub name: String,
|
||||
pub targets: i32,
|
||||
}
|
||||
|
||||
pub struct ActionResult {
|
||||
pub name: String,
|
||||
pub roll_result: i32,
|
||||
pub roll_target: i32,
|
||||
}
|
|
@ -1,4 +1,19 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use super::{action::{ActionDefinition, ActionResult, GameEntry}, chat_message::ChatMessage};
|
||||
|
||||
pub trait Character<E: GameEntry> : CharacterSheet {
|
||||
/// types of actions that can be done on the specified entry (e.g. cast for spells and wear for armor)
|
||||
fn actions(&self, entry: &E) -> Vec<ActionDefinition>;
|
||||
/// uses an action for a specific character, some actions should be split into multiple
|
||||
///
|
||||
/// for example, an attack sequence will be of:
|
||||
///
|
||||
/// - `Attack` invoked on a weapon entry, which will have the `roll damage` option in the chat
|
||||
/// - `roll damage` will be done on the attacking character, which show the `damage` and `double` actions
|
||||
/// - `damage` will be used on the target character (which could apply reductions as well)
|
||||
fn use_action(&mut self, entry: &E, action: &ActionResult) -> ChatMessage;
|
||||
}
|
||||
|
||||
pub trait CharacterSheet : Default {
|
||||
/// Character sheet inputs (stuff that are not calculated from different items), such as Name, Age, Strength
|
||||
|
@ -18,6 +33,7 @@ pub enum EntryType {
|
|||
Number(i32),
|
||||
Text(String),
|
||||
Bool(bool),
|
||||
Array(Vec<EntryType>),
|
||||
}
|
||||
impl Default for EntryType {
|
||||
fn default() -> Self {
|
||||
|
@ -52,10 +68,20 @@ impl EntryType {
|
|||
}
|
||||
impl std::fmt::Display for EntryType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match(self) {
|
||||
match self {
|
||||
EntryType::Number(n) => write!(f, "{}", n),
|
||||
EntryType::Text(t) => write!(f, "{}", t),
|
||||
EntryType::Bool(b) => write!(f, "{}", b),
|
||||
EntryType::Array(v) => {
|
||||
write!(f, "[ ")?;
|
||||
if v.len() > 0 {
|
||||
for i in v.iter().take(v.len() - 1) {
|
||||
write!(f, "{}, ", i)?;
|
||||
};
|
||||
write!(f, "{} ", v.iter().last().unwrap())?;
|
||||
};
|
||||
write!(f, "]")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
34
src/game/chat_message.rs
Normal file
34
src/game/chat_message.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
/// message text, `{item}` can be used to refer to items and such, where item is of the path such as `items/sword` or `spells/fireball`
|
||||
pub text: String,
|
||||
/// Rolls of the action, if not empty a roll should happen before
|
||||
pub roll: Option<Vec<RollDialogOption>>,
|
||||
/// Optional roll target
|
||||
pub roll_target: Option<i32>,
|
||||
/// Optional action buttons, for a chat message this will be empty
|
||||
pub actions: Option<Vec<String>>,
|
||||
/// Source/Caster/Whoever initiated the action/sent the message
|
||||
pub source: String,
|
||||
/// Targets of the action, for a chat message this will be empty
|
||||
pub targets: Option<Vec<String>>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RollDialogOption {
|
||||
/// Field name
|
||||
pub name: String,
|
||||
/// dice size (aka d6, d12, d20)
|
||||
pub dice_type: u16,
|
||||
/// amount of dice (aka 1d6, 2d12, 10d20)
|
||||
pub dice_amount: u16,
|
||||
/// Constant amout to add (+7, +3, -1)
|
||||
pub constant: i16,
|
||||
/// Extra data, like damage type
|
||||
pub extra: String,
|
||||
/// should be enabled by default
|
||||
pub enabled: bool
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
|
||||
|
||||
pub struct Interaction {
|
||||
/// Max number of entities that can be targeted in one go, 0 for non.
|
||||
pub max_targets: u32,
|
||||
|
||||
}
|
||||
|
||||
pub struct ChatMessage {
|
||||
/// Optional text portion
|
||||
text: String,
|
||||
/// Optional action buttons, for a chat message this will be empty
|
||||
actions: Vec<String>,
|
||||
/// Source/Caster/Whoever initiated the action/sent the message
|
||||
actor: String,
|
||||
/// Targets of the action, for a chat message this will be empty
|
||||
targets: Vec<String>
|
||||
}
|
||||
|
||||
pub struct RollDialogOption {
|
||||
/// Field name
|
||||
name: String,
|
||||
/// dice size (aka d6, d12, d20)
|
||||
dice: u16,
|
||||
/// amount of dice (aka 1d6, 2d12, 10d20)
|
||||
dice_amount: u16,
|
||||
/// Constant amout to add (+7, +3, -1)
|
||||
constant: i16,
|
||||
/// Extra data, like damage type
|
||||
extra: String,
|
||||
/// should be enabled by default
|
||||
enabled: bool
|
||||
}
|
|
@ -1,5 +1,33 @@
|
|||
//! Game Parser and Data types
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use character_sheet::Character;
|
||||
use serde::Serialize;
|
||||
|
||||
pub mod character_sheet;
|
||||
pub mod interaction;
|
||||
pub mod chat_message;
|
||||
pub mod action;
|
||||
|
||||
pub trait GameImpl<C: Character<A> + Serialize, A: action::GameEntry + Serialize> {
|
||||
fn new() -> Self;
|
||||
fn create_character(&mut self);
|
||||
}
|
||||
|
||||
pub struct Game<C: Character<A> + Serialize, A: action::GameEntry + Serialize> {
|
||||
_c: PhantomData<C>,
|
||||
_a: PhantomData<A>,
|
||||
characters: Vec<C>,
|
||||
}
|
||||
impl<C: Character<A> + Serialize, A: action::GameEntry + Serialize> GameImpl<C, A> for Game<C, A> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
_c: PhantomData,
|
||||
_a: PhantomData,
|
||||
characters: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_character(&mut self) {
|
||||
self.characters.push(C::default());
|
||||
}
|
||||
}
|
69
src/main.rs
69
src/main.rs
|
@ -1,34 +1,18 @@
|
|||
use axum::{
|
||||
extract::ws, response, routing, Router
|
||||
extract::ws::{self,Message}, response, routing, Router
|
||||
};
|
||||
use open_tavern::{game::character_sheet::{CharacterSheet, EntryType}, pathfinder2r_impl};
|
||||
use open_tavern::api::{Request, RequestError, Response};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut pf = pathfinder2r_impl::Pathfinder2rCharacterSheet::default();
|
||||
pf.set("Dexterity", EntryType::Number(10));
|
||||
pf.set("Name", EntryType::Text("AAAA".to_string()));
|
||||
pf.set("Items", EntryType::Text("Short Sword, Dragonleather Helmet".to_string()));
|
||||
let app = Router::new()
|
||||
.route("/", routing::get(root))
|
||||
.route("/socket.js", routing::get(socket))
|
||||
.route("/ws", routing::get(ws_handler))
|
||||
;
|
||||
|
||||
let d = pf.display();
|
||||
for e in d {
|
||||
if let Some((s, v)) = e {
|
||||
println!("{}: {}", s, v);
|
||||
}
|
||||
else {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// let app = Router::new()
|
||||
// .route("/", routing::get(root))
|
||||
// .route("/socket.js", routing::get(socket))
|
||||
// .route("/ws", routing::get(ws_handler))
|
||||
// ;
|
||||
|
||||
// let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
|
||||
// axum::serve(listener, app).await.unwrap();
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn ws_handler(ws: ws::WebSocketUpgrade) -> impl axum::response::IntoResponse {
|
||||
|
@ -36,35 +20,36 @@ async fn ws_handler(ws: ws::WebSocketUpgrade) -> impl axum::response::IntoRespon
|
|||
}
|
||||
|
||||
async fn handle_socket(mut socket: ws::WebSocket) {
|
||||
if socket.send(ws::Message::Text("This is my test message!".to_string())).await.is_ok() {
|
||||
println!("pinged!");
|
||||
}
|
||||
let mut logged_in = false;
|
||||
|
||||
println!("Got a new socket");
|
||||
loop {
|
||||
if let Some(msg) = socket.recv().await {
|
||||
if let Ok(msg) = msg {
|
||||
match msg {
|
||||
ws::Message::Text(t) => {
|
||||
println!("Got message: {}", t);
|
||||
if !logged_in && t.starts_with("login") {
|
||||
let mut split = t.splitn(2, ' ');
|
||||
split.next(); // the login part
|
||||
let user = split.next().unwrap();
|
||||
if open_tavern::user::User::default_admin().login(user) {
|
||||
socket.send(ws::Message::Text("ok".to_string())).await.unwrap();
|
||||
let response: Message = match msg {
|
||||
Message::Text(t) => {
|
||||
let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
|
||||
println!("Got message: {:?}", t);
|
||||
match req {
|
||||
Request::Error => Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap()),
|
||||
Request::Login(r) => if !logged_in {
|
||||
if r.username == "rusty" {
|
||||
logged_in = true;
|
||||
Message::Text(serde_json::to_string(&Response::Login(open_tavern::api::login::LoginResult { success: true })).unwrap())
|
||||
}
|
||||
else {
|
||||
socket.send(ws::Message::Text("bad".to_string())).await.unwrap();
|
||||
Message::Text(serde_json::to_string(&Response::Login(open_tavern::api::login::LoginResult { success: false })).unwrap())
|
||||
}
|
||||
} else {
|
||||
Message::Text(serde_json::to_string(&Response::Error(open_tavern::api::RequestError::AlreadyLoggedIn)).unwrap())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
ws::Message::Binary(b) => println!("got bytes: {:?}", b),
|
||||
ws::Message::Binary(_) => todo!(),
|
||||
ws::Message::Ping(_) => todo!(),
|
||||
ws::Message::Pong(_) => todo!(),
|
||||
ws::Message::Close(_) => break,
|
||||
}
|
||||
};
|
||||
socket.send(response).await.expect("failed sending to socket");
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
80
src/pathfinder2r_impl/entry.rs
Normal file
80
src/pathfinder2r_impl/entry.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::game::{action::GameEntry, chat_message::ChatMessage};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum PEntry {
|
||||
Weapon(Weapon),
|
||||
Consumable(Consumable),
|
||||
Spell(Spell),
|
||||
}
|
||||
impl GameEntry for PEntry {
|
||||
fn display_name(&self) -> String {
|
||||
match self {
|
||||
PEntry::Weapon(weapon) => weapon.name.clone(),
|
||||
PEntry::Consumable(consumable) => consumable.name.clone(),
|
||||
PEntry::Spell(spell) => spell.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load(entry: &str) -> Option<PEntry> {
|
||||
println!("loading {}", entry);
|
||||
let json = std::fs::read_to_string(format!("assets/pf2r/{}.json", entry)).ok()?;
|
||||
println!("{}", &json);
|
||||
if entry.starts_with("weapon/") {
|
||||
serde_json::from_str::<Weapon>(&json).map(|w| PEntry::Weapon(w)).ok()
|
||||
}
|
||||
else if entry.starts_with("spell/") {
|
||||
serde_json::from_str::<Spell>(&json).map(|s| PEntry::Spell(s)).ok()
|
||||
}
|
||||
else if entry.starts_with("consumable/") {
|
||||
serde_json::from_str::<Consumable>(&json).map(|s| PEntry::Consumable(s)).ok()
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_chat(&self, source: &str) -> ChatMessage {
|
||||
ChatMessage {
|
||||
text: format!("{} - it might be a {{weapon/short_bow}} it might not", self.display_name()),
|
||||
roll: None,
|
||||
roll_target: None,
|
||||
actions: None,
|
||||
source: String::from(source),
|
||||
targets: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn all(_filter: Option<&str>) -> Vec<String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Weapon {
|
||||
pub name: String,
|
||||
pub two_handed: bool,
|
||||
pub one_handed: bool,
|
||||
pub melee_reach: i32,
|
||||
pub ranged_reach: i32,
|
||||
// this is a weird thing but suffices for now, as it is expected as a string of `1d4 2 2d6`
|
||||
pub damage: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Consumable {
|
||||
name: String,
|
||||
traits: Vec<String>,
|
||||
/// Price in copper
|
||||
price: i32,
|
||||
bulk: i32,
|
||||
desc: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Spell {
|
||||
name: String,
|
||||
actions: i32,
|
||||
damage: String,
|
||||
damage_type: String,
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
use crate::game::{action::{ActionDefinition, ActionResult, GameEntry}, character_sheet::*, chat_message::{ChatMessage, RollDialogOption}};
|
||||
use tavern_macros::CharacterSheet;
|
||||
use crate::game::character_sheet::*;
|
||||
|
||||
pub mod entry;
|
||||
use entry::{PEntry, Weapon};
|
||||
|
||||
#[derive(Default, CharacterSheet)]
|
||||
pub struct Pathfinder2rCharacterSheet {
|
||||
// Genral stuff
|
||||
#[Input("Name")]
|
||||
name: String,
|
||||
#[Input("Items")]
|
||||
items: String,
|
||||
#[Input("Weapons")]
|
||||
#[InputExpr(set_items, get_items)]
|
||||
weapon: [Option<Weapon>; 2],
|
||||
// Attributes
|
||||
#[Seperator]
|
||||
#[Input("Strength")]
|
||||
|
@ -42,3 +46,81 @@ pub struct Pathfinder2rCharacterSheet {
|
|||
// TODO: Add more skills
|
||||
// TODO: Also add all the rest of the sheet items
|
||||
}
|
||||
impl Pathfinder2rCharacterSheet {
|
||||
fn set_items(&mut self, entry: EntryType) {
|
||||
if let EntryType::Array(a) = entry {
|
||||
let ws: Vec<PEntry> = a
|
||||
.iter()
|
||||
.map(|e| PEntry::load(&e.as_text()))
|
||||
.flatten()
|
||||
.collect();
|
||||
self.weapon = [None, None];
|
||||
for w in ws {
|
||||
if let PEntry::Weapon(w) = w {
|
||||
if self.weapon[0].is_none() {
|
||||
self.weapon[0] = Some(w);
|
||||
} else if self.weapon[1].is_none() {
|
||||
self.weapon[1] = Some(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
let s = entry.as_text();
|
||||
if let Some(PEntry::Weapon(w)) = PEntry::load(&s) {
|
||||
self.weapon[0] = Some(w);
|
||||
self.weapon[1] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn get_items(&self) -> EntryType {
|
||||
EntryType::Array(
|
||||
self.weapon
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|w| EntryType::Text(w.name.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Character<PEntry> for Pathfinder2rCharacterSheet {
|
||||
fn use_action(&mut self, entry: &PEntry, action: &ActionResult) -> ChatMessage {
|
||||
match entry {
|
||||
PEntry::Weapon(_) => ChatMessage {
|
||||
text: String::from("Attack"),
|
||||
roll: Some(vec![RollDialogOption { name: String::from("pierce"), dice_type: 4, dice_amount: 1, constant: 0, extra: String::new(), enabled: true }]),
|
||||
roll_target: Some(10),
|
||||
actions: Some(vec!["damage".to_string(), "double".to_string()]),
|
||||
source: self.name.clone(),
|
||||
targets: None,
|
||||
},
|
||||
PEntry::Consumable(_consumable) => if action.name == "consume" {
|
||||
ChatMessage {
|
||||
text: "Heal".to_string(),
|
||||
roll: Some(vec![RollDialogOption { name: "heal".to_string(), dice_type: 6, dice_amount: 1, constant: 0, extra: String::new(), enabled: true }]),
|
||||
roll_target: None,
|
||||
actions: Some(vec!["heal".to_string()]),
|
||||
source: self.name.clone(),
|
||||
targets: None,
|
||||
}
|
||||
} else { todo!() },
|
||||
PEntry::Spell(_spell) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self, entry: &PEntry) -> Vec<ActionDefinition> {
|
||||
let v;
|
||||
match entry {
|
||||
PEntry::Weapon(_) =>
|
||||
// should technically check if the item is in the user's packpack or something
|
||||
// but for now just return a constant list
|
||||
v = vec![ ("wield", 0), ("attack", 1), ("drop", 0), ("stow", 0) ],
|
||||
PEntry::Consumable(_) => v = vec![("consume", 0), ("stow", 0), ("drop", 0)],
|
||||
PEntry::Spell(_) => v = vec![("cast", 1)],
|
||||
};
|
||||
v.iter()
|
||||
.map(|s| ActionDefinition { name: s.0.to_string(), targets: s.1 })
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use proc_macro2::{Delimiter, Group, Span, TokenStream};
|
|||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{parse_macro_input, DeriveInput, Ident};
|
||||
|
||||
#[proc_macro_derive(CharacterSheet, attributes(Input, Field, FieldExpr, Seperator))]
|
||||
#[proc_macro_derive(CharacterSheet, attributes(Input, InputExpr, Field, FieldExpr, Seperator))]
|
||||
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
|
||||
let mut output = quote! {
|
||||
|
@ -62,12 +62,26 @@ fn impl_inputs(data: &syn::Data) -> TokenStream {
|
|||
let name = f.ident.clone().unwrap();
|
||||
let attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||
if let Some(attr) = attr {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
|
||||
let exp: Option<&syn::Attribute> = f.attrs.iter().find(|a| a.path.is_ident("InputExpr"));
|
||||
if let Some(attr) = exp {
|
||||
let exp: syn::Meta = attr.parse_meta().expect("Failed to parse MetaList!");
|
||||
if let syn::Meta::List(l) = exp {
|
||||
let from_input = &l.nested[1];
|
||||
items.extend(quote! {
|
||||
(#arg.to_string(), self.#from_input()),
|
||||
})
|
||||
}
|
||||
else {
|
||||
panic!("Failed parsing InputExpr attribute, expected `(&mut self, EntryType), (&self) -> EntryType`");
|
||||
}
|
||||
}
|
||||
else {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
items.extend(quote! {
|
||||
(#arg.to_string(), EntryType::#t(self.#name.clone())),
|
||||
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,12 +106,26 @@ fn impl_fields(data: &syn::Data) -> TokenStream {
|
|||
let name = f.ident.clone().unwrap();
|
||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||
if let Some(attr) = input_attr {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
|
||||
let exp = f.attrs.iter().find(|a| a.path.is_ident("InputExpr"));
|
||||
if let Some(attr) = exp {
|
||||
let exp: syn::Meta = attr.parse_meta().expect("Failed to parse MetaList!");
|
||||
if let syn::Meta::List(l) = exp {
|
||||
let from_input = &l.nested[1];
|
||||
items.extend(quote! {
|
||||
(#arg.to_string(), self.#from_input()),
|
||||
})
|
||||
}
|
||||
else {
|
||||
panic!("Failed parsing InputExpr attribute, expected `(&mut self, EntryType), (&self) -> EntryType`");
|
||||
}
|
||||
}
|
||||
else {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
items.extend(quote! {
|
||||
(#arg.to_string(), EntryType::#t(self.#name.clone())),
|
||||
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
||||
if let Some(attr) = field_attr {
|
||||
|
@ -140,11 +168,26 @@ fn impl_get(data: &syn::Data) -> TokenStream {
|
|||
let name = f.ident.clone().unwrap();
|
||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||
if let Some(attr) = input_attr {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
|
||||
let exp: Option<&syn::Attribute> = f.attrs.iter().find(|a| a.path.is_ident("InputExpr"));
|
||||
if let Some(attr) = exp {
|
||||
let exp: syn::Meta = attr.parse_meta().expect("Failed to parse MetaList!");
|
||||
if let syn::Meta::List(l) = exp {
|
||||
let from_input = &l.nested[1];
|
||||
match_hands.extend(quote! {
|
||||
#arg => Some(self.#from_input()),
|
||||
});
|
||||
}
|
||||
else {
|
||||
panic!("Failed parsing InputExpr attribute, expected `(&mut self, EntryType), (&self) -> EntryType`");
|
||||
}
|
||||
}
|
||||
else {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
match_hands.extend(quote! {
|
||||
#arg => Some(EntryType::#t(self.#name.clone())),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
||||
if let Some(attr) = field_attr {
|
||||
|
@ -188,11 +231,26 @@ fn impl_set(data: &syn::Data) -> TokenStream {
|
|||
let name = f.ident.clone().unwrap();
|
||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||
if let Some(attr) = input_attr {
|
||||
let t = get_type_ident_set(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
|
||||
let exp = f.attrs.iter().find(|a| a.path.is_ident("InputExpr"));
|
||||
if let Some(attr) = exp {
|
||||
let exp: syn::Meta = attr.parse_meta().expect("Failed to parse MetaList!");
|
||||
if let syn::Meta::List(l) = exp {
|
||||
let to_input = &l.nested[0];
|
||||
match_hands.extend(quote! {
|
||||
#arg => self.#to_input(value),
|
||||
});
|
||||
}
|
||||
else {
|
||||
panic!("Failed parsing InputExpr attribute, expected `(&mut self, EntryType), (&self) -> EntryType`");
|
||||
}
|
||||
}
|
||||
else {
|
||||
let t = get_type_ident_set(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
match_hands.extend(quote! {
|
||||
#arg => self.#name = value.#t(),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,12 +279,26 @@ fn impl_display(data: &syn::Data) -> TokenStream {
|
|||
}
|
||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||
if let Some(attr) = input_attr {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
|
||||
let exp = f.attrs.iter().find(|a| a.path.is_ident("InputExpr"));
|
||||
if let Some(attr) = exp {
|
||||
let exp: syn::Meta = attr.parse_meta().expect("Failed to parse MetaList!");
|
||||
if let syn::Meta::List(l) = exp {
|
||||
let from_input = &l.nested[1];
|
||||
items.extend(quote! {
|
||||
Some((#arg.to_string(), self.#from_input())),
|
||||
})
|
||||
}
|
||||
else {
|
||||
panic!("Failed parsing InputExpr attribute, expected `(&mut self, EntryType), (&self) -> EntryType`");
|
||||
}
|
||||
}
|
||||
else {
|
||||
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
|
||||
items.extend(quote! {
|
||||
Some((#arg.to_string(), EntryType::#t(self.#name.clone()))),
|
||||
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
||||
if let Some(attr) = field_attr {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
use open_tavern::game::character_sheet::EntryType;
|
||||
use tavern_macros::CharacterSheet;
|
||||
|
||||
#[test]
|
||||
fn test_macro() {
|
||||
trait CharacterSheet {
|
||||
fn inputs(&self) -> Vec<(String, EntryType)>;
|
||||
fn fields(&self) -> Vec<(String, EntryType)>;
|
||||
fn get(&self, entry: &str) -> Option<EntryType>;
|
||||
fn set(&mut self, entry: &str, value: EntryType);
|
||||
}
|
||||
|
||||
#[derive(CharacterSheet)]
|
||||
struct Testy {
|
||||
a: String,
|
||||
#[Input("AA")]
|
||||
b: String,
|
||||
#[Input("BB")]
|
||||
c: String,
|
||||
#[Input("Strength")]
|
||||
str: i32,
|
||||
#[Field("Athletics")]
|
||||
#[FieldExpr(self.str + (self.athletics_trained as i32) * 2)]
|
||||
athletics: i32,
|
||||
#[Input("Trained Athletics")]
|
||||
athletics_trained: bool,
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue