Files
sillytavern-rts-mode/index.js
T
2025-08-04 00:20:02 -07:00

630 lines
27 KiB
JavaScript

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));
}
});