Files
sillytavern-rts-mode/ui/RTSUIController.js
T
2025-08-03 17:35:34 -07:00

378 lines
14 KiB
JavaScript

import { renderExtensionTemplateAsync } from '../../../extensions.js';
import { RTSMapCanvas } from './MapCanvas.js';
import GameStateManager from '../src/GameStateManager.js';
import { sendTurn } from '../src/LLMAdapter.js';
/**
* Controls the full-screen RTS UI that replaces the main SillyTavern interface
*/
export class RTSUIController {
constructor() {
this.isFullscreen = false;
this.originalSheldContent = null;
this.rtsContainer = null;
this.mapCanvas = null;
this.gameLog = [];
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);
}
async enterFullscreen() {
if (this.isFullscreen) return;
console.log('RTS: Entering fullscreen mode');
// Hide original SillyTavern UI - hide the entire main content area
const sheld = document.getElementById('sheld');
if (sheld) {
this.originalSheldDisplay = sheld.style.display;
sheld.style.display = 'none';
}
// Create and inject RTS UI
await this.createRTSInterface();
this.isFullscreen = true;
// Initialize game state display
this.updateUI();
// Add escape key listener
document.addEventListener('keydown', this.handleKeydown.bind(this));
}
async exitFullscreen() {
if (!this.isFullscreen) return;
console.log('RTS: Exiting fullscreen mode');
// Remove RTS UI
if (this.rtsContainer) {
this.rtsContainer.remove();
this.rtsContainer = null;
}
// Restore original SillyTavern UI
const sheld = document.getElementById('sheld');
if (sheld) {
sheld.style.display = this.originalSheldDisplay || '';
}
this.isFullscreen = false;
this.mapCanvas = null;
// Remove escape key listener
document.removeEventListener('keydown', this.handleKeydown.bind(this));
}
async createRTSInterface() {
try {
console.log('RTS: Loading template...');
// Load the RTS UI template
const rtsHTML = await renderExtensionTemplateAsync('rts-mode', 'rts-ui');
console.log('RTS: Creating container...');
// Create container and inject into page
this.rtsContainer = document.createElement('div');
this.rtsContainer.innerHTML = rtsHTML;
document.body.appendChild(this.rtsContainer);
console.log('RTS: Initializing map canvas...');
// Initialize map canvas
this.mapCanvas = new RTSMapCanvas('rts-game-map');
console.log('RTS: Setting up event listeners...');
// Set up event listeners
this.setupEventListeners();
console.log('RTS: Updating displays...');
// Initialize with current game state
this.updateResourceDisplay();
this.updateUnitsDisplay();
this.updateBuildingsDisplay();
this.addLogEntry('system', 'RTS Mode activated. Command your forces!');
console.log('RTS: Interface created successfully');
} catch (error) {
console.error('Error creating RTS interface:', error);
throw error;
}
}
setupEventListeners() {
console.log('RTS: Setting up event listeners for buttons...');
// Quick action buttons
const buildBtn = document.getElementById('rts-build-btn');
const recruitBtn = document.getElementById('rts-recruit-btn');
const exploreBtn = document.getElementById('rts-explore-btn');
const tradeBtn = document.getElementById('rts-trade-btn');
console.log('RTS: Build button found:', !!buildBtn);
console.log('RTS: Recruit button found:', !!recruitBtn);
console.log('RTS: Explore button found:', !!exploreBtn);
console.log('RTS: Trade button found:', !!tradeBtn);
buildBtn?.addEventListener('click', () => this.handleQuickAction('build'));
recruitBtn?.addEventListener('click', () => this.handleQuickAction('recruit'));
exploreBtn?.addEventListener('click', () => this.handleQuickAction('explore'));
tradeBtn?.addEventListener('click', () => this.handleQuickAction('trade'));
// Command execution
const executeBtn = document.getElementById('rts-execute-btn');
const clearBtn = document.getElementById('rts-clear-btn');
console.log('RTS: Execute button found:', !!executeBtn);
console.log('RTS: Clear button found:', !!clearBtn);
executeBtn?.addEventListener('click', this.handleExecuteCommand);
clearBtn?.addEventListener('click', this.handleClearCommand);
// Map controls
const zoomInBtn = document.getElementById('rts-zoom-in');
const zoomOutBtn = document.getElementById('rts-zoom-out');
const centerBtn = document.getElementById('rts-center-map');
console.log('RTS: Zoom in button found:', !!zoomInBtn);
console.log('RTS: Zoom out button found:', !!zoomOutBtn);
console.log('RTS: Center button found:', !!centerBtn);
zoomInBtn?.addEventListener('click', () => this.handleMapControls('zoomIn'));
zoomOutBtn?.addEventListener('click', () => this.handleMapControls('zoomOut'));
centerBtn?.addEventListener('click', () => this.handleMapControls('center'));
// Enter key in command input
const commandInput = document.getElementById('rts-command-input');
console.log('RTS: Command input found:', !!commandInput);
if (commandInput) {
commandInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleExecuteCommand();
}
});
}
console.log('RTS: Event listeners setup complete');
}
handleQuickAction(action) {
const commandInput = document.getElementById('rts-command-input');
if (!commandInput) return;
const actionTemplates = {
build: 'Build a new structure. Specify type and location (e.g., "Build a barracks near the town hall")',
recruit: 'Recruit new units. Specify type and quantity (e.g., "Recruit 5 warriors and 3 archers")',
explore: 'Send units to explore. Specify direction or target (e.g., "Send scouts to explore the northern forest")',
trade: 'Initiate trade with neighbors or manage resources (e.g., "Trade wood for gold with nearby village")'
};
commandInput.value = actionTemplates[action] || '';
commandInput.focus();
commandInput.select();
}
async handleExecuteCommand() {
const commandInput = document.getElementById('rts-command-input');
if (!commandInput) return;
const command = commandInput.value.trim();
if (!command) return;
// Add to log
this.addLogEntry('action', command);
// Clear input
commandInput.value = '';
// Show processing state
const executeBtn = document.getElementById('rts-execute-btn');
if (executeBtn) {
executeBtn.disabled = true;
executeBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Processing...';
}
try {
// Send command to LLM
await sendTurn(command);
// The LLMAdapter will handle updating the game state
// We'll update the UI when the state changes
setTimeout(() => {
this.updateUI();
this.addLogEntry('result', 'Command executed successfully!');
}, 1000);
} catch (error) {
console.error('Failed to execute RTS command:', error);
this.addLogEntry('error', `Failed to execute command: ${error.message}`);
} finally {
// Restore execute button
if (executeBtn) {
executeBtn.disabled = false;
executeBtn.innerHTML = '<i class="fa-solid fa-play"></i> Execute Turn';
}
}
}
handleClearCommand() {
const commandInput = 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;
}
}
handleKeydown(e) {
if (e.key === 'Escape') {
this.exitFullscreen();
// Update button state to match (find the button in DOM)
const topButton = document.querySelector('#rts-mode-button');
if (topButton) {
const toggleBtn = topButton.querySelector('#rts-toggle-ui-btn');
if (toggleBtn) {
const icon = toggleBtn.querySelector('i');
if (icon) {
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
toggleBtn.setAttribute('title', 'Show RTS UI');
toggleBtn.setAttribute('data-i18n', '[title]Show RTS UI');
}
}
}
}
updateUI() {
this.updateResourceDisplay();
this.updateUnitsDisplay();
this.updateBuildingsDisplay();
this.updateTurnInfo();
if (this.mapCanvas) {
this.mapCanvas.updateFromGameState(GameStateManager.getState());
}
}
updateResourceDisplay() {
const state = GameStateManager.getState();
const resources = state.resources || {};
this.updateElementText('rts-gold', resources.gold || 100);
this.updateElementText('rts-wood', resources.wood || 50);
this.updateElementText('rts-stone', resources.stone || 25);
this.updateElementText('rts-food', resources.food || 75);
}
updateUnitsDisplay() {
const state = GameStateManager.getState();
const units = state.units || [];
// Count units by type
const unitCounts = units.reduce((counts, unit) => {
counts[unit.type] = (counts[unit.type] || 0) + 1;
return counts;
}, {});
this.updateElementText('rts-warriors', unitCounts.warrior || 5);
this.updateElementText('rts-archers', unitCounts.archer || 3);
this.updateElementText('rts-cavalry', unitCounts.cavalry || 2);
}
updateBuildingsDisplay() {
const state = GameStateManager.getState();
// For now, use default values - this would be expanded based on game state
this.updateElementText('rts-town-hall', 1);
this.updateElementText('rts-barracks', 1);
this.updateElementText('rts-storage', 1);
}
updateTurnInfo() {
const state = GameStateManager.getState();
this.updateElementText('rts-current-turn', state.turn || 1);
// Simple season calculation based on turn
const seasons = ['Spring', 'Summer', 'Autumn', 'Winter'];
const season = seasons[Math.floor((state.turn || 1) / 10) % 4];
this.updateElementText('rts-season', season);
}
updateElementText(id, value) {
const element = document.getElementById(id);
if (element) {
element.textContent = value;
}
}
addLogEntry(type, message) {
const gameLog = document.getElementById('rts-game-log');
if (!gameLog) return;
const state = GameStateManager.getState();
const turn = state.turn || 1;
const logEntry = document.createElement('div');
logEntry.className = `rts-log-entry rts-log-${type}`;
logEntry.innerHTML = `
<span class="rts-log-timestamp">[Turn ${turn}]</span>
<span class="rts-log-message">${message}</span>
`;
gameLog.appendChild(logEntry);
gameLog.scrollTop = gameLog.scrollHeight;
// Keep log manageable size
while (gameLog.children.length > 50) {
gameLog.removeChild(gameLog.firstChild);
}
}
// Method to update button state from external calls
updateButtonState(isActive) {
// This will be called from the main extension to update button appearance
// when the UI state changes
console.log('RTS UI state updated:', isActive);
}
// Public methods for external control
isActive() {
return this.isFullscreen;
}
toggle() {
if (this.isFullscreen) {
this.exitFullscreen();
} else {
this.enterFullscreen();
}
}
}
// Global instance
export const rtsUI = new RTSUIController();