wa
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **SillyTavern extension** called "RTS Chat Mode (Modular)" that creates a real-time strategy-style interface for interactive storytelling. The extension transforms normal chat interactions into a game-like experience with maps, entities, resources, and narrative events.
|
||||
|
||||
## Key Architecture Components
|
||||
|
||||
### Core System Files (`src/`)
|
||||
- **GameStateManager.js**: Central state management with enhanced tracking for casualties, escaped animals, incidents, personnel, and map entities. Handles state persistence and updates from AI responses.
|
||||
- **LLMAdapter.js**: Processes user commands and LLM responses. Handles JSON extraction from AI responses and synchronizes AI state with the map canvas.
|
||||
- **PresetManager.js**: Loads and manages game scenarios/presets from JSON files.
|
||||
- **EventManager.js**: Manages narrative events based on zones and threat levels.
|
||||
- **PromptCompressor.js**: Builds optimized prompts for the LLM including game state and context.
|
||||
|
||||
### UI Components (`ui/`)
|
||||
- **RTSUIController.js**: Main UI controller that manages fullscreen interface, command input, quick actions, and status displays.
|
||||
- **MapCanvas.js**: Interactive canvas-based map renderer for visualizing entities and game world.
|
||||
- **ResourcePanel.js**: Status panel showing game statistics and information.
|
||||
|
||||
### Entry Point
|
||||
- **index.js**: Main extension loader that registers slash commands, handles SillyTavern integration, and manages the top bar button.
|
||||
|
||||
## Game Preset System
|
||||
|
||||
The extension uses JSON preset files (`presets/` and `maps/`) that define:
|
||||
- **Scenarios**: Story setup, character descriptions, initial messages
|
||||
- **Maps**: Entity positions, terrain data, zone definitions
|
||||
- **Narrative Engine**: Zone-based threat tables for dynamic events
|
||||
- **Features**: UI options like quickActions, resources, fogOfWar
|
||||
|
||||
### Key Preset Properties
|
||||
- `narrativeEngine.zones[]`: Defines map areas with associated threat tables
|
||||
- `narrativeEngine.threatTables`: Collections of events by threat level (low/medium/high)
|
||||
- `features.quickActions[]`: Configurable action buttons with commands
|
||||
- `initialState`: Starting game conditions and threat levels
|
||||
|
||||
## Development Commands
|
||||
|
||||
The extension integrates with SillyTavern's existing systems and doesn't have separate build/test commands. Development workflow:
|
||||
|
||||
1. **Testing**: Load SillyTavern and activate the extension via the top bar button
|
||||
2. **Debugging**: Check browser console for logs prefixed with "RTS-MODE:" or "RTS:"
|
||||
3. **Preset Testing**: Modify JSON files in `presets/` and `maps/` directories
|
||||
|
||||
## Slash Commands
|
||||
|
||||
- `/rts-start` - Resets game state with selected preset
|
||||
- `/rts-cmd <command>` - Sends action to the game master
|
||||
- `/rts-ui` - Toggles fullscreen RTS interface
|
||||
- `/rts-observe`, `/rts-move`, `/rts-hide`, `/rts-interact` - Quick actions
|
||||
|
||||
## State Management Architecture
|
||||
|
||||
The system uses a multi-layered state approach:
|
||||
1. **Game State** (GameStateManager): Core game logic, entities, tracking systems
|
||||
2. **Map State**: Visual representation with entity positions and visibility
|
||||
3. **AI Response State**: Structured data from LLM containing narrative and updates
|
||||
4. **UI State**: Display state for panels, logs, and user interactions
|
||||
|
||||
### State Synchronization Flow
|
||||
1. User action → LLMAdapter → AI response → GameStateManager.setState()
|
||||
2. State changes → UI updates → Map canvas refresh → Status panel updates
|
||||
3. Entity updates → Map entity sync → Visual position updates
|
||||
|
||||
## Extension Integration
|
||||
|
||||
This extension follows SillyTavern's extension patterns:
|
||||
- Uses `renderExtensionTemplateAsync()` for HTML templates
|
||||
- Integrates with `generateQuietPrompt()` for LLM communication
|
||||
- Respects SillyTavern's settings system via `extension_settings`
|
||||
- Registers with the extension system through `manifest.json`
|
||||
|
||||
## Important Implementation Notes
|
||||
|
||||
- **Entity Management**: The system maintains entity data in both GameStateManager and map canvas, requiring careful synchronization
|
||||
- **AI Response Parsing**: Handles multiple JSON formats and reasoning model outputs with thought blocks
|
||||
- **Map Coordinate System**: Uses x,y grid coordinates for entity positioning
|
||||
- **Event System**: Custom events (`rts-narrative-update`, `rts-map-update`) coordinate between UI components
|
||||
- **Fullscreen Mode**: Completely replaces SillyTavern UI when active, managed by RTSUIController
|
||||
@@ -1,155 +0,0 @@
|
||||
<div id="rts-main-container" class="wide100p height100p">
|
||||
<!-- RTS Header -->
|
||||
<div id="rts-header" class="fa-solid fa-grip drag-grabber"></div>
|
||||
|
||||
<!-- Main RTS Game Area -->
|
||||
<div id="rts-game-area" class="flex-container wide100p height100p">
|
||||
<!-- Left Panel: Resources and Units -->
|
||||
<div id="rts-left-panel" class="rts-panel flex-container flexFlowColumn">
|
||||
<div class="rts-panel-header">
|
||||
<h3>Game Status</h3>
|
||||
</div>
|
||||
|
||||
<!-- Status Overview Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Status Overview</h4>
|
||||
<div id="rts-overview-stats">
|
||||
<div class="stat-item">Turn: <span id="turn-counter">1</span></div>
|
||||
<div class="stat-item">Zone: <span id="current-zone">Unknown</span></div>
|
||||
<div class="stat-item">Threat: <span id="threat-level">Low</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Casualties Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Casualties</h4>
|
||||
<div id="rts-casualties-stats">
|
||||
<div class="stat-item">Total Deaths: <span id="total-deaths">0</span></div>
|
||||
<div class="stat-item">Recent: <span id="recent-deaths">0</span></div>
|
||||
</div>
|
||||
<div id="rts-recent-casualties" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Escaped Animals Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Escaped Animals</h4>
|
||||
<div id="rts-animals-stats">
|
||||
<div class="stat-item">Active Threats: <span id="active-animals">0</span></div>
|
||||
</div>
|
||||
<div id="rts-escaped-animals" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Personnel Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Personnel</h4>
|
||||
<div id="rts-personnel-stats">
|
||||
<div class="stat-item">Alive: <span id="alive-personnel">0</span></div>
|
||||
<div class="stat-item">Injured: <span id="injured-personnel">0</span></div>
|
||||
<div class="stat-item">Missing: <span id="missing-personnel">0</span></div>
|
||||
</div>
|
||||
<div id="rts-personnel-list" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Active Incidents Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Active Incidents</h4>
|
||||
<div id="rts-incidents-stats">
|
||||
<div class="stat-item">Emergency: <span id="emergency-incidents">0</span></div>
|
||||
<div class="stat-item">Ongoing: <span id="ongoing-incidents">0</span></div>
|
||||
</div>
|
||||
<div id="rts-incidents-list" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Threat Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Threat</h4>
|
||||
<div id="rts-threat" class="rts-threat-display">
|
||||
<div class="rts-threat-item">
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
<span>Zone: <span id="rts-current-zone">Entrance</span></span>
|
||||
</div>
|
||||
<div class="rts-threat-item">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<span>Threat: <span id="rts-threat-level">Low</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Game Map -->
|
||||
<div id="rts-map-container" class="flex1 flex-container flexFlowColumn">
|
||||
<div class="rts-map-header">
|
||||
<h3>Battle Map</h3>
|
||||
<div class="rts-map-controls">
|
||||
<button id="rts-zoom-in" class="menu_button menu_button_icon" title="Zoom In">
|
||||
<i class="fa-solid fa-magnifying-glass-plus"></i>
|
||||
</button>
|
||||
<button id="rts-zoom-out" class="menu_button menu_button_icon" title="Zoom Out">
|
||||
<i class="fa-solid fa-magnifying-glass-minus"></i>
|
||||
</button>
|
||||
<button id="rts-center-map" class="menu_button menu_button_icon" title="Center Map">
|
||||
<i class="fa-solid fa-crosshairs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rts-map-wrapper" class="flex1 rts-map-wrapper">
|
||||
<canvas id="rts-game-map" width="800" height="600"></canvas>
|
||||
</div>
|
||||
<div id="rts-map-overlay" class="rts-map-overlay">
|
||||
<!-- Selected unit/building info will appear here -->
|
||||
</div>
|
||||
<div class="rts-map-footer">
|
||||
<div class="rts-turn-info">
|
||||
<span>Turn: <span id="rts-current-turn">1</span></span>
|
||||
<span class="rts-separator">|</span>
|
||||
<span>Season: <span id="rts-season">Spring</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Commands and Log -->
|
||||
<div id="rts-right-panel" class="rts-panel flex-container flexFlowColumn">
|
||||
<div class="rts-panel-header">
|
||||
<h3>Command Center</h3>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Quick Actions</h4>
|
||||
<div id="rts-quick-actions" class="rts-quick-actions">
|
||||
<!-- Quick action buttons will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Input -->
|
||||
<div class="rts-section flex1">
|
||||
<h4 class="rts-section-title">Command Input</h4>
|
||||
<div class="rts-command-input-area">
|
||||
<textarea id="rts-command-input"
|
||||
placeholder="Enter your strategy commands here... (e.g., 'Build a barracks north of the town hall' or 'Send 3 warriors to explore the eastern forest')"
|
||||
class="text_pole textarea_compact"
|
||||
rows="3"></textarea>
|
||||
<div class="rts-command-controls">
|
||||
<button id="rts-execute-btn" class="menu_button menu_button_bold">
|
||||
<i class="fa-solid fa-play"></i> Execute Turn
|
||||
</button>
|
||||
<button id="rts-clear-btn" class="menu_button menu_button_icon" title="Clear Input">
|
||||
<i class="fa-solid fa-eraser"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Log -->
|
||||
<div class="rts-section flex1">
|
||||
<h4 class="rts-section-title">Battle Log</h4>
|
||||
<div id="rts-game-log" class="rts-game-log scrollY">
|
||||
<div class="rts-log-entry rts-log-system">
|
||||
<span class="rts-log-timestamp">[Turn 1]</span>
|
||||
<span class="rts-log-message">Welcome to RTS Mode! Your settlement awaits your commands.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,30 +25,272 @@ const extensionName = 'RTS Chat Mode';
|
||||
// RTS-mode specific settings
|
||||
const rtsSettings = {
|
||||
structuredOutput: false,
|
||||
autoSchemaUpdates: true,
|
||||
contentFilter: true,
|
||||
};
|
||||
|
||||
// Placeholder for the JSON schema
|
||||
const RTS_JSON_SCHEMA = {
|
||||
// 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": "The story portion of the turn."
|
||||
"description": "Brief atmospheric narrative (2-3 sentences max) focusing on immediate sensory details and events."
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"description": "The complete game state."
|
||||
"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", "null"],
|
||||
"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": "A list of entities that have changed.",
|
||||
"description": "Specific entity position and status changes for map synchronization",
|
||||
"items": {
|
||||
"type": "object"
|
||||
"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"]
|
||||
"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) {
|
||||
@@ -59,8 +301,14 @@ function isModelCompatible(chatCompletionSettings) {
|
||||
'gemini',
|
||||
];
|
||||
|
||||
const modelName = chatCompletionSettings.model.toLowerCase();
|
||||
return compatibleModels.some(m => modelName.includes(m));
|
||||
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() {
|
||||
@@ -69,6 +317,14 @@ async function loadRtsSettings() {
|
||||
|
||||
// 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) {
|
||||
@@ -78,6 +334,29 @@ function onStructuredOutputToggle(event) {
|
||||
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;
|
||||
@@ -157,6 +436,9 @@ async function onRtsStartCommand() {
|
||||
|
||||
GameStateManager.reset();
|
||||
|
||||
// Update schema for fresh game state
|
||||
updateSchemaForGameState(GameStateManager.getState());
|
||||
|
||||
// If RTS UI is active, update it
|
||||
if (rtsUI.isActive()) {
|
||||
rtsUI.updateUI();
|
||||
@@ -209,6 +491,7 @@ jQuery(async function() {
|
||||
|
||||
// Add event listeners
|
||||
$('#rts-structured-output-toggle').on('input', onStructuredOutputToggle);
|
||||
$('#rts-content-filter-toggle').on('input', onContentFilterToggle);
|
||||
|
||||
// Register slash commands
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
@@ -257,13 +540,89 @@ jQuery(async function() {
|
||||
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');
|
||||
}
|
||||
});
|
||||
+45
@@ -98,6 +98,51 @@
|
||||
<h3>Action Center</h3>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="rts-section">
|
||||
<details>
|
||||
<summary class="rts-section-title">Settings</summary>
|
||||
<div style="padding: 10px 0;">
|
||||
<div>
|
||||
<h2>Preset Selection</h2>
|
||||
<p>Select a scenario preset to begin.</p>
|
||||
<select id="rts-preset-select">
|
||||
<option value="/scripts/extensions/rts-mode/presets/zoo_escape.json">Zoo Escape</option>
|
||||
<option value="/scripts/extensions/rts-mode/presets/classic_rts.json">Classic RTS</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px 0;">
|
||||
<div>
|
||||
<h2>Content Filter</h2>
|
||||
<p>Enable to allow graphic descriptions of violence and sexual themes.</p>
|
||||
<div class="rts-settings-toggle">
|
||||
<label for="rts-content-filter-toggle" class="rts-toggle-label">Explicit Content</label>
|
||||
<label class="rts-switch">
|
||||
<input type="checkbox" id="rts-content-filter-toggle" checked>
|
||||
<span class="rts-slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px 0;">
|
||||
<div>
|
||||
<h2>Model Settings</h2>
|
||||
<p>Use structured output for models that support it (e.g., OpenAI, Gemini, Anthropic).</p>
|
||||
<div class="rts-settings-toggle">
|
||||
<label for="rts-structured-output-toggle" class="rts-toggle-label">Structured Output</label>
|
||||
<label class="rts-switch">
|
||||
<input type="checkbox" id="rts-structured-output-toggle">
|
||||
<span class="rts-slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Quick Actions</h4>
|
||||
|
||||
+6
-1
@@ -111,7 +111,12 @@ export async function sendTurn(userCmd) {
|
||||
console.log('RTS: Entity updates in response:', responseJson.entityUpdates?.length || 0);
|
||||
|
||||
GameStateManager.setState(responseJson.state);
|
||||
const narrative = responseJson.narrative.trim();
|
||||
let narrative = responseJson.narrative;
|
||||
if (typeof narrative !== 'string') {
|
||||
// Coerce to string if it's not one, to prevent trim() error
|
||||
narrative = String(narrative);
|
||||
}
|
||||
narrative = narrative.trim();
|
||||
|
||||
// Handle entity updates if provided
|
||||
if (responseJson.entityUpdates && Array.isArray(responseJson.entityUpdates)) {
|
||||
|
||||
@@ -13,14 +13,158 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#rts-header {
|
||||
height: 20px;
|
||||
/* Updated Header Styling */
|
||||
.rts-header-bar {
|
||||
height: 40px;
|
||||
background: var(--SmartThemeBlurTintColor);
|
||||
cursor: move;
|
||||
border-bottom: 1px solid var(--SmartThemeBorderColor);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rts-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.rts-header-left .drag-grabber {
|
||||
cursor: move;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.rts-header-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
}
|
||||
|
||||
.rts-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Settings Dropdown */
|
||||
.rts-settings-dropdown {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 12px;
|
||||
background: var(--SmartThemeBlurTintColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1001;
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.rts-settings-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rts-settings-content h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border-bottom: 1px solid var(--SmartThemeBorderColor);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.rts-setting-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rts-setting-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rts-setting-description {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--SmartThemeQuoteColor);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.rts-setting-select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 4px;
|
||||
background: var(--SmartThemeEmColor);
|
||||
color: var(--SmartThemeBodyColor);
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Custom Toggle Switch */
|
||||
.rts-toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.rts-toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.rts-toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--SmartThemeBorderColor);
|
||||
transition: .3s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.rts-toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .rts-toggle-slider {
|
||||
background-color: var(--SmartThemeAccentColor);
|
||||
}
|
||||
|
||||
input:checked + .rts-toggle-slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.rts-setting-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
.rts-setting-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#rts-game-area {
|
||||
@@ -1140,3 +1284,4 @@ input:checked + .rts-slider:before {
|
||||
.rts-slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
+251
-29
@@ -1,11 +1,23 @@
|
||||
|
||||
import { renderExtensionTemplateAsync } from '../../../extensions.js';
|
||||
import { RTSMapCanvas } from './MapCanvas.js';
|
||||
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';
|
||||
import {
|
||||
sendTurn
|
||||
} from '../src/LLMAdapter.js';
|
||||
import {
|
||||
updateResourcePanel
|
||||
} from './ResourcePanel.js';
|
||||
import {
|
||||
getRTSSettings,
|
||||
onAutoSchemaToggle,
|
||||
onStructuredOutputToggle
|
||||
} from '../index.js';
|
||||
|
||||
export class RTSUIController {
|
||||
constructor() {
|
||||
@@ -28,6 +40,11 @@ export class RTSUIController {
|
||||
this.handleSelectionChange = this.handleSelectionChange.bind(this);
|
||||
this.handleNarrativeUpdate = this.handleNarrativeUpdate.bind(this);
|
||||
this.handleZoneChange = this.handleZoneChange.bind(this);
|
||||
this.handleSettingsToggle = this.handleSettingsToggle.bind(this);
|
||||
this.handleSettingChange = this.handleSettingChange.bind(this);
|
||||
this.handleRestartGame = this.handleRestartGame.bind(this);
|
||||
this.handleCloseSettings = this.handleCloseSettings.bind(this);
|
||||
this.handleExitRTS = this.handleExitRTS.bind(this);
|
||||
}
|
||||
|
||||
async enterFullscreen() {
|
||||
@@ -73,8 +90,8 @@ export class RTSUIController {
|
||||
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';
|
||||
const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-dropdown-preset-select'));
|
||||
const selectedPreset = (presetSelect && presetSelect.value) ? presetSelect.value : '/scripts/extensions/rts-mode/presets/zoo_escape.json';
|
||||
await PresetManager.loadPreset(selectedPreset);
|
||||
GameStateManager.reset();
|
||||
const preset = PresetManager.getPreset();
|
||||
@@ -111,6 +128,22 @@ export class RTSUIController {
|
||||
document.getElementById('rts-execute-btn')?.addEventListener('click', this.handleExecuteCommand);
|
||||
document.getElementById('rts-clear-btn')?.addEventListener('click', this.handleClearCommand);
|
||||
|
||||
// Header Controls
|
||||
document.getElementById('rts-settings-btn')?.addEventListener('click', this.handleSettingsToggle);
|
||||
document.getElementById('rts-exit-btn')?.addEventListener('click', this.handleExitRTS);
|
||||
|
||||
// Settings Dropdown Controls
|
||||
document.getElementById('rts-close-settings')?.addEventListener('click', this.handleCloseSettings);
|
||||
document.getElementById('rts-restart-game')?.addEventListener('click', this.handleRestartGame);
|
||||
|
||||
// Settings Toggle Listeners
|
||||
document.getElementById('rts-structured-output-toggle')?.addEventListener('change', this.handleSettingChange);
|
||||
document.getElementById('rts-content-filter-toggle')?.addEventListener('change', this.handleSettingChange);
|
||||
document.getElementById('rts-dropdown-structured-output')?.addEventListener('change', this.handleSettingChange);
|
||||
document.getElementById('rts-dropdown-content-filter')?.addEventListener('change', this.handleSettingChange);
|
||||
document.getElementById('rts-dropdown-auto-schema')?.addEventListener('change', this.handleSettingChange);
|
||||
document.getElementById('rts-dropdown-preset-select')?.addEventListener('change', this.handleSettingChange);
|
||||
|
||||
// Map Controls
|
||||
document.getElementById('rts-zoom-in')?.addEventListener('click', () => this.handleMapControls('zoomIn'));
|
||||
document.getElementById('rts-zoom-out')?.addEventListener('click', () => this.handleMapControls('zoomOut'));
|
||||
@@ -127,6 +160,16 @@ export class RTSUIController {
|
||||
});
|
||||
}
|
||||
|
||||
// Click outside to close settings dropdown
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('rts-settings-dropdown');
|
||||
const settingsBtn = document.getElementById('rts-settings-btn');
|
||||
if (dropdown && settingsBtn && dropdown.style.display === 'block' &&
|
||||
!dropdown.contains(/** @type {Node} */ (e.target)) && !settingsBtn.contains(/** @type {Node} */ (e.target))) {
|
||||
this.closeSettingsDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Narrative and Zone Listeners
|
||||
document.addEventListener('rts-narrative-update', this.handleNarrativeUpdate);
|
||||
document.addEventListener('rts-zone-changed', this.handleZoneChange);
|
||||
@@ -186,12 +229,19 @@ export class RTSUIController {
|
||||
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' }
|
||||
];
|
||||
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');
|
||||
@@ -263,9 +313,15 @@ export class RTSUIController {
|
||||
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;
|
||||
case 'zoomIn':
|
||||
this.mapCanvas.zoomIn();
|
||||
break;
|
||||
case 'zoomOut':
|
||||
this.mapCanvas.zoomOut();
|
||||
break;
|
||||
case 'center':
|
||||
this.mapCanvas.centerMap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +363,10 @@ export class RTSUIController {
|
||||
|
||||
handleEntitySelection(entity) {
|
||||
this.mapCanvas.clearSelection();
|
||||
this.mapCanvas.selectElement({ type: 'entity', data: entity });
|
||||
this.mapCanvas.selectElement({
|
||||
type: 'entity',
|
||||
data: entity
|
||||
});
|
||||
this.updateEscapedAnimalsDisplay();
|
||||
}
|
||||
|
||||
@@ -429,7 +488,10 @@ export class RTSUIController {
|
||||
calculateEscapeChance(person) {
|
||||
const state = GameStateManager.getState();
|
||||
const threatLevel = state.threatLevel || 'none';
|
||||
const playerPos = state.mapState?.playerPosition || { x: 0, y: 0 };
|
||||
const playerPos = state.mapState?.playerPosition || {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
// Calculate distance from player
|
||||
const distance = Math.sqrt(
|
||||
@@ -440,12 +502,23 @@ export class RTSUIController {
|
||||
// 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;
|
||||
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)
|
||||
@@ -462,13 +535,25 @@ export class RTSUIController {
|
||||
|
||||
// Determine level and text
|
||||
if (escapeScore >= 80) {
|
||||
return { level: 'high', text: 'Safe' };
|
||||
return {
|
||||
level: 'high',
|
||||
text: 'Safe'
|
||||
};
|
||||
} else if (escapeScore >= 60) {
|
||||
return { level: 'medium', text: 'At Risk' };
|
||||
return {
|
||||
level: 'medium',
|
||||
text: 'At Risk'
|
||||
};
|
||||
} else if (escapeScore >= 30) {
|
||||
return { level: 'low', text: 'Danger' };
|
||||
return {
|
||||
level: 'low',
|
||||
text: 'Danger'
|
||||
};
|
||||
} else {
|
||||
return { level: 'critical', text: 'Critical' };
|
||||
return {
|
||||
level: 'critical',
|
||||
text: 'Critical'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +573,11 @@ export class RTSUIController {
|
||||
}
|
||||
|
||||
handleNarrativeUpdate(event) {
|
||||
const { type, message, entityUpdates } = event.detail;
|
||||
const {
|
||||
type,
|
||||
message,
|
||||
entityUpdates
|
||||
} = event.detail;
|
||||
this.addLogEntry(type, message);
|
||||
|
||||
// Handle entity updates and refresh map
|
||||
@@ -563,6 +652,139 @@ async loadPreset(presetPath) {
|
||||
await this.enterFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// Settings Dropdown Methods
|
||||
handleSettingsToggle() {
|
||||
const dropdown = document.getElementById('rts-settings-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
if (dropdown.style.display === 'none' || !dropdown.style.display) {
|
||||
this.openSettingsDropdown();
|
||||
} else {
|
||||
this.closeSettingsDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
openSettingsDropdown() {
|
||||
const dropdown = document.getElementById('rts-settings-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
// Sync dropdown values with current settings
|
||||
this.syncDropdownSettings();
|
||||
dropdown.style.display = 'block';
|
||||
console.log('RTS: Settings dropdown opened');
|
||||
}
|
||||
|
||||
closeSettingsDropdown() {
|
||||
const dropdown = document.getElementById('rts-settings-dropdown');
|
||||
if (dropdown) dropdown.style.display = 'none';
|
||||
}
|
||||
|
||||
syncDropdownSettings() {
|
||||
try {
|
||||
const rtsSettings = getRTSSettings();
|
||||
|
||||
// Sync structured output setting
|
||||
const structuredOutputToggle = /** @type {HTMLInputElement} */ (document.getElementById('rts-dropdown-structured-output'));
|
||||
if (structuredOutputToggle) {
|
||||
structuredOutputToggle.checked = rtsSettings.structuredOutput || false;
|
||||
}
|
||||
|
||||
// Sync content filter setting
|
||||
const dropdownContentFilter = /** @type {HTMLInputElement} */ (document.getElementById('rts-dropdown-content-filter'));
|
||||
if (dropdownContentFilter) {
|
||||
dropdownContentFilter.checked = getRTSSettings().contentFilter;
|
||||
}
|
||||
|
||||
// Sync auto-schema setting
|
||||
const autoSchemaToggle = /** @type {HTMLInputElement} */ (document.getElementById('rts-dropdown-auto-schema'));
|
||||
if (autoSchemaToggle) {
|
||||
autoSchemaToggle.checked = rtsSettings.autoSchemaUpdates !== false;
|
||||
}
|
||||
|
||||
// Sync preset selection
|
||||
const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-preset-select'));
|
||||
const dropdownPresetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-dropdown-preset-select'));
|
||||
if (presetSelect && dropdownPresetSelect) {
|
||||
dropdownPresetSelect.value = presetSelect.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('RTS: Could not sync settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
handleSettingChange(event) {
|
||||
const setting = event.target.id;
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
|
||||
console.log('RTS: Setting changed:', setting, value);
|
||||
|
||||
switch (setting) {
|
||||
case 'rts-structured-output-toggle':
|
||||
case 'rts-dropdown-structured-output':
|
||||
this.updateStructuredOutputSetting(value);
|
||||
this.addLogEntry('system', `Structured Output ${value ? 'enabled' : 'disabled'}`);
|
||||
break;
|
||||
case 'rts-content-filter-toggle':
|
||||
case 'rts-dropdown-content-filter':
|
||||
this.updateContentFilterSetting(value);
|
||||
this.addLogEntry('system', `Explicit Content ${value ? 'enabled' : 'disabled'}`);
|
||||
break;
|
||||
|
||||
case 'rts-dropdown-auto-schema':
|
||||
onAutoSchemaToggle(value);
|
||||
this.addLogEntry('system', `Auto Schema Updates ${value ? 'enabled' : 'disabled'}`);
|
||||
break;
|
||||
|
||||
case 'rts-dropdown-preset-select':
|
||||
this.addLogEntry('system', `Preset will change to: ${value.split('/').pop().replace('.json', '')}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateStructuredOutputSetting(enabled) {
|
||||
// Update the original toggle
|
||||
const originalToggle = /** @type {HTMLInputElement} */ (document.getElementById('rts-structured-output-toggle'));
|
||||
if (originalToggle) {
|
||||
originalToggle.checked = enabled;
|
||||
// Directly call the settings update function
|
||||
onStructuredOutputToggle({ target: originalToggle });
|
||||
}
|
||||
}
|
||||
|
||||
updateContentFilterSetting(enabled) {
|
||||
// Update the original toggle
|
||||
const originalToggle = /** @type {HTMLInputElement} */ (document.getElementById('rts-content-filter-toggle'));
|
||||
if (originalToggle) {
|
||||
originalToggle.checked = enabled;
|
||||
originalToggle.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
|
||||
async handleRestartGame() {
|
||||
const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-dropdown-preset-select'));
|
||||
const selectedPreset = presetSelect ? presetSelect.value : '/scripts/extensions/rts-mode/presets/zoo_escape.json';
|
||||
|
||||
this.addLogEntry('system', 'Restarting game with selected preset...');
|
||||
|
||||
try {
|
||||
await this.loadPreset(selectedPreset);
|
||||
this.addLogEntry('system', 'Game restarted successfully!');
|
||||
this.closeSettingsDropdown();
|
||||
} catch (error) {
|
||||
console.error('RTS: Error restarting game:', error);
|
||||
this.addLogEntry('error', `Failed to restart game: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseSettings() {
|
||||
this.closeSettingsDropdown();
|
||||
this.addLogEntry('system', 'Settings applied');
|
||||
}
|
||||
|
||||
async handleExitRTS() {
|
||||
await this.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
export const rtsUI = new RTSUIController();
|
||||
|
||||
Reference in New Issue
Block a user