/** * User Interface & Interactive Controls Module * Binds expanded sliders, preset actions, live HUD badges, population history chart, * entity inspector, and the post-extinction analysis modal. */ export class UIManager { constructor(sim, configManager) { this.sim = sim; this.configManager = configManager; this.cacheDOMElements(); this.bindEvents(); this.syncSlidersFromConfig(); } cacheDOMElements() { // Primary Controls this.btnPlayPause = document.getElementById('btnPlayPause'); this.btnRestart = document.getElementById('btnRestart'); this.btnFullscreen = document.getElementById('btnFullscreen'); this.btnExitFullscreen = document.getElementById('btnExitFullscreen'); this.btnSetDefaults = document.getElementById('btnSetDefaults'); this.btnDefaults = document.getElementById('btnDefaults'); this.btnCopyJson = document.getElementById('btnCopyJson'); this.btnRain = document.getElementById('btnRain'); this.toastMsg = document.getElementById('toastMsg'); // Continuous Screen Saver Mode this.checkContinuous = document.getElementById('checkContinuous'); // Speed buttons this.speedBtns = document.querySelectorAll('.speed-btn'); // Tool buttons this.toolBtns = document.querySelectorAll('.tool-btn'); // Sliders & value displays this.sliders = { initialSheep: { input: document.getElementById('sliderSheep'), label: document.getElementById('valSheep'), format: (v) => v }, initialWolves: { input: document.getElementById('sliderWolves'), label: document.getElementById('valWolves'), format: (v) => v }, initialPlants: { input: document.getElementById('sliderPlants'), label: document.getElementById('valPlants'), format: (v) => v }, maxPlantCap: { input: document.getElementById('sliderMaxPlantCap'), label: document.getElementById('valMaxPlantCap'), format: (v) => v }, sheepHungerDepletionRate: { input: document.getElementById('sliderSheepHunger'), label: document.getElementById('valSheepHunger'), format: (v) => `${Number(v).toFixed(1)}/s` }, sheepGrazingReach: { input: document.getElementById('sliderSheepReach'), label: document.getElementById('valSheepReach'), format: (v) => `${v}px` }, sheepMatingCooldown: { input: document.getElementById('sliderSheepMating'), label: document.getElementById('valSheepMating'), format: (v) => `${v}s` }, wolfLungeCost: { input: document.getElementById('sliderWolfLunge'), label: document.getElementById('valWolfLunge'), format: (v) => `${v} HP` }, wolfHungerDepletionRate: { input: document.getElementById('sliderWolfHunger'), label: document.getElementById('valWolfHunger'), format: (v) => `${Number(v).toFixed(1)}/s` }, wolfAttackDamage: { input: document.getElementById('sliderWolfDamage'), label: document.getElementById('valWolfDamage'), format: (v) => v }, wolfMatingCooldown: { input: document.getElementById('sliderWolfMating'), label: document.getElementById('valWolfMating'), format: (v) => `${v}s` }, plantSeedRate: { input: document.getElementById('sliderSeedRate'), label: document.getElementById('valSeedRate'), format: (v) => `${v}%` }, plantDryRate: { input: document.getElementById('sliderDryRate'), label: document.getElementById('valDryRate'), format: (v) => `${Number(v).toFixed(1)}/s` }, rainFrequency: { input: document.getElementById('sliderRain'), label: document.getElementById('valRain'), format: (v) => `${v}%` }, decaySpeed: { input: document.getElementById('sliderDecay'), label: document.getElementById('valDecay'), format: (v) => `${Number(v).toFixed(1)}x` }, // Continuous Mode Influx intervals & wave size continuousSheepInterval: { input: document.getElementById('sliderContinuousSheep'), label: document.getElementById('valContinuousSheep'), format: (v) => `${v}s` }, continuousWolfInterval: { input: document.getElementById('sliderContinuousWolf'), label: document.getElementById('valContinuousWolf'), format: (v) => `${v}s` }, continuousMaxGroupSize: { input: document.getElementById('sliderContinuousGroup'), label: document.getElementById('valContinuousGroup'), format: (v) => `${v}` }, }; // Live HUD badges this.hudSheep = document.getElementById('hudSheep'); this.hudWolves = document.getElementById('hudWolves'); this.hudPlants = document.getElementById('hudPlants'); this.hudTime = document.getElementById('hudTime'); this.hudWeather = document.getElementById('hudWeather'); // Mini chart canvas this.miniChartCanvas = document.getElementById('miniChart'); if (this.miniChartCanvas) { this.miniChartCtx = this.miniChartCanvas.getContext('2d'); } // Extinction modal elements this.extinctionModal = document.getElementById('extinctionModal'); this.extinctionChartCanvas = document.getElementById('extinctionChart'); if (this.extinctionChartCanvas) { this.extinctionChartCtx = this.extinctionChartCanvas.getContext('2d'); } this.extTimeSurv = document.getElementById('extTimeSurv'); this.extPeakSheep = document.getElementById('extPeakSheep'); this.extPeakWolves = document.getElementById('extPeakWolves'); this.extPeakPlants = document.getElementById('extPeakPlants'); this.extTotalBorn = document.getElementById('extTotalBorn'); this.extWolfKills = document.getElementById('extWolfKills'); this.extDecayBlooms = document.getElementById('extDecayBlooms'); this.extSeeds = document.getElementById('extSeeds'); this.btnModalRestart = document.getElementById('btnModalRestart'); this.btnModalReview = document.getElementById('btnModalReview'); // Entity Inspector this.inspectorPanel = document.getElementById('inspectorPanel'); this.inspectorDetails = document.getElementById('inspectorDetails'); this.btnCloseInspector = document.getElementById('btnCloseInspector'); } showToast(text) { if (!this.toastMsg) return; this.toastMsg.textContent = text; this.toastMsg.classList.remove('hidden'); clearTimeout(this._toastTimeout); this._toastTimeout = setTimeout(() => { this.toastMsg.classList.add('hidden'); }, 2400); } syncSlidersFromConfig() { const cfg = this.configManager.current; if (this.checkContinuous && cfg.continuousMode !== undefined) { this.checkContinuous.checked = !!cfg.continuousMode; } for (const [key, item] of Object.entries(this.sliders)) { if (item.input && cfg[key] !== undefined) { item.input.value = cfg[key]; if (item.label) { item.label.textContent = item.format(cfg[key]); } } } } readSliders() { const config = {}; if (this.checkContinuous) { config.continuousMode = this.checkContinuous.checked; } for (const [key, item] of Object.entries(this.sliders)) { if (item.input) { config[key] = parseFloat(item.input.value); } } return config; } bindEvents() { // 1. Play / Pause this.btnPlayPause.addEventListener('click', () => { this.sim.isRunning = !this.sim.isRunning; this.updatePlayPauseButton(); }); // 2. Restart Simulation this.btnRestart.addEventListener('click', () => { this.extinctionModal.classList.add('hidden'); this.sim.restart(); this.sim.isRunning = true; this.updatePlayPauseButton(); }); // 3. Save current settings as custom defaults if (this.btnSetDefaults) { this.btnSetDefaults.addEventListener('click', () => { const currentVals = this.readSliders(); this.configManager.saveAsNewDefaults(currentVals); this.sim.config = { ...this.configManager.current }; this.showToast('✓ Saved As Custom Defaults!'); }); } // 4. Restore Defaults this.btnDefaults.addEventListener('click', () => { this.configManager.resetToDefaults(); this.syncSlidersFromConfig(); this.sim.config = { ...this.configManager.current }; this.showToast('↺ Restored Defaults'); }); // 5. Copy JSON Config if (this.btnCopyJson) { this.btnCopyJson.addEventListener('click', () => { const jsonStr = JSON.stringify(this.sim.config, null, 2); navigator.clipboard.writeText(jsonStr).then(() => { this.showToast('📋 JSON Copied to Clipboard!'); }).catch(() => { this.showToast('Failed to copy to clipboard'); }); }); } // 6. Summon Rain button if (this.btnRain) { this.btnRain.addEventListener('click', () => { this.sim.weather.forceRain(); }); } // 7. Speed Buttons (1x, 2x, 4x) this.speedBtns.forEach((btn) => { btn.addEventListener('click', () => { this.speedBtns.forEach((b) => b.classList.remove('active')); btn.classList.add('active'); const speed = parseFloat(btn.dataset.speed || '1'); this.sim.speedMultiplier = speed; }); }); // 8. Spawn / Inspect Tools this.toolBtns.forEach((btn) => { btn.addEventListener('click', () => { this.toolBtns.forEach((b) => b.classList.remove('active')); btn.classList.add('active'); this.sim.activeTool = btn.dataset.tool || 'inspect'; }); }); // 9. Real-time Slider input listeners for (const [key, item] of Object.entries(this.sliders)) { if (item.input) { item.input.addEventListener('input', () => { const val = parseFloat(item.input.value); if (item.label) { item.label.textContent = item.format(val); } // Live update simulation config & persist const updated = this.readSliders(); this.configManager.save(updated); this.sim.config = { ...this.sim.config, ...updated }; }); } } // 10. Canvas click interactions (Spawn or Inspect) this.sim.canvas.addEventListener('click', (e) => { const rect = this.sim.canvas.getBoundingClientRect(); const scaleX = this.sim.canvas.width / rect.width; const scaleY = this.sim.canvas.height / rect.height; const clickX = (e.clientX - rect.left) * scaleX; const clickY = (e.clientY - rect.top) * scaleY; if (this.sim.activeTool === 'inspect') { this.handleInspectClick(clickX, clickY); } else { this.sim.spawnEntity(this.sim.activeTool, clickX, clickY); } }); // 11. Extinction modal actions if (this.btnModalRestart) { this.btnModalRestart.addEventListener('click', () => { this.extinctionModal.classList.add('hidden'); this.sim.restart(); this.sim.isRunning = true; this.updatePlayPauseButton(); }); } if (this.btnModalReview) { this.btnModalReview.addEventListener('click', () => { this.extinctionModal.classList.add('hidden'); }); } if (this.btnCloseInspector) { this.btnCloseInspector.addEventListener('click', () => { this.sim.selectedEntity = null; this.inspectorPanel.classList.add('hidden'); }); } // 12. Fullscreen Screen Saver Mode if (this.btnFullscreen) { this.btnFullscreen.addEventListener('click', () => { this.enterFullscreen(); }); } if (this.btnExitFullscreen) { this.btnExitFullscreen.addEventListener('click', () => { this.exitFullscreen(); }); } // Keyboard ESC & browser fullscreenchange listener document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement && this.isFullscreen) { this.exitFullscreen(false); } }); window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isFullscreen) { this.exitFullscreen(); } }); // 13. Continuous Mode Toggle if (this.checkContinuous) { this.checkContinuous.addEventListener('change', () => { const isChecked = this.checkContinuous.checked; this.sim.config.continuousMode = isChecked; this.configManager.save({ continuousMode: isChecked }); if (isChecked) { this.extinctionModal.classList.add('hidden'); this.showToast('📺 Continuous Screen Saver Mode Active'); } else { this.showToast('Continuous Mode Disabled'); } }); } } enterFullscreen() { this.isFullscreen = true; const appLayout = document.querySelector('.app-layout'); if (appLayout) { appLayout.classList.add('fullscreen-mode'); } if (this.btnExitFullscreen) { this.btnExitFullscreen.classList.remove('hidden'); } if (document.documentElement.requestFullscreen && !document.fullscreenElement) { document.documentElement.requestFullscreen().catch(() => {}); } setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 60); } exitFullscreen(requestExit = true) { this.isFullscreen = false; const appLayout = document.querySelector('.app-layout'); if (appLayout) { appLayout.classList.remove('fullscreen-mode'); } if (this.btnExitFullscreen) { this.btnExitFullscreen.classList.add('hidden'); } if (requestExit && document.fullscreenElement && document.exitFullscreen) { document.exitFullscreen().catch(() => {}); } setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 60); } handleInspectClick(x, y) { let bestEntity = null; let minD = 28; const checkList = (list) => { for (let i = 0; i < list.length; i++) { const item = list[i]; if (item.isDead) continue; const d = Math.hypot(item.x - x, item.y - y); if (d < minD) { minD = d; bestEntity = item; } } }; checkList(this.sim.wolves); checkList(this.sim.sheep); checkList(this.sim.plants); this.sim.selectedEntity = bestEntity; this.updateInspectorPanel(); } updateInspectorPanel() { const e = this.sim.selectedEntity; if (!e || e.isDead) { this.inspectorPanel.classList.add('hidden'); return; } this.inspectorPanel.classList.remove('hidden'); let html = ''; if (e.species === 'sheep' || e.species === 'wolf') { const icon = e.species === 'sheep' ? '🐑' : '🐺'; const name = e.isBaby ? `Baby ${e.species}` : `Adult ${e.species}`; html = `