Compare commits
11 commits
850cd62d7b
...
40198fa0cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40198fa0cf | ||
|
|
16861ba88a | ||
|
|
6f121e2a90 | ||
|
|
982bdb77e2 | ||
|
|
a82e6f4856 | ||
|
|
a0698753c5 | ||
|
|
395b7fb7ec | ||
|
|
230f08fab8 | ||
|
|
d025e2623f | ||
|
|
3d0a0b33a3 | ||
|
|
87bf7b7189 |
5 changed files with 1107 additions and 225 deletions
607
docs/plans/2026-02-26-script-js-cleanup-plan.md
Normal file
607
docs/plans/2026-02-26-script-js-cleanup-plan.md
Normal file
|
|
@ -0,0 +1,607 @@
|
||||||
|
# script.js Code Review Cleanup - Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Fix memory leaks, reduce console noise, eliminate code duplication, and improve rendering performance in the main frontend script.
|
||||||
|
|
||||||
|
**Architecture:** Targeted fixes to a single 2,310-line vanilla JS file. No module splitting, no architectural changes. Each task modifies `static/script.js` and can be tested by refreshing the browser.
|
||||||
|
|
||||||
|
**Tech Stack:** Vanilla JavaScript, served directly by FastAPI StaticFiles. No build system.
|
||||||
|
|
||||||
|
**CRITICAL:** Do NOT push to git until all tasks pass manual testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add DEBUG flag and debugLog helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js:1-3` (top of file)
|
||||||
|
|
||||||
|
**Step 1: Add DEBUG flag and helper after first line**
|
||||||
|
|
||||||
|
At the top of the file, after `console.log('...')` on line 1, add:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const DEBUG = false;
|
||||||
|
function debugLog(...args) { if (DEBUG) console.log(...args); }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Replace all console.log calls with debugLog**
|
||||||
|
|
||||||
|
Replace every `console.log(` with `debugLog(` throughout the file. There are ~35 instances.
|
||||||
|
|
||||||
|
Keep `console.error(` unchanged - those should always be visible.
|
||||||
|
|
||||||
|
Lines to change (all `console.log` → `debugLog`):
|
||||||
|
- Lines 100, 104, 106, 109 (chat button clicks)
|
||||||
|
- Lines 117, 121, 123, 126 (stats button clicks)
|
||||||
|
- Lines 134, 138, 140, 143 (inventory button clicks)
|
||||||
|
- Line 563 (heatmap loaded)
|
||||||
|
- Line 648 (portals loaded)
|
||||||
|
- Line 710 (portals rendered)
|
||||||
|
- Lines 729, 732, 739, 742, 786, 789 (stats window)
|
||||||
|
- Lines 824, 827, 834, 837, 1029, 1032 (inventory window)
|
||||||
|
- Line 1433 (server status)
|
||||||
|
- Lines 1511, 1515, 1733 (render cycle)
|
||||||
|
- Lines 1801, 1804, 1811, 1814, 1851, 1854 (chat window)
|
||||||
|
- Line 2173 (milestone)
|
||||||
|
|
||||||
|
**Step 3: Verify**
|
||||||
|
|
||||||
|
Open browser, load map page. Console should be clean (no log spam). Temporarily set `DEBUG = true`, refresh - logs should reappear.
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Add DEBUG flag and gate console.log behind debugLog helper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add named constants for magic numbers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js:298-300` (constants block)
|
||||||
|
- Modify: `static/script.js:556` (heatmap fetch)
|
||||||
|
- Modify: `static/script.js:1448-1452` (polling intervals)
|
||||||
|
- Modify: `static/script.js:2069` (notification duration)
|
||||||
|
- Modify: `static/script.js:2148` (glow duration)
|
||||||
|
|
||||||
|
**Step 1: Add constants after existing POLL_MS definition (line 300)**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const POLL_RARES_MS = 300000; // 5 minutes
|
||||||
|
const POLL_KILLS_MS = 300000; // 5 minutes
|
||||||
|
const POLL_HEALTH_MS = 30000; // 30 seconds
|
||||||
|
const NOTIFICATION_DURATION_MS = 6000; // Rare notification display time
|
||||||
|
const GLOW_DURATION_MS = 5000; // Player glow after rare find
|
||||||
|
const MAX_HEATMAP_POINTS = 50000;
|
||||||
|
const HEATMAP_HOURS = 24;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Replace hardcoded values**
|
||||||
|
|
||||||
|
Line 556 - heatmap fetch URL:
|
||||||
|
```javascript
|
||||||
|
// Before:
|
||||||
|
const response = await fetch(`${API_BASE}/spawns/heatmap?hours=24&limit=50000`);
|
||||||
|
// After:
|
||||||
|
const response = await fetch(`${API_BASE}/spawns/heatmap?hours=${HEATMAP_HOURS}&limit=${MAX_HEATMAP_POINTS}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
Lines 1448-1452 - polling intervals:
|
||||||
|
```javascript
|
||||||
|
// Before:
|
||||||
|
setInterval(pollTotalRares, 300000);
|
||||||
|
setInterval(pollTotalKills, 300000);
|
||||||
|
setInterval(pollServerHealth, 30000);
|
||||||
|
// After:
|
||||||
|
setInterval(pollTotalRares, POLL_RARES_MS);
|
||||||
|
setInterval(pollTotalKills, POLL_KILLS_MS);
|
||||||
|
setInterval(pollServerHealth, POLL_HEALTH_MS);
|
||||||
|
```
|
||||||
|
|
||||||
|
Line 2069 - notification timeout:
|
||||||
|
```javascript
|
||||||
|
// Before:
|
||||||
|
}, 6000);
|
||||||
|
// After:
|
||||||
|
}, NOTIFICATION_DURATION_MS);
|
||||||
|
```
|
||||||
|
|
||||||
|
Line 2148 - glow timeout:
|
||||||
|
```javascript
|
||||||
|
// Before:
|
||||||
|
}, 5000);
|
||||||
|
// After:
|
||||||
|
}, GLOW_DURATION_MS);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify**
|
||||||
|
|
||||||
|
Refresh page. Players load, positions update every 2s. Heatmap toggle works. (These confirm intervals and heatmap constants are correct.)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Replace magic numbers with named constants"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Fix polling interval memory leak
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js:1440-1453` (startPolling function)
|
||||||
|
|
||||||
|
**Step 1: Replace startPolling function**
|
||||||
|
|
||||||
|
Replace the current `startPolling()` function (lines 1440-1453) with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const pollIntervals = [];
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
// Clear any existing intervals first (prevents leak on re-init)
|
||||||
|
pollIntervals.forEach(id => clearInterval(id));
|
||||||
|
pollIntervals.length = 0;
|
||||||
|
|
||||||
|
// Initial fetches
|
||||||
|
pollLive();
|
||||||
|
pollTotalRares();
|
||||||
|
pollTotalKills();
|
||||||
|
pollServerHealth();
|
||||||
|
|
||||||
|
// Set up recurring polls
|
||||||
|
pollIntervals.push(setInterval(pollLive, POLL_MS));
|
||||||
|
pollIntervals.push(setInterval(pollTotalRares, POLL_RARES_MS));
|
||||||
|
pollIntervals.push(setInterval(pollTotalKills, POLL_KILLS_MS));
|
||||||
|
pollIntervals.push(setInterval(pollServerHealth, POLL_HEALTH_MS));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also remove the old `pollID` variable and the `if (pollID !== null) return;` guard since the new function handles re-initialization properly.
|
||||||
|
|
||||||
|
**Step 2: Verify**
|
||||||
|
|
||||||
|
Refresh page. Players load, update every 2s. Open DevTools > Performance Monitor - verify no interval accumulation. Refresh several times rapidly - should not see multiplied polling.
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Fix polling interval memory leak - store all interval IDs"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Fix highlightRareFinder DOM query
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js:2138-2151` (highlightRareFinder function)
|
||||||
|
|
||||||
|
**Step 1: Replace highlightRareFinder function**
|
||||||
|
|
||||||
|
Replace lines 2138-2151 with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function highlightRareFinder(characterName) {
|
||||||
|
// Use element pool for O(1) lookup instead of querySelectorAll
|
||||||
|
for (const item of elementPools.activeListItems) {
|
||||||
|
if (item.playerData && item.playerData.character_name === characterName) {
|
||||||
|
item.classList.add('rare-finder-glow');
|
||||||
|
setTimeout(() => {
|
||||||
|
item.classList.remove('rare-finder-glow');
|
||||||
|
}, GLOW_DURATION_MS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify**
|
||||||
|
|
||||||
|
This fires on rare notifications. If not easily testable live, verify by temporarily adding to console:
|
||||||
|
```javascript
|
||||||
|
highlightRareFinder('SomeOnlinePlayerName');
|
||||||
|
```
|
||||||
|
Confirm the player glows briefly in the list.
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Fix highlightRareFinder to use element pool instead of DOM query"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Extract createWindow helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js` - add helper before showStatsWindow (~line 725)
|
||||||
|
- Modify: `static/script.js:728-794` (showStatsWindow)
|
||||||
|
- Modify: `static/script.js:823-1036` (showInventoryWindow)
|
||||||
|
- Modify: `static/script.js:1800-1858` (showChatWindow)
|
||||||
|
|
||||||
|
**Step 1: Add createWindow helper function**
|
||||||
|
|
||||||
|
Insert before `showStatsWindow` (around line 725):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Create or show a draggable window. Returns { win, content, isNew }.
|
||||||
|
* If window already exists, brings it to front and returns isNew: false.
|
||||||
|
*/
|
||||||
|
function createWindow(id, title, className, options = {}) {
|
||||||
|
const { width = '400px', height = '300px', onClose } = options;
|
||||||
|
|
||||||
|
// Check if window already exists - bring to front
|
||||||
|
const existing = document.getElementById(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.style.display = 'flex';
|
||||||
|
if (!window.__chatZ) window.__chatZ = 10000;
|
||||||
|
window.__chatZ += 1;
|
||||||
|
existing.style.zIndex = window.__chatZ;
|
||||||
|
return { win: existing, content: existing.querySelector('.window-content'), isNew: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new window
|
||||||
|
if (!window.__chatZ) window.__chatZ = 10000;
|
||||||
|
window.__chatZ += 1;
|
||||||
|
|
||||||
|
const win = document.createElement('div');
|
||||||
|
win.id = id;
|
||||||
|
win.className = className;
|
||||||
|
win.style.display = 'flex';
|
||||||
|
win.style.zIndex = window.__chatZ;
|
||||||
|
win.style.width = width;
|
||||||
|
win.style.height = height;
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'window-header';
|
||||||
|
|
||||||
|
const titleSpan = document.createElement('span');
|
||||||
|
titleSpan.textContent = title;
|
||||||
|
header.appendChild(titleSpan);
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.textContent = '\u00D7';
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
win.style.display = 'none';
|
||||||
|
if (onClose) onClose();
|
||||||
|
});
|
||||||
|
header.appendChild(closeBtn);
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'window-content';
|
||||||
|
|
||||||
|
win.appendChild(header);
|
||||||
|
win.appendChild(content);
|
||||||
|
document.body.appendChild(win);
|
||||||
|
makeDraggable(win, header);
|
||||||
|
|
||||||
|
return { win, content, isNew: true };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Refactor showStatsWindow to use createWindow**
|
||||||
|
|
||||||
|
Replace the window creation boilerplate in `showStatsWindow` with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function showStatsWindow(name) {
|
||||||
|
debugLog('showStatsWindow called for:', name);
|
||||||
|
const windowId = `statsWindow-${name}`;
|
||||||
|
|
||||||
|
const { content, isNew } = createWindow(
|
||||||
|
windowId, `Stats: ${name}`, 'stats-window',
|
||||||
|
{ width: '800px', height: '600px' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNew) {
|
||||||
|
debugLog('Existing stats window found, showing it');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats-specific content (time range buttons + iframes)
|
||||||
|
// ... keep existing content creation logic from original function ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep ALL the stats-specific logic (time range buttons, iframe creation, updateStatsTimeRange) - only remove the duplicated window setup code.
|
||||||
|
|
||||||
|
**Step 3: Refactor showChatWindow to use createWindow**
|
||||||
|
|
||||||
|
Same pattern - replace window boilerplate, keep chat-specific content (message list, input field, send button).
|
||||||
|
|
||||||
|
**Step 4: Refactor showInventoryWindow to use createWindow**
|
||||||
|
|
||||||
|
Same pattern - replace window boilerplate, keep inventory-specific content (grid, icon rendering, tooltips).
|
||||||
|
|
||||||
|
**Step 5: Verify**
|
||||||
|
|
||||||
|
Test each window type:
|
||||||
|
1. Click a player's chat button - window opens, is draggable, close works
|
||||||
|
2. Click a player's stats button - window opens, iframes load, time range works
|
||||||
|
3. Click a player's inventory button - window opens, items display, tooltips work
|
||||||
|
4. Open multiple windows - z-index stacking works (latest on top)
|
||||||
|
5. Close and reopen each - brings to front correctly
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Extract createWindow helper to eliminate window creation duplication"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Add requestAnimationFrame batching for pan/zoom
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js` - add scheduleViewUpdate near updateView (~line 1295)
|
||||||
|
- Modify: `static/script.js:1908` (wheel handler)
|
||||||
|
- Modify: `static/script.js:1919` (mousemove drag handler)
|
||||||
|
- Modify: `static/script.js:1935` (touchmove handler)
|
||||||
|
|
||||||
|
**Step 1: Add scheduleViewUpdate after updateView function**
|
||||||
|
|
||||||
|
After the `updateView()` function (line 1295), add:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let pendingFrame = null;
|
||||||
|
function scheduleViewUpdate() {
|
||||||
|
if (!pendingFrame) {
|
||||||
|
pendingFrame = requestAnimationFrame(() => {
|
||||||
|
updateView();
|
||||||
|
pendingFrame = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Replace updateView() calls in interaction handlers**
|
||||||
|
|
||||||
|
Line 1908 (wheel zoom):
|
||||||
|
```javascript
|
||||||
|
// Before: updateView();
|
||||||
|
// After: scheduleViewUpdate();
|
||||||
|
```
|
||||||
|
|
||||||
|
Line 1919 (mousemove drag):
|
||||||
|
```javascript
|
||||||
|
// Before: updateView();
|
||||||
|
// After: scheduleViewUpdate();
|
||||||
|
```
|
||||||
|
|
||||||
|
Line 1935 (touchmove):
|
||||||
|
```javascript
|
||||||
|
// Before: updateView();
|
||||||
|
// After: scheduleViewUpdate();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keep** the direct `updateView()` calls at:
|
||||||
|
- Line 1301 (fitToWindow) - one-time call, not a hot path
|
||||||
|
- Line 1767 (selectPlayer) - one-time call on player click
|
||||||
|
|
||||||
|
**Step 3: Verify**
|
||||||
|
|
||||||
|
Pan the map by dragging - should feel smooth, no jitter. Zoom with scroll wheel - smooth. Toggle heatmap on, zoom/pan - heatmap updates correctly. Overall responsiveness should be same or better.
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Add requestAnimationFrame batching for pan/zoom updates"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Optimize renderTrails
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js:1737-1757` (renderTrails function)
|
||||||
|
|
||||||
|
**Step 1: Replace renderTrails function**
|
||||||
|
|
||||||
|
Replace lines 1737-1757 with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function renderTrails(trailData) {
|
||||||
|
trailsContainer.innerHTML = '';
|
||||||
|
// Build point strings directly - avoid intermediate arrays
|
||||||
|
const byChar = {};
|
||||||
|
for (const pt of trailData) {
|
||||||
|
const { x, y } = worldToPx(pt.ew, pt.ns);
|
||||||
|
const key = pt.character_name;
|
||||||
|
if (!byChar[key]) byChar[key] = { points: `${x},${y}`, count: 1 };
|
||||||
|
else { byChar[key].points += ` ${x},${y}`; byChar[key].count++; }
|
||||||
|
}
|
||||||
|
for (const name in byChar) {
|
||||||
|
if (byChar[name].count < 2) continue;
|
||||||
|
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||||
|
poly.setAttribute('points', byChar[name].points);
|
||||||
|
poly.setAttribute('stroke', getColorFor(name));
|
||||||
|
poly.setAttribute('fill', 'none');
|
||||||
|
poly.setAttribute('class', 'trail-path');
|
||||||
|
trailsContainer.appendChild(poly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify**
|
||||||
|
|
||||||
|
Refresh page. Player trails should render as colored lines following player positions. Verify trails match player colors. Multiple players should each have distinct trail lines.
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Optimize renderTrails to build SVG point strings directly"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Remove redundant .slice() in renderList
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js:1487` (renderList function)
|
||||||
|
|
||||||
|
**Step 1: Remove .slice()**
|
||||||
|
|
||||||
|
Line 1487:
|
||||||
|
```javascript
|
||||||
|
// Before:
|
||||||
|
const sorted = filtered.slice().sort(currentSort.comparator);
|
||||||
|
// After:
|
||||||
|
filtered.sort(currentSort.comparator);
|
||||||
|
const sorted = filtered;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify**
|
||||||
|
|
||||||
|
Refresh page. Player list should sort correctly. Click each sort button (name, KPH, kills, rares, total kills, KPR) - all should work correctly.
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js
|
||||||
|
git commit -m "Remove redundant .slice() before .sort() in renderList"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Add centralized error handling
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `static/script.js` - add handleError near top (~after debugLog)
|
||||||
|
- Modify: `static/style.css` - add .error-toast CSS
|
||||||
|
- Modify: `static/script.js` - update catch blocks throughout
|
||||||
|
|
||||||
|
**Step 1: Add handleError function after debugLog**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function handleError(context, error, showUI = false) {
|
||||||
|
console.error(`[${context}]`, error);
|
||||||
|
if (showUI) {
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
msg.className = 'error-toast';
|
||||||
|
msg.textContent = `${context}: ${error.message || 'Unknown error'}`;
|
||||||
|
document.body.appendChild(msg);
|
||||||
|
setTimeout(() => msg.remove(), GLOW_DURATION_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Add CSS for error toast**
|
||||||
|
|
||||||
|
In `static/style.css`, add at the end:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Error Toast */
|
||||||
|
.error-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(220, 38, 38, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 99999;
|
||||||
|
animation: toastFadeIn 0.3s ease;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Update catch blocks**
|
||||||
|
|
||||||
|
Background polling (showUI = false):
|
||||||
|
```javascript
|
||||||
|
// pollLive catch (~line 1329):
|
||||||
|
// Before: console.error('Live or trails fetch failed:', e);
|
||||||
|
// After: handleError('Player update', e);
|
||||||
|
|
||||||
|
// pollTotalRares catch (~line 1339):
|
||||||
|
// Before: console.error('Total rares fetch failed:', e);
|
||||||
|
// After: handleError('Rare counter', e);
|
||||||
|
|
||||||
|
// pollTotalKills catch (~line 1358):
|
||||||
|
// Before: console.error('Total kills fetch failed:', e);
|
||||||
|
// After: handleError('Kill counter', e);
|
||||||
|
|
||||||
|
// pollServerHealth catch (~line 1375):
|
||||||
|
// Before: console.error('Server health fetch failed:', e);
|
||||||
|
// After: handleError('Server health', e);
|
||||||
|
```
|
||||||
|
|
||||||
|
User-triggered actions (showUI = true):
|
||||||
|
```javascript
|
||||||
|
// showInventoryWindow catch (~line 1024):
|
||||||
|
// Before: content.innerHTML = `<div class="error">Failed to load...</div>`;
|
||||||
|
// After: handleError('Inventory', err, true);
|
||||||
|
// content.innerHTML = `<div class="error">Failed to load inventory</div>`;
|
||||||
|
|
||||||
|
// fetchHeatmapData catch (~line 565):
|
||||||
|
// Before: console.error('Heatmap fetch error:', err);
|
||||||
|
// After: handleError('Heatmap', err);
|
||||||
|
|
||||||
|
// fetchPortalData catch (~line 650):
|
||||||
|
// Before: (similar)
|
||||||
|
// After: handleError('Portals', err);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Verify**
|
||||||
|
|
||||||
|
1. Normal operation - no toasts appear
|
||||||
|
2. Temporarily break a URL (e.g., change API_BASE) - verify console.error appears with `[context]` prefix
|
||||||
|
3. For user-triggered errors (inventory of offline player) - verify red toast appears bottom-right and auto-dismisses
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add static/script.js static/style.css
|
||||||
|
git commit -m "Add centralized error handling with UI toast for user-facing errors"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Full Manual Testing Pass
|
||||||
|
|
||||||
|
**Files:** None modified - testing only
|
||||||
|
|
||||||
|
**CRITICAL: Do not push to git until ALL tests pass.**
|
||||||
|
|
||||||
|
**Test Checklist:**
|
||||||
|
|
||||||
|
| # | Test | How to Verify | Status |
|
||||||
|
|---|------|---------------|--------|
|
||||||
|
| 1 | Page load | Map renders, players appear, no console errors | |
|
||||||
|
| 2 | Live tracking | Players update positions every ~2 seconds | |
|
||||||
|
| 3 | Player filter | Type name in filter, list updates correctly | |
|
||||||
|
| 4 | Sort buttons | Each of 6 sort options works (name, KPH, kills, rares, total_kills, KPR) | |
|
||||||
|
| 5 | Player selection | Click player in list, map pans and zooms to them | |
|
||||||
|
| 6 | Pan/zoom | Mouse drag smooth, scroll wheel zoom smooth, no jitter or lag | |
|
||||||
|
| 7 | Heatmap | Toggle on - renders points. Pan/zoom - updates. Toggle off - clears | |
|
||||||
|
| 8 | Portals | Toggle on - portal icons appear. Toggle off - clears | |
|
||||||
|
| 9 | Chat window | Opens, draggable, close button works, receives WebSocket messages | |
|
||||||
|
| 10 | Stats window | Opens, iframes load, time range buttons work, draggable | |
|
||||||
|
| 11 | Inventory window | Opens, items display with icons, hover tooltip works, draggable | |
|
||||||
|
| 12 | Multiple windows | Open several windows - latest always on top (z-index) | |
|
||||||
|
| 13 | Console (DEBUG=false) | Console is clean, only errors/warnings visible | |
|
||||||
|
| 14 | Console (DEBUG=true) | Set DEBUG=true in code, refresh - verbose logs appear | |
|
||||||
|
| 15 | Page reload x5 | Reload 5 times rapidly - no interval accumulation, consistent behavior | |
|
||||||
|
| 16 | Trail rendering | Player trails visible as colored lines matching player dot colors | |
|
||||||
|
| 17 | Error handling | Briefly disconnect network - no crashes, console shows `[context]` errors | |
|
||||||
|
|
||||||
|
**If all pass:** Ready to push. Inform user and await approval.
|
||||||
|
|
||||||
|
**If any fail:** Fix the specific issue, re-test that item, then re-run full checklist.
|
||||||
206
docs/plans/2026-02-26-script-js-review-design.md
Normal file
206
docs/plans/2026-02-26-script-js-review-design.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# script.js Code Review & Performance Cleanup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Comprehensive cleanup of `static/script.js` (2,310 lines, 54 functions) - the main frontend controller for the live player tracking map. Approach B: surgical fixes + performance pass without full refactor.
|
||||||
|
|
||||||
|
**File:** `static/script.js` (+ minor CSS for error toast)
|
||||||
|
**Risk:** Low - targeted changes only, no architectural restructuring
|
||||||
|
**IMPORTANT:** Do NOT push to git until extensive manual testing is complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 1. Bug Fixes
|
||||||
|
|
||||||
|
#### 1a. Store All Polling Interval IDs (Memory Leak Fix)
|
||||||
|
|
||||||
|
Currently only `pollID` is stored. The other 3 intervals (`pollTotalRares`, `pollTotalKills`, `pollServerHealth`) leak on page reload.
|
||||||
|
|
||||||
|
**Fix:** Use a `pollIntervals` array, clear all before restarting.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const pollIntervals = [];
|
||||||
|
function startPolling() {
|
||||||
|
pollIntervals.forEach(id => clearInterval(id));
|
||||||
|
pollIntervals.length = 0;
|
||||||
|
pollIntervals.push(setInterval(pollLive, POLL_MS));
|
||||||
|
pollIntervals.push(setInterval(pollTotalRares, POLL_RARES_MS));
|
||||||
|
pollIntervals.push(setInterval(pollTotalKills, POLL_KILLS_MS));
|
||||||
|
pollIntervals.push(setInterval(pollServerHealth, POLL_HEALTH_MS));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1b. Fix `highlightRareFinder()` DOM Query
|
||||||
|
|
||||||
|
Replace expensive `querySelectorAll` + string search with existing element pool lookup.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// O(1) lookup from existing pool instead of O(n) DOM query
|
||||||
|
for (const item of elementPools.activeListItems) {
|
||||||
|
if (item.playerData?.character_name === characterName) {
|
||||||
|
item.classList.add('rare-finder-glow');
|
||||||
|
setTimeout(() => item.classList.remove('rare-finder-glow'), GLOW_DURATION_MS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Console Logging & Constants Cleanup
|
||||||
|
|
||||||
|
#### 2a. DEBUG Flag
|
||||||
|
|
||||||
|
Gate all `console.log` calls behind a flag. Errors and warnings stay always-visible.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const DEBUG = false;
|
||||||
|
function debugLog(...args) { if (DEBUG) console.log(...args); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace all `console.log(...)` with `debugLog(...)` throughout. Keep `console.error(...)` as-is.
|
||||||
|
|
||||||
|
#### 2b. Named Constants
|
||||||
|
|
||||||
|
Add to existing constants block:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const POLL_RARES_MS = 300000;
|
||||||
|
const POLL_KILLS_MS = 300000;
|
||||||
|
const POLL_HEALTH_MS = 30000;
|
||||||
|
const NOTIFICATION_DURATION_MS = 6000;
|
||||||
|
const GLOW_DURATION_MS = 5000;
|
||||||
|
const MAX_HEATMAP_POINTS = 50000;
|
||||||
|
const HEATMAP_HOURS = 24;
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace hardcoded values where they appear.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Extract Window Creation Helper
|
||||||
|
|
||||||
|
Three functions (`showChatWindow`, `showStatsWindow`, `showInventoryWindow`) duplicate ~50 lines of window setup.
|
||||||
|
|
||||||
|
**Extract `createWindow(id, title, className, options)`** that handles:
|
||||||
|
- Existing window detection and bring-to-front
|
||||||
|
- Z-index management
|
||||||
|
- DOM structure (header, close button, content area)
|
||||||
|
- `makeDraggable()` setup
|
||||||
|
|
||||||
|
Each `show*Window()` function then contains only its unique content logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Performance Pass
|
||||||
|
|
||||||
|
#### 4a. requestAnimationFrame for Pan/Zoom
|
||||||
|
|
||||||
|
Batch `updateView()` calls during mousemove/wheel into animation frames.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let pendingFrame = null;
|
||||||
|
function scheduleViewUpdate() {
|
||||||
|
if (!pendingFrame) {
|
||||||
|
pendingFrame = requestAnimationFrame(() => {
|
||||||
|
updateView();
|
||||||
|
pendingFrame = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace direct `updateView()` calls in mousemove and wheel handlers with `scheduleViewUpdate()`.
|
||||||
|
|
||||||
|
#### 4b. Optimize renderTrails()
|
||||||
|
|
||||||
|
Build SVG point strings directly instead of creating intermediate arrays:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const byChar = {};
|
||||||
|
for (const pt of trailData) {
|
||||||
|
const { x, y } = worldToPx(pt.ew, pt.ns);
|
||||||
|
const key = pt.character_name;
|
||||||
|
if (!byChar[key]) byChar[key] = `${x},${y}`;
|
||||||
|
else byChar[key] += ` ${x},${y}`;
|
||||||
|
}
|
||||||
|
for (const name in byChar) {
|
||||||
|
const poly = document.createElementNS(...);
|
||||||
|
poly.setAttribute('points', byChar[name]);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Eliminates: `Object.entries()` allocation, per-character `.map()`, per-point `.join()`.
|
||||||
|
|
||||||
|
#### 4c. Remove Redundant .slice()
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// filtered is already a local copy from .filter(), no need to .slice()
|
||||||
|
filtered.sort(currentSort.comparator);
|
||||||
|
const sorted = filtered;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Centralized Error Handling
|
||||||
|
|
||||||
|
Add `handleError(context, error, showUI)` for consistent error logging. Background polling uses `showUI = false`, user-triggered actions use `showUI = true` with a brief toast notification.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function handleError(context, error, showUI = false) {
|
||||||
|
console.error(`[${context}]`, error);
|
||||||
|
if (showUI) {
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
msg.className = 'error-toast';
|
||||||
|
msg.textContent = `${context}: ${error.message || 'Unknown error'}`;
|
||||||
|
document.body.appendChild(msg);
|
||||||
|
setTimeout(() => msg.remove(), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires minor CSS addition for `.error-toast` styling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
**CRITICAL: All changes must be manually tested before any git push.**
|
||||||
|
|
||||||
|
### Test Checklist
|
||||||
|
1. **Page load** - Map renders, players appear, no console errors
|
||||||
|
2. **Live tracking** - Players update positions every 2 seconds
|
||||||
|
3. **Player list** - Filter by name works, all 6 sort options work
|
||||||
|
4. **Player selection** - Click player in list, map centers on them
|
||||||
|
5. **Pan/zoom** - Mouse drag panning smooth, scroll wheel zoom smooth, no jitter
|
||||||
|
6. **Heatmap** - Toggle on/off, renders correctly, updates during pan/zoom
|
||||||
|
7. **Portals** - Toggle on/off, portals appear at correct positions
|
||||||
|
8. **Chat window** - Opens, receives messages via WebSocket, draggable
|
||||||
|
9. **Stats window** - Opens, iframes load, time range buttons work, draggable
|
||||||
|
10. **Inventory window** - Opens, items display, tooltip on hover, draggable
|
||||||
|
11. **Rare notifications** - If testable, fireworks + player glow appear
|
||||||
|
12. **Page reload** - No interval accumulation (check DevTools memory)
|
||||||
|
13. **Console output** - Clean when DEBUG=false, verbose when DEBUG=true
|
||||||
|
14. **Error scenarios** - Disconnect network briefly, verify graceful handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `static/script.js` | All changes above |
|
||||||
|
| `static/style.css` | Add `.error-toast` CSS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's NOT Changing
|
||||||
|
|
||||||
|
- File stays as single script.js (no module split)
|
||||||
|
- WebSocket reconnection logic stays as-is (fixed 2s retry)
|
||||||
|
- Element pooling system untouched (already optimized)
|
||||||
|
- Rendering pipeline structure stays the same
|
||||||
|
- No architectural changes
|
||||||
419
static/script.js
419
static/script.js
|
|
@ -22,6 +22,21 @@
|
||||||
* 6. Rendering functions for list and map
|
* 6. Rendering functions for list and map
|
||||||
* 7. Event listeners for map interactions and WebSocket messages
|
* 7. Event listeners for map interactions and WebSocket messages
|
||||||
*/
|
*/
|
||||||
|
/* ---------- Debug configuration ---------------------------------- */
|
||||||
|
const DEBUG = false;
|
||||||
|
function debugLog(...args) { if (DEBUG) console.log(...args); }
|
||||||
|
|
||||||
|
function handleError(context, error, showUI = false) {
|
||||||
|
console.error(`[${context}]`, error);
|
||||||
|
if (showUI) {
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
msg.className = 'error-toast';
|
||||||
|
msg.textContent = `${context}: ${error.message || 'Unknown error'}`;
|
||||||
|
document.body.appendChild(msg);
|
||||||
|
setTimeout(() => msg.remove(), GLOW_DURATION_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- DOM references --------------------------------------- */
|
/* ---------- DOM references --------------------------------------- */
|
||||||
const wrap = document.getElementById('mapContainer');
|
const wrap = document.getElementById('mapContainer');
|
||||||
const group = document.getElementById('mapGroup');
|
const group = document.getElementById('mapGroup');
|
||||||
|
|
@ -97,16 +112,16 @@ function createNewListItem() {
|
||||||
chatBtn.className = 'chat-btn';
|
chatBtn.className = 'chat-btn';
|
||||||
chatBtn.textContent = 'Chat';
|
chatBtn.textContent = 'Chat';
|
||||||
chatBtn.addEventListener('click', (e) => {
|
chatBtn.addEventListener('click', (e) => {
|
||||||
console.log('🔥 CHAT BUTTON CLICKED!', e.target, e.currentTarget);
|
debugLog('🔥 CHAT BUTTON CLICKED!', e.target, e.currentTarget);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Try button's own playerData first, fallback to DOM traversal
|
// Try button's own playerData first, fallback to DOM traversal
|
||||||
const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
|
const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
|
||||||
console.log('🔥 Player data found:', playerData);
|
debugLog('🔥 Player data found:', playerData);
|
||||||
if (playerData) {
|
if (playerData) {
|
||||||
console.log('🔥 Opening chat for:', playerData.character_name);
|
debugLog('🔥 Opening chat for:', playerData.character_name);
|
||||||
showChatWindow(playerData.character_name);
|
showChatWindow(playerData.character_name);
|
||||||
} else {
|
} else {
|
||||||
console.log('🔥 No player data found!');
|
debugLog('🔥 No player data found!');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -114,16 +129,16 @@ function createNewListItem() {
|
||||||
statsBtn.className = 'stats-btn';
|
statsBtn.className = 'stats-btn';
|
||||||
statsBtn.textContent = 'Stats';
|
statsBtn.textContent = 'Stats';
|
||||||
statsBtn.addEventListener('click', (e) => {
|
statsBtn.addEventListener('click', (e) => {
|
||||||
console.log('📊 STATS BUTTON CLICKED!', e.target, e.currentTarget);
|
debugLog('📊 STATS BUTTON CLICKED!', e.target, e.currentTarget);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Try button's own playerData first, fallback to DOM traversal
|
// Try button's own playerData first, fallback to DOM traversal
|
||||||
const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
|
const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
|
||||||
console.log('📊 Player data found:', playerData);
|
debugLog('📊 Player data found:', playerData);
|
||||||
if (playerData) {
|
if (playerData) {
|
||||||
console.log('📊 Opening stats for:', playerData.character_name);
|
debugLog('📊 Opening stats for:', playerData.character_name);
|
||||||
showStatsWindow(playerData.character_name);
|
showStatsWindow(playerData.character_name);
|
||||||
} else {
|
} else {
|
||||||
console.log('📊 No player data found!');
|
debugLog('📊 No player data found!');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -131,16 +146,16 @@ function createNewListItem() {
|
||||||
inventoryBtn.className = 'inventory-btn';
|
inventoryBtn.className = 'inventory-btn';
|
||||||
inventoryBtn.textContent = 'Inventory';
|
inventoryBtn.textContent = 'Inventory';
|
||||||
inventoryBtn.addEventListener('click', (e) => {
|
inventoryBtn.addEventListener('click', (e) => {
|
||||||
console.log('🎒 INVENTORY BUTTON CLICKED!', e.target, e.currentTarget);
|
debugLog('🎒 INVENTORY BUTTON CLICKED!', e.target, e.currentTarget);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Try button's own playerData first, fallback to DOM traversal
|
// Try button's own playerData first, fallback to DOM traversal
|
||||||
const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
|
const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData;
|
||||||
console.log('🎒 Player data found:', playerData);
|
debugLog('🎒 Player data found:', playerData);
|
||||||
if (playerData) {
|
if (playerData) {
|
||||||
console.log('🎒 Opening inventory for:', playerData.character_name);
|
debugLog('🎒 Opening inventory for:', playerData.character_name);
|
||||||
showInventoryWindow(playerData.character_name);
|
showInventoryWindow(playerData.character_name);
|
||||||
} else {
|
} else {
|
||||||
console.log('🎒 No player data found!');
|
debugLog('🎒 No player data found!');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -298,6 +313,13 @@ const inventoryWindows = {};
|
||||||
const MAX_Z = 20;
|
const MAX_Z = 20;
|
||||||
const FOCUS_ZOOM = 3; // zoom level when you click a name
|
const FOCUS_ZOOM = 3; // zoom level when you click a name
|
||||||
const POLL_MS = 2000;
|
const POLL_MS = 2000;
|
||||||
|
const POLL_RARES_MS = 300000; // 5 minutes
|
||||||
|
const POLL_KILLS_MS = 300000; // 5 minutes
|
||||||
|
const POLL_HEALTH_MS = 30000; // 30 seconds
|
||||||
|
const NOTIFICATION_DURATION_MS = 6000; // Rare notification display time
|
||||||
|
const GLOW_DURATION_MS = 5000; // Player glow after rare find
|
||||||
|
const MAX_HEATMAP_POINTS = 50000;
|
||||||
|
const HEATMAP_HOURS = 24;
|
||||||
// UtilityBelt's more accurate coordinate bounds
|
// UtilityBelt's more accurate coordinate bounds
|
||||||
const MAP_BOUNDS = {
|
const MAP_BOUNDS = {
|
||||||
west: -102.1,
|
west: -102.1,
|
||||||
|
|
@ -488,7 +510,7 @@ let imgW = 0, imgH = 0;
|
||||||
let scale = 1, offX = 0, offY = 0, minScale = 1;
|
let scale = 1, offX = 0, offY = 0, minScale = 1;
|
||||||
let dragging = false, sx = 0, sy = 0;
|
let dragging = false, sx = 0, sy = 0;
|
||||||
let selected = "";
|
let selected = "";
|
||||||
let pollID = null;
|
const pollIntervals = [];
|
||||||
|
|
||||||
/* ---------- utility functions ----------------------------------- */
|
/* ---------- utility functions ----------------------------------- */
|
||||||
const hue = name => {
|
const hue = name => {
|
||||||
|
|
@ -553,17 +575,17 @@ function initHeatMap() {
|
||||||
|
|
||||||
async function fetchHeatmapData() {
|
async function fetchHeatmapData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/spawns/heatmap?hours=24&limit=50000`);
|
const response = await fetch(`${API_BASE}/spawns/heatmap?hours=${HEATMAP_HOURS}&limit=${MAX_HEATMAP_POINTS}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Heat map API error: ${response.status}`);
|
throw new Error(`Heat map API error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
heatmapData = data.spawn_points; // [{ew, ns, intensity}]
|
heatmapData = data.spawn_points; // [{ew, ns, intensity}]
|
||||||
console.log(`Loaded ${heatmapData.length} heat map points from last ${data.hours_window} hours`);
|
debugLog(`Loaded ${heatmapData.length} heat map points from last ${data.hours_window} hours`);
|
||||||
renderHeatmap();
|
renderHeatmap();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch heat map data:', err);
|
handleError('Heatmap', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -645,10 +667,10 @@ async function fetchPortalData() {
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
portalData = data.portals; // [{portal_name, coordinates: {ns, ew, z}, discovered_by, discovered_at}]
|
portalData = data.portals; // [{portal_name, coordinates: {ns, ew, z}, discovered_by, discovered_at}]
|
||||||
console.log(`Loaded ${portalData.length} portals from last hour`);
|
debugLog(`Loaded ${portalData.length} portals from last hour`);
|
||||||
renderPortals();
|
renderPortals();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch portal data:', err);
|
handleError('Portals', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -707,7 +729,7 @@ function renderPortals() {
|
||||||
portalContainer.appendChild(icon);
|
portalContainer.appendChild(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Rendered ${portalData.length} portal icons`);
|
debugLog(`Rendered ${portalData.length} portal icons`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPortals() {
|
function clearPortals() {
|
||||||
|
|
@ -724,37 +746,77 @@ function debounce(fn, ms) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show or create a stats window for a character
|
/**
|
||||||
function showStatsWindow(name) {
|
* Create or show a draggable window. Returns { win, content, isNew }.
|
||||||
console.log('📊 showStatsWindow called for:', name);
|
* If window already exists, brings it to front and returns isNew: false.
|
||||||
if (statsWindows[name]) {
|
*/
|
||||||
const existing = statsWindows[name];
|
function createWindow(id, title, className, options = {}) {
|
||||||
console.log('📊 Existing stats window found, showing it:', existing);
|
const { onClose } = options;
|
||||||
// Always show the window (no toggle)
|
|
||||||
existing.style.display = 'flex';
|
// Check if window already exists - bring to front
|
||||||
// Bring to front when opening
|
const existing = document.getElementById(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.style.display = 'flex';
|
||||||
|
if (!window.__chatZ) window.__chatZ = 10000;
|
||||||
|
window.__chatZ += 1;
|
||||||
|
existing.style.zIndex = window.__chatZ;
|
||||||
|
return { win: existing, content: existing.querySelector('.window-content'), isNew: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new window
|
||||||
if (!window.__chatZ) window.__chatZ = 10000;
|
if (!window.__chatZ) window.__chatZ = 10000;
|
||||||
window.__chatZ += 1;
|
window.__chatZ += 1;
|
||||||
existing.style.zIndex = window.__chatZ;
|
|
||||||
console.log('📊 Stats window shown with zIndex:', window.__chatZ);
|
const win = document.createElement('div');
|
||||||
|
win.id = id;
|
||||||
|
win.className = className;
|
||||||
|
win.style.display = 'flex';
|
||||||
|
win.style.zIndex = window.__chatZ;
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'chat-header';
|
||||||
|
|
||||||
|
const titleSpan = document.createElement('span');
|
||||||
|
titleSpan.textContent = title;
|
||||||
|
header.appendChild(titleSpan);
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'chat-close-btn';
|
||||||
|
closeBtn.textContent = '\u00D7';
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
win.style.display = 'none';
|
||||||
|
if (onClose) onClose();
|
||||||
|
});
|
||||||
|
header.appendChild(closeBtn);
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'window-content';
|
||||||
|
|
||||||
|
win.appendChild(header);
|
||||||
|
win.appendChild(content);
|
||||||
|
document.body.appendChild(win);
|
||||||
|
makeDraggable(win, header);
|
||||||
|
|
||||||
|
return { win, content, isNew: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show or create a stats window for a character
|
||||||
|
function showStatsWindow(name) {
|
||||||
|
debugLog('showStatsWindow called for:', name);
|
||||||
|
const windowId = `statsWindow-${name}`;
|
||||||
|
|
||||||
|
const { win, content, isNew } = createWindow(
|
||||||
|
windowId, `Stats: ${name}`, 'stats-window'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNew) {
|
||||||
|
debugLog('Existing stats window found, showing it');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('📊 Creating new stats window for:', name);
|
|
||||||
const win = document.createElement('div');
|
|
||||||
win.className = 'stats-window';
|
|
||||||
win.dataset.character = name;
|
win.dataset.character = name;
|
||||||
// Header (reuses chat-header styling)
|
statsWindows[name] = win;
|
||||||
const header = document.createElement('div');
|
|
||||||
header.className = 'chat-header';
|
|
||||||
const title = document.createElement('span');
|
|
||||||
title.textContent = `Stats: ${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);
|
|
||||||
// Time period controls
|
// Time period controls
|
||||||
const controls = document.createElement('div');
|
const controls = document.createElement('div');
|
||||||
controls.className = 'stats-controls';
|
controls.className = 'stats-controls';
|
||||||
|
|
@ -764,6 +826,12 @@ function showStatsWindow(name) {
|
||||||
{ label: '24H', value: 'now-24h' },
|
{ label: '24H', value: 'now-24h' },
|
||||||
{ label: '7D', value: 'now-7d' }
|
{ label: '7D', value: 'now-7d' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Stats content container (iframes grid)
|
||||||
|
const statsContent = document.createElement('div');
|
||||||
|
statsContent.className = 'chat-messages';
|
||||||
|
statsContent.textContent = 'Loading stats...';
|
||||||
|
|
||||||
timeRanges.forEach(range => {
|
timeRanges.forEach(range => {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'time-range-btn';
|
btn.className = 'time-range-btn';
|
||||||
|
|
@ -772,25 +840,17 @@ function showStatsWindow(name) {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
controls.querySelectorAll('.time-range-btn').forEach(b => b.classList.remove('active'));
|
controls.querySelectorAll('.time-range-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
updateStatsTimeRange(content, name, range.value);
|
updateStatsTimeRange(statsContent, name, range.value);
|
||||||
});
|
});
|
||||||
controls.appendChild(btn);
|
controls.appendChild(btn);
|
||||||
});
|
});
|
||||||
win.appendChild(controls);
|
|
||||||
|
|
||||||
// Content container
|
content.appendChild(controls);
|
||||||
const content = document.createElement('div');
|
content.appendChild(statsContent);
|
||||||
content.className = 'chat-messages';
|
|
||||||
content.textContent = 'Loading stats...';
|
debugLog('Stats window created for:', name);
|
||||||
win.appendChild(content);
|
|
||||||
console.log('📊 Appending stats window to DOM:', win);
|
|
||||||
document.body.appendChild(win);
|
|
||||||
statsWindows[name] = win;
|
|
||||||
console.log('📊 Stats window added to DOM, total children:', document.body.children.length);
|
|
||||||
// Load initial stats with default 24h range
|
// Load initial stats with default 24h range
|
||||||
updateStatsTimeRange(content, name, 'now-24h');
|
updateStatsTimeRange(statsContent, name, 'now-24h');
|
||||||
// Enable dragging using the global drag system
|
|
||||||
makeDraggable(win, header);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatsTimeRange(content, name, timeRange) {
|
function updateStatsTimeRange(content, name, timeRange) {
|
||||||
|
|
@ -821,46 +881,32 @@ function updateStatsTimeRange(content, name, timeRange) {
|
||||||
|
|
||||||
// Show or create an inventory window for a character
|
// Show or create an inventory window for a character
|
||||||
function showInventoryWindow(name) {
|
function showInventoryWindow(name) {
|
||||||
console.log('🎒 showInventoryWindow called for:', name);
|
debugLog('showInventoryWindow called for:', name);
|
||||||
if (inventoryWindows[name]) {
|
const windowId = `inventoryWindow-${name}`;
|
||||||
const existing = inventoryWindows[name];
|
|
||||||
console.log('🎒 Existing inventory window found, showing it:', existing);
|
const { win, content, isNew } = createWindow(
|
||||||
// Always show the window (no toggle)
|
windowId, `Inventory: ${name}`, 'inventory-window'
|
||||||
existing.style.display = 'flex';
|
);
|
||||||
// Bring to front when opening
|
|
||||||
if (!window.__chatZ) window.__chatZ = 10000;
|
if (!isNew) {
|
||||||
window.__chatZ += 1;
|
debugLog('Existing inventory window found, showing it');
|
||||||
existing.style.zIndex = window.__chatZ;
|
|
||||||
console.log('🎒 Inventory window shown with zIndex:', window.__chatZ);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('🎒 Creating new inventory window for:', name);
|
|
||||||
const win = document.createElement('div');
|
|
||||||
win.className = 'inventory-window';
|
|
||||||
win.dataset.character = name;
|
win.dataset.character = name;
|
||||||
// Header (reuses chat-header styling)
|
inventoryWindows[name] = win;
|
||||||
const header = document.createElement('div');
|
|
||||||
header.className = 'chat-header';
|
|
||||||
const title = document.createElement('span');
|
|
||||||
title.textContent = `Inventory: ${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);
|
|
||||||
// Loading message
|
// Loading message
|
||||||
const loading = document.createElement('div');
|
const loading = document.createElement('div');
|
||||||
loading.className = 'inventory-loading';
|
loading.className = 'inventory-loading';
|
||||||
loading.textContent = 'Loading inventory...';
|
loading.textContent = 'Loading inventory...';
|
||||||
win.appendChild(loading);
|
content.appendChild(loading);
|
||||||
|
|
||||||
// Content container
|
// Inventory content container
|
||||||
const content = document.createElement('div');
|
const invContent = document.createElement('div');
|
||||||
content.className = 'inventory-content';
|
invContent.className = 'inventory-content';
|
||||||
content.style.display = 'none';
|
invContent.style.display = 'none';
|
||||||
win.appendChild(content);
|
content.appendChild(invContent);
|
||||||
|
|
||||||
// Fetch inventory data from main app (which will proxy to inventory service)
|
// Fetch inventory data from main app (which will proxy to inventory service)
|
||||||
fetch(`${API_BASE}/inventory/${encodeURIComponent(name)}?limit=1000`)
|
fetch(`${API_BASE}/inventory/${encodeURIComponent(name)}?limit=1000`)
|
||||||
|
|
@ -870,7 +916,7 @@ function showInventoryWindow(name) {
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
content.style.display = 'block';
|
invContent.style.display = 'block';
|
||||||
|
|
||||||
// Create inventory grid
|
// Create inventory grid
|
||||||
const grid = document.createElement('div');
|
const grid = document.createElement('div');
|
||||||
|
|
@ -1013,26 +1059,20 @@ function showInventoryWindow(name) {
|
||||||
grid.appendChild(slot);
|
grid.appendChild(slot);
|
||||||
});
|
});
|
||||||
|
|
||||||
content.appendChild(grid);
|
invContent.appendChild(grid);
|
||||||
|
|
||||||
// Add item count
|
// Add item count
|
||||||
const count = document.createElement('div');
|
const count = document.createElement('div');
|
||||||
count.className = 'inventory-count';
|
count.className = 'inventory-count';
|
||||||
count.textContent = `${data.item_count} items`;
|
count.textContent = `${data.item_count} items`;
|
||||||
content.appendChild(count);
|
invContent.appendChild(count);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
handleError('Inventory', err, true);
|
||||||
loading.textContent = `Failed to load inventory: ${err.message}`;
|
loading.textContent = `Failed to load inventory: ${err.message}`;
|
||||||
console.error('Inventory fetch failed:', err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🎒 Appending inventory window to DOM:', win);
|
debugLog('Inventory window created for:', name);
|
||||||
document.body.appendChild(win);
|
|
||||||
inventoryWindows[name] = win;
|
|
||||||
console.log('🎒 Inventory window added to DOM, total children:', document.body.children.length);
|
|
||||||
|
|
||||||
// Enable dragging using the global drag system
|
|
||||||
makeDraggable(win, header);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inventory tooltip functions
|
// Inventory tooltip functions
|
||||||
|
|
@ -1294,6 +1334,16 @@ function updateView() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pendingFrame = null;
|
||||||
|
function scheduleViewUpdate() {
|
||||||
|
if (!pendingFrame) {
|
||||||
|
pendingFrame = requestAnimationFrame(() => {
|
||||||
|
updateView();
|
||||||
|
pendingFrame = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fitToWindow() {
|
function fitToWindow() {
|
||||||
const r = wrap.getBoundingClientRect();
|
const r = wrap.getBoundingClientRect();
|
||||||
scale = Math.min(r.width / imgW, r.height / imgH);
|
scale = Math.min(r.width / imgW, r.height / imgH);
|
||||||
|
|
@ -1326,7 +1376,7 @@ async function pollLive() {
|
||||||
renderTrails(trails);
|
renderTrails(trails);
|
||||||
renderList();
|
renderList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Live or trails fetch failed:', e);
|
handleError('Player update', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1336,7 +1386,7 @@ async function pollTotalRares() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
updateTotalRaresDisplay(data);
|
updateTotalRaresDisplay(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Total rares fetch failed:', e);
|
handleError('Rare counter', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1355,7 +1405,7 @@ async function pollTotalKills() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
updateTotalKillsDisplay(data);
|
updateTotalKillsDisplay(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Total kills fetch failed:', e);
|
handleError('Kill counter', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1372,7 +1422,7 @@ async function pollServerHealth() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
updateServerStatusDisplay(data);
|
updateServerStatusDisplay(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Server health fetch failed:', e);
|
handleError('Server health', e);
|
||||||
updateServerStatusDisplay({ status: 'error' });
|
updateServerStatusDisplay({ status: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1430,7 +1480,7 @@ function handleServerStatusUpdate(msg) {
|
||||||
// Handle real-time server status updates via WebSocket
|
// Handle real-time server status updates via WebSocket
|
||||||
if (msg.status === 'up' && msg.message) {
|
if (msg.status === 'up' && msg.message) {
|
||||||
// Show notification for server coming back online
|
// Show notification for server coming back online
|
||||||
console.log(`Server Status: ${msg.message}`);
|
debugLog(`Server Status: ${msg.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an immediate server health poll to refresh the display
|
// Trigger an immediate server health poll to refresh the display
|
||||||
|
|
@ -1438,18 +1488,21 @@ function handleServerStatusUpdate(msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPolling() {
|
function startPolling() {
|
||||||
if (pollID !== null) return;
|
// Clear any existing intervals first (prevents leak on re-init)
|
||||||
|
pollIntervals.forEach(id => clearInterval(id));
|
||||||
|
pollIntervals.length = 0;
|
||||||
|
|
||||||
|
// Initial fetches
|
||||||
pollLive();
|
pollLive();
|
||||||
pollTotalRares(); // Initial fetch
|
pollTotalRares();
|
||||||
pollTotalKills(); // Initial fetch
|
pollTotalKills();
|
||||||
pollServerHealth(); // Initial server health check
|
pollServerHealth();
|
||||||
pollID = setInterval(pollLive, POLL_MS);
|
|
||||||
// Poll total rares every 5 minutes (300,000 ms)
|
// Set up recurring polls
|
||||||
setInterval(pollTotalRares, 300000);
|
pollIntervals.push(setInterval(pollLive, POLL_MS));
|
||||||
// Poll total kills every 5 minutes (300,000 ms)
|
pollIntervals.push(setInterval(pollTotalRares, POLL_RARES_MS));
|
||||||
setInterval(pollTotalKills, 300000);
|
pollIntervals.push(setInterval(pollTotalKills, POLL_KILLS_MS));
|
||||||
// Poll server health every 30 seconds (30,000 ms)
|
pollIntervals.push(setInterval(pollServerHealth, POLL_HEALTH_MS));
|
||||||
setInterval(pollServerHealth, 30000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
|
@ -1484,7 +1537,8 @@ function renderList() {
|
||||||
p.character_name.toLowerCase().startsWith(currentFilter)
|
p.character_name.toLowerCase().startsWith(currentFilter)
|
||||||
);
|
);
|
||||||
// Sort filtered list
|
// Sort filtered list
|
||||||
const sorted = filtered.slice().sort(currentSort.comparator);
|
filtered.sort(currentSort.comparator);
|
||||||
|
const sorted = filtered;
|
||||||
render(sorted);
|
render(sorted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1508,11 +1562,11 @@ document.addEventListener('mouseup', () => {
|
||||||
|
|
||||||
function render(players) {
|
function render(players) {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
console.log('🔄 RENDER STARTING:', new Date().toISOString());
|
debugLog('🔄 RENDER STARTING:', new Date().toISOString());
|
||||||
|
|
||||||
// If user is actively clicking, defer this render briefly
|
// If user is actively clicking, defer this render briefly
|
||||||
if (userInteracting) {
|
if (userInteracting) {
|
||||||
console.log('🔄 RENDER DEFERRED: User interaction detected');
|
debugLog('🔄 RENDER DEFERRED: User interaction detected');
|
||||||
setTimeout(() => render(players), 100);
|
setTimeout(() => render(players), 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1730,30 +1784,29 @@ function render(players) {
|
||||||
// Optimization is achieving 100% element reuse consistently
|
// Optimization is achieving 100% element reuse consistently
|
||||||
|
|
||||||
const renderTime = performance.now() - startTime;
|
const renderTime = performance.now() - startTime;
|
||||||
console.log('🔄 RENDER COMPLETED:', renderTime.toFixed(2) + 'ms');
|
debugLog('🔄 RENDER COMPLETED:', renderTime.toFixed(2) + 'ms');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- rendering trails ------------------------------- */
|
/* ---------- rendering trails ------------------------------- */
|
||||||
function renderTrails(trailData) {
|
function renderTrails(trailData) {
|
||||||
trailsContainer.innerHTML = '';
|
trailsContainer.innerHTML = '';
|
||||||
const byChar = trailData.reduce((acc, pt) => {
|
// Build point strings directly - avoid intermediate arrays
|
||||||
(acc[pt.character_name] = acc[pt.character_name] || []).push(pt);
|
const byChar = {};
|
||||||
return acc;
|
for (const pt of trailData) {
|
||||||
}, {});
|
const { x, y } = worldToPx(pt.ew, pt.ns);
|
||||||
for (const [name, pts] of Object.entries(byChar)) {
|
const key = pt.character_name;
|
||||||
if (pts.length < 2) continue;
|
if (!byChar[key]) byChar[key] = { points: `${x},${y}`, count: 1 };
|
||||||
const points = pts.map(pt => {
|
else { byChar[key].points += ` ${x},${y}`; byChar[key].count++; }
|
||||||
const { x, y } = worldToPx(pt.ew, pt.ns);
|
}
|
||||||
return `${x},${y}`;
|
for (const name in byChar) {
|
||||||
}).join(' ');
|
if (byChar[name].count < 2) continue;
|
||||||
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||||
poly.setAttribute('points', points);
|
poly.setAttribute('points', byChar[name].points);
|
||||||
// Use the same color as the player dot for consistency
|
poly.setAttribute('stroke', getColorFor(name));
|
||||||
poly.setAttribute('stroke', getColorFor(name));
|
poly.setAttribute('fill', 'none');
|
||||||
poly.setAttribute('fill', 'none');
|
poly.setAttribute('class', 'trail-path');
|
||||||
poly.setAttribute('class', 'trail-path');
|
trailsContainer.appendChild(poly);
|
||||||
trailsContainer.appendChild(poly);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/* ---------- selection centering, focus zoom & blink ------------ */
|
/* ---------- selection centering, focus zoom & blink ------------ */
|
||||||
function selectPlayer(p, x, y) {
|
function selectPlayer(p, x, y) {
|
||||||
|
|
@ -1793,44 +1846,31 @@ function initWebSocket() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.addEventListener('close', () => setTimeout(initWebSocket, 2000));
|
socket.addEventListener('close', () => setTimeout(initWebSocket, 2000));
|
||||||
socket.addEventListener('error', e => console.error('WebSocket error:', e));
|
socket.addEventListener('error', e => handleError('WebSocket', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display or create a chat window for a character
|
// Display or create a chat window for a character
|
||||||
function showChatWindow(name) {
|
function showChatWindow(name) {
|
||||||
console.log('💬 showChatWindow called for:', name);
|
debugLog('showChatWindow called for:', name);
|
||||||
if (chatWindows[name]) {
|
const windowId = `chatWindow-${name}`;
|
||||||
const existing = chatWindows[name];
|
|
||||||
console.log('💬 Existing chat window found, showing it:', existing);
|
const { win, content, isNew } = createWindow(
|
||||||
// Always show the window (no toggle)
|
windowId, `Chat: ${name}`, 'chat-window'
|
||||||
existing.style.display = 'flex';
|
);
|
||||||
// Bring to front when opening
|
|
||||||
if (!window.__chatZ) window.__chatZ = 10000;
|
if (!isNew) {
|
||||||
window.__chatZ += 1;
|
debugLog('Existing chat window found, showing it');
|
||||||
existing.style.zIndex = window.__chatZ;
|
|
||||||
console.log('💬 Chat window shown with zIndex:', window.__chatZ);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('💬 Creating new chat window for:', name);
|
|
||||||
const win = document.createElement('div');
|
|
||||||
win.className = 'chat-window';
|
|
||||||
win.dataset.character = name;
|
win.dataset.character = name;
|
||||||
// Header
|
chatWindows[name] = win;
|
||||||
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
|
// Messages container
|
||||||
const msgs = document.createElement('div');
|
const msgs = document.createElement('div');
|
||||||
msgs.className = 'chat-messages';
|
msgs.className = 'chat-messages';
|
||||||
win.appendChild(msgs);
|
content.appendChild(msgs);
|
||||||
|
|
||||||
// Input form
|
// Input form
|
||||||
const form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
form.className = 'chat-form';
|
form.className = 'chat-form';
|
||||||
|
|
@ -1847,14 +1887,9 @@ function showChatWindow(name) {
|
||||||
socket.send(JSON.stringify({ player_name: name, command: text }));
|
socket.send(JSON.stringify({ player_name: name, command: text }));
|
||||||
input.value = '';
|
input.value = '';
|
||||||
});
|
});
|
||||||
win.appendChild(form);
|
content.appendChild(form);
|
||||||
console.log('💬 Appending chat window to DOM:', win);
|
|
||||||
document.body.appendChild(win);
|
|
||||||
chatWindows[name] = win;
|
|
||||||
console.log('💬 Chat window added to DOM, total children:', document.body.children.length);
|
|
||||||
|
|
||||||
// Enable dragging using the global drag system
|
debugLog('Chat window created for:', name);
|
||||||
makeDraggable(win, header);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a chat message to the correct window
|
// Append a chat message to the correct window
|
||||||
|
|
@ -1905,7 +1940,7 @@ wrap.addEventListener('wheel', e => {
|
||||||
offX -= mx * (ns - scale);
|
offX -= mx * (ns - scale);
|
||||||
offY -= my * (ns - scale);
|
offY -= my * (ns - scale);
|
||||||
scale = ns;
|
scale = ns;
|
||||||
updateView();
|
scheduleViewUpdate();
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
wrap.addEventListener('mousedown', e => {
|
wrap.addEventListener('mousedown', e => {
|
||||||
|
|
@ -1916,7 +1951,7 @@ window.addEventListener('mousemove', e => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
offX += e.clientX - sx; offY += e.clientY - sy;
|
offX += e.clientX - sx; offY += e.clientY - sy;
|
||||||
sx = e.clientX; sy = e.clientY;
|
sx = e.clientX; sy = e.clientY;
|
||||||
updateView();
|
scheduleViewUpdate();
|
||||||
});
|
});
|
||||||
window.addEventListener('mouseup', () => {
|
window.addEventListener('mouseup', () => {
|
||||||
dragging = false; wrap.classList.remove('dragging');
|
dragging = false; wrap.classList.remove('dragging');
|
||||||
|
|
@ -1932,7 +1967,7 @@ wrap.addEventListener('touchmove', e => {
|
||||||
const t = e.touches[0];
|
const t = e.touches[0];
|
||||||
offX += t.clientX - sx; offY += t.clientY - sy;
|
offX += t.clientX - sx; offY += t.clientY - sy;
|
||||||
sx = t.clientX; sy = t.clientY;
|
sx = t.clientX; sy = t.clientY;
|
||||||
updateView();
|
scheduleViewUpdate();
|
||||||
});
|
});
|
||||||
wrap.addEventListener('touchend', () => {
|
wrap.addEventListener('touchend', () => {
|
||||||
dragging = false;
|
dragging = false;
|
||||||
|
|
@ -2059,14 +2094,14 @@ function processNotificationQueue() {
|
||||||
|
|
||||||
container.appendChild(notifEl);
|
container.appendChild(notifEl);
|
||||||
|
|
||||||
// Remove notification after 6 seconds and process next
|
// Remove notification after display duration and process next
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notifEl.style.animation = 'notification-slide-out 0.5s ease-in forwards';
|
notifEl.style.animation = 'notification-slide-out 0.5s ease-in forwards';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notifEl.remove();
|
notifEl.remove();
|
||||||
processNotificationQueue();
|
processNotificationQueue();
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 6000);
|
}, NOTIFICATION_DURATION_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add slide out animation
|
// Add slide out animation
|
||||||
|
|
@ -2136,18 +2171,16 @@ function createFireworks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightRareFinder(characterName) {
|
function highlightRareFinder(characterName) {
|
||||||
// Find the player in the list
|
// Use element pool for O(1) lookup instead of querySelectorAll
|
||||||
const playerItems = document.querySelectorAll('#playerList li');
|
for (const item of elementPools.activeListItems) {
|
||||||
playerItems.forEach(item => {
|
if (item.playerData && item.playerData.character_name === characterName) {
|
||||||
const nameSpan = item.querySelector('.player-name');
|
item.classList.add('rare-finder-glow');
|
||||||
if (nameSpan && nameSpan.textContent.includes(characterName)) {
|
setTimeout(() => {
|
||||||
item.classList.add('rare-finder-glow');
|
item.classList.remove('rare-finder-glow');
|
||||||
// Remove glow after 5 seconds
|
}, GLOW_DURATION_MS);
|
||||||
setTimeout(() => {
|
break;
|
||||||
item.classList.remove('rare-finder-glow');
|
}
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total rares display to trigger fireworks on increase
|
// Update total rares display to trigger fireworks on increase
|
||||||
|
|
@ -2170,7 +2203,7 @@ updateTotalRaresDisplay = function(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerMilestoneCelebration(rareNumber) {
|
function triggerMilestoneCelebration(rareNumber) {
|
||||||
console.log(`🏆 MILESTONE: Rare #${rareNumber}! 🏆`);
|
debugLog(`🏆 MILESTONE: Rare #${rareNumber}! 🏆`);
|
||||||
|
|
||||||
// Create full-screen milestone overlay
|
// Create full-screen milestone overlay
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,14 @@ body {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -540,6 +540,14 @@ body {
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -1562,3 +1570,23 @@ body.noselect, body.noselect * {
|
||||||
color: #88ccff;
|
color: #88ccff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Error Toast */
|
||||||
|
.error-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(220, 38, 38, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 99999;
|
||||||
|
animation: toastFadeIn 0.3s ease;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue