From 9189d9cd88bdbe245b4ba7c10460db4fccbdaba9 Mon Sep 17 00:00:00 2001 From: Rusty Striker Date: Sat, 28 Sep 2024 20:13:11 +0300 Subject: [PATCH] i did some stuff, honestly it starts to build pretty well if i might sayy --- assets/pf2r/consumable/alcohol.json | 7 ++ assets/pf2r/spell/phase_bolt.json | 6 ++ assets/pf2r/weapon/dagger.json | 8 ++ assets/pf2r/weapon/katar.json | 8 ++ assets/sheet_lang_def.txt | 16 ---- assets/web/index.html | 124 +++++++++++++++++++++++++++- assets/web/socket.js | 33 +++++--- readme.md | 77 +++++++++++++++++ reqs.md | 2 +- src/api/get_table.rs | 0 src/api/login.rs | 9 +- src/api/mod.rs | 24 ++++-- src/game/action.rs | 33 ++++++++ src/game/character_sheet.rs | 28 ++++++- src/game/chat_message.rs | 34 ++++++++ src/game/interaction.rs | 33 -------- src/game/mod.rs | 30 ++++++- src/main.rs | 75 +++++++---------- src/pathfinder2r_impl/entry.rs | 80 ++++++++++++++++++ src/pathfinder2r_impl/mod.rs | 88 +++++++++++++++++++- tavern_macros/src/lib.rs | 122 +++++++++++++++++++++------ tests/macro_test.rs | 28 ------- 22 files changed, 689 insertions(+), 176 deletions(-) create mode 100644 assets/pf2r/consumable/alcohol.json create mode 100644 assets/pf2r/spell/phase_bolt.json create mode 100644 assets/pf2r/weapon/dagger.json create mode 100644 assets/pf2r/weapon/katar.json delete mode 100644 assets/sheet_lang_def.txt create mode 100644 readme.md delete mode 100644 src/api/get_table.rs create mode 100644 src/game/action.rs create mode 100644 src/game/chat_message.rs delete mode 100644 src/game/interaction.rs create mode 100644 src/pathfinder2r_impl/entry.rs delete mode 100644 tests/macro_test.rs diff --git a/assets/pf2r/consumable/alcohol.json b/assets/pf2r/consumable/alcohol.json new file mode 100644 index 0000000..41b4972 --- /dev/null +++ b/assets/pf2r/consumable/alcohol.json @@ -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", +} diff --git a/assets/pf2r/spell/phase_bolt.json b/assets/pf2r/spell/phase_bolt.json new file mode 100644 index 0000000..979c239 --- /dev/null +++ b/assets/pf2r/spell/phase_bolt.json @@ -0,0 +1,6 @@ +{ + name: "Phase Bolt", + actions: 2, + damage: "3d4", + damage_type: "piercing" +} diff --git a/assets/pf2r/weapon/dagger.json b/assets/pf2r/weapon/dagger.json new file mode 100644 index 0000000..6cec7de --- /dev/null +++ b/assets/pf2r/weapon/dagger.json @@ -0,0 +1,8 @@ +{ + "name": "Dagger", + "two_handed": false, + "one_handed": true, + "melee_reach": 1, + "ranged_reach": 10, + "damage": "1d4" +} diff --git a/assets/pf2r/weapon/katar.json b/assets/pf2r/weapon/katar.json new file mode 100644 index 0000000..c7a3e5c --- /dev/null +++ b/assets/pf2r/weapon/katar.json @@ -0,0 +1,8 @@ +{ + name: "Katar", + two_handed: false, + one_handed: true, + melee_reach: 1, + ranged_reach: 0, + damage: "1d4" +} diff --git a/assets/sheet_lang_def.txt b/assets/sheet_lang_def.txt deleted file mode 100644 index 45a283c..0000000 --- a/assets/sheet_lang_def.txt +++ /dev/null @@ -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 -> (>|<|>=|<=|==|!=); diff --git a/assets/web/index.html b/assets/web/index.html index 55d89be..75d9f22 100644 --- a/assets/web/index.html +++ b/assets/web/index.html @@ -1,2 +1,122 @@ - - + + + + + + + + + +
+
+ + + +
+
+ + + \ No newline at end of file diff --git a/assets/web/socket.js b/assets/web/socket.js index 3aceb0e..1018df6 100644 --- a/assets/web/socket.js +++ b/assets/web/socket.js @@ -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 }})); +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..de85f0c --- /dev/null +++ b/readme.md @@ -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`) + [ ] 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` + [ ] 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 { + + fn can_use_action(&self, action: &A) -> bool; +} +struct Game<'dec, 'dea, C: CS + 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 + 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::(s).unwrap() + } +} + +#[derive(Serialize, Deserialize)] +struct Spell { + pub mana: i32, +} +impl Action for Spell {} + +#[derive(Serialize, Deserialize)] +struct Sheet; +impl CS 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, +} + +``` + diff --git a/reqs.md b/reqs.md index 944962e..eb1bffb 100644 --- a/reqs.md +++ b/reqs.md @@ -3,7 +3,7 @@ ## User initiated - Login -- get available tabes +- get available tables - get table data - connect to table - get map data diff --git a/src/api/get_table.rs b/src/api/get_table.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/login.rs b/src/api/login.rs index 27e01f4..835d19a 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -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 } \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 0d6ced7..f5c92a0 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,9 +4,23 @@ pub mod map_actions; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] -pub struct ResultBase { - pub success: bool, - pub fail_reason: Option, - pub data: Option +#[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, } \ No newline at end of file diff --git a/src/game/action.rs b/src/game/action.rs new file mode 100644 index 0000000..fde3e22 --- /dev/null +++ b/src/game/action.rs @@ -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; + /// Loads multiple items, blanket impl using Self::load + fn load_mul(entries: &[&str]) -> Vec> { + 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; + /// 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, +} \ No newline at end of file diff --git a/src/game/character_sheet.rs b/src/game/character_sheet.rs index db13b7a..2e5ce48 100644 --- a/src/game/character_sheet.rs +++ b/src/game/character_sheet.rs @@ -1,4 +1,19 @@ +use std::fmt::Debug; +use super::{action::{ActionDefinition, ActionResult, GameEntry}, chat_message::ChatMessage}; + +pub trait Character : 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; + /// 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), } 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, "]") + }, } } } \ No newline at end of file diff --git a/src/game/chat_message.rs b/src/game/chat_message.rs new file mode 100644 index 0000000..0c13f81 --- /dev/null +++ b/src/game/chat_message.rs @@ -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>, + /// Optional roll target + pub roll_target: Option, + /// Optional action buttons, for a chat message this will be empty + pub actions: Option>, + /// 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> +} + +#[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 +} diff --git a/src/game/interaction.rs b/src/game/interaction.rs deleted file mode 100644 index a685b88..0000000 --- a/src/game/interaction.rs +++ /dev/null @@ -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, - /// 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 -} - -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 -} diff --git a/src/game/mod.rs b/src/game/mod.rs index 18cb31b..ac9e863 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -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; \ No newline at end of file +pub mod chat_message; +pub mod action; + +pub trait GameImpl + Serialize, A: action::GameEntry + Serialize> { + fn new() -> Self; + fn create_character(&mut self); +} + +pub struct Game + Serialize, A: action::GameEntry + Serialize> { + _c: PhantomData, + _a: PhantomData, + characters: Vec, +} +impl + Serialize, A: action::GameEntry + Serialize> GameImpl for Game { + fn new() -> Self { + Self { + _c: PhantomData, + _a: PhantomData, + characters: Vec::new(), + } + } + + fn create_character(&mut self) { + self.characters.push(C::default()); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 41e3b13..f256a77 100644 --- a/src/main.rs +++ b/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(); - logged_in = true; + let response: Message = match msg { + Message::Text(t) => { + let req = serde_json::from_str::(&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 { + 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()) + }, } - else { - socket.send(ws::Message::Text("bad".to_string())).await.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 { diff --git a/src/pathfinder2r_impl/entry.rs b/src/pathfinder2r_impl/entry.rs new file mode 100644 index 0000000..bfdf882 --- /dev/null +++ b/src/pathfinder2r_impl/entry.rs @@ -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 { + 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::(&json).map(|w| PEntry::Weapon(w)).ok() + } + else if entry.starts_with("spell/") { + serde_json::from_str::(&json).map(|s| PEntry::Spell(s)).ok() + } + else if entry.starts_with("consumable/") { + serde_json::from_str::(&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 { + 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, + /// 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, +} + diff --git a/src/pathfinder2r_impl/mod.rs b/src/pathfinder2r_impl/mod.rs index 99f6284..0534e7f 100644 --- a/src/pathfinder2r_impl/mod.rs +++ b/src/pathfinder2r_impl/mod.rs @@ -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; 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 = 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 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 { + 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() + } +} diff --git a/tavern_macros/src/lib.rs b/tavern_macros/src/lib.rs index 353a0dd..c982021 100644 --- a/tavern_macros/src/lib.rs +++ b/tavern_macros/src/lib.rs @@ -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! { @@ -44,7 +44,7 @@ fn get_type_ident_set(ty: &syn::Type) -> Option { Some(Ident::new("as_num", Span::call_site())) } else if p.path.is_ident("bool") { Some(Ident::new("as_bool", Span::call_site())) - } else { None } + } else { None } } _ => panic!("Invalid data type"), } @@ -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\")`"); - items.extend(quote! { - (#arg.to_string(), EntryType::#t(self.#name.clone())), - - }) + 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\")`"); - items.extend(quote! { - (#arg.to_string(), EntryType::#t(self.#name.clone())), - - }) + 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\")`"); - match_hands.extend(quote! { - #arg => Some(EntryType::#t(self.#name.clone())), - }) + 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\")`"); - match_hands.extend(quote! { - #arg => self.#name = value.#t(), - }) + 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\")`"); - items.extend(quote! { - Some((#arg.to_string(), EntryType::#t(self.#name.clone()))), - - }) + 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 { diff --git a/tests/macro_test.rs b/tests/macro_test.rs deleted file mode 100644 index 06fcae5..0000000 --- a/tests/macro_test.rs +++ /dev/null @@ -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; - 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, - } -} \ No newline at end of file