575 lines
22 KiB
JavaScript
575 lines
22 KiB
JavaScript
|
|
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 = '<i class="fa-solid fa-spinner fa-spin"></i> 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 = '<i class="fa-solid fa-play"></i> 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 = `
|
|
<i class="fa-solid fa-paw" style="color: #dc2626;"></i>
|
|
<span class="rts-animal-name">${entity.name || entity.type.replace('_', ' ')}</span>
|
|
<span class="rts-animal-location">${entity.location || 'Roaming'}</span>
|
|
<span class="rts-animal-status rts-status-hunting">${entity.mood || 'Hunting'}</span>
|
|
`;
|
|
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 = `
|
|
<i class="fa-solid fa-fire" style="color: #f59e0b;"></i>
|
|
<span class="rts-incident-desc">${incident.description}</span>
|
|
<span class="rts-incident-location">${incident.location}</span>
|
|
`;
|
|
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 = `<span class="rts-escape-status rts-escape-${escapeChance.level}">${escapeChance.text}</span>`;
|
|
}
|
|
|
|
element.innerHTML = `
|
|
<i class="fa-solid ${icon}" style="color: ${color};"></i>
|
|
<span class="rts-person-name">${person.name || `${person.type} ${index + 1}`}</span>
|
|
<span class="rts-person-status rts-status-${person.status || 'active'}">${person.status || 'Active'}</span>
|
|
${escapeStatus}
|
|
<span class="rts-person-location">@ (${person.x}, ${person.y})</span>
|
|
`;
|
|
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 = `<span class="rts-log-message">${message}</span>`;
|
|
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();
|
|
}
|
|
});
|