diff --git a/docs/plans/2026-02-26-script-js-cleanup-plan.md b/docs/plans/2026-02-26-script-js-cleanup-plan.md deleted file mode 100644 index 2debfee8..00000000 --- a/docs/plans/2026-02-26-script-js-cleanup-plan.md +++ /dev/null @@ -1,607 +0,0 @@ -# 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 = `
Failed to load...
`; -// After: handleError('Inventory', err, true); -// content.innerHTML = `
Failed to load inventory
`; - -// 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. diff --git a/docs/plans/2026-02-26-script-js-review-design.md b/docs/plans/2026-02-26-script-js-review-design.md deleted file mode 100644 index e16b3a51..00000000 --- a/docs/plans/2026-02-26-script-js-review-design.md +++ /dev/null @@ -1,206 +0,0 @@ -# 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 diff --git a/static/script.js b/static/script.js index ecbf64ac..20b926f4 100644 --- a/static/script.js +++ b/static/script.js @@ -22,21 +22,6 @@ * 6. Rendering functions for list and map * 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 --------------------------------------- */ const wrap = document.getElementById('mapContainer'); const group = document.getElementById('mapGroup'); @@ -112,16 +97,16 @@ function createNewListItem() { chatBtn.className = 'chat-btn'; chatBtn.textContent = 'Chat'; chatBtn.addEventListener('click', (e) => { - debugLog('🔥 CHAT BUTTON CLICKED!', e.target, e.currentTarget); + console.log('🔥 CHAT BUTTON CLICKED!', e.target, e.currentTarget); e.stopPropagation(); // Try button's own playerData first, fallback to DOM traversal const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData; - debugLog('🔥 Player data found:', playerData); + console.log('🔥 Player data found:', playerData); if (playerData) { - debugLog('🔥 Opening chat for:', playerData.character_name); + console.log('🔥 Opening chat for:', playerData.character_name); showChatWindow(playerData.character_name); } else { - debugLog('🔥 No player data found!'); + console.log('🔥 No player data found!'); } }); @@ -129,16 +114,16 @@ function createNewListItem() { statsBtn.className = 'stats-btn'; statsBtn.textContent = 'Stats'; statsBtn.addEventListener('click', (e) => { - debugLog('📊 STATS BUTTON CLICKED!', e.target, e.currentTarget); + console.log('📊 STATS BUTTON CLICKED!', e.target, e.currentTarget); e.stopPropagation(); // Try button's own playerData first, fallback to DOM traversal const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData; - debugLog('📊 Player data found:', playerData); + console.log('📊 Player data found:', playerData); if (playerData) { - debugLog('📊 Opening stats for:', playerData.character_name); + console.log('📊 Opening stats for:', playerData.character_name); showStatsWindow(playerData.character_name); } else { - debugLog('📊 No player data found!'); + console.log('📊 No player data found!'); } }); @@ -146,16 +131,16 @@ function createNewListItem() { inventoryBtn.className = 'inventory-btn'; inventoryBtn.textContent = 'Inventory'; inventoryBtn.addEventListener('click', (e) => { - debugLog('🎒 INVENTORY BUTTON CLICKED!', e.target, e.currentTarget); + console.log('🎒 INVENTORY BUTTON CLICKED!', e.target, e.currentTarget); e.stopPropagation(); // Try button's own playerData first, fallback to DOM traversal const playerData = e.currentTarget.playerData || e.target.closest('li.player-item')?.playerData; - debugLog('🎒 Player data found:', playerData); + console.log('🎒 Player data found:', playerData); if (playerData) { - debugLog('🎒 Opening inventory for:', playerData.character_name); + console.log('🎒 Opening inventory for:', playerData.character_name); showInventoryWindow(playerData.character_name); } else { - debugLog('🎒 No player data found!'); + console.log('🎒 No player data found!'); } }); @@ -313,13 +298,6 @@ const inventoryWindows = {}; const MAX_Z = 20; const FOCUS_ZOOM = 3; // zoom level when you click a name 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 const MAP_BOUNDS = { west: -102.1, @@ -510,7 +488,7 @@ let imgW = 0, imgH = 0; let scale = 1, offX = 0, offY = 0, minScale = 1; let dragging = false, sx = 0, sy = 0; let selected = ""; -const pollIntervals = []; +let pollID = null; /* ---------- utility functions ----------------------------------- */ const hue = name => { @@ -575,17 +553,17 @@ function initHeatMap() { async function fetchHeatmapData() { try { - const response = await fetch(`${API_BASE}/spawns/heatmap?hours=${HEATMAP_HOURS}&limit=${MAX_HEATMAP_POINTS}`); + const response = await fetch(`${API_BASE}/spawns/heatmap?hours=24&limit=50000`); if (!response.ok) { throw new Error(`Heat map API error: ${response.status}`); } const data = await response.json(); heatmapData = data.spawn_points; // [{ew, ns, intensity}] - debugLog(`Loaded ${heatmapData.length} heat map points from last ${data.hours_window} hours`); + console.log(`Loaded ${heatmapData.length} heat map points from last ${data.hours_window} hours`); renderHeatmap(); } catch (err) { - handleError('Heatmap', err); + console.error('Failed to fetch heat map data:', err); } } @@ -667,10 +645,10 @@ async function fetchPortalData() { const data = await response.json(); portalData = data.portals; // [{portal_name, coordinates: {ns, ew, z}, discovered_by, discovered_at}] - debugLog(`Loaded ${portalData.length} portals from last hour`); + console.log(`Loaded ${portalData.length} portals from last hour`); renderPortals(); } catch (err) { - handleError('Portals', err); + console.error('Failed to fetch portal data:', err); } } @@ -729,7 +707,7 @@ function renderPortals() { portalContainer.appendChild(icon); } - debugLog(`Rendered ${portalData.length} portal icons`); + console.log(`Rendered ${portalData.length} portal icons`); } function clearPortals() { @@ -746,77 +724,37 @@ function debounce(fn, ms) { }; } -/** - * 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 { 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; - - 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'); + console.log('📊 showStatsWindow called for:', name); + if (statsWindows[name]) { + const existing = statsWindows[name]; + console.log('📊 Existing stats window found, showing it:', existing); + // Always show the window (no toggle) + existing.style.display = 'flex'; + // Bring to front when opening + if (!window.__chatZ) window.__chatZ = 10000; + window.__chatZ += 1; + existing.style.zIndex = window.__chatZ; + console.log('📊 Stats window shown with zIndex:', window.__chatZ); return; } - + console.log('📊 Creating new stats window for:', name); + const win = document.createElement('div'); + win.className = 'stats-window'; win.dataset.character = name; - statsWindows[name] = win; - + // Header (reuses chat-header styling) + 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 const controls = document.createElement('div'); controls.className = 'stats-controls'; @@ -826,12 +764,6 @@ function showStatsWindow(name) { { label: '24H', value: 'now-24h' }, { 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 => { const btn = document.createElement('button'); btn.className = 'time-range-btn'; @@ -840,17 +772,25 @@ function showStatsWindow(name) { btn.addEventListener('click', () => { controls.querySelectorAll('.time-range-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); - updateStatsTimeRange(statsContent, name, range.value); + updateStatsTimeRange(content, name, range.value); }); controls.appendChild(btn); }); - - content.appendChild(controls); - content.appendChild(statsContent); - - debugLog('Stats window created for:', name); + win.appendChild(controls); + + // Content container + const content = document.createElement('div'); + content.className = 'chat-messages'; + content.textContent = 'Loading stats...'; + 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 - updateStatsTimeRange(statsContent, name, 'now-24h'); + updateStatsTimeRange(content, name, 'now-24h'); + // Enable dragging using the global drag system + makeDraggable(win, header); } function updateStatsTimeRange(content, name, timeRange) { @@ -881,33 +821,47 @@ function updateStatsTimeRange(content, name, timeRange) { // Show or create an inventory window for a character function showInventoryWindow(name) { - debugLog('showInventoryWindow called for:', name); - const windowId = `inventoryWindow-${name}`; - - const { win, content, isNew } = createWindow( - windowId, `Inventory: ${name}`, 'inventory-window' - ); - - if (!isNew) { - debugLog('Existing inventory window found, showing it'); + console.log('🎒 showInventoryWindow called for:', name); + if (inventoryWindows[name]) { + const existing = inventoryWindows[name]; + console.log('🎒 Existing inventory window found, showing it:', existing); + // Always show the window (no toggle) + existing.style.display = 'flex'; + // Bring to front when opening + if (!window.__chatZ) window.__chatZ = 10000; + window.__chatZ += 1; + existing.style.zIndex = window.__chatZ; + console.log('🎒 Inventory window shown with zIndex:', window.__chatZ); return; } - + console.log('🎒 Creating new inventory window for:', name); + const win = document.createElement('div'); + win.className = 'inventory-window'; win.dataset.character = name; - inventoryWindows[name] = win; - + // Header (reuses chat-header styling) + 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 const loading = document.createElement('div'); loading.className = 'inventory-loading'; loading.textContent = 'Loading inventory...'; - content.appendChild(loading); - - // Inventory content container - const invContent = document.createElement('div'); - invContent.className = 'inventory-content'; - invContent.style.display = 'none'; - content.appendChild(invContent); - + win.appendChild(loading); + + // Content container + const content = document.createElement('div'); + content.className = 'inventory-content'; + content.style.display = 'none'; + win.appendChild(content); + // Fetch inventory data from main app (which will proxy to inventory service) fetch(`${API_BASE}/inventory/${encodeURIComponent(name)}?limit=1000`) .then(response => { @@ -916,38 +870,38 @@ function showInventoryWindow(name) { }) .then(data => { loading.style.display = 'none'; - invContent.style.display = 'block'; - + content.style.display = 'block'; + // Create inventory grid const grid = document.createElement('div'); grid.className = 'inventory-grid'; - + // Render each item data.items.forEach(item => { - + const slot = document.createElement('div'); slot.className = 'inventory-slot'; - + // Create layered icon container const iconContainer = document.createElement('div'); iconContainer.className = 'item-icon-composite'; - + // Get base icon ID with portal.dat offset const baseIconId = (item.icon + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); - + // Check for overlay and underlay from enhanced format or legacy format let overlayIconId = null; let underlayIconId = null; - + // Enhanced format (inventory service) - check for proper icon overlay/underlay properties if (item.icon_overlay_id && item.icon_overlay_id > 0) { overlayIconId = (item.icon_overlay_id + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); } - + if (item.icon_underlay_id && item.icon_underlay_id > 0) { underlayIconId = (item.icon_underlay_id + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); } - + // Fallback: Enhanced format (inventory service) - check spells object for decal info if (!overlayIconId && !underlayIconId && item.spells && typeof item.spells === 'object') { // Icon overlay (using the actual property names from the data) @@ -955,8 +909,8 @@ function showInventoryWindow(name) { if (item.spells.spell_decal_218103838 && item.spells.spell_decal_218103838 > 100) { overlayIconId = (item.spells.spell_decal_218103838 + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); } - - // Icon underlay + + // Icon underlay if (item.spells.spell_decal_218103848 && item.spells.spell_decal_218103848 > 100) { underlayIconId = (item.spells.spell_decal_218103848 + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); } @@ -964,13 +918,13 @@ function showInventoryWindow(name) { // Legacy format - parse item_data try { const itemData = typeof item.item_data === 'string' ? JSON.parse(item.item_data) : item.item_data; - + if (itemData.IntValues) { // Icon overlay (ID 218103849) - only use valid icon IDs if (itemData.IntValues['218103849'] && itemData.IntValues['218103849'] > 100) { overlayIconId = (itemData.IntValues['218103849'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); } - + // Icon underlay (ID 218103850) - only use valid icon IDs if (itemData.IntValues['218103850'] && itemData.IntValues['218103850'] > 100) { underlayIconId = (itemData.IntValues['218103850'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0'); @@ -980,7 +934,7 @@ function showInventoryWindow(name) { console.warn('Failed to parse item data for', item.name); } } - + // Create underlay (bottom layer) if (underlayIconId) { const underlayImg = document.createElement('img'); @@ -990,7 +944,7 @@ function showInventoryWindow(name) { underlayImg.onerror = function() { this.style.display = 'none'; }; iconContainer.appendChild(underlayImg); } - + // Create base icon (middle layer) const baseImg = document.createElement('img'); baseImg.className = 'icon-base'; @@ -1001,7 +955,7 @@ function showInventoryWindow(name) { this.src = '/icons/06000133.png'; }; iconContainer.appendChild(baseImg); - + // Create overlay (top layer) if (overlayIconId) { const overlayImg = document.createElement('img'); @@ -1011,68 +965,74 @@ function showInventoryWindow(name) { overlayImg.onerror = function() { this.style.display = 'none'; }; iconContainer.appendChild(overlayImg); } - + // Create tooltip data slot.dataset.name = item.name || 'Unknown Item'; slot.dataset.value = item.value || 0; slot.dataset.burden = item.burden || 0; - + // Store enhanced data for tooltips // All data now comes from inventory service (no more local fallback) if (item.max_damage !== undefined || item.object_class_name !== undefined || item.spells !== undefined) { // Inventory service provides clean, structured data with translations // Only include properties that actually exist on the item const enhancedData = {}; - + // Check all possible enhanced properties from inventory service const possibleProps = [ 'max_damage', 'armor_level', 'damage_bonus', 'attack_bonus', 'wield_level', 'skill_level', 'lore_requirement', 'equip_skill', 'equip_skill_name', - 'material', 'material_name', 'material_id', 'imbue', 'item_set', 'tinks', - 'workmanship', 'workmanship_text', 'damage_rating', 'crit_rating', + 'material', 'material_name', 'material_id', 'imbue', 'item_set', 'tinks', + 'workmanship', 'workmanship_text', 'damage_rating', 'crit_rating', 'heal_boost_rating', 'has_id_data', 'object_class_name', 'spells', 'enhanced_properties', 'damage_range', 'damage_type', 'min_damage', 'speed_text', 'speed_value', 'mana_display', 'spellcraft', 'current_mana', 'max_mana', - 'melee_defense_bonus', 'magic_defense_bonus', 'missile_defense_bonus', + 'melee_defense_bonus', 'magic_defense_bonus', 'missile_defense_bonus', 'elemental_damage_vs_monsters', 'mana_conversion_bonus', 'icon_overlay_id', 'icon_underlay_id' ]; - + // Only add properties that exist and have meaningful values possibleProps.forEach(prop => { if (item.hasOwnProperty(prop) && item[prop] !== undefined && item[prop] !== null) { enhancedData[prop] = item[prop]; } }); - + slot.dataset.enhancedData = JSON.stringify(enhancedData); } else { // No enhanced data available slot.dataset.enhancedData = JSON.stringify({}); } - + // Add tooltip on hover slot.addEventListener('mouseenter', e => showInventoryTooltip(e, slot)); slot.addEventListener('mousemove', e => showInventoryTooltip(e, slot)); slot.addEventListener('mouseleave', hideInventoryTooltip); - + slot.appendChild(iconContainer); grid.appendChild(slot); }); - - invContent.appendChild(grid); - + + content.appendChild(grid); + // Add item count const count = document.createElement('div'); count.className = 'inventory-count'; count.textContent = `${data.item_count} items`; - invContent.appendChild(count); + content.appendChild(count); }) .catch(err => { - handleError('Inventory', err, true); loading.textContent = `Failed to load inventory: ${err.message}`; + console.error('Inventory fetch failed:', err); }); - - debugLog('Inventory window created for:', name); + + console.log('🎒 Appending inventory window to DOM:', win); + 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 @@ -1334,16 +1294,6 @@ function updateView() { } } -let pendingFrame = null; -function scheduleViewUpdate() { - if (!pendingFrame) { - pendingFrame = requestAnimationFrame(() => { - updateView(); - pendingFrame = null; - }); - } -} - function fitToWindow() { const r = wrap.getBoundingClientRect(); scale = Math.min(r.width / imgW, r.height / imgH); @@ -1376,7 +1326,7 @@ async function pollLive() { renderTrails(trails); renderList(); } catch (e) { - handleError('Player update', e); + console.error('Live or trails fetch failed:', e); } } @@ -1386,7 +1336,7 @@ async function pollTotalRares() { const data = await response.json(); updateTotalRaresDisplay(data); } catch (e) { - handleError('Rare counter', e); + console.error('Total rares fetch failed:', e); } } @@ -1405,7 +1355,7 @@ async function pollTotalKills() { const data = await response.json(); updateTotalKillsDisplay(data); } catch (e) { - handleError('Kill counter', e); + console.error('Total kills fetch failed:', e); } } @@ -1422,7 +1372,7 @@ async function pollServerHealth() { const data = await response.json(); updateServerStatusDisplay(data); } catch (e) { - handleError('Server health', e); + console.error('Server health fetch failed:', e); updateServerStatusDisplay({ status: 'error' }); } } @@ -1480,7 +1430,7 @@ function handleServerStatusUpdate(msg) { // Handle real-time server status updates via WebSocket if (msg.status === 'up' && msg.message) { // Show notification for server coming back online - debugLog(`Server Status: ${msg.message}`); + console.log(`Server Status: ${msg.message}`); } // Trigger an immediate server health poll to refresh the display @@ -1488,21 +1438,18 @@ function handleServerStatusUpdate(msg) { } function startPolling() { - // Clear any existing intervals first (prevents leak on re-init) - pollIntervals.forEach(id => clearInterval(id)); - pollIntervals.length = 0; - - // Initial fetches + if (pollID !== null) return; 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)); + pollTotalRares(); // Initial fetch + pollTotalKills(); // Initial fetch + pollServerHealth(); // Initial server health check + pollID = setInterval(pollLive, POLL_MS); + // Poll total rares every 5 minutes (300,000 ms) + setInterval(pollTotalRares, 300000); + // Poll total kills every 5 minutes (300,000 ms) + setInterval(pollTotalKills, 300000); + // Poll server health every 30 seconds (30,000 ms) + setInterval(pollServerHealth, 30000); } img.onload = () => { @@ -1537,8 +1484,7 @@ function renderList() { p.character_name.toLowerCase().startsWith(currentFilter) ); // Sort filtered list - filtered.sort(currentSort.comparator); - const sorted = filtered; + const sorted = filtered.slice().sort(currentSort.comparator); render(sorted); } @@ -1562,11 +1508,11 @@ document.addEventListener('mouseup', () => { function render(players) { const startTime = performance.now(); - debugLog('🔄 RENDER STARTING:', new Date().toISOString()); + console.log('🔄 RENDER STARTING:', new Date().toISOString()); // If user is actively clicking, defer this render briefly if (userInteracting) { - debugLog('🔄 RENDER DEFERRED: User interaction detected'); + console.log('🔄 RENDER DEFERRED: User interaction detected'); setTimeout(() => render(players), 100); return; } @@ -1784,29 +1730,30 @@ function render(players) { // Optimization is achieving 100% element reuse consistently const renderTime = performance.now() - startTime; - debugLog('🔄 RENDER COMPLETED:', renderTime.toFixed(2) + 'ms'); + console.log('🔄 RENDER COMPLETED:', renderTime.toFixed(2) + 'ms'); } /* ---------- rendering trails ------------------------------- */ 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); - } + trailsContainer.innerHTML = ''; + const byChar = trailData.reduce((acc, pt) => { + (acc[pt.character_name] = acc[pt.character_name] || []).push(pt); + return acc; + }, {}); + for (const [name, pts] of Object.entries(byChar)) { + if (pts.length < 2) continue; + const points = pts.map(pt => { + const { x, y } = worldToPx(pt.ew, pt.ns); + return `${x},${y}`; + }).join(' '); + const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); + poly.setAttribute('points', points); + // 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); + } } /* ---------- selection centering, focus zoom & blink ------------ */ function selectPlayer(p, x, y) { @@ -1846,31 +1793,44 @@ function initWebSocket() { } }); socket.addEventListener('close', () => setTimeout(initWebSocket, 2000)); - socket.addEventListener('error', e => handleError('WebSocket', e)); + socket.addEventListener('error', e => console.error('WebSocket error:', e)); } // Display or create a chat window for a character function showChatWindow(name) { - debugLog('showChatWindow called for:', name); - const windowId = `chatWindow-${name}`; - - const { win, content, isNew } = createWindow( - windowId, `Chat: ${name}`, 'chat-window' - ); - - if (!isNew) { - debugLog('Existing chat window found, showing it'); + console.log('💬 showChatWindow called for:', name); + if (chatWindows[name]) { + const existing = chatWindows[name]; + console.log('💬 Existing chat window found, showing it:', existing); + // Always show the window (no toggle) + existing.style.display = 'flex'; + // Bring to front when opening + if (!window.__chatZ) window.__chatZ = 10000; + window.__chatZ += 1; + existing.style.zIndex = window.__chatZ; + console.log('💬 Chat window shown with zIndex:', window.__chatZ); return; } - + console.log('💬 Creating new chat window for:', name); + const win = document.createElement('div'); + win.className = 'chat-window'; win.dataset.character = name; - chatWindows[name] = win; - + // Header + const header = document.createElement('div'); + header.className = 'chat-header'; + const title = document.createElement('span'); + title.textContent = `Chat: ${name}`; + const closeBtn = document.createElement('button'); + closeBtn.className = 'chat-close-btn'; + closeBtn.textContent = '×'; + closeBtn.addEventListener('click', () => { win.style.display = 'none'; }); + header.appendChild(title); + header.appendChild(closeBtn); + win.appendChild(header); // Messages container const msgs = document.createElement('div'); msgs.className = 'chat-messages'; - content.appendChild(msgs); - + win.appendChild(msgs); // Input form const form = document.createElement('form'); form.className = 'chat-form'; @@ -1887,9 +1847,14 @@ function showChatWindow(name) { socket.send(JSON.stringify({ player_name: name, command: text })); input.value = ''; }); - content.appendChild(form); + win.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); - debugLog('Chat window created for:', name); + // Enable dragging using the global drag system + makeDraggable(win, header); } // Append a chat message to the correct window @@ -1940,7 +1905,7 @@ wrap.addEventListener('wheel', e => { offX -= mx * (ns - scale); offY -= my * (ns - scale); scale = ns; - scheduleViewUpdate(); + updateView(); }, { passive: false }); wrap.addEventListener('mousedown', e => { @@ -1951,7 +1916,7 @@ window.addEventListener('mousemove', e => { if (!dragging) return; offX += e.clientX - sx; offY += e.clientY - sy; sx = e.clientX; sy = e.clientY; - scheduleViewUpdate(); + updateView(); }); window.addEventListener('mouseup', () => { dragging = false; wrap.classList.remove('dragging'); @@ -1967,7 +1932,7 @@ wrap.addEventListener('touchmove', e => { const t = e.touches[0]; offX += t.clientX - sx; offY += t.clientY - sy; sx = t.clientX; sy = t.clientY; - scheduleViewUpdate(); + updateView(); }); wrap.addEventListener('touchend', () => { dragging = false; @@ -2094,14 +2059,14 @@ function processNotificationQueue() { container.appendChild(notifEl); - // Remove notification after display duration and process next + // Remove notification after 6 seconds and process next setTimeout(() => { notifEl.style.animation = 'notification-slide-out 0.5s ease-in forwards'; setTimeout(() => { notifEl.remove(); processNotificationQueue(); }, 500); - }, NOTIFICATION_DURATION_MS); + }, 6000); } // Add slide out animation @@ -2171,16 +2136,18 @@ function createFireworks() { } 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; - } + // Find the player in the list + const playerItems = document.querySelectorAll('#playerList li'); + playerItems.forEach(item => { + const nameSpan = item.querySelector('.player-name'); + if (nameSpan && nameSpan.textContent.includes(characterName)) { + item.classList.add('rare-finder-glow'); + // Remove glow after 5 seconds + setTimeout(() => { + item.classList.remove('rare-finder-glow'); + }, 5000); } + }); } // Update total rares display to trigger fireworks on increase @@ -2203,7 +2170,7 @@ updateTotalRaresDisplay = function(data) { } function triggerMilestoneCelebration(rareNumber) { - debugLog(`🏆 MILESTONE: Rare #${rareNumber}! 🏆`); + console.log(`🏆 MILESTONE: Rare #${rareNumber}! 🏆`); // Create full-screen milestone overlay const overlay = document.createElement('div'); diff --git a/static/style-ac.css b/static/style-ac.css index a25e1680..7d30d4cf 100644 --- a/static/style-ac.css +++ b/static/style-ac.css @@ -363,14 +363,6 @@ body { border-radius: 2px; } -.window-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - min-height: 0; -} - .chat-header { display: flex; justify-content: space-between; diff --git a/static/style.css b/static/style.css index b17b8d70..d1b8c82f 100644 --- a/static/style.css +++ b/static/style.css @@ -540,14 +540,6 @@ body { z-index: 10000; } -.window-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - min-height: 0; -} - .chat-header { display: flex; justify-content: space-between; @@ -1570,23 +1562,3 @@ body.noselect, body.noselect * { 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); } -} -