console.log('RTS-MODE: index.js script loading...'); // Test if jQuery is available console.log('RTS-MODE: jQuery available:', typeof jQuery !== 'undefined'); console.log('RTS-MODE: $ available:', typeof $ !== 'undefined'); import { extension_settings, saveSettingsDebounced } from '../../../script.js'; import { renderExtensionTemplateAsync } from '../../extensions.js'; import { eventSource, event_types } from '../../events.js'; import { createMapCanvas } from './ui/MapCanvas.js'; import { createResourcePanel } from './ui/ResourcePanel.js'; import { rtsUI } from './ui/RTSUIController.js'; import GameStateManager from './src/GameStateManager.js'; import { sendTurn } from './src/LLMAdapter.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; import './js/rts-mode.js'; console.log('RTS-MODE: All imports successful'); const extensionId = 'rts-mode'; const extensionName = 'RTS Chat Mode'; // RTS-mode specific settings const rtsSettings = { structuredOutput: false, autoSchemaUpdates: true, contentFilter: true, }; // Export settings for use in other modules export { rtsSettings }; /** * Generates a comprehensive JSON schema based on the current game state structure * @param {object} gameState - Current game state to base schema on * @returns {object} Complete JSON schema for AI responses */ function generateRTSJSONSchema(gameState) { // Ensure gameState is a valid object const safeGameState = gameState && typeof gameState === 'object' ? gameState : {}; return { "type": "object", "properties": { "narrative": { "type": "string", "description": "Brief atmospheric narrative (2-3 sentences max) focusing on immediate sensory details and events." }, "state": { "type": "object", "description": "Complete updated game state", "properties": { "turn": { "type": "integer", "description": "Current turn number" }, "currentZone": { "type": "string", "description": "Current zone/area the player is in" }, "threatLevel": { "type": "string", "enum": ["none", "low", "medium", "high", "extreme"], "description": "Current threat level" }, "lastEvent": { "type": "string", "description": "Description of the last significant event" }, "casualties": { "type": "object", "properties": { "total": { "type": "integer", "minimum": 0 }, "recent": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "cause": { "type": "string" }, "location": { "type": "string" }, "perpetrator": { "type": "string" }, "turn": { "type": "integer" }, "description": { "type": "string" } }, "required": ["name", "cause", "location", "perpetrator", "turn"] } }, "byZone": { "type": "object" }, "byAnimal": { "type": "object" } }, "required": ["total", "recent", "byZone", "byAnimal"] }, "escapedAnimals": { "type": "object", "properties": { "total": { "type": "integer", "minimum": 0 }, "active": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string" }, "currentLocation": { "type": "string" }, "escapedFrom": { "type": "string" }, "threat": { "type": "string", "enum": ["low", "medium", "high", "extreme"] }, "lastSeen": { "type": "integer" }, "behavior": { "type": "string" } }, "required": ["id", "name", "type", "currentLocation", "threat", "behavior"] } }, "byType": { "type": "object" }, "byZone": { "type": "object" } }, "required": ["total", "active", "byType", "byZone"] }, "activeIncidents": { "type": "object", "properties": { "emergency": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "description": { "type": "string" }, "location": { "type": "string" }, "priority": { "type": "string", "enum": ["emergency", "high", "medium", "low"] }, "turn": { "type": "integer" } }, "required": ["type", "description", "location", "priority"] } }, "ongoing": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "description": { "type": "string" }, "location": { "type": "string" }, "priority": { "type": "string", "enum": ["emergency", "high", "medium", "low"] }, "turn": { "type": "integer" } }, "required": ["type", "description", "location", "priority"] } }, "resolved": { "type": "array" } }, "required": ["emergency", "ongoing", "resolved"] }, "personnel": { "type": "object", "properties": { "alive": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string", "enum": ["visitor", "staff", "keeper", "veterinarian"] }, "position": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } }, "required": ["x", "y"] }, "status": { "type": "string" }, "lastSeen": { "type": "integer" }, "description": { "type": "string" } }, "required": ["id", "name", "type", "position", "status"] } }, "injured": { "type": "array" }, "missing": { "type": "array" }, "evacuated": { "type": "array" } }, "required": ["alive", "injured", "missing", "evacuated"] }, "playerPosition": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } }, "required": ["x", "y"], "description": "Player's current map coordinates" }, "visibleEntities": { "type": "array", "description": "All entities currently visible to the player (within ~5 tiles)", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string" }, "position": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } }, "required": ["x", "y"] }, "status": { "type": "string" }, "action": { "type": "string", "description": "What the entity is currently doing" }, "description": { "type": "string" } }, "required": ["id", "name", "type", "position"] } }, "environment": { "type": "object", "properties": { "timeOfDay": { "type": "string", "enum": ["evening", "night", "dawn", "morning", "afternoon"] }, "weather": { "type": "string", "enum": ["clear", "rain", "storm", "fog"] }, "powerStatus": { "type": "string", "enum": ["full", "partial", "none"] }, "evacuationStatus": { "type": "string", "enum": ["open", "blocked", "chaos"] }, "zooStatus": { "type": "string", "enum": ["normal", "incident", "chaos", "lockdown"] } }, "required": ["timeOfDay", "weather", "powerStatus", "evacuationStatus", "zooStatus"] } }, "required": ["turn", "currentZone", "threatLevel", "casualties", "escapedAnimals", "activeIncidents", "personnel", "playerPosition", "visibleEntities", "environment"] }, "entityUpdates": { "type": "array", "description": "Specific entity position and status changes for map synchronization", "items": { "type": "object", "properties": { "id": { "type": "string" }, "x": { "type": "number" }, "y": { "type": "number" }, "status": { "type": "string" }, "action": { "type": "string" } }, "required": ["id", "x", "y"] } } }, "required": ["narrative", "state"], "additionalProperties": false }; } // Dynamic schema - will be populated when needed let RTS_JSON_SCHEMA = generateRTSJSONSchema(); function getModelName(settings) { if (!settings) return undefined; const source = settings.chat_completion_source; if (!source) { // Fallback for older structures or different contexts return settings.model; } // Maps chat completion source to its corresponding model property name const modelPropertyMap = { 'openai': 'openai_model', 'google': 'google_model', 'vertexai': 'vertexai_model', 'claude': 'claude_model', 'openrouter': 'openrouter_model', 'mistralai': 'mistralai_model', 'cohere': 'cohere_model', 'groq': 'groq_model', 'deepseek': 'deepseek_model', 'nanogpt': 'nanogpt_model', 'xai': 'xai_model', 'ai21': 'ai21_model', 'aimlapi': 'aimlapi_model', 'moonshot': 'moonshot_model', 'perplexity': 'perplexity_model', 'pollinations': 'pollinations_model', 'custom': 'custom_model', }; const modelPropertyName = modelPropertyMap[source]; return modelPropertyName ? settings[modelPropertyName] : settings.model; } function isModelCompatible(chatCompletionSettings) { // TODO: Expand this list with more models that support structured output const compatibleModels = [ 'claude-3', 'gpt-4', 'gemini', ]; const modelName = getModelName(chatCompletionSettings); if (!modelName) { console.warn('RTS: Could not determine model name from settings', chatCompletionSettings); return false; } const modelNameLower = modelName.toLowerCase(); return compatibleModels.some(m => modelNameLower.includes(m)); } async function loadRtsSettings() { // Load settings Object.assign(rtsSettings, extension_settings[extensionId]); // Set UI elements $('#rts-structured-output-toggle').prop('checked', rtsSettings.structuredOutput); $('#rts-content-filter-toggle').prop('checked', rtsSettings.contentFilter); // Auto-schema is enabled by default if (rtsSettings.autoSchemaUpdates === undefined) { rtsSettings.autoSchemaUpdates = true; extension_settings[extensionId].autoSchemaUpdates = true; saveSettingsDebounced(); } } function onStructuredOutputToggle(event) { const value = Boolean($(event.target).prop('checked')); rtsSettings.structuredOutput = value; extension_settings[extensionId].structuredOutput = value; saveSettingsDebounced(); } function onContentFilterToggle(event) { const value = Boolean($(event.target).prop('checked')); rtsSettings.contentFilter = value; extension_settings[extensionId].contentFilter = value; saveSettingsDebounced(); } function onAutoSchemaToggle(enabled) { rtsSettings.autoSchemaUpdates = enabled; extension_settings[extensionId].autoSchemaUpdates = enabled; saveSettingsDebounced(); console.log('RTS: Auto-schema updates', enabled ? 'enabled' : 'disabled'); } // Function to get current settings (for dropdown sync) function getRTSSettings() { return { ...rtsSettings }; } // Export functions for use in RTSUIController export { getRTSSettings, onAutoSchemaToggle, onStructuredOutputToggle }; let root; let topButton; let uiMounted = false; async function addTopBarButton() { console.log('RTS Chat Mode: Adding top bar button...'); if (topButton) { console.warn('RTS Chat Mode: Top button already exists, skipping creation.'); return; } topButton = $(await renderExtensionTemplateAsync('rts-mode', 'button')); $('#top-settings-holder').append(topButton); // Set up direct RTS UI toggle functionality topButton.on('click', async (e) => { e.stopPropagation(); console.log('RTS Mode button clicked'); try { // Toggle between fullscreen RTS UI and normal SillyTavern UI if (rtsUI.isActive()) { await rtsUI.exitFullscreen(); // Update button to show we're back to normal mode topButton.find('i').removeClass('fa-eye-slash').addClass('fa-chess-board'); topButton.attr('title', 'Toggle RTS UI').attr('data-i18n', '[title]Toggle RTS UI'); console.log('RTS UI hidden, returned to SillyTavern interface'); } else { await rtsUI.enterFullscreen(); // Update button to show we're in fullscreen mode topButton.find('i').removeClass('fa-chess-board').addClass('fa-eye-slash'); topButton.attr('title', 'Hide RTS UI').attr('data-i18n', '[title]Hide RTS UI'); console.log('RTS UI activated'); } } catch (error) { console.error('Error toggling RTS UI:', error); } }); } function mountUI() { if (!document.getElementById('rts-mode-root')) { root = document.createElement('div'); root.id = 'rts-mode-root'; document.body.appendChild(root); createMapCanvas(root); createResourcePanel(root); console.log('RTS Chat Mode UI mounted.'); uiMounted = true; // Update button icon to show UI is active topButton?.find('i').removeClass('fa-chess-board').addClass('fa-eye-slash'); topButton?.attr('title', 'Hide RTS UI').attr('data-i18n', '[title]Hide RTS UI'); } } function unmountUI() { if (root) { root.remove(); root = null; console.log('RTS Chat Mode UI unmounted.'); uiMounted = false; // Update button icon to show UI is hidden topButton?.find('i').removeClass('fa-eye-slash').addClass('fa-chess-board'); topButton?.attr('title', 'Toggle RTS UI').attr('data-i18n', '[title]Toggle RTS UI'); } } async function onRtsStartCommand() { console.log('RTS Start command executed.'); const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-preset-select')); const selectedPreset = presetSelect ? presetSelect.value : '/scripts/extensions/rts-mode/presets/zoo_escape.json'; await rtsUI.loadPreset(selectedPreset); GameStateManager.reset(); // Update schema for fresh game state updateSchemaForGameState(GameStateManager.getState()); // If RTS UI is active, update it if (rtsUI.isActive()) { rtsUI.updateUI(); rtsUI.addLogEntry('system', 'Game state reset. New campaign begins!'); } return 'RTS game has been reset with the selected preset.'; } function onRtsCmdCommand(args, value) { console.log('RTS Command executed with args:', args, 'value:', value); if (value) { // The UI will now be updated by the 'rts-narrative-update' event listener sendTurn(value); return `RTS Command executed: ${value}`; } return 'No command provided'; } async function onRtsUICommand() { console.log('RTS UI toggle command executed.'); if (rtsUI.isActive()) { await rtsUI.exitFullscreen(); // Update button state to match if (topButton) { topButton.find('i').removeClass('fa-eye-slash').addClass('fa-chess-board'); topButton.attr('title', 'Toggle RTS UI').attr('data-i18n', '[title]Toggle RTS UI'); } return 'RTS UI hidden. Returned to normal SillyTavern interface.'; } else { await rtsUI.enterFullscreen(); // Update button state to match if (topButton) { topButton.find('i').removeClass('fa-chess-board').addClass('fa-eye-slash'); topButton.attr('title', 'Hide RTS UI').attr('data-i18n', '[title]Hide RTS UI'); } return 'RTS UI activated. Use ESC key or /rts-ui to return to normal interface.'; } } jQuery(async function() { console.log('RTS Chat Mode: jQuery ready, initializing extension...'); // Load settings extension_settings[extensionId] = extension_settings[extensionId] || {}; loadRtsSettings(); await addTopBarButton(); // Add event listeners $('#rts-structured-output-toggle').on('input', onStructuredOutputToggle); $('#rts-content-filter-toggle').on('input', onContentFilterToggle); // Register slash commands SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-start', callback: onRtsStartCommand, helpString: 'Resets the RTS game state.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-cmd', callback: onRtsCmdCommand, helpString: 'Sends a command to the RTS game master.', unnamedArgumentList: [ new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING], true), ], })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-ui', callback: onRtsUICommand, aliases: ['rts-toggle', 'rts-fullscreen'], helpString: 'Toggles the full-screen RTS interface.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-observe', callback: () => onRtsCmdCommand([], 'Look around carefully to assess the situation.'), helpString: 'Quick action: Observe the surroundings.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-move', callback: () => onRtsCmdCommand([], 'Move to a specific location.'), helpString: 'Quick action: Move to another location.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-hide', callback: () => onRtsCmdCommand([], 'Find a hiding spot.'), helpString: 'Quick action: Find a place to hide.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rts-interact', callback: () => onRtsCmdCommand([], 'Interact with something in the environment.'), helpString: 'Quick action: Interact with nearby objects or people.', })); // Listen for game state updates to keep schema current document.addEventListener('rts-narrative-update', (event) => { if (event instanceof CustomEvent && rtsSettings.autoSchemaUpdates && event.detail && event.detail.state) { updateSchemaForGameState(event.detail.state); } }); console.log('RTS Chat Mode: Extension initialized successfully'); }); /** * Updates the JSON schema based on current game state to optimize for what's actually needed * @param {object} gameState - Current game state */ function updateSchemaForGameState(gameState) { if (!gameState) { RTS_JSON_SCHEMA = generateRTSJSONSchema(); return; } // Analyze current state to determine what fields are actually needed const schemaOptimizations = { // Always require core fields coreFields: ["turn", "currentZone", "threatLevel", "playerPosition", "visibleEntities", "environment"], // Conditionally require tracking fields based on game progression conditionalFields: [] }; // If game has progressed beyond setup, require tracking systems if (gameState.turn > 0) { schemaOptimizations.conditionalFields.push("casualties", "escapedAnimals", "activeIncidents"); } // Always require personnel since we track visitors and staff schemaOptimizations.conditionalFields.push("personnel"); // Generate optimized schema const baseSchema = generateRTSJSONSchema(gameState); // Update required fields in state object based on analysis const stateRequired = [...schemaOptimizations.coreFields, ...schemaOptimizations.conditionalFields]; baseSchema.properties.state.required = stateRequired; // Add contextual descriptions based on current state if (gameState.turn === 0) { baseSchema.properties.narrative.description = "Brief description of the peaceful zoo atmosphere before any incidents occur."; baseSchema.properties.state.properties.casualties.description = "Should remain at zero totals for pre-incident state."; baseSchema.properties.state.properties.escapedAnimals.description = "Should remain empty for pre-incident state."; } else { baseSchema.properties.narrative.description = "Brief atmospheric narrative (2-3 sentences max) focusing on immediate danger, actions, and consequences."; } // Optimize entity schema based on visible entities if (gameState.visibleEntities && gameState.visibleEntities.length > 0) { // Add more specific validation for entity types we've seen const entityTypes = [...new Set(gameState.visibleEntities.map(e => e.type))]; if (entityTypes.length > 0) { baseSchema.properties.state.properties.visibleEntities.items.properties.type.enum = entityTypes; } } RTS_JSON_SCHEMA = baseSchema; console.log('RTS: Updated JSON schema for turn', gameState.turn, 'with required fields:', stateRequired); } eventSource.on(event_types.CHAT_COMPLETION_SETTINGS_READY, (data) => { const { chatCompletionSettings } = SillyTavern.getContext(); console.log('RTS: Chat completion settings ready:', chatCompletionSettings); console.log('RTS: Structured output setting:', rtsSettings.structuredOutput); console.log('RTS: Model compatibility check:', isModelCompatible(chatCompletionSettings)); if (rtsSettings.structuredOutput && isModelCompatible(chatCompletionSettings)) { // Update schema based on current game state before sending try { const currentState = GameStateManager.getState(); updateSchemaForGameState(currentState); } catch (error) { console.warn('RTS: Could not update schema with current state:', error); RTS_JSON_SCHEMA = generateRTSJSONSchema(); } data.responseMimeType = "application/json"; data.responseSchema = RTS_JSON_SCHEMA; console.log('RTS: Using structured output with dynamic schema'); //dump the schema to console for debugging console.log('RTS: Current JSON schema:', JSON.stringify(RTS_JSON_SCHEMA, null, 2)); } });