ws version with nice DB select
This commit is contained in:
parent
a121d57a13
commit
73ae756e5c
6 changed files with 491 additions and 106 deletions
154
static/script.js
154
static/script.js
|
|
@ -8,6 +8,11 @@ const list = document.getElementById('playerList');
|
|||
const btnContainer = document.getElementById('sortButtons');
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
// WebSocket for chat and commands
|
||||
let socket;
|
||||
// Keep track of open chat windows: character_name -> DOM element
|
||||
const chatWindows = {};
|
||||
|
||||
/* ---------- constants ------------------------------------------- */
|
||||
const MAX_Z = 10;
|
||||
const FOCUS_ZOOM = 3; // zoom level when you click a name
|
||||
|
|
@ -19,6 +24,44 @@ const MAP_BOUNDS = {
|
|||
south: -102.00
|
||||
};
|
||||
|
||||
// Base path for tracker API endpoints; prefix API calls with '/api' when served behind a proxy
|
||||
const API_BASE = '/api';
|
||||
// Maximum number of lines to retain in each chat window scrollback
|
||||
const MAX_CHAT_LINES = 1000;
|
||||
// Map numeric chat color codes to CSS hex colors
|
||||
const CHAT_COLOR_MAP = {
|
||||
0: '#00FF00', // Broadcast
|
||||
2: '#FFFFFF', // Speech
|
||||
3: '#FFD700', // Tell
|
||||
4: '#CCCC00', // OutgoingTell
|
||||
5: '#FF00FF', // System
|
||||
6: '#FF0000', // Combat
|
||||
7: '#00CCFF', // Magic
|
||||
8: '#DDDDDD', // Channel
|
||||
9: '#FF9999', // ChannelSend
|
||||
10: '#FFFF33', // Social
|
||||
11: '#CCFF33', // SocialSend
|
||||
12: '#FFFFFF', // Emote
|
||||
13: '#00FFFF', // Advancement
|
||||
14: '#66CCFF', // Abuse
|
||||
15: '#FF0000', // Help
|
||||
16: '#33FF00', // Appraisal
|
||||
17: '#0099FF', // Spellcasting
|
||||
18: '#FF6600', // Allegiance
|
||||
19: '#CC66FF', // Fellowship
|
||||
20: '#00FF00', // WorldBroadcast
|
||||
21: '#FF0000', // CombatEnemy
|
||||
22: '#FF33CC', // CombatSelf
|
||||
23: '#00CC00', // Recall
|
||||
24: '#00FF00', // Craft
|
||||
25: '#00FF66', // Salvaging
|
||||
27: '#FFFFFF', // General
|
||||
28: '#33FF33', // Trade
|
||||
29: '#CCCCCC', // LFG
|
||||
30: '#CC00CC', // Roleplay
|
||||
31: '#FFFF00' // AdminTell
|
||||
};
|
||||
|
||||
/* ---------- sort configuration ---------------------------------- */
|
||||
const sortOptions = [
|
||||
{
|
||||
|
|
@ -132,8 +175,8 @@ function hideTooltip() {
|
|||
async function pollLive() {
|
||||
try {
|
||||
const [liveRes, trailsRes] = await Promise.all([
|
||||
fetch('/live/'),
|
||||
fetch('/trails/?seconds=600'),
|
||||
fetch(`${API_BASE}/live/`),
|
||||
fetch(`${API_BASE}/trails/?seconds=600`),
|
||||
]);
|
||||
const { players } = await liveRes.json();
|
||||
const { trails } = await trailsRes.json();
|
||||
|
|
@ -162,6 +205,7 @@ img.onload = () => {
|
|||
}
|
||||
fitToWindow();
|
||||
startPolling();
|
||||
initWebSocket();
|
||||
};
|
||||
|
||||
/* ---------- rendering sorted list & dots ------------------------ */
|
||||
|
|
@ -215,6 +259,15 @@ function render(players) {
|
|||
|
||||
li.addEventListener('click', () => selectPlayer(p, x, y));
|
||||
if (p.character_name === selected) li.classList.add('selected');
|
||||
// Chat button
|
||||
const chatBtn = document.createElement('button');
|
||||
chatBtn.className = 'chat-btn';
|
||||
chatBtn.textContent = 'Chat';
|
||||
chatBtn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
showChatWindow(p.character_name);
|
||||
});
|
||||
li.appendChild(chatBtn);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
|
@ -253,6 +306,103 @@ function selectPlayer(p, x, y) {
|
|||
renderList(); // keep sorted + highlight
|
||||
}
|
||||
|
||||
/* ---------- chat & command handlers ---------------------------- */
|
||||
// Initialize WebSocket for chat and commands
|
||||
function initWebSocket() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${location.host}${API_BASE}/ws/live`;
|
||||
socket = new WebSocket(wsUrl);
|
||||
socket.addEventListener('message', evt => {
|
||||
let msg;
|
||||
try { msg = JSON.parse(evt.data); } catch { return; }
|
||||
if (msg.type === 'chat') {
|
||||
appendChatMessage(msg);
|
||||
}
|
||||
});
|
||||
socket.addEventListener('close', () => setTimeout(initWebSocket, 2000));
|
||||
socket.addEventListener('error', e => console.error('WebSocket error:', e));
|
||||
}
|
||||
|
||||
// Display or create a chat window for a character
|
||||
function showChatWindow(name) {
|
||||
if (chatWindows[name]) {
|
||||
// Restore flex layout when reopening
|
||||
chatWindows[name].style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
const win = document.createElement('div');
|
||||
win.className = 'chat-window';
|
||||
win.dataset.character = name;
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'chat-header';
|
||||
const title = document.createElement('span');
|
||||
title.textContent = `Chat: ${name}`;
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'chat-close-btn';
|
||||
closeBtn.textContent = '×';
|
||||
closeBtn.addEventListener('click', () => { win.style.display = 'none'; });
|
||||
header.appendChild(title);
|
||||
header.appendChild(closeBtn);
|
||||
win.appendChild(header);
|
||||
// Messages container
|
||||
const msgs = document.createElement('div');
|
||||
msgs.className = 'chat-messages';
|
||||
win.appendChild(msgs);
|
||||
// Input form
|
||||
const form = document.createElement('form');
|
||||
form.className = 'chat-form';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'chat-input';
|
||||
input.placeholder = 'Enter chat...';
|
||||
form.appendChild(input);
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
// Send command envelope: player_name and command only
|
||||
socket.send(JSON.stringify({ player_name: name, command: text }));
|
||||
input.value = '';
|
||||
});
|
||||
win.appendChild(form);
|
||||
document.body.appendChild(win);
|
||||
chatWindows[name] = win;
|
||||
}
|
||||
|
||||
// Append a chat message to the correct window
|
||||
/**
|
||||
* Append a chat message to the correct window, optionally coloring the text.
|
||||
* msg: { type: 'chat', character_name, text, color? }
|
||||
*/
|
||||
function appendChatMessage(msg) {
|
||||
const { character_name: name, text, color } = msg;
|
||||
const win = chatWindows[name];
|
||||
if (!win) return;
|
||||
const msgs = win.querySelector('.chat-messages');
|
||||
const p = document.createElement('div');
|
||||
if (color !== undefined) {
|
||||
let c = color;
|
||||
if (typeof c === 'number') {
|
||||
// map numeric chat code to configured color, or fallback to raw hex
|
||||
if (CHAT_COLOR_MAP.hasOwnProperty(c)) {
|
||||
c = CHAT_COLOR_MAP[c];
|
||||
} else {
|
||||
c = '#' + c.toString(16).padStart(6, '0');
|
||||
}
|
||||
}
|
||||
p.style.color = c;
|
||||
}
|
||||
p.textContent = text;
|
||||
msgs.appendChild(p);
|
||||
// Enforce max number of lines in scrollback
|
||||
while (msgs.children.length > MAX_CHAT_LINES) {
|
||||
msgs.removeChild(msgs.firstChild);
|
||||
}
|
||||
// Scroll to bottom
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
}
|
||||
|
||||
/* ---------- pan & zoom handlers -------------------------------- */
|
||||
wrap.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@
|
|||
--text: #eee;
|
||||
--accent: #88f;
|
||||
}
|
||||
/* Placeholder text in chat input should be white */
|
||||
.chat-input::placeholder {
|
||||
color: #fff;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
|
|
@ -201,6 +206,81 @@ body {
|
|||
background: var(--accent);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* ---------- chat window styling ------------------------------- */
|
||||
.chat-btn {
|
||||
margin-top: 4px;
|
||||
padding: 2px 6px;
|
||||
background: var(--accent);
|
||||
color: #111;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
/* position window to start just right of the sidebar */
|
||||
left: calc(var(--sidebar-width) + 10px);
|
||||
/* increase default size for better usability */
|
||||
width: 760px; /* increased width for larger terminal area */
|
||||
height: 300px;
|
||||
background: var(--card);
|
||||
border: 1px solid #555;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--accent);
|
||||
padding: 4px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.chat-close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
/* reserve space so messages aren't hidden behind the input */
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.chat-form {
|
||||
display: flex;
|
||||
border-top: 1px solid #333;
|
||||
/* fix input area to the bottom of the chat window */
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
padding: 4px 6px;
|
||||
border: none;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
.stat.onlinetime::before { content: "🕑 "}
|
||||
.stat.deaths::before { content: "💀 "}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue