378 lines
14 KiB
JavaScript
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(); |