debug-panel.js
1 // ============================================================================ 2 // DEBUG PANEL - Activated with '?' key 3 // ============================================================================ 4 5 class DebugPanel { 6 constructor() { 7 this.enabled = false; 8 this.element = null; 9 this.iconElement = null; 10 this.setupKeyListener(); 11 this.createIcon(); 12 } 13 14 setupKeyListener() { 15 document.addEventListener("keydown", (e) => { 16 if (e.key === "?" || (e.shiftKey && e.key === "/")) { 17 this.toggle(); 18 } 19 }); 20 } 21 22 createIcon() { 23 this.iconElement = document.createElement("div"); 24 this.iconElement.id = "debug-icon"; 25 this.iconElement.innerHTML = "⚙"; 26 27 // Check if mobile 28 const isMobile = window.innerWidth <= 768 || window.matchMedia("(orientation: portrait)").matches; 29 console.log('Debug icon - isMobile:', isMobile, 'width:', window.innerWidth); 30 31 // Desktop: fixed to viewport (right corner), Mobile: absolute in content box (left corner) 32 if (isMobile) { 33 this.iconElement.style.position = 'absolute'; 34 this.iconElement.style.bottom = '70px'; 35 this.iconElement.style.left = '20px'; 36 } else { 37 this.iconElement.style.position = 'fixed'; 38 this.iconElement.style.bottom = '30px'; 39 this.iconElement.style.right = '30px'; 40 } 41 42 this.iconElement.style.width = '40px'; 43 this.iconElement.style.height = '40px'; 44 this.iconElement.style.background = 'rgba(0, 0, 0, 0.4)'; 45 this.iconElement.style.color = 'rgba(255, 255, 255, 0.3)'; 46 this.iconElement.style.border = '1px solid rgba(255, 255, 255, 0.15)'; 47 this.iconElement.style.borderRadius = '50%'; 48 this.iconElement.style.display = 'flex'; 49 this.iconElement.style.alignItems = 'center'; 50 this.iconElement.style.justifyContent = 'center'; 51 this.iconElement.style.fontFamily = "'JetBrains Mono', monospace"; 52 this.iconElement.style.fontSize = '20px'; 53 this.iconElement.style.fontWeight = '400'; 54 this.iconElement.style.cursor = 'pointer'; 55 this.iconElement.style.pointerEvents = 'auto'; 56 this.iconElement.style.zIndex = '9999'; 57 this.iconElement.style.transition = 'all 0.2s ease'; 58 this.iconElement.style.backdropFilter = 'blur(4px)'; 59 this.iconElement.style.webkitBackdropFilter = 'blur(4px)'; 60 this.iconElement.style.userSelect = 'none'; 61 this.iconElement.style.webkitUserSelect = 'none'; 62 this.iconElement.style.touchAction = 'manipulation'; 63 this.iconElement.style.webkitTapHighlightColor = 'transparent'; 64 65 // Hover/touch active effect 66 const setActiveState = () => { 67 this.iconElement.style.background = "rgba(0, 0, 0, 0.6)"; 68 this.iconElement.style.color = "rgba(255, 255, 255, 0.6)"; 69 this.iconElement.style.borderColor = "rgba(255, 255, 255, 0.3)"; 70 }; 71 72 const setInactiveState = () => { 73 this.iconElement.style.background = "rgba(0, 0, 0, 0.4)"; 74 this.iconElement.style.color = "rgba(255, 255, 255, 0.3)"; 75 this.iconElement.style.borderColor = "rgba(255, 255, 255, 0.15)"; 76 }; 77 78 // Mouse events for desktop 79 this.iconElement.addEventListener("mouseenter", setActiveState); 80 this.iconElement.addEventListener("mouseleave", setInactiveState); 81 82 // Touch/click handling with proper event management 83 let touchUsed = false; 84 85 // Touch events for mobile (with debugging and click prevention) 86 this.iconElement.addEventListener("touchstart", (e) => { 87 console.log('DEBUG: touchstart fired on icon'); 88 touchUsed = true; 89 e.preventDefault(); 90 e.stopPropagation(); 91 setActiveState(); 92 }, { passive: false }); 93 94 this.iconElement.addEventListener("touchend", (e) => { 95 console.log('DEBUG: touchend fired on icon'); 96 e.preventDefault(); 97 e.stopPropagation(); 98 setInactiveState(); 99 this.toggle(); 100 }, { passive: false }); 101 102 // Click event for desktop (prevent on touch devices) 103 this.iconElement.addEventListener("click", (e) => { 104 console.log('DEBUG: click fired on icon, touchUsed:', touchUsed); 105 // Prevent click if touch was used (avoids double-firing) 106 if (touchUsed) { 107 e.preventDefault(); 108 e.stopPropagation(); 109 return; 110 } 111 e.stopPropagation(); 112 this.toggle(); 113 }); 114 115 // Desktop: append to body (fixed), Mobile: append to header (absolute within content) 116 if (isMobile) { 117 const headerInfo = document.querySelector('header .info'); 118 if (headerInfo) { 119 headerInfo.style.position = 'relative'; 120 headerInfo.appendChild(this.iconElement); 121 console.log('Debug icon appended to header .info'); 122 } else { 123 document.body.appendChild(this.iconElement); 124 console.log('Debug icon appended to body (fallback)'); 125 } 126 } else { 127 // Desktop: append directly to body for fixed positioning 128 document.body.appendChild(this.iconElement); 129 console.log('Debug icon appended to body (desktop)'); 130 } 131 } 132 133 toggle() { 134 this.enabled = !this.enabled; 135 if (this.enabled && !this.element) { 136 this.create(); 137 } 138 if (this.element) { 139 this.element.style.display = this.enabled ? "block" : "none"; 140 } 141 // Update icon appearance when panel is open (rotate cog wheel) 142 if (this.iconElement) { 143 if (this.enabled) { 144 this.iconElement.style.background = "rgba(0, 0, 0, 0.7)"; 145 this.iconElement.style.color = "rgba(255, 255, 255, 0.8)"; 146 this.iconElement.style.borderColor = "rgba(255, 255, 255, 0.4)"; 147 this.iconElement.style.transform = "rotate(180deg)"; 148 } else { 149 this.iconElement.style.background = "rgba(0, 0, 0, 0.4)"; 150 this.iconElement.style.color = "rgba(255, 255, 255, 0.3)"; 151 this.iconElement.style.borderColor = "rgba(255, 255, 255, 0.15)"; 152 this.iconElement.style.transform = "rotate(0deg)"; 153 } 154 } 155 } 156 157 create() { 158 this.element = document.createElement("div"); 159 this.element.id = "debug-panel"; 160 this.element.style.cssText = ` 161 position: fixed; 162 top: 20px; 163 right: 20px; 164 background: rgba(0, 0, 0, 0.92); 165 color: #fff; 166 padding: 20px; 167 font-family: 'JetBrains Mono', monospace; 168 font-size: 12px; 169 line-height: 1.6; 170 border: 1px solid rgba(255, 255, 255, 0.3); 171 border-radius: 4px; 172 max-width: 400px; 173 max-height: 90vh; 174 overflow-y: auto; 175 z-index: 9999; 176 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); 177 `; 178 179 // Create static header with close button 180 const header = document.createElement("div"); 181 header.style.cssText = "margin-bottom: 16px; padding-bottom: 8px; position: relative;"; 182 header.innerHTML = ` 183 <strong style="font-size: 14px;">DEBUG PANEL</strong> 184 <button id="close-debug-btn" style=" 185 position: absolute; 186 top: -5px; 187 right: -5px; 188 background: transparent; 189 border: none; 190 color: rgba(255, 255, 255, 0.5); 191 font-size: 28px; 192 cursor: pointer; 193 padding: 5px; 194 width: 36px; 195 height: 36px; 196 line-height: 24px; 197 font-family: monospace; 198 display: flex; 199 align-items: center; 200 justify-content: center; 201 ">×</button> 202 <div style="font-size: 10px; color: #888; margin-top: 4px;">Press ? or tap ⚙ to toggle</div> 203 `; 204 205 // Create static controls section 206 const controls = document.createElement("div"); 207 controls.style.cssText = "margin-bottom: 16px; padding: 12px; background: rgba(255, 255, 255, 0.05); border-radius: 4px;"; 208 controls.innerHTML = ` 209 <div style="color: #4CAF50; font-weight: bold; margin-bottom: 8px;">CONTROLS</div> 210 <div style="margin-bottom: 12px;"> 211 <button id="spawn-virus-btn" style=" 212 background: rgba(255, 50, 50, 0.3); 213 border: 1px solid #f44336; 214 color: #fff; 215 padding: 6px 12px; 216 margin: 4px; 217 cursor: pointer; 218 font-family: 'JetBrains Mono', monospace; 219 font-size: 11px; 220 border-radius: 3px; 221 transition: transform 0.1s ease; 222 ">+ Virus</button> 223 <button id="spawn-anomaly-btn" style=" 224 background: rgba(156, 39, 176, 0.3); 225 border: 1px solid #9C27B0; 226 color: #fff; 227 padding: 6px 12px; 228 margin: 4px; 229 cursor: pointer; 230 font-family: 'JetBrains Mono', monospace; 231 font-size: 11px; 232 border-radius: 3px; 233 transition: transform 0.1s ease; 234 ">+ Anomaly</button> 235 <button id="spawn-station-btn" style=" 236 background: rgba(255, 193, 7, 0.3); 237 border: 1px solid #FFC107; 238 color: #fff; 239 padding: 6px 12px; 240 margin: 4px; 241 cursor: pointer; 242 font-family: 'JetBrains Mono', monospace; 243 font-size: 11px; 244 border-radius: 3px; 245 transition: transform 0.1s ease; 246 ">+ Station</button> 247 </div> 248 <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.1);"> 249 <div style="font-size: 10px; color: #888; margin-bottom: 6px;"> 250 Particle Visibility: <span id="particle-visibility-value">Auto</span> 251 </div> 252 <input type="range" id="particle-visibility-slider" min="0" max="100" value="0" style=" 253 width: 100%; 254 height: 6px; 255 cursor: pointer; 256 accent-color: #4CAF50; 257 "> 258 <div style="font-size: 9px; color: #666; margin-top: 4px; display: flex; justify-content: space-between;"> 259 <span>Auto</span> 260 <span>25%</span> 261 <span>50%</span> 262 <span>75%</span> 263 <span>100%</span> 264 </div> 265 </div> 266 `; 267 268 // Create dynamic content container 269 this.contentElement = document.createElement("div"); 270 this.contentElement.id = "debug-content"; 271 272 // Append everything 273 this.element.appendChild(header); 274 this.element.appendChild(controls); 275 this.element.appendChild(this.contentElement); 276 document.body.appendChild(this.element); 277 278 // Set up button handlers once (they won't be recreated) 279 this.setupActionButtons(); 280 } 281 282 setupActionButtons() { 283 // Close button 284 const closeBtn = this.element.querySelector('#close-debug-btn'); 285 if (closeBtn) { 286 // Add touch support 287 closeBtn.style.touchAction = 'manipulation'; 288 289 // Track which event type fired to prevent double-firing 290 let closeTouchUsed = false; 291 let closeResetTimeout; 292 293 closeBtn.addEventListener('touchend', (e) => { 294 e.preventDefault(); 295 e.stopPropagation(); 296 closeTouchUsed = true; 297 clearTimeout(closeResetTimeout); 298 closeResetTimeout = setTimeout(() => { closeTouchUsed = false; }, 300); 299 this.toggle(); 300 }, { passive: false }); 301 302 closeBtn.addEventListener('click', (e) => { 303 e.preventDefault(); 304 e.stopPropagation(); 305 // Prevent click if touch was used (avoids double-firing) 306 if (closeTouchUsed) { 307 console.log('DEBUG: Close button click blocked (touch already fired)'); 308 return; 309 } 310 this.toggle(); 311 }); 312 313 // Hover effect for close button 314 closeBtn.addEventListener("mouseenter", () => { 315 closeBtn.style.color = "rgba(255, 255, 255, 1.0)"; 316 }); 317 closeBtn.addEventListener("mouseleave", () => { 318 closeBtn.style.color = "rgba(255, 255, 255, 0.5)"; 319 }); 320 } else { 321 console.error('Close button not found'); 322 } 323 324 // Helper function for button setup 325 const setupButton = (buttonId, spawnFunction, buttonName) => { 326 const button = this.element.querySelector(buttonId); 327 if (button) { 328 button.style.touchAction = 'manipulation'; 329 330 const handleSpawn = (e) => { 331 e.preventDefault(); 332 e.stopPropagation(); 333 334 if (typeof spawnFunction === 'function') { 335 spawnFunction(); 336 // Visual feedback 337 button.style.transform = 'scale(0.95)'; 338 setTimeout(() => { 339 button.style.transform = 'scale(1)'; 340 }, 100); 341 } else { 342 console.error(`${buttonName} function not found`); 343 } 344 }; 345 346 button.addEventListener('click', handleSpawn); 347 button.addEventListener('touchend', handleSpawn, { passive: false }); 348 } else { 349 console.error(`${buttonName} button not found`); 350 } 351 }; 352 353 // Setup all spawn buttons 354 setupButton('#spawn-virus-btn', window.debugSpawnVirus, 'debugSpawnVirus'); 355 setupButton('#spawn-anomaly-btn', window.debugSpawnAnomaly, 'debugSpawnAnomaly'); 356 setupButton('#spawn-station-btn', window.debugSpawnStation, 'debugSpawnStation'); 357 358 // Setup particle visibility slider 359 const visibilitySlider = this.element.querySelector('#particle-visibility-slider'); 360 const visibilityValue = this.element.querySelector('#particle-visibility-value'); 361 if (visibilitySlider && visibilityValue && window.debugSetParticleVisibility) { 362 visibilitySlider.addEventListener('input', (e) => { 363 const value = parseInt(e.target.value); 364 if (value === 0) { 365 visibilityValue.textContent = 'Auto'; 366 } else { 367 visibilityValue.textContent = value + '%'; 368 } 369 window.debugSetParticleVisibility(value); 370 }); 371 } 372 } 373 374 update(debugData) { 375 if (!this.enabled || !this.contentElement) return; 376 377 const { 378 time, 379 particles, 380 cursor, 381 viruses, 382 anomalies, 383 stations, 384 performance, 385 gameState, 386 } = debugData; 387 388 // Only update the dynamic content, not the header/buttons 389 let html = ''; 390 391 // Time Section 392 html += this.section("TIME", [ 393 `Elapsed: ${time.elapsed}`, 394 `FPS: ${performance.fps}`, 395 ]); 396 397 // Particles Section 398 html += this.section("PARTICLES", [ 399 `Active: ${particles.active} / ${particles.total} (${particles.percentage}%)`, 400 `Spacing: ${particles.spacing}px`, 401 `Growth: ${particles.growthPhase}`, 402 ]); 403 404 // Cursor Section 405 const cursorInfo = [ 406 `Position: (${cursor.x}, ${cursor.y})`, 407 `Velocity: (${cursor.vx}, ${cursor.vy})`, 408 `Speed: ${cursor.speed}`, 409 `Radius: ${cursor.radius}px (${cursor.radiusMultiplier}x)`, 410 `Damage Mult: ${cursor.damageMultiplier}`, 411 `Mode: ${cursor.mode}`, 412 ]; 413 if (cursor.buffs.length > 0) { 414 cursorInfo.push(`<span style="color: #4CAF50;">Buffs: ${cursor.buffs.join(", ")}</span>`); 415 } 416 html += this.section("CURSOR", cursorInfo); 417 418 // Viruses Section 419 if (viruses.count > 0) { 420 const virusInfo = [ 421 `Count: ${viruses.count}`, 422 `Next spawn: ${viruses.nextSpawn}`, 423 ]; 424 425 if (viruses.largest) { 426 const v = viruses.largest; 427 virusInfo.push(`<div style="margin-top: 8px; padding: 8px; background: rgba(255, 0, 0, 0.1); border-left: 2px solid #f44336;"> 428 <strong style="color: #f44336;">Largest Virus</strong><br> 429 Health: ${v.health}/${v.maxHealth} (${v.healthPercent}%)<br> 430 Radius: ${v.radius}px<br> 431 Frame: ${v.frame} (${v.phase})<br> 432 Pull: ${v.pullRadius}px (${v.pullForce}x)<br> 433 Regen: ${v.regenActive ? '<span style="color: #ff9800;">ACTIVE</span>' : 'none'} 434 </div>`); 435 } 436 html += this.section("VIRUSES", virusInfo); 437 } 438 439 // Anomalies Section 440 if (anomalies.count > 0) { 441 const anomalyInfo = [ 442 `Count: ${anomalies.count}`, 443 `Vortex Radius: ${anomalies.vortexRadius}px`, 444 `Vortex Strength: ${anomalies.vortexStrength}`, 445 ]; 446 html += this.section("ANOMALIES", anomalyInfo); 447 } 448 449 // Disruption Section 450 if (gameState.disruption) { 451 const d = gameState.disruption; 452 const disruptionInfo = [ 453 `Available: ${d.available ? '<span style="color: #4CAF50;">YES</span>' : '<span style="color: #f44336;">NO</span>'}`, 454 `Cooldown: ${d.cooldownRemaining}s`, 455 `Coverage: ${d.coverage}%`, 456 `Power: ${d.power}x`, 457 ]; 458 html += this.section("DISRUPTION", disruptionInfo); 459 } 460 461 // Performance Section 462 const perfInfo = [ 463 `Canvas: ${performance.canvasWidth}x${performance.canvasHeight}`, 464 `Screen multiplier: ${performance.screenMultiplier}`, 465 `Device: ${performance.isMobile ? 'Mobile' : 'Desktop'}`, 466 ]; 467 html += this.section("PERFORMANCE", perfInfo); 468 469 // Only update the dynamic content area, leaving buttons intact 470 this.contentElement.innerHTML = html; 471 } 472 473 section(title, items) { 474 let html = `<div style="margin-bottom: 16px;"> 475 <div style="color: #4CAF50; font-weight: bold; margin-bottom: 6px;">${title}</div> 476 `; 477 478 items.forEach(item => { 479 html += `<div style="padding-left: 8px; margin-bottom: 2px;">${item}</div>`; 480 }); 481 482 html += `</div>`; 483 return html; 484 } 485 486 destroy() { 487 if (this.element) { 488 this.element.remove(); 489 this.element = null; 490 } 491 } 492 } 493 494 export default DebugPanel;