new comments

This commit is contained in:
erik 2025-05-24 18:33:03 +00:00
parent b2f649a489
commit 09404da121
13 changed files with 430 additions and 70 deletions

View file

@ -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>

View file

@ -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`;

View file

@ -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;