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