diff --git a/assets/web/index.html b/assets/web/index.html index 75d9f22..577061c 100644 --- a/assets/web/index.html +++ b/assets/web/index.html @@ -14,25 +14,57 @@ view.onauxclick = (e) => console.log(e); view.onmousemove = onGameMouseMove; view.oncontextmenu = () => false; + // allow sending chat message using enter (and shift-enter for new line) + document.getElementById('newmsg-content').onkeypress = (e) => { + if(e.key == "Enter" && !e.shiftKey) { + sendChatMessage(); + return 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'; + // get all chat history as soon as we get in + tavern.get_chat_history(0, 0); } else { alert("Invalid username or password!"); } } + tavern.onmessage = (m) => { + console.log(m); + let msg = document.createElement('div'); + msg.className = 'chat-message'; + // #abusing_style_order_as_both_id_variable_and_forcing_chronological_order + msg.style.order = m.id; + msg.innerHTML = ` + ${m.source} +
+
+ ${m.text} + ` + let history = document.getElementById('chat-history'); + // this is to force update everytime we get a duplicate msg to allow msg editing (yay) + let exists = Array.from(history.children).filter(e => e.style.order == m.id)[0]; + if(exists) { + history.removeChild(exists); + } + history.appendChild(msg); + } + function onLoginClick() { + let username = document.getElementById('login-username').value; + let pass = document.getElementById('login-pass').value; + if(username == 'test') { + // TODO: Remove this for when im done dev-ing with this file + tavern.onlogin(true); + } + tavern.login(username, pass); + } function onGameViewScroll(event) { let map = document.getElementById('map'); mapScale += (event.wheelDelta / 1800.0); @@ -50,6 +82,13 @@ map.style.top = `${mapOffsetY}px`; } } + function sendChatMessage() { + let tb = document.getElementById('newmsg-content'); + // get the msg and reset the textarea + let text = tb.value; + tb.value = ''; + tavern.simple_msg(text); + } @@ -100,10 +151,11 @@
- Chat history
-
- new message input +
+ + +
diff --git a/assets/web/socket.js b/assets/web/socket.js index 1018df6..3c9d904 100644 --- a/assets/web/socket.js +++ b/assets/web/socket.js @@ -17,8 +17,24 @@ tavern.socket.onmessage = (m) => { tavern.socket.loggedIn = m.login.success; tavern.call(tavern.onlogin, tavern.socket.loggedIn); } + if(m.message) { + tavern.call(tavern.onmessage, m.message) + } + if(m.get_chat_history) { + m.get_chat_history.forEach(msg => { + tavern.call(tavern.onmessage, msg); + }); + } } tavern.login = (username, password) => { if(!tavern.connected || tavern.loggedIn) { return false; } tavern.socket.send(JSON.stringify({ login: { username, password }})); +} +tavern.simple_msg = (msg, token) => { + if(!tavern.connected || tavern.loggedIn) { return false; } + tavern.socket.send(JSON.stringify({ message: { text: msg, source: token ?? "" } })); +} +tavern.get_chat_history = (from, amount) => { + if(!tavern.connected || tavern.loggedIn) { return false; } + tavern.socket.send(JSON.stringify({ get_chat_history: { from: from, amount: amount } })) } \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 08061d7..c65e844 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -13,6 +13,7 @@ pub enum Request { Error, Login(login::LoginRequest), Message(ChatMessage), + GetChatHistory { amount: usize, from: usize }, Quit, Shutdown } @@ -22,6 +23,7 @@ pub enum Response { Error(RequestError), Login(login::LoginResult), Message(ChatMessage), + GetChatHistory(Vec), Quit { id: String }, Shutdown, diff --git a/src/game/character_sheet.rs b/src/game/character_sheet.rs index 2e5ce48..58e7414 100644 --- a/src/game/character_sheet.rs +++ b/src/game/character_sheet.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use super::{action::{ActionDefinition, ActionResult, GameEntry}, chat_message::ChatMessage}; +use super::{entry::{ActionDefinition, ActionResult, GameEntry}, chat_message::ChatMessage}; pub trait Character : CharacterSheet { /// types of actions that can be done on the specified entry (e.g. cast for spells and wear for armor) diff --git a/src/game/chat_message.rs b/src/game/chat_message.rs index 586c7d0..6b188ca 100644 --- a/src/game/chat_message.rs +++ b/src/game/chat_message.rs @@ -14,9 +14,14 @@ pub struct ChatMessage { /// 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> + pub targets: Option>, + /// message id, should be left emitted or 0 for new messages + #[serde(default = "default_id")] + pub id: usize, } +fn default_id() -> usize { 0 } + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RollDialogOption { /// Field name diff --git a/src/game/action.rs b/src/game/entry.rs similarity index 86% rename from src/game/action.rs rename to src/game/entry.rs index fde3e22..2284c3a 100644 --- a/src/game/action.rs +++ b/src/game/entry.rs @@ -16,7 +16,9 @@ pub trait GameEntry : Serialize + Sized { /// 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; + fn all(filter: Option<&str>) -> Vec; + /// Get all categories (such as weapons/consumables/spells) + fn categories() -> Vec; /// returns a chat message to show the entry (with description and all) fn to_chat(&self, source: &str) -> ChatMessage; } diff --git a/src/game/mod.rs b/src/game/mod.rs index ac9e863..5826638 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -6,19 +6,19 @@ use serde::Serialize; pub mod character_sheet; pub mod chat_message; -pub mod action; +pub mod entry; -pub trait GameImpl + Serialize, A: action::GameEntry + Serialize> { +pub trait GameImpl + Serialize, A: entry::GameEntry + Serialize> { fn new() -> Self; fn create_character(&mut self); } -pub struct Game + Serialize, A: action::GameEntry + Serialize> { +pub struct Game + Serialize, A: entry::GameEntry + Serialize> { _c: PhantomData, _a: PhantomData, characters: Vec, } -impl + Serialize, A: action::GameEntry + Serialize> GameImpl for Game { +impl + Serialize, A: entry::GameEntry + Serialize> GameImpl for Game { fn new() -> Self { Self { _c: PhantomData, diff --git a/src/lib.rs b/src/lib.rs index 37474ec..388583d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use game::{Game, GameImpl}; +use game::{chat_message::ChatMessage, Game, GameImpl}; use tokio::sync::{broadcast, mpsc}; pub mod user; @@ -8,25 +8,55 @@ pub mod game; pub mod pathfinder2r_impl; pub struct GameServer { - game: Game + _game: Game, + chat: Vec<(String, ChatMessage)>, } impl GameServer { pub fn new() -> Self { Self { - game: Game::new(), + _game: Game::new(), + chat: Vec::new(), } } - pub async fn server_loop(mut self, mut msgs: mpsc::Receiver<(String, api::Request)>, mut broadcast: broadcast::Sender) { + pub async fn server_loop(mut self, mut msgs: mpsc::Receiver<(String, api::Request)>, broadcast: broadcast::Sender) { while let Some(req) = msgs.recv().await { // TODO: do stuff yo! let (id, req) = req; + println!("Got message from {}: {:?}", &id, &req); + match req { api::Request::Error => {}, api::Request::Login(_) => {}, - api::Request::Message(msg) => { _ = broadcast.send(api::Response::Message(msg)); }, + api::Request::Message(mut msg) => { + if msg.id == 0 || msg.id >= self.chat.len() { + msg.id = self.chat.len() + 1; // set the message id, 0 is invalid + } + // TODO: check if the editor is an admin as well + else if id == self.chat[msg.id].0 { + self.chat[msg.id] = (id.clone(), msg.clone()); + } + else { + // if its an edit message and editor is not the owner, skip + continue; + } + if msg.source.is_empty() { + msg.source = id.clone(); + } + self.chat.push((id.clone(), msg.clone())); + _ = broadcast.send(api::Response::Message(msg)); + }, api::Request::Quit => { _ = broadcast.send(api::Response::Quit { id })}, api::Request::Shutdown => todo!(), + api::Request::GetChatHistory { mut amount, from: last_msg } => { + if amount == 0 { amount = self.chat.len(); } + let history: Vec = self.chat.iter() + .skip(last_msg) + .take(amount) + .map(|m| m.1.clone()) + .collect(); + _ = broadcast.send(api::Response::GetChatHistory(history)); + }, } } _ = broadcast.send(api::Response::Shutdown); diff --git a/src/main.rs b/src/main.rs index fb15575..754ad60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,6 @@ async fn socket_receiver(mut recv: SplitStream, msend: mpsc::Send match msg { Message::Text(t) => { let req = serde_json::from_str::(&t).unwrap_or_default(); - println!("Got message: {:?}", t); let erred = msend.send((id.clone(), req)).await.is_err(); if erred { break; @@ -68,7 +67,7 @@ async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, R match msg { Message::Text(t) => { let req = serde_json::from_str::(&t).unwrap_or_default(); - println!("Got message: {:?}", t); + println!("Got unauthorized message: {:?}", t); match req { // TODO: Actual signing in mechanism with multiple ids :) Request::Login(r) => if r.username == "rusty" { diff --git a/src/pathfinder2r_impl/entry.rs b/src/pathfinder2r_impl/entry.rs index bfdf882..a917021 100644 --- a/src/pathfinder2r_impl/entry.rs +++ b/src/pathfinder2r_impl/entry.rs @@ -1,34 +1,34 @@ use serde::{Deserialize, Serialize}; -use crate::game::{action::GameEntry, chat_message::ChatMessage}; +use crate::game::{entry::GameEntry, chat_message::ChatMessage}; #[derive(Serialize, Deserialize)] -pub enum PEntry { +pub enum Entry { Weapon(Weapon), Consumable(Consumable), Spell(Spell), } -impl GameEntry for PEntry { +impl GameEntry for Entry { 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(), + Entry::Weapon(weapon) => weapon.name.clone(), + Entry::Consumable(consumable) => consumable.name.clone(), + Entry::Spell(spell) => spell.name.clone(), } } - fn load(entry: &str) -> Option { + fn load(entry: &str) -> Option { println!("loading {}", entry); let json = std::fs::read_to_string(format!("assets/pf2r/{}.json", entry)).ok()?; println!("{}", &json); if entry.starts_with("weapon/") { - serde_json::from_str::(&json).map(|w| PEntry::Weapon(w)).ok() + serde_json::from_str::(&json).map(|w| Entry::Weapon(w)).ok() } else if entry.starts_with("spell/") { - serde_json::from_str::(&json).map(|s| PEntry::Spell(s)).ok() + serde_json::from_str::(&json).map(|s| Entry::Spell(s)).ok() } else if entry.starts_with("consumable/") { - serde_json::from_str::(&json).map(|s| PEntry::Consumable(s)).ok() + serde_json::from_str::(&json).map(|s| Entry::Consumable(s)).ok() } else { None @@ -43,12 +43,22 @@ impl GameEntry for PEntry { actions: None, source: String::from(source), targets: None, + id: 0, } } fn all(_filter: Option<&str>) -> Vec { todo!() } + + fn categories() -> Vec { + vec![ + "weapon", "consumable", "spell" + ] + .iter() + .map(|s| s.to_string()) + .collect() + } } #[derive(Serialize, Deserialize, Default)] diff --git a/src/pathfinder2r_impl/mod.rs b/src/pathfinder2r_impl/mod.rs index da55e8b..b1586dd 100644 --- a/src/pathfinder2r_impl/mod.rs +++ b/src/pathfinder2r_impl/mod.rs @@ -1,9 +1,9 @@ -use crate::game::{action::{ActionDefinition, ActionResult, GameEntry}, character_sheet::*, chat_message::{ChatMessage, RollDialogOption}}; +use crate::game::{entry::{ActionDefinition, ActionResult, GameEntry}, character_sheet::*, chat_message::{ChatMessage, RollDialogOption}}; use serde::Serialize; use tavern_macros::CharacterSheet; pub mod entry; -use entry::{PEntry, Weapon}; +use entry::{Entry, Weapon}; #[derive(Default, CharacterSheet, Serialize)] pub struct Pathfinder2rCharacterSheet { @@ -50,14 +50,14 @@ pub struct Pathfinder2rCharacterSheet { impl Pathfinder2rCharacterSheet { fn set_items(&mut self, entry: EntryType) { if let EntryType::Array(a) = entry { - let ws: Vec = a + let ws: Vec = a .iter() - .map(|e| PEntry::load(&e.as_text())) + .map(|e| Entry::load(&e.as_text())) .flatten() .collect(); self.weapon = [None, None]; for w in ws { - if let PEntry::Weapon(w) = w { + if let Entry::Weapon(w) = w { if self.weapon[0].is_none() { self.weapon[0] = Some(w); } else if self.weapon[1].is_none() { @@ -69,7 +69,7 @@ impl Pathfinder2rCharacterSheet { } else { let s = entry.as_text(); - if let Some(PEntry::Weapon(w)) = PEntry::load(&s) { + if let Some(Entry::Weapon(w)) = Entry::load(&s) { self.weapon[0] = Some(w); self.weapon[1] = None; } @@ -85,18 +85,19 @@ impl Pathfinder2rCharacterSheet { ) } } -impl Character for Pathfinder2rCharacterSheet { - fn use_action(&mut self, entry: &PEntry, action: &ActionResult) -> ChatMessage { +impl Character for Pathfinder2rCharacterSheet { + fn use_action(&mut self, entry: &Entry, action: &ActionResult) -> ChatMessage { match entry { - PEntry::Weapon(_) => ChatMessage { + Entry::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, + id: 0, }, - PEntry::Consumable(_consumable) => if action.name == "consume" { + Entry::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 }]), @@ -104,21 +105,22 @@ impl Character for Pathfinder2rCharacterSheet { actions: Some(vec!["heal".to_string()]), source: self.name.clone(), targets: None, + id: 0, } } else { todo!() }, - PEntry::Spell(_spell) => todo!(), + Entry::Spell(_spell) => todo!(), } } - fn actions(&self, entry: &PEntry) -> Vec { + fn actions(&self, entry: &Entry) -> Vec { let v; match entry { - PEntry::Weapon(_) => + Entry::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)], + Entry::Consumable(_) => v = vec![("consume", 0), ("stow", 0), ("drop", 0)], + Entry::Spell(_) => v = vec![("cast", 1)], }; v.iter() .map(|s| ActionDefinition { name: s.0.to_string(), targets: s.1 })