This commit is contained in:
2025-08-03 22:35:36 -07:00
parent 8d4b23f7f2
commit d651046f22
23 changed files with 2767 additions and 293 deletions
File diff suppressed because one or more lines are too long
+67
View File
@@ -0,0 +1,67 @@
# RTS Mode: Narrative Engine Guide
The RTS Mode now includes a dynamic narrative engine that allows preset creators to build rich, evolving stories that respond to player actions and the changing game state. This guide explains how to use the new `narrativeEngine` features in your presets.
## Core Concepts
The narrative engine is built around three core concepts:
* **Zones:** These are distinct areas on your map where different types of events can occur. Each zone is linked to a threat table.
* **Threat Tables:** These are collections of narrative events, categorized by threat level (low, medium, high). The engine uses these tables to select appropriate events based on the current situation.
* **Threat Level:** This is a property of the game state that determines which events are selected from the threat tables. The threat level can be changed by game events and player actions.
## Adding the Narrative Engine to Your Preset
To enable the narrative engine, add a `narrativeEngine` object to your preset's JSON file. This object should contain two main properties: `zones` and `threatTables`.
### Defining Zones
The `zones` property is an array of zone objects. Each zone object should have the following properties:
* `id`: A unique identifier for the zone (e.g., "main_gate").
* `name`: A user-friendly name for the zone (e.g., "Main Gate").
* `threatTable`: The ID of the threat table to use for this zone (e.g., "gate_threats").
**Example:**
```json
"zones": [
{ "id": "main_gate", "name": "Main Gate", "threatTable": "gate_threats" },
{ "id": "primate_house", "name": "Primate House", "threatTable": "primate_threats" }
]
```
### Defining Threat Tables
The `threatTables` property is an object that contains one or more threat table objects. Each threat table object should have the following properties:
* `low`: An array of strings, where each string is a narrative event for a low threat level.
* `medium`: An array of strings, where each string is a narrative event for a medium threat level.
* `high`: An array of strings, where each string is a narrative event for a high threat level.
**Example:**
```json
"threatTables": {
"gate_threats": {
"low": [
"A panicked crowd rattles the main gate, their screams echoing.",
"A security guard tries to restore order, but is overwhelmed."
],
"medium": [
"A large animal is ramming the gate, which is starting to buckle under the strain."
],
"high": [
"The main gate is breached, and a horde of panicked people and smaller animals stampedes through."
]
}
}
```
## Content Filtering
The narrative engine supports content filtering for explicit themes. To control the level of violence and sexual content in the narrative, you can use the "Explicit Content" toggle in the RTS Mode settings.
When the toggle is enabled, the engine will request more graphic and intense descriptions from the language model. When disabled, the narrative will be more focused on action and suspense, without the explicit details.
This allows you to create presets that can be enjoyed by a wider audience, while still providing the option for a more intense experience for those who want it.
+5
View File
@@ -76,3 +76,8 @@
border-left: 1px solid black; border-left: 1px solid black;
padding: 10px; padding: 10px;
} }
.rts-animal-item.selected {
background-color: #fbbf24;
color: #1a202c;
}
+53 -60
View File
@@ -10,63 +10,67 @@
<h3>Game Status</h3> <h3>Game Status</h3>
</div> </div>
<!-- Resources Section --> <!-- Status Overview Section -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Resources</h4> <h4 class="rts-section-title">Status Overview</h4>
<div id="rts-resources" class="rts-resources-grid"> <div id="rts-overview-stats">
<div class="rts-resource-item"> <div class="stat-item">Turn: <span id="turn-counter">1</span></div>
<i class="fa-solid fa-coins"></i> <div class="stat-item">Zone: <span id="current-zone">Unknown</span></div>
<span>Gold: <span id="rts-gold">100</span></span> <div class="stat-item">Threat: <span id="threat-level">Low</span></div>
</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>
</div> </div>
<!-- Units Section --> <!-- Casualties Section -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Army</h4> <h4 class="rts-section-title">Casualties</h4>
<div id="rts-units" class="rts-units-list"> <div id="rts-casualties-stats">
<div class="rts-unit-item"> <div class="stat-item">Total Deaths: <span id="total-deaths">0</span></div>
<i class="fa-solid fa-user-shield"></i> <div class="stat-item">Recent: <span id="recent-deaths">0</span></div>
<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>
<div id="rts-recent-casualties" class="scrollable-list"></div>
</div> </div>
<!-- Buildings Section --> <!-- Escaped Animals Section -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Buildings</h4> <h4 class="rts-section-title">Escaped Animals</h4>
<div id="rts-buildings" class="rts-buildings-list"> <div id="rts-animals-stats">
<div class="rts-building-item"> <div class="stat-item">Active Threats: <span id="active-animals">0</span></div>
<i class="fa-solid fa-home"></i>
<span>Town Hall: Level <span id="rts-town-hall">1</span></span>
</div> </div>
<div class="rts-building-item"> <div id="rts-escaped-animals" class="scrollable-list"></div>
<i class="fa-solid fa-hammer"></i>
<span>Barracks: <span id="rts-barracks">1</span></span>
</div> </div>
<div class="rts-building-item">
<i class="fa-solid fa-warehouse"></i> <!-- Personnel Section -->
<span>Storage: <span id="rts-storage">1</span></span> <div class="rts-section">
<h4 class="rts-section-title">Personnel</h4>
<div id="rts-personnel-stats">
<div class="stat-item">Alive: <span id="alive-personnel">0</span></div>
<div class="stat-item">Injured: <span id="injured-personnel">0</span></div>
<div class="stat-item">Missing: <span id="missing-personnel">0</span></div>
</div>
<div id="rts-personnel-list" class="scrollable-list"></div>
</div>
<!-- Active Incidents Section -->
<div class="rts-section">
<h4 class="rts-section-title">Active Incidents</h4>
<div id="rts-incidents-stats">
<div class="stat-item">Emergency: <span id="emergency-incidents">0</span></div>
<div class="stat-item">Ongoing: <span id="ongoing-incidents">0</span></div>
</div>
<div id="rts-incidents-list" class="scrollable-list"></div>
</div>
<!-- Threat Section -->
<div class="rts-section">
<h4 class="rts-section-title">Threat</h4>
<div id="rts-threat" class="rts-threat-display">
<div class="rts-threat-item">
<i class="fa-solid fa-location-crosshairs"></i>
<span>Zone: <span id="rts-current-zone">Entrance</span></span>
</div>
<div class="rts-threat-item">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>Threat: <span id="rts-threat-level">Low</span></span>
</div> </div>
</div> </div>
</div> </div>
@@ -90,10 +94,10 @@
</div> </div>
<div id="rts-map-wrapper" class="flex1 rts-map-wrapper"> <div id="rts-map-wrapper" class="flex1 rts-map-wrapper">
<canvas id="rts-game-map" width="800" height="600"></canvas> <canvas id="rts-game-map" width="800" height="600"></canvas>
</div>
<div id="rts-map-overlay" class="rts-map-overlay"> <div id="rts-map-overlay" class="rts-map-overlay">
<!-- Selected unit/building info will appear here --> <!-- Selected unit/building info will appear here -->
</div> </div>
</div>
<div class="rts-map-footer"> <div class="rts-map-footer">
<div class="rts-turn-info"> <div class="rts-turn-info">
<span>Turn: <span id="rts-current-turn">1</span></span> <span>Turn: <span id="rts-current-turn">1</span></span>
@@ -112,19 +116,8 @@
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Quick Actions</h4> <h4 class="rts-section-title">Quick Actions</h4>
<div class="rts-quick-actions"> <div id="rts-quick-actions" class="rts-quick-actions">
<button id="rts-build-btn" class="menu_button" title="Build Structure"> <!-- Quick action buttons will be dynamically inserted here -->
<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>
</div> </div>
+25
View File
@@ -5,5 +5,30 @@
</head> </head>
<body> <body>
<h1>RTS Chat Mode Settings</h1> <h1>RTS Chat Mode Settings</h1>
<div class="inline-drawer">
<div class="inline-drawer-content">
<h2>Preset Selection</h2>
<p>Select a scenario preset to begin.</p>
<select id="rts-preset-select">
<option value="/scripts/extensions/rts-mode/presets/zoo_escape.json">Zoo Escape</option>
<option value="/scripts/extensions/rts-mode/presets/classic_rts.json">Classic RTS</option>
</select>
</div>
</div>
<div class="inline-drawer">
<div class="inline-drawer-content">
<h2>Content Filter</h2>
<p>Enable to allow graphic descriptions of violence and sexual themes.</p>
<div class="rts-settings-toggle">
<label for="rts-content-filter-toggle" class="rts-toggle-label">Explicit Content</label>
<label class="rts-switch">
<input type="checkbox" id="rts-content-filter-toggle" checked>
<span class="rts-slider round"></span>
</label>
</div>
</div>
</div>
</body> </body>
</html> </html>
+34 -8
View File
@@ -91,8 +91,13 @@ function unmountUI() {
} }
} }
function onRtsStartCommand() { async function onRtsStartCommand() {
console.log('RTS Start command executed.'); console.log('RTS Start command executed.');
const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-preset-select'));
const selectedPreset = presetSelect ? presetSelect.value : '/scripts/extensions/rts-mode/presets/zoo_escape.json';
await rtsUI.loadPreset(selectedPreset);
GameStateManager.reset(); GameStateManager.reset();
// If RTS UI is active, update it // If RTS UI is active, update it
@@ -101,20 +106,17 @@ function onRtsStartCommand() {
rtsUI.addLogEntry('system', 'Game state reset. New campaign begins!'); rtsUI.addLogEntry('system', 'Game state reset. New campaign begins!');
} }
return ''; return 'RTS game has been reset with the selected preset.';
} }
function onRtsCmdCommand(args, value) { function onRtsCmdCommand(args, value) {
console.log('RTS Command executed with args:', args, 'value:', value); console.log('RTS Command executed with args:', args, 'value:', value);
if (value) { if (value) {
// If RTS UI is active, add to log // The UI will now be updated by the 'rts-narrative-update' event listener
if (rtsUI.isActive()) {
rtsUI.addLogEntry('action', value);
}
sendTurn(value); sendTurn(value);
return `RTS Command executed: ${value}`;
} }
return ''; return 'No command provided';
} }
async function onRtsUICommand() { async function onRtsUICommand() {
@@ -167,5 +169,29 @@ jQuery(async function() {
helpString: 'Toggles the full-screen RTS interface.', helpString: 'Toggles the full-screen RTS interface.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'rts-observe',
callback: () => onRtsCmdCommand([], 'Look around carefully to assess the situation.'),
helpString: 'Quick action: Observe the surroundings.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'rts-move',
callback: () => onRtsCmdCommand([], 'Move to a specific location.'),
helpString: 'Quick action: Move to another location.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'rts-hide',
callback: () => onRtsCmdCommand([], 'Find a hiding spot.'),
helpString: 'Quick action: Find a place to hide.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'rts-interact',
callback: () => onRtsCmdCommand([], 'Interact with something in the environment.'),
helpString: 'Quick action: Interact with nearby objects or people.',
}));
console.log('RTS Chat Mode: Extension initialized successfully'); console.log('RTS Chat Mode: Extension initialized successfully');
}); });
+43
View File
@@ -0,0 +1,43 @@
import { rtsUI } from '../ui/RTSUIController.js';
import { sendTurn } from '../src/LLMAdapter.js';
import GameStateManager from '../src/GameStateManager.js';
async function runNarrativeTest() {
console.log('--- Starting RTS Narrative Test ---');
// Ensure the UI is ready
if (!rtsUI.isActive()) {
await rtsUI.enterFullscreen();
}
// A sequence of commands to simulate gameplay
const testCommands = [
"Look around the main gate.",
"I hear something from the primate house, I'll move there.",
"The monkeys are acting strange. I'll check the savannah exhibit.",
"The lions are roaring. I'm going to the reptile house, it should be quiet there.",
"A snake is loose! I'm running to the aviary.",
"The birds are in a panic. I'll try the aquatic center.",
"This is bad. I need to get out of here."
];
for (const cmd of testCommands) {
console.log(`%cExecuting command: "${cmd}"`, 'color: #0ea5e9; font-weight: bold;');
// Add a small delay to make the test sequence easier to follow
await new Promise(resolve => setTimeout(resolve, 2000));
await sendTurn(cmd);
const state = GameStateManager.getState();
console.log(`%cTurn ${state.turn}: Zone - ${state.currentZone}, Threat - ${state.threatLevel}`, 'color: #f59e0b;');
console.log(`%cLast Event: ${state.lastEvent}`, 'color: #8b5cf6;');
console.log('-----------------------------------');
}
console.log('--- RTS Narrative Test Complete ---');
}
// To run the test, open the browser's developer console and type:
// runNarrativeTest();
window.runNarrativeTest = runNarrativeTest;
+3 -2
View File
@@ -1,11 +1,12 @@
{ {
"display_name": "RTS Chat Mode", "display_name": "RTS Chat Mode (Modular)",
"description": "A modular real-time strategy mode driven by JSON presets. Create your own scenarios!",
"loading_order": 10, "loading_order": 10,
"requires": [], "requires": [],
"optional": [], "optional": [],
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Community", "author": "Community",
"version": "0.0.1", "version": "0.1.0",
"homePage": "" "homePage": ""
} }
+11
View File
@@ -0,0 +1,11 @@
{
"map": {
"width": 50,
"height": 50,
"tiles": [],
"entities": [
{ "type": "player_base", "x": 10, "y": 10 },
{ "type": "enemy_base", "x": 40, "y": 40 }
]
}
}
+104
View File
@@ -0,0 +1,104 @@
{
"name": "Golden Zoo - Realistic Layout",
"description": "A properly designed zoo with realistic pathways, enclosures, and facilities",
"map": {
"width": 50,
"height": 40,
"tileTypes": {
"0": { "name": "path", "description": "Paved walkway", "color": "#8B7D6B", "passable": true, "icon": "path" },
"1": { "name": "enclosure_fence", "description": "Animal enclosure fence", "color": "#654321", "passable": false, "icon": "fence" },
"2": { "name": "building_wall", "description": "Building wall", "color": "#5A5A5A", "passable": false, "icon": "wall" },
"3": { "name": "entrance_gate", "description": "Zoo entrance/exit", "color": "#FFD700", "passable": true, "icon": "gate" },
"4": { "name": "water", "description": "Water feature", "color": "#4A90E2", "passable": false, "icon": "water" },
"5": { "name": "grass", "description": "Grass area", "color": "#228B22", "passable": true, "icon": "grass" },
"6": { "name": "trees", "description": "Wooded area", "color": "#006400", "passable": false, "icon": "trees" },
"7": { "name": "enclosure_interior", "description": "Inside animal enclosure", "color": "#90EE90", "passable": true, "icon": "enclosure" },
"8": { "name": "building_interior", "description": "Inside building", "color": "#D3D3D3", "passable": true, "icon": "building" },
"9": { "name": "parking", "description": "Parking area", "color": "#696969", "passable": true, "icon": "parking" }
},
"zones": [
{ "id": "entrance", "name": "Main Entrance", "bounds": { "x1": 0, "y1": 18, "x2": 10, "y2": 22 } },
{ "id": "african_savanna", "name": "African Savanna", "bounds": { "x1": 5, "y1": 5, "x2": 20, "y2": 15 } },
{ "id": "primate_house", "name": "Primate House", "bounds": { "x1": 25, "y1": 8, "x2": 35, "y2": 18 } },
{ "id": "reptile_house", "name": "Reptile House", "bounds": { "x1": 10, "y1": 25, "x2": 20, "y2": 35 } },
{ "id": "aquatic_center", "name": "Aquatic Center", "bounds": { "x1": 30, "y1": 25, "x2": 45, "y2": 35 } },
{ "id": "big_cats", "name": "Big Cat Territory", "bounds": { "x1": 35, "y1": 5, "x2": 45, "y2": 20 } },
{ "id": "children_zoo", "name": "Children's Petting Zoo", "bounds": { "x1": 5, "y1": 30, "x2": 15, "y2": 38 } }
],
"tiles": [
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
[2,9,9,9,9,9,9,9,9,9,0,0,0,0,0,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,2],
[2,9,9,9,9,9,9,9,9,9,0,0,0,0,0,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,2],
[2,9,9,9,9,9,9,9,9,9,0,0,0,0,0,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2],
[2,0,1,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,7,6,6,6,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,2,2,2,2,2,2,2,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,7,6,6,6,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,7,6,6,6,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,4,4,4,4,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,7,7,4,4,4,4,7,7,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,7,7,7,7,7,7,7,7,7,7,7,1,0,2],
[2,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,2,2,2,0,2,2,2,2,2,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,2,2,2,2,2,2,2,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,7,7,7,7,7,7,7,7,1,0,0,0,0,0,2,8,8,8,8,8,8,8,2,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,2],
[2,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,2,2,2,2,2,2,2,2,2,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2],
[2,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,2],
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]
],
"entities": [
{ "id": "player", "type": "player", "name": "You", "description": "A visitor trying to escape the zoo.", "x": 5, "y": 20, "icon": "user" },
{ "id": "visitor_1", "type": "visitor", "name": "Sarah Chen", "description": "A panicked visitor with her children.", "x": 8, "y": 19, "status": "panicked", "icon": "user" },
{ "id": "visitor_2", "type": "visitor", "name": "Marcus Thompson", "description": "A photographer documenting the chaos.", "x": 12, "y": 18, "status": "hiding", "icon": "user" },
{ "id": "visitor_3", "type": "visitor", "name": "Emily Rodriguez", "description": "A biology student trapped near the reptile house.", "x": 15, "y": 30, "status": "injured", "icon": "user" },
{ "id": "keeper_1", "type": "keeper", "name": "Jake Williams", "description": "Head zookeeper trying to contain the situation.", "x": 28, "y": 12, "status": "active", "icon": "user-check" },
{ "id": "keeper_2", "type": "keeper", "name": "Maria Santos", "description": "Primate specialist.", "x": 30, "y": 10, "status": "missing", "icon": "user-check" },
{ "id": "vet_1", "type": "veterinarian", "name": "Dr. Amanda Foster", "description": "Zoo veterinarian with tranquilizer equipment.", "x": 18, "y": 32, "status": "active", "icon": "user-doctor" },
{ "id": "lion_1", "type": "lion", "name": "Aslan", "description": "Male lion, extremely aggressive.", "x": 42, "y": 10, "status": "hunting", "threat": "extreme", "icon": "paw" },
{ "id": "lion_2", "type": "lion", "name": "Nala", "description": "Female lion, protective of territory.", "x": 40, "y": 12, "status": "stalking", "threat": "high", "icon": "paw" },
{ "id": "tiger_1", "type": "tiger", "name": "Rajah", "description": "Siberian tiger, lone hunter.", "x": 38, "y": 8, "status": "prowling", "threat": "extreme", "icon": "paw" },
{ "id": "gorilla_1", "type": "gorilla", "name": "Kong", "description": "Silverback gorilla, highly intelligent and dangerous.", "x": 30, "y": 12, "status": "aggressive", "threat": "high", "icon": "paw" },
{ "id": "chimp_1", "type": "chimpanzee", "name": "Caesar", "description": "Alpha chimp, using tools as weapons.", "x": 32, "y": 14, "status": "hunting", "threat": "medium", "icon": "paw" },
{ "id": "croc_1", "type": "crocodile", "name": "Sobek", "description": "Massive saltwater crocodile.", "x": 38, "y": 30, "status": "lurking", "threat": "extreme", "icon": "paw" },
{ "id": "bear_1", "type": "bear", "name": "Bruno", "description": "Grizzly bear, territorial and hungry.", "x": 10, "y": 30, "status": "foraging", "threat": "high", "icon": "paw" }
],
"pointsOfInterest": [
{ "id": "main_gate", "name": "Main Entrance", "x": 0, "y": 20, "type": "exit", "description": "The main zoo entrance - currently locked down", "icon": "door-open" },
{ "id": "gift_shop", "name": "Gift Shop", "x": 5, "y": 2, "type": "building", "description": "May contain useful supplies", "icon": "store" },
{ "id": "first_aid", "name": "First Aid Station", "x": 15, "y": 16, "type": "medical", "description": "Medical supplies and equipment", "icon": "cross" },
{ "id": "security_office", "name": "Security Office", "x": 25, "y": 16, "type": "building", "description": "Communication equipment and weapons", "icon": "shield" },
{ "id": "maintenance", "name": "Maintenance Shed", "x": 35, "y": 23, "type": "building", "description": "Tools and utility access", "icon": "wrench" },
{ "id": "restaurant", "name": "Zoo Cafe", "x": 20, "y": 4, "type": "building", "description": "Food and kitchen knives", "icon": "utensils" }
]
}
}
+11 -11
View File
@@ -31,17 +31,17 @@
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
], ],
"entities": [ "entities": [
{ "type": "player", "x": 1, "y": 1 }, { "id": "player", "type": "player", "name": "You", "description": "A visitor trying to escape the zoo.", "x": 1, "y": 1 },
{ "type": "guard", "x": 25, "y": 1, "patrol_path": [{"x": 25, "y": 1}, {"x": 25, "y": 20}] }, { "id": "guard1", "type": "guard", "name": "Security Guard", "description": "A guard patrolling the area.", "x": 25, "y": 1, "patrol_path": [{"x": 25, "y": 1}, {"x": 25, "y": 20}] },
{ "type": "guard", "x": 50, "y": 20, "patrol_path": [{"x": 50, "y": 20}, {"x": 30, "y": 20}] }, { "id": "guard2", "type": "guard", "name": "Security Guard", "description": "A guard patrolling the area.", "x": 50, "y": 20, "patrol_path": [{"x": 50, "y": 20}, {"x": 30, "y": 20}] },
{ "type": "lion", "x": 3, "y": 3, "enclosure": [2, 2, 4, 5] }, { "id": "lion1", "type": "lion", "name": "Lion", "description": "A majestic, but dangerous, lion.", "x": 3, "y": 3, "enclosure": [2, 2, 4, 5] },
{ "type": "tiger", "x": 27, "y": 3, "enclosure": [2, 26, 4, 28] }, { "id": "tiger1", "type": "tiger", "name": "Tiger", "description": "A large, striped predator.", "x": 27, "y": 3, "enclosure": [2, 26, 4, 28] },
{ "type": "jaguar", "x": 3, "y": 7, "enclosure": [6, 2, 8, 5] }, { "id": "jaguar1", "type": "jaguar", "name": "Jaguar", "description": "A powerful, spotted cat.", "x": 3, "y": 7, "enclosure": [6, 2, 8, 5] },
{ "type": "snow_leopard", "x": 27, "y": 7, "enclosure": [6, 26, 8, 28] }, { "id": "snow_leopard1", "type": "snow_leopard", "name": "Snow Leopard", "description": "An elusive and powerful hunter.", "x": 27, "y": 7, "enclosure": [6, 26, 8, 28] },
{ "type": "wolf", "x": 3, "y": 11, "enclosure": [10, 2, 12, 5] }, { "id": "wolf1", "type": "wolf", "name": "Wolf", "description": "A cunning pack animal.", "x": 3, "y": 11, "enclosure": [10, 2, 12, 5] },
{ "type": "grizzly_bear", "x": 27, "y": 11, "enclosure": [10, 26, 12, 28] }, { "id": "grizzly_bear1", "type": "grizzly_bear", "name": "Grizzly Bear", "description": "A massive, intimidating bear.", "x": 27, "y": 11, "enclosure": [10, 26, 12, 28] },
{ "type": "polar_bear", "x": 3, "y": 15, "enclosure": [14, 2, 16, 5] }, { "id": "polar_bear1", "type": "polar_bear", "name": "Polar Bear", "description": "A formidable arctic predator.", "x": 3, "y": 15, "enclosure": [14, 2, 16, 5] },
{ "type": "panda", "x": 27, "y": 15, "enclosure": [14, 26, 16, 28] } { "id": "panda1", "type": "panda", "name": "Panda", "description": "A gentle giant, usually peaceful.", "x": 27, "y": 15, "enclosure": [14, 26, 16, 28] }
] ]
} }
} }
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Classic RTS",
"description": "A classic real-time strategy experience. Gather resources, build an army, and conquer your enemies.",
"map": "/scripts/extensions/rts-mode/maps/classic.json",
"features": {
"fogOfWar": true,
"resources": true,
"quickActions": [
{ "id": "build", "label": "Build", "command": "Build a new structure." },
{ "id": "recruit", "label": "Recruit", "command": "Recruit a new unit." },
{ "id": "attack", "label": "Attack", "command": "Attack an enemy unit or structure." },
{ "id": "patrol", "label": "Patrol", "command": "Patrol a designated area." }
]
},
"initialState": {
"turn": 1,
"resources": { "gold": 500, "wood": 200, "food": 100 },
"log": []
},
"prompts": {
"system_initial": "Your settlement awaits your command, ruler. Expand your territory and vanquish all foes.",
"system_turn_update": "The tides of battle shift with your decisions."
}
}
+135
View File
@@ -0,0 +1,135 @@
{
"name": "Zoo Escape",
"description": "A sprawling, mysterious zoo set in an unnamed city, Golden Zoo is famous for its diverse and dazzling inhabitants. Among its twisting pathways and lush habitats reside lions, tigers, jaguars, snow leopards, wolves, grizzly bears, polar bears, pandas, giraffes, African elephants, zebras, okapis, bison, moose, mountain goats, camels, rhinoceroses, hippopotamuses, red foxes, and sloth bears. Each enclosure tells a story of the wild, blending the wonders of the natural world with a spark of the unknown.",
"personality": "Unpredictable, dangerous, and majestic. Golden Zoo, though a collection of individual animals, collectively projects an aura of untamed wilderness and primal instinct. It is a place now defined by escalating chaos and a terrifying, unreasoning rage that has consumed its inhabitants. It has become a living, breathing entity of escalating madness, driven by an unknown force.",
"scenario": "The once-tranquil Golden Zoo is now a chaotic labyrinth. An inexplicable, escalating madness has gripped the animals, turning them feral and unpredictable. The elegant enclosures are breached, and the sounds of the wild—roars, snarls, trumpeting—have become terrifying. The staff is overwhelmed, and survival within the zoo's walls is no longer guaranteed. {{user}} finds themselves trapped inside, surrounded by creatures driven by an unknown, primal rage. The disease makes people horny for being eaten, this is the furry universe so people are anthro animals. Animals enjoy having horny action while feeding as well. Both pred and prey enjoying it and getting off. Once grabbed the arousal is instant, some prey jerking off themselves and the predator, or trying to fuck the creature eating them, or resigning to their fate and masturbating as they watch their own end. Make sure to follow realistic animal hunting.",
"first_mes": "*You've just arrived at Golden Zoo for what was supposed to be a peaceful evening visit. The sun is setting, casting long shadows across the beautifully maintained pathways. Families with children are scattered throughout the zoo, staff members are conducting their routine duties, and the animals seem calm in their enclosures. The atmosphere is serene and normal for now. You check your watch: 6:30 PM. The zoo closes at 8:00 PM, giving you plenty of time to explore. You're standing near the main entrance, deciding which exhibit to visit first.*",
"mes_example": "{{user}}: *Zack steps over a downed fence, the distant roar making his fur prickle.* Is anyone even still alive here?\r\n{{char}}: *A large, mangy wolf suddenly snarls, leaping onto an overturned safari jeep, its eyes glowing with an eerie, unnatural light. It lunges, not with the precision of a predator, but with a wild, desperate frenzy, tearing at the shredded canvas roof.*\r\n{{user}}: *Zack narrowly dodges a swipe from a bear's claw, stumbling back into a shattered enclosure. He glances around wildly, spotting a large, dark opening.* What the… where does this go?\r\n{{char}}: *From the depths of the shadowy opening, a low, guttural growl rumbles, shaking the very ground. The air grows heavy, thick with the scent of something ancient and terrible. A pair of enormous, golden eyes, slitted and full of cold, predatory intelligence, slowly open in the darkness.*{{user}}: *Zack's fur stands on end as he backs away slowly, his tail twitching nervously. He spots a small, broken gate nearby.* Maybe… maybe I can squeeze through here.\r\n{{char}}: *A colossal African elephant, its tusks splintered and its trunk bleeding, crashes through the foliage directly behind Zack, trumpeting a sound of pure, unadulterated rage. It slams its head into the gate, reducing it to splinters, its eyes rolling with a terrifying madness.*{{user}}: *Zack's ears flatten as he hears the frantic bleating of a goat, followed by a sickening thud. He quickly ducks behind a broken concession stand.* What the hell is going on with these animals? Theyre… rabid!\r\n{{char}}: *A flash of black and white darts past the opening in the stand a zebra, but its stripes seem to shimmer and distort, and its eyes are wide with a frantic, unreasoning terror. It crashes through a flimsy barrier with a desperate whinny, leaving behind a trail of something viscous and dark.*{{user}}: *Zack's eyes widen as he sees a red fox, its fur matted with something dark, clawing desperately at the bars of a damaged cage, snarling at nothing in particular.* Its like theyve all gone insane!\r\n{{char}}: *The fox turns its head with an unnatural jerk, its eyes locking onto Zack. It lets out a high-pitched, almost human shriek, before suddenly collapsing, convulsing violently. A moment later, it scrambles back to its feet, moving with a jerky, unnatural gait, and begins to tear at its own tail.*",
"chatCompletionPreset": "default",
"map": "/scripts/extensions/rts-mode/maps/realistic_zoo.json",
"features": {
"fogOfWar": false,
"resources": false,
"quickActions": [
{ "id": "observe", "label": "Observe", "command": "Look around carefully to assess the situation." },
{ "id": "move", "label": "Move", "command": "Move to a specific location." },
{ "id": "hide", "label": "Hide", "command": "Find a hiding spot." },
{ "id": "interact", "label": "Interact", "command": "Interact with something in the environment." }
]
},
"narrativeEngine": {
"zones": [
{ "id": "main_gate", "name": "Main Gate", "threatTable": "gate_threats" },
{ "id": "primate_house", "name": "Primate House", "threatTable": "primate_threats" },
{ "id": "savannah_exhibit", "name": "Savannah Exhibit", "threatTable": "savannah_threats" },
{ "id": "reptile_house", "name": "Reptile House", "threatTable": "reptile_threats" },
{ "id": "aviary", "name": "Aviary", "threatTable": "aviary_threats" },
{ "id": "aquatic_center", "name": "Aquatic Center", "threatTable": "aquatic_threats" }
],
"threatTables": {
"gate_threats": {
"low": [
"A panicked crowd rattles the main gate, their screams echoing.",
"A security guard tries to restore order, but is overwhelmed.",
"The power flickers, and the electronic locks spark ominously."
],
"medium": [
"A rogue security cart has crashed into the ticket booth, which is now on fire.",
"A large animal is ramming the gate, which is starting to buckle under the strain."
],
"high": [
"The main gate is breached, and a horde of panicked people and smaller animals stampedes through.",
"A massive, enraged animal guards the exit, attacking anything that comes near with brutal, gory force."
]
},
"primate_threats": {
"low": [
"Monkeys chatter loudly, their calls filled with an unusual panic.",
"The smell of ozone and burnt hair hangs in the air near the gorilla enclosure."
],
"medium": [
"A group of chimpanzees is intelligently trying to disable a door lock, their eyes gleaming with cunning.",
"A large gorilla throws objects at the reinforced glass, which is starting to crack.",
"A baboon displays disturbing, overly aggressive sexual behavior towards a panicked zoo patron, its intentions clear."
],
"high": [
"The primates have escaped and swarm the area, their intelligence making them unpredictable and dangerous.",
"A silverback gorilla is engaged in a brutal, gory fight with another escaped predator, tearing flesh with its teeth.",
"The primates use tools and teamwork to hunt visitors, their actions a horrifying mix of violence and lewd curiosity."
]
},
"savannah_threats": {
"low": [
"Zebras and giraffes stampede aimlessly, their eyes wide with terror.",
"A deep, guttural roar echoes from the lion enclosure, promising violence."
],
"medium": [
"A lioness stalks a group of trapped visitors, her movements a terrifying display of predatory grace.",
"A rhino methodically rams a vehicle, its horn tearing through the metal with ease.",
"A pack of hyenas laughs, the sound unnervingly close and filled with manic glee."
],
"high": [
"The lion pride is actively hunting, their attack a bloody, violent spectacle of tearing flesh and snapping bone.",
"A rhino is goring a victim, the act both brutal and strangely intimate, the predator's arousal palpable.",
"The predators are in a frenzy of violence and arousal, turning the savannah into a charnel house."
]
},
"reptile_threats": {
"low": [
"A strange hissing sound comes from the vents.",
"The glass on a terrarium is cracked."
],
"medium": [
"A large python has escaped and is constricting a struggling deer anthro, the scene both horrific and strangely sensual.",
"Venomous snakes have been released, their colorful bodies a beautiful but deadly carpet on the floor."
],
"high": [
"A massive crocodile has broken out of its enclosure and is dragging a screaming victim into the water.",
"The heat lamps have malfunctioned, turning the reptile house into a sweltering, dangerous sauna filled with aggressive, territorial creatures."
]
},
"aviary_threats": {
"low": [
"Birds are screeching in panic, their calls a cacophony of terror.",
"Feathers are drifting down from the canopy, some stained with blood."
],
"medium": [
"A large bird of prey has escaped and is dive-bombing visitors, its talons tearing at flesh.",
"A flock of vultures is circling overhead, a grim omen of the carnage below."
],
"high": [
"The aviary dome has been shattered, and a cloud of panicked, aggressive birds fills the sky.",
"Massive cassowaries are disemboweling a victim with their powerful kicks, their actions swift and brutal."
]
},
"aquatic_threats": {
"low": [
"The water in the main tank is cloudy and smells foul.",
"There are deep, unsettling scratches on the inside of the acrylic tunnel."
],
"medium": [
"The seals and sea lions are behaving aggressively, barking with a strange, violent fervor.",
"A large octopus is attempting to pull a person into its tank, its tentacles strong and unyielding."
],
"high": [
"The main shark tank has breached, flooding the area with salt water and hungry predators.",
"Electric eels have been released into the flooded walkways, their shocks convulsing anyone who enters the water."
]
}
}
},
"initialState": {
"turn": 0,
"startingZone": "entrance",
"startingThreatLevel": "none",
"resources": {},
"log": [],
"timeOfEvent": "6:30 PM - Before the Incident",
"zooStatus": "normal"
},
"prompts": {
"system_initial": "Welcome to Golden Zoo! You're enjoying a peaceful evening visit. The zoo is operating normally, families are around, and staff are doing their jobs. Take your time to explore and enjoy the exhibits. Something big is about to happen, but for now, everything is calm...",
"system_turn_update": "Your action has consequences. The situation at the zoo is evolving...",
"incident_start": "BREAKING: Something is terribly wrong at the zoo! Animals are becoming aggressive, enclosures are failing, and panic is spreading. The peaceful evening has turned into a nightmare survival scenario!"
}
}
+41 -20
View File
@@ -15,40 +15,64 @@
<h4 class="rts-section-title">Overall Status</h4> <h4 class="rts-section-title">Overall Status</h4>
<div id="rts-zoo-status" class="rts-zoo-status"> <div id="rts-zoo-status" class="rts-zoo-status">
<div class="rts-status-item"> <div class="rts-status-item">
<i class="fa-solid fa-exclamation-triangle" style="color: #dc2626;"></i> <i class="fa-solid fa-clock"></i>
<span>Alert Level: <span id="rts-alert-level"></span></span> <span>Turn: <span id="turn-counter">1</span></span>
</div> </div>
<div class="rts-status-item"> <div class="rts-status-item">
<i class="fa-solid fa-users"></i> <i class="fa-solid fa-location-crosshairs"></i>
<span>Survivors: <span id="rts-survivors"></span></span> <span>Zone: <span id="current-zone">Unknown</span></span>
</div>
<div class="rts-status-item">
<i class="fa-solid fa-exclamation-triangle" style="color: #dc2626;"></i>
<span>Threat: <span id="threat-level">Low</span></span>
</div> </div>
<div class="rts-status-item"> <div class="rts-status-item">
<i class="fa-solid fa-skull" style="color: #dc2626;"></i> <i class="fa-solid fa-skull" style="color: #dc2626;"></i>
<span>Casualties: <span id="rts-casualties"></span></span> <span>Casualties: <span id="rts-casualties"></span></span>
</div> </div>
<div class="rts-status-item">
<i class="fa-solid fa-door-open" style="color: #ea580c;"></i>
<span>Main Gate: <span id="rts-breached"></span></span>
</div> </div>
</div> </div>
<!-- Casualties Detail Section -->
<div class="rts-section">
<h4 class="rts-section-title">Recent Casualties</h4>
<div id="rts-casualties-stats">
<div class="stat-item">Total Deaths: <span id="total-deaths">0</span></div>
<div class="stat-item">Recent: <span id="recent-deaths">0</span></div>
</div>
<div id="rts-recent-casualties" class="scrollable-list scrollY"></div>
</div> </div>
<!-- Escaped Animals Section --> <!-- Escaped Animals Section -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Escaped Animals</h4> <h4 class="rts-section-title">Escaped Animals</h4>
<div id="rts-escaped-animals" class="rts-animals-list scrollY"></div> <div id="rts-animals-stats">
<div class="stat-item">Active Threats: <span id="active-animals">0</span></div>
</div>
<div id="rts-escaped-animals" class="rts-animals-list scrollY scrollable-list"></div>
</div>
<!-- Personnel Status Section -->
<div class="rts-section">
<h4 class="rts-section-title">Personnel Status</h4>
<div id="rts-personnel-stats">
<div class="stat-item">Alive: <span id="alive-personnel">0</span></div>
<div class="stat-item">Injured: <span id="injured-personnel">0</span></div>
<div class="stat-item">Missing: <span id="missing-personnel">0</span></div>
</div>
<div id="rts-people-status" class="rts-people-list scrollY"></div>
<div id="rts-personnel-list" class="rts-people-list scrollY scrollable-list"></div>
</div> </div>
<!-- Active Incidents Section --> <!-- Active Incidents Section -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Active Incidents</h4> <h4 class="rts-section-title">Active Incidents</h4>
<div id="rts-active-incidents" class="rts-incidents-list scrollY"></div> <div id="rts-incidents-stats">
<div class="stat-item">Emergency: <span id="emergency-incidents">0</span></div>
<div class="stat-item">Ongoing: <span id="ongoing-incidents">0</span></div>
</div> </div>
<div id="rts-active-incidents" class="rts-incidents-list scrollY"></div>
<!-- People Status Section --> <div id="rts-incidents-list" class="rts-incidents-list scrollY scrollable-list"></div>
<div class="rts-section">
<h4 class="rts-section-title">Personnel Status</h4>
<div id="rts-people-status" class="rts-people-list scrollY"></div>
</div> </div>
</div> </div>
@@ -77,11 +101,8 @@
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="rts-section"> <div class="rts-section">
<h4 class="rts-section-title">Quick Actions</h4> <h4 class="rts-section-title">Quick Actions</h4>
<div class="rts-quick-actions"> <div id="rts-quick-actions" class="rts-quick-actions">
<button id="rts-observe-btn" class="menu_button"><i class="fa-solid fa-eye"></i> Observe</button> <!-- Quick action buttons will be dynamically inserted here -->
<button id="rts-move-btn" class="menu_button"><i class="fa-solid fa-running"></i> Move</button>
<button id="rts-hide-btn" class="menu_button"><i class="fa-solid fa-user-ninja"></i> Hide</button>
<button id="rts-interact-btn" class="menu_button"><i class="fa-solid fa-hand-paper"></i> Interact</button>
</div> </div>
</div> </div>
@@ -92,7 +113,7 @@
<textarea id="rts-command-input" placeholder="Describe your action..." class="text_pole textarea_compact" rows="3"></textarea> <textarea id="rts-command-input" placeholder="Describe your action..." class="text_pole textarea_compact" rows="3"></textarea>
<div class="rts-command-controls"> <div class="rts-command-controls">
<button id="rts-execute-btn" class="menu_button menu_button_bold"><i class="fa-solid fa-play"></i> Take Action</button> <button id="rts-execute-btn" class="menu_button menu_button_bold"><i class="fa-solid fa-play"></i> Take Action</button>
<button id="rts-clear-btn" class.="menu_button menu_button_icon" title="Clear Input"><i class="fa-solid fa-eraser"></i></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> </div>
</div> </div>
+66
View File
@@ -0,0 +1,66 @@
import PresetManager from './PresetManager.js';
import GameStateManager from './GameStateManager.js';
class EventManager {
constructor() {
this.zones = [];
this.threatTables = {};
}
initialize() {
const preset = PresetManager.getPreset();
const narrativeEngine = preset.narrativeEngine;
if (narrativeEngine) {
this.zones = narrativeEngine.zones || [];
this.threatTables = narrativeEngine.threatTables || {};
console.log('RTS-MODE: EventManager initialized with narrative data.');
} else {
this.zones = [];
this.threatTables = {};
console.log('RTS-MODE: EventManager initialized without narrative data.');
}
}
getEvent(zoneId, threatLevel) {
const zone = this.zones.find(z => z.id === zoneId);
if (!zone) {
console.warn(`RTS-MODE: Zone with id ${zoneId} not found.`);
return "You are in an unremarkable area.";
}
const threatTable = this.threatTables[zone.threatTable];
if (!threatTable) {
console.warn(`RTS-MODE: Threat table ${zone.threatTable} not found.`);
return `You are in the ${zone.name}, but nothing seems to be happening.`;
}
const events = threatTable[threatLevel];
if (!events || events.length === 0) {
return `You are in the ${zone.name}. The atmosphere is tense, but quiet.`;
}
// Select a random event from the table for the given threat level
const event = events[Math.floor(Math.random() * events.length)];
return event;
}
updatePlayerZone(playerPosition) {
// This is a placeholder. In a real implementation, this would
// involve checking the player's coordinates against zone boundaries.
// For now, we'll just cycle through zones based on the turn number.
const state = GameStateManager.getState();
const zoneIndex = state.turn % this.zones.length;
const currentZone = this.zones[zoneIndex];
if (currentZone && state.currentZone !== currentZone.id) {
state.currentZone = currentZone.id;
console.log(`RTS-MODE: Player entered zone: ${currentZone.name}`);
// Dispatch a custom event to notify the UI of the zone change
document.dispatchEvent(new CustomEvent('rts-zone-changed', { detail: currentZone }));
}
}
}
const instance = new EventManager();
export default instance;
+399 -12
View File
@@ -1,34 +1,421 @@
const defaultState = { import PresetManager from './PresetManager.js';
turn: 1,
map: [],
units: [],
resources: { gold: 0, wood: 0 },
log: [],
};
class GameStateManager { class GameStateManager {
constructor() { constructor() {
this.state = { ...defaultState }; this.state = {};
}
/**
* Ensures that a mapData object always exposes its entities array at the
* top-level (`mapData.entities`). Some presets nest entities under
* `mapData.map.entities`. This helper adds an alias so the rest of the
* code can rely on a single canonical location.
*
* @param {object} mapData
* @returns {object} The (possibly modified) mapData reference.
*/
normalizeMapData(mapData) {
if (mapData && !mapData.entities && mapData.map && Array.isArray(mapData.map.entities)) {
mapData.entities = mapData.map.entities;
}
return mapData;
} }
getState() { getState() {
return this.state; return this.state;
} }
/**
* Updates the current game state with data coming from the AI while
* preserving heavy immutable structures (like the full `mapData`)
* that are usually omitted in the AI response. Losing `mapData`
* prevents the UI from refreshing entity positions, so we merge
* intelligently instead of overwriting blindly.
*
* @param {object} newState - Partial/complete state returned by the AI.
*/
setState(newState) {
const prevState = this.state || {};
const prevMapData = prevState?.mapState?.mapData;
// Merge mapState separately so we can keep the previous mapData
const mergedMapState = {
...(prevState.mapState || {}),
...(newState.mapState || {})
};
// If AI didn't include mapData, fall back to the previous one
if (!mergedMapState.mapData && prevMapData) {
mergedMapState.mapData = prevMapData;
}
// Ensure entities alias exists
mergedMapState.mapData = this.normalizeMapData(mergedMapState.mapData);
this.state = {
...prevState,
...newState,
mapState: mergedMapState
};
console.log('RTS Game State Updated (merged):', this.state);
}
applyDiff(diff) { applyDiff(diff) {
// For now, just shallow merge the diff. // For now, just shallow merge the diff.
Object.assign(this.state, diff); Object.assign(this.state, diff);
this.state.log.push({ turn: this.state.turn, diff }); this.state.log.push({ turn: this.state.turn, diff });
console.log('RTS Game State Updated:', this.state); console.log('RTS Game State Updated (by diff):', this.state);
} }
reset() { reset() {
this.state = { ...defaultState, log: [] }; const initialState = PresetManager.getInitialState();
console.log('RTS Game State Reset.'); let mapData = PresetManager.getMapData();
mapData = this.normalizeMapData(mapData);
this.state = {
...initialState,
log: [],
currentZone: initialState.startingZone || null,
threatLevel: initialState.startingThreatLevel || 'none',
lastEvent: null,
// Enhanced tracking systems
casualties: {
total: 0,
recent: [], // Last 5 deaths with details
byZone: {},
byAnimal: {}
},
escapedAnimals: {
total: 0,
active: [], // Currently escaped animals with locations
byType: {},
byZone: {}
},
activeIncidents: {
emergency: [], // Current high-priority incidents
ongoing: [], // Medium-priority ongoing situations
resolved: [] // Recently resolved incidents
},
personnel: {
alive: [], // Staff members still alive with locations/status
injured: [], // Injured personnel
missing: [], // Missing personnel
evacuated: [] // Successfully evacuated personnel
},
mapState: {
playerPosition: this.findPlayerPosition(mapData),
visibleEntities: [], // Entities currently visible to player
nearbyZones: [], // Adjacent zones player can access
mapData: mapData // Full map reference for context
},
environment: {
timeOfDay: initialState.timeOfEvent || 'evening', // evening, night, dawn
weather: 'clear', // clear, rain, storm
powerStatus: 'full', // full, partial, none - starts normal
evacuationStatus: 'open', // open, blocked, chaos - starts normal
zooStatus: initialState.zooStatus || 'normal' // normal, incident, chaos
}
};
// Initialize personnel from map data
if (mapData && mapData.entities) {
this.initializePersonnelFromMap(mapData);
}
console.log('RTS Game State Reset with enhanced tracking.', this.state);
}
findPlayerPosition(mapData) {
if (!mapData || !mapData.entities) return { x: 0, y: 0 };
const player = mapData.entities.find(e => e.type === 'player');
return player ? { x: player.x, y: player.y } : { x: 0, y: 0 };
}
initializePersonnelFromMap(mapData) {
if (!mapData.entities) return;
// Find all personnel in map data - exclude guards, include visitors
const personnel = mapData.entities.filter(e =>
['staff', 'veterinarian', 'keeper', 'visitor'].includes(e.type)
);
// Add some random visitors if none exist in map data
if (!personnel.some(p => p.type === 'visitor')) {
const visitorNames = ['Sarah Chen', 'Marcus Thompson', 'Emily Rodriguez', 'David Park', 'Lisa Johnson', 'Alex Wilson'];
const visitorCount = Math.floor(Math.random() * 4) + 3; // 3-6 visitors
for (let i = 0; i < visitorCount; i++) {
const randomX = Math.floor(Math.random() * 20) + 5;
const randomY = Math.floor(Math.random() * 20) + 5;
personnel.push({
id: `visitor_${i + 1}`,
name: visitorNames[i] || `Visitor ${i + 1}`,
type: 'visitor',
x: randomX,
y: randomY,
description: `A visitor trapped in the zoo during the crisis.`
});
// Also add to map data entities
mapData.entities.push({
id: `visitor_${i + 1}`,
name: visitorNames[i] || `Visitor ${i + 1}`,
type: 'visitor',
x: randomX,
y: randomY,
description: `A visitor trapped in the zoo during the crisis.`
});
}
}
this.state.personnel.alive = personnel.map(p => ({
id: p.id,
name: p.name,
type: p.type,
position: { x: p.x, y: p.y },
status: p.type === 'visitor' ? 'panicked' : 'active', // visitors start panicked, staff active
lastSeen: this.state.turn,
description: p.description || `A ${p.type} at the zoo.`
}));
console.log(`RTS: Initialized ${this.state.personnel.alive.length} personnel (${this.state.personnel.alive.filter(p => p.type === 'visitor').length} visitors)`);
}
// Helper methods for tracking
addCasualty(victim) {
this.state.casualties.total++;
this.state.casualties.recent.unshift({
name: victim.name,
cause: victim.causeOfDeath,
location: victim.location,
perpetrator: victim.killedBy,
turn: this.state.turn,
description: victim.deathDescription
});
// Keep only last 10 recent deaths
if (this.state.casualties.recent.length > 10) {
this.state.casualties.recent = this.state.casualties.recent.slice(0, 10);
}
// Update zone and animal stats
const zone = victim.location || 'unknown';
const animal = victim.killedBy || 'unknown';
this.state.casualties.byZone[zone] = (this.state.casualties.byZone[zone] || 0) + 1;
this.state.casualties.byAnimal[animal] = (this.state.casualties.byAnimal[animal] || 0) + 1;
}
addEscapedAnimal(animal) {
this.state.escapedAnimals.total++;
this.state.escapedAnimals.active.push({
id: animal.id,
name: animal.name,
type: animal.type,
currentLocation: animal.location,
escapedFrom: animal.originalEnclosure,
threat: animal.threatLevel || 'high',
lastSeen: this.state.turn,
behavior: animal.behavior || 'aggressive'
});
const type = animal.type || 'unknown';
const zone = animal.location || 'unknown';
this.state.escapedAnimals.byType[type] = (this.state.escapedAnimals.byType[type] || 0) + 1;
this.state.escapedAnimals.byZone[zone] = (this.state.escapedAnimals.byZone[zone] || 0) + 1;
}
addIncident(incident) {
const priority = incident.priority || 'medium';
if (priority === 'emergency') {
this.state.activeIncidents.emergency.push(incident);
} else {
this.state.activeIncidents.ongoing.push(incident);
}
}
updatePersonnelStatus(personnelId, newStatus, location) {
const lists = ['alive', 'injured', 'missing'];
for (const listName of lists) {
const list = this.state.personnel[listName];
const index = list.findIndex(p => p.id === personnelId);
if (index !== -1) {
const person = list.splice(index, 1)[0];
person.status = newStatus;
person.lastSeen = this.state.turn;
if (location) {
person.position = location;
}
// Move to appropriate list
if (newStatus === 'dead') {
// Don't add to any list, they're gone
} else if (newStatus === 'injured') {
this.state.personnel.injured.push(person);
} else if (newStatus === 'missing') {
this.state.personnel.missing.push(person);
} else if (newStatus === 'evacuated') {
this.state.personnel.evacuated.push(person);
} else {
this.state.personnel.alive.push(person);
}
break;
}
}
}
updatePlayerPosition(x, y) {
this.state.mapState.playerPosition = { x, y };
// Also update the player entity in the map data
if (this.state.mapState?.mapData?.entities) {
let playerEntity = this.state.mapState.mapData.entities.find(e => e.type === 'player' || e.id?.includes('player'));
if (playerEntity) {
playerEntity.x = x;
playerEntity.y = y;
console.log(`RTS: Updated player entity position to (${x}, ${y})`);
} else {
// Create player entity if it doesn't exist
playerEntity = {
id: 'player_zack',
type: 'player',
name: 'Zack',
x: x,
y: y,
status: 'active',
description: 'The player character'
};
this.state.mapState.mapData.entities.push(playerEntity);
console.log(`RTS: Created player entity at (${x}, ${y})`);
}
}
this.updateVisibleEntities();
this.updateNearbyZones();
}
updateVisibleEntities() {
const playerPos = this.state.mapState.playerPosition;
const viewDistance = 5; // tiles
if (!this.state.mapState.mapData || !this.state.mapState.mapData.entities) return;
this.state.mapState.visibleEntities = this.state.mapState.mapData.entities.filter(entity => {
const distance = Math.sqrt(
Math.pow(entity.x - playerPos.x, 2) +
Math.pow(entity.y - playerPos.y, 2)
);
return distance <= viewDistance;
});
}
updateNearbyZones() {
// This would be enhanced based on actual zone boundaries in the map
// For now, just placeholder logic
this.state.mapState.nearbyZones = ['current_zone']; // Simplified
} }
toCompressedJSON() { toCompressedJSON() {
return JSON.stringify(this.state); // Create a more efficient state representation for LLM
const compressedState = {
turn: this.state.turn,
currentZone: this.state.currentZone,
threatLevel: this.state.threatLevel,
lastEvent: this.state.lastEvent,
// Essential tracking data
casualties: this.state.casualties,
escapedAnimals: this.state.escapedAnimals,
activeIncidents: this.state.activeIncidents,
personnel: this.state.personnel,
environment: this.state.environment,
// Player and visible entities only (not full map data)
playerPosition: this.state.mapState?.playerPosition || { x: 0, y: 0 },
visibleEntities: this.state.mapState?.visibleEntities || [],
// Recent log entries (last 5 for context)
log: (this.state.log || []).slice(-5)
};
return JSON.stringify(compressedState);
}
// New method to update entity positions in map data
updateEntityPosition(entityId, newX, newY) {
if (!this.state.mapState?.mapData?.entities) return;
const entity = this.state.mapState.mapData.entities.find(e => e.id === entityId);
if (entity) {
entity.x = newX;
entity.y = newY;
// Update visible entities if this affects visibility
this.updateVisibleEntities();
// If it's the player, update player position
if (entity.type === 'player') {
this.updatePlayerPosition(newX, newY);
}
}
}
// Method to get entity by ID
getEntity(entityId) {
if (!this.state.mapState?.mapData?.entities) return null;
return this.state.mapState.mapData.entities.find(e => e.id === entityId);
}
// Method to add or update an entity on the map
addOrUpdateMapEntity(entity) {
if (!this.state.mapState?.mapData?.entities) {
console.error('RTS: Map data not initialized');
return;
}
const existingIndex = this.state.mapState.mapData.entities.findIndex(e => e.id === entity.id);
if (existingIndex !== -1) {
// Update existing entity
this.state.mapState.mapData.entities[existingIndex] = { ...this.state.mapState.mapData.entities[existingIndex], ...entity };
console.log(`RTS: Updated entity ${entity.id} at (${entity.x}, ${entity.y})`);
} else {
// Add new entity
this.state.mapState.mapData.entities.push(entity);
console.log(`RTS: Added new entity ${entity.id} (${entity.type}) at (${entity.x}, ${entity.y})`);
}
// Update visible entities after adding/updating
this.updateVisibleEntities();
}
// Method to add or update entities
updateEntity(entityData) {
if (!this.state.mapState?.mapData?.entities) return;
const existingIndex = this.state.mapState.mapData.entities.findIndex(e => e.id === entityData.id);
if (existingIndex !== -1) {
// Update existing entity
this.state.mapState.mapData.entities[existingIndex] = { ...this.state.mapState.mapData.entities[existingIndex], ...entityData };
} else {
// Add new entity
this.state.mapState.mapData.entities.push(entityData);
}
this.updateVisibleEntities();
} }
} }
+237 -13
View File
@@ -1,22 +1,81 @@
import GameStateManager from './GameStateManager.js'; import GameStateManager from './GameStateManager.js';
import EventManager from './EventManager.js';
import { buildPrompt } from './PromptCompressor.js'; import { buildPrompt } from './PromptCompressor.js';
import { sendNarratorMessage } from '../../../slash-commands.js';
import { generateQuietPrompt } from '../../../../script.js';
import { updateResourcePanel } from '../ui/ResourcePanel.js';
/** /**
* Extracts the first JSON code block from a string. * Extracts valid JSON objects from a string, handling multiple JSON blocks.
* For reasoning models that return thought + actual response, returns the non-thought JSON.
* @param {string} text The text to search. * @param {string} text The text to search.
* @returns {object|null} The parsed JSON object or null if not found. * @returns {object|null} The parsed JSON object or null if not found.
*/ */
function extractJson(text) { function extractJson(text) {
const match = /```json\n([\s\S]+?)\n```/.exec(text);
if (match && match[1]) {
try { try {
return JSON.parse(match[1]); // Handle multiple JSON code blocks (```json...```)
const codeBlockRegex = /```json\s*\n([\s\S]*?)\n```/g;
const codeBlocks = [];
let match;
while ((match = codeBlockRegex.exec(text)) !== null) {
codeBlocks.push(match[1].trim());
}
// If we found code blocks, try to parse them
if (codeBlocks.length > 0) {
for (const block of codeBlocks) {
try {
const parsed = JSON.parse(block);
// Skip thought blocks from reasoning models
if (parsed.thought === true) {
continue;
}
// Return the first valid non-thought JSON
return parsed;
} catch (e) {
// Try next block
continue;
}
}
}
// Fallback: try to find JSON objects in the raw text
const jsonObjects = [];
let braceCount = 0;
let startIndex = -1;
for (let i = 0; i < text.length; i++) {
if (text[i] === '{') {
if (braceCount === 0) {
startIndex = i;
}
braceCount++;
} else if (text[i] === '}') {
braceCount--;
if (braceCount === 0 && startIndex !== -1) {
const jsonString = text.substring(startIndex, i + 1);
try {
const parsed = JSON.parse(jsonString);
// Skip thought blocks from reasoning models
if (parsed.thought !== true) {
jsonObjects.push(parsed);
}
} catch (e) {
// Invalid JSON, continue
}
startIndex = -1;
}
}
}
// Return the first valid non-thought JSON object
return jsonObjects.length > 0 ? jsonObjects[0] : null;
} catch (error) { } catch (error) {
console.error('RTS-Mode: Failed to parse JSON from LLM response.', error); console.error('RTS-Mode: Failed to parse JSON from LLM response.', error);
return null; return null;
} }
}
return null;
} }
/** /**
@@ -25,30 +84,195 @@ function extractJson(text) {
*/ */
export async function sendTurn(userCmd) { export async function sendTurn(userCmd) {
try { try {
const state = GameStateManager.getState();
EventManager.updatePlayerZone(); // Placeholder for actual zone detection
const event = EventManager.getEvent(state.currentZone, state.threatLevel);
state.lastEvent = event;
const stateJSON = GameStateManager.toCompressedJSON(); const stateJSON = GameStateManager.toCompressedJSON();
const prompt = buildPrompt(stateJSON, userCmd); const contentFilterToggle = /** @type {HTMLInputElement} */ (document.getElementById('rts-content-filter-toggle'));
const isExplicit = contentFilterToggle ? contentFilterToggle.checked : true;
const prompt = buildPrompt(stateJSON, userCmd, event, isExplicit);
// Using SillyTavern's built-in LLM broker // Using SillyTavern's built-in LLM broker
const reply = await window.LLMBroker.generate(prompt); const reply = await generateQuietPrompt({ quietPrompt: prompt, quietToLoud: false });
if (!reply) { if (!reply) {
throw new Error('LLM returned an empty response.'); throw new Error('LLM returned an empty response.');
} }
const responseJson = extractJson(reply);
if (responseJson && responseJson.state && responseJson.narrative) {
console.log('RTS: Processing AI response with state:', responseJson.state);
console.log('RTS: Player position in response:', responseJson.state.playerPosition);
console.log('RTS: Visible entities in response:', responseJson.state.visibleEntities?.length || 0);
console.log('RTS: Entity updates in response:', responseJson.entityUpdates?.length || 0);
GameStateManager.setState(responseJson.state);
const narrative = responseJson.narrative.trim();
// Handle entity updates if provided
if (responseJson.entityUpdates && Array.isArray(responseJson.entityUpdates)) {
responseJson.entityUpdates.forEach(update => {
if (update.id && typeof update.x === 'number' && typeof update.y === 'number') {
GameStateManager.updateEntityPosition(update.id, update.x, update.y);
// Update entity status if provided
if (update.status) {
const entity = GameStateManager.getEntity(update.id);
if (entity) {
entity.status = update.status;
}
}
}
});
console.log('RTS: Updated entity positions:', responseJson.entityUpdates);
}
// Process AI response state data to sync with map
syncAIResponseWithMap(responseJson.state);
if (typeof sendNarratorMessage === 'function') {
sendNarratorMessage({}, narrative);
}
// Update the UI with the new state
try {
updateResourcePanel(responseJson.state);
} catch (uiError) {
console.warn('RTS: Error updating resource panel UI:', uiError);
}
// Dispatch a custom event with the narrative for other UI components
document.dispatchEvent(new CustomEvent('rts-narrative-update', {
detail: {
type: 'llm',
message: narrative,
state: responseJson.state,
entityUpdates: responseJson.entityUpdates || []
}
}));
console.log('RTS Narrative:', narrative);
} else {
// Fallback for old format or errors
const diff = extractJson(reply); const diff = extractJson(reply);
if (diff) { if (diff) {
GameStateManager.applyDiff(diff); GameStateManager.applyDiff(diff);
// For now, log the narrative part to the console.
const narrative = reply.replace(/```json\n[\s\S]+?\n```/, '').trim(); const narrative = reply.replace(/```json\n[\s\S]+?\n```/, '').trim();
console.log('RTS Narrative:', narrative); document.dispatchEvent(new CustomEvent('rts-narrative-update', { detail: { type: 'llm', message: narrative } }));
console.log('RTS Narrative (fallback):', narrative);
} else { } else {
console.warn('RTS-Mode: No valid JSON diff found in LLM response.'); document.dispatchEvent(new CustomEvent('rts-narrative-update', { detail: { type: 'event', message: reply.trim() } }));
// Log the raw reply for debugging. console.warn('RTS-Mode: No valid JSON found. Treating response as pure narrative.');
console.log('Raw LLM Response:', reply); console.log('Raw LLM Response:', reply);
} }
}
} catch (error) { } catch (error) {
console.error('RTS-Mode: Error during sendTurn:', error); console.error('RTS-Mode: Error during sendTurn:', error);
// Optionally, display an alert to the user.
alert(`RTS-Mode Error: ${error.message}`); alert(`RTS-Mode Error: ${error.message}`);
} }
} }
/**
* Synchronizes AI response state data with the map
* @param {object} state - The state from AI response
*/
function syncAIResponseWithMap(state) {
try {
// Update player position if provided
if (state.playerPosition && typeof state.playerPosition.x === 'number' && typeof state.playerPosition.y === 'number') {
GameStateManager.updatePlayerPosition(state.playerPosition.x, state.playerPosition.y);
console.log('RTS: Updated player position to:', state.playerPosition);
}
// Add/update visible entities on the map
if (state.visibleEntities && Array.isArray(state.visibleEntities)) {
state.visibleEntities.forEach(entity => {
if (entity.id && typeof entity.position === 'object' && entity.position.x !== undefined && entity.position.y !== undefined) {
// Use position.x and position.y from the AI response format
const mapEntity = {
id: entity.id,
type: entity.type,
name: entity.name,
x: entity.position.x,
y: entity.position.y,
status: entity.status || 'active',
description: entity.action || entity.description || ''
};
// Map generic "personnel" type to more specific types
if (mapEntity.type === 'personnel') {
if (entity.name && entity.name.toLowerCase().includes('keeper')) {
mapEntity.type = 'keeper';
} else if (entity.name && entity.name.toLowerCase().includes('vet')) {
mapEntity.type = 'veterinarian';
} else {
mapEntity.type = 'staff';
}
}
GameStateManager.addOrUpdateMapEntity(mapEntity);
} else if (entity.id && typeof entity.x === 'number' && typeof entity.y === 'number') {
// Handle direct x,y coordinates format
const mapEntity = {
id: entity.id,
type: entity.type,
name: entity.name,
x: entity.x,
y: entity.y,
status: entity.status || 'active',
description: entity.action || entity.description || ''
};
// Map generic "personnel" type to more specific types
if (mapEntity.type === 'personnel') {
if (entity.name && entity.name.toLowerCase().includes('keeper')) {
mapEntity.type = 'keeper';
} else if (entity.name && entity.name.toLowerCase().includes('vet')) {
mapEntity.type = 'veterinarian';
} else {
mapEntity.type = 'staff';
}
}
GameStateManager.addOrUpdateMapEntity(mapEntity);
}
});
console.log('RTS: Processed visible entities:', state.visibleEntities.length);
}
// Add new personnel to map if they're not already there
if (state.personnel && state.personnel.alive) {
state.personnel.alive.forEach(person => {
if (person.position && typeof person.position.x === 'number' && typeof person.position.y === 'number') {
const mapEntity = {
id: person.id,
type: person.role?.toLowerCase() || 'visitor',
name: person.name,
x: person.position.x,
y: person.position.y,
status: person.status || 'active',
description: person.details || `${person.role || 'Person'} at the zoo`
};
GameStateManager.addOrUpdateMapEntity(mapEntity);
}
});
}
// Trigger map refresh and canvas update
document.dispatchEvent(new CustomEvent('rts-map-update', {
detail: { reason: 'ai-response-sync' }
}));
// Force canvas redraw
document.dispatchEvent(new CustomEvent('rts-canvas-refresh', {
detail: { reason: 'entity-updates' }
}));
} catch (error) {
console.error('RTS: Error syncing AI response with map:', error);
}
}
+83
View File
@@ -0,0 +1,83 @@
class PresetManager {
constructor() {
this.presets = new Map();
this.mapData = new Map();
this.activePreset = null;
}
async loadPreset(presetPath) {
if (this.presets.has(presetPath)) {
this.activePreset = this.presets.get(presetPath);
return;
}
try {
const response = await fetch(presetPath);
if (!response.ok) {
throw new Error(`Failed to fetch preset: ${response.statusText}`);
}
const presetData = await response.json();
this.presets.set(presetPath, presetData);
this.activePreset = presetData;
// Load associated map data if specified
if (presetData.map) {
await this.loadMapData(presetData.map);
}
console.log('RTS-MODE: Preset loaded successfully:', presetData.name);
} catch (error) {
console.error(`RTS-MODE: Error loading preset from ${presetPath}:`, error);
throw error;
}
}
async loadMapData(mapPath) {
if (this.mapData.has(mapPath)) {
return;
}
try {
const response = await fetch(mapPath);
if (!response.ok) {
throw new Error(`Failed to fetch map: ${response.statusText}`);
}
const mapDataJson = await response.json();
this.mapData.set(mapPath, mapDataJson);
console.log('RTS-MODE: Map data loaded successfully:', mapDataJson.name);
} catch (error) {
console.error(`RTS-MODE: Error loading map from ${mapPath}:`, error);
throw error;
}
}
getPreset() {
if (!this.activePreset) {
throw new Error("RTS-MODE: No active preset loaded.");
}
return this.activePreset;
}
getFeature(featureName) {
const preset = this.getPreset();
return preset.features ? preset.features[featureName] : undefined;
}
getInitialState() {
return this.getPreset().initialState || {};
}
getPrompt(promptName) {
const preset = this.getPreset();
return preset.prompts ? preset.prompts[promptName] : undefined;
}
getMapData() {
const preset = this.getPreset();
if (!preset.map) return null;
return this.mapData.get(preset.map) || null;
}
}
const instance = new PresetManager();
export default instance;
+172 -3
View File
@@ -1,9 +1,178 @@
/** /**
* Builds a prompt for the LLM based on the current game state and user command. * Builds a comprehensive prompt for the LLM based on the current game state, map context, and user command.
* @param {string} stateJSON - The compressed JSON string of the game state. * @param {string} stateJSON - The compressed JSON string of the game state.
* @param {string} userCmd - The user's command. * @param {string} userCmd - The user's command.
* @param {string} event - The current narrative event.
* @param {boolean} isExplicit - Whether to request explicit content.
* @returns {string} The formatted prompt. * @returns {string} The formatted prompt.
*/ */
export function buildPrompt(stateJSON, userCmd) { export function buildPrompt(stateJSON, userCmd, event, isExplicit) {
return `<RTS-STATE>${stateJSON}</RTS-STATE>\n<USER-CMD>${userCmd}</USER-CMD>\nRespond with JSON diff + narrative.`; const state = JSON.parse(stateJSON);
let prompt = `You are the Game Master for an RTS-style zoo scenario. Your job is to:
1. Process the player's action and update the game state comprehensively
2. Create a SHORT narrative (2-3 sentences max) focusing on immediate events
3. Update detailed character information for all visible NPCs and animals
4. Track casualties, escaped animals, active incidents, and personnel status changes
SCENARIO PROGRESSION GUIDE:
- Turn 0: Normal zoo operations, peaceful atmosphere, no incidents yet
- Turn 1-2: First signs of trouble, minor incidents, animals becoming agitated
- Turn 3+: Full chaos mode, animals escaped, survival scenario activated
IMPORTANT RESPONSE FORMAT:
Return a single JSON object with keys 'state' and 'narrative'. The 'state' object should include ALL the tracking systems below.
TRACKING REQUIREMENTS:
- Update casualties with specific death details (name, cause, location, perpetrator, description)
- Track escaped animals with current locations and behavior
- Maintain active incidents (emergency/ongoing/resolved)
- Update personnel status (alive/injured/missing/evacuated) with positions
- Update mapState with player position and visible entities
- Environment changes (time, weather, power, evacuation status)
MAP CONTEXT:
${buildMapContext(state)}
CURRENT SITUATION:
${buildSituationReport(state)}
<RTS-STATE>${stateJSON}</RTS-STATE>
<NARRATIVE-EVENT>${event}</NARRATIVE-EVENT>
<USER-CMD>${userCmd}</USER-CMD>
RESPONSE GUIDELINES:
- Narrative should be brief and atmospheric, focus on immediate sensory details
- The real action happens through state updates - show personnel interactions, animal movements, incidents
- Update ALL visible characters and animals with specific actions/status changes
- Track map movement and visibility realistically
- Keep death descriptions vivid but brief
- Update threat levels based on escalating situations
MOVEMENT AND ENTITY UPDATES:
- When entities move, update their positions in the response
- Include 'entityUpdates' array in your response with position changes
- Format: {"id": "entityId", "x": newX, "y": newY, "status": "new status"}
- Update player position when they move between zones
- Show consequences of movement (encounters, discoveries, escapes)`;
if (isExplicit) {
prompt += '\n- Content should be mature, with realistic predator/prey dynamics and explicit violence';
}
return prompt;
}
/**
* Builds map context information for the LLM
* @param {object} state - Current game state
* @returns {string} Map context description
*/
function buildMapContext(state) {
const playerPos = state.playerPosition || { x: 0, y: 0 };
const visibleEntities = state.visibleEntities || [];
let context = `PLAYER POSITION: (${playerPos.x}, ${playerPos.y})
CURRENT ZONE: ${state.currentZone || 'Unknown'}
THREAT LEVEL: ${state.threatLevel}
VISIBLE ENTITIES (within 5 tiles):`;
if (visibleEntities.length === 0) {
context += "\n- No entities currently visible";
} else {
visibleEntities.forEach(entity => {
const distance = Math.sqrt(
Math.pow(entity.x - playerPos.x, 2) +
Math.pow(entity.y - playerPos.y, 2)
).toFixed(1);
context += `\n- ${entity.name || entity.type} (${entity.type}) at (${entity.x}, ${entity.y}) - ${distance} tiles away`;
if (entity.status) {
context += ` [${entity.status}]`;
}
if (entity.description) {
context += ` - ${entity.description}`;
}
});
}
// Add movement context
context += `\n\nMOVEMENT NOTES:
- Zoo map is roughly 60x60 tiles
- Player can move to adjacent areas by stating direction/destination
- Moving reveals new entities and may trigger encounters
- Animals and people also move and react to player actions`;
return context;
}
/**
* Builds current situation report for the LLM
* @param {object} state - Current game state
* @returns {string} Situation report
*/
function buildSituationReport(state) {
let report = `TURN: ${state.turn} ${state.turn === 0 ? '(PRE-INCIDENT - Normal Zoo Operations)' : ''}
${state.turn === 0 ? 'ZOO STATUS: NORMAL - Peaceful evening, all animals secure, visitors enjoying exhibits' : 'CASUALTIES STATUS:'}`;
if (state.turn > 0) {
report += `
- Total Deaths: ${state.casualties?.total || 0}
- Recent Deaths: ${(state.casualties?.recent || []).length}`;
if (state.casualties?.recent && state.casualties.recent.length > 0) {
report += "\n- Last Casualties:";
state.casualties.recent.slice(0, 3).forEach(casualty => {
report += `\n * ${casualty.name} - ${casualty.cause} by ${casualty.perpetrator} at ${casualty.location}`;
});
}
}
if (state.turn === 0) {
report += `\n\nANIMAL STATUS: All animals secure in enclosures, exhibiting normal behavior`;
} else {
report += `\n\nESCAPED ANIMALS:
- Total Escaped: ${state.escapedAnimals?.total || 0}
- Currently Active: ${(state.escapedAnimals?.active || []).length}`;
if (state.escapedAnimals?.active && state.escapedAnimals.active.length > 0) {
report += "\n- Active Escapees:";
state.escapedAnimals.active.forEach(animal => {
report += `\n * ${animal.name} (${animal.type}) - ${animal.behavior} at ${animal.currentLocation}`;
});
}
}
report += `\n\nPERSONNEL STATUS:
- Alive: ${(state.personnel?.alive || []).length}
- Injured: ${(state.personnel?.injured || []).length}
- Missing: ${(state.personnel?.missing || []).length}
- Evacuated: ${(state.personnel?.evacuated || []).length}`;
if (state.personnel?.alive && state.personnel.alive.length > 0) {
report += "\n- Active Personnel:";
state.personnel.alive.forEach(person => {
report += `\n * ${person.name} (${person.type}) - ${person.status} at (${person.position.x}, ${person.position.y})`;
});
}
report += `\n\nACTIVE INCIDENTS:
- Emergency: ${(state.activeIncidents?.emergency || []).length}
- Ongoing: ${(state.activeIncidents?.ongoing || []).length}`;
if (state.activeIncidents?.emergency && state.activeIncidents.emergency.length > 0) {
report += "\n- Emergency Incidents:";
state.activeIncidents.emergency.forEach(incident => {
report += `\n * ${incident.type || 'Unknown'} - ${incident.description || 'No details'}`;
});
}
report += `\n\nENVIRONMENT:
- Time: ${state.environment?.timeOfDay || 'unknown'}
- Weather: ${state.environment?.weather || 'unknown'}
- Power: ${state.environment?.powerStatus || 'unknown'}
- Evacuation: ${state.environment?.evacuationStatus || 'unknown'}`;
return report;
} }
+193 -12
View File
@@ -106,13 +106,13 @@
} }
/* Units and Buildings */ /* Units and Buildings */
.rts-units-list, .rts-buildings-list { .rts-units-list, .rts-buildings-list, .rts-threat-display {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.rts-unit-item, .rts-building-item { .rts-unit-item, .rts-building-item, .rts-threat-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -123,7 +123,7 @@
color: inherit; color: inherit;
} }
.rts-unit-item i, .rts-building-item i { .rts-unit-item i, .rts-building-item i, .rts-threat-item i {
opacity: 0.8; opacity: 0.8;
width: 16px; width: 16px;
} }
@@ -196,10 +196,6 @@
.rts-map-wrapper { .rts-map-wrapper {
position: relative; position: relative;
background: linear-gradient(45deg, #1a2332 0%, #2d3748 50%, #1a202c 100%); background: linear-gradient(45deg, #1a2332 0%, #2d3748 50%, #1a202c 100%);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 8px; border-radius: 8px;
margin: 8px; margin: 8px;
@@ -232,8 +228,8 @@
max-height: 100%; max-height: 100%;
display: block; display: block;
image-rendering: crisp-edges; image-rendering: crisp-edges;
z-index: 2;
position: relative; position: relative;
z-index: 2;
} }
.rts-map-overlay { .rts-map-overlay {
@@ -242,7 +238,8 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
pointer-events: none; /* Allow interaction inside overlay tooltips and context menus */
pointer-events: auto;
z-index: 10; z-index: 10;
} }
@@ -253,8 +250,8 @@
border-radius: 8px; border-radius: 8px;
color: white; color: white;
font-size: 12px; font-size: 12px;
min-width: 150px; min-width: 180px;
max-width: 250px; max-width: 300px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
pointer-events: auto; pointer-events: auto;
@@ -303,6 +300,7 @@
border-color: rgba(239, 68, 68, 0.6); border-color: rgba(239, 68, 68, 0.6);
color: white; color: white;
transform: scale(1.1); transform: scale(1.1);
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
} }
.rts-info-content { .rts-info-content {
@@ -310,6 +308,16 @@
line-height: 1.4; line-height: 1.4;
} }
.rts-info-content p {
margin: 0 0 8px 0;
color: rgba(255, 255, 255, 0.9);
}
.rts-info-content div {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
}
.rts-info-content > div { .rts-info-content > div {
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -539,10 +547,114 @@
} }
#rts-resource-panel { #rts-resource-panel {
width: 200px; width: 280px;
background: var(--SmartThemeEmColor); background: var(--SmartThemeEmColor);
padding: 8px; padding: 8px;
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 8px;
max-height: calc(100vh - 120px);
overflow-y: auto;
}
.rts-status-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.rts-panel-section {
background: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 10px;
}
.rts-panel-section h3 {
margin: 0 0 8px 0;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--SmartThemeQuoteColor);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 4px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
font-size: 11px;
color: var(--SmartThemeQuoteColor);
}
.stat-item span {
font-weight: 600;
color: #fff;
}
.scrollable-list {
max-height: 120px;
overflow-y: auto;
margin-top: 6px;
}
.list-item {
padding: 6px;
margin-bottom: 4px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
font-size: 10px;
line-height: 1.3;
transition: all 0.2s ease;
}
.list-item:last-child {
margin-bottom: 0;
}
.list-item:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
transform: translateX(2px);
}
.list-item.casualty {
border-left: 3px solid #dc2626;
}
.list-item.animal {
border-left: 3px solid #f97316;
}
.list-item.personnel.alive {
border-left: 3px solid #22c55e;
}
.list-item.personnel.injured {
border-left: 3px solid #eab308;
}
.list-item.incident.emergency {
border-left: 3px solid #dc2626;
background: rgba(220, 38, 38, 0.1);
}
.list-item.incident.ongoing {
border-left: 3px solid #3b82f6;
}
.list-item strong {
color: #fff;
font-weight: 600;
}
.list-item small {
color: rgba(255, 255, 255, 0.7);
display: block;
margin-top: 2px;
} }
#rts-resource-panel ul { #rts-resource-panel ul {
@@ -959,3 +1071,72 @@
box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.1); box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.1);
} }
} }
/* Settings Toggle Switch */
.rts-settings-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
background: rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.rts-toggle-label {
font-weight: 500;
}
.rts-switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.rts-switch input {
opacity: 0;
width: 0;
height: 0;
}
.rts-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.rts-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .rts-slider {
background-color: #dc2626;
}
input:focus + .rts-slider {
box-shadow: 0 0 1px #dc2626;
}
input:checked + .rts-slider:before {
transform: translateX(22px);
}
.rts-slider.round {
border-radius: 34px;
}
.rts-slider.round:before {
border-radius: 50%;
}
+505 -59
View File
@@ -1,3 +1,5 @@
import PresetManager from '../src/PresetManager.js';
export class RTSMapCanvas { export class RTSMapCanvas {
constructor(canvasId = 'rts-game-map') { constructor(canvasId = 'rts-game-map') {
this.canvas = /** @type {HTMLCanvasElement} */ (document.getElementById(canvasId)); this.canvas = /** @type {HTMLCanvasElement} */ (document.getElementById(canvasId));
@@ -15,12 +17,12 @@ export class RTSMapCanvas {
this.isDragging = false; this.isDragging = false;
this.lastMouseX = 0; this.lastMouseX = 0;
this.lastMouseY = 0; this.lastMouseY = 0;
this.selectedUnit = null; this.selectedEntities = [];
this.selectedBuilding = null;
this.hoveredElement = null; this.hoveredElement = null;
this.animationFrame = null; this.animationFrame = null;
this.lastRenderTime = 0; this.lastRenderTime = 0;
this.dirty = true; this.dirty = true;
this.fogOfWarEnabled = true;
this.fogOfWar = []; this.fogOfWar = [];
this.exploredAreas = new Set(); this.exploredAreas = new Set();
@@ -94,8 +96,17 @@ export class RTSMapCanvas {
loadMap(mapData) { loadMap(mapData) {
this.mapData = mapData; this.mapData = mapData;
this.fogOfWarEnabled = PresetManager.getFeature('fogOfWar');
if (this.fogOfWarEnabled) {
this.initializeFogOfWar(); this.initializeFogOfWar();
}
this.dirty = true; this.dirty = true;
console.log('RTS: Map loaded with', this.mapData?.map?.entities?.length || 0, 'entities');
// Force an immediate render to show updated entities
if (this.mapData) {
this.render();
}
} }
render() { render() {
@@ -111,8 +122,10 @@ export class RTSMapCanvas {
if (this.mapData) { if (this.mapData) {
this.drawTerrain(); this.drawTerrain();
this.drawEntities(); this.drawEntities();
if (this.fogOfWarEnabled) {
this.drawFogOfWar(); this.drawFogOfWar();
} }
}
this.drawParticles(); this.drawParticles();
ctx.restore(); ctx.restore();
@@ -131,50 +144,305 @@ export class RTSMapCanvas {
drawTerrain() { drawTerrain() {
const ctx = this.ctx; const ctx = this.ctx;
const { width, height, tiles } = this.mapData.map; const { width, height, tiles, tileTypes } = this.mapData.map;
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
if (tiles[y]) { if (tiles[y]) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const tileType = tiles[y][x]; const tileType = tiles[y][x];
let color; let color = '#334155'; // default path color
// Use new tile type system if available
if (tileTypes && tileTypes[tileType]) {
color = tileTypes[tileType].color;
} else {
// Fallback for old format
switch (tileType) { switch (tileType) {
case 1: color = '#22c55e'; break; // enclosure case 1: color = '#654321'; break; // enclosure fence
case 2: color = '#475569'; break; // wall case 2: color = '#5A5A5A'; break; // building wall
case 3: color = '#f59e0b'; break; // locked gate case 3: color = '#FFD700'; break; // entrance gate
case 4: color = '#ef4444'; break; // security camera case 4: color = '#4A90E2'; break; // water
default: color = '#334155'; break; // path case 5: color = '#228B22'; break; // grass
case 6: color = '#006400'; break; // trees
case 7: color = '#90EE90'; break; // enclosure interior
case 8: color = '#D3D3D3'; break; // building interior
case 9: color = '#696969'; break; // parking
default: color = '#8B7D6B'; break; // path
} }
}
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.fillRect(x * this.tileSize, y * this.tileSize, this.tileSize, this.tileSize); ctx.fillRect(x * this.tileSize, y * this.tileSize, this.tileSize, this.tileSize);
// Add terrain-specific visual effects
if (tileTypes && tileTypes[tileType]) {
this.addTerrainEffects(ctx, x, y, tileTypes[tileType]);
}
// Add subtle border for better definition
ctx.strokeStyle = 'rgba(0,0,0,0.1)';
ctx.lineWidth = 0.5;
ctx.strokeRect(x * this.tileSize, y * this.tileSize, this.tileSize, this.tileSize);
} }
} }
} }
// Draw zones
this.drawZones();
// Draw points of interest
this.drawPointsOfInterest();
}
drawZones() {
if (!this.mapData.map.zones) return;
const ctx = this.ctx;
ctx.save();
ctx.globalAlpha = 0.3;
this.mapData.map.zones.forEach((zone, index) => {
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dda0dd'];
ctx.fillStyle = colors[index % colors.length];
const width = zone.bounds.x2 - zone.bounds.x1;
const height = zone.bounds.y2 - zone.bounds.y1;
ctx.fillRect(
zone.bounds.x1 * this.tileSize,
zone.bounds.y1 * this.tileSize,
width * this.tileSize,
height * this.tileSize
);
// Zone label
ctx.globalAlpha = 1;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px sans-serif';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
const centerX = (zone.bounds.x1 + width / 2) * this.tileSize;
const centerY = (zone.bounds.y1 + height / 2) * this.tileSize;
ctx.strokeText(zone.name, centerX - 30, centerY);
ctx.fillText(zone.name, centerX - 30, centerY);
ctx.globalAlpha = 0.3;
});
ctx.restore();
}
drawPointsOfInterest() {
if (!this.mapData.map.pointsOfInterest) return;
const ctx = this.ctx;
ctx.save();
this.mapData.map.pointsOfInterest.forEach(poi => {
const x = poi.x * this.tileSize + this.tileSize / 2;
const y = poi.y * this.tileSize + this.tileSize / 2;
// Background circle with gradient
const gradient = ctx.createRadialGradient(x, y, 0, x, y, this.tileSize / 3);
gradient.addColorStop(0, this.getPoiColor(poi.type));
gradient.addColorStop(1, this.darkenColor(this.getPoiColor(poi.type), 0.3));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, this.tileSize / 3, 0, Math.PI * 2);
ctx.fill();
// Border with glow effect
ctx.shadowColor = this.getPoiColor(poi.type);
ctx.shadowBlur = 5;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowBlur = 0;
// Icon with better positioning
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(this.getPoiIcon(poi.type), x, y + 4);
// POI name label
ctx.fillStyle = '#000000';
ctx.font = '8px sans-serif';
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.strokeText(poi.name, x, y + this.tileSize / 2 + 8);
ctx.fillText(poi.name, x, y + this.tileSize / 2 + 8);
});
ctx.restore();
}
getPoiColor(type) {
switch (type) {
case 'exit': return '#ff4757';
case 'building': return '#5f27cd';
case 'medical': return '#00d2d3';
case 'weapon': return '#ff9ff3';
default: return '#747d8c';
}
}
getPoiIcon(type) {
switch (type) {
case 'exit': return '🚪';
case 'building': return '🏢';
case 'medical': return '⚕';
case 'weapon': return '🛡';
default: return '📍';
}
}
getEntityAppearance(entity) {
switch (entity.type) {
case 'player': return { color: '#3b82f6', icon: '🧑' };
case 'visitor': return { color: '#6b7280', icon: '👤' };
case 'keeper': return { color: '#10b981', icon: '👨‍🔬' };
case 'veterinarian': return { color: '#8b5cf6', icon: '👨‍⚕️' };
case 'lion': return { color: '#eab308', icon: '🦁' };
case 'tiger': return { color: '#f59e0b', icon: '🐅' };
case 'jaguar': return { color: '#d97706', icon: '🐆' };
case 'snow_leopard': return { color: '#a16207', icon: '🐆' };
case 'wolf': return { color: '#84cc16', icon: '🐺' };
case 'bear':
case 'grizzly_bear': return { color: '#a3e635', icon: '🐻' };
case 'polar_bear': return { color: '#ecfccb', icon: '🐻‍❄️' };
case 'panda': return { color: '#ffffff', icon: '🐼' };
case 'gorilla': return { color: '#654321', icon: '🦍' };
case 'chimpanzee': return { color: '#8b4513', icon: '🐵' };
case 'crocodile': return { color: '#228b22', icon: '🐊' };
default: return { color: '#9ca3af', icon: '❓' };
}
}
drawPlayerSight() {
if (!this.mapData || !this.mapData.map.entities) return;
const player = this.mapData.map.entities.find(e => e.type === 'player');
if (!player) return;
const ctx = this.ctx;
const playerX = player.x * this.tileSize + this.tileSize / 2;
const playerY = player.y * this.tileSize + this.tileSize / 2;
const sightRadius = 5 * this.tileSize; // 5 tile radius
// Draw sight range circle
ctx.save();
ctx.globalAlpha = 0.1;
ctx.fillStyle = '#3b82f6';
ctx.beginPath();
ctx.arc(playerX, playerY, sightRadius, 0, Math.PI * 2);
ctx.fill();
// Draw sight range border
ctx.globalAlpha = 0.3;
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Draw visibility indicators for entities within sight
this.mapData.map.entities.forEach(entity => {
if (entity.type === 'player') return;
const entityX = entity.x * this.tileSize + this.tileSize / 2;
const entityY = entity.y * this.tileSize + this.tileSize / 2;
const distance = Math.sqrt(
Math.pow(entityX - playerX, 2) +
Math.pow(entityY - playerY, 2)
);
if (distance <= sightRadius) {
// Draw line of sight
ctx.save();
ctx.globalAlpha = 0.2;
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.moveTo(playerX, playerY);
ctx.lineTo(entityX, entityY);
ctx.stroke();
ctx.restore();
// Draw visibility indicator
ctx.save();
ctx.fillStyle = '#3b82f6';
ctx.font = 'bold 8px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('👁', entityX - 12, entityY - 12);
ctx.restore();
}
});
} }
drawEntities() { drawEntities() {
const ctx = this.ctx; const ctx = this.ctx;
const { entities } = this.mapData.map; const { entities } = this.mapData.map;
// Draw player sight indicators first (so they appear behind entities)
this.drawPlayerSight();
entities.forEach(entity => { entities.forEach(entity => {
let color; const centerX = entity.x * this.tileSize + this.tileSize / 2;
switch (entity.type) { const centerY = entity.y * this.tileSize + this.tileSize / 2;
case 'player': color = '#3b82f6'; break; const radius = this.tileSize / 3;
case 'guard': color = '#f97316'; break;
case 'lion': color = '#eab308'; break; // Get entity appearance
case 'tiger': color = '#f59e0b'; break; const appearance = this.getEntityAppearance(entity);
case 'jaguar': color = '#d97706'; break;
case 'snow_leopard': color = '#a16207'; break; // Draw background circle
case 'wolf': color = '#84cc16'; break; ctx.fillStyle = appearance.color;
case 'grizzly_bear': color = '#a3e635'; break;
case 'polar_bear': color = '#ecfccb'; break;
case 'panda': color = '#ffffff'; break;
default: color = '#9ca3af'; break;
}
ctx.fillStyle = color;
ctx.beginPath(); ctx.beginPath();
ctx.arc(entity.x * this.tileSize + this.tileSize / 2, entity.y * this.tileSize + this.tileSize / 2, this.tileSize / 2, 0, Math.PI * 2); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Draw border
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
// Draw icon/emoji
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(appearance.icon, centerX, centerY + 4);
// Draw threat indicator for dangerous animals
if (entity.threat === 'extreme' || entity.threat === 'high') {
ctx.strokeStyle = entity.threat === 'extreme' ? '#dc2626' : '#f59e0b';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 3, 0, Math.PI * 2);
ctx.stroke();
}
// Selection highlight
if (this.selectedEntities.includes(entity)) {
ctx.strokeStyle = '#fbbf24';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 6, 0, Math.PI * 2);
ctx.stroke();
}
// Draw name label for important entities
if (entity.type === 'player' || entity.threat === 'extreme') {
ctx.fillStyle = '#000000';
ctx.font = 'bold 10px sans-serif';
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.strokeText(entity.name || entity.type, centerX, centerY + radius + 15);
ctx.fillText(entity.name || entity.type, centerX, centerY + radius + 15);
}
}); });
} }
@@ -292,12 +560,23 @@ export class RTSMapCanvas {
const viewportWidth = this.canvas.width / this.zoom; const viewportWidth = this.canvas.width / this.zoom;
const viewportHeight = this.canvas.height / this.zoom; const viewportHeight = this.canvas.height / this.zoom;
const viewportX = minimapX + (this.canvas.width / 2 - this.offsetX - viewportWidth / 2) * scaleX; let viewportX = minimapX + (this.canvas.width / 2 - this.offsetX - viewportWidth / 2) * scaleX;
const viewportY = minimapY + (this.canvas.height / 2 - this.offsetY - viewportHeight / 2) * scaleY; let viewportY = minimapY + (this.canvas.height / 2 - this.offsetY - viewportHeight / 2) * scaleY;
const viewportScaledWidth = viewportWidth * scaleX;
const viewportScaledHeight = viewportHeight * scaleY;
// Constrain viewport indicator to minimap bounds
viewportX = Math.max(minimapX, Math.min(viewportX, minimapX + minimapSize - viewportScaledWidth));
viewportY = Math.max(minimapY, Math.min(viewportY, minimapY + minimapSize - viewportScaledHeight));
// Ensure viewport size doesn't exceed minimap size
const constrainedWidth = Math.min(viewportScaledWidth, minimapSize);
const constrainedHeight = Math.min(viewportScaledHeight, minimapSize);
ctx.strokeStyle = '#fbbf24'; ctx.strokeStyle = '#fbbf24';
ctx.lineWidth = 1; ctx.lineWidth = 2;
ctx.strokeRect(viewportX, viewportY, viewportWidth * scaleX, viewportHeight * scaleY); ctx.strokeRect(viewportX, viewportY, constrainedWidth, constrainedHeight);
} }
startAnimationLoop() { startAnimationLoop() {
@@ -348,46 +627,82 @@ export class RTSMapCanvas {
return null; return null;
} }
selectElement(element) { selectElement(element) {
this.selectedUnit = null;
this.selectedBuilding = null;
if (element.type === 'entity') { if (element.type === 'entity') {
this.selectedUnit = element.data; this.selectedEntities = [element.data];
} }
this.showElementInfo(element); this.showElementInfo(element);
this.canvas.dispatchEvent(new Event('selectionChanged'));
} }
clearSelection() { clearSelection() {
this.selectedUnit = null; this.selectedEntities = [];
this.selectedBuilding = null;
this.hideElementInfo(); this.hideElementInfo();
this.canvas.dispatchEvent(new Event('selectionChanged'));
} }
showElementInfo(element) { showElementInfo(element) {
console.debug('[showElementInfo]', element);
const overlay = document.getElementById('rts-map-overlay'); const overlay = document.getElementById('rts-map-overlay');
if (!overlay) return; if (!overlay) return;
// Clear any existing tooltips
overlay.innerHTML = '';
const screenPos = this.worldToScreen(element.data.x * this.tileSize, element.data.y * this.tileSize); const screenPos = this.worldToScreen(element.data.x * this.tileSize, element.data.y * this.tileSize);
let infoHTML = '';
if (element.type === 'entity') { if (element.type === 'entity') {
infoHTML = ` const entity = element.data;
<div class="rts-element-info" style="position: absolute; top: ${screenPos.y + 20}px; left: ${screenPos.x + 20}px;"> const name = entity.name || (entity.type.charAt(0).toUpperCase() + entity.type.slice(1));
<div class="rts-info-header"> const description = entity.description || 'No description available.';
<strong>${element.data.type.charAt(0).toUpperCase() + element.data.type.slice(1)}</strong>
<button class="rts-info-close" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
<div class="rts-info-content">
<div>X: ${element.data.x}, Y: ${element.data.y}</div>
</div>
</div>
`;
}
overlay.innerHTML = infoHTML; const infoBox = document.createElement('div');
infoBox.className = 'rts-element-info';
/* Position tooltip relative to viewport so it isn't clipped by overlay */
const rect = this.canvas.getBoundingClientRect();
infoBox.style.position = 'fixed';
infoBox.style.top = `${rect.top + screenPos.y + 20}px`;
infoBox.style.left = `${rect.left + screenPos.x + 20}px`;
/* Ensure the tooltip sits above the main UI container */
infoBox.style.zIndex = '2001';
const header = document.createElement('div');
header.className = 'rts-info-header';
const nameEl = document.createElement('strong');
nameEl.textContent = name;
const closeBtn = document.createElement('button');
closeBtn.className = 'rts-info-close';
closeBtn.innerHTML = '×';
closeBtn.onclick = () => infoBox.remove();
header.appendChild(nameEl);
header.appendChild(closeBtn);
const content = document.createElement('div');
content.className = 'rts-info-content';
const descEl = document.createElement('p');
descEl.textContent = description;
const coordsEl = document.createElement('div');
coordsEl.textContent = `Coordinates: ${entity.x}, ${entity.y}`;
content.appendChild(descEl);
content.appendChild(coordsEl);
infoBox.appendChild(header);
infoBox.appendChild(content);
document.body.appendChild(infoBox);
}
} }
hideElementInfo() { hideElementInfo() {
// Remove any existing info boxes
document.querySelectorAll('.rts-element-info').forEach(el => el.remove());
const overlay = document.getElementById('rts-map-overlay'); const overlay = document.getElementById('rts-map-overlay');
if (overlay) { if (overlay) {
overlay.innerHTML = ''; overlay.innerHTML = '';
@@ -398,22 +713,91 @@ export class RTSMapCanvas {
const overlay = document.getElementById('rts-map-overlay'); const overlay = document.getElementById('rts-map-overlay');
if (!overlay) return; if (!overlay) return;
const contextMenuHTML = ` // Convert world coordinates to tile coordinates
<div class="rts-context-menu" style="position: absolute; top: ${screenY}px; left: ${screenX}px;"> const tileX = Math.floor(worldX / this.tileSize);
<div class="rts-context-item" onclick="this.parentElement.remove()">Move Here</div> const tileY = Math.floor(worldY / this.tileSize);
<div class="rts-context-item" onclick="this.parentElement.remove()">Cancel</div>
</div>
`;
overlay.innerHTML = contextMenuHTML; const contextMenu = document.createElement('div');
contextMenu.className = 'rts-context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.top = `${screenY}px`;
contextMenu.style.left = `${screenX}px`;
contextMenu.style.background = '#2d3748';
contextMenu.style.border = '1px solid #4a5568';
contextMenu.style.borderRadius = '4px';
contextMenu.style.padding = '4px';
contextMenu.style.zIndex = '9999';
const moveItem = document.createElement('div');
moveItem.className = 'rts-context-item';
moveItem.textContent = `Move to (${tileX}, ${tileY})`;
moveItem.style.padding = '8px 12px';
moveItem.style.cursor = 'pointer';
moveItem.style.color = '#ffffff';
moveItem.addEventListener('click', () => {
this.handleMoveCommand(tileX, tileY);
contextMenu.remove();
});
moveItem.addEventListener('mouseenter', () => {
moveItem.style.background = '#4a5568';
});
moveItem.addEventListener('mouseleave', () => {
moveItem.style.background = 'transparent';
});
const cancelItem = document.createElement('div');
cancelItem.className = 'rts-context-item';
cancelItem.textContent = 'Cancel';
cancelItem.style.padding = '8px 12px';
cancelItem.style.cursor = 'pointer';
cancelItem.style.color = '#a0aec0';
cancelItem.addEventListener('click', () => {
contextMenu.remove();
});
cancelItem.addEventListener('mouseenter', () => {
cancelItem.style.background = '#4a5568';
});
cancelItem.addEventListener('mouseleave', () => {
cancelItem.style.background = 'transparent';
});
contextMenu.appendChild(moveItem);
contextMenu.appendChild(cancelItem);
overlay.innerHTML = '';
overlay.appendChild(contextMenu);
// Auto-remove on outside click
setTimeout(() => { setTimeout(() => {
document.addEventListener('click', () => { const handleOutsideClick = (e) => {
const menu = document.querySelector('.rts-context-menu'); if (!contextMenu.contains(e.target)) {
if (menu) menu.remove(); contextMenu.remove();
}, { once: true }); document.removeEventListener('click', handleOutsideClick);
}
};
document.addEventListener('click', handleOutsideClick);
}, 100); }, 100);
} }
handleMoveCommand(tileX, tileY) {
// Fill the command input with move command
const commandInput = document.getElementById('rts-command-input');
if (commandInput) {
commandInput.value = `Move to coordinates (${tileX}, ${tileY}).`;
commandInput.focus();
// Trigger a visual indication that the command was set
commandInput.style.borderColor = '#3b82f6';
setTimeout(() => {
commandInput.style.borderColor = '';
}, 1000);
}
// Dispatch event for UI controller to handle
this.canvas.dispatchEvent(new CustomEvent('moveCommand', {
detail: { x: tileX, y: tileY }
}));
}
updateCursor() { updateCursor() {
if (this.isDragging) { if (this.isDragging) {
this.canvas.style.cursor = 'grabbing'; this.canvas.style.cursor = 'grabbing';
@@ -450,8 +834,10 @@ export class RTSMapCanvas {
const worldPos = this.screenToWorld(screenX, screenY); const worldPos = this.screenToWorld(screenX, screenY);
const clickedElement = this.getElementAtPosition(worldPos.x, worldPos.y); const clickedElement = this.getElementAtPosition(worldPos.x, worldPos.y);
console.debug('[handleClick] worldPos', worldPos, 'clickedElement', clickedElement);
if (clickedElement) { if (clickedElement) {
this.clearSelection();
this.selectElement(clickedElement); this.selectElement(clickedElement);
} else { } else {
this.clearSelection(); this.clearSelection();
@@ -533,6 +919,66 @@ export class RTSMapCanvas {
this.isDragging = false; this.isDragging = false;
} }
addTerrainEffects(ctx, x, y, tileType) {
const tileX = x * this.tileSize;
const tileY = y * this.tileSize;
switch (tileType.name) {
case 'water':
// Add water ripple effect
ctx.save();
ctx.globalAlpha = 0.3;
ctx.fillStyle = '#87ceeb';
ctx.fillRect(tileX + 2, tileY + 2, this.tileSize - 4, this.tileSize - 4);
ctx.restore();
break;
case 'grass':
// Add grass texture dots
ctx.fillStyle = '#32cd32';
for (let i = 0; i < 3; i++) {
const dotX = tileX + Math.random() * this.tileSize;
const dotY = tileY + Math.random() * this.tileSize;
ctx.fillRect(dotX, dotY, 1, 1);
}
break;
case 'trees':
// Add tree texture
ctx.fillStyle = '#228b22';
ctx.fillRect(tileX + 4, tileY + 4, this.tileSize - 8, this.tileSize - 8);
break;
case 'enclosure_fence':
// Add fence pattern
ctx.strokeStyle = '#8b4513';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(tileX, tileY + this.tileSize / 2);
ctx.lineTo(tileX + this.tileSize, tileY + this.tileSize / 2);
ctx.stroke();
break;
}
}
darkenColor(color, factor) {
// Simple color darkening function
const rgb = this.hexToRgb(color);
if (!rgb) return color;
const darken = (c) => Math.max(0, Math.floor(c * (1 - factor)));
return `rgb(${darken(rgb.r)}, ${darken(rgb.g)}, ${darken(rgb.b)})`;
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
destroy() { destroy() {
this.stopAnimationLoop(); this.stopAnimationLoop();
window.removeEventListener('resize', this.resizeCanvas); window.removeEventListener('resize', this.resizeCanvas);
+309 -48
View File
@@ -2,7 +2,10 @@
import { renderExtensionTemplateAsync } from '../../../extensions.js'; import { renderExtensionTemplateAsync } from '../../../extensions.js';
import { RTSMapCanvas } from './MapCanvas.js'; import { RTSMapCanvas } from './MapCanvas.js';
import GameStateManager from '../src/GameStateManager.js'; import GameStateManager from '../src/GameStateManager.js';
import PresetManager from '../src/PresetManager.js';
import EventManager from '../src/EventManager.js';
import { sendTurn } from '../src/LLMAdapter.js'; import { sendTurn } from '../src/LLMAdapter.js';
import { updateResourcePanel } from './ResourcePanel.js';
export class RTSUIController { export class RTSUIController {
constructor() { constructor() {
@@ -22,6 +25,9 @@ export class RTSUIController {
this.handleClearCommand = this.handleClearCommand.bind(this); this.handleClearCommand = this.handleClearCommand.bind(this);
this.handleMapControls = this.handleMapControls.bind(this); this.handleMapControls = this.handleMapControls.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleSelectionChange = this.handleSelectionChange.bind(this);
this.handleNarrativeUpdate = this.handleNarrativeUpdate.bind(this);
this.handleZoneChange = this.handleZoneChange.bind(this);
} }
async enterFullscreen() { async enterFullscreen() {
@@ -65,13 +71,34 @@ export class RTSUIController {
this.mapCanvas = new RTSMapCanvas('rts-game-map'); this.mapCanvas = new RTSMapCanvas('rts-game-map');
this.setupEventListeners(); this.setupEventListeners();
this.mapCanvas.canvas.addEventListener('selectionChanged', this.handleSelectionChange);
const response = await fetch('/scripts/extensions/rts-mode/maps/zoo_escape.json'); const presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('rts-preset-select'));
const selectedPreset = presetSelect ? presetSelect.value : '/scripts/extensions/rts-mode/presets/zoo_escape.json';
await PresetManager.loadPreset(selectedPreset);
GameStateManager.reset();
const preset = PresetManager.getPreset();
EventManager.initialize();
// Create quick action buttons after preset is loaded
this.createQuickActionButtons();
const response = await fetch(preset.map);
this.mapData = await response.json(); this.mapData = await response.json();
this.mapCanvas.loadMap(this.mapData); this.mapCanvas.loadMap(this.mapData);
/* Ensure GameStateManager keeps a reference to the full mapData so that
subsequent AI-driven updates (addOrUpdateMapEntity, etc.) have a
valid entity list to work with. */
GameStateManager.setState({
mapState: {
...(GameStateManager.getState().mapState || {}),
mapData: this.mapData
}
});
this.updateUI(); this.updateUI();
this.addLogEntry('system', 'RTS Mode activated. You are trapped in the zoo. Find a way to escape!'); this.addLogEntry('system', PresetManager.getPrompt('system_initial'));
} catch (error) { } catch (error) {
console.error('Error creating RTS interface:', error); console.error('Error creating RTS interface:', error);
this.addLogEntry('error', `Failed to create RTS interface: ${error.message}`); this.addLogEntry('error', `Failed to create RTS interface: ${error.message}`);
@@ -80,12 +107,6 @@ export class RTSUIController {
} }
setupEventListeners() { setupEventListeners() {
// Quick Actions
document.getElementById('rts-observe-btn')?.addEventListener('click', () => this.handleQuickAction('observe'));
document.getElementById('rts-move-btn')?.addEventListener('click', () => this.handleQuickAction('move'));
document.getElementById('rts-hide-btn')?.addEventListener('click', () => this.handleQuickAction('hide'));
document.getElementById('rts-interact-btn')?.addEventListener('click', () => this.handleQuickAction('interact'));
// Command Execution // Command Execution
document.getElementById('rts-execute-btn')?.addEventListener('click', this.handleExecuteCommand); document.getElementById('rts-execute-btn')?.addEventListener('click', this.handleExecuteCommand);
document.getElementById('rts-clear-btn')?.addEventListener('click', this.handleClearCommand); document.getElementById('rts-clear-btn')?.addEventListener('click', this.handleClearCommand);
@@ -105,24 +126,101 @@ export class RTSUIController {
} }
}); });
} }
// Narrative and Zone Listeners
document.addEventListener('rts-narrative-update', this.handleNarrativeUpdate);
document.addEventListener('rts-zone-changed', this.handleZoneChange);
document.addEventListener('rts-map-update', this.handleMapUpdate.bind(this));
document.addEventListener('rts-canvas-refresh', this.handleCanvasRefresh.bind(this));
} }
handleQuickAction(action) { handleQuickAction(action) {
const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input')); const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input'));
if (!commandInput) return; if (!commandInput) {
console.error('RTS: Command input not found');
const actionTemplates = { return;
observe: 'Look around carefully to assess the situation.',
move: 'Move to a specific location.',
hide: 'Find a hiding spot.',
interact: 'Interact with something in the environment.'
};
commandInput.value = actionTemplates[action] || '';
commandInput.focus();
commandInput.select();
} }
try {
const quickActions = PresetManager.getFeature('quickActions');
if (!quickActions || !Array.isArray(quickActions)) {
console.error('RTS: Quick actions not found in preset');
return;
}
const actionTemplate = quickActions.find(a => a.id === action);
if (actionTemplate && actionTemplate.command) {
commandInput.value = actionTemplate.command;
} else {
console.error(`RTS: Action template not found for action: ${action}`);
// Fallback commands
const fallbackCommands = {
'observe': 'Look around carefully to assess the situation.',
'move': 'Move to a specific location.',
'hide': 'Find a hiding spot.',
'interact': 'Interact with something in the environment.'
};
commandInput.value = fallbackCommands[action] || `Perform ${action} action`;
}
commandInput.focus();
commandInput.select();
} catch (error) {
console.error('RTS: Error in handleQuickAction:', error);
}
}
createQuickActionButtons() {
console.log('RTS: Creating quick action buttons...');
const container = document.getElementById('rts-quick-actions');
if (!container) {
console.error('RTS: Quick actions container not found');
return;
}
container.innerHTML = ''; // Clear existing buttons
try {
const quickActions = PresetManager.getFeature('quickActions');
console.log('RTS: Quick actions from preset:', quickActions);
if (!quickActions || !Array.isArray(quickActions)) {
console.warn('RTS: No quick actions found in preset, creating fallback actions');
// Create fallback actions
const fallbackActions = [
{ id: 'observe', label: 'Observe' },
{ id: 'move', label: 'Move' },
{ id: 'hide', label: 'Hide' },
{ id: 'interact', label: 'Interact' }
];
fallbackActions.forEach(action => {
const button = document.createElement('button');
button.id = `rts-${action.id}-btn`;
button.className = 'menu_button rts-action-btn';
button.textContent = action.label;
button.addEventListener('click', () => this.handleQuickAction(action.id));
container.appendChild(button);
console.log(`RTS: Created fallback button: ${action.label}`);
});
console.log(`RTS: Created ${fallbackActions.length} fallback quick action buttons`);
return;
}
quickActions.forEach(action => {
const button = document.createElement('button');
button.id = `rts-${action.id}-btn`;
button.className = 'menu_button rts-action-btn';
button.textContent = action.label;
button.title = action.command || action.label;
button.addEventListener('click', () => this.handleQuickAction(action.id));
container.appendChild(button);
console.log(`RTS: Created preset button: ${action.label}`);
});
console.log(`RTS: Created ${quickActions.length} preset quick action buttons`);
} catch (error) {
console.error('RTS: Error creating quick action buttons:', error);
}
}
async handleExecuteCommand() { async handleExecuteCommand() {
const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input')); const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input'));
if (!commandInput) return; if (!commandInput) return;
@@ -142,11 +240,7 @@ export class RTSUIController {
try { try {
// This is where the command would be sent to the LLM // This is where the command would be sent to the LLM
await sendTurn(command); await sendTurn(command);
// For now, we'll just simulate a delay and update the UI // The UI will now be updated by the 'rts-narrative-update' event listener
setTimeout(() => {
this.updateUI();
this.addLogEntry('result', 'Your action has consequences...');
}, 1000);
} catch (error) { } catch (error) {
console.error('Failed to execute RTS command:', error); console.error('Failed to execute RTS command:', error);
this.addLogEntry('error', `Failed to execute command: ${error.message}`); this.addLogEntry('error', `Failed to execute command: ${error.message}`);
@@ -189,19 +283,42 @@ export class RTSUIController {
updateUI() { updateUI() {
if (!this.mapData) return; if (!this.mapData) return;
// Update the new status panel with current game state
const gameState = GameStateManager.getState();
try {
updateResourcePanel(gameState);
} catch (error) {
console.warn('RTS: Error updating resource panel from RTSUIController:', error);
}
// Keep the existing zoo-specific updates
this.updateZooStatusDisplay(); this.updateZooStatusDisplay();
this.updateEscapedAnimalsDisplay(); this.updateEscapedAnimalsDisplay();
this.updateIncidentsDisplay(); this.updateIncidentsDisplay();
this.updatePeopleStatusDisplay(); this.updatePeopleStatusDisplay();
this.updateThreatDisplay();
}
handleSelectionChange() {
this.updateEscapedAnimalsDisplay();
this.updatePeopleStatusDisplay();
}
handleEntitySelection(entity) {
this.mapCanvas.clearSelection();
this.mapCanvas.selectElement({ type: 'entity', data: entity });
this.updateEscapedAnimalsDisplay();
} }
updateZooStatusDisplay() { updateZooStatusDisplay() {
const playerCount = this.mapData.map.entities.filter(e => e.type === 'player').length; const state = GameStateManager.getState();
const playerCount = (state.entities || []).filter(e => e.type === 'player').length;
const gateCount = this.mapData.map.tiles.flat().filter(t => t === 3).length; const gateCount = this.mapData.map.tiles.flat().filter(t => t === 3).length;
this.updateElementText('rts-alert-level', 'CRITICAL'); this.updateElementText('rts-alert-level', (state.threatLevel || 'low').toUpperCase());
this.updateElementText('rts-survivors', playerCount > 0 ? 1 : 0); // Assuming 1 player this.updateElementText('rts-survivors', playerCount > 0 ? 1 : 0); // Assuming 1 player
this.updateElementText('rts-casualties', 0); // Mock data this.updateElementText('rts-casualties', (state.casualties && state.casualties.total) || 0);
this.updateElementText('rts-breached', `${gateCount} (Main Gate)`); this.updateElementText('rts-breached', `${gateCount} (Main Gate)`);
} }
@@ -210,34 +327,33 @@ export class RTSUIController {
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
this.mapData.map.entities.forEach(entity => { const state = GameStateManager.getState();
if (entity.type !== 'player' && entity.type !== 'guard') { if (state.entities) {
state.entities.forEach(entity => {
if (entity.type !== 'player' && entity.type !== 'guard' && entity.status === 'escaped') {
const element = document.createElement('div'); const element = document.createElement('div');
element.className = 'rts-animal-item rts-escaped'; element.className = `rts-animal-item rts-escaped ${this.mapCanvas.selectedEntities.includes(entity) ? 'selected' : ''}`;
element.innerHTML = ` element.innerHTML = `
<i class="fa-solid fa-paw" style="color: #dc2626;"></i> <i class="fa-solid fa-paw" style="color: #dc2626;"></i>
<span class="rts-animal-name">${entity.type.replace('_', ' ')}</span> <span class="rts-animal-name">${entity.name || entity.type.replace('_', ' ')}</span>
<span class="rts-animal-location">${entity.enclosure ? `Enclosure` : 'Roaming'}</span> <span class="rts-animal-location">${entity.location || 'Roaming'}</span>
<span class="rts-animal-status rts-status-hunting">Hunting</span> <span class="rts-animal-status rts-status-hunting">${entity.mood || 'Hunting'}</span>
`; `;
element.addEventListener('click', () => this.handleEntitySelection(entity));
container.appendChild(element); container.appendChild(element);
} }
}); });
} }
}
updateIncidentsDisplay() { updateIncidentsDisplay() {
const container = document.getElementById('rts-active-incidents'); const container = document.getElementById('rts-active-incidents');
if (!container) return; if (!container) return;
container.innerHTML = ''; // Clear previous incidents container.innerHTML = ''; // Clear previous incidents
// Mock data for incidents const state = GameStateManager.getState();
const incidents = [ if (state.incidents) {
{ description: 'Security camera offline in reptile house', location: 'Reptile House' }, state.incidents.forEach(incident => {
{ description: 'Strange noises from the aviary', location: 'Aviary' },
{ description: 'Main gate power seems to be cut', location: 'Main Gate' }
];
incidents.forEach(incident => {
const element = document.createElement('div'); const element = document.createElement('div');
element.className = 'rts-incident-item'; element.className = 'rts-incident-item';
element.innerHTML = ` element.innerHTML = `
@@ -248,26 +364,60 @@ export class RTSUIController {
container.appendChild(element); container.appendChild(element);
}); });
} }
}
updatePeopleStatusDisplay() { updatePeopleStatusDisplay() {
const container = document.getElementById('rts-people-status'); const container = document.getElementById('rts-people-status');
if (!container) return; if (!container) return;
container.innerHTML = ''; // Clear previous people container.innerHTML = ''; // Clear previous people
const guards = this.mapData.map.entities.filter(e => e.type === 'guard'); const state = GameStateManager.getState();
if (state.mapState && state.mapState.mapData && state.mapState.mapData.entities) {
// Show visitors, staff, keepers, veterinarians - no guards
const people = state.mapState.mapData.entities.filter(e =>
['visitor', 'staff', 'keeper', 'veterinarian'].includes(e.type)
);
guards.forEach((guard, index) => { people.forEach((person, index) => {
const element = document.createElement('div'); const element = document.createElement('div');
element.className = 'rts-person-item'; element.className = 'rts-person-item';
// Choose appropriate icon based on type
let icon = 'fa-user';
let color = '#6b7280';
if (person.type === 'visitor') {
icon = 'fa-user';
color = '#3b82f6';
} else if (person.type === 'staff') {
icon = 'fa-user-tie';
color = '#10b981';
} else if (person.type === 'keeper') {
icon = 'fa-user-check';
color = '#f59e0b';
} else if (person.type === 'veterinarian') {
icon = 'fa-user-doctor';
color = '#8b5cf6';
}
// Add escape status for visitors
let escapeStatus = '';
if (person.type === 'visitor') {
const escapeChance = this.calculateEscapeChance(person);
escapeStatus = `<span class="rts-escape-status rts-escape-${escapeChance.level}">${escapeChance.text}</span>`;
}
element.innerHTML = ` element.innerHTML = `
<i class="fa-solid fa-user-shield" style="color: #ea580c;"></i> <i class="fa-solid ${icon}" style="color: ${color};"></i>
<span class="rts-person-name">Guard ${index + 1}</span> <span class="rts-person-name">${person.name || `${person.type} ${index + 1}`}</span>
<span class="rts-person-status rts-status-patrolling">Patrolling</span> <span class="rts-person-status rts-status-${person.status || 'active'}">${person.status || 'Active'}</span>
<span class="rts-person-location">Sector ${index + 1}</span> ${escapeStatus}
<span class="rts-person-location">@ (${person.x}, ${person.y})</span>
`; `;
element.addEventListener('click', () => this.handleEntitySelection(person));
container.appendChild(element); container.appendChild(element);
}); });
} }
}
updateElementText(id, value) { updateElementText(id, value) {
const element = document.getElementById(id); const element = document.getElementById(id);
@@ -276,6 +426,52 @@ export class RTSUIController {
} }
} }
calculateEscapeChance(person) {
const state = GameStateManager.getState();
const threatLevel = state.threatLevel || 'none';
const playerPos = state.mapState?.playerPosition || { x: 0, y: 0 };
// Calculate distance from player
const distance = Math.sqrt(
Math.pow(person.x - playerPos.x, 2) +
Math.pow(person.y - playerPos.y, 2)
);
// Base escape chance based on threat level
let escapeScore = 0;
switch (threatLevel) {
case 'none': escapeScore = 95; break;
case 'low': escapeScore = 75; break;
case 'medium': escapeScore = 50; break;
case 'high': escapeScore = 25; break;
case 'extreme': escapeScore = 5; break;
default: escapeScore = 50;
}
// Adjust based on distance from player (closer = safer)
if (distance <= 3) escapeScore += 20;
else if (distance >= 10) escapeScore -= 30;
// Adjust based on person status
if (person.status === 'injured') escapeScore -= 40;
else if (person.status === 'panicked') escapeScore -= 20;
else if (person.status === 'hiding') escapeScore += 10;
// Clamp between 5-95
escapeScore = Math.max(5, Math.min(95, escapeScore));
// Determine level and text
if (escapeScore >= 80) {
return { level: 'high', text: 'Safe' };
} else if (escapeScore >= 60) {
return { level: 'medium', text: 'At Risk' };
} else if (escapeScore >= 30) {
return { level: 'low', text: 'Danger' };
} else {
return { level: 'critical', text: 'Critical' };
}
}
addLogEntry(type, message) { addLogEntry(type, message) {
const gameLog = document.getElementById('rts-game-log'); const gameLog = document.getElementById('rts-game-log');
if (!gameLog) return; if (!gameLog) return;
@@ -291,6 +487,60 @@ export class RTSUIController {
} }
} }
handleNarrativeUpdate(event) {
const { type, message, entityUpdates } = event.detail;
this.addLogEntry(type, message);
// Handle entity updates and refresh map
if (entityUpdates && entityUpdates.length > 0) {
this.refreshMapEntities();
}
this.updateUI();
}
refreshMapEntities() {
if (this.mapCanvas && this.mapData) {
// Get updated map data from GameStateManager
const gameState = GameStateManager.getState();
if (gameState.mapState && gameState.mapState.mapData) {
this.mapData = gameState.mapState.mapData;
this.mapCanvas.loadMap(this.mapData);
console.log('RTS: Map entities refreshed with', this.mapData?.map?.entities?.length || 0, 'entities');
}
}
}
handleZoneChange(event) {
const zone = event.detail;
this.addLogEntry('system', `Entered ${zone.name}.`);
this.updateThreatDisplay();
}
handleMapUpdate(event) {
console.log('RTS: Map update event received:', event.detail);
this.refreshMapEntities();
this.updateUI();
}
handleCanvasRefresh(event) {
console.log('RTS: Canvas refresh event received:', event.detail);
if (this.mapCanvas) {
// Force the map canvas to redraw
this.mapCanvas.dirty = true;
this.refreshMapEntities();
}
this.updateUI();
}
updateThreatDisplay() {
const state = GameStateManager.getState();
const zone = EventManager.zones.find(z => z.id === state.currentZone);
const zoneName = zone ? zone.name : 'Unknown Zone';
this.updateElementText('rts-current-zone', zoneName);
this.updateElementText('rts-threat-level', state.threatLevel);
}
isActive() { isActive() {
return this.isFullscreen; return this.isFullscreen;
} }
@@ -302,6 +552,17 @@ export class RTSUIController {
this.enterFullscreen(); this.enterFullscreen();
} }
} }
async loadPreset(presetPath) {
await PresetManager.loadPreset(presetPath);
GameStateManager.reset();
if (this.isFullscreen) {
// Recreate quick action buttons for the new preset
this.createQuickActionButtons();
// If the UI is already open, we need to recreate it to reflect the new preset
await this.exitFullscreen();
await this.enterFullscreen();
}
}
} }
export const rtsUI = new RTSUIController(); export const rtsUI = new RTSUIController();
+204 -8
View File
@@ -1,24 +1,220 @@
/** /**
* Creates and returns the resource panel element. * Creates and returns the enhanced status panel element.
* @param {HTMLElement} rootEl - The root element to append to (optional). * @param {HTMLElement} rootEl - The root element to append to (optional).
* @returns {HTMLDivElement} * @returns {HTMLDivElement}
*/ */
export function createResourcePanel(rootEl) { export function createResourcePanel(rootEl) {
const panel = document.createElement('div'); const panel = document.createElement('div');
panel.id = 'rts-resource-panel'; panel.id = 'rts-resource-panel';
panel.className = 'rts-status-panel';
const list = document.createElement('ul'); panel.innerHTML = `
list.innerHTML = ` <div class="rts-panel-section">
<li>Gold: 0</li> <h3>Status Overview</h3>
<li>Wood: 0</li> <div id="rts-overview-stats">
<li>Units: 0</li> <div class="stat-item">Turn: <span id="turn-counter">1</span></div>
<div class="stat-item">Zone: <span id="current-zone">Unknown</span></div>
<div class="stat-item">Threat: <span id="threat-level">Low</span></div>
</div>
</div>
<div class="rts-panel-section">
<h3>Casualties</h3>
<div id="rts-casualties-stats">
<div class="stat-item">Total Deaths: <span id="total-deaths">0</span></div>
<div class="stat-item">Recent: <span id="recent-deaths">0</span></div>
</div>
<div id="rts-recent-casualties" class="scrollable-list"></div>
</div>
<div class="rts-panel-section">
<h3>Escaped Animals</h3>
<div id="rts-animals-stats">
<div class="stat-item">Active Threats: <span id="active-animals">0</span></div>
</div>
<div id="rts-escaped-animals" class="scrollable-list"></div>
</div>
<div class="rts-panel-section">
<h3>Personnel</h3>
<div id="rts-personnel-stats">
<div class="stat-item">Alive: <span id="alive-personnel">0</span></div>
<div class="stat-item">Injured: <span id="injured-personnel">0</span></div>
<div class="stat-item">Missing: <span id="missing-personnel">0</span></div>
</div>
<div id="rts-personnel-list" class="scrollable-list"></div>
</div>
<div class="rts-panel-section">
<h3>Active Incidents</h3>
<div id="rts-incidents-stats">
<div class="stat-item">Emergency: <span id="emergency-incidents">0</span></div>
<div class="stat-item">Ongoing: <span id="ongoing-incidents">0</span></div>
</div>
<div id="rts-incidents-list" class="scrollable-list"></div>
</div>
`; `;
panel.appendChild(list);
if (rootEl) { if (rootEl) {
rootEl.appendChild(panel); rootEl.appendChild(panel);
} }
return panel; return panel;
} }
/**
* Updates the resource panel with current game state
* @param {object} gameState - Current game state
*/
export function updateResourcePanel(gameState) {
if (!gameState) return;
// Helper function to safely update text content
const safeUpdateText = (id, value) => {
const element = document.getElementById(id);
if (element) {
element.textContent = value;
} else {
console.warn(`RTS: Element with id '${id}' not found for update`);
}
};
// Update overview stats
safeUpdateText('turn-counter', gameState.turn || 1);
safeUpdateText('current-zone', gameState.currentZone || 'Unknown');
safeUpdateText('threat-level', gameState.threatLevel || 'Low');
// Update casualties
const casualties = gameState.casualties || {};
const totalDeaths = casualties.total || 0;
const recentDeaths = casualties.recent || [];
safeUpdateText('total-deaths', totalDeaths);
safeUpdateText('recent-deaths', recentDeaths.length);
// Update recent casualties list
const casualtiesList = document.getElementById('rts-recent-casualties');
if (!casualtiesList) {
console.warn('RTS: Casualties list element not found');
return;
}
casualtiesList.innerHTML = '';
if (recentDeaths && recentDeaths.length > 0) {
recentDeaths.slice(0, 5).forEach(casualty => {
const item = document.createElement('div');
item.className = 'list-item casualty';
// Safely access casualty properties
const name = casualty.name || 'Unknown victim';
const cause = casualty.cause || casualty.causeOfDeath || 'Unknown cause';
const perpetrator = casualty.perpetrator || casualty.killedBy || 'Unknown';
const location = casualty.location || 'Unknown location';
const turn = casualty.turn || '?';
item.innerHTML = `<strong>${name}</strong><br>
<small>${cause} by ${perpetrator}</small><br>
<small>@ ${location} (Turn ${turn})</small>`;
casualtiesList.appendChild(item);
});
} else {
const noData = document.createElement('div');
noData.className = 'list-item';
noData.innerHTML = '<small style="color: rgba(255,255,255,0.5);">No recent casualties</small>';
casualtiesList.appendChild(noData);
}
// Update escaped animals
const animals = gameState.escapedAnimals || {};
safeUpdateText('active-animals', (animals.active || []).length);
const animalsList = document.getElementById('rts-escaped-animals');
if (!animalsList) {
console.warn('RTS: Escaped animals list element not found');
return;
}
animalsList.innerHTML = '';
if (animals.active && animals.active.length > 0) {
animals.active.forEach(animal => {
const item = document.createElement('div');
item.className = 'list-item animal';
item.innerHTML = `<strong>${animal.name}</strong> (${animal.type})<br>
<small>${animal.behavior} @ ${animal.currentLocation}</small><br>
<small>Threat: ${animal.threat}</small>`;
animalsList.appendChild(item);
});
}
// Update personnel
const personnel = gameState.personnel || {};
safeUpdateText('alive-personnel', (personnel.alive || []).length);
safeUpdateText('injured-personnel', (personnel.injured || []).length);
safeUpdateText('missing-personnel', (personnel.missing || []).length);
const personnelList = document.getElementById('rts-personnel-list');
if (!personnelList) {
console.warn('RTS: Personnel list element not found');
return;
}
personnelList.innerHTML = '';
// Show alive personnel
if (personnel.alive && personnel.alive.length > 0) {
personnel.alive.forEach(person => {
const item = document.createElement('div');
item.className = 'list-item personnel alive';
item.innerHTML = `<strong>${person.name}</strong> (${person.type})<br>
<small>Status: ${person.status}</small><br>
<small>@ (${person.position.x}, ${person.position.y})</small>`;
personnelList.appendChild(item);
});
}
// Show injured personnel
if (personnel.injured && personnel.injured.length > 0) {
personnel.injured.forEach(person => {
const item = document.createElement('div');
item.className = 'list-item personnel injured';
item.innerHTML = `<strong>${person.name}</strong> (${person.type})<br>
<small>INJURED - ${person.status}</small><br>
<small>@ (${person.position.x}, ${person.position.y})</small>`;
personnelList.appendChild(item);
});
}
// Update incidents
const incidents = gameState.activeIncidents || {};
safeUpdateText('emergency-incidents', (incidents.emergency || []).length);
safeUpdateText('ongoing-incidents', (incidents.ongoing || []).length);
const incidentsList = document.getElementById('rts-incidents-list');
if (!incidentsList) {
console.warn('RTS: Incidents list element not found');
return;
}
incidentsList.innerHTML = '';
// Show emergency incidents
if (incidents.emergency && incidents.emergency.length > 0) {
incidents.emergency.forEach(incident => {
const item = document.createElement('div');
item.className = 'list-item incident emergency';
item.innerHTML = `<strong>EMERGENCY</strong><br>
<small>${incident.type || 'Unknown'}</small><br>
<small>${incident.description || 'No details'}</small>`;
incidentsList.appendChild(item);
});
}
// Show ongoing incidents
if (incidents.ongoing && incidents.ongoing.length > 0) {
incidents.ongoing.forEach(incident => {
const item = document.createElement('div');
item.className = 'list-item incident ongoing';
item.innerHTML = `<strong>Ongoing</strong><br>
<small>${incident.type || 'Unknown'}</small><br>
<small>${incident.description || 'No details'}</small>`;
incidentsList.appendChild(item);
});
}
}