diff --git a/button.html b/button.html
new file mode 100644
index 0000000..4d12976
--- /dev/null
+++ b/button.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/default-ui.png b/default-ui.png
new file mode 100644
index 0000000..e610f99
Binary files /dev/null and b/default-ui.png differ
diff --git a/html/rts-ui.html b/html/rts-ui.html
new file mode 100644
index 0000000..4417f46
--- /dev/null
+++ b/html/rts-ui.html
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Resources
+
+
+
+ Gold: 100
+
+
+
+ Wood: 50
+
+
+
+ Stone: 25
+
+
+
+ Food: 75
+
+
+
+
+
+
+
Army
+
+
+
+ Warriors: 5
+
+
+
+ Archers: 3
+
+
+
+ Cavalry: 2
+
+
+
+
+
+
+
Buildings
+
+
+
+ Town Hall: Level 1
+
+
+
+ Barracks: 1
+
+
+
+ Storage: 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/html/settings.html b/html/settings.html
new file mode 100644
index 0000000..35ae775
--- /dev/null
+++ b/html/settings.html
@@ -0,0 +1,9 @@
+
+
+
+ RTS Chat Mode Settings
+
+
+ RTS Chat Mode Settings
+
+
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..d615ecd
--- /dev/null
+++ b/index.js
@@ -0,0 +1,170 @@
+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 { 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';
+
+console.log('RTS-MODE: All imports successful');
+
+const extensionId = 'rts-mode';
+const extensionName = 'RTS Chat Mode';
+
+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');
+ }
+}
+
+function onRtsStartCommand() {
+ console.log('RTS Start command executed.');
+ GameStateManager.reset();
+
+ // If RTS UI is active, update it
+ if (rtsUI.isActive()) {
+ rtsUI.updateUI();
+ rtsUI.addLogEntry('system', 'Game state reset. New campaign begins!');
+ }
+
+ return '';
+}
+
+function onRtsCmdCommand(args, value) {
+ console.log('RTS Command executed with args:', args, 'value:', value);
+ if (value) {
+ // If RTS UI is active, add to log
+ if (rtsUI.isActive()) {
+ rtsUI.addLogEntry('action', value);
+ }
+
+ sendTurn(value);
+ }
+ return '';
+}
+
+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...');
+
+ await addTopBarButton();
+
+ // 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.',
+ }));
+
+ console.log('RTS Chat Mode: Extension initialized successfully');
+});
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..e43db45
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,11 @@
+{
+ "display_name": "RTS Chat Mode",
+ "loading_order": 10,
+ "requires": [],
+ "optional": [],
+ "js": "index.js",
+ "css": "style.css",
+ "author": "Community",
+ "version": "0.0.1",
+ "homePage": ""
+}
\ No newline at end of file
diff --git a/rts-ui.html b/rts-ui.html
new file mode 100644
index 0000000..4417f46
--- /dev/null
+++ b/rts-ui.html
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Resources
+
+
+
+ Gold: 100
+
+
+
+ Wood: 50
+
+
+
+ Stone: 25
+
+
+
+ Food: 75
+
+
+
+
+
+
+
Army
+
+
+
+ Warriors: 5
+
+
+
+ Archers: 3
+
+
+
+ Cavalry: 2
+
+
+
+
+
+
+
Buildings
+
+
+
+ Town Hall: Level 1
+
+
+
+ Barracks: 1
+
+
+
+ Storage: 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rts-ui.png b/rts-ui.png
new file mode 100644
index 0000000..77bdae2
Binary files /dev/null and b/rts-ui.png differ
diff --git a/src/GameStateManager.js b/src/GameStateManager.js
new file mode 100644
index 0000000..8bd4705
--- /dev/null
+++ b/src/GameStateManager.js
@@ -0,0 +1,36 @@
+const defaultState = {
+ turn: 1,
+ map: [],
+ units: [],
+ resources: { gold: 0, wood: 0 },
+ log: [],
+};
+
+class GameStateManager {
+ constructor() {
+ this.state = { ...defaultState };
+ }
+
+ getState() {
+ return this.state;
+ }
+
+ applyDiff(diff) {
+ // For now, just shallow merge the diff.
+ Object.assign(this.state, diff);
+ this.state.log.push({ turn: this.state.turn, diff });
+ console.log('RTS Game State Updated:', this.state);
+ }
+
+ reset() {
+ this.state = { ...defaultState, log: [] };
+ console.log('RTS Game State Reset.');
+ }
+
+ toCompressedJSON() {
+ return JSON.stringify(this.state);
+ }
+}
+
+const instance = new GameStateManager();
+export default instance;
\ No newline at end of file
diff --git a/src/LLMAdapter.js b/src/LLMAdapter.js
new file mode 100644
index 0000000..1c995b5
--- /dev/null
+++ b/src/LLMAdapter.js
@@ -0,0 +1,54 @@
+import GameStateManager from './GameStateManager.js';
+import { buildPrompt } from './PromptCompressor.js';
+
+/**
+ * Extracts the first JSON code block from a string.
+ * @param {string} text The text to search.
+ * @returns {object|null} The parsed JSON object or null if not found.
+ */
+function extractJson(text) {
+ const match = /```json\n([\s\S]+?)\n```/.exec(text);
+ if (match && match[1]) {
+ try {
+ return JSON.parse(match[1]);
+ } catch (error) {
+ console.error('RTS-Mode: Failed to parse JSON from LLM response.', error);
+ return null;
+ }
+ }
+ return null;
+}
+
+/**
+ * Sends the user's command to the LLM and processes the turn.
+ * @param {string} userCmd The user's command.
+ */
+export async function sendTurn(userCmd) {
+ try {
+ const stateJSON = GameStateManager.toCompressedJSON();
+ const prompt = buildPrompt(stateJSON, userCmd);
+
+ // Using SillyTavern's built-in LLM broker
+ const reply = await window.LLMBroker.generate(prompt);
+
+ if (!reply) {
+ throw new Error('LLM returned an empty response.');
+ }
+
+ const diff = extractJson(reply);
+ if (diff) {
+ GameStateManager.applyDiff(diff);
+ // For now, log the narrative part to the console.
+ const narrative = reply.replace(/```json\n[\s\S]+?\n```/, '').trim();
+ console.log('RTS Narrative:', narrative);
+ } else {
+ console.warn('RTS-Mode: No valid JSON diff found in LLM response.');
+ // Log the raw reply for debugging.
+ console.log('Raw LLM Response:', reply);
+ }
+ } catch (error) {
+ console.error('RTS-Mode: Error during sendTurn:', error);
+ // Optionally, display an alert to the user.
+ alert(`RTS-Mode Error: ${error.message}`);
+ }
+}
\ No newline at end of file
diff --git a/src/PromptCompressor.js b/src/PromptCompressor.js
new file mode 100644
index 0000000..d789d94
--- /dev/null
+++ b/src/PromptCompressor.js
@@ -0,0 +1,9 @@
+/**
+ * Builds a prompt for the LLM based on the current game state and user command.
+ * @param {string} stateJSON - The compressed JSON string of the game state.
+ * @param {string} userCmd - The user's command.
+ * @returns {string} The formatted prompt.
+ */
+export function buildPrompt(stateJSON, userCmd) {
+ return `${stateJSON} \n${userCmd} \nRespond with JSON diff + narrative.`;
+}
\ No newline at end of file
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..0a4590f
--- /dev/null
+++ b/style.css
@@ -0,0 +1,336 @@
+/* RTS Mode Full UI Styles */
+
+#rts-main-container {
+ position: fixed;
+ top: var(--topBarBlockSize);
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: inherit;
+ color: inherit;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+}
+
+#rts-header {
+ height: 20px;
+ background: var(--SmartThemeBlurTintColor);
+ cursor: move;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-bottom: 1px solid var(--SmartThemeBorderColor);
+}
+
+#rts-game-area {
+ flex: 1;
+ min-height: 0;
+ gap: 0;
+ padding: 0;
+}
+
+/* Side Panels - Match SillyTavern's panel styling */
+.rts-panel {
+ width: calc((100vw - var(--sheldWidth) - 2px) / 2);
+ width: calc((100dvw - var(--sheldWidth) - 2px) / 2);
+ min-width: 280px;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ padding: 0;
+ min-height: 0;
+ position: relative;
+ overflow-y: auto;
+ max-height: calc(100vh - var(--topBarBlockSize) - 20px);
+ max-height: calc(100dvh - var(--topBarBlockSize) - 20px);
+}
+
+.rts-panel-header {
+ background: var(--SmartThemeBlurTintColor);
+ padding: 12px;
+ border-bottom: 1px solid var(--SmartThemeBorderColor);
+ border-radius: 6px 6px 0 0;
+}
+
+.rts-panel-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.rts-section {
+ padding: 15px;
+ margin: 10px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ backdrop-filter: blur(10px);
+}
+
+.rts-section:last-child {
+ border-bottom: 1px solid var(--SmartThemeBorderColor);
+}
+
+.rts-section-title {
+ margin: 0 0 10px 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: inherit;
+ opacity: 0.9;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Resources */
+.rts-resources-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.rts-resource-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ font-size: 12px;
+ color: inherit;
+}
+
+.rts-resource-item i {
+ opacity: 0.8;
+ width: 16px;
+}
+
+/* Units and Buildings */
+.rts-units-list, .rts-buildings-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.rts-unit-item, .rts-building-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ font-size: 12px;
+ color: inherit;
+}
+
+.rts-unit-item i, .rts-building-item i {
+ opacity: 0.8;
+ width: 16px;
+}
+
+/* Map Container - Match SillyTavern's main content area */
+#rts-map-container {
+ background: var(--SmartThemeBodyColor);
+ border: none;
+ border-radius: 0;
+ min-height: 0;
+ width: var(--sheldWidth);
+ margin: 0 auto;
+ position: relative;
+}
+
+.rts-map-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px;
+ margin: 10px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ backdrop-filter: blur(10px);
+}
+
+.rts-map-header h3 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: inherit;
+ opacity: 0.9;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.rts-map-controls {
+ display: flex;
+ gap: 4px;
+}
+
+.rts-map-wrapper {
+ position: relative;
+ background: var(--SmartThemeBodyColor);
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#rts-game-map {
+ border: 1px solid var(--SmartThemeBorderColor);
+ background: #1a2332;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+.rts-map-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+}
+
+.rts-map-footer {
+ padding: 15px;
+ margin: 10px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ backdrop-filter: blur(10px);
+ display: flex;
+ justify-content: center;
+}
+
+.rts-turn-info {
+ display: flex;
+ gap: 12px;
+ font-size: 12px;
+ color: var(--SmartThemeQuoteColor);
+}
+
+.rts-separator {
+ opacity: 0.5;
+}
+
+/* Quick Actions */
+.rts-quick-actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+}
+
+.rts-quick-actions .menu_button {
+ font-size: 11px;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ text-align: center;
+}
+
+.rts-quick-actions .menu_button i {
+ font-size: 14px;
+}
+
+/* Command Input */
+.rts-command-input-area {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ height: 100%;
+}
+
+#rts-command-input {
+ flex: 1;
+ resize: none;
+ min-height: 80px;
+}
+
+.rts-command-controls {
+ display: flex;
+ gap: 6px;
+}
+
+#rts-execute-btn {
+ flex: 1;
+}
+
+/* Game Log */
+.rts-game-log {
+ max-height: 200px;
+ overflow-y: auto;
+ background: rgba(0, 0, 0, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ padding: 8px;
+}
+
+.rts-log-entry {
+ margin-bottom: 8px;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+.rts-log-entry:last-child {
+ margin-bottom: 0;
+}
+
+.rts-log-timestamp {
+ color: var(--SmartThemeQuoteColor);
+ opacity: 0.7;
+ font-weight: 500;
+}
+
+.rts-log-message {
+ margin-left: 8px;
+}
+
+.rts-log-system .rts-log-message {
+ color: var(--SmartThemeFastUIBGColor);
+}
+
+.rts-log-action .rts-log-message {
+ color: var(--SmartThemeQuoteColor);
+}
+
+.rts-log-result .rts-log-message {
+ color: #4CAF50;
+}
+
+.rts-log-error .rts-log-message {
+ color: #f44336;
+}
+
+/* Legacy small UI styles for when RTS UI is not full-screen */
+#rts-mode-root {
+ padding: 8px;
+ background: var(--SmartThemeBodyColor);
+ color: var(--SmartThemeQuoteColor);
+ display: flex;
+ gap: 12px;
+}
+
+#rts-map-canvas {
+ border: 1px solid var(--SmartThemeBorderColor);
+ max-width: 100%;
+ max-height: 100%;
+}
+
+#rts-resource-panel {
+ width: 200px;
+ background: var(--SmartThemeEmColor);
+ padding: 8px;
+ border: 1px solid var(--SmartThemeBorderColor);
+}
+
+#rts-resource-panel ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#rts-resource-panel li {
+ margin-bottom: 4px;
+}
\ No newline at end of file
diff --git a/ui/MapCanvas.js b/ui/MapCanvas.js
new file mode 100644
index 0000000..cf4485f
--- /dev/null
+++ b/ui/MapCanvas.js
@@ -0,0 +1,318 @@
+/**
+ * Enhanced map canvas for RTS mode with terrain, units, and buildings
+ */
+export class RTSMapCanvas {
+ constructor(canvasId = 'rts-game-map') {
+ this.canvas = document.getElementById(canvasId);
+ if (!this.canvas) {
+ this.canvas = document.createElement('canvas');
+ this.canvas.id = canvasId;
+ }
+
+ this.ctx = this.canvas.getContext('2d');
+ this.zoom = 1;
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.selectedUnit = null;
+ this.selectedBuilding = null;
+
+ this.initializeCanvas();
+ this.setupEventListeners();
+ }
+
+ initializeCanvas() {
+ this.canvas.width = 800;
+ this.canvas.height = 600;
+ this.drawTerrain();
+ this.drawGrid();
+ }
+
+ setupEventListeners() {
+ this.canvas.addEventListener('click', (e) => this.handleClick(e));
+ this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
+ }
+
+ drawTerrain() {
+ const ctx = this.ctx;
+
+ // Clear canvas
+ ctx.fillStyle = '#1a2332';
+ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ // Draw terrain features
+ this.drawForests();
+ this.drawMountains();
+ this.drawRivers();
+ this.drawSettlement();
+ }
+
+ drawGrid() {
+ const ctx = this.ctx;
+ ctx.strokeStyle = '#334155';
+ ctx.lineWidth = 0.5;
+ ctx.globalAlpha = 0.3;
+
+ const gridSize = 40;
+
+ // Vertical lines
+ for (let x = 0; x <= this.canvas.width; x += gridSize) {
+ ctx.beginPath();
+ ctx.moveTo(x, 0);
+ ctx.lineTo(x, this.canvas.height);
+ ctx.stroke();
+ }
+
+ // Horizontal lines
+ for (let y = 0; y <= this.canvas.height; y += gridSize) {
+ ctx.beginPath();
+ ctx.moveTo(0, y);
+ ctx.lineTo(this.canvas.width, y);
+ ctx.stroke();
+ }
+
+ ctx.globalAlpha = 1;
+ }
+
+ drawForests() {
+ const ctx = this.ctx;
+ ctx.fillStyle = '#22c55e';
+
+ // Forest patches
+ const forests = [
+ { x: 100, y: 100, radius: 60 },
+ { x: 600, y: 150, radius: 80 },
+ { x: 200, y: 400, radius: 50 },
+ { x: 650, y: 450, radius: 70 }
+ ];
+
+ forests.forEach(forest => {
+ ctx.beginPath();
+ ctx.arc(forest.x, forest.y, forest.radius, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Add tree symbols
+ for (let i = 0; i < 8; i++) {
+ const angle = (i / 8) * Math.PI * 2;
+ const treeX = forest.x + Math.cos(angle) * (forest.radius * 0.7);
+ const treeY = forest.y + Math.sin(angle) * (forest.radius * 0.7);
+
+ ctx.fillStyle = '#16a34a';
+ ctx.fillRect(treeX - 2, treeY - 2, 4, 4);
+ }
+ ctx.fillStyle = '#22c55e';
+ });
+ }
+
+ drawMountains() {
+ const ctx = this.ctx;
+ ctx.fillStyle = '#64748b';
+
+ const mountains = [
+ { x: 300, y: 80, width: 100, height: 80 },
+ { x: 500, y: 300, width: 120, height: 100 }
+ ];
+
+ mountains.forEach(mountain => {
+ ctx.beginPath();
+ ctx.moveTo(mountain.x, mountain.y + mountain.height);
+ ctx.lineTo(mountain.x + mountain.width / 2, mountain.y);
+ ctx.lineTo(mountain.x + mountain.width, mountain.y + mountain.height);
+ ctx.closePath();
+ ctx.fill();
+ });
+ }
+
+ drawRivers() {
+ const ctx = this.ctx;
+ ctx.strokeStyle = '#3b82f6';
+ ctx.lineWidth = 8;
+
+ ctx.beginPath();
+ ctx.moveTo(0, 200);
+ ctx.quadraticCurveTo(200, 180, 400, 220);
+ ctx.quadraticCurveTo(600, 260, 800, 240);
+ ctx.stroke();
+ }
+
+ drawSettlement() {
+ const ctx = this.ctx;
+ const centerX = 400;
+ const centerY = 350;
+
+ // Town hall (main building)
+ ctx.fillStyle = '#8b5cf6';
+ ctx.fillRect(centerX - 20, centerY - 20, 40, 40);
+
+ // Surrounding buildings
+ const buildings = [
+ { x: centerX - 60, y: centerY - 10, type: 'barracks', color: '#ef4444' },
+ { x: centerX + 40, y: centerY - 10, type: 'storage', color: '#f59e0b' },
+ { x: centerX - 10, y: centerY - 60, type: 'farm', color: '#22c55e' },
+ { x: centerX - 10, y: centerY + 40, type: 'market', color: '#3b82f6' }
+ ];
+
+ buildings.forEach(building => {
+ ctx.fillStyle = building.color;
+ ctx.fillRect(building.x - 15, building.y - 15, 30, 30);
+ });
+
+ // Draw units around settlement
+ this.drawUnits();
+ }
+
+ drawUnits() {
+ const ctx = this.ctx;
+ const centerX = 400;
+ const centerY = 350;
+
+ const units = [
+ { x: centerX - 80, y: centerY + 20, type: 'warrior' },
+ { x: centerX - 70, y: centerY + 35, type: 'warrior' },
+ { x: centerX + 60, y: centerY + 25, type: 'archer' },
+ { x: centerX + 20, y: centerY - 80, type: 'cavalry' }
+ ];
+
+ units.forEach(unit => {
+ if (unit.type === 'warrior') {
+ ctx.fillStyle = '#dc2626';
+ ctx.beginPath();
+ ctx.arc(unit.x, unit.y, 6, 0, Math.PI * 2);
+ ctx.fill();
+ } else if (unit.type === 'archer') {
+ ctx.fillStyle = '#059669';
+ ctx.fillRect(unit.x - 5, unit.y - 5, 10, 10);
+ } else if (unit.type === 'cavalry') {
+ ctx.fillStyle = '#7c3aed';
+ ctx.beginPath();
+ ctx.moveTo(unit.x, unit.y - 8);
+ ctx.lineTo(unit.x - 6, unit.y + 4);
+ ctx.lineTo(unit.x + 6, unit.y + 4);
+ ctx.closePath();
+ ctx.fill();
+ }
+ });
+ }
+
+ handleClick(e) {
+ const rect = this.canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ console.log(`Map clicked at: ${x}, ${y}`);
+
+ // Check if clicked on a unit or building
+ // This is a simplified example - in a real game you'd have proper hit detection
+ this.showMapInfo(x, y);
+ }
+
+ handleMouseMove(e) {
+ const rect = this.canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ // Update cursor based on what's under it
+ this.canvas.style.cursor = 'default';
+
+ // Check for interactive elements
+ if (this.isNearSettlement(x, y)) {
+ this.canvas.style.cursor = 'pointer';
+ }
+ }
+
+ isNearSettlement(x, y) {
+ const centerX = 400;
+ const centerY = 350;
+ const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
+ return distance < 100;
+ }
+
+ showMapInfo(x, y) {
+ const overlay = document.getElementById('rts-map-overlay');
+ if (overlay) {
+ if (this.isNearSettlement(x, y)) {
+ overlay.innerHTML = `
+
+ Main Settlement
+ Population: 150
+ Defense: High
+ Close
+
+ `;
+ } else {
+ overlay.innerHTML = '';
+ }
+ }
+ }
+
+ zoomIn() {
+ this.zoom = Math.min(this.zoom * 1.2, 3);
+ this.redraw();
+ }
+
+ zoomOut() {
+ this.zoom = Math.max(this.zoom / 1.2, 0.5);
+ this.redraw();
+ }
+
+ centerMap() {
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.zoom = 1;
+ this.redraw();
+ }
+
+ redraw() {
+ this.ctx.save();
+ this.ctx.scale(this.zoom, this.zoom);
+ this.ctx.translate(this.offsetX, this.offsetY);
+
+ this.drawTerrain();
+ this.drawGrid();
+
+ this.ctx.restore();
+ }
+
+ updateFromGameState(gameState) {
+ // Update the map based on game state changes
+ console.log('Updating map from game state:', gameState);
+ this.redraw();
+ }
+}
+
+/**
+ * Legacy function for backward compatibility
+ * @param {HTMLElement} rootEl - The root element to append to (optional).
+ * @returns {HTMLCanvasElement}
+ */
+export function createMapCanvas(rootEl) {
+ const canvas = document.createElement('canvas');
+ canvas.id = 'rts-map-canvas';
+ canvas.width = 640;
+ canvas.height = 640;
+
+ const ctx = canvas.getContext('2d');
+ ctx.strokeStyle = '#444';
+ ctx.lineWidth = 1;
+
+ for (let i = 0; i <= canvas.width; i += 40) {
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, canvas.height);
+ ctx.stroke();
+ }
+
+ for (let i = 0; i <= canvas.height; i += 40) {
+ ctx.beginPath();
+ ctx.moveTo(0, i);
+ ctx.lineTo(canvas.width, i);
+ ctx.stroke();
+ }
+
+ if (rootEl) {
+ rootEl.appendChild(canvas);
+ }
+
+ return canvas;
+}
\ No newline at end of file
diff --git a/ui/RTSUIController.js b/ui/RTSUIController.js
new file mode 100644
index 0000000..34db371
--- /dev/null
+++ b/ui/RTSUIController.js
@@ -0,0 +1,378 @@
+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 = ' 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 = ' 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 = `
+ [Turn ${turn}]
+ ${message}
+ `;
+
+ 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();
\ No newline at end of file
diff --git a/ui/ResourcePanel.js b/ui/ResourcePanel.js
new file mode 100644
index 0000000..4fde5e4
--- /dev/null
+++ b/ui/ResourcePanel.js
@@ -0,0 +1,24 @@
+/**
+ * Creates and returns the resource panel element.
+ * @param {HTMLElement} rootEl - The root element to append to (optional).
+ * @returns {HTMLDivElement}
+ */
+export function createResourcePanel(rootEl) {
+ const panel = document.createElement('div');
+ panel.id = 'rts-resource-panel';
+
+ const list = document.createElement('ul');
+ list.innerHTML = `
+ Gold: 0
+ Wood: 0
+ Units: 0
+ `;
+
+ panel.appendChild(list);
+
+ if (rootEl) {
+ rootEl.appendChild(panel);
+ }
+
+ return panel;
+}
\ No newline at end of file