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

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