some tokens stuff (ye i started to work on the map, also i 'fixed' the context menu for msgs but still needs to be impl-ed)

This commit is contained in:
Rusty Striker 2024-10-08 19:55:48 +03:00
parent 97475599a7
commit be6dd7c0e4
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
13 changed files with 296 additions and 76 deletions

1
Cargo.lock generated
View file

@ -1031,6 +1031,7 @@ name = "open_tavern"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"base64",
"futures-util", "futures-util",
"parking_lot", "parking_lot",
"serde", "serde",

View file

@ -12,3 +12,4 @@ sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] }
tavern_macros = { version = "0.1.0", path = "tavern_macros" } tavern_macros = { version = "0.1.0", path = "tavern_macros" }
parking_lot = "0.12.3" parking_lot = "0.12.3"
futures-util = "0.3.30" futures-util = "0.3.30"
base64 = "0.22.1"

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View file

@ -4,9 +4,11 @@
<script src="./socket.js"></script> <script src="./socket.js"></script>
<script> <script>
// init - game view (map) // init - game view (map)
const GRID_SIZE = 200; // Grid size in pixels
var mapScale = 1.0; var mapScale = 1.0;
var mapOffsetX = 0.0; var mapOffsetX = 0.0;
var mapOffsetY = 0.0; var mapOffsetY = 0.0;
function init() { function init() {
let view = document.getElementById('game-view'); let view = document.getElementById('game-view');
view.onwheel = onGameViewScroll; view.onwheel = onGameViewScroll;
@ -35,6 +37,11 @@
} }
// focus on the username field for the sake of just pressing enter multiple times // focus on the username field for the sake of just pressing enter multiple times
document.getElementById('login-username').focus(); document.getElementById('login-username').focus();
document.body.onclick = (e) => {
document.getElementById('msg-context-menu').style.display = 'none';
}
// TODO: Remove when done dev-ing
tavern.onmessage({ text: 'test', id: 1, source: 'rusty', character: 'bart' });
} }
tavern.onlogin = (s) => { tavern.onlogin = (s) => {
@ -45,6 +52,7 @@
game.style.display = 'flex'; game.style.display = 'flex';
// get last 50 msgs (i think that is enough for now) when we get in // get last 50 msgs (i think that is enough for now) when we get in
tavern.get_last_msgs(50); tavern.get_last_msgs(50);
tavern.get_tokens();
} }
else { else {
alert("Invalid username or password!"); alert("Invalid username or password!");
@ -57,13 +65,21 @@
// #abusing_style_order_as_both_id_variable_and_forcing_chronological_order // #abusing_style_order_as_both_id_variable_and_forcing_chronological_order
msg.style.order = m.id; msg.style.order = m.id;
msg.innerHTML = ` msg.innerHTML = `
<b style='align-self: center;'>${m.source}</b> <p style='align-self: center; margin: 4px 0;'>
<br> <b>${m.character ?? ''}</b>
(${m.source})
</p>
<hr style='width: 75%;' /> <hr style='width: 75%;' />
${m.text} <p style='margin: 4px 2px;'>
${m.text}
</p>
` `
msg.oncontextmenu = () => { msg.oncontextmenu = (e) => {
document.getElementById('msg-context-menu').style.display = 'flex'; if(e.shiftKey) { return true; }
let cm = document.getElementById('msg-context-menu');
cm.style.display = 'flex';
cm.style.top = `${e.pageY}px`;
cm.style.left = `${e.pageX}px`;
return false; return false;
} }
let history = document.getElementById('chat-history'); let history = document.getElementById('chat-history');
@ -75,6 +91,26 @@
history.appendChild(msg); history.appendChild(msg);
msg.scrollIntoView(); msg.scrollIntoView();
} }
tavern.onspawntoken = (t) => {
console.log(t);
let map = document.getElementById('map');
let token = document.createElement('div');
token.className = 'token';
token.style.top = `${t.y * GRID_SIZE}px`;
token.style.left = `${t.x * GRID_SIZE}px`;
token.token_id = t.token_id;
token.innerHTML = `
<img src='data:image/jpg;base64,${t.img}'>
`
map.append(token);
}
tavern.onmovetoken = (m) => {
let token = Array.from(document.getElementsByClassName('token')).filter(t => t.token_id == m.token_id)[0]
if(token) {
token.style.top = `${m.y * GRID_SIZE}px`;
token.style.left = `${m.x * GRID_SIZE}px`;
}
}
function onLoginClick() { function onLoginClick() {
let username = document.getElementById('login-username').value; let username = document.getElementById('login-username').value;
let pass = document.getElementById('login-pass').value; let pass = document.getElementById('login-pass').value;
@ -152,6 +188,36 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#msg-context-menu {
display: none;
position: absolute;
z-index: 1000;
padding: 4px;
}
#msg-context-menu ul {
padding: 0px;
margin: 0px;
list-style: none;
}
#msg-context-menu ul li {
padding: 4px;
}
#msg-context-menu ul li:hover {
background: darkgray;
cursor: pointer;
}
.token {
position: absolute;
transition:
top 0.5s ease-in,
left 0.5s ease-in;
}
.token img {
cursor: grab;
width: 200px;
height: 200px;
}
</style> </style>
</head> </head>
@ -184,13 +250,12 @@
</div> </div>
<div id="map" style="position:absolute;"> <div id="map" style="position:absolute;">
<img src="https://rustystriker.dev/molly.jpg" height="200%" > <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> </div>
</div> </div>
<div id="msg-context-menu" class="chat-message" style="display: none; position: absolute; z-index: 1000; top: 0px;"> <div id="msg-context-menu" class="chat-message">
<ul> <ul>
<li onclick='document.getElementById("msg-context-menu").style.display="none"'>Edit</li> <li>Delete (TODO)</li>
</ul> </ul>
</div> </div>
</body> </body>

