From a6bfd86c24db4e0aa51520a977381fc43008975f Mon Sep 17 00:00:00 2001 From: Rusty Striker Date: Fri, 18 Oct 2024 16:07:00 +0300 Subject: [PATCH 1/2] pass character_short from game --- src/game/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 03e09b4..804d494 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -91,8 +91,9 @@ impl<'a, C: Character + Serialize + Deserialize<'a>, A: entry::GameEntry + Se } fn character_short(&self, character_id: usize) -> Option { - // self.characters.get(character_id).map(|c| c.char) - todo!() + self.characters + .get(character_id) + .map(|c| c.short()) } } From af11db6f55b28b1588f5de32fe3d42520db9cadd Mon Sep 17 00:00:00 2001 From: Rusty Striker Date: Fri, 18 Oct 2024 16:07:10 +0300 Subject: [PATCH 2/2] split index.html to multiple files --- assets/web/app.js | 263 +++++++++++++++++++++ assets/web/index.html | 351 +--------------------------- assets/web/style.css | 74 ++++++ assets/web/{socket.js => tavern.js} | 0 src/main.rs | 15 +- 5 files changed, 356 insertions(+), 347 deletions(-) create mode 100644 assets/web/app.js create mode 100644 assets/web/style.css rename assets/web/{socket.js => tavern.js} (100%) diff --git a/assets/web/app.js b/assets/web/app.js new file mode 100644 index 0000000..1d341b5 --- /dev/null +++ b/assets/web/app.js @@ -0,0 +1,263 @@ +// init - game view (map) +const GRID_SIZE = 200; // Grid size in pixels +var mapScale = 1.0; +var mapOffsetX = 0.0; +var mapOffsetY = 0.0; +var draggedToken = { token: null, offX: 0, offY: 0 }; +var draggedDiv = { div: null, offX: 0, offY: 0 }; + +function init() { + let view = document.getElementById('game-view'); + view.onwheel = onGameViewScroll; + view.onmousemove = onGameMouseMove; + view.onmouseup = onGameMouseUp; + 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; + } + } + document.getElementById('login-username').onkeypress = (e) => { + if(e.key == 'Enter') { + document.getElementById('login-pass').focus(); + return false; + } + } + document.getElementById('login-pass').onkeypress = (e) => { + if(e.key == 'Enter') { + onLoginClick(); + return false; + } + } + // focus on the username field for the sake of just pressing enter multiple times + document.getElementById('login-username').focus(); + document.body.onclick = (e) => { + document.getElementById('msg-context-menu').style.display = 'none'; + } + document.body.onmousemove = onMoveableDivDrag; + document.body.onmouseup = onMoveableDivMouseUp; + // TODO: Remove when done dev-ing + tavern.onmessage({ text: 'test', id: 1, source: 'rusty', character: 'bart' }); +} + +tavern.onlogin = (s) => { + if(s) { + let login = document.getElementById('login-screen'); + let game = document.getElementById('game'); + login.style.display = 'none'; + game.style.display = 'flex'; + // get last 50 msgs (i think that is enough for now) when we get in + tavern.get_last_msgs(50); + tavern.get_tokens(); + } + 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.character ?? ''} + (${m.source}) +

+
+

+ ${m.text.replace('\n', '\n
\n')} +

+ ` + msg.oncontextmenu = (e) => { + 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; + } + if(m.actions) { + let holder = document.createElement('div'); + holder.style.display = 'flex'; + holder.style.flexWrap = 'wrap'; + msg.appendChild(holder); + for (const act of m.actions) { + let button = document.createElement('button'); + button.innerText = act.display_name ?? act.name; + button.action = act; + button.onclick = () => console.log(button.action); + button.style.margin = '2px'; + button.onclick = () => openRollsPopup(act); + holder.appendChild(button); + } + } + 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); + msg.scrollIntoView(); +} +tavern.onspawntoken = (t) => { + console.log(t); + let map = document.getElementById('map'); + let token = document.createElement('div'); + token.className = 'token token-transition'; + token.style.top = `${t.y * GRID_SIZE}px`; + token.style.left = `${t.x * GRID_SIZE}px`; + token.token_id = t.token_id; + token.innerHTML = ` + + ` + token.onmousedown = (e) => { + token.classList.remove('token-transition'); + draggedToken.token = token; + draggedToken.offX = ((e.clientX - mapOffsetX) / mapScale) - parseInt(token.style.left); + draggedToken.offY = ((e.clientY - mapOffsetY) / mapScale) - parseInt(token.style.top); + token.children[0].style.cursor = 'grabbing'; + } + map.appendChild(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() { + 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); + if(mapScale < 0.1) { mapScale = 0.1; } + map.style.transform = `scale(${mapScale})`; +} +function onGameMouseMove(event) { + if(event.buttons == 2) { + // right 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`; + } + else if(draggedToken.token != null && event.buttons == 1) { + let top = (event.clientY - mapOffsetY) / mapScale - draggedToken.offY; + let left = (event.clientX - mapOffsetX) / mapScale - draggedToken.offX; + draggedToken.token.style.top = `${top}px`; + draggedToken.token.style.left = `${left}px`; + } +} +function onGameMouseUp() { + if(draggedToken.token != null) { + let t = draggedToken.token; + let x = Math.floor(0.5 + parseInt(t.style.left) / GRID_SIZE); + let y = Math.floor(0.5 + parseInt(t.style.top) / GRID_SIZE); + let id = t.token_id; + t.classList.add('token-transition'); + t.children[0].style.cursor = ''; + tavern.move_token(id, x, y); + draggedToken.token = null; + } +} +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); +} +function openRollsPopup(action) { + let holder = document.getElementById('dice-roll-holder'); + holder.innerHTML = ''; // remove all holder children + holder.action = action; + if(action.rolls != undefined && Array.isArray(action.rolls)) { + for (const r of action.rolls) { + // name (extra) (dice_amount)d(dice_type) + constant | enabled + console.log(r); + let row = document.createElement('div'); + row.style.display = 'flex'; + row.roll = r; + row.innerHTML = ` +

${r.name} ${r.extra != null ? '(' + r.extra + ')' : ''}

+

${r.dice_amount}d${r.dice_type} + ${r.constant}

+ + `; + holder.appendChild(row); + } + } + document.getElementById('dice-roll-title').innerText = action.display_name ?? action.name; + document.getElementById('dice-roll-popup').style.display = 'flex'; +} +function rollPopup() { + // TODO: Maybe let the server roll the dice? + // first - hide the popup + document.getElementById('dice-roll-popup').style.display = 'none'; + // get the holder and start rolling dice + let rolls = []; + let holder = document.getElementById('dice-roll-holder'); + for(const h of holder.children) { + if(h.roll && h.children[2].checked) { + let roll = { name: h.roll.name, extra: h.roll.extra }; + let msg = ''; + let sum = 0; + for (let i = 0; i < h.roll.dice_amount; i++) { + // Math.random gives a value in [0, 1), so we need to add 1 at the end + let roll = Math.floor(Math.random() * h.roll.dice_type) + 1; + sum += roll; + msg += `${roll}`; + if(i != h.roll.dice_amount - 1 || h.roll.constant != 0) { + msg += ' + '; + } + } + if(h.roll.constant != 0) { + sum += h.roll.constant; + msg += `${h.roll.constant}`; + } + roll.result = sum; + roll.result_text = msg; + rolls.push(roll); + } + } + console.log(rolls); + tavern.action_result(holder.action.name, 'Louise', [], rolls); +} +function onMoveableDivMouseDown(e, id) { + if(e.buttons == 1) { + let div = document.getElementById(id); + let rect = div.getBoundingClientRect(); + draggedDiv.div = div; + draggedDiv.offX = e.clientX - rect.x; + draggedDiv.offY = e.clientY - rect.y; + + } +} +function onMoveableDivDrag(e) { + if(draggedDiv.div) { + draggedDiv.div.style.right = ''; + draggedDiv.div.style.top = `${e.clientY - draggedDiv.offY}px`; + draggedDiv.div.style.left = `${e.clientX - draggedDiv.offX}px`; + } +} +function onMoveableDivMouseUp(e, id) { + draggedDiv.div = null; +} +function showHideDiv(id) { + let div = document.getElementById(id); + div.style.display = div.style.display == 'none' ? 'flex' : 'none'; +} \ No newline at end of file diff --git a/assets/web/index.html b/assets/web/index.html index 5eab126..79ff186 100644 --- a/assets/web/index.html +++ b/assets/web/index.html @@ -1,360 +1,22 @@ - - - - + + + +
+
+ +
  • Delete (TODO)
  • diff --git a/assets/web/style.css b/assets/web/style.css new file mode 100644 index 0000000..fbba9dc --- /dev/null +++ b/assets/web/style.css @@ -0,0 +1,74 @@ +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-image: linear-gradient(135deg, #0f0f2f 0px, #0f0f2f 99%, rgb(188, 255, 185) 100%); +} +#chat-history { + display: flex; + flex-direction: column; + width: 100%; + height: 90%; + resize: none; + overflow: auto; + margin-top: 10px; + background-color:#0f0f2f; +} +.chat-message { + background-color: #ffffd6; + color: #000000; + border-width: 2px; + border-color: rgb(73, 49, 49); + border-style: solid; + border-radius: 8px; + padding: 4px 16px; + margin: 2px; + display: flex; + 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; +} +.token-transition { + transition: + top 0.5s ease-in, + left 0.5s ease-in; +} +.token img { + cursor: grab; + width: 200px; + height: 200px; +} \ No newline at end of file diff --git a/assets/web/socket.js b/assets/web/tavern.js similarity index 100% rename from assets/web/socket.js rename to assets/web/tavern.js diff --git a/src/main.rs b/src/main.rs index 0149d09..d1cb901 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,9 @@ async fn main() { let bsend2 = bsend.clone(); let app = Router::new() .route("/", routing::get(root)) - .route("/socket.js", routing::get(socket)) + .route("/tavern.js", routing::get(socket)) + .route("/app.js", routing::get(app_js)) + .route("/style.css", routing::get(style)) .route("/ws", routing::get(move |w| ws_handler(w, msend, bsend2.clone().subscribe()))); let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap(); @@ -135,7 +137,14 @@ async fn root() -> axum::response::Html<&'static str> { response::Html(include_str!("../assets/web/index.html")) } -async fn socket() -> &'static str { - include_str!("../assets/web/socket.js") +async fn socket() -> impl response::IntoResponse { + ([(axum::http::header::CONTENT_TYPE, "text/javascript")], include_str!("../assets/web/tavern.js")) } +async fn app_js() -> impl response::IntoResponse { + ([(axum::http::header::CONTENT_TYPE, "text/javascript")], include_str!("../assets/web/app.js")) +} + +async fn style() -> impl response::IntoResponse { + ([(axum::http::header::CONTENT_TYPE, "text/css")], include_str!("../assets/web/style.css")) +} \ No newline at end of file