new comments
This commit is contained in:
parent
b2f649a489
commit
09404da121
13 changed files with 430 additions and 70 deletions
|
|
@ -1,22 +1,29 @@
|
|||
<!--
|
||||
Dereth Tracker Single-Page Application
|
||||
Displays live player locations, trails, and statistics on a map.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Dereth Tracker</title>
|
||||
<!-- Link to main stylesheet -->
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<!-- Sidebar for active players list and filters -->
|
||||
<aside id="sidebar">
|
||||
<!-- Segmented sort buttons -->
|
||||
<!-- Container for sort and filter controls -->
|
||||
<div id="sortButtons" class="sort-buttons"></div>
|
||||
|
||||
<h2>Active Players</h2>
|
||||
<!-- Text input to filter active players by name -->
|
||||
<input type="text" id="playerFilter" class="player-filter" placeholder="Filter players..." />
|
||||
<ul id="playerList"></ul>
|
||||
</aside>
|
||||
|
||||
<!-- MAP -->
|
||||
<!-- Main map container showing terrain and player data -->
|
||||
<div id="mapContainer">
|
||||
<div id="mapGroup">
|
||||
<img id="map" src="dereth.png" alt="Dereth map">
|
||||
|
|
@ -26,6 +33,7 @@
|
|||
<div id="tooltip" class="tooltip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main JavaScript file for WebSocket communication and UI logic -->
|
||||
<script src="script.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
162
static/script.js
162
static/script.js
|
|
@ -1,3 +1,27 @@
|
|||
/*
|
||||
* script.js - Frontend logic for Dereth Tracker Single-Page Application.
|
||||
* Handles WebSocket communication, UI rendering of player lists, map display,
|
||||
* and user interactions (filtering, sorting, chat, stats windows).
|
||||
*/
|
||||
/**
|
||||
* script.js - Frontend controller for Dereth Tracker SPA
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Establish WebSocket connections to receive live telemetry and chat data
|
||||
* - Fetch and render live player lists, trails, and map dots
|
||||
* - Handle user interactions: filtering, sorting, selecting players
|
||||
* - Manage dynamic UI components: chat windows, stats panels, tooltips
|
||||
* - Provide smooth pan/zoom of map overlay using CSS transforms
|
||||
*
|
||||
* Structure:
|
||||
* 1. DOM references and constant definitions
|
||||
* 2. Color palette and assignment logic
|
||||
* 3. Sorting and filtering setup
|
||||
* 4. Utility functions (coordinate mapping, color hashing)
|
||||
* 5. UI window creation (stats, chat)
|
||||
* 6. Rendering functions for list and map
|
||||
* 7. Event listeners for map interactions and WebSocket messages
|
||||
*/
|
||||
/* ---------- DOM references --------------------------------------- */
|
||||
const wrap = document.getElementById('mapContainer');
|
||||
const group = document.getElementById('mapGroup');
|
||||
|
|
@ -7,6 +31,15 @@ const trailsContainer = document.getElementById('trails');
|
|||
const list = document.getElementById('playerList');
|
||||
const btnContainer = document.getElementById('sortButtons');
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
// Filter input for player names (starts-with filter)
|
||||
let currentFilter = '';
|
||||
const filterInput = document.getElementById('playerFilter');
|
||||
if (filterInput) {
|
||||
filterInput.addEventListener('input', e => {
|
||||
currentFilter = e.target.value.toLowerCase().trim();
|
||||
renderList();
|
||||
});
|
||||
}
|
||||
|
||||
// WebSocket for chat and commands
|
||||
let socket;
|
||||
|
|
@ -15,6 +48,18 @@ const chatWindows = {};
|
|||
// Keep track of open stats windows: character_name -> DOM element
|
||||
const statsWindows = {};
|
||||
|
||||
/**
|
||||
* ---------- Application Constants -----------------------------
|
||||
* Defines key parameters for map rendering, data polling, and UI limits.
|
||||
*
|
||||
* MAX_Z: Maximum altitude difference considered (filter out outliers by Z)
|
||||
* FOCUS_ZOOM: Zoom level when focusing on a selected character
|
||||
* POLL_MS: Millisecond interval to fetch live player data and trails
|
||||
* MAP_BOUNDS: World coordinate bounds for the game map (used for projection)
|
||||
* API_BASE: Prefix for AJAX endpoints (set when behind a proxy)
|
||||
* MAX_CHAT_LINES: Max number of lines per chat window to cap memory usage
|
||||
* CHAT_COLOR_MAP: Color mapping for in-game chat channels by channel code
|
||||
*/
|
||||
/* ---------- constants ------------------------------------------- */
|
||||
const MAX_Z = 10;
|
||||
const FOCUS_ZOOM = 3; // zoom level when you click a name
|
||||
|
|
@ -65,6 +110,50 @@ const CHAT_COLOR_MAP = {
|
|||
31: '#FFFF00' // AdminTell
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------- Player Color Assignment ----------------------------
|
||||
* Uses a predefined accessible color palette for player dots to ensure
|
||||
* high contrast and colorblind-friendly display. Once the palette
|
||||
* is exhausted, falls back to a deterministic hash-to-hue function.
|
||||
*/
|
||||
/* ---------- player/dot color assignment ------------------------- */
|
||||
// A base palette of distinct, color-blind-friendly colors
|
||||
const PALETTE = [
|
||||
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
|
||||
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'
|
||||
];
|
||||
// Map from character name to assigned color
|
||||
const colorMap = {};
|
||||
// Next index to pick from PALETTE
|
||||
let nextPaletteIndex = 0;
|
||||
/**
|
||||
* Assigns or returns a consistent color for a given name.
|
||||
* Uses a fixed palette first, then falls back to hue hashing.
|
||||
*/
|
||||
function getColorFor(name) {
|
||||
if (colorMap[name]) {
|
||||
return colorMap[name];
|
||||
}
|
||||
let color;
|
||||
if (nextPaletteIndex < PALETTE.length) {
|
||||
color = PALETTE[nextPaletteIndex++];
|
||||
} else {
|
||||
// Fallback: hash to HSL hue
|
||||
color = hue(name);
|
||||
}
|
||||
colorMap[name] = color;
|
||||
return color;
|
||||
}
|
||||
|
||||
/*
|
||||
* ---------- Sort Configuration -------------------------------
|
||||
* Defines available sort criteria for the active player list:
|
||||
* - name: alphabetical ascending
|
||||
* - kph: kills per hour descending
|
||||
* - kills: total kills descending
|
||||
* - rares: rare events found during current session descending
|
||||
* Each option includes a label for UI display and a comparator function.
|
||||
*/
|
||||
/* ---------- sort configuration ---------------------------------- */
|
||||
const sortOptions = [
|
||||
{
|
||||
|
|
@ -188,6 +277,53 @@ function showStatsWindow(name) {
|
|||
iframe.allowFullscreen = true;
|
||||
content.appendChild(iframe);
|
||||
});
|
||||
// Enable dragging of the stats window via its header
|
||||
if (!window.__chatZ) window.__chatZ = 10000;
|
||||
let drag = false;
|
||||
let startX = 0, startY = 0, startLeft = 0, startTop = 0;
|
||||
header.style.cursor = 'move';
|
||||
const bringToFront = () => {
|
||||
window.__chatZ += 1;
|
||||
win.style.zIndex = window.__chatZ;
|
||||
};
|
||||
header.addEventListener('mousedown', e => {
|
||||
if (e.target.closest('button')) return;
|
||||
e.preventDefault();
|
||||
drag = true;
|
||||
bringToFront();
|
||||
startX = e.clientX; startY = e.clientY;
|
||||
startLeft = win.offsetLeft; startTop = win.offsetTop;
|
||||
document.body.classList.add('noselect');
|
||||
});
|
||||
window.addEventListener('mousemove', e => {
|
||||
if (!drag) return;
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
win.style.left = `${startLeft + dx}px`;
|
||||
win.style.top = `${startTop + dy}px`;
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
drag = false;
|
||||
document.body.classList.remove('noselect');
|
||||
});
|
||||
// Touch support for dragging
|
||||
header.addEventListener('touchstart', e => {
|
||||
if (e.touches.length !== 1 || e.target.closest('button')) return;
|
||||
drag = true;
|
||||
bringToFront();
|
||||
const t = e.touches[0];
|
||||
startX = t.clientX; startY = t.clientY;
|
||||
startLeft = win.offsetLeft; startTop = win.offsetTop;
|
||||
});
|
||||
window.addEventListener('touchmove', e => {
|
||||
if (!drag || e.touches.length !== 1) return;
|
||||
const t = e.touches[0];
|
||||
const dx = t.clientX - startX;
|
||||
const dy = t.clientY - startY;
|
||||
win.style.left = `${startLeft + dx}px`;
|
||||
win.style.top = `${startTop + dy}px`;
|
||||
});
|
||||
window.addEventListener('touchend', () => { drag = false; });
|
||||
}
|
||||
|
||||
const applyTransform = () =>
|
||||
|
|
@ -265,8 +401,16 @@ img.onload = () => {
|
|||
};
|
||||
|
||||
/* ---------- rendering sorted list & dots ------------------------ */
|
||||
/**
|
||||
* Filter and sort the currentPlayers, then render them.
|
||||
*/
|
||||
function renderList() {
|
||||
const sorted = [...currentPlayers].sort(currentSort.comparator);
|
||||
// Filter by name prefix
|
||||
const filtered = currentPlayers.filter(p =>
|
||||
p.character_name.toLowerCase().startsWith(currentFilter)
|
||||
);
|
||||
// Sort filtered list
|
||||
const sorted = filtered.slice().sort(currentSort.comparator);
|
||||
render(sorted);
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +426,7 @@ function render(players) {
|
|||
dot.className = 'dot';
|
||||
dot.style.left = `${x}px`;
|
||||
dot.style.top = `${y}px`;
|
||||
dot.style.background = hue(p.character_name);
|
||||
dot.style.background = getColorFor(p.character_name);
|
||||
|
||||
|
||||
|
||||
|
|
@ -299,7 +443,7 @@ function render(players) {
|
|||
dots.appendChild(dot);
|
||||
//sidebar
|
||||
const li = document.createElement('li');
|
||||
const color = hue(p.character_name);
|
||||
const color = getColorFor(p.character_name);
|
||||
li.style.borderLeftColor = color;
|
||||
li.className = 'player-item';
|
||||
li.innerHTML = `
|
||||
|
|
@ -364,7 +508,8 @@ function renderTrails(trailData) {
|
|||
}).join(' ');
|
||||
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||
poly.setAttribute('points', points);
|
||||
poly.setAttribute('stroke', hue(name));
|
||||
// Use the same color as the player dot for consistency
|
||||
poly.setAttribute('stroke', getColorFor(name));
|
||||
poly.setAttribute('fill', 'none');
|
||||
poly.setAttribute('class', 'trail-path');
|
||||
trailsContainer.appendChild(poly);
|
||||
|
|
@ -383,8 +528,13 @@ function selectPlayer(p, x, y) {
|
|||
renderList(); // keep sorted + highlight
|
||||
}
|
||||
|
||||
/* ---------- chat & command handlers ---------------------------- */
|
||||
// Initialize WebSocket for chat and commands
|
||||
/*
|
||||
* ---------- Chat & Command WebSocket Handlers ------------------
|
||||
* Maintains a persistent WebSocket connection to the /ws/live endpoint
|
||||
* for receiving chat messages and sending user commands to plugin clients.
|
||||
* Reconnects automatically on close and logs errors.
|
||||
*/
|
||||
// Initialize WebSocket for chat and command streams
|
||||
function initWebSocket() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${location.host}${API_BASE}/ws/live`;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
/*
|
||||
* style.css - Core styles for Dereth Tracker Single-Page Application
|
||||
*
|
||||
* Defines CSS variables for theming, layout rules for sidebar and map,
|
||||
* interactive element styling (buttons, inputs), and responsive considerations.
|
||||
*/
|
||||
/* CSS Custom Properties for theme colors and sizing */
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
--bg-main: #111;
|
||||
|
|
@ -7,6 +14,10 @@
|
|||
--text: #eee;
|
||||
--accent: #88f;
|
||||
}
|
||||
/*
|
||||
* style.css - Styling for Dereth Tracker SPA frontend.
|
||||
* Defines layout, theming variables, and component styles (sidebar, map, controls).
|
||||
*/
|
||||
/* Placeholder text in chat input should be white */
|
||||
.chat-input::placeholder {
|
||||
color: #fff;
|
||||
|
|
@ -29,13 +40,14 @@ body {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---------- sort buttons --------------------------------------- */
|
||||
.sort-buttons {
|
||||
/* Container for sorting controls; uses flex layout to distribute buttons equally */
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin: 12px 16px 8px;
|
||||
}
|
||||
.sort-buttons .btn {
|
||||
/* Base styling for each sort button: color, padding, border */
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: #222;
|
||||
|
|
@ -48,6 +60,7 @@ body {
|
|||
font-size: 0.9rem;
|
||||
}
|
||||
.sort-buttons .btn.active {
|
||||
/* Active sort button highlighted with accent color */
|
||||
background: var(--accent);
|
||||
color: #111;
|
||||
border-color: var(--accent);
|
||||
|
|
@ -73,6 +86,18 @@ body {
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* Filter input in sidebar for player list */
|
||||
.player-filter {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#playerList li {
|
||||
margin: 4px 0;
|
||||
padding: 6px 8px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue