/** * 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 = `
${icon} ${name}
Health: ${Math.round(e.health)}/100
Hunger: ${Math.round(e.hunger)}/100
State: ${e.actionState}
Age: ${Math.round(e.age)}s
${e.isBaby ? `
Food for Adult: ${e.foodEatenAsBaby}/3 meals
` : ''} `; } else { const isFlower = e.type === 'flower'; html = `
${isFlower ? '🌸 Dense Flower Tuft' : '🌿 Grass Tuft'}
Moisture: ${Math.round(e.moisture)}%
Growth: ${Math.round(e.growth * 100)}%
Age: ${Math.round(e.age)}s
`; } this.inspectorDetails.innerHTML = html; } updatePlayPauseButton() { if (this.sim.isRunning) { this.btnPlayPause.innerHTML = ` Pause `; this.btnPlayPause.classList.add('btn-active'); } else { this.btnPlayPause.innerHTML = ` Play `; this.btnPlayPause.classList.remove('btn-active'); } } updateHUD(stats, sheepCount, wolfCount, plantCount) { if (this.hudSheep) this.hudSheep.textContent = sheepCount; if (this.hudWolves) this.hudWolves.textContent = wolfCount; if (this.hudPlants) this.hudPlants.textContent = plantCount; if (this.hudTime) { const totalSec = Math.floor(stats.elapsedSimTime); const mins = Math.floor(totalSec / 60); const secs = totalSec % 60; this.hudTime.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } if (this.hudWeather) { const wState = this.sim.weather.state; let label = '☀️ Sunny'; if (wState === 'raining') label = '🌧️ Raining'; else if (wState === 'cloudy') label = '☁️ Overcast'; else if (wState === 'clearing') label = '⛅ Clearing'; this.hudWeather.textContent = label; } // Refresh inspector if entity is active if (this.sim.selectedEntity) { this.updateInspectorPanel(); } // Draw Mini Chart if (this.miniChartCtx && stats.history.length > 1) { this.drawChart(this.miniChartCtx, this.miniChartCanvas.width, this.miniChartCanvas.height, stats.history, false); } } showExtinctionModal(stats) { this.extinctionModal.classList.remove('hidden'); const totalSec = Math.floor(stats.extinctionTime || stats.elapsedSimTime); const mins = Math.floor(totalSec / 60); const secs = totalSec % 60; this.extTimeSurv.textContent = `${mins}m ${secs}s`; this.extPeakSheep.textContent = stats.peakSheep; this.extPeakWolves.textContent = stats.peakWolves; this.extPeakPlants.textContent = stats.peakPlants; this.extTotalBorn.textContent = `${stats.totalSheepBorn} sheep / ${stats.totalWolvesBorn} wolves`; this.extWolfKills.textContent = `${stats.sheepKilledByWolves} sheep hunted (${stats.sheepStarved} starved)`; this.extDecayBlooms.textContent = `${stats.totalPlantsFromDecay} flower tufts`; this.extSeeds.textContent = `${stats.totalSeedsSprouted} seed puffs sprouted`; // Render large extinction history chart if (this.extinctionChartCtx) { this.drawChart( this.extinctionChartCtx, this.extinctionChartCanvas.width, this.extinctionChartCanvas.height, stats.history, true ); } } drawChart(ctx, width, height, history, showAxes = true) { ctx.clearRect(0, 0, width, height); if (!history || history.length < 2) return; const padLeft = showAxes ? 45 : 4; const padRight = showAxes ? 20 : 4; const padTop = showAxes ? 25 : 4; const padBottom = showAxes ? 25 : 4; const chartW = width - padLeft - padRight; const chartH = height - padTop - padBottom; // Determine max value for Y-scaling let maxY = 10; for (let i = 0; i < history.length; i++) { const h = history[i]; if (h.sheep > maxY) maxY = h.sheep; if (h.wolves > maxY) maxY = h.wolves; if (h.plants > maxY) maxY = h.plants; } maxY = Math.ceil(maxY * 1.15); // Background grid if (showAxes) { ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; ctx.lineWidth = 1; // Horizontal grid lines for (let g = 0; g <= 4; g++) { const yVal = Math.round((maxY / 4) * g); const yPos = padTop + chartH - (g / 4) * chartH; ctx.beginPath(); ctx.moveTo(padLeft, yPos); ctx.lineTo(padLeft + chartW, yPos); ctx.stroke(); ctx.fillStyle = 'rgba(255, 255, 255, 0.45)'; ctx.font = '10px monospace'; ctx.textAlign = 'right'; ctx.fillText(yVal.toString(), padLeft - 6, yPos + 3); } } const mapX = (idx) => padLeft + (idx / (history.length - 1)) * chartW; const mapY = (val) => padTop + chartH - (val / maxY) * chartH; // Draw Line Series helper const drawSeries = (key, color, lineWidth = 2) => { ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.beginPath(); for (let i = 0; i < history.length; i++) { const x = mapX(i); const y = mapY(history[i][key]); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); }; // Draw series: Plants (green), Sheep (sky blue/white), Wolves (coral red) drawSeries('plants', '#38D39F', showAxes ? 2.5 : 1.5); drawSeries('sheep', '#4CC9F0', showAxes ? 2.5 : 1.5); drawSeries('wolves', '#FF4D6D', showAxes ? 2.5 : 1.5); // Chart Legend on full chart if (showAxes) { ctx.font = '11px sans-serif'; ctx.textAlign = 'left'; // Sheep ctx.fillStyle = '#4CC9F0'; ctx.fillRect(padLeft + 10, 10, 10, 10); ctx.fillStyle = '#E0E0E0'; ctx.fillText('Sheep', padLeft + 25, 19); // Wolves ctx.fillStyle = '#FF4D6D'; ctx.fillRect(padLeft + 85, 10, 10, 10); ctx.fillStyle = '#E0E0E0'; ctx.fillText('Wolves', padLeft + 100, 19); // Plants ctx.fillStyle = '#38D39F'; ctx.fillRect(padLeft + 165, 10, 10, 10); ctx.fillStyle = '#E0E0E0'; ctx.fillText('Plants / Flowers', padLeft + 180, 19); } } }