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
|
@ -1,12 +1,13 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub hashed_password: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LoginData {
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResult {
|
||||
pub success: bool,
|
||||
// TODO: Figure out what the user needs on successful login to reduce traffic
|
||||
}
|
|
@ -4,9 +4,23 @@ pub mod map_actions;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ResultBase<T: Serialize> {
|
||||
pub success: bool,
|
||||
pub fail_reason: Option<String>,
|
||||
pub data: Option<T>
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Request {
|
||||
#[default]
|
||||
Error,
|
||||
Login(login::LoginRequest)
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Response {
|
||||
Error(RequestError),
|
||||
Login(login::LoginResult)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RequestError {
|
||||
InvalidRequest,
|
||||
AlreadyLoggedIn,
|
||||
}
|
33
src/game/action.rs
Normal file
33
src/game/action.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::chat_message::ChatMessage;
|
||||
|
||||
pub trait GameEntry : Serialize + Sized {
|
||||
/// Load a single entry from string
|
||||
fn load(entry: &str) -> Option<Self>;
|
||||
/// Loads multiple items, blanket impl using Self::load
|
||||
fn load_mul(entries: &[&str]) -> Vec<Option<Self>> {
|
||||
let mut r = Vec::with_capacity(entries.len());
|
||||
for &e in entries {
|
||||
r.push(Self::load(e));
|
||||
}
|
||||
r
|
||||
}
|
||||
/// Display name (e.g. `weapon/dagger` -> `Dagger`)
|
||||
fn display_name(&self) -> String;
|
||||
/// Get all entries, with an optional filter (could be `weapon` for example to show only weapons)
|
||||
fn all(filter: Option<&str>) -> Vec<String>;
|
||||
/// returns a chat message to show the entry (with description and all)
|
||||
fn to_chat(&self, source: &str) -> ChatMessage;
|
||||
}
|
||||
|
||||
pub struct ActionDefinition {
|
||||
pub name: String,
|
||||
pub targets: i32,
|
||||
}
|
||||
|
||||
pub struct ActionResult {
|
||||
pub name: String,
|
||||
pub roll_result: i32,
|
||||
pub roll_target: i32,
|
||||
}
|
|
@ -1,4 +1,19 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use super::{action::{ActionDefinition, ActionResult, GameEntry}, chat_message::ChatMessage};
|
||||
|
||||
pub trait Character<E: GameEntry> : CharacterSheet {
|
||||
/// types of actions that can be done on the specified entry (e.g. cast for spells and wear for armor)
|
||||
fn actions(&self, entry: &E) -> Vec<ActionDefinition>;
|
||||
/// uses an action for a specific character, some actions should be split into multiple
|
||||
///
|
||||
/// for example, an attack sequence will be of:
|
||||
///
|
||||
/// - `Attack` invoked on a weapon entry, which will have the `roll damage` option in the chat
|
||||
/// - `roll damage` will be done on the attacking character, which show the `damage` and `double` actions
|
||||
/// - `damage` will be used on the target character (which could apply reductions as well)
|
||||
fn use_action(&mut self, entry: &E, action: &ActionResult) -> ChatMessage;
|
||||
}
|
||||
|
||||
pub trait CharacterSheet : Default {
|
||||
/// Character sheet inputs (stuff that are not calculated from different items), such as Name, Age, Strength
|
||||
|
@ -18,6 +33,7 @@ pub enum EntryType {
|
|||
Number(i32),
|
||||
Text(String),
|
||||
Bool(bool),
|
||||
Array(Vec<EntryType>),
|
||||
}
|
||||
impl Default for EntryType {
|
||||
fn default() -> Self {
|
||||
|
@ -52,10 +68,20 @@ impl EntryType {
|
|||
}
|
||||
impl std::fmt::Display for EntryType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match(self) {
|
||||
match self {
|
||||
EntryType::Number(n) => write!(f, "{}", n),
|
||||
EntryType::Text(t) => write!(f, "{}", t),
|
||||
EntryType::Bool(b) => write!(f, "{}", b),
|
||||
EntryType::Array(v) => {
|
||||
write!(f, "[ ")?;
|
||||
if v.len() > 0 {
|
||||
for i in v.iter().take(v.len() - 1) {
|
||||
write!(f, "{}, ", i)?;
|
||||
};
|
||||
write!(f, "{} ", v.iter().last().unwrap())?;
|
||||
};
|
||||
write!(f, "]")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
34
src/game/chat_message.rs
Normal file
34
src/game/chat_message.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
/// message text, `{item}` can be used to refer to items and such, where item is of the path such as `items/sword` or `spells/fireball`
|
||||
pub text: String,
|
||||
/// Rolls of the action, if not empty a roll should happen before
|
||||
pub roll: Option<Vec<RollDialogOption>>,
|
||||
/// Optional roll target
|
||||
pub roll_target: Option<i32>,
|
||||
/// Optional action buttons, for a chat message this will be empty
|
||||
pub actions: Option<Vec<String>>,
|
||||
/// Source/Caster/Whoever initiated the action/sent the message
|
||||
pub source: String,
|
||||
/// Targets of the action, for a chat message this will be empty
|
||||
pub targets: Option<Vec<String>>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RollDialogOption {
|
||||
/// Field name
|
||||
pub name: String,
|
||||
/// dice size (aka d6, d12, d20)
|
||||
pub dice_type: u16,
|
||||
/// amount of dice (aka 1d6, 2d12, 10d20)
|
||||
pub dice_amount: u16,
|
||||
/// Constant amout to add (+7, +3, -1)
|
||||
pub constant: i16,
|
||||
/// Extra data, like damage type
|
||||
pub extra: String,
|
||||
/// should be enabled by default
|
||||
pub enabled: bool
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
|
||||
|
||||
pub struct Interaction {
|
||||
/// Max number of entities that can be targeted in one go, 0 for non.
|
||||
pub max_targets: u32,
|
||||
|
||||
}
|
||||
|
||||
pub struct ChatMessage {
|
||||
/// Optional text portion
|
||||
text: String,
|
||||
/// Optional action buttons, for a chat message this will be empty
|
||||
actions: Vec<String>,
|
||||
/// Source/Caster/Whoever initiated the action/sent the message
|
||||
actor: String,
|
||||
/// Targets of the action, for a chat message this will be empty
|
||||
targets: Vec<String>
|
||||
}
|
||||
|
||||
pub struct RollDialogOption {
|
||||
/// Field name
|
||||
name: String,
|
||||
/// dice size (aka d6, d12, d20)
|
||||
dice: u16,
|
||||
/// amount of dice (aka 1d6, 2d12, 10d20)
|
||||
dice_amount: u16,
|
||||
/// Constant amout to add (+7, +3, -1)
|
||||
constant: i16,
|
||||
/// Extra data, like damage type
|
||||
extra: String,
|
||||
/// should be enabled by default
|
||||
enabled: bool
|
||||
}
|
|
@ -1,5 +1,33 @@
|
|||
//! Game Parser and Data types
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use character_sheet::Character;
|
||||
use serde::Serialize;
|
||||
|
||||
pub mod character_sheet;
|
||||
pub mod interaction;
|
||||
pub mod chat_message;
|
||||
pub mod action;
|
||||
|
||||
pub trait GameImpl<C: Character<A> + Serialize, A: action::GameEntry + Serialize> {
|
||||
fn new() -> Self;
|
||||
fn create_character(&mut self);
|
||||
}
|
||||
|
||||
pub struct Game<C: Character<A> + Serialize, A: action::GameEntry + Serialize> {
|
||||
_c: PhantomData<C>,
|
||||
_a: PhantomData<A>,
|
||||
characters: Vec<C>,
|
||||
}
|
||||
impl<C: Character<A> + Serialize, A: action::GameEntry + Serialize> GameImpl<C, A> for Game<C, A> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
_c: PhantomData,
|
||||
_a: PhantomData,
|
||||
characters: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_character(&mut self) {
|
||||
self.characters.push(C::default());
|
||||
}
|
||||
}
|
75
src/main.rs
75
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::<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 {
|
||||
|
|
80
src/pathfinder2r_impl/entry.rs
Normal file
80
src/pathfinder2r_impl/entry.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::game::{action::GameEntry, chat_message::ChatMessage};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum PEntry {
|
||||
Weapon(Weapon),
|
||||
Consumable(Consumable),
|
||||
Spell(Spell),
|
||||
}
|
||||
impl GameEntry for PEntry {
|
||||
fn display_name(&self) -> String {
|
||||
match self {
|
||||
PEntry::Weapon(weapon) => weapon.name.clone(),
|
||||
PEntry::Consumable(consumable) => consumable.name.clone(),
|
||||
PEntry::Spell(spell) => spell.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load(entry: &str) -> Option<PEntry> {
|
||||
println!("loading {}", entry);
|
||||
let json = std::fs::read_to_string(format!("assets/pf2r/{}.json", entry)).ok()?;
|
||||
println!("{}", &json);
|
||||
if entry.starts_with("weapon/") {
|
||||
serde_json::from_str::<Weapon>(&json).map(|w| PEntry::Weapon(w)).ok()
|
||||
}
|
||||
else if entry.starts_with("spell/") {
|
||||
serde_json::from_str::<Spell>(&json).map(|s| PEntry::Spell(s)).ok()
|
||||
}
|
||||
else if entry.starts_with("consumable/") {
|
||||
serde_json::from_str::<Consumable>(&json).map(|s| PEntry::Consumable(s)).ok()
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_chat(&self, source: &str) -> ChatMessage {
|
||||
ChatMessage {
|
||||
text: format!("{} - it might be a {{weapon/short_bow}} it might not", self.display_name()),
|
||||
roll: None,
|
||||
roll_target: None,
|
||||
actions: None,
|
||||
source: String::from(source),
|
||||
targets: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn all(_filter: Option<&str>) -> Vec<String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Weapon {
|
||||
pub name: String,
|
||||
pub two_handed: bool,
|
||||
pub one_handed: bool,
|
||||
pub melee_reach: i32,
|
||||
pub ranged_reach: i32,
|
||||
// this is a weird thing but suffices for now, as it is expected as a string of `1d4 2 2d6`
|
||||
pub damage: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Consumable {
|
||||
name: String,
|
||||
traits: Vec<String>,
|
||||
/// Price in copper
|
||||
price: i32,
|
||||
bulk: i32,
|
||||
desc: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct Spell {
|
||||
name: String,
|
||||
actions: i32,
|
||||
damage: String,
|
||||
damage_type: String,
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
use crate::game::{action::{ActionDefinition, ActionResult, GameEntry}, character_sheet::*, chat_message::{ChatMessage, RollDialogOption}};
|
||||
use tavern_macros::CharacterSheet;
|
||||
use crate::game::character_sheet::*;
|
||||
|
||||
pub mod entry;
|
||||
use entry::{PEntry, Weapon};
|
||||
|
||||
#[derive(Default, CharacterSheet)]
|
||||
pub struct Pathfinder2rCharacterSheet {
|
||||
// Genral stuff
|
||||
#[Input("Name")]
|
||||
name: String,
|
||||
#[Input("Items")]
|
||||
items: String,
|
||||
#[Input("Weapons")]
|
||||
#[InputExpr(set_items, get_items)]
|
||||
weapon: [Option<Weapon>; 2],
|
||||
// Attributes
|
||||
#[Seperator]
|
||||
#[Input("Strength")]
|
||||
|
@ -42,3 +46,81 @@ pub struct Pathfinder2rCharacterSheet {
|
|||
// TODO: Add more skills
|
||||
// TODO: Also add all the rest of the sheet items
|
||||
}
|
||||
impl Pathfinder2rCharacterSheet {
|
||||
fn set_items(&mut self, entry: EntryType) {
|
||||
if let EntryType::Array(a) = entry {
|
||||
let ws: Vec<PEntry> = a
|
||||
.iter()
|
||||
.map(|e| PEntry::load(&e.as_text()))
|
||||
.flatten()
|
||||
.collect();
|
||||
self.weapon = [None, None];
|
||||
for w in ws {
|
||||
if let PEntry::Weapon(w) = w {
|
||||
if self.weapon[0].is_none() {
|
||||
self.weapon[0] = Some(w);
|
||||
} else if self.weapon[1].is_none() {
|
||||
self.weapon[1] = Some(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
let s = entry.as_text();
|
||||
if let Some(PEntry::Weapon(w)) = PEntry::load(&s) {
|
||||
self.weapon[0] = Some(w);
|
||||
self.weapon[1] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn get_items(&self) -> EntryType {
|
||||
EntryType::Array(
|
||||
self.weapon
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|w| EntryType::Text(w.name.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Character<PEntry> for Pathfinder2rCharacterSheet {
|
||||
fn use_action(&mut self, entry: &PEntry, action: &ActionResult) -> ChatMessage {
|
||||
match entry {
|
||||
PEntry::Weapon(_) => ChatMessage {
|
||||
text: String::from("Attack"),
|
||||
roll: Some(vec![RollDialogOption { name: String::from("pierce"), dice_type: 4, dice_amount: 1, constant: 0, extra: String::new(), enabled: true }]),
|
||||
roll_target: Some(10),
|
||||
actions: Some(vec!["damage".to_string(), "double".to_string()]),
|
||||
source: self.name.clone(),
|
||||
targets: None,
|
||||
},
|
||||
PEntry::Consumable(_consumable) => if action.name == "consume" {
|
||||
ChatMessage {
|
||||
text: "Heal".to_string(),
|
||||
roll: Some(vec![RollDialogOption { name: "heal".to_string(), dice_type: 6, dice_amount: 1, constant: 0, extra: String::new(), enabled: true }]),
|
||||
roll_target: None,
|
||||
actions: Some(vec!["heal".to_string()]),
|
||||
source: self.name.clone(),
|
||||
targets: None,
|
||||
}
|
||||
} else { todo!() },
|
||||
PEntry::Spell(_spell) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self, entry: &PEntry) -> Vec<ActionDefinition> {
|
||||
let v;
|
||||
match entry {
|
||||
PEntry::Weapon(_) =>
|
||||
// should technically check if the item is in the user's packpack or something
|
||||
// but for now just return a constant list
|
||||
v = vec![ ("wield", 0), ("attack", 1), ("drop", 0), ("stow", 0) ],
|
||||
PEntry::Consumable(_) => v = vec![("consume", 0), ("stow", 0), ("drop", 0)],
|
||||
PEntry::Spell(_) => v = vec![("cast", 1)],
|
||||
};
|
||||
v.iter()
|
||||
.map(|s| ActionDefinition { name: s.0.to_string(), targets: s.1 })
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue