/ js / debug-panel.js
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;