yay
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
@@ -76,3 +76,8 @@
|
||||
border-left: 1px solid black;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.rts-animal-item.selected {
|
||||
background-color: #fbbf24;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
+53
-60
@@ -10,63 +10,67 @@
|
||||
<h3>Game Status</h3>
|
||||
</div>
|
||||
|
||||
<!-- Resources Section -->
|
||||
<!-- Status Overview Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Resources</h4>
|
||||
<div id="rts-resources" class="rts-resources-grid">
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
<span>Gold: <span id="rts-gold">100</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-tree"></i>
|
||||
<span>Wood: <span id="rts-wood">50</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-mountain"></i>
|
||||
<span>Stone: <span id="rts-stone">25</span></span>
|
||||
</div>
|
||||
<div class="rts-resource-item">
|
||||
<i class="fa-solid fa-seedling"></i>
|
||||
<span>Food: <span id="rts-food">75</span></span>
|
||||
</div>
|
||||
<h4 class="rts-section-title">Status Overview</h4>
|
||||
<div id="rts-overview-stats">
|
||||
<div class="stat-item">Turn: <span id="turn-counter">1</span></div>
|
||||
<div class="stat-item">Zone: <span id="current-zone">Unknown</span></div>
|
||||
<div class="stat-item">Threat: <span id="threat-level">Low</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Units Section -->
|
||||
<!-- Casualties Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Army</h4>
|
||||
<div id="rts-units" class="rts-units-list">
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
<span>Warriors: <span id="rts-warriors">5</span></span>
|
||||
</div>
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-bow-and-arrow"></i>
|
||||
<span>Archers: <span id="rts-archers">3</span></span>
|
||||
</div>
|
||||
<div class="rts-unit-item">
|
||||
<i class="fa-solid fa-horse"></i>
|
||||
<span>Cavalry: <span id="rts-cavalry">2</span></span>
|
||||
</div>
|
||||
<h4 class="rts-section-title">Casualties</h4>
|
||||
<div id="rts-casualties-stats">
|
||||
<div class="stat-item">Total Deaths: <span id="total-deaths">0</span></div>
|
||||
<div class="stat-item">Recent: <span id="recent-deaths">0</span></div>
|
||||
</div>
|
||||
<div id="rts-recent-casualties" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Buildings Section -->
|
||||
<!-- Escaped Animals Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Buildings</h4>
|
||||
<div id="rts-buildings" class="rts-buildings-list">
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-home"></i>
|
||||
<span>Town Hall: Level <span id="rts-town-hall">1</span></span>
|
||||
<h4 class="rts-section-title">Escaped Animals</h4>
|
||||
<div id="rts-animals-stats">
|
||||
<div class="stat-item">Active Threats: <span id="active-animals">0</span></div>
|
||||
</div>
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-hammer"></i>
|
||||
<span>Barracks: <span id="rts-barracks">1</span></span>
|
||||
<div id="rts-escaped-animals" class="scrollable-list"></div>
|
||||
</div>
|
||||
<div class="rts-building-item">
|
||||
<i class="fa-solid fa-warehouse"></i>
|
||||
<span>Storage: <span id="rts-storage">1</span></span>
|
||||
|
||||
<!-- Personnel Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Personnel</h4>
|
||||
<div id="rts-personnel-stats">
|
||||
<div class="stat-item">Alive: <span id="alive-personnel">0</span></div>
|
||||
<div class="stat-item">Injured: <span id="injured-personnel">0</span></div>
|
||||
<div class="stat-item">Missing: <span id="missing-personnel">0</span></div>
|
||||
</div>
|
||||
<div id="rts-personnel-list" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Active Incidents Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Active Incidents</h4>
|
||||
<div id="rts-incidents-stats">
|
||||
<div class="stat-item">Emergency: <span id="emergency-incidents">0</span></div>
|
||||
<div class="stat-item">Ongoing: <span id="ongoing-incidents">0</span></div>
|
||||
</div>
|
||||
<div id="rts-incidents-list" class="scrollable-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Threat Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Threat</h4>
|
||||
<div id="rts-threat" class="rts-threat-display">
|
||||
<div class="rts-threat-item">
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
<span>Zone: <span id="rts-current-zone">Entrance</span></span>
|
||||
</div>
|
||||
<div class="rts-threat-item">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<span>Threat: <span id="rts-threat-level">Low</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,10 +94,10 @@
|
||||
</div>
|
||||
<div id="rts-map-wrapper" class="flex1 rts-map-wrapper">
|
||||
<canvas id="rts-game-map" width="800" height="600"></canvas>
|
||||
</div>
|
||||
<div id="rts-map-overlay" class="rts-map-overlay">
|
||||
<!-- Selected unit/building info will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="rts-map-footer">
|
||||
<div class="rts-turn-info">
|
||||
<span>Turn: <span id="rts-current-turn">1</span></span>
|
||||
@@ -112,19 +116,8 @@
|
||||
<!-- Quick Actions -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Quick Actions</h4>
|
||||
<div class="rts-quick-actions">
|
||||
<button id="rts-build-btn" class="menu_button" title="Build Structure">
|
||||
<i class="fa-solid fa-hammer"></i> Build
|
||||
</button>
|
||||
<button id="rts-recruit-btn" class="menu_button" title="Recruit Units">
|
||||
<i class="fa-solid fa-user-plus"></i> Recruit
|
||||
</button>
|
||||
<button id="rts-explore-btn" class="menu_button" title="Explore Territory">
|
||||
<i class="fa-solid fa-compass"></i> Explore
|
||||
</button>
|
||||
<button id="rts-trade-btn" class="menu_button" title="Trade Resources">
|
||||
<i class="fa-solid fa-handshake"></i> Trade
|
||||
</button>
|
||||
<div id="rts-quick-actions" class="rts-quick-actions">
|
||||
<!-- Quick action buttons will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,5 +5,30 @@
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</html>
|
||||
@@ -91,8 +91,13 @@ function unmountUI() {
|
||||
}
|
||||
}
|
||||
|
||||
function onRtsStartCommand() {
|
||||
async function onRtsStartCommand() {
|
||||
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();
|
||||
|
||||
// If RTS UI is active, update it
|
||||
@@ -101,20 +106,17 @@ function onRtsStartCommand() {
|
||||
rtsUI.addLogEntry('system', 'Game state reset. New campaign begins!');
|
||||
}
|
||||
|
||||
return '';
|
||||
return 'RTS game has been reset with the selected preset.';
|
||||
}
|
||||
|
||||
function onRtsCmdCommand(args, value) {
|
||||
console.log('RTS Command executed with args:', args, 'value:', value);
|
||||
if (value) {
|
||||
// If RTS UI is active, add to log
|
||||
if (rtsUI.isActive()) {
|
||||
rtsUI.addLogEntry('action', value);
|
||||
}
|
||||
|
||||
// The UI will now be updated by the 'rts-narrative-update' event listener
|
||||
sendTurn(value);
|
||||
return `RTS Command executed: ${value}`;
|
||||
}
|
||||
return '';
|
||||
return 'No command provided';
|
||||
}
|
||||
|
||||
async function onRtsUICommand() {
|
||||
@@ -167,5 +169,29 @@ jQuery(async function() {
|
||||
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');
|
||||
});
|
||||
@@ -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
@@ -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,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Community",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"homePage": ""
|
||||
}
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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]
|
||||
],
|
||||
"entities": [
|
||||
{ "type": "player", "x": 1, "y": 1 },
|
||||
{ "type": "guard", "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}] },
|
||||
{ "type": "lion", "x": 3, "y": 3, "enclosure": [2, 2, 4, 5] },
|
||||
{ "type": "tiger", "x": 27, "y": 3, "enclosure": [2, 26, 4, 28] },
|
||||
{ "type": "jaguar", "x": 3, "y": 7, "enclosure": [6, 2, 8, 5] },
|
||||
{ "type": "snow_leopard", "x": 27, "y": 7, "enclosure": [6, 26, 8, 28] },
|
||||
{ "type": "wolf", "x": 3, "y": 11, "enclosure": [10, 2, 12, 5] },
|
||||
{ "type": "grizzly_bear", "x": 27, "y": 11, "enclosure": [10, 26, 12, 28] },
|
||||
{ "type": "polar_bear", "x": 3, "y": 15, "enclosure": [14, 2, 16, 5] },
|
||||
{ "type": "panda", "x": 27, "y": 15, "enclosure": [14, 26, 16, 28] }
|
||||
{ "id": "player", "type": "player", "name": "You", "description": "A visitor trying to escape the zoo.", "x": 1, "y": 1 },
|
||||
{ "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}] },
|
||||
{ "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}] },
|
||||
{ "id": "lion1", "type": "lion", "name": "Lion", "description": "A majestic, but dangerous, lion.", "x": 3, "y": 3, "enclosure": [2, 2, 4, 5] },
|
||||
{ "id": "tiger1", "type": "tiger", "name": "Tiger", "description": "A large, striped predator.", "x": 27, "y": 3, "enclosure": [2, 26, 4, 28] },
|
||||
{ "id": "jaguar1", "type": "jaguar", "name": "Jaguar", "description": "A powerful, spotted cat.", "x": 3, "y": 7, "enclosure": [6, 2, 8, 5] },
|
||||
{ "id": "snow_leopard1", "type": "snow_leopard", "name": "Snow Leopard", "description": "An elusive and powerful hunter.", "x": 27, "y": 7, "enclosure": [6, 26, 8, 28] },
|
||||
{ "id": "wolf1", "type": "wolf", "name": "Wolf", "description": "A cunning pack animal.", "x": 3, "y": 11, "enclosure": [10, 2, 12, 5] },
|
||||
{ "id": "grizzly_bear1", "type": "grizzly_bear", "name": "Grizzly Bear", "description": "A massive, intimidating bear.", "x": 27, "y": 11, "enclosure": [10, 26, 12, 28] },
|
||||
{ "id": "polar_bear1", "type": "polar_bear", "name": "Polar Bear", "description": "A formidable arctic predator.", "x": 3, "y": 15, "enclosure": [14, 2, 16, 5] },
|
||||
{ "id": "panda1", "type": "panda", "name": "Panda", "description": "A gentle giant, usually peaceful.", "x": 27, "y": 15, "enclosure": [14, 26, 16, 28] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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? They’re… 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.* It’s like they’ve 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
@@ -15,40 +15,64 @@
|
||||
<h4 class="rts-section-title">Overall Status</h4>
|
||||
<div id="rts-zoo-status" class="rts-zoo-status">
|
||||
<div class="rts-status-item">
|
||||
<i class="fa-solid fa-exclamation-triangle" style="color: #dc2626;"></i>
|
||||
<span>Alert Level: <span id="rts-alert-level"></span></span>
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<span>Turn: <span id="turn-counter">1</span></span>
|
||||
</div>
|
||||
<div class="rts-status-item">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
<span>Survivors: <span id="rts-survivors"></span></span>
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
<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 class="rts-status-item">
|
||||
<i class="fa-solid fa-skull" style="color: #dc2626;"></i>
|
||||
<span>Casualties: <span id="rts-casualties"></span></span>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Escaped Animals Section -->
|
||||
<div class="rts-section">
|
||||
<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>
|
||||
|
||||
<!-- Active Incidents Section -->
|
||||
<div class="rts-section">
|
||||
<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>
|
||||
|
||||
<!-- People Status Section -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Personnel Status</h4>
|
||||
<div id="rts-people-status" class="rts-people-list scrollY"></div>
|
||||
<div id="rts-active-incidents" class="rts-incidents-list scrollY"></div>
|
||||
<div id="rts-incidents-list" class="rts-incidents-list scrollY scrollable-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,11 +101,8 @@
|
||||
<!-- Quick Actions -->
|
||||
<div class="rts-section">
|
||||
<h4 class="rts-section-title">Quick Actions</h4>
|
||||
<div class="rts-quick-actions">
|
||||
<button id="rts-observe-btn" class="menu_button"><i class="fa-solid fa-eye"></i> Observe</button>
|
||||
<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 id="rts-quick-actions" class="rts-quick-actions">
|
||||
<!-- Quick action buttons will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +113,7 @@
|
||||
<textarea id="rts-command-input" placeholder="Describe your action..." class="text_pole textarea_compact" rows="3"></textarea>
|
||||
<div class="rts-command-controls">
|
||||
<button id="rts-execute-btn" class="menu_button menu_button_bold"><i class="fa-solid fa-play"></i> 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>
|
||||
|
||||
@@ -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
@@ -1,34 +1,421 @@
|
||||
const defaultState = {
|
||||
turn: 1,
|
||||
map: [],
|
||||
units: [],
|
||||
resources: { gold: 0, wood: 0 },
|
||||
log: [],
|
||||
};
|
||||
import PresetManager from './PresetManager.js';
|
||||
|
||||
class GameStateManager {
|
||||
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() {
|
||||
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) {
|
||||
// For now, just shallow merge the diff.
|
||||
Object.assign(this.state, diff);
|
||||
this.state.log.push({ turn: this.state.turn, diff });
|
||||
console.log('RTS Game State Updated:', this.state);
|
||||
console.log('RTS Game State Updated (by diff):', this.state);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.state = { ...defaultState, log: [] };
|
||||
console.log('RTS Game State Reset.');
|
||||
const initialState = PresetManager.getInitialState();
|
||||
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() {
|
||||
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
@@ -1,22 +1,81 @@
|
||||
import GameStateManager from './GameStateManager.js';
|
||||
import EventManager from './EventManager.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.
|
||||
* @returns {object|null} The parsed JSON object or null if not found.
|
||||
*/
|
||||
function extractJson(text) {
|
||||
const match = /```json\n([\s\S]+?)\n```/.exec(text);
|
||||
if (match && match[1]) {
|
||||
try {
|
||||
return JSON.parse(match[1]);
|
||||
// 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) {
|
||||
console.error('RTS-Mode: Failed to parse JSON from LLM response.', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,30 +84,195 @@ function extractJson(text) {
|
||||
*/
|
||||
export async function sendTurn(userCmd) {
|
||||
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 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
|
||||
const reply = await window.LLMBroker.generate(prompt);
|
||||
const reply = await generateQuietPrompt({ quietPrompt: prompt, quietToLoud: false });
|
||||
|
||||
if (!reply) {
|
||||
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);
|
||||
if (diff) {
|
||||
GameStateManager.applyDiff(diff);
|
||||
// For now, log the narrative part to the console.
|
||||
const narrative = reply.replace(/```json\n[\s\S]+?\n```/, '').trim();
|
||||
console.log('RTS Narrative:', narrative);
|
||||
document.dispatchEvent(new CustomEvent('rts-narrative-update', { detail: { type: 'llm', message: narrative } }));
|
||||
console.log('RTS Narrative (fallback):', narrative);
|
||||
} else {
|
||||
console.warn('RTS-Mode: No valid JSON diff found in LLM response.');
|
||||
// Log the raw reply for debugging.
|
||||
document.dispatchEvent(new CustomEvent('rts-narrative-update', { detail: { type: 'event', message: reply.trim() } }));
|
||||
console.warn('RTS-Mode: No valid JSON found. Treating response as pure narrative.');
|
||||
console.log('Raw LLM Response:', reply);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('RTS-Mode: Error during sendTurn:', error);
|
||||
// Optionally, display an alert to the user.
|
||||
alert(`RTS-Mode Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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} 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.
|
||||
*/
|
||||
export function buildPrompt(stateJSON, userCmd) {
|
||||
return `<RTS-STATE>${stateJSON}</RTS-STATE>\n<USER-CMD>${userCmd}</USER-CMD>\nRespond with JSON diff + narrative.`;
|
||||
export function buildPrompt(stateJSON, userCmd, event, isExplicit) {
|
||||
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;
|
||||
}
|
||||
@@ -106,13 +106,13 @@
|
||||
}
|
||||
|
||||
/* Units and Buildings */
|
||||
.rts-units-list, .rts-buildings-list {
|
||||
.rts-units-list, .rts-buildings-list, .rts-threat-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rts-unit-item, .rts-building-item {
|
||||
.rts-unit-item, .rts-building-item, .rts-threat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -123,7 +123,7 @@
|
||||
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;
|
||||
width: 16px;
|
||||
}
|
||||
@@ -196,10 +196,6 @@
|
||||
.rts-map-wrapper {
|
||||
position: relative;
|
||||
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-radius: 8px;
|
||||
margin: 8px;
|
||||
@@ -232,8 +228,8 @@
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
image-rendering: crisp-edges;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rts-map-overlay {
|
||||
@@ -242,7 +238,8 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
/* Allow interaction inside overlay tooltips and context menus */
|
||||
pointer-events: auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -253,8 +250,8 @@
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
min-width: 150px;
|
||||
max-width: 250px;
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
pointer-events: auto;
|
||||
@@ -303,6 +300,7 @@
|
||||
border-color: rgba(239, 68, 68, 0.6);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.rts-info-content {
|
||||
@@ -310,6 +308,16 @@
|
||||
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 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -539,10 +547,114 @@
|
||||
}
|
||||
|
||||
#rts-resource-panel {
|
||||
width: 200px;
|
||||
width: 280px;
|
||||
background: var(--SmartThemeEmColor);
|
||||
padding: 8px;
|
||||
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 {
|
||||
@@ -959,3 +1071,72 @@
|
||||
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
@@ -1,3 +1,5 @@
|
||||
import PresetManager from '../src/PresetManager.js';
|
||||
|
||||
export class RTSMapCanvas {
|
||||
constructor(canvasId = 'rts-game-map') {
|
||||
this.canvas = /** @type {HTMLCanvasElement} */ (document.getElementById(canvasId));
|
||||
@@ -15,12 +17,12 @@ export class RTSMapCanvas {
|
||||
this.isDragging = false;
|
||||
this.lastMouseX = 0;
|
||||
this.lastMouseY = 0;
|
||||
this.selectedUnit = null;
|
||||
this.selectedBuilding = null;
|
||||
this.selectedEntities = [];
|
||||
this.hoveredElement = null;
|
||||
this.animationFrame = null;
|
||||
this.lastRenderTime = 0;
|
||||
this.dirty = true;
|
||||
this.fogOfWarEnabled = true;
|
||||
|
||||
this.fogOfWar = [];
|
||||
this.exploredAreas = new Set();
|
||||
@@ -94,8 +96,17 @@ export class RTSMapCanvas {
|
||||
|
||||
loadMap(mapData) {
|
||||
this.mapData = mapData;
|
||||
this.fogOfWarEnabled = PresetManager.getFeature('fogOfWar');
|
||||
if (this.fogOfWarEnabled) {
|
||||
this.initializeFogOfWar();
|
||||
}
|
||||
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() {
|
||||
@@ -111,8 +122,10 @@ export class RTSMapCanvas {
|
||||
if (this.mapData) {
|
||||
this.drawTerrain();
|
||||
this.drawEntities();
|
||||
if (this.fogOfWarEnabled) {
|
||||
this.drawFogOfWar();
|
||||
}
|
||||
}
|
||||
|
||||
this.drawParticles();
|
||||
ctx.restore();
|
||||
@@ -131,50 +144,305 @@ export class RTSMapCanvas {
|
||||
|
||||
drawTerrain() {
|
||||
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++) {
|
||||
if (tiles[y]) {
|
||||
for (let x = 0; x < width; 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) {
|
||||
case 1: color = '#22c55e'; break; // enclosure
|
||||
case 2: color = '#475569'; break; // wall
|
||||
case 3: color = '#f59e0b'; break; // locked gate
|
||||
case 4: color = '#ef4444'; break; // security camera
|
||||
default: color = '#334155'; break; // path
|
||||
case 1: color = '#654321'; break; // enclosure fence
|
||||
case 2: color = '#5A5A5A'; break; // building wall
|
||||
case 3: color = '#FFD700'; break; // entrance gate
|
||||
case 4: color = '#4A90E2'; break; // water
|
||||
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.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() {
|
||||
const ctx = this.ctx;
|
||||
const { entities } = this.mapData.map;
|
||||
|
||||
// Draw player sight indicators first (so they appear behind entities)
|
||||
this.drawPlayerSight();
|
||||
|
||||
entities.forEach(entity => {
|
||||
let color;
|
||||
switch (entity.type) {
|
||||
case 'player': color = '#3b82f6'; break;
|
||||
case 'guard': color = '#f97316'; break;
|
||||
case 'lion': color = '#eab308'; break;
|
||||
case 'tiger': color = '#f59e0b'; break;
|
||||
case 'jaguar': color = '#d97706'; break;
|
||||
case 'snow_leopard': color = '#a16207'; break;
|
||||
case 'wolf': color = '#84cc16'; break;
|
||||
case 'grizzly_bear': color = '#a3e635'; break;
|
||||
case 'polar_bear': color = '#ecfccb'; break;
|
||||
case 'panda': color = '#ffffff'; break;
|
||||
default: color = '#9ca3af'; break;
|
||||
}
|
||||
ctx.fillStyle = color;
|
||||
const centerX = entity.x * this.tileSize + this.tileSize / 2;
|
||||
const centerY = entity.y * this.tileSize + this.tileSize / 2;
|
||||
const radius = this.tileSize / 3;
|
||||
|
||||
// Get entity appearance
|
||||
const appearance = this.getEntityAppearance(entity);
|
||||
|
||||
// Draw background circle
|
||||
ctx.fillStyle = appearance.color;
|
||||
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();
|
||||
|
||||
// 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 viewportHeight = this.canvas.height / this.zoom;
|
||||
const viewportX = minimapX + (this.canvas.width / 2 - this.offsetX - viewportWidth / 2) * scaleX;
|
||||
const viewportY = minimapY + (this.canvas.height / 2 - this.offsetY - viewportHeight / 2) * scaleY;
|
||||
let viewportX = minimapX + (this.canvas.width / 2 - this.offsetX - viewportWidth / 2) * scaleX;
|
||||
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.lineWidth = 1;
|
||||
ctx.strokeRect(viewportX, viewportY, viewportWidth * scaleX, viewportHeight * scaleY);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(viewportX, viewportY, constrainedWidth, constrainedHeight);
|
||||
}
|
||||
|
||||
startAnimationLoop() {
|
||||
@@ -348,46 +627,82 @@ export class RTSMapCanvas {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
selectElement(element) {
|
||||
this.selectedUnit = null;
|
||||
this.selectedBuilding = null;
|
||||
if (element.type === 'entity') {
|
||||
this.selectedUnit = element.data;
|
||||
this.selectedEntities = [element.data];
|
||||
}
|
||||
this.showElementInfo(element);
|
||||
this.canvas.dispatchEvent(new Event('selectionChanged'));
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedUnit = null;
|
||||
this.selectedBuilding = null;
|
||||
this.selectedEntities = [];
|
||||
this.hideElementInfo();
|
||||
this.canvas.dispatchEvent(new Event('selectionChanged'));
|
||||
}
|
||||
|
||||
showElementInfo(element) {
|
||||
console.debug('[showElementInfo]', element);
|
||||
const overlay = document.getElementById('rts-map-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
// Clear any existing tooltips
|
||||
overlay.innerHTML = '';
|
||||
|
||||
const screenPos = this.worldToScreen(element.data.x * this.tileSize, element.data.y * this.tileSize);
|
||||
let infoHTML = '';
|
||||
|
||||
if (element.type === 'entity') {
|
||||
infoHTML = `
|
||||
<div class="rts-element-info" style="position: absolute; top: ${screenPos.y + 20}px; left: ${screenPos.x + 20}px;">
|
||||
<div class="rts-info-header">
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
const entity = element.data;
|
||||
const name = entity.name || (entity.type.charAt(0).toUpperCase() + entity.type.slice(1));
|
||||
const description = entity.description || 'No description available.';
|
||||
|
||||
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() {
|
||||
// Remove any existing info boxes
|
||||
document.querySelectorAll('.rts-element-info').forEach(el => el.remove());
|
||||
const overlay = document.getElementById('rts-map-overlay');
|
||||
if (overlay) {
|
||||
overlay.innerHTML = '';
|
||||
@@ -398,22 +713,91 @@ export class RTSMapCanvas {
|
||||
const overlay = document.getElementById('rts-map-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
const contextMenuHTML = `
|
||||
<div class="rts-context-menu" style="position: absolute; top: ${screenY}px; left: ${screenX}px;">
|
||||
<div class="rts-context-item" onclick="this.parentElement.remove()">Move Here</div>
|
||||
<div class="rts-context-item" onclick="this.parentElement.remove()">Cancel</div>
|
||||
</div>
|
||||
`;
|
||||
// Convert world coordinates to tile coordinates
|
||||
const tileX = Math.floor(worldX / this.tileSize);
|
||||
const tileY = Math.floor(worldY / this.tileSize);
|
||||
|
||||
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(() => {
|
||||
document.addEventListener('click', () => {
|
||||
const menu = document.querySelector('.rts-context-menu');
|
||||
if (menu) menu.remove();
|
||||
}, { once: true });
|
||||
const handleOutsideClick = (e) => {
|
||||
if (!contextMenu.contains(e.target)) {
|
||||
contextMenu.remove();
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
}, 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() {
|
||||
if (this.isDragging) {
|
||||
this.canvas.style.cursor = 'grabbing';
|
||||
@@ -450,8 +834,10 @@ export class RTSMapCanvas {
|
||||
const worldPos = this.screenToWorld(screenX, screenY);
|
||||
|
||||
const clickedElement = this.getElementAtPosition(worldPos.x, worldPos.y);
|
||||
console.debug('[handleClick] worldPos', worldPos, 'clickedElement', clickedElement);
|
||||
|
||||
if (clickedElement) {
|
||||
this.clearSelection();
|
||||
this.selectElement(clickedElement);
|
||||
} else {
|
||||
this.clearSelection();
|
||||
@@ -533,6 +919,66 @@ export class RTSMapCanvas {
|
||||
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() {
|
||||
this.stopAnimationLoop();
|
||||
window.removeEventListener('resize', this.resizeCanvas);
|
||||
|
||||
+309
-48
@@ -2,7 +2,10 @@
|
||||
import { renderExtensionTemplateAsync } from '../../../extensions.js';
|
||||
import { RTSMapCanvas } from './MapCanvas.js';
|
||||
import GameStateManager from '../src/GameStateManager.js';
|
||||
import PresetManager from '../src/PresetManager.js';
|
||||
import EventManager from '../src/EventManager.js';
|
||||
import { sendTurn } from '../src/LLMAdapter.js';
|
||||
import { updateResourcePanel } from './ResourcePanel.js';
|
||||
|
||||
export class RTSUIController {
|
||||
constructor() {
|
||||
@@ -22,6 +25,9 @@ export class RTSUIController {
|
||||
this.handleClearCommand = this.handleClearCommand.bind(this);
|
||||
this.handleMapControls = this.handleMapControls.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() {
|
||||
@@ -65,13 +71,34 @@ export class RTSUIController {
|
||||
|
||||
this.mapCanvas = new RTSMapCanvas('rts-game-map');
|
||||
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.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.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) {
|
||||
console.error('Error creating RTS interface:', error);
|
||||
this.addLogEntry('error', `Failed to create RTS interface: ${error.message}`);
|
||||
@@ -80,12 +107,6 @@ export class RTSUIController {
|
||||
}
|
||||
|
||||
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
|
||||
document.getElementById('rts-execute-btn')?.addEventListener('click', this.handleExecuteCommand);
|
||||
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) {
|
||||
const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input'));
|
||||
if (!commandInput) return;
|
||||
|
||||
const actionTemplates = {
|
||||
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();
|
||||
if (!commandInput) {
|
||||
console.error('RTS: Command input not found');
|
||||
return;
|
||||
}
|
||||
|
||||
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() {
|
||||
const commandInput = /** @type {HTMLInputElement} */ (document.getElementById('rts-command-input'));
|
||||
if (!commandInput) return;
|
||||
@@ -142,11 +240,7 @@ export class RTSUIController {
|
||||
try {
|
||||
// This is where the command would be sent to the LLM
|
||||
await sendTurn(command);
|
||||
// For now, we'll just simulate a delay and update the UI
|
||||
setTimeout(() => {
|
||||
this.updateUI();
|
||||
this.addLogEntry('result', 'Your action has consequences...');
|
||||
}, 1000);
|
||||
// The UI will now be updated by the 'rts-narrative-update' event listener
|
||||
} catch (error) {
|
||||
console.error('Failed to execute RTS command:', error);
|
||||
this.addLogEntry('error', `Failed to execute command: ${error.message}`);
|
||||
@@ -189,19 +283,42 @@ export class RTSUIController {
|
||||
|
||||
updateUI() {
|
||||
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.updateEscapedAnimalsDisplay();
|
||||
this.updateIncidentsDisplay();
|
||||
this.updatePeopleStatusDisplay();
|
||||
this.updateThreatDisplay();
|
||||
}
|
||||
|
||||
handleSelectionChange() {
|
||||
this.updateEscapedAnimalsDisplay();
|
||||
this.updatePeopleStatusDisplay();
|
||||
}
|
||||
|
||||
handleEntitySelection(entity) {
|
||||
this.mapCanvas.clearSelection();
|
||||
this.mapCanvas.selectElement({ type: 'entity', data: entity });
|
||||
this.updateEscapedAnimalsDisplay();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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-casualties', 0); // Mock data
|
||||
this.updateElementText('rts-casualties', (state.casualties && state.casualties.total) || 0);
|
||||
this.updateElementText('rts-breached', `${gateCount} (Main Gate)`);
|
||||
}
|
||||
|
||||
@@ -210,34 +327,33 @@ export class RTSUIController {
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
this.mapData.map.entities.forEach(entity => {
|
||||
if (entity.type !== 'player' && entity.type !== 'guard') {
|
||||
const state = GameStateManager.getState();
|
||||
if (state.entities) {
|
||||
state.entities.forEach(entity => {
|
||||
if (entity.type !== 'player' && entity.type !== 'guard' && entity.status === 'escaped') {
|
||||
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 = `
|
||||
<i class="fa-solid fa-paw" style="color: #dc2626;"></i>
|
||||
<span class="rts-animal-name">${entity.type.replace('_', ' ')}</span>
|
||||
<span class="rts-animal-location">${entity.enclosure ? `Enclosure` : 'Roaming'}</span>
|
||||
<span class="rts-animal-status rts-status-hunting">Hunting</span>
|
||||
<span class="rts-animal-name">${entity.name || entity.type.replace('_', ' ')}</span>
|
||||
<span class="rts-animal-location">${entity.location || 'Roaming'}</span>
|
||||
<span class="rts-animal-status rts-status-hunting">${entity.mood || 'Hunting'}</span>
|
||||
`;
|
||||
element.addEventListener('click', () => this.handleEntitySelection(entity));
|
||||
container.appendChild(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateIncidentsDisplay() {
|
||||
const container = document.getElementById('rts-active-incidents');
|
||||
if (!container) return;
|
||||
container.innerHTML = ''; // Clear previous incidents
|
||||
|
||||
// Mock data for incidents
|
||||
const incidents = [
|
||||
{ description: 'Security camera offline in reptile house', location: 'Reptile House' },
|
||||
{ description: 'Strange noises from the aviary', location: 'Aviary' },
|
||||
{ description: 'Main gate power seems to be cut', location: 'Main Gate' }
|
||||
];
|
||||
|
||||
incidents.forEach(incident => {
|
||||
const state = GameStateManager.getState();
|
||||
if (state.incidents) {
|
||||
state.incidents.forEach(incident => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'rts-incident-item';
|
||||
element.innerHTML = `
|
||||
@@ -248,26 +364,60 @@ export class RTSUIController {
|
||||
container.appendChild(element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updatePeopleStatusDisplay() {
|
||||
const container = document.getElementById('rts-people-status');
|
||||
if (!container) return;
|
||||
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');
|
||||
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 = `
|
||||
<i class="fa-solid fa-user-shield" style="color: #ea580c;"></i>
|
||||
<span class="rts-person-name">Guard ${index + 1}</span>
|
||||
<span class="rts-person-status rts-status-patrolling">Patrolling</span>
|
||||
<span class="rts-person-location">Sector ${index + 1}</span>
|
||||
<i class="fa-solid ${icon}" style="color: ${color};"></i>
|
||||
<span class="rts-person-name">${person.name || `${person.type} ${index + 1}`}</span>
|
||||
<span class="rts-person-status rts-status-${person.status || 'active'}">${person.status || 'Active'}</span>
|
||||
${escapeStatus}
|
||||
<span class="rts-person-location">@ (${person.x}, ${person.y})</span>
|
||||
`;
|
||||
element.addEventListener('click', () => this.handleEntitySelection(person));
|
||||
container.appendChild(element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateElementText(id, value) {
|
||||
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) {
|
||||
const gameLog = document.getElementById('rts-game-log');
|
||||
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() {
|
||||
return this.isFullscreen;
|
||||
}
|
||||
@@ -302,6 +552,17 @@ export class RTSUIController {
|
||||
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();
|
||||
|
||||
+204
-8
@@ -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).
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
export function createResourcePanel(rootEl) {
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'rts-resource-panel';
|
||||
panel.className = 'rts-status-panel';
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.innerHTML = `
|
||||
<li>Gold: 0</li>
|
||||
<li>Wood: 0</li>
|
||||
<li>Units: 0</li>
|
||||
panel.innerHTML = `
|
||||
<div class="rts-panel-section">
|
||||
<h3>Status Overview</h3>
|
||||
<div id="rts-overview-stats">
|
||||
<div class="stat-item">Turn: <span id="turn-counter">1</span></div>
|
||||
<div class="stat-item">Zone: <span id="current-zone">Unknown</span></div>
|
||||
<div class="stat-item">Threat: <span id="threat-level">Low</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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) {
|
||||
rootEl.appendChild(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user