i did some stuff, honestly it starts to build pretty well if i might sayy

This commit is contained in:
Rusty Striker 2024-09-28 20:13:11 +03:00
parent 22319e84a1
commit 9189d9cd88
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
22 changed files with 689 additions and 176 deletions

View 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",
}

View file

@ -0,0 +1,6 @@
{
name: "Phase Bolt",
actions: 2,
damage: "3d4",
damage_type: "piercing"
}

View file

@ -0,0 +1,8 @@
{
"name": "Dagger",
"two_handed": false,
"one_handed": true,
"melee_reach": 1,
"ranged_reach": 10,
"damage": "1d4"
}

View file

@ -0,0 +1,8 @@
{
name: "Katar",
two_handed: false,
one_handed: true,
melee_reach: 1,
ranged_reach: 0,
damage: "1d4"
}

View file

@ -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 -> (>|<|>=|<=|==|!=);

View file

@ -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>

View file

@ -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
View 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,
}
```

View file

@ -3,7 +3,7 @@
## User initiated
- Login
- get available tabes
- get available tables
- get table data
- connect to table
- get map data

View file

View file

@ -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
}

View file

@ -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
View 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,
}

View file

@ -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
View 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
}

View file

@ -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
}

View file

@ -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());
}
}

View file

@ -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::<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 {
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 {

View 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,
}

View file

@ -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()
}
}

View file

@ -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\")`");
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 {

View file

@ -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,
}
}