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>
|
<!DOCTYPE html>
|
||||||
<script src='socket.js'></script>
|
<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');
|
const tavern = {
|
||||||
|
socket: socket = new WebSocket('ws://localhost:3001/ws'),
|
||||||
socket.addEventListener('open', e => { socket.send('hello server'); });
|
connected: false,
|
||||||
|
loggedIn: false,
|
||||||
socket.addEventListener('message', e => {
|
call: (f, ...args) => {
|
||||||
console.log('message from server', e.data);
|
if(typeof(f) == "function") {
|
||||||
if (e.data == 'ok') {
|
f(...args);
|
||||||
document.getElementById('login_button').innerText = "logged in!";
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function onClick() {
|
tavern.socket.onopen = () => tavern.connected = true;
|
||||||
socket.send('login admin admin123');
|
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
|
## User initiated
|
||||||
|
|
||||||
- Login
|
- Login
|
||||||
- get available tabes
|
- get available tables
|
||||||
- get table data
|
- get table data
|
||||||
- connect to table
|
- connect to table
|
||||||
- get map data
|
- get map data
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub hashed_password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize)]
|
||||||
pub struct LoginData {
|
pub struct LoginResult {
|
||||||
|
pub success: bool,
|
||||||
// TODO: Figure out what the user needs on successful login to reduce traffic
|
// 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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||||
pub struct ResultBase<T: Serialize> {
|
#[serde(rename_all = "snake_case")]
|
||||||
pub success: bool,
|
pub enum Request {
|
||||||
pub fail_reason: Option<String>,
|
#[default]
|
||||||
pub data: Option<T>
|
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 {
|
pub trait CharacterSheet : Default {
|
||||||
/// Character sheet inputs (stuff that are not calculated from different items), such as Name, Age, Strength
|
/// 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),
|
Number(i32),
|
||||||
Text(String),
|
Text(String),
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
|
Array(Vec<EntryType>),
|
||||||
}
|
}
|
||||||
impl Default for EntryType {
|
impl Default for EntryType {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -52,10 +68,20 @@ impl EntryType {
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for EntryType {
|
impl std::fmt::Display for EntryType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match(self) {
|
match self {
|
||||||
EntryType::Number(n) => write!(f, "{}", n),
|
EntryType::Number(n) => write!(f, "{}", n),
|
||||||
EntryType::Text(t) => write!(f, "{}", t),
|
EntryType::Text(t) => write!(f, "{}", t),
|
||||||
EntryType::Bool(b) => write!(f, "{}", b),
|
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
|
//! Game Parser and Data types
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use character_sheet::Character;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
pub mod character_sheet;
|
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::{
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let mut pf = pathfinder2r_impl::Pathfinder2rCharacterSheet::default();
|
let app = Router::new()
|
||||||
pf.set("Dexterity", EntryType::Number(10));
|
.route("/", routing::get(root))
|
||||||
pf.set("Name", EntryType::Text("AAAA".to_string()));
|
.route("/socket.js", routing::get(socket))
|
||||||
pf.set("Items", EntryType::Text("Short Sword, Dragonleather Helmet".to_string()));
|
.route("/ws", routing::get(ws_handler))
|
||||||
|
;
|
||||||
|
|
||||||
let d = pf.display();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
|
||||||
for e in d {
|
axum::serve(listener, app).await.unwrap();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ws_handler(ws: ws::WebSocketUpgrade) -> impl axum::response::IntoResponse {
|
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) {
|
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;
|
let mut logged_in = false;
|
||||||
|
println!("Got a new socket");
|
||||||
loop {
|
loop {
|
||||||
if let Some(msg) = socket.recv().await {
|
if let Some(msg) = socket.recv().await {
|
||||||
if let Ok(msg) = msg {
|
if let Ok(msg) = msg {
|
||||||
match msg {
|
let response: Message = match msg {
|
||||||
ws::Message::Text(t) => {
|
Message::Text(t) => {
|
||||||
println!("Got message: {}", t);
|
let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
|
||||||
if !logged_in && t.starts_with("login") {
|
println!("Got message: {:?}", t);
|
||||||
let mut split = t.splitn(2, ' ');
|
match req {
|
||||||
split.next(); // the login part
|
Request::Error => Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap()),
|
||||||
let user = split.next().unwrap();
|
Request::Login(r) => if !logged_in {
|
||||||
if open_tavern::user::User::default_admin().login(user) {
|
if r.username == "rusty" {
|
||||||
socket.send(ws::Message::Text("ok".to_string())).await.unwrap();
|
|
||||||
logged_in = true;
|
logged_in = true;
|
||||||
|
Message::Text(serde_json::to_string(&Response::Login(open_tavern::api::login::LoginResult { success: true })).unwrap())
|
||||||
}
|
}
|
||||||
else {
|
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(_) => todo!(),
|
||||||
ws::Message::Binary(b) => println!("got bytes: {:?}", b),
|
|
||||||
ws::Message::Ping(_) => todo!(),
|
ws::Message::Ping(_) => todo!(),
|
||||||
ws::Message::Pong(_) => todo!(),
|
ws::Message::Pong(_) => todo!(),
|
||||||
ws::Message::Close(_) => break,
|
ws::Message::Close(_) => break,
|
||||||
}
|
};
|
||||||
|
socket.send(response).await.expect("failed sending to socket");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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 tavern_macros::CharacterSheet;
|
||||||
use crate::game::character_sheet::*;
|
|
||||||
|
pub mod entry;
|
||||||
|
use entry::{PEntry, Weapon};
|
||||||
|
|
||||||
#[derive(Default, CharacterSheet)]
|
#[derive(Default, CharacterSheet)]
|
||||||
pub struct Pathfinder2rCharacterSheet {
|
pub struct Pathfinder2rCharacterSheet {
|
||||||
// Genral stuff
|
// Genral stuff
|
||||||
#[Input("Name")]
|
#[Input("Name")]
|
||||||
name: String,
|
name: String,
|
||||||
#[Input("Items")]
|
#[Input("Weapons")]
|
||||||
items: String,
|
#[InputExpr(set_items, get_items)]
|
||||||
|
weapon: [Option<Weapon>; 2],
|
||||||
// Attributes
|
// Attributes
|
||||||
#[Seperator]
|
#[Seperator]
|
||||||
#[Input("Strength")]
|
#[Input("Strength")]
|
||||||
|
@ -42,3 +46,81 @@ pub struct Pathfinder2rCharacterSheet {
|
||||||
// TODO: Add more skills
|
// TODO: Add more skills
|
||||||
// TODO: Also add all the rest of the sheet items
|
// 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 quote::{quote, ToTokens, TokenStreamExt};
|
||||||
use syn::{parse_macro_input, DeriveInput, Ident};
|
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 {
|
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
|
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
|
||||||
let mut output = quote! {
|
let mut output = quote! {
|
||||||
|
@ -62,12 +62,26 @@ fn impl_inputs(data: &syn::Data) -> TokenStream {
|
||||||
let name = f.ident.clone().unwrap();
|
let name = f.ident.clone().unwrap();
|
||||||
let attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
let attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||||
if let Some(attr) = attr {
|
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 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! {
|
items.extend(quote! {
|
||||||
(#arg.to_string(), EntryType::#t(self.#name.clone())),
|
(#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 name = f.ident.clone().unwrap();
|
||||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||||
if let Some(attr) = input_attr {
|
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 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! {
|
items.extend(quote! {
|
||||||
(#arg.to_string(), EntryType::#t(self.#name.clone())),
|
(#arg.to_string(), EntryType::#t(self.#name.clone())),
|
||||||
|
});
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
||||||
if let Some(attr) = field_attr {
|
if let Some(attr) = field_attr {
|
||||||
|
@ -140,11 +168,26 @@ fn impl_get(data: &syn::Data) -> TokenStream {
|
||||||
let name = f.ident.clone().unwrap();
|
let name = f.ident.clone().unwrap();
|
||||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||||
if let Some(attr) = input_attr {
|
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 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! {
|
match_hands.extend(quote! {
|
||||||
#arg => Some(EntryType::#t(self.#name.clone())),
|
#arg => Some(EntryType::#t(self.#name.clone())),
|
||||||
})
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
||||||
if let Some(attr) = field_attr {
|
if let Some(attr) = field_attr {
|
||||||
|
@ -188,11 +231,26 @@ fn impl_set(data: &syn::Data) -> TokenStream {
|
||||||
let name = f.ident.clone().unwrap();
|
let name = f.ident.clone().unwrap();
|
||||||
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||||
if let Some(attr) = input_attr {
|
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 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! {
|
match_hands.extend(quote! {
|
||||||
#arg => self.#name = value.#t(),
|
#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"));
|
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
|
||||||
if let Some(attr) = input_attr {
|
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 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! {
|
items.extend(quote! {
|
||||||
Some((#arg.to_string(), EntryType::#t(self.#name.clone()))),
|
Some((#arg.to_string(), EntryType::#t(self.#name.clone()))),
|
||||||
|
});
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
|
||||||
if let Some(attr) = field_attr {
|
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