init
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<div id="rts-mode-button" class="menu_button menu_button_icon" title="Toggle RTS UI" data-i18n="[title]Toggle RTS UI">
|
||||
<i class="fa-solid fa-chess-board fa-fw"></i>
|
||||
</div>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 567 KiB |
@@ -0,0 +1,162 @@
|
||||
<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>
|
||||
|
||||
<!-- Resources Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Resources</h4>
|
||||
<div id="rts-resources" class="rts-resources-grid">
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
<span>Gold: <span id="rts-gold">100</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-tree"></i>
|
||||
<span>Wood: <span id="rts-wood">50</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-mountain"></i>
|
||||
<span>Stone: <span id="rts-stone">25</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-seedling"></i>
|
||||
<span>Food: <span id="rts-food">75</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Units Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Army</h4>
|
||||
<div id="rts-units" class="rts-units-list">
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
<span>Warriors: <span id="rts-warriors">5</span></span>
|
||||
</div>
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-bow-and-arrow"></i>
|
||||
<span>Archers: <span id="rts-archers">3</span></span>
|
||||
</div>
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-horse"></i>
|
||||
<span>Cavalry: <span id="rts-cavalry">2</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buildings Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Buildings</h4>
|
||||
<div id="rts-buildings" class="rts-buildings-list">
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-home"></i>
|
||||
<span>Town Hall: Level <span id="rts-town-hall">1</span></span>
|
||||
</div>
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-hammer"></i>
|
||||
<span>Barracks: <span id="rts-barracks">1</span></span>
|
||||
</div>
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-warehouse"></i>
|
||||
<span>Storage: <span id="rts-storage">1</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 id="rts-map-overlay" class="rts-map-overlay">
|
||||
<!-- Selected unit/building info will appear here -->
|
||||
</div>
|
||||
</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 class="rts-quick-actions">
|
||||
<button id="rts-build-btn" class="menu_button" title="Build Structure">
|
||||
<i class="fa-solid fa-hammer"></i> Build
|
||||
</button>
|
||||
<button id="rts-recruit-btn" class="menu_button" title="Recruit Units">
|
||||
<i class="fa-solid fa-user-plus"></i> Recruit
|
||||
</button>
|
||||
<button id="rts-explore-btn" class="menu_button" title="Explore Territory">
|
||||
<i class="fa-solid fa-compass"></i> Explore
|
||||
</button>
|
||||
<button id="rts-trade-btn" class="menu_button" title="Trade Resources">
|
||||
<i class="fa-solid fa-handshake"></i> Trade
|
||||
</button>
|
||||
</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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>RTS Chat Mode Settings</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>RTS Chat Mode Settings</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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": ""
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<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>
|
||||
|
||||
<!-- Resources Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Resources</h4>
|
||||
<div id="rts-resources" class="rts-resources-grid">
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
<span>Gold: <span id="rts-gold">100</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-tree"></i>
|
||||
<span>Wood: <span id="rts-wood">50</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-mountain"></i>
|
||||
<span>Stone: <span id="rts-stone">25</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-seedling"></i>
|
||||
<span>Food: <span id="rts-food">75</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Units Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Army</h4>
|
||||
<div id="rts-units" class="rts-units-list">
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
<span>Warriors: <span id="rts-warriors">5</span></span>
|
||||
</div>
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-bow-and-arrow"></i>
|
||||
<span>Archers: <span id="rts-archers">3</span></span>
|
||||
</div>
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-horse"></i>
|
||||
<span>Cavalry: <span id="rts-cavalry">2</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buildings Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Buildings</h4>
|
||||
<div id="rts-buildings" class="rts-buildings-list">
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-home"></i>
|
||||
<span>Town Hall: Level <span id="rts-town-hall">1</span></span>
|
||||
</div>
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-hammer"></i>
|
||||
<span>Barracks: <span id="rts-barracks">1</span></span>
|
||||
</div>
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-warehouse"></i>
|
||||
<span>Storage: <span id="rts-storage">1</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 id="rts-map-overlay" class="rts-map-overlay">
|
||||
<!-- Selected unit/building info will appear here -->
|
||||
</div>
|
||||
</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 class="rts-quick-actions">
|
||||
<button id="rts-build-btn" class="menu_button" title="Build Structure">
|
||||
<i class="fa-solid fa-hammer"></i> Build
|
||||
</button>
|
||||
<button id="rts-recruit-btn" class="menu_button" title="Recruit Units">
|
||||
<i class="fa-solid fa-user-plus"></i> Recruit
|
||||
</button>
|
||||
<button id="rts-explore-btn" class="menu_button" title="Explore Territory">
|
||||
<i class="fa-solid fa-compass"></i> Explore
|
||||
</button>
|
||||
<button id="rts-trade-btn" class="menu_button" title="Trade Resources">
|
||||
<i class="fa-solid fa-handshake"></i> Trade
|
||||
</button>
|
||||
</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>
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
@@ -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;
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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 `<RTS-STATE>${stateJSON}</RTS-STATE>\n<USER-CMD>${userCmd}</USER-CMD>\nRespond with JSON diff + narrative.`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+318
@@ -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 = `
|
||||
<div style="position: absolute; top: ${y + 10}px; left: ${x + 10}px;
|
||||
background: rgba(0,0,0,0.8); color: white; padding: 8px;
|
||||
border-radius: 4px; font-size: 12px; pointer-events: auto;">
|
||||
<strong>Main Settlement</strong><br>
|
||||
Population: 150<br>
|
||||
Defense: High<br>
|
||||
<button onclick="this.parentElement.remove()" style="margin-top: 4px;">Close</button>
|
||||
</div>
|
||||
`;
|
||||
} 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;
|
||||
}
|
||||
@@ -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 = '<i class="fa-solid fa-spinner fa-spin"></i> Processing...';
|
||||
}
|
||||
|
||||
try {
|
||||
// Send command to LLM
|
||||
await sendTurn(command);
|
||||
|
||||
// The LLMAdapter will handle updating the game state
|
||||
// We'll update the UI when the state changes
|
||||
setTimeout(() => {
|
||||
this.updateUI();
|
||||
this.addLogEntry('result', 'Command executed successfully!');
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to execute RTS command:', error);
|
||||
this.addLogEntry('error', `Failed to execute command: ${error.message}`);
|
||||
} finally {
|
||||
// Restore execute button
|
||||
if (executeBtn) {
|
||||
executeBtn.disabled = false;
|
||||
executeBtn.innerHTML = '<i class="fa-solid fa-play"></i> Execute Turn';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClearCommand() {
|
||||
const commandInput = document.getElementById('rts-command-input');
|
||||
if (commandInput) {
|
||||
commandInput.value = '';
|
||||
commandInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleMapControls(action) {
|
||||
if (!this.mapCanvas) return;
|
||||
|
||||
switch (action) {
|
||||
case 'zoomIn':
|
||||
this.mapCanvas.zoomIn();
|
||||
break;
|
||||
case 'zoomOut':
|
||||
this.mapCanvas.zoomOut();
|
||||
break;
|
||||
case 'center':
|
||||
this.mapCanvas.centerMap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(e) {
|
||||
if (e.key === 'Escape') {
|
||||
this.exitFullscreen();
|
||||
|
||||
// Update button state to match (find the button in DOM)
|
||||
const topButton = document.querySelector('#rts-mode-button');
|
||||
if (topButton) {
|
||||
const toggleBtn = topButton.querySelector('#rts-toggle-ui-btn');
|
||||
if (toggleBtn) {
|
||||
const icon = toggleBtn.querySelector('i');
|
||||
if (icon) {
|
||||
icon.classList.remove('fa-eye-slash');
|
||||
icon.classList.add('fa-eye');
|
||||
}
|
||||
toggleBtn.setAttribute('title', 'Show RTS UI');
|
||||
toggleBtn.setAttribute('data-i18n', '[title]Show RTS UI');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
this.updateResourceDisplay();
|
||||
this.updateUnitsDisplay();
|
||||
this.updateBuildingsDisplay();
|
||||
this.updateTurnInfo();
|
||||
|
||||
if (this.mapCanvas) {
|
||||
this.mapCanvas.updateFromGameState(GameStateManager.getState());
|
||||
}
|
||||
}
|
||||
|
||||
updateResourceDisplay() {
|
||||
const state = GameStateManager.getState();
|
||||
const resources = state.resources || {};
|
||||
|
||||
this.updateElementText('rts-gold', resources.gold || 100);
|
||||
this.updateElementText('rts-wood', resources.wood || 50);
|
||||
this.updateElementText('rts-stone', resources.stone || 25);
|
||||
this.updateElementText('rts-food', resources.food || 75);
|
||||
}
|
||||
|
||||
updateUnitsDisplay() {
|
||||
const state = GameStateManager.getState();
|
||||
const units = state.units || [];
|
||||
|
||||
// Count units by type
|
||||
const unitCounts = units.reduce((counts, unit) => {
|
||||
counts[unit.type] = (counts[unit.type] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
|
||||
this.updateElementText('rts-warriors', unitCounts.warrior || 5);
|
||||
this.updateElementText('rts-archers', unitCounts.archer || 3);
|
||||
this.updateElementText('rts-cavalry', unitCounts.cavalry || 2);
|
||||
}
|
||||
|
||||
updateBuildingsDisplay() {
|
||||
const state = GameStateManager.getState();
|
||||
// For now, use default values - this would be expanded based on game state
|
||||
|
||||
this.updateElementText('rts-town-hall', 1);
|
||||
this.updateElementText('rts-barracks', 1);
|
||||
this.updateElementText('rts-storage', 1);
|
||||
}
|
||||
|
||||
updateTurnInfo() {
|
||||
const state = GameStateManager.getState();
|
||||
this.updateElementText('rts-current-turn', state.turn || 1);
|
||||
|
||||
// Simple season calculation based on turn
|
||||
const seasons = ['Spring', 'Summer', 'Autumn', 'Winter'];
|
||||
const season = seasons[Math.floor((state.turn || 1) / 10) % 4];
|
||||
this.updateElementText('rts-season', season);
|
||||
}
|
||||
|
||||
updateElementText(id, value) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
addLogEntry(type, message) {
|
||||
const gameLog = document.getElementById('rts-game-log');
|
||||
if (!gameLog) return;
|
||||
|
||||
const state = GameStateManager.getState();
|
||||
const turn = state.turn || 1;
|
||||
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `rts-log-entry rts-log-${type}`;
|
||||
logEntry.innerHTML = `
|
||||
<span class="rts-log-timestamp">[Turn ${turn}]</span>
|
||||
<span class="rts-log-message">${message}</span>
|
||||
`;
|
||||
|
||||
gameLog.appendChild(logEntry);
|
||||
gameLog.scrollTop = gameLog.scrollHeight;
|
||||
|
||||
// Keep log manageable size
|
||||
while (gameLog.children.length > 50) {
|
||||
gameLog.removeChild(gameLog.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update button state from external calls
|
||||
updateButtonState(isActive) {
|
||||
// This will be called from the main extension to update button appearance
|
||||
// when the UI state changes
|
||||
console.log('RTS UI state updated:', isActive);
|
||||
}
|
||||
|
||||
// Public methods for external control
|
||||
isActive() {
|
||||
return this.isFullscreen;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isFullscreen) {
|
||||
this.exitFullscreen();
|
||||
} else {
|
||||
this.enterFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
export const rtsUI = new RTSUIController();
|
||||
@@ -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 = `
|
||||
<li>Gold: 0</li>
|
||||
<li>Wood: 0</li>
|
||||
<li>Units: 0</li>
|
||||
`;
|
||||
|
||||
panel.appendChild(list);
|
||||
|
||||
if (rootEl) {
|
||||
rootEl.appendChild(panel);
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
Reference in New Issue
Block a user