import { renderExtensionTemplateAsync } from '../../../extensions.js'; import { RTSMapCanvas } from './MapCanvas.js'; import GameStateManager from '../src/GameStateManager.js'; import PresetManager from '../src/PresetManager.js'; import EventManager from '../src/EventManager.js'; import { sendTurn } from '../src/LLMAdapter.js'; import { updateResourcePanel } from './ResourcePanel.js'; export class RTSUIController { constructor() { this.isFullscreen = false; this.originalSheldContent = null; this.rtsContainer = null; this.mapCanvas = null; this.gameLog = []; this.mapData = null; this.bindMethods(); } bindMethods() { this.handleQuickAction = this.handleQuickAction.bind(this); this.handleExecuteCommand = this.handleExecuteCommand.bind(this); this.handleClearCommand = this.handleClearCommand.bind(this); this.handleMapControls = this.handleMapControls.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleSelectionChange = this.handleSelectionChange.bind(this); this.handleNarrativeUpdate = this.handleNarrativeUpdate.bind(this); this.handleZoneChange = this.handleZoneChange.bind(this); } async enterFullscreen() { if (this.isFullscreen) return; const sheld = document.getElementById('sheld'); if (sheld) { this.originalSheldDisplay = sheld.style.display; sheld.style.display = 'none'; } await this.createRTSInterface(); this.isFullscreen = true; document.addEventListener('keydown', this.handleKeydown.bind(this)); } async exitFullscreen() { if (!this.isFullscreen) return; if (this.rtsContainer) { this.rtsContainer.remove(); this.rtsContainer = null; } const sheld = document.getElementById('sheld'); if (sheld) { sheld.style.display = this.originalSheldDisplay || ''; } this.isFullscreen = false; this.mapCanvas = null; document.removeEventListener('keydown', this.handleKeydown.bind(this)); } async createRTSInterface() { try { const rtsHTML = await renderExtensionTemplateAsync('rts-mode', 'rts-ui'); this.rtsContainer = document.createElement('div'); this.rtsContainer.innerHTML = rtsHTML; document.body.appendChild(this.rtsContainer); this.mapCanvas = new RTSMapCanvas('rts-game-map'); this.setupEventListeners(); this.mapCanvas.canvas.addEventListener('selectionChanged', this.handleSelectionChange); const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-preset-select')); const selectedPreset = presetSelect ? presetSelect.value : '/scripts/extensions/rts-mode/presets/zoo_escape.json'; await PresetManager.loadPreset(selectedPreset); GameStateManager.reset(); const preset = PresetManager.getPreset(); EventManager.initialize(); // Create quick action buttons after preset is loaded this.createQuickActionButtons(); const response = await fetch(preset.map); this.mapData = await response.json(); this.mapCanvas.loadMap(this.mapData); /* Ensure GameStateManager keeps a reference to the full mapData so that subsequent AI-driven updates (addOrUpdateMapEntity, etc.) have a valid entity list to work with. */ GameStateManager.setState({ mapState: { ...(GameStateManager.getState().mapState || {}), mapData: this.mapData } }); this.updateUI(); this.addLogEntry('system', PresetManager.getPrompt('system_initial')); } catch (error) { console.error('Error creating RTS interface:', error); this.addLogEntry('error', `Failed to create RTS interface: ${error.message}`); throw error; } } setupEventListeners() { // Command Execution document.getElementById('rts-execute-btn')?.addEventListener('click', this.handleExecuteCommand); document.getElementById('rts-clear-btn')?.addEventListener('click', this.handleClearCommand); // Map Controls document.getElementById('rts-zoom-in')?.addEventListener('click', () => this.handleMapControls('zoomIn')); document.getElementById('rts-zoom-out')?.addEventListener('click', () => this.handleMapControls('zoomOut')); document.getElementById('rts-center-map')?.addEventListener('click', () => this.handleMapControls('center')); // Command Input Enter Key const commandInput = document.getElementById('rts-command-input'); if (commandInput) { commandInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this.handleExecuteCommand(); } }); } // Narrative and Zone Listeners document.addEventListener('rts-narrative-update', this.handleNarrativeUpdate); document.addEventListener('rts-zone-changed', this.handleZoneChange); document.addEventListener('rts-map-update', this.handleMapUpdate.bind(this)); document.addEventListener('rts-canvas-refresh', this.handleCanvasRefresh.bind(this)); } handleQuickAction(action) { const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input')); if (!commandInput) { console.error('RTS: Command input not found'); return; } try { const quickActions = PresetManager.getFeature('quickActions'); if (!quickActions || !Array.isArray(quickActions)) { console.error('RTS: Quick actions not found in preset'); return; } const actionTemplate = quickActions.find(a => a.id === action); if (actionTemplate && actionTemplate.command) { commandInput.value = actionTemplate.command; } else { console.error(`RTS: Action template not found for action: ${action}`); // Fallback commands const fallbackCommands = { 'observe': 'Look around carefully to assess the situation.', 'move': 'Move to a specific location.', 'hide': 'Find a hiding spot.', 'interact': 'Interact with something in the environment.' }; commandInput.value = fallbackCommands[action] || `Perform ${action} action`; } commandInput.focus(); commandInput.select(); } catch (error) { console.error('RTS: Error in handleQuickAction:', error); } } createQuickActionButtons() { console.log('RTS: Creating quick action buttons...'); const container = document.getElementById('rts-quick-actions'); if (!container) { console.error('RTS: Quick actions container not found'); return; } container.innerHTML = ''; // Clear existing buttons try { const quickActions = PresetManager.getFeature('quickActions'); console.log('RTS: Quick actions from preset:', quickActions); if (!quickActions || !Array.isArray(quickActions)) { console.warn('RTS: No quick actions found in preset, creating fallback actions'); // Create fallback actions const fallbackActions = [ { id: 'observe', label: 'Observe' }, { id: 'move', label: 'Move' }, { id: 'hide', label: 'Hide' }, { id: 'interact', label: 'Interact' } ]; fallbackActions.forEach(action => { const button = document.createElement('button'); button.id = `rts-${action.id}-btn`; button.className = 'menu_button rts-action-btn'; button.textContent = action.label; button.addEventListener('click', () => this.handleQuickAction(action.id)); container.appendChild(button); console.log(`RTS: Created fallback button: ${action.label}`); }); console.log(`RTS: Created ${fallbackActions.length} fallback quick action buttons`); return; } quickActions.forEach(action => { const button = document.createElement('button'); button.id = `rts-${action.id}-btn`; button.className = 'menu_button rts-action-btn'; button.textContent = action.label; button.title = action.command || action.label; button.addEventListener('click', () => this.handleQuickAction(action.id)); container.appendChild(button); console.log(`RTS: Created preset button: ${action.label}`); }); console.log(`RTS: Created ${quickActions.length} preset quick action buttons`); } catch (error) { console.error('RTS: Error creating quick action buttons:', error); } } async handleExecuteCommand() { const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input')); if (!commandInput) return; const command = commandInput.value.trim(); if (!command) return; this.addLogEntry('action', command); commandInput.value = ''; const executeBtn = /** @type {HTMLButtonElement} */ (document.getElementById('rts-execute-btn')); if (executeBtn) { executeBtn.disabled = true; executeBtn.innerHTML = ' Processing...'; } try { // This is where the command would be sent to the LLM await sendTurn(command); // The UI will now be updated by the 'rts-narrative-update' event listener } catch (error) { console.error('Failed to execute RTS command:', error); this.addLogEntry('error', `Failed to execute command: ${error.message}`); } finally { if (executeBtn) { executeBtn.disabled = false; executeBtn.innerHTML = ' Take Action'; } } } handleClearCommand() { const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input')); if (commandInput) { commandInput.value = ''; commandInput.focus(); } } handleMapControls(action) { if (!this.mapCanvas) return; switch (action) { case 'zoomIn': this.mapCanvas.zoomIn(); break; case 'zoomOut': this.mapCanvas.zoomOut(); break; case 'center': this.mapCanvas.centerMap(); break; } } handleMouseMove(e) { if (this.mapCanvas) { this.mapCanvas.handleMouseMove(e); } } handleKeydown(e) { if (e.key === 'Escape') { this.exitFullscreen(); } } updateUI() { if (!this.mapData) return; // Update the new status panel with current game state const gameState = GameStateManager.getState(); try { updateResourcePanel(gameState); } catch (error) { console.warn('RTS: Error updating resource panel from RTSUIController:', error); } // Keep the existing zoo-specific updates this.updateZooStatusDisplay(); this.updateEscapedAnimalsDisplay(); this.updateIncidentsDisplay(); this.updatePeopleStatusDisplay(); this.updateThreatDisplay(); } handleSelectionChange() { this.updateEscapedAnimalsDisplay(); this.updatePeopleStatusDisplay(); } handleEntitySelection(entity) { this.mapCanvas.clearSelection(); this.mapCanvas.selectElement({ type: 'entity', data: entity }); this.updateEscapedAnimalsDisplay(); } updateZooStatusDisplay() { const state = GameStateManager.getState(); const playerCount = (state.entities || []).filter(e => e.type === 'player').length; const gateCount = this.mapData.map.tiles.flat().filter(t => t === 3).length; this.updateElementText('rts-alert-level', (state.threatLevel || 'low').toUpperCase()); this.updateElementText('rts-survivors', playerCount > 0 ? 1 : 0); // Assuming 1 player this.updateElementText('rts-casualties', (state.casualties && state.casualties.total) || 0); this.updateElementText('rts-breached', `${gateCount} (Main Gate)`); } updateEscapedAnimalsDisplay() { const container = document.getElementById('rts-escaped-animals'); if (!container) return; container.innerHTML = ''; const state = GameStateManager.getState(); if (state.entities) { state.entities.forEach(entity => { if (entity.type !== 'player' && entity.type !== 'guard' && entity.status === 'escaped') { const element = document.createElement('div'); element.className = `rts-animal-item rts-escaped ${this.mapCanvas.selectedEntities.includes(entity) ? 'selected' : ''}`; element.innerHTML = ` ${entity.name || entity.type.replace('_', ' ')} ${entity.location || 'Roaming'} ${entity.mood || 'Hunting'} `; element.addEventListener('click', () => this.handleEntitySelection(entity)); container.appendChild(element); } }); } } updateIncidentsDisplay() { const container = document.getElementById('rts-active-incidents'); if (!container) return; container.innerHTML = ''; // Clear previous incidents const state = GameStateManager.getState(); if (state.incidents) { state.incidents.forEach(incident => { const element = document.createElement('div'); element.className = 'rts-incident-item'; element.innerHTML = ` ${incident.description} ${incident.location} `; container.appendChild(element); }); } } updatePeopleStatusDisplay() { const container = document.getElementById('rts-people-status'); if (!container) return; container.innerHTML = ''; // Clear previous people const state = GameStateManager.getState(); if (state.mapState && state.mapState.mapData && state.mapState.mapData.entities) { // Show visitors, staff, keepers, veterinarians - no guards const people = state.mapState.mapData.entities.filter(e => ['visitor', 'staff', 'keeper', 'veterinarian'].includes(e.type) ); people.forEach((person, index) => { const element = document.createElement('div'); element.className = 'rts-person-item'; // Choose appropriate icon based on type let icon = 'fa-user'; let color = '#6b7280'; if (person.type === 'visitor') { icon = 'fa-user'; color = '#3b82f6'; } else if (person.type === 'staff') { icon = 'fa-user-tie'; color = '#10b981'; } else if (person.type === 'keeper') { icon = 'fa-user-check'; color = '#f59e0b'; } else if (person.type === 'veterinarian') { icon = 'fa-user-doctor'; color = '#8b5cf6'; } // Add escape status for visitors let escapeStatus = ''; if (person.type === 'visitor') { const escapeChance = this.calculateEscapeChance(person); escapeStatus = `${escapeChance.text}`; } element.innerHTML = ` ${person.name || `${person.type} ${index + 1}`} ${person.status || 'Active'} ${escapeStatus} @ (${person.x}, ${person.y}) `; element.addEventListener('click', () => this.handleEntitySelection(person)); container.appendChild(element); }); } } updateElementText(id, value) { const element = document.getElementById(id); if (element) { element.textContent = value; } } calculateEscapeChance(person) { const state = GameStateManager.getState(); const threatLevel = state.threatLevel || 'none'; const playerPos = state.mapState?.playerPosition || { x: 0, y: 0 }; // Calculate distance from player const distance = Math.sqrt( Math.pow(person.x - playerPos.x, 2) + Math.pow(person.y - playerPos.y, 2) ); // Base escape chance based on threat level let escapeScore = 0; switch (threatLevel) { case 'none': escapeScore = 95; break; case 'low': escapeScore = 75; break; case 'medium': escapeScore = 50; break; case 'high': escapeScore = 25; break; case 'extreme': escapeScore = 5; break; default: escapeScore = 50; } // Adjust based on distance from player (closer = safer) if (distance <= 3) escapeScore += 20; else if (distance >= 10) escapeScore -= 30; // Adjust based on person status if (person.status === 'injured') escapeScore -= 40; else if (person.status === 'panicked') escapeScore -= 20; else if (person.status === 'hiding') escapeScore += 10; // Clamp between 5-95 escapeScore = Math.max(5, Math.min(95, escapeScore)); // Determine level and text if (escapeScore >= 80) { return { level: 'high', text: 'Safe' }; } else if (escapeScore >= 60) { return { level: 'medium', text: 'At Risk' }; } else if (escapeScore >= 30) { return { level: 'low', text: 'Danger' }; } else { return { level: 'critical', text: 'Critical' }; } } addLogEntry(type, message) { const gameLog = document.getElementById('rts-game-log'); if (!gameLog) return; const logEntry = document.createElement('div'); logEntry.className = `rts-log-entry rts-log-${type}`; logEntry.innerHTML = `${message}`; gameLog.appendChild(logEntry); gameLog.scrollTop = gameLog.scrollHeight; while (gameLog.children.length > 100) { gameLog.removeChild(gameLog.firstChild); } } handleNarrativeUpdate(event) { const { type, message, entityUpdates } = event.detail; this.addLogEntry(type, message); // Handle entity updates and refresh map if (entityUpdates && entityUpdates.length > 0) { this.refreshMapEntities(); } this.updateUI(); } refreshMapEntities() { if (this.mapCanvas && this.mapData) { // Get updated map data from GameStateManager const gameState = GameStateManager.getState(); if (gameState.mapState && gameState.mapState.mapData) { this.mapData = gameState.mapState.mapData; this.mapCanvas.loadMap(this.mapData); console.log('RTS: Map entities refreshed with', this.mapData?.map?.entities?.length || 0, 'entities'); } } } handleZoneChange(event) { const zone = event.detail; this.addLogEntry('system', `Entered ${zone.name}.`); this.updateThreatDisplay(); } handleMapUpdate(event) { console.log('RTS: Map update event received:', event.detail); this.refreshMapEntities(); this.updateUI(); } handleCanvasRefresh(event) { console.log('RTS: Canvas refresh event received:', event.detail); if (this.mapCanvas) { // Force the map canvas to redraw this.mapCanvas.dirty = true; this.refreshMapEntities(); } this.updateUI(); } updateThreatDisplay() { const state = GameStateManager.getState(); const zone = EventManager.zones.find(z => z.id === state.currentZone); const zoneName = zone ? zone.name : 'Unknown Zone'; this.updateElementText('rts-current-zone', zoneName); this.updateElementText('rts-threat-level', state.threatLevel); } isActive() { return this.isFullscreen; } toggle() { if (this.isFullscreen) { this.exitFullscreen(); } else { this.enterFullscreen(); } } async loadPreset(presetPath) { await PresetManager.loadPreset(presetPath); GameStateManager.reset(); if (this.isFullscreen) { // Recreate quick action buttons for the new preset this.createQuickActionButtons(); // If the UI is already open, we need to recreate it to reflect the new preset await this.exitFullscreen(); await this.enterFullscreen(); } } } export const rtsUI = new RTSUIController(); document.addEventListener('rts-state-updated', () => { if (rtsUI.isActive()) { rtsUI.updateUI(); } });