View file

@ -1,5 +1,6 @@
const tavern = { const tavern = {
socket: socket = new WebSocket('ws:/' + window.location.host + '/ws'), socket: socket = new WebSocket('ws:/' + window.location.host + '/ws'),
msgs: [],
connected: false, connected: false,
loggedIn: false, loggedIn: false,
call: (f, ...args) => { call: (f, ...args) => {
@ -8,6 +9,27 @@ const tavern = {
} }
} }
}; };
tavern.add_msg_to_history = (m) => {
let id = m.id - 1;
if(id >= 0) {
if(id < tavern.msgs.length) {
if(tavern.msgs[id].id == id + 1) {
tavern.msgs[id] = m;
}
else {
for(let i = 0; i < tavern.msgs.length; i += 1) {
if(tavern.msgs[i].id > id) {
tavern.msgs.splice(i, 0, m);
break;
}
}
}
}
else {
tavern.msgs.push(m)
}
}
}
tavern.socket.onopen = () => tavern.connected = true; tavern.socket.onopen = () => tavern.connected = true;
tavern.socket.onmessage = (m) => { tavern.socket.onmessage = (m) => {
@ -18,13 +40,21 @@ tavern.socket.onmessage = (m) => {
tavern.call(tavern.onlogin, tavern.socket.loggedIn); tavern.call(tavern.onlogin, tavern.socket.loggedIn);
} }
if(m.message) { if(m.message) {
tavern.call(tavern.onmessage, m.message) tavern.add_msg_to_history(m.message);
tavern.call(tavern.onmessage, m.message);
} }
if(m.get_chat_history) { if(m.get_chat_history) {
m.get_chat_history.forEach(msg => { m.get_chat_history.forEach(msg => {
tavern.add_msg_to_history(msg);
tavern.call(tavern.onmessage, msg); tavern.call(tavern.onmessage, msg);
}); });
} }
if(m.spawn_token) {
tavern.call(tavern.onspawntoken, m.spawn_token);
}
if(m.move_token) {
tavern.call(tavern.onmovetoken, m.move_token);
}
} }
tavern.login = (username, password) => { tavern.login = (username, password) => {
if(!tavern.connected || tavern.loggedIn) { return false; } if(!tavern.connected || tavern.loggedIn) { return false; }
@ -32,7 +62,14 @@ tavern.login = (username, password) => {
} }
tavern.simple_msg = (msg, token) => { tavern.simple_msg = (msg, token) => {
if(!tavern.connected || tavern.loggedIn) { return false; } if(!tavern.connected || tavern.loggedIn) { return false; }
tavern.socket.send(JSON.stringify({ message: { text: msg, source: token ?? "" } })); tavern.socket.send(JSON.stringify({ message: { text: msg, character: token ?? "" } }));
}
tavern.edit_msg = (new_text, id) => {
if(id <= tavern.msgs.length && id > 0) {
let msg = tavern.msgs[id - 1];
msg.text = new_text;
tavern.socket.send(JSON.stringify({ message: msg }));
}
} }
tavern.get_chat_history = (from, amount) => { tavern.get_chat_history = (from, amount) => {
if(!tavern.connected || tavern.loggedIn) { return false; } if(!tavern.connected || tavern.loggedIn) { return false; }
@ -42,3 +79,11 @@ tavern.get_last_msgs = (amount) => {
if(!tavern.connected || tavern.loggedIn) { return false; } if(!tavern.connected || tavern.loggedIn) { return false; }
tavern.socket.send(JSON.stringify({ get_last_messages: { amount: amount } })) tavern.socket.send(JSON.stringify({ get_last_messages: { amount: amount } }))
} }
tavern.get_tokens = () => {
if(!tavern.connected || tavern.loggedIn) { return false; }
tavern.socket.send(JSON.stringify('get_tokens'));
}
tavern.move_token = (id, x, y) => {
if(!tavern.connected || tavern.loggedIn) { return false; }
tavern.socket.send(JSON.stringify({ move_token: { token_id: id, x: x, y: y } }));
}

View file

@ -15,7 +15,7 @@
[ ] impl different requests [ ] impl different requests
[ ] actual normal login [ ] actual normal login
[ ] allow sending of old info [x] allow sending of old info
[x] chat history [x] chat history
[ ] send texture (map/token/image) [ ] send texture (map/token/image)
[ ] force show something [ ] force show something

View file

@ -15,7 +15,11 @@ pub enum Request {
Message(ChatMessage), Message(ChatMessage),
GetChatHistory { amount: usize, from: usize }, GetChatHistory { amount: usize, from: usize },
GetLastMessages { amount: usize, }, GetLastMessages { amount: usize, },
GetTokens,
SpawnToken { x: i32, y: i32, img_path: String },
MoveToken { token_id: usize, x: i32, y: i32 },
Quit, Quit,
Kick(String),
Shutdown Shutdown
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
@ -25,6 +29,8 @@ pub enum Response {
Login(login::LoginResult), Login(login::LoginResult),
Message(ChatMessage), Message(ChatMessage),
GetChatHistory(Vec<ChatMessage>), GetChatHistory(Vec<ChatMessage>),
MoveToken { token_id: usize, x: i32, y: i32 },
SpawnToken { token_id: usize, x: i32, y: i32, img: String },
Quit { id: String }, Quit { id: String },
Shutdown, Shutdown,

View file

@ -1,24 +1,107 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ChatMessage { 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` /// 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, pub text: String,
/// Source user initiated the action/sent the message
#[serde(default = "default_source")]
pub source: String,
/// Character "sending" the message
pub character: Option<String>,
/// whisper item
pub whisper: Option<String>,
/// Rolls of the action, if not empty a roll should happen before /// Rolls of the action, if not empty a roll should happen before
pub roll: Option<Vec<RollDialogOption>>, pub roll: Option<Vec<RollDialogOption>>,
/// Optional roll target /// Optional roll target
pub roll_target: Option<i32>, pub roll_target: Option<i32>,
/// Optional action buttons, for a chat message this will be empty /// Optional action buttons, for a chat message this will be empty
pub actions: Option<Vec<String>>, 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 /// Targets of the action, for a chat message this will be empty
pub targets: Option<Vec<String>>, pub targets: Option<Vec<String>>,
/// message id, should be left emitted or 0 for new messages /// message id, should be left emitted or 0 for new messages
#[serde(default = "default_id")] #[serde(default = "default_id")]
pub id: usize, pub id: usize,
} }
fn default_source() -> String {
String::new()
}
impl ChatMessage {
/// Creates a new chat message with a given text and source
pub fn new(text: String) -> Self {
Self {
text, ..Default::default()
}
}
pub fn source(mut self, source: String) -> Self {
self.source = source;
self
}
/// sets the whisper value of the message
pub fn whisper(mut self, whisper: Option<String>) -> Self {
self.whisper = whisper;
self
}
/// sets the roll value for the message (chaining multiple will override each other)
pub fn roll(mut self, roll: Option<Vec<RollDialogOption>>) -> Self {
self.roll = roll;
self
}
/// adds a single roll to the message (chaining multiple will add multiple rolls)
pub fn with_roll(mut self, roll: RollDialogOption) -> Self {
if let Some(rs) = &mut self.roll {
rs.push(roll);
}
else {
self.roll = Some(vec![roll]);
}
self
}
pub fn roll_target(mut self, target: Option<i32>) -> Self {
self.roll_target = target;
self
}
/// sets the actions value (chaining multiple will override)
pub fn actions(mut self, actions: Option<Vec<String>>) -> Self {
self.actions = actions;
self
}
/// adds a single action to the message (chaining multiple will add multiple actions)
pub fn with_action(mut self, action: String) -> Self {
if let Some(acts) = &mut self.actions {
acts.push(action);
}
else {
self.actions = Some(vec![action]);
}
self
}
/// sets the targets value (chaining multiple will override)
pub fn targets(mut self, targets: Option<Vec<String>>) -> Self {
self.targets = targets;
self
}
/// adds a single target to the message (chaining multiple will add multiple targets)
pub fn with_target(mut self, target: String) -> Self {
if let Some(targets) = &mut self.targets {
targets.push(target);
}
else {
self.targets = Some(vec![target]);
}
self
}
/// Sets the message id
///
/// WARNING: duplicate message id will cause an overwrite of the original message (and an edit at the client)
pub fn id(mut self, id: usize) -> Self {
self.id = id;
self
}
}
fn default_id() -> usize { 0 } fn default_id() -> usize { 0 }

View file

@ -20,7 +20,7 @@ pub trait GameEntry : Serialize + Sized {
/// Get all categories (such as weapons/consumables/spells) /// Get all categories (such as weapons/consumables/spells)
fn categories() -> Vec<String>; fn categories() -> Vec<String>;
/// returns a chat message to show the entry (with description and all) /// returns a chat message to show the entry (with description and all)
fn to_chat(&self, source: &str) -> ChatMessage; fn to_chat(&self) -> ChatMessage;
} }
pub struct ActionDefinition { pub struct ActionDefinition {

View file

@ -1,75 +1,109 @@
use game::{chat_message::ChatMessage, Game, GameImpl}; use game::{chat_message::ChatMessage, Game, GameImpl};
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
pub mod user;
pub mod table;
pub mod api; pub mod api;
pub mod game; pub mod game;
pub mod pathfinder2r_impl; pub mod pathfinder2r_impl;
pub mod table;
pub mod user;
pub struct GameServer { pub struct GameServer {
_game: Game<pathfinder2r_impl::Pathfinder2rCharacterSheet, pathfinder2r_impl::entry::Entry>, _game: Game<pathfinder2r_impl::Pathfinder2rCharacterSheet, pathfinder2r_impl::entry::Entry>,
tokens: Vec<(String, i32, i32)>,
chat: Vec<(String, ChatMessage)>, chat: Vec<(String, ChatMessage)>,
} }
impl GameServer { impl GameServer {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
_game: Game::new(), _game: Game::new(),
tokens: vec![("assets/pf2r/tokens/louise.jpg".to_string(), 2, 2)],
chat: Vec::new(), chat: Vec::new(),
} }
} }
pub async fn server_loop(mut self, mut msgs: mpsc::Receiver<(String, api::Request)>, broadcast: broadcast::Sender<(Option<String>, api::Response)>) { pub async fn server_loop(
mut self,
mut msgs: mpsc::Receiver<(String, api::Request)>,
broadcast: broadcast::Sender<(Option<String>, api::Response)>,
) {
while let Some(req) = msgs.recv().await { while let Some(req) = msgs.recv().await {
// TODO: do stuff yo! // TODO: do stuff yo!
let (id, req) = req; let (id, req) = req;
println!("Got message from {}: {:?}", &id, &req); println!("Got message from {}: {:?}", &id, &req);
match req { match req {
api::Request::Error => {}, api::Request::Error => {}
api::Request::Login(_) => {}, api::Request::Login(_) => {}
api::Request::Message(mut msg) => { api::Request::Message(mut msg) => {
if msg.id == 0 || msg.id >= self.chat.len() { if msg.id == 0 || msg.id > self.chat.len() {
msg.id = self.chat.len() + 1; // set the message id, 0 is invalid msg.id = self.chat.len() + 1; // set the message id, 0 is invalid
} }
// TODO: check if the editor is an admin as well // TODO: check if the editor is an admin as well
else if id == self.chat[msg.id].0 { else if id == self.chat[msg.id - 1].0 {
self.chat[msg.id] = (id.clone(), msg.clone()); self.chat[msg.id - 1] = (id.clone(), msg.clone());
} } else {
else {
// if its an edit message and editor is not the owner, skip // if its an edit message and editor is not the owner, skip
continue; continue;
} }
if msg.source.is_empty() { // Force the sender id to be the new id of the message, even if an id was provided
msg.source = format!("({})", id.clone()); msg.source = id.clone();
}
else {
msg.source = format!("{} ({})", msg.source, id.clone());
}
self.chat.push((id.clone(), msg.clone())); self.chat.push((id.clone(), msg.clone()));
_ = broadcast.send((None, api::Response::Message(msg))); if msg.whisper.is_some() {
}, _ = broadcast.send((Some(id.clone()), api::Response::Message(msg.clone())));
api::Request::Quit => { _ = broadcast.send((None, api::Response::Quit { id }))}, }
api::Request::Shutdown => todo!(), _ = broadcast.send((msg.whisper.clone(), api::Response::Message(msg)));
api::Request::GetChatHistory { mut amount, from: last_msg } => { }
if amount == 0 { amount = self.chat.len(); } api::Request::GetChatHistory {
let history: Vec<ChatMessage> = self.chat.iter() mut amount,
from: last_msg,
} => {
if amount == 0 {
amount = self.chat.len();
}
let history: Vec<ChatMessage> = self
.chat
.iter()
.skip(last_msg) .skip(last_msg)
.take(amount) .take(amount)
.map(|m| m.1.clone()) .map(|m| m.1.clone())
.collect(); .collect();
_ = broadcast.send((Some(id), api::Response::GetChatHistory(history))); _ = broadcast.send((Some(id), api::Response::GetChatHistory(history)));
}, }
api::Request::GetLastMessages { mut amount } => { api::Request::GetLastMessages { mut amount } => {
if amount == 0 { amount = self.chat.len(); } if amount == 0 {
let start = if amount >= self.chat.len() { self.chat.len() } else { self.chat.len() - amount }; amount = self.chat.len();
let history: Vec<ChatMessage> = self.chat.iter() }
.skip(start) let start = if amount >= self.chat.len() {
.map(|m| m.1.clone()) self.chat.len()
.collect(); } else {
self.chat.len() - amount
};
let history: Vec<ChatMessage> =
self.chat.iter().skip(start).map(|m| m.1.clone()).collect();
_ = broadcast.send((Some(id), api::Response::GetChatHistory(history))); _ = broadcast.send((Some(id), api::Response::GetChatHistory(history)));
}, },
api::Request::GetTokens => {
for (i, (path, x, y)) in self.tokens.iter().enumerate() {
let bits = std::fs::read(path).expect("FAILED READING TOKEN IMAGE");
let img = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &bits);
_ = broadcast.send((Some(id.clone()), api::Response::SpawnToken { token_id: i, x: *x, y: *y, img }));
}
},
api::Request::SpawnToken { x, y, img_path } => {
let token_id = self.tokens.len();
self.tokens.push((img_path.clone(), x, y));
let bits = std::fs::read(img_path).expect("FAILED READING TOKEN IMAGE");
let img = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &bits);
_ = broadcast.send((Some(id.clone()), api::Response::SpawnToken { token_id, x, y, img }));
},
api::Request::MoveToken { token_id, x, y } => {
// TODO: add check to make sure the actor is authorized to move the token
_ = broadcast.send((None, api::Response::MoveToken { token_id, x, y }));
},
api::Request::Quit => _ = broadcast.send((None, api::Response::Quit { id })),
api::Request::Kick(id) => _ = broadcast.send((Some(id), api::Response::Shutdown)),
api::Request::Shutdown => break,
} }
} }
_ = broadcast.send((None, api::Response::Shutdown)); _ = broadcast.send((None, api::Response::Shutdown));

View file

@ -31,6 +31,7 @@ async fn socket_receiver(mut recv: SplitStream<ws::WebSocket>, msend: mpsc::Send
if let Ok(msg) = msg { if let Ok(msg) = msg {
match msg { match msg {
Message::Text(t) => { Message::Text(t) => {
println!("Got message from {}: {}", &id, &t);
let req = serde_json::from_str::<Request>(&t).unwrap_or_default(); let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
let erred = msend.send((id.clone(), req)).await.is_err(); let erred = msend.send((id.clone(), req)).await.is_err();
if erred { if erred {
@ -54,7 +55,8 @@ async fn socket_sender(id: String, mut send: SplitSink<ws::WebSocket, ws::Messag
while let Ok((to_id, msg)) = brecv.recv().await { while let Ok((to_id, msg)) = brecv.recv().await {
if to_id.is_none() || to_id.map(|t| t == id).unwrap_or(false) { if to_id.is_none() || to_id.map(|t| t == id).unwrap_or(false) {
let err = send.send(ws::Message::Text(serde_json::to_string(&msg).unwrap())).await.is_err(); let err = send.send(ws::Message::Text(serde_json::to_string(&msg).unwrap())).await.is_err();
if err { if err || matches!(msg, Response::Shutdown) {
_ = send.close().await;
break; break;
} }
} }

View file

@ -35,16 +35,9 @@ impl GameEntry for Entry {
} }
} }
fn to_chat(&self, source: &str) -> ChatMessage { fn to_chat(&self) -> ChatMessage {
ChatMessage { let text = format!("{} - it might be a {{weapon/short_bow}} it might not", self.display_name());
text: format!("{} - it might be a {{weapon/short_bow}} it might not", self.display_name()), ChatMessage::new(text)
roll: None,
roll_target: None,
actions: None,
source: String::from(source),
targets: None,
id: 0,
}
} }
fn all(_filter: Option<&str>) -> Vec<String> { fn all(_filter: Option<&str>) -> Vec<String> {

View file

@ -88,25 +88,15 @@ impl Pathfinder2rCharacterSheet {
impl Character<Entry> for Pathfinder2rCharacterSheet { impl Character<Entry> for Pathfinder2rCharacterSheet {
fn use_action(&mut self, entry: &Entry, action: &ActionResult) -> ChatMessage { fn use_action(&mut self, entry: &Entry, action: &ActionResult) -> ChatMessage {
match entry { match entry {
Entry::Weapon(_) => ChatMessage { Entry::Weapon(_) => ChatMessage::new("Attack".to_string())
text: String::from("Attack"), .with_roll(RollDialogOption { name: String::from("pierce"), dice_type: 4, dice_amount: 1, constant: 0, extra: String::new(), enabled: true })
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))
roll_target: Some(10), .with_action("damage".to_string())
actions: Some(vec!["damage".to_string(), "double".to_string()]), .with_action("double".to_string()),
source: self.name.clone(),
targets: None,
id: 0,
},
Entry::Consumable(_consumable) => if action.name == "consume" { Entry::Consumable(_consumable) => if action.name == "consume" {
ChatMessage { ChatMessage::new("Heal".to_string())
text: "Heal".to_string(), .with_roll(RollDialogOption { name: "heal".to_string(), dice_type: 6, dice_amount: 1, constant: 0, extra: String::new(), enabled: true })
roll: Some(vec![RollDialogOption { name: "heal".to_string(), dice_type: 6, dice_amount: 1, constant: 0, extra: String::new(), enabled: true }]), .with_action("heal".to_string())
roll_target: None,
actions: Some(vec!["heal".to_string()]),
source: self.name.clone(),
targets: None,
id: 0,
}
} else { todo!() }, } else { todo!() },
Entry::Spell(_spell) => todo!(), Entry::Spell(_spell) => todo!(),
} }