open_tavern/assets/web/index.html

427 lines
20 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<script src="./socket.js"></script>
<script>
// 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 = `
<p style='align-self: center; margin: 4px 0;'>
<b>${m.character ?? ''}</b>
(${m.source})
</p>
<hr style='width: 75%;' />
<p style='margin: 4px 2px;'>
${m.text.replace('\n', '\n<br>\n')}
</p>
`
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 = `
<img src='data:image/jpg;base64,${t.img}' ondragstart='return false;'>
`
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 = `
<p style='flex-grow: 1;'>${r.name} ${r.extra != null ? '(' + r.extra + ')' : ''}</p>
<p style='margin-right: 8px;'>${r.dice_amount}d${r.dice_type} + ${r.constant}</p>
<input type='checkbox' ${r.enabled ? 'checked' : ''} />
`;
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';
}
</script>
<style>
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;
}
</style>
</head>
<body onload="init()">
<div id="login-screen" style="display: flex; justify-content: center; width: 100%; height: 100%;">
<div style="display: flex; justify-content: center; flex-direction: column;" >
<input type="text" id="login-username" placeholder="Username..." >
<input type="password" id="login-pass" placeholder="Password..." >
<button onclick="onLoginClick()">
Login
</button>
</div>
</div>
<div id="game" style="display: none; width: 100%; height: 100%;" >
<div id="side-panel">
<div style="display: flex; flex-direction: column; width: calc(100% - 15px); height: 100%;" >
<div id="chat-history">
</div>
<div id="chat-input" style="width: 100%; flex-grow: 1; margin: 10px 0; display:flex;">
<textarea id="newmsg-content" placeholder="Message..." spellcheck="true" style="resize:none; height: auto; flex-grow: 1; min-width:0px;"></textarea>
<!-- it looks better without the button :( -->
<!-- <button style="height: fit-content; align-self: center; margin: 0 4px;" onclick="sendChatMessage()">Send</button> -->
</div>
</div>
</div>
<div id="game-view" style="display: flex; overflow: hidden; position: relative; top: 0px; left: 0px; flex-grow: 1;" >
<div style="position:absolute; top: 10px; left: 5px; background-color: rgb(255, 166, 0); z-index: 5;" >
floating<br>stuff
</div>
<div id="dice-roll-popup" style="display:none; justify-content: center; width: 100%; height: 100%; background-color: transparent; position: absolute; z-index: 10; top: 0px; left: 0px">
<div style="display:flex; width: 50%; justify-content: center; flex-direction: column;">
<div style="display: flex; height: fit-content; flex-direction: column; background-color: #ffffd6; color: black; border: black solid 1px; padding: 8px; border-radius: 8px;">
<h3 id="dice-roll-title" style="align-self: center;">
THIS IS THE ROLL DISPLAY
</h3>
<div id="dice-roll-holder" style="display: flex; flex-direction: column;">
</div>
<div style="display: flex;">
<button style="flex-grow: 1; margin: 4px;" onclick="document.getElementById('dice-roll-popup').style.display = 'none';">Cancel</button>
<button style="flex-grow: 1; margin: 4px;" onclick="rollPopup()">Roll</button>
</div>
</div>
</div>
</div>
<div style="position: absolute; top: 10px; right: 10px; color: black; z-index: 5; display: flex; flex-direction: row;">
<button style="background-color: #ffffd6;"><b>s</b></button>
<button style="background-color: #ffffd6;" onclick="showHideDiv('initiative-tracker')"><b>i</b></button>
</div>
<div id="map" style="position:absolute; transform-origin: top left;">
<img src="https://rustystriker.dev/molly.jpg" height="200%" >
</div>
</div>
</div>
<div id="msg-context-menu" class="chat-message">
<ul>
<li>Delete (TODO)</li>
</ul>
</div>
<div id="initiative-tracker" style="position: absolute; background-color: #ffffd6; color:black; display: none; flex-direction: column; top: 40px; right: 8px">
<div style="background-color: black; color:#ffffd6; margin-top: 0px; margin-bottom: 8px; padding: 0 4px; min-width: 150px; user-select: none; display: flex;"
onmousedown="onMoveableDivMouseDown(event, 'initiative-tracker')">
<h3 style="flex-grow: 1;">Initiative</h3>
<!-- it looks bad, for now just click the 'open initiative tracker' button again -->
<button onclick="document.getElementById('initiative-tracker').style.display = 'none';"
style="height: fit-content; align-self: center; background-color: transparent; color:#ffffd6; font-size: 20px; border: 0;">
X
</button>
</div>
<div>
aaaa
</div>
<div>
bbbb
</div>
<div>
cccc
</div>
</div>
</body>
</html>