Spaces:
Running
Running
6rz6
commited on
Commit
·
a32dc8b
1
Parent(s):
dfb3704
Add Medieval Village AI Emulator
Browse files- README.md +70 -5
- app.js +687 -0
- app_new.js +1798 -0
- index.html +398 -19
- requirements.txt +1 -0
- simple_app.js +127 -0
- src/ai/README.md +109 -0
- src/ai/behavior.js +183 -0
- src/ai/crowd.js +263 -0
- src/ai/environment.js +233 -0
- src/ai/llmHandler.js +189 -0
- src/ai/main.js +199 -0
- src/ai/optimization.js +274 -0
- src/ai/pathfinding.js +87 -0
- src/ai/routines.js +228 -0
- src/ai/routines_simple.js +224 -0
README.md
CHANGED
|
@@ -1,10 +1,75 @@
|
|
| 1 |
---
|
| 2 |
-
title: Medieval Village AI
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Medieval Village AI Emulator
|
| 3 |
+
emoji: 🏰
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Medieval Village AI Emulator
|
| 13 |
+
|
| 14 |
+
An interactive 3D simulation of a medieval village powered by artificial intelligence. Watch as AI-controlled villagers go about their daily routines, work, eat, sleep, and socialize in a dynamically generated environment.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
|
| 18 |
+
- **3D Visualization**: Real-time 3D rendering of villagers, buildings, and resources using Three.js
|
| 19 |
+
- **AI-Powered Villagers**: Villagers with complex behaviors based on needs (energy, hunger, social)
|
| 20 |
+
- **Dynamic Environment**: Weather system, disasters, and wildlife interactions
|
| 21 |
+
- **Interactive Controls**: Add villagers, trigger disasters, spawn animals, and dispatch warriors
|
| 22 |
+
- **LLM Integration**: Optional integration with Hugging Face models for enhanced AI behaviors
|
| 23 |
+
|
| 24 |
+
## How to Use
|
| 25 |
+
|
| 26 |
+
1. The simulation starts automatically with 3 villagers
|
| 27 |
+
2. Use the controls panel on the left to interact with the village:
|
| 28 |
+
- Add Villager: Creates a new villager at a random position
|
| 29 |
+
- Reset Simulation: Clears all villagers and restarts the simulation
|
| 30 |
+
- Adjust Time Speed: Control simulation speed (0.1x to 5.0x)
|
| 31 |
+
- Toggle Paths/Titles: Show/hide movement paths and villager names
|
| 32 |
+
3. Camera controls:
|
| 33 |
+
- Mouse: Look around (orbit)
|
| 34 |
+
- Mouse Wheel: Zoom in/out
|
| 35 |
+
- Right Mouse + Drag: Pan camera
|
| 36 |
+
4. Click on any villager to see detailed information about their state, needs, and current activities
|
| 37 |
+
|
| 38 |
+
## Technical Details
|
| 39 |
+
|
| 40 |
+
This simulation demonstrates several AI techniques:
|
| 41 |
+
|
| 42 |
+
- **Pathfinding**: A* algorithm for efficient navigation
|
| 43 |
+
- **Behavior Trees**: Decision-making system for villager actions
|
| 44 |
+
- **Daily Routines**: Time-based scheduling system
|
| 45 |
+
- **Crowd Simulation**: Steering behaviors and collision avoidance
|
| 46 |
+
- **Resource Management**: Villagers gather and use resources
|
| 47 |
+
- **Environmental Interaction**: Villagers interact with buildings and resources
|
| 48 |
+
|
| 49 |
+
## LLM Integration
|
| 50 |
+
|
| 51 |
+
The simulator can optionally integrate with Hugging Face models for enhanced AI behaviors. To use this feature:
|
| 52 |
+
|
| 53 |
+
1. Get a Hugging Face API token from [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 54 |
+
2. Set the token in the browser console: `window.HF_TOKEN = "your-token-here"`
|
| 55 |
+
3. Select a model from the LLM Controls dropdown
|
| 56 |
+
4. Ask questions about the simulation using the LLM query input
|
| 57 |
+
|
| 58 |
+
## Architecture
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
index.html # Main HTML file with UI
|
| 62 |
+
├── app_new.js # Main application logic
|
| 63 |
+
└── src/ai/ # AI system components
|
| 64 |
+
├── main.js # AI system integration
|
| 65 |
+
├── routines.js # Daily routine management
|
| 66 |
+
├── environment.js # Environment interaction
|
| 67 |
+
├── pathfinding.js # Navigation system
|
| 68 |
+
├── behavior.js # Behavior trees
|
| 69 |
+
├── crowd.js # Crowd simulation
|
| 70 |
+
└── optimization.js # Performance optimization
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## License
|
| 74 |
+
|
| 75 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
app.js
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Using globally available THREE from script tag in index.html
|
| 2 |
+
import VillageAISystem from './src/ai/main.js';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Medieval Village AI System - Three.js Visualization Application
|
| 6 |
+
*
|
| 7 |
+
* This application demonstrates the village AI system with real-time 3D visualization
|
| 8 |
+
* using Three.js. It shows villagers moving around, performing activities, and interacting
|
| 9 |
+
* with their environment.
|
| 10 |
+
*/
|
| 11 |
+
class VillageVisualizationApp {
|
| 12 |
+
constructor() {
|
| 13 |
+
this.scene = null;
|
| 14 |
+
this.camera = null;
|
| 15 |
+
this.renderer = null;
|
| 16 |
+
this.controls = null;
|
| 17 |
+
this.aiSystem = null;
|
| 18 |
+
|
| 19 |
+
// 3D Objects
|
| 20 |
+
this.villagerMeshes = new Map();
|
| 21 |
+
this.buildingMeshes = new Map();
|
| 22 |
+
this.resourceMeshes = new Map();
|
| 23 |
+
this.pathLines = new Map();
|
| 24 |
+
|
| 25 |
+
// UI Elements
|
| 26 |
+
this.uiElements = {};
|
| 27 |
+
this.selectedVillager = null;
|
| 28 |
+
this.timeSpeed = 1.0;
|
| 29 |
+
this.showPaths = true;
|
| 30 |
+
|
| 31 |
+
// Animation
|
| 32 |
+
this.clock = new THREE.Clock();
|
| 33 |
+
this.lastTime = 0;
|
| 34 |
+
this.frameCount = 0;
|
| 35 |
+
this.fps = 0;
|
| 36 |
+
|
| 37 |
+
this.init();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Initialize the application
|
| 42 |
+
*/
|
| 43 |
+
init() {
|
| 44 |
+
console.log('Initializing Village Visualization App...');
|
| 45 |
+
this.initThreeJS();
|
| 46 |
+
this.initAI();
|
| 47 |
+
this.initUI();
|
| 48 |
+
this.createEnvironment();
|
| 49 |
+
this.createInitialVillagers();
|
| 50 |
+
this.animate();
|
| 51 |
+
console.log('Village Visualization App initialized successfully');
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Initialize Three.js scene, camera, and renderer
|
| 56 |
+
*/
|
| 57 |
+
initThreeJS() {
|
| 58 |
+
console.log('Initializing Three.js...');
|
| 59 |
+
|
| 60 |
+
// Scene
|
| 61 |
+
this.scene = new THREE.Scene();
|
| 62 |
+
this.scene.background = new THREE.Color(0x87CEEB); // Sky blue
|
| 63 |
+
console.log('Scene created');
|
| 64 |
+
|
| 65 |
+
// Camera
|
| 66 |
+
this.camera = new THREE.PerspectiveCamera(
|
| 67 |
+
75,
|
| 68 |
+
window.innerWidth / window.innerHeight,
|
| 69 |
+
0.1,
|
| 70 |
+
1000
|
| 71 |
+
);
|
| 72 |
+
this.camera.position.set(20, 20, 20);
|
| 73 |
+
this.camera.lookAt(0, 0, 0);
|
| 74 |
+
console.log('Camera created');
|
| 75 |
+
|
| 76 |
+
// Renderer
|
| 77 |
+
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 78 |
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 79 |
+
this.renderer.shadowMap.enabled = true;
|
| 80 |
+
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 81 |
+
console.log('Renderer created');
|
| 82 |
+
|
| 83 |
+
// Add to DOM
|
| 84 |
+
const container = document.getElementById('container');
|
| 85 |
+
if (container) {
|
| 86 |
+
container.appendChild(this.renderer.domElement);
|
| 87 |
+
console.log('Renderer added to DOM');
|
| 88 |
+
} else {
|
| 89 |
+
console.error('Container element not found!');
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Controls
|
| 93 |
+
console.log('Initializing controls...');
|
| 94 |
+
console.log('THREE.OrbitControls:', typeof THREE !== 'undefined' ? THREE.OrbitControls : 'undefined');
|
| 95 |
+
try {
|
| 96 |
+
// Check if OrbitControls is available globally
|
| 97 |
+
if (typeof THREE !== 'undefined' && typeof THREE.OrbitControls !== 'undefined') {
|
| 98 |
+
console.log('Creating OrbitControls instance from global THREE object...');
|
| 99 |
+
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
|
| 100 |
+
console.log('OrbitControls instance created:', this.controls);
|
| 101 |
+
this.controls.enableDamping = true;
|
| 102 |
+
this.controls.dampingFactor = 0.05;
|
| 103 |
+
this.controls.enableZoom = true;
|
| 104 |
+
this.controls.enablePan = true;
|
| 105 |
+
console.log('OrbitControls initialized successfully');
|
| 106 |
+
} else {
|
| 107 |
+
console.warn('OrbitControls is not available');
|
| 108 |
+
this.controls = null;
|
| 109 |
+
}
|
| 110 |
+
} catch (error) {
|
| 111 |
+
console.warn('Error initializing OrbitControls:', error);
|
| 112 |
+
this.controls = null;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Lighting
|
| 116 |
+
this.addLighting();
|
| 117 |
+
|
| 118 |
+
// Ground plane
|
| 119 |
+
this.createGround();
|
| 120 |
+
|
| 121 |
+
// Grid helper
|
| 122 |
+
const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222);
|
| 123 |
+
this.scene.add(gridHelper);
|
| 124 |
+
|
| 125 |
+
// Handle window resize
|
| 126 |
+
window.addEventListener('resize', () => this.onWindowResize());
|
| 127 |
+
window.addEventListener('keydown', (event) => this.onKeyDown(event));
|
| 128 |
+
this.renderer.domElement.addEventListener('click', (event) => this.onMouseClick(event));
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Add lighting to the scene
|
| 133 |
+
*/
|
| 134 |
+
addLighting() {
|
| 135 |
+
// Ambient light
|
| 136 |
+
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
|
| 137 |
+
this.scene.add(ambientLight);
|
| 138 |
+
|
| 139 |
+
// Directional light (sun)
|
| 140 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| 141 |
+
directionalLight.position.set(10, 10, 5);
|
| 142 |
+
directionalLight.castShadow = true;
|
| 143 |
+
directionalLight.shadow.mapSize.width = 2048;
|
| 144 |
+
directionalLight.shadow.mapSize.height = 2048;
|
| 145 |
+
directionalLight.shadow.camera.near = 0.5;
|
| 146 |
+
directionalLight.shadow.camera.far = 50;
|
| 147 |
+
this.scene.add(directionalLight);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Create ground plane
|
| 152 |
+
*/
|
| 153 |
+
createGround() {
|
| 154 |
+
const groundGeometry = new THREE.PlaneGeometry(100, 100);
|
| 155 |
+
const groundMaterial = new THREE.MeshLambertMaterial({
|
| 156 |
+
color: 0x228B22,
|
| 157 |
+
transparent: true,
|
| 158 |
+
opacity: 0.8
|
| 159 |
+
});
|
| 160 |
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
| 161 |
+
ground.rotation.x = -Math.PI / 2;
|
| 162 |
+
ground.receiveShadow = true;
|
| 163 |
+
this.scene.add(ground);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* Initialize the AI system
|
| 168 |
+
*/
|
| 169 |
+
initAI() {
|
| 170 |
+
this.aiSystem = new VillageAISystem(this.scene);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* Initialize UI event listeners
|
| 175 |
+
*/
|
| 176 |
+
initUI() {
|
| 177 |
+
// Get UI elements
|
| 178 |
+
this.uiElements = {
|
| 179 |
+
addVillagerBtn: document.getElementById('add-villager-btn'),
|
| 180 |
+
resetBtn: document.getElementById('reset-btn'),
|
| 181 |
+
timeSpeed: document.getElementById('time-speed'),
|
| 182 |
+
timeSpeedDisplay: document.getElementById('time-speed-display'),
|
| 183 |
+
showPaths: document.getElementById('show-paths'),
|
| 184 |
+
villagerCountDisplay: document.getElementById('villager-count-display'),
|
| 185 |
+
villagerCountStat: document.getElementById('villager-count-stat'),
|
| 186 |
+
gameTime: document.getElementById('game-time'),
|
| 187 |
+
fps: document.getElementById('fps'),
|
| 188 |
+
buildingCount: document.getElementById('building-count'),
|
| 189 |
+
resourceCount: document.getElementById('resource-count'),
|
| 190 |
+
villagerList: document.getElementById('villager-list')
|
| 191 |
+
};
|
| 192 |
+
|
| 193 |
+
// Add event listeners
|
| 194 |
+
this.uiElements.addVillagerBtn.addEventListener('click', () => this.addVillager());
|
| 195 |
+
this.uiElements.resetBtn.addEventListener('click', () => this.resetSimulation());
|
| 196 |
+
this.uiElements.timeSpeed.addEventListener('input', (e) => this.updateTimeSpeed(e.target.value));
|
| 197 |
+
this.uiElements.showPaths.addEventListener('change', (e) => this.togglePaths(e.target.checked));
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* Create the 3D environment (buildings and resources)
|
| 202 |
+
*/
|
| 203 |
+
createEnvironment() {
|
| 204 |
+
// Buildings are already created in the AI system
|
| 205 |
+
// We need to create 3D meshes for them
|
| 206 |
+
this.createBuildingMeshes();
|
| 207 |
+
this.createResourceMeshes();
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/**
|
| 211 |
+
* Create 3D meshes for buildings
|
| 212 |
+
*/
|
| 213 |
+
createBuildingMeshes() {
|
| 214 |
+
const buildingGeometry = new THREE.BoxGeometry(3, 3, 3);
|
| 215 |
+
const materials = {
|
| 216 |
+
house: new THREE.MeshLambertMaterial({ color: 0x8B4513 }),
|
| 217 |
+
workshop: new THREE.MeshLambertMaterial({ color: 0x696969 }),
|
| 218 |
+
market: new THREE.MeshLambertMaterial({ color: 0xFFD700 })
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
for (const [id, building] of this.aiSystem.environmentSystem.buildings) {
|
| 222 |
+
const material = materials[building.type] || materials.house;
|
| 223 |
+
const mesh = new THREE.Mesh(buildingGeometry, material);
|
| 224 |
+
mesh.position.set(building.position[0], building.position[1], building.position[2]);
|
| 225 |
+
mesh.position.y = 1.5; // Half height
|
| 226 |
+
mesh.castShadow = true;
|
| 227 |
+
mesh.receiveShadow = true;
|
| 228 |
+
|
| 229 |
+
// Add building label
|
| 230 |
+
const label = this.createTextSprite(building.type);
|
| 231 |
+
label.position.set(0, 2.5, 0);
|
| 232 |
+
mesh.add(label);
|
| 233 |
+
|
| 234 |
+
this.buildingMeshes.set(id, mesh);
|
| 235 |
+
this.scene.add(mesh);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
this.updateBuildingCount();
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/**
|
| 242 |
+
* Create 3D meshes for resources
|
| 243 |
+
*/
|
| 244 |
+
createResourceMeshes() {
|
| 245 |
+
const resourceGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2);
|
| 246 |
+
const materials = {
|
| 247 |
+
wood: new THREE.MeshLambertMaterial({ color: 0x8B4513 }),
|
| 248 |
+
stone: new THREE.MeshLambertMaterial({ color: 0x708090 }),
|
| 249 |
+
food: new THREE.MeshLambertMaterial({ color: 0x32CD32 })
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
for (const [id, resource] of this.aiSystem.environmentSystem.resources) {
|
| 253 |
+
const material = materials[resource.type] || materials.wood;
|
| 254 |
+
const mesh = new THREE.Mesh(resourceGeometry, material);
|
| 255 |
+
mesh.position.set(resource.position[0], resource.position[1], resource.position[2]);
|
| 256 |
+
mesh.position.y = 1; // Half height
|
| 257 |
+
mesh.castShadow = true;
|
| 258 |
+
mesh.receiveShadow = true;
|
| 259 |
+
|
| 260 |
+
// Add resource label
|
| 261 |
+
const label = this.createTextSprite(`${resource.type} (${resource.amount})`);
|
| 262 |
+
label.position.set(0, 1.5, 0);
|
| 263 |
+
mesh.add(label);
|
| 264 |
+
|
| 265 |
+
this.resourceMeshes.set(id, mesh);
|
| 266 |
+
this.scene.add(mesh);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
this.updateResourceCount();
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/**
|
| 273 |
+
* Create initial villagers
|
| 274 |
+
*/
|
| 275 |
+
createInitialVillagers() {
|
| 276 |
+
// Create a few initial villagers at random positions
|
| 277 |
+
const positions = [
|
| 278 |
+
[0, 0, 0],
|
| 279 |
+
[5, 0, 5],
|
| 280 |
+
[-3, 0, -3]
|
| 281 |
+
];
|
| 282 |
+
|
| 283 |
+
positions.forEach((position, index) => {
|
| 284 |
+
this.createVillager(`villager${index + 1}`, position);
|
| 285 |
+
});
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/**
|
| 289 |
+
* Create a new villager
|
| 290 |
+
*/
|
| 291 |
+
createVillager(id, position) {
|
| 292 |
+
const villager = this.aiSystem.createVillager(id, position);
|
| 293 |
+
this.createVillagerMesh(villager);
|
| 294 |
+
this.updateVillagerCount();
|
| 295 |
+
return villager;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
/**
|
| 299 |
+
* Create 3D mesh for a villager
|
| 300 |
+
*/
|
| 301 |
+
createVillagerMesh(villager) {
|
| 302 |
+
// Create villager geometry (sphere)
|
| 303 |
+
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
|
| 304 |
+
const material = new THREE.MeshLambertMaterial({
|
| 305 |
+
color: this.getStateColor(villager.state)
|
| 306 |
+
});
|
| 307 |
+
|
| 308 |
+
const mesh = new THREE.Mesh(geometry, material);
|
| 309 |
+
mesh.position.set(villager.position[0], villager.position[1], villager.position[2]);
|
| 310 |
+
mesh.position.y = 0.5; // Half height
|
| 311 |
+
mesh.castShadow = true;
|
| 312 |
+
mesh.receiveShadow = true;
|
| 313 |
+
|
| 314 |
+
// Add villager ID label
|
| 315 |
+
const label = this.createTextSprite(villager.id);
|
| 316 |
+
label.position.set(0, 1.2, 0);
|
| 317 |
+
mesh.add(label);
|
| 318 |
+
|
| 319 |
+
// Store reference to villager in mesh
|
| 320 |
+
mesh.userData.villager = villager;
|
| 321 |
+
|
| 322 |
+
this.villagerMeshes.set(villager.id, mesh);
|
| 323 |
+
this.scene.add(mesh);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/**
|
| 327 |
+
* Get color for villager state
|
| 328 |
+
*/
|
| 329 |
+
getStateColor(state) {
|
| 330 |
+
const colors = {
|
| 331 |
+
sleep: 0x7f8c8d, // Gray
|
| 332 |
+
work: 0xe74c3c, // Red
|
| 333 |
+
eat: 0xf39c12, // Orange
|
| 334 |
+
socialize: 0x9b59b6, // Purple
|
| 335 |
+
idle: 0x95a5a6 // Light gray
|
| 336 |
+
};
|
| 337 |
+
return colors[state] || colors.idle;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/**
|
| 341 |
+
* Create a text sprite for labels
|
| 342 |
+
*/
|
| 343 |
+
createTextSprite(text) {
|
| 344 |
+
const canvas = document.createElement('canvas');
|
| 345 |
+
const context = canvas.getContext('2d');
|
| 346 |
+
canvas.width = 256;
|
| 347 |
+
canvas.height = 128;
|
| 348 |
+
|
| 349 |
+
context.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
| 350 |
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
| 351 |
+
|
| 352 |
+
context.fillStyle = 'white';
|
| 353 |
+
context.font = '32px Arial';
|
| 354 |
+
context.textAlign = 'center';
|
| 355 |
+
context.textBaseline = 'middle';
|
| 356 |
+
context.fillText(text, canvas.width / 2, canvas.height / 2);
|
| 357 |
+
|
| 358 |
+
const texture = new THREE.CanvasTexture(canvas);
|
| 359 |
+
const spriteMaterial = new THREE.SpriteMaterial({
|
| 360 |
+
map: texture,
|
| 361 |
+
transparent: true
|
| 362 |
+
});
|
| 363 |
+
const sprite = new THREE.Sprite(spriteMaterial);
|
| 364 |
+
sprite.scale.set(3, 1.5, 1);
|
| 365 |
+
|
| 366 |
+
return sprite;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/**
|
| 370 |
+
* Add a new villager via UI
|
| 371 |
+
*/
|
| 372 |
+
addVillager() {
|
| 373 |
+
const villagerCount = this.villagerMeshes.size;
|
| 374 |
+
const id = `villager${villagerCount + 1}`;
|
| 375 |
+
|
| 376 |
+
// Random position within bounds
|
| 377 |
+
const position = [
|
| 378 |
+
(Math.random() - 0.5) * 40,
|
| 379 |
+
0,
|
| 380 |
+
(Math.random() - 0.5) * 40
|
| 381 |
+
];
|
| 382 |
+
|
| 383 |
+
this.createVillager(id, position);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
/**
|
| 387 |
+
* Reset the simulation
|
| 388 |
+
*/
|
| 389 |
+
resetSimulation() {
|
| 390 |
+
// Clear all villagers
|
| 391 |
+
for (const [id, mesh] of this.villagerMeshes) {
|
| 392 |
+
this.scene.remove(mesh);
|
| 393 |
+
}
|
| 394 |
+
this.villagerMeshes.clear();
|
| 395 |
+
|
| 396 |
+
// Clear path lines
|
| 397 |
+
for (const [id, line] of this.pathLines) {
|
| 398 |
+
this.scene.remove(line);
|
| 399 |
+
}
|
| 400 |
+
this.pathLines.clear();
|
| 401 |
+
|
| 402 |
+
// Reset AI system
|
| 403 |
+
this.aiSystem = new VillageAISystem(this.scene);
|
| 404 |
+
this.createEnvironment();
|
| 405 |
+
|
| 406 |
+
// Clear selection
|
| 407 |
+
this.selectedVillager = null;
|
| 408 |
+
this.updateVillagerInfo();
|
| 409 |
+
|
| 410 |
+
this.updateVillagerCount();
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
/**
|
| 414 |
+
* Update time speed
|
| 415 |
+
*/
|
| 416 |
+
updateTimeSpeed(speed) {
|
| 417 |
+
this.timeSpeed = parseFloat(speed);
|
| 418 |
+
this.uiElements.timeSpeedDisplay.textContent = `${speed}x`;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/**
|
| 422 |
+
* Toggle path visibility
|
| 423 |
+
*/
|
| 424 |
+
togglePaths(show) {
|
| 425 |
+
this.showPaths = show;
|
| 426 |
+
for (const [id, line] of this.pathLines) {
|
| 427 |
+
line.visible = show;
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/**
|
| 432 |
+
* Handle window resize
|
| 433 |
+
*/
|
| 434 |
+
onWindowResize() {
|
| 435 |
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
| 436 |
+
this.camera.updateProjectionMatrix();
|
| 437 |
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
/**
|
| 441 |
+
* Handle keyboard input
|
| 442 |
+
*/
|
| 443 |
+
onKeyDown(event) {
|
| 444 |
+
console.log('Key pressed:', event.code);
|
| 445 |
+
const speed = 0.5;
|
| 446 |
+
|
| 447 |
+
// Handle keyboard input directly for camera movement
|
| 448 |
+
// Even when OrbitControls are available, we want to handle WASD keys
|
| 449 |
+
switch (event.code) {
|
| 450 |
+
case 'KeyW':
|
| 451 |
+
this.camera.position.z -= speed;
|
| 452 |
+
this.camera.lookAt(0, 0, 0);
|
| 453 |
+
console.log('Moved camera forward');
|
| 454 |
+
break;
|
| 455 |
+
case 'KeyS':
|
| 456 |
+
this.camera.position.z += speed;
|
| 457 |
+
this.camera.lookAt(0, 0, 0);
|
| 458 |
+
console.log('Moved camera backward');
|
| 459 |
+
break;
|
| 460 |
+
case 'KeyA':
|
| 461 |
+
this.camera.position.x -= speed;
|
| 462 |
+
this.camera.lookAt(0, 0, 0);
|
| 463 |
+
console.log('Moved camera left');
|
| 464 |
+
break;
|
| 465 |
+
case 'KeyD':
|
| 466 |
+
this.camera.position.x += speed;
|
| 467 |
+
this.camera.lookAt(0, 0, 0);
|
| 468 |
+
console.log('Moved camera right');
|
| 469 |
+
break;
|
| 470 |
+
case 'Space':
|
| 471 |
+
this.camera.position.y += speed;
|
| 472 |
+
this.camera.lookAt(0, 0, 0);
|
| 473 |
+
console.log('Moved camera up');
|
| 474 |
+
break;
|
| 475 |
+
case 'ShiftLeft':
|
| 476 |
+
this.camera.position.y -= speed;
|
| 477 |
+
this.camera.lookAt(0, 0, 0);
|
| 478 |
+
console.log('Moved camera down');
|
| 479 |
+
break;
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
/**
|
| 484 |
+
* Handle mouse clicks for villager selection
|
| 485 |
+
*/
|
| 486 |
+
onMouseClick(event) {
|
| 487 |
+
const mouse = { x: 0, y: 0 };
|
| 488 |
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
| 489 |
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
| 490 |
+
|
| 491 |
+
const raycaster = new THREE.Raycaster();
|
| 492 |
+
raycaster.setFromCamera(mouse, this.camera);
|
| 493 |
+
|
| 494 |
+
const villagerMeshes = Array.from(this.villagerMeshes.values());
|
| 495 |
+
const intersects = raycaster.intersectObjects(villagerMeshes);
|
| 496 |
+
|
| 497 |
+
if (intersects.length > 0) {
|
| 498 |
+
const selectedMesh = intersects[0].object;
|
| 499 |
+
this.selectedVillager = selectedMesh.userData.villager;
|
| 500 |
+
this.updateVillagerInfo();
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
/**
|
| 505 |
+
* Update villager count display
|
| 506 |
+
*/
|
| 507 |
+
updateVillagerCount() {
|
| 508 |
+
const count = this.villagerMeshes.size;
|
| 509 |
+
this.uiElements.villagerCountDisplay.textContent = count;
|
| 510 |
+
this.uiElements.villagerCountStat.textContent = count;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
/**
|
| 514 |
+
* Update building count display
|
| 515 |
+
*/
|
| 516 |
+
updateBuildingCount() {
|
| 517 |
+
const count = this.buildingMeshes.size;
|
| 518 |
+
this.uiElements.buildingCount.textContent = count;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/**
|
| 522 |
+
* Update resource count display
|
| 523 |
+
*/
|
| 524 |
+
updateResourceCount() {
|
| 525 |
+
const count = this.resourceMeshes.size;
|
| 526 |
+
this.uiElements.resourceCount.textContent = count;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
/**
|
| 530 |
+
* Update villager information panel
|
| 531 |
+
*/
|
| 532 |
+
updateVillagerInfo() {
|
| 533 |
+
const villagerList = this.uiElements.villagerList;
|
| 534 |
+
|
| 535 |
+
if (!this.selectedVillager) {
|
| 536 |
+
villagerList.innerHTML = '<p>No villager selected</p>';
|
| 537 |
+
return;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
const villager = this.selectedVillager;
|
| 541 |
+
villagerList.innerHTML = `
|
| 542 |
+
<div class="villager-item selected">
|
| 543 |
+
<div><strong>${villager.id}</strong></div>
|
| 544 |
+
<div>State: <span class="state-indicator state-${villager.state}"></span>${villager.state}</div>
|
| 545 |
+
<div>Position: (${villager.position[0].toFixed(1)}, ${villager.position[1].toFixed(1)}, ${villager.position[2].toFixed(1)})</div>
|
| 546 |
+
<div>Energy: ${villager.energy.toFixed(1)}%</div>
|
| 547 |
+
<div>Hunger: ${villager.hunger.toFixed(1)}%</div>
|
| 548 |
+
<div>Social Need: ${villager.socialNeed.toFixed(1)}%</div>
|
| 549 |
+
<div>Path Points: ${villager.path.length}</div>
|
| 550 |
+
</div>
|
| 551 |
+
`;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
/**
|
| 555 |
+
* Update path visualization for a villager
|
| 556 |
+
*/
|
| 557 |
+
updatePathVisualization(villager) {
|
| 558 |
+
const villagerId = villager.id;
|
| 559 |
+
|
| 560 |
+
// Remove existing path line
|
| 561 |
+
if (this.pathLines.has(villagerId)) {
|
| 562 |
+
this.scene.remove(this.pathLines.get(villagerId));
|
| 563 |
+
this.pathLines.delete(villagerId);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
// Create new path line if villager has a path
|
| 567 |
+
if (villager.path.length > 1) {
|
| 568 |
+
const geometry = new THREE.BufferGeometry();
|
| 569 |
+
const positions = [];
|
| 570 |
+
|
| 571 |
+
// Add current position
|
| 572 |
+
positions.push(villager.position[0], 0.1, villager.position[2]);
|
| 573 |
+
|
| 574 |
+
// Add path points
|
| 575 |
+
for (const point of villager.path) {
|
| 576 |
+
positions.push(point[0], 0.1, point[2]);
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
| 580 |
+
|
| 581 |
+
const material = new THREE.LineBasicMaterial({
|
| 582 |
+
color: this.getStateColor(villager.state),
|
| 583 |
+
linewidth: 3
|
| 584 |
+
});
|
| 585 |
+
|
| 586 |
+
const line = new THREE.Line(geometry, material);
|
| 587 |
+
line.visible = this.showPaths;
|
| 588 |
+
|
| 589 |
+
this.pathLines.set(villagerId, line);
|
| 590 |
+
this.scene.add(line);
|
| 591 |
+
}
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
/**
|
| 595 |
+
* Update game time display
|
| 596 |
+
*/
|
| 597 |
+
updateGameTime() {
|
| 598 |
+
const time = this.aiSystem.routineManager.currentTime;
|
| 599 |
+
const hours = Math.floor(time);
|
| 600 |
+
const minutes = Math.floor((time - hours) * 60);
|
| 601 |
+
this.uiElements.gameTime.textContent = `${hours}:${minutes.toString().padStart(2, '0')}`;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
/**
|
| 605 |
+
* Update FPS counter
|
| 606 |
+
*/
|
| 607 |
+
updateFPS() {
|
| 608 |
+
this.frameCount++;
|
| 609 |
+
const currentTime = performance.now();
|
| 610 |
+
|
| 611 |
+
if (currentTime - this.lastTime >= 1000) {
|
| 612 |
+
this.fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime));
|
| 613 |
+
this.frameCount = 0;
|
| 614 |
+
this.lastTime = currentTime;
|
| 615 |
+
this.uiElements.fps.textContent = this.fps;
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
/**
|
| 620 |
+
* Animation loop
|
| 621 |
+
*/
|
| 622 |
+
animate() {
|
| 623 |
+
requestAnimationFrame(() => this.animate());
|
| 624 |
+
|
| 625 |
+
const deltaTime = this.clock.getDelta() * this.timeSpeed;
|
| 626 |
+
|
| 627 |
+
// Update AI system
|
| 628 |
+
this.aiSystem.update(deltaTime);
|
| 629 |
+
|
| 630 |
+
// Update 3D visualization
|
| 631 |
+
this.updateVillagerMeshes();
|
| 632 |
+
this.updatePathVisualizations();
|
| 633 |
+
|
| 634 |
+
// Update UI
|
| 635 |
+
this.updateGameTime();
|
| 636 |
+
this.updateFPS();
|
| 637 |
+
this.updateVillagerInfo();
|
| 638 |
+
|
| 639 |
+
// Update controls
|
| 640 |
+
if (this.controls) {
|
| 641 |
+
// console.log('Updating controls');
|
| 642 |
+
this.controls.update();
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
// Render scene
|
| 646 |
+
if (this.renderer && this.scene && this.camera) {
|
| 647 |
+
this.renderer.render(this.scene, this.camera);
|
| 648 |
+
} else {
|
| 649 |
+
console.error('Missing renderer, scene, or camera for rendering');
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
/**
|
| 654 |
+
* Update villager mesh positions and colors
|
| 655 |
+
*/
|
| 656 |
+
updateVillagerMeshes() {
|
| 657 |
+
for (const [villagerId, mesh] of this.villagerMeshes) {
|
| 658 |
+
const villager = mesh.userData.villager;
|
| 659 |
+
|
| 660 |
+
// Update position
|
| 661 |
+
mesh.position.set(villager.position[0], villager.position[1], villager.position[2]);
|
| 662 |
+
mesh.position.y = 0.5;
|
| 663 |
+
|
| 664 |
+
// Update color based on state
|
| 665 |
+
const material = mesh.material;
|
| 666 |
+
const newColor = this.getStateColor(villager.state);
|
| 667 |
+
if (material.color.getHex() !== newColor) {
|
| 668 |
+
material.color.setHex(newColor);
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
/**
|
| 674 |
+
* Update all path visualizations
|
| 675 |
+
*/
|
| 676 |
+
updatePathVisualizations() {
|
| 677 |
+
for (const [villagerId, mesh] of this.villagerMeshes) {
|
| 678 |
+
const villager = mesh.userData.villager;
|
| 679 |
+
this.updatePathVisualization(villager);
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
// Initialize the application when the page loads
|
| 685 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 686 |
+
const app = new VillageVisualizationApp();
|
| 687 |
+
});
|
app_new.js
ADDED
|
@@ -0,0 +1,1798 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Medieval Village AI System - Three.js Visualization Application
|
| 2 |
+
// Using globally available THREE from script tag in index.html
|
| 3 |
+
import VillageAISystem from './src/ai/main.js';
|
| 4 |
+
import LLMHandler from './src/ai/llmHandler.js';
|
| 5 |
+
|
| 6 |
+
class VillageVisualizationApp {
|
| 7 |
+
constructor(hfToken = null) {
|
| 8 |
+
this.scene = null;
|
| 9 |
+
this.camera = null;
|
| 10 |
+
this.renderer = null;
|
| 11 |
+
this.controls = null;
|
| 12 |
+
this.aiSystem = null;
|
| 13 |
+
|
| 14 |
+
// 3D Objects
|
| 15 |
+
this.villagerMeshes = new Map();
|
| 16 |
+
this.buildingMeshes = new Map();
|
| 17 |
+
this.resourceMeshes = new Map();
|
| 18 |
+
this.pathLines = new Map();
|
| 19 |
+
|
| 20 |
+
// UI Elements
|
| 21 |
+
this.uiElements = {};
|
| 22 |
+
this.selectedVillager = null;
|
| 23 |
+
this.timeSpeed = 1.0;
|
| 24 |
+
this.showPaths = true;
|
| 25 |
+
this.showTitles = true;
|
| 26 |
+
|
| 27 |
+
// LLM System
|
| 28 |
+
this.llmHandler = new LLMHandler(hfToken);
|
| 29 |
+
this.llmConnected = false;
|
| 30 |
+
|
| 31 |
+
// Log token status for debugging
|
| 32 |
+
console.log('LLM Handler initialized with token:', hfToken ? 'Set' : 'Not set');
|
| 33 |
+
if (hfToken) {
|
| 34 |
+
console.log('Token length:', hfToken.length);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Initialize LLM status indicator to red (disconnected)
|
| 38 |
+
this.updateLLMStatusIndicator(false);
|
| 39 |
+
|
| 40 |
+
// New systems
|
| 41 |
+
this.weatherSystem = {
|
| 42 |
+
fogIntensity: 50,
|
| 43 |
+
currentWeather: 'sun',
|
| 44 |
+
rainParticles: [],
|
| 45 |
+
snowParticles: []
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
this.disasterSystem = {
|
| 49 |
+
activeDisasters: new Map(),
|
| 50 |
+
fireEffects: [],
|
| 51 |
+
floodEffects: []
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
this.animalSystem = {
|
| 55 |
+
animals: new Map(),
|
| 56 |
+
beasts: new Map()
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
this.warriorSystem = {
|
| 60 |
+
warriors: new Map(),
|
| 61 |
+
dispatched: false
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
// Animation
|
| 65 |
+
this.clock = new THREE.Clock();
|
| 66 |
+
this.lastTime = 0;
|
| 67 |
+
this.frameCount = 0;
|
| 68 |
+
this.fps = 0;
|
| 69 |
+
|
| 70 |
+
this.init();
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
init() {
|
| 74 |
+
console.log('Initializing Village Visualization App...');
|
| 75 |
+
this.initThreeJS();
|
| 76 |
+
this.initAI();
|
| 77 |
+
this.initUI();
|
| 78 |
+
this.createEnvironment();
|
| 79 |
+
this.createInitialVillagers();
|
| 80 |
+
this.animate();
|
| 81 |
+
console.log('Village Visualization App initialized successfully');
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
initThreeJS() {
|
| 85 |
+
console.log('Initializing Three.js...');
|
| 86 |
+
|
| 87 |
+
// Scene
|
| 88 |
+
this.scene = new THREE.Scene();
|
| 89 |
+
// Create a more realistic sky gradient background
|
| 90 |
+
this.scene.background = new THREE.Color(0x87CEEB);
|
| 91 |
+
console.log('Scene created');
|
| 92 |
+
|
| 93 |
+
// Camera
|
| 94 |
+
this.camera = new THREE.PerspectiveCamera(
|
| 95 |
+
75,
|
| 96 |
+
window.innerWidth / window.innerHeight,
|
| 97 |
+
0.1,
|
| 98 |
+
1000
|
| 99 |
+
);
|
| 100 |
+
this.camera.position.set(15, 12, 15);
|
| 101 |
+
this.camera.lookAt(0, 0, 0);
|
| 102 |
+
console.log('Camera created');
|
| 103 |
+
|
| 104 |
+
// Renderer
|
| 105 |
+
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 106 |
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 107 |
+
this.renderer.shadowMap.enabled = true;
|
| 108 |
+
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 109 |
+
this.renderer.setClearColor(0x87CEEB, 1); // Set clear color to sky blue
|
| 110 |
+
this.renderer.gammaOutput = true;
|
| 111 |
+
this.renderer.gammaFactor = 2.2;
|
| 112 |
+
console.log('Renderer created');
|
| 113 |
+
|
| 114 |
+
// Add to DOM
|
| 115 |
+
const container = document.getElementById('container');
|
| 116 |
+
if (container) {
|
| 117 |
+
container.appendChild(this.renderer.domElement);
|
| 118 |
+
console.log('Renderer added to DOM');
|
| 119 |
+
} else {
|
| 120 |
+
console.error('Container element not found!');
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Lighting
|
| 124 |
+
this.addLighting();
|
| 125 |
+
|
| 126 |
+
// Ground plane
|
| 127 |
+
this.createGround();
|
| 128 |
+
|
| 129 |
+
// Grid helper
|
| 130 |
+
const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222);
|
| 131 |
+
this.scene.add(gridHelper);
|
| 132 |
+
|
| 133 |
+
// Controls
|
| 134 |
+
console.log('Initializing controls...');
|
| 135 |
+
console.log('THREE.OrbitControls:', typeof THREE !== 'undefined' ? THREE.OrbitControls : 'undefined');
|
| 136 |
+
try {
|
| 137 |
+
// Check if OrbitControls is available globally
|
| 138 |
+
if (typeof THREE !== 'undefined' && typeof THREE.OrbitControls !== 'undefined') {
|
| 139 |
+
console.log('Creating OrbitControls instance from global THREE object...');
|
| 140 |
+
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
|
| 141 |
+
console.log('OrbitControls instance created:', this.controls);
|
| 142 |
+
this.controls.enableDamping = true;
|
| 143 |
+
this.controls.dampingFactor = 0.05;
|
| 144 |
+
this.controls.enableZoom = true;
|
| 145 |
+
this.controls.enablePan = true;
|
| 146 |
+
console.log('OrbitControls initialized successfully');
|
| 147 |
+
} else {
|
| 148 |
+
console.warn('OrbitControls is not available');
|
| 149 |
+
this.controls = null;
|
| 150 |
+
}
|
| 151 |
+
} catch (error) {
|
| 152 |
+
console.warn('Error initializing OrbitControls:', error);
|
| 153 |
+
this.controls = null;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Handle window resize
|
| 157 |
+
window.addEventListener('resize', () => this.onWindowResize());
|
| 158 |
+
window.addEventListener('keydown', (event) => this.onKeyDown(event));
|
| 159 |
+
|
| 160 |
+
// Handle mouse clicks for object selection
|
| 161 |
+
this.renderer.domElement.addEventListener('click', (event) => this.onMouseClick(event));
|
| 162 |
+
|
| 163 |
+
// Configure OrbitControls to work properly with object selection
|
| 164 |
+
if (this.controls) {
|
| 165 |
+
// Enable all controls but make sure they don't interfere with clicks
|
| 166 |
+
this.controls.enableRotate = true;
|
| 167 |
+
this.controls.enableZoom = true;
|
| 168 |
+
this.controls.enablePan = true;
|
| 169 |
+
|
| 170 |
+
// Disable keyboard navigation in OrbitControls to avoid conflicts
|
| 171 |
+
this.controls.enableKeys = false;
|
| 172 |
+
|
| 173 |
+
// Use damping for smoother controls
|
| 174 |
+
this.controls.enableDamping = true;
|
| 175 |
+
this.controls.dampingFactor = 0.05;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
addLighting() {
|
| 180 |
+
// Ambient light
|
| 181 |
+
const ambientLight = new THREE.AmbientLight(0x404040, 0.4);
|
| 182 |
+
this.scene.add(ambientLight);
|
| 183 |
+
|
| 184 |
+
// Directional light (sun)
|
| 185 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| 186 |
+
directionalLight.position.set(10, 15, 5);
|
| 187 |
+
directionalLight.castShadow = true;
|
| 188 |
+
directionalLight.shadow.mapSize.width = 2048;
|
| 189 |
+
directionalLight.shadow.mapSize.height = 2048;
|
| 190 |
+
directionalLight.shadow.camera.near = 0.5;
|
| 191 |
+
directionalLight.shadow.camera.far = 50;
|
| 192 |
+
directionalLight.shadow.camera.left = -20;
|
| 193 |
+
directionalLight.shadow.camera.right = 20;
|
| 194 |
+
directionalLight.shadow.camera.top = 20;
|
| 195 |
+
directionalLight.shadow.camera.bottom = -20;
|
| 196 |
+
this.scene.add(directionalLight);
|
| 197 |
+
|
| 198 |
+
// Add a fill light
|
| 199 |
+
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
| 200 |
+
fillLight.position.set(-10, 5, -10);
|
| 201 |
+
this.scene.add(fillLight);
|
| 202 |
+
|
| 203 |
+
// Add a hemisphere light for more natural outdoor lighting
|
| 204 |
+
const hemisphereLight = new THREE.HemisphereLight(0x87CEEB, 0x3a5f3a, 0.2);
|
| 205 |
+
this.scene.add(hemisphereLight);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
createGround() {
|
| 209 |
+
// Create a more detailed ground with texture
|
| 210 |
+
const groundGeometry = new THREE.PlaneGeometry(100, 100, 20, 20);
|
| 211 |
+
|
| 212 |
+
// Create a more realistic ground material
|
| 213 |
+
const groundMaterial = new THREE.MeshLambertMaterial({
|
| 214 |
+
color: 0x3a5f3a,
|
| 215 |
+
wireframe: false
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
| 219 |
+
ground.rotation.x = -Math.PI / 2;
|
| 220 |
+
ground.receiveShadow = true;
|
| 221 |
+
|
| 222 |
+
// Add some variation to the ground
|
| 223 |
+
const vertices = ground.geometry.attributes.position.array;
|
| 224 |
+
for (let i = 0; i < vertices.length; i += 3) {
|
| 225 |
+
// Add some noise to the y position for a more natural look
|
| 226 |
+
vertices[i + 1] = (Math.random() - 0.5) * 0.5;
|
| 227 |
+
}
|
| 228 |
+
ground.geometry.attributes.position.needsUpdate = true;
|
| 229 |
+
ground.geometry.computeVertexNormals();
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
this.scene.add(ground);
|
| 233 |
+
|
| 234 |
+
// Add fog to the scene for a more realistic atmosphere
|
| 235 |
+
this.scene.fog = new THREE.Fog(0x87CEEB, 20, 50);
|
| 236 |
+
}
|
| 237 |
+
initAI() {
|
| 238 |
+
this.aiSystem = new VillageAISystem(this.scene);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
initUI() {
|
| 242 |
+
// Get UI elements
|
| 243 |
+
this.uiElements = {
|
| 244 |
+
addVillagerBtn: document.getElementById('add-villager-btn'),
|
| 245 |
+
resetBtn: document.getElementById('reset-btn'),
|
| 246 |
+
timeSpeed: document.getElementById('time-speed'),
|
| 247 |
+
timeSpeedDisplay: document.getElementById('time-speed-display'),
|
| 248 |
+
showPaths: document.getElementById('show-paths'),
|
| 249 |
+
showTitles: document.getElementById('show-titles'),
|
| 250 |
+
fogControl: document.getElementById('fog-control'),
|
| 251 |
+
weatherSun: document.getElementById('weather-sun'),
|
| 252 |
+
weatherRain: document.getElementById('weather-rain'),
|
| 253 |
+
weatherSnow: document.getElementById('weather-snow'),
|
| 254 |
+
disasterFire: document.getElementById('disaster-fire'),
|
| 255 |
+
disasterHurricane: document.getElementById('disaster-hurricane'),
|
| 256 |
+
disasterFlood: document.getElementById('disaster-flood'),
|
| 257 |
+
disasterEarthquake: document.getElementById('disaster-earthquake'),
|
| 258 |
+
disasterPlague: document.getElementById('disaster-plague'),
|
| 259 |
+
spawnWolf: document.getElementById('spawn-wolf'),
|
| 260 |
+
spawnBear: document.getElementById('spawn-bear'),
|
| 261 |
+
spawnDragon: document.getElementById('spawn-dragon'),
|
| 262 |
+
addWarrior: document.getElementById('add-warrior'),
|
| 263 |
+
dispatchWarriors: document.getElementById('dispatch-warriors'),
|
| 264 |
+
villagerCountDisplay: document.getElementById('villager-count-display'),
|
| 265 |
+
villagerCountStat: document.getElementById('villager-count-stat'),
|
| 266 |
+
gameTime: document.getElementById('game-time'),
|
| 267 |
+
fps: document.getElementById('fps'),
|
| 268 |
+
buildingCount: document.getElementById('building-count'),
|
| 269 |
+
resourceCount: document.getElementById('resource-count'),
|
| 270 |
+
villagerList: document.getElementById('villager-list'),
|
| 271 |
+
// LLM UI elements
|
| 272 |
+
llmModel: document.getElementById('llm-model'),
|
| 273 |
+
llmQuery: document.getElementById('llm-query'),
|
| 274 |
+
llmSubmit: document.getElementById('llm-submit'),
|
| 275 |
+
llmResponse: document.getElementById('llm-response')
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
// Add event listeners
|
| 279 |
+
if (this.uiElements.addVillagerBtn) {
|
| 280 |
+
this.uiElements.addVillagerBtn.addEventListener('click', () => this.addVillager());
|
| 281 |
+
}
|
| 282 |
+
if (this.uiElements.resetBtn) {
|
| 283 |
+
this.uiElements.resetBtn.addEventListener('click', () => this.resetSimulation());
|
| 284 |
+
}
|
| 285 |
+
if (this.uiElements.timeSpeed) {
|
| 286 |
+
this.uiElements.timeSpeed.addEventListener('input', (e) => this.updateTimeSpeed(e.target.value));
|
| 287 |
+
}
|
| 288 |
+
if (this.uiElements.showPaths) {
|
| 289 |
+
this.uiElements.showPaths.addEventListener('change', (e) => this.togglePaths(e.target.checked));
|
| 290 |
+
}
|
| 291 |
+
if (this.uiElements.showTitles) {
|
| 292 |
+
this.uiElements.showTitles.addEventListener('change', (e) => this.toggleTitles(e.target.checked));
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// Weather controls
|
| 296 |
+
if (this.uiElements.fogControl) {
|
| 297 |
+
this.uiElements.fogControl.addEventListener('input', (e) => this.updateFog(e.target.value));
|
| 298 |
+
}
|
| 299 |
+
if (this.uiElements.weatherSun) {
|
| 300 |
+
this.uiElements.weatherSun.addEventListener('click', () => this.setWeather('sun'));
|
| 301 |
+
}
|
| 302 |
+
if (this.uiElements.weatherRain) {
|
| 303 |
+
this.uiElements.weatherRain.addEventListener('click', () => this.setWeather('rain'));
|
| 304 |
+
}
|
| 305 |
+
if (this.uiElements.weatherSnow) {
|
| 306 |
+
this.uiElements.weatherSnow.addEventListener('click', () => this.setWeather('snow'));
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Disaster controls
|
| 310 |
+
if (this.uiElements.disasterFire) {
|
| 311 |
+
this.uiElements.disasterFire.addEventListener('click', () => this.triggerDisaster('fire'));
|
| 312 |
+
}
|
| 313 |
+
if (this.uiElements.disasterHurricane) {
|
| 314 |
+
this.uiElements.disasterHurricane.addEventListener('click', () => this.triggerDisaster('hurricane'));
|
| 315 |
+
}
|
| 316 |
+
if (this.uiElements.disasterFlood) {
|
| 317 |
+
this.uiElements.disasterFlood.addEventListener('click', () => this.triggerDisaster('flood'));
|
| 318 |
+
}
|
| 319 |
+
if (this.uiElements.disasterEarthquake) {
|
| 320 |
+
this.uiElements.disasterEarthquake.addEventListener('click', () => this.triggerDisaster('earthquake'));
|
| 321 |
+
}
|
| 322 |
+
if (this.uiElements.disasterPlague) {
|
| 323 |
+
this.uiElements.disasterPlague.addEventListener('click', () => this.triggerDisaster('plague'));
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Animal/Beast controls
|
| 327 |
+
if (this.uiElements.spawnWolf) {
|
| 328 |
+
this.uiElements.spawnWolf.addEventListener('click', () => this.spawnAnimal('wolf'));
|
| 329 |
+
}
|
| 330 |
+
if (this.uiElements.spawnBear) {
|
| 331 |
+
this.uiElements.spawnBear.addEventListener('click', () => this.spawnAnimal('bear'));
|
| 332 |
+
}
|
| 333 |
+
if (this.uiElements.spawnDragon) {
|
| 334 |
+
this.uiElements.spawnDragon.addEventListener('click', () => this.spawnAnimal('dragon'));
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Warrior controls
|
| 338 |
+
if (this.uiElements.addWarrior) {
|
| 339 |
+
this.uiElements.addWarrior.addEventListener('click', () => this.addWarrior());
|
| 340 |
+
}
|
| 341 |
+
if (this.uiElements.dispatchWarriors) {
|
| 342 |
+
this.uiElements.dispatchWarriors.addEventListener('click', () => this.dispatchWarriors());
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// LLM event listeners
|
| 346 |
+
if (this.uiElements.llmModel) {
|
| 347 |
+
this.uiElements.llmModel.addEventListener('change', (e) => this.handleLLMModelChange(e.target.value));
|
| 348 |
+
}
|
| 349 |
+
if (this.uiElements.llmQuery) {
|
| 350 |
+
this.uiElements.llmQuery.addEventListener('keypress', (e) => {
|
| 351 |
+
if (e.key === 'Enter') {
|
| 352 |
+
this.handleLLMQuerySubmit();
|
| 353 |
+
}
|
| 354 |
+
});
|
| 355 |
+
}
|
| 356 |
+
if (this.uiElements.llmSubmit) {
|
| 357 |
+
this.uiElements.llmSubmit.addEventListener('click', () => this.handleLLMQuerySubmit());
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// Test LLM connection
|
| 361 |
+
this.testLLMConnection();
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
/**
|
| 365 |
+
* Update the LLM status indicator
|
| 366 |
+
* @param {boolean} connected - Whether the LLM is connected
|
| 367 |
+
*/
|
| 368 |
+
updateLLMStatusIndicator(connected) {
|
| 369 |
+
this.llmConnected = connected;
|
| 370 |
+
const indicator = document.getElementById('llm-status-indicator');
|
| 371 |
+
if (indicator) {
|
| 372 |
+
indicator.style.backgroundColor = connected ? 'green' : 'red';
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
/**
|
| 377 |
+
* Test the LLM connection with an initial prompt
|
| 378 |
+
*/
|
| 379 |
+
async testLLMConnection() {
|
| 380 |
+
// Log token status for debugging
|
| 381 |
+
console.log('Testing LLM connection, token set:', this.llmHandler.isApiTokenSet());
|
| 382 |
+
if (this.llmHandler.isApiTokenSet()) {
|
| 383 |
+
console.log('Token length:', this.llmHandler.getApiToken().length);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// Check if API token is set
|
| 387 |
+
if (!this.llmHandler.isApiTokenSet()) {
|
| 388 |
+
// Update status indicator to red (disconnected)
|
| 389 |
+
this.updateLLMStatusIndicator(false);
|
| 390 |
+
|
| 391 |
+
// Update UI with error message
|
| 392 |
+
if (this.uiElements.llmResponse) {
|
| 393 |
+
this.uiElements.llmResponse.textContent = "Hugging Face API token not set. Please set the HF_TOKEN environment variable or use the browser console to set the token.";
|
| 394 |
+
}
|
| 395 |
+
return;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
// Update UI to show testing state
|
| 399 |
+
if (this.uiElements.llmResponse) {
|
| 400 |
+
this.uiElements.llmResponse.textContent = 'Testing LLM connection...';
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
try {
|
| 404 |
+
// Send an initial prompt to test the connection
|
| 405 |
+
const initialPrompt = "Provide a brief summary of a medieval village simulation with AI-controlled villagers. Include information about villager behaviors, resource management, and building types.";
|
| 406 |
+
const response = await this.llmHandler.sendQuery(initialPrompt);
|
| 407 |
+
|
| 408 |
+
// Update UI with response
|
| 409 |
+
if (this.uiElements.llmResponse) {
|
| 410 |
+
this.uiElements.llmResponse.textContent = response;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
// Update status indicator to green (connected)
|
| 414 |
+
this.updateLLMStatusIndicator(true);
|
| 415 |
+
} catch (error) {
|
| 416 |
+
console.error('Error testing LLM connection:', error);
|
| 417 |
+
|
| 418 |
+
// Update status indicator to red (disconnected)
|
| 419 |
+
this.updateLLMStatusIndicator(false);
|
| 420 |
+
|
| 421 |
+
// Update UI with error message
|
| 422 |
+
if (this.uiElements.llmResponse) {
|
| 423 |
+
if (error.message.includes("API token is not set")) {
|
| 424 |
+
this.uiElements.llmResponse.textContent = "Please set your Hugging Face API token by setting the HF_TOKEN environment variable or using the browser console.";
|
| 425 |
+
} else {
|
| 426 |
+
this.uiElements.llmResponse.textContent = 'Error connecting to LLM: ' + error.message;
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
/**
|
| 433 |
+
* Handle LLM model change
|
| 434 |
+
* @param {string} model - The selected model
|
| 435 |
+
*/
|
| 436 |
+
handleLLMModelChange(model) {
|
| 437 |
+
console.log('LLM model changed to:', model);
|
| 438 |
+
this.llmHandler.setSelectedModel(model);
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
/**
|
| 442 |
+
* Handle LLM query submission
|
| 443 |
+
*/
|
| 444 |
+
async handleLLMQuerySubmit() {
|
| 445 |
+
if (!this.uiElements.llmQuery || !this.uiElements.llmResponse) return;
|
| 446 |
+
|
| 447 |
+
const query = this.uiElements.llmQuery.value.trim();
|
| 448 |
+
if (!query) return;
|
| 449 |
+
|
| 450 |
+
console.log('Submitting LLM query:', query);
|
| 451 |
+
console.log('LLM Handler token set:', this.llmHandler.isApiTokenSet());
|
| 452 |
+
if (this.llmHandler.isApiTokenSet()) {
|
| 453 |
+
console.log('Token length:', this.llmHandler.getApiToken().length);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// Update UI to show loading state
|
| 457 |
+
this.uiElements.llmResponse.textContent = 'Processing your query...';
|
| 458 |
+
this.uiElements.llmSubmit.disabled = true;
|
| 459 |
+
|
| 460 |
+
try {
|
| 461 |
+
// Send query to LLM handler
|
| 462 |
+
const response = await this.llmHandler.sendQuery(query);
|
| 463 |
+
|
| 464 |
+
// Update UI with response
|
| 465 |
+
this.uiElements.llmResponse.textContent = response;
|
| 466 |
+
|
| 467 |
+
// Update status indicator to green (connected)
|
| 468 |
+
this.updateLLMStatusIndicator(true);
|
| 469 |
+
} catch (error) {
|
| 470 |
+
console.error('Error processing LLM query:', error);
|
| 471 |
+
if (error.message.includes("API token is not set")) {
|
| 472 |
+
this.uiElements.llmResponse.textContent = "Please set your Hugging Face API token in the HTML file to enable LLM functionality. Get one from https://huggingface.co/settings/tokens";
|
| 473 |
+
// Update status indicator to red (disconnected)
|
| 474 |
+
this.updateLLMStatusIndicator(false);
|
| 475 |
+
} else {
|
| 476 |
+
this.uiElements.llmResponse.textContent = 'Error: ' + error.message;
|
| 477 |
+
// Update status indicator to red (disconnected) if it's a connection error
|
| 478 |
+
if (error.message.includes("API request failed")) {
|
| 479 |
+
this.updateLLMStatusIndicator(false);
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
} finally {
|
| 483 |
+
// Re-enable submit button
|
| 484 |
+
this.uiElements.llmSubmit.disabled = false;
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
createEnvironment() {
|
| 489 |
+
// Buildings are already created in the AI system
|
| 490 |
+
this.createBuildingMeshes();
|
| 491 |
+
this.createResourceMeshes();
|
| 492 |
+
this.createRoads();
|
| 493 |
+
this.createTrees();
|
| 494 |
+
console.log('Environment created with roads and trees');
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
createBuildingMeshes() {
|
| 498 |
+
if (this.aiSystem && this.aiSystem.environmentSystem) {
|
| 499 |
+
for (const [id, building] of this.aiSystem.environmentSystem.buildings) {
|
| 500 |
+
let mesh = null;
|
| 501 |
+
|
| 502 |
+
// Create unique geometry and materials for each building type
|
| 503 |
+
switch (building.type) {
|
| 504 |
+
case 'house':
|
| 505 |
+
// Cozy cottage with sloped roof
|
| 506 |
+
const houseGroup = new THREE.Group();
|
| 507 |
+
|
| 508 |
+
const houseBase = new THREE.Mesh(
|
| 509 |
+
new THREE.BoxGeometry(3, 2, 3),
|
| 510 |
+
new THREE.MeshLambertMaterial({ color: 0xD2691E })
|
| 511 |
+
);
|
| 512 |
+
houseBase.position.y = 1;
|
| 513 |
+
|
| 514 |
+
const houseRoof = new THREE.Mesh(
|
| 515 |
+
new THREE.ConeGeometry(2.5, 1.5, 4),
|
| 516 |
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
| 517 |
+
);
|
| 518 |
+
houseRoof.position.y = 2.75;
|
| 519 |
+
|
| 520 |
+
houseGroup.add(houseBase);
|
| 521 |
+
houseGroup.add(houseRoof);
|
| 522 |
+
mesh = houseGroup;
|
| 523 |
+
mesh.position.y = 0;
|
| 524 |
+
break;
|
| 525 |
+
|
| 526 |
+
case 'workshop':
|
| 527 |
+
// Industrial workshop with chimney
|
| 528 |
+
const workshopGroup = new THREE.Group();
|
| 529 |
+
|
| 530 |
+
const workshopBase = new THREE.Mesh(
|
| 531 |
+
new THREE.BoxGeometry(4, 3, 4),
|
| 532 |
+
new THREE.MeshLambertMaterial({ color: 0x708090 })
|
| 533 |
+
);
|
| 534 |
+
workshopBase.position.y = 1.5;
|
| 535 |
+
|
| 536 |
+
const chimney = new THREE.Mesh(
|
| 537 |
+
new THREE.CylinderGeometry(0.3, 0.3, 2),
|
| 538 |
+
new THREE.MeshLambertMaterial({ color: 0x696969 })
|
| 539 |
+
);
|
| 540 |
+
chimney.position.set(1.5, 3, 0);
|
| 541 |
+
|
| 542 |
+
workshopGroup.add(workshopBase);
|
| 543 |
+
workshopGroup.add(chimney);
|
| 544 |
+
mesh = workshopGroup;
|
| 545 |
+
mesh.position.y = 0;
|
| 546 |
+
break;
|
| 547 |
+
|
| 548 |
+
case 'market':
|
| 549 |
+
// Large marketplace with dome
|
| 550 |
+
const marketGroup = new THREE.Group();
|
| 551 |
+
|
| 552 |
+
const marketBase = new THREE.Mesh(
|
| 553 |
+
new THREE.CylinderGeometry(5, 5, 2, 32),
|
| 554 |
+
new THREE.MeshLambertMaterial({ color: 0xFFD700 })
|
| 555 |
+
);
|
| 556 |
+
marketBase.position.y = 1;
|
| 557 |
+
|
| 558 |
+
const marketDome = new THREE.Mesh(
|
| 559 |
+
new THREE.SphereGeometry(3, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2),
|
| 560 |
+
new THREE.MeshLambertMaterial({ color: 0xFFA500 })
|
| 561 |
+
);
|
| 562 |
+
marketDome.position.y = 3.5;
|
| 563 |
+
|
| 564 |
+
marketGroup.add(marketBase);
|
| 565 |
+
marketGroup.add(marketDome);
|
| 566 |
+
mesh = marketGroup;
|
| 567 |
+
mesh.position.y = 0;
|
| 568 |
+
break;
|
| 569 |
+
|
| 570 |
+
case 'university':
|
| 571 |
+
// Academic building with tower and columns
|
| 572 |
+
const universityGroup = new THREE.Group();
|
| 573 |
+
|
| 574 |
+
const uniBase = new THREE.Mesh(
|
| 575 |
+
new THREE.BoxGeometry(6, 4, 5),
|
| 576 |
+
new THREE.MeshLambertMaterial({ color: 0x4169E1 })
|
| 577 |
+
);
|
| 578 |
+
uniBase.position.y = 2;
|
| 579 |
+
|
| 580 |
+
const uniTower = new THREE.Mesh(
|
| 581 |
+
new THREE.CylinderGeometry(1.5, 1.5, 6),
|
| 582 |
+
new THREE.MeshLambertMaterial({ color: 0x1E90FF })
|
| 583 |
+
);
|
| 584 |
+
uniTower.position.set(2, 5, 0);
|
| 585 |
+
|
| 586 |
+
// Add columns
|
| 587 |
+
for (let i = -2; i <= 2; i += 2) {
|
| 588 |
+
const column = new THREE.Mesh(
|
| 589 |
+
new THREE.CylinderGeometry(0.3, 0.3, 3),
|
| 590 |
+
new THREE.MeshLambertMaterial({ color: 0xF5F5F5 })
|
| 591 |
+
);
|
| 592 |
+
column.position.set(i, 1.5, 2);
|
| 593 |
+
universityGroup.add(column);
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
universityGroup.add(uniBase);
|
| 597 |
+
universityGroup.add(uniTower);
|
| 598 |
+
mesh = universityGroup;
|
| 599 |
+
mesh.position.y = 0;
|
| 600 |
+
break;
|
| 601 |
+
|
| 602 |
+
case 'store':
|
| 603 |
+
// Modern store with large windows
|
| 604 |
+
const storeGroup = new THREE.Group();
|
| 605 |
+
|
| 606 |
+
const storeBase = new THREE.Mesh(
|
| 607 |
+
new THREE.BoxGeometry(5, 3, 4),
|
| 608 |
+
new THREE.MeshLambertMaterial({ color: 0x32CD32 })
|
| 609 |
+
);
|
| 610 |
+
storeBase.position.y = 1.5;
|
| 611 |
+
|
| 612 |
+
// Add windows
|
| 613 |
+
const windowMaterial = new THREE.MeshLambertMaterial({ color: 0x87CEEB });
|
| 614 |
+
for (let i = -1.5; i <= 1.5; i += 1.5) {
|
| 615 |
+
const window = new THREE.Mesh(
|
| 616 |
+
new THREE.PlaneGeometry(1, 1),
|
| 617 |
+
windowMaterial
|
| 618 |
+
);
|
| 619 |
+
window.position.set(i, 1.5, 2.01);
|
| 620 |
+
storeGroup.add(window);
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
storeGroup.add(storeBase);
|
| 624 |
+
mesh = storeGroup;
|
| 625 |
+
mesh.position.y = 0;
|
| 626 |
+
break;
|
| 627 |
+
|
| 628 |
+
case 'bank':
|
| 629 |
+
// Impressive bank building
|
| 630 |
+
const bankGroup = new THREE.Group();
|
| 631 |
+
|
| 632 |
+
const bankBase = new THREE.Mesh(
|
| 633 |
+
new THREE.BoxGeometry(6, 5, 5),
|
| 634 |
+
new THREE.MeshLambertMaterial({ color: 0xC0C0C0 })
|
| 635 |
+
);
|
| 636 |
+
bankBase.position.y = 2.5;
|
| 637 |
+
|
| 638 |
+
// Add pillars
|
| 639 |
+
for (let i = -2; i <= 2; i += 2) {
|
| 640 |
+
const pillar = new THREE.Mesh(
|
| 641 |
+
new THREE.CylinderGeometry(0.4, 0.4, 4),
|
| 642 |
+
new THREE.MeshLambertMaterial({ color: 0xF5F5F5 })
|
| 643 |
+
);
|
| 644 |
+
pillar.position.set(i, 2, 2.5);
|
| 645 |
+
bankGroup.add(pillar);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
bankGroup.add(bankBase);
|
| 649 |
+
mesh = bankGroup;
|
| 650 |
+
mesh.position.y = 0;
|
| 651 |
+
break;
|
| 652 |
+
|
| 653 |
+
case 'hospital':
|
| 654 |
+
// Medical facility with cross
|
| 655 |
+
const hospitalGroup = new THREE.Group();
|
| 656 |
+
|
| 657 |
+
const hospitalBase = new THREE.Mesh(
|
| 658 |
+
new THREE.BoxGeometry(7, 5, 5),
|
| 659 |
+
new THREE.MeshLambertMaterial({ color: 0xFF0000 })
|
| 660 |
+
);
|
| 661 |
+
hospitalBase.position.y = 2.5;
|
| 662 |
+
|
| 663 |
+
// Medical cross
|
| 664 |
+
const crossVertical = new THREE.Mesh(
|
| 665 |
+
new THREE.BoxGeometry(0.3, 2, 0.3),
|
| 666 |
+
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
|
| 667 |
+
);
|
| 668 |
+
crossVertical.position.set(0, 4.5, 2.5);
|
| 669 |
+
|
| 670 |
+
const crossHorizontal = new THREE.Mesh(
|
| 671 |
+
new THREE.BoxGeometry(1.5, 0.3, 0.3),
|
| 672 |
+
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
|
| 673 |
+
);
|
| 674 |
+
crossHorizontal.position.set(0, 4.5, 2.5);
|
| 675 |
+
|
| 676 |
+
hospitalGroup.add(hospitalBase);
|
| 677 |
+
hospitalGroup.add(crossVertical);
|
| 678 |
+
hospitalGroup.add(crossHorizontal);
|
| 679 |
+
mesh = hospitalGroup;
|
| 680 |
+
mesh.position.y = 0;
|
| 681 |
+
break;
|
| 682 |
+
|
| 683 |
+
case 'restaurant':
|
| 684 |
+
// Fancy restaurant with unique shape
|
| 685 |
+
const restaurantGroup = new THREE.Group();
|
| 686 |
+
|
| 687 |
+
const restaurantBase = new THREE.Mesh(
|
| 688 |
+
new THREE.CylinderGeometry(4, 4, 3, 32),
|
| 689 |
+
new THREE.MeshLambertMaterial({ color: 0xFF6347 })
|
| 690 |
+
);
|
| 691 |
+
restaurantBase.position.y = 1.5;
|
| 692 |
+
|
| 693 |
+
const restaurantRoof = new THREE.Mesh(
|
| 694 |
+
new THREE.ConeGeometry(3.5, 2, 8),
|
| 695 |
+
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
|
| 696 |
+
);
|
| 697 |
+
restaurantRoof.position.y = 3.5;
|
| 698 |
+
|
| 699 |
+
restaurantGroup.add(restaurantBase);
|
| 700 |
+
restaurantGroup.add(restaurantRoof);
|
| 701 |
+
mesh = restaurantGroup;
|
| 702 |
+
mesh.position.y = 0;
|
| 703 |
+
break;
|
| 704 |
+
|
| 705 |
+
default:
|
| 706 |
+
// Default building
|
| 707 |
+
mesh = new THREE.Mesh(
|
| 708 |
+
new THREE.BoxGeometry(3, 3, 3),
|
| 709 |
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
| 710 |
+
);
|
| 711 |
+
mesh.position.y = 1.5;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
mesh.position.set(building.position[0], building.position[1], building.position[2]);
|
| 715 |
+
mesh.castShadow = true;
|
| 716 |
+
mesh.receiveShadow = true;
|
| 717 |
+
|
| 718 |
+
this.buildingMeshes.set(id, mesh);
|
| 719 |
+
this.scene.add(mesh);
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
this.updateBuildingCount();
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
createResourceMeshes() {
|
| 727 |
+
const resourceGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2);
|
| 728 |
+
const materials = {
|
| 729 |
+
wood: new THREE.MeshLambertMaterial({ color: 0x8B4513 }),
|
| 730 |
+
stone: new THREE.MeshLambertMaterial({ color: 0x708090 }),
|
| 731 |
+
food: new THREE.MeshLambertMaterial({ color: 0x32CD32 })
|
| 732 |
+
};
|
| 733 |
+
|
| 734 |
+
if (this.aiSystem && this.aiSystem.environmentSystem) {
|
| 735 |
+
for (const [id, resource] of this.aiSystem.environmentSystem.resources) {
|
| 736 |
+
const material = materials[resource.type] || materials.wood;
|
| 737 |
+
const mesh = new THREE.Mesh(resourceGeometry, material);
|
| 738 |
+
mesh.position.set(resource.position[0], resource.position[1], resource.position[2]);
|
| 739 |
+
mesh.position.y = 1;
|
| 740 |
+
mesh.castShadow = true;
|
| 741 |
+
mesh.receiveShadow = true;
|
| 742 |
+
|
| 743 |
+
this.resourceMeshes.set(id, mesh);
|
| 744 |
+
this.scene.add(mesh);
|
| 745 |
+
}
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
this.updateResourceCount();
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
/**
|
| 752 |
+
* Create roads for the village
|
| 753 |
+
*/
|
| 754 |
+
createRoads() {
|
| 755 |
+
console.log('Creating roads...');
|
| 756 |
+
|
| 757 |
+
// Create a simple crossroad in the center - very visible
|
| 758 |
+
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
|
| 759 |
+
|
| 760 |
+
// Horizontal road - very wide and visible
|
| 761 |
+
const horizontalRoad = new THREE.Mesh(
|
| 762 |
+
new THREE.BoxGeometry(50, 0.5, 6),
|
| 763 |
+
roadMaterial
|
| 764 |
+
);
|
| 765 |
+
horizontalRoad.position.set(0, 0.25, 0);
|
| 766 |
+
horizontalRoad.receiveShadow = true;
|
| 767 |
+
this.scene.add(horizontalRoad);
|
| 768 |
+
console.log('Added horizontal road at center');
|
| 769 |
+
|
| 770 |
+
// Vertical road - very wide and visible
|
| 771 |
+
const verticalRoad = new THREE.Mesh(
|
| 772 |
+
new THREE.BoxGeometry(6, 0.5, 50),
|
| 773 |
+
roadMaterial
|
| 774 |
+
);
|
| 775 |
+
verticalRoad.position.set(0, 0.25, 0);
|
| 776 |
+
verticalRoad.receiveShadow = true;
|
| 777 |
+
this.scene.add(verticalRoad);
|
| 778 |
+
console.log('Added vertical road at center');
|
| 779 |
+
|
| 780 |
+
// Add bright yellow road markings
|
| 781 |
+
const markingMaterial = new THREE.MeshLambertMaterial({ color: 0xFFFF00 });
|
| 782 |
+
|
| 783 |
+
// Horizontal road markings
|
| 784 |
+
for (let x = -20; x <= 20; x += 4) {
|
| 785 |
+
if (Math.abs(x) > 2) { // Skip center
|
| 786 |
+
const marking = new THREE.Mesh(
|
| 787 |
+
new THREE.BoxGeometry(2, 0.3, 0.5),
|
| 788 |
+
markingMaterial
|
| 789 |
+
);
|
| 790 |
+
marking.position.set(x, 0.4, 0);
|
| 791 |
+
this.scene.add(marking);
|
| 792 |
+
}
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
// Vertical road markings
|
| 796 |
+
for (let z = -20; z <= 20; z += 4) {
|
| 797 |
+
if (Math.abs(z) > 2) { // Skip center
|
| 798 |
+
const marking = new THREE.Mesh(
|
| 799 |
+
new THREE.BoxGeometry(0.5, 0.3, 2),
|
| 800 |
+
markingMaterial
|
| 801 |
+
);
|
| 802 |
+
marking.position.set(0, 0.4, z);
|
| 803 |
+
this.scene.add(marking);
|
| 804 |
+
}
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
console.log('Roads creation completed');
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
/**
|
| 811 |
+
* Create trees for the village
|
| 812 |
+
*/
|
| 813 |
+
createTrees() {
|
| 814 |
+
console.log('Creating trees...');
|
| 815 |
+
|
| 816 |
+
// Create very visible trees at key positions
|
| 817 |
+
const treePositions = [
|
| 818 |
+
[-15, 0, -15], [15, 0, -15], [-15, 0, 15], [15, 0, 15],
|
| 819 |
+
[-25, 0, 0], [25, 0, 0], [0, 0, -25], [0, 0, 25]
|
| 820 |
+
];
|
| 821 |
+
|
| 822 |
+
treePositions.forEach((pos, index) => {
|
| 823 |
+
// Create a very visible tree
|
| 824 |
+
const treeGroup = new THREE.Group();
|
| 825 |
+
|
| 826 |
+
// Large trunk
|
| 827 |
+
const trunk = new THREE.Mesh(
|
| 828 |
+
new THREE.CylinderGeometry(0.8, 1, 6, 8),
|
| 829 |
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
| 830 |
+
);
|
| 831 |
+
trunk.position.y = 3;
|
| 832 |
+
trunk.castShadow = true;
|
| 833 |
+
trunk.receiveShadow = true;
|
| 834 |
+
treeGroup.add(trunk);
|
| 835 |
+
|
| 836 |
+
// Large foliage - multiple layers for visibility
|
| 837 |
+
const foliage1 = new THREE.Mesh(
|
| 838 |
+
new THREE.SphereGeometry(5, 8, 6),
|
| 839 |
+
new THREE.MeshLambertMaterial({ color: 0x228B22 })
|
| 840 |
+
);
|
| 841 |
+
foliage1.position.y = 7;
|
| 842 |
+
foliage1.castShadow = true;
|
| 843 |
+
foliage1.receiveShadow = true;
|
| 844 |
+
treeGroup.add(foliage1);
|
| 845 |
+
|
| 846 |
+
const foliage2 = new THREE.Mesh(
|
| 847 |
+
new THREE.SphereGeometry(3, 8, 6),
|
| 848 |
+
new THREE.MeshLambertMaterial({ color: 0x32CD32 })
|
| 849 |
+
);
|
| 850 |
+
foliage2.position.y = 10;
|
| 851 |
+
foliage2.castShadow = true;
|
| 852 |
+
foliage2.receiveShadow = true;
|
| 853 |
+
treeGroup.add(foliage2);
|
| 854 |
+
|
| 855 |
+
treeGroup.position.set(pos[0], 0, pos[2]);
|
| 856 |
+
this.scene.add(treeGroup);
|
| 857 |
+
console.log(`Added tree ${index + 1} at:`, pos);
|
| 858 |
+
});
|
| 859 |
+
|
| 860 |
+
console.log('Trees creation completed');
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
createInitialVillagers() {
|
| 864 |
+
const positions = [
|
| 865 |
+
[0, 0, 0],
|
| 866 |
+
[5, 0, 5],
|
| 867 |
+
[-3, 0, -3]
|
| 868 |
+
];
|
| 869 |
+
|
| 870 |
+
positions.forEach((position, index) => {
|
| 871 |
+
this.createVillager(`villager${index + 1}`, position);
|
| 872 |
+
});
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
createVillager(id, position) {
|
| 876 |
+
if (this.aiSystem) {
|
| 877 |
+
const villager = this.aiSystem.createVillager(id, position);
|
| 878 |
+
this.createVillagerMesh(villager);
|
| 879 |
+
this.updateVillagerCount();
|
| 880 |
+
return villager;
|
| 881 |
+
}
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
createVillagerMesh(villager) {
|
| 885 |
+
// Create a more detailed villager with a body and head
|
| 886 |
+
const villagerGroup = new THREE.Group();
|
| 887 |
+
|
| 888 |
+
// Body
|
| 889 |
+
const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 0.8, 8);
|
| 890 |
+
const bodyMaterial = new THREE.MeshLambertMaterial({
|
| 891 |
+
color: this.getStateColor(villager.state)
|
| 892 |
+
});
|
| 893 |
+
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
| 894 |
+
body.position.y = 0.4;
|
| 895 |
+
body.castShadow = true;
|
| 896 |
+
body.receiveShadow = true;
|
| 897 |
+
villagerGroup.add(body);
|
| 898 |
+
|
| 899 |
+
// Head
|
| 900 |
+
const headGeometry = new THREE.SphereGeometry(0.25, 16, 16);
|
| 901 |
+
const headMaterial = new THREE.MeshLambertMaterial({
|
| 902 |
+
color: 0xffd700 // Gold color for head
|
| 903 |
+
});
|
| 904 |
+
const head = new THREE.Mesh(headGeometry, headMaterial);
|
| 905 |
+
head.position.y = 0.9;
|
| 906 |
+
head.castShadow = true;
|
| 907 |
+
head.receiveShadow = true;
|
| 908 |
+
villagerGroup.add(head);
|
| 909 |
+
|
| 910 |
+
// Create villager label (sprite)
|
| 911 |
+
const canvas = document.createElement('canvas');
|
| 912 |
+
const context = canvas.getContext('2d');
|
| 913 |
+
canvas.width = 256;
|
| 914 |
+
canvas.height = 128;
|
| 915 |
+
|
| 916 |
+
context.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
| 917 |
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
| 918 |
+
|
| 919 |
+
context.fillStyle = 'white';
|
| 920 |
+
context.font = '32px Arial';
|
| 921 |
+
context.textAlign = 'center';
|
| 922 |
+
context.textBaseline = 'middle';
|
| 923 |
+
context.fillText(villager.id, canvas.width / 2, canvas.height / 2);
|
| 924 |
+
|
| 925 |
+
const texture = new THREE.CanvasTexture(canvas);
|
| 926 |
+
const spriteMaterial = new THREE.SpriteMaterial({
|
| 927 |
+
map: texture,
|
| 928 |
+
transparent: true
|
| 929 |
+
});
|
| 930 |
+
const label = new THREE.Sprite(spriteMaterial);
|
| 931 |
+
label.scale.set(3, 1.5, 1);
|
| 932 |
+
label.position.y = 1.5;
|
| 933 |
+
label.visible = this.showTitles; // Set initial visibility based on showTitles flag
|
| 934 |
+
villagerGroup.add(label);
|
| 935 |
+
|
| 936 |
+
villagerGroup.position.set(villager.position[0], villager.position[1], villager.position[2]);
|
| 937 |
+
villagerGroup.position.y = 0;
|
| 938 |
+
villagerGroup.castShadow = true;
|
| 939 |
+
villagerGroup.receiveShadow = true;
|
| 940 |
+
|
| 941 |
+
villagerGroup.userData.villager = villager;
|
| 942 |
+
|
| 943 |
+
this.villagerMeshes.set(villager.id, villagerGroup);
|
| 944 |
+
this.scene.add(villagerGroup);
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
getStateColor(state) {
|
| 948 |
+
const colors = {
|
| 949 |
+
sleep: 0x7f8c8d,
|
| 950 |
+
work: 0xe74c3c,
|
| 951 |
+
eat: 0xf39c12,
|
| 952 |
+
socialize: 0x9b59b6,
|
| 953 |
+
idle: 0x95a5a6
|
| 954 |
+
};
|
| 955 |
+
return colors[state] || colors.idle;
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
addVillager() {
|
| 959 |
+
const villagerCount = this.villagerMeshes.size;
|
| 960 |
+
const id = `villager${villagerCount + 1}`;
|
| 961 |
+
|
| 962 |
+
const position = [
|
| 963 |
+
(Math.random() - 0.5) * 40,
|
| 964 |
+
0,
|
| 965 |
+
(Math.random() - 0.5) * 40
|
| 966 |
+
];
|
| 967 |
+
|
| 968 |
+
this.createVillager(id, position);
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
resetSimulation() {
|
| 972 |
+
// Clear all villagers
|
| 973 |
+
for (const [id, mesh] of this.villagerMeshes) {
|
| 974 |
+
this.scene.remove(mesh);
|
| 975 |
+
}
|
| 976 |
+
this.villagerMeshes.clear();
|
| 977 |
+
|
| 978 |
+
// Clear path lines
|
| 979 |
+
for (const [id, line] of this.pathLines) {
|
| 980 |
+
this.scene.remove(line);
|
| 981 |
+
}
|
| 982 |
+
this.pathLines.clear();
|
| 983 |
+
|
| 984 |
+
// Reset AI system
|
| 985 |
+
this.aiSystem = new VillageAISystem(this.scene);
|
| 986 |
+
this.createEnvironment();
|
| 987 |
+
|
| 988 |
+
// Clear selection
|
| 989 |
+
this.selectedVillager = null;
|
| 990 |
+
this.updateVillagerInfo();
|
| 991 |
+
|
| 992 |
+
this.updateVillagerCount();
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
updateTimeSpeed(speed) {
|
| 996 |
+
this.timeSpeed = parseFloat(speed);
|
| 997 |
+
if (this.uiElements.timeSpeedDisplay) {
|
| 998 |
+
this.uiElements.timeSpeedDisplay.textContent = `${speed}x`;
|
| 999 |
+
}
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
togglePaths(show) {
|
| 1003 |
+
console.log('Toggling paths:', show);
|
| 1004 |
+
this.showPaths = show;
|
| 1005 |
+
for (const [id, line] of this.pathLines) {
|
| 1006 |
+
line.visible = show;
|
| 1007 |
+
}
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
toggleTitles(show) {
|
| 1011 |
+
console.log('Toggling titles:', show);
|
| 1012 |
+
this.showTitles = show;
|
| 1013 |
+
for (const [id, mesh] of this.villagerMeshes) {
|
| 1014 |
+
// Find the text sprite (label) in the mesh children
|
| 1015 |
+
// The label is the third child (index 2) - body, head, label
|
| 1016 |
+
if (mesh.children.length >= 3) {
|
| 1017 |
+
const label = mesh.children[2]; // Label is the third child
|
| 1018 |
+
if (label instanceof THREE.Sprite) {
|
| 1019 |
+
label.visible = show;
|
| 1020 |
+
}
|
| 1021 |
+
}
|
| 1022 |
+
}
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
updateFog(intensity) {
|
| 1026 |
+
console.log('Updating fog intensity:', intensity);
|
| 1027 |
+
this.weatherSystem.fogIntensity = intensity;
|
| 1028 |
+
|
| 1029 |
+
// Update fog in the scene
|
| 1030 |
+
if (this.scene.fog) {
|
| 1031 |
+
// Convert intensity (0-100) to fog density
|
| 1032 |
+
const near = 10 + (100 - intensity); // More intensity = less near distance
|
| 1033 |
+
const far = 30 + (100 - intensity) * 2; // More intensity = less far distance
|
| 1034 |
+
this.scene.fog.near = near;
|
| 1035 |
+
this.scene.fog.far = far;
|
| 1036 |
+
}
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
setWeather(weatherType) {
|
| 1040 |
+
console.log('Setting weather to:', weatherType);
|
| 1041 |
+
this.weatherSystem.currentWeather = weatherType;
|
| 1042 |
+
|
| 1043 |
+
// Update scene based on weather
|
| 1044 |
+
switch (weatherType) {
|
| 1045 |
+
case 'sun':
|
| 1046 |
+
this.scene.background = new THREE.Color(0x87CEEB); // Sky blue
|
| 1047 |
+
if (this.scene.fog) {
|
| 1048 |
+
this.scene.fog.color = new THREE.Color(0x87CEEB);
|
| 1049 |
+
}
|
| 1050 |
+
break;
|
| 1051 |
+
case 'rain':
|
| 1052 |
+
this.scene.background = new THREE.Color(0x778899); // Gray
|
| 1053 |
+
if (this.scene.fog) {
|
| 1054 |
+
this.scene.fog.color = new THREE.Color(0x778899);
|
| 1055 |
+
}
|
| 1056 |
+
this.createRainEffect();
|
| 1057 |
+
break;
|
| 1058 |
+
case 'snow':
|
| 1059 |
+
this.scene.background = new THREE.Color(0xE0E6EF); // Light gray
|
| 1060 |
+
if (this.scene.fog) {
|
| 1061 |
+
this.scene.fog.color = new THREE.Color(0xE0E6EF);
|
| 1062 |
+
}
|
| 1063 |
+
this.createSnowEffect();
|
| 1064 |
+
break;
|
| 1065 |
+
}
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
createRainEffect() {
|
| 1069 |
+
// Clear existing rain particles
|
| 1070 |
+
this.weatherSystem.rainParticles.forEach(particle => {
|
| 1071 |
+
this.scene.remove(particle);
|
| 1072 |
+
});
|
| 1073 |
+
this.weatherSystem.rainParticles = [];
|
| 1074 |
+
|
| 1075 |
+
// Create new rain particles
|
| 1076 |
+
const rainCount = 1000;
|
| 1077 |
+
const rainGeometry = new THREE.BufferGeometry();
|
| 1078 |
+
const positions = new Float32Array(rainCount * 3);
|
| 1079 |
+
|
| 1080 |
+
for (let i = 0; i < rainCount * 3; i += 3) {
|
| 1081 |
+
positions[i] = (Math.random() - 0.5) * 100; // x
|
| 1082 |
+
positions[i + 1] = Math.random() * 50 + 10; // y
|
| 1083 |
+
positions[i + 2] = (Math.random() - 0.5) * 100; // z
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
rainGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
| 1087 |
+
|
| 1088 |
+
const rainMaterial = new THREE.PointsMaterial({
|
| 1089 |
+
color: 0xAAAAFF,
|
| 1090 |
+
size: 0.1,
|
| 1091 |
+
transparent: true
|
| 1092 |
+
});
|
| 1093 |
+
|
| 1094 |
+
const rainSystem = new THREE.Points(rainGeometry, rainMaterial);
|
| 1095 |
+
this.scene.add(rainSystem);
|
| 1096 |
+
this.weatherSystem.rainParticles.push(rainSystem);
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
createSnowEffect() {
|
| 1100 |
+
// Clear existing snow particles
|
| 1101 |
+
this.weatherSystem.snowParticles.forEach(particle => {
|
| 1102 |
+
this.scene.remove(particle);
|
| 1103 |
+
});
|
| 1104 |
+
this.weatherSystem.snowParticles = [];
|
| 1105 |
+
|
| 1106 |
+
// Create new snow particles
|
| 1107 |
+
const snowCount = 1000;
|
| 1108 |
+
const snowGeometry = new THREE.BufferGeometry();
|
| 1109 |
+
const positions = new Float32Array(snowCount * 3);
|
| 1110 |
+
|
| 1111 |
+
for (let i = 0; i < snowCount * 3; i += 3) {
|
| 1112 |
+
positions[i] = (Math.random() - 0.5) * 100; // x
|
| 1113 |
+
positions[i + 1] = Math.random() * 50 + 10; // y
|
| 1114 |
+
positions[i + 2] = (Math.random() - 0.5) * 100; // z
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
snowGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
| 1118 |
+
|
| 1119 |
+
const snowMaterial = new THREE.PointsMaterial({
|
| 1120 |
+
color: 0xFFFFFF,
|
| 1121 |
+
size: 0.2,
|
| 1122 |
+
transparent: true
|
| 1123 |
+
});
|
| 1124 |
+
|
| 1125 |
+
const snowSystem = new THREE.Points(snowGeometry, snowMaterial);
|
| 1126 |
+
this.scene.add(snowSystem);
|
| 1127 |
+
this.weatherSystem.snowParticles.push(snowSystem);
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
triggerDisaster(disasterType) {
|
| 1131 |
+
console.log('Triggering disaster:', disasterType);
|
| 1132 |
+
|
| 1133 |
+
// Create disaster effect
|
| 1134 |
+
switch (disasterType) {
|
| 1135 |
+
case 'fire':
|
| 1136 |
+
this.createFireEffect();
|
| 1137 |
+
break;
|
| 1138 |
+
case 'hurricane':
|
| 1139 |
+
this.createHurricaneEffect();
|
| 1140 |
+
break;
|
| 1141 |
+
case 'flood':
|
| 1142 |
+
this.createFloodEffect();
|
| 1143 |
+
break;
|
| 1144 |
+
case 'earthquake':
|
| 1145 |
+
this.createEarthquakeEffect();
|
| 1146 |
+
break;
|
| 1147 |
+
case 'plague':
|
| 1148 |
+
this.createPlagueEffect();
|
| 1149 |
+
break;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
// Add to active disasters
|
| 1153 |
+
this.disasterSystem.activeDisasters.set(disasterType, {
|
| 1154 |
+
startTime: Date.now(),
|
| 1155 |
+
intensity: 1.0
|
| 1156 |
+
});
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
createFireEffect() {
|
| 1160 |
+
// Create fire particles at random building locations
|
| 1161 |
+
if (this.buildingMeshes.size > 0) {
|
| 1162 |
+
const buildingArray = Array.from(this.buildingMeshes.values());
|
| 1163 |
+
const building = buildingArray[Math.floor(Math.random() * buildingArray.length)];
|
| 1164 |
+
|
| 1165 |
+
const fireGeometry = new THREE.SphereGeometry(1, 8, 8);
|
| 1166 |
+
const fireMaterial = new THREE.MeshBasicMaterial({
|
| 1167 |
+
color: 0xFF4500,
|
| 1168 |
+
transparent: true,
|
| 1169 |
+
opacity: 0.7
|
| 1170 |
+
});
|
| 1171 |
+
|
| 1172 |
+
const fireEffect = new THREE.Mesh(fireGeometry, fireMaterial);
|
| 1173 |
+
fireEffect.position.copy(building.position);
|
| 1174 |
+
fireEffect.position.y = 3;
|
| 1175 |
+
|
| 1176 |
+
this.scene.add(fireEffect);
|
| 1177 |
+
this.disasterSystem.fireEffects.push(fireEffect);
|
| 1178 |
+
|
| 1179 |
+
// Remove fire after some time
|
| 1180 |
+
setTimeout(() => {
|
| 1181 |
+
this.scene.remove(fireEffect);
|
| 1182 |
+
const index = this.disasterSystem.fireEffects.indexOf(fireEffect);
|
| 1183 |
+
if (index > -1) {
|
| 1184 |
+
this.disasterSystem.fireEffects.splice(index, 1);
|
| 1185 |
+
}
|
| 1186 |
+
}, 5000);
|
| 1187 |
+
}
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
createHurricaneEffect() {
|
| 1191 |
+
// Create a rotating wind effect in the center of the village
|
| 1192 |
+
const tornadoGeometry = new THREE.CylinderGeometry(0.5, 2, 20, 8);
|
| 1193 |
+
const tornadoMaterial = new THREE.MeshBasicMaterial({
|
| 1194 |
+
color: 0x888888,
|
| 1195 |
+
transparent: true,
|
| 1196 |
+
opacity: 0.5
|
| 1197 |
+
});
|
| 1198 |
+
|
| 1199 |
+
const tornado = new THREE.Mesh(tornadoGeometry, tornadoMaterial);
|
| 1200 |
+
tornado.position.set(0, 10, 0);
|
| 1201 |
+
|
| 1202 |
+
this.scene.add(tornado);
|
| 1203 |
+
|
| 1204 |
+
// Animate tornado
|
| 1205 |
+
let tornadoTime = 0;
|
| 1206 |
+
const animateTornado = () => {
|
| 1207 |
+
tornadoTime += 0.1;
|
| 1208 |
+
tornado.rotation.y = tornadoTime;
|
| 1209 |
+
tornado.position.x = Math.sin(tornadoTime) * 5;
|
| 1210 |
+
tornado.position.z = Math.cos(tornadoTime) * 5;
|
| 1211 |
+
|
| 1212 |
+
if (tornadoTime < 20) { // Run for 20 seconds
|
| 1213 |
+
requestAnimationFrame(animateTornado);
|
| 1214 |
+
} else {
|
| 1215 |
+
this.scene.remove(tornado);
|
| 1216 |
+
}
|
| 1217 |
+
};
|
| 1218 |
+
|
| 1219 |
+
animateTornado();
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
createFloodEffect() {
|
| 1223 |
+
// Create a water plane that rises
|
| 1224 |
+
const waterGeometry = new THREE.PlaneGeometry(100, 100);
|
| 1225 |
+
const waterMaterial = new THREE.MeshBasicMaterial({
|
| 1226 |
+
color: 0x4169E1,
|
| 1227 |
+
transparent: true,
|
| 1228 |
+
opacity: 0.6
|
| 1229 |
+
});
|
| 1230 |
+
|
| 1231 |
+
const water = new THREE.Mesh(waterGeometry, waterMaterial);
|
| 1232 |
+
water.rotation.x = -Math.PI / 2;
|
| 1233 |
+
water.position.y = 0.1;
|
| 1234 |
+
|
| 1235 |
+
this.scene.add(water);
|
| 1236 |
+
this.disasterSystem.floodEffects.push(water);
|
| 1237 |
+
|
| 1238 |
+
// Animate water rising
|
| 1239 |
+
let waterLevel = 0.1;
|
| 1240 |
+
const raiseWater = () => {
|
| 1241 |
+
waterLevel += 0.1;
|
| 1242 |
+
water.position.y = waterLevel;
|
| 1243 |
+
|
| 1244 |
+
if (waterLevel < 3) { // Raise to 3 units
|
| 1245 |
+
setTimeout(raiseWater, 200);
|
| 1246 |
+
} else {
|
| 1247 |
+
// Remove water after some time
|
| 1248 |
+
setTimeout(() => {
|
| 1249 |
+
this.scene.remove(water);
|
| 1250 |
+
const index = this.disasterSystem.floodEffects.indexOf(water);
|
| 1251 |
+
if (index > -1) {
|
| 1252 |
+
this.disasterSystem.floodEffects.splice(index, 1);
|
| 1253 |
+
}
|
| 1254 |
+
}, 3000);
|
| 1255 |
+
}
|
| 1256 |
+
};
|
| 1257 |
+
|
| 1258 |
+
raiseWater();
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
createEarthquakeEffect() {
|
| 1262 |
+
// Shake the camera
|
| 1263 |
+
const originalCameraPosition = this.camera.position.clone();
|
| 1264 |
+
let shakeIntensity = 0.5;
|
| 1265 |
+
let shakeTime = 0;
|
| 1266 |
+
|
| 1267 |
+
const shakeCamera = () => {
|
| 1268 |
+
shakeTime += 0.1;
|
| 1269 |
+
shakeIntensity *= 0.95; // Decrease intensity over time
|
| 1270 |
+
|
| 1271 |
+
this.camera.position.x = originalCameraPosition.x + (Math.random() - 0.5) * shakeIntensity;
|
| 1272 |
+
this.camera.position.y = originalCameraPosition.y + (Math.random() - 0.5) * shakeIntensity;
|
| 1273 |
+
this.camera.position.z = originalCameraPosition.z + (Math.random() - 0.5) * shakeIntensity;
|
| 1274 |
+
|
| 1275 |
+
if (shakeTime < 5) { // Shake for 5 seconds
|
| 1276 |
+
requestAnimationFrame(shakeCamera);
|
| 1277 |
+
} else {
|
| 1278 |
+
// Reset camera position
|
| 1279 |
+
this.camera.position.copy(originalCameraPosition);
|
| 1280 |
+
}
|
| 1281 |
+
};
|
| 1282 |
+
|
| 1283 |
+
shakeCamera();
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
createPlagueEffect() {
|
| 1287 |
+
// Change villager colors to show they're sick
|
| 1288 |
+
for (const [id, mesh] of this.villagerMeshes) {
|
| 1289 |
+
if (mesh.children.length > 0) {
|
| 1290 |
+
const body = mesh.children[0];
|
| 1291 |
+
if (body.material) {
|
| 1292 |
+
body.material.color.setHex(0x808080); // Gray color for sick villagers
|
| 1293 |
+
}
|
| 1294 |
+
}
|
| 1295 |
+
}
|
| 1296 |
+
|
| 1297 |
+
// Reset colors after some time
|
| 1298 |
+
setTimeout(() => {
|
| 1299 |
+
for (const [id, mesh] of this.villagerMeshes) {
|
| 1300 |
+
const villager = mesh.userData.villager;
|
| 1301 |
+
if (mesh.children.length > 0) {
|
| 1302 |
+
const body = mesh.children[0];
|
| 1303 |
+
if (body.material) {
|
| 1304 |
+
body.material.color.setHex(this.getStateColor(villager.state));
|
| 1305 |
+
}
|
| 1306 |
+
}
|
| 1307 |
+
}
|
| 1308 |
+
}, 10000);
|
| 1309 |
+
}
|
| 1310 |
+
|
| 1311 |
+
spawnAnimal(animalType) {
|
| 1312 |
+
console.log('Spawning animal:', animalType);
|
| 1313 |
+
|
| 1314 |
+
// Create animal at random position
|
| 1315 |
+
const position = [
|
| 1316 |
+
(Math.random() - 0.5) * 40,
|
| 1317 |
+
0,
|
| 1318 |
+
(Math.random() - 0.5) * 40
|
| 1319 |
+
];
|
| 1320 |
+
|
| 1321 |
+
this.createAnimal(animalType, position);
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
createAnimal(animalType, position) {
|
| 1325 |
+
let animalMesh = null;
|
| 1326 |
+
|
| 1327 |
+
switch (animalType) {
|
| 1328 |
+
case 'wolf':
|
| 1329 |
+
animalMesh = new THREE.Mesh(
|
| 1330 |
+
new THREE.BoxGeometry(1, 0.5, 0.5),
|
| 1331 |
+
new THREE.MeshLambertMaterial({ color: 0x696969 })
|
| 1332 |
+
);
|
| 1333 |
+
break;
|
| 1334 |
+
case 'bear':
|
| 1335 |
+
animalMesh = new THREE.Mesh(
|
| 1336 |
+
new THREE.BoxGeometry(1.5, 1, 1),
|
| 1337 |
+
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
|
| 1338 |
+
);
|
| 1339 |
+
break;
|
| 1340 |
+
case 'dragon':
|
| 1341 |
+
// Create a more complex dragon
|
| 1342 |
+
const dragonGroup = new THREE.Group();
|
| 1343 |
+
|
| 1344 |
+
// Body
|
| 1345 |
+
const body = new THREE.Mesh(
|
| 1346 |
+
new THREE.CylinderGeometry(0.5, 0.8, 2, 8),
|
| 1347 |
+
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
|
| 1348 |
+
);
|
| 1349 |
+
body.rotation.z = Math.PI / 2;
|
| 1350 |
+
dragonGroup.add(body);
|
| 1351 |
+
|
| 1352 |
+
// Head
|
| 1353 |
+
const head = new THREE.Mesh(
|
| 1354 |
+
new THREE.SphereGeometry(0.5, 8, 8),
|
| 1355 |
+
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
|
| 1356 |
+
);
|
| 1357 |
+
head.position.x = 1.2;
|
| 1358 |
+
dragonGroup.add(head);
|
| 1359 |
+
|
| 1360 |
+
// Wings
|
| 1361 |
+
const leftWing = new THREE.Mesh(
|
| 1362 |
+
new THREE.BoxGeometry(1.5, 0.1, 0.5),
|
| 1363 |
+
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
|
| 1364 |
+
);
|
| 1365 |
+
leftWing.position.set(-0.5, 0, 0.5);
|
| 1366 |
+
leftWing.rotation.z = Math.PI / 4;
|
| 1367 |
+
dragonGroup.add(leftWing);
|
| 1368 |
+
|
| 1369 |
+
const rightWing = new THREE.Mesh(
|
| 1370 |
+
new THREE.BoxGeometry(1.5, 0.1, 0.5),
|
| 1371 |
+
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
|
| 1372 |
+
);
|
| 1373 |
+
rightWing.position.set(-0.5, 0, -0.5);
|
| 1374 |
+
rightWing.rotation.z = -Math.PI / 4;
|
| 1375 |
+
dragonGroup.add(rightWing);
|
| 1376 |
+
|
| 1377 |
+
animalMesh = dragonGroup;
|
| 1378 |
+
break;
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
if (animalMesh) {
|
| 1382 |
+
animalMesh.position.set(position[0], position[1], position[2]);
|
| 1383 |
+
animalMesh.position.y = 0.5;
|
| 1384 |
+
animalMesh.castShadow = true;
|
| 1385 |
+
animalMesh.receiveShadow = true;
|
| 1386 |
+
|
| 1387 |
+
this.scene.add(animalMesh);
|
| 1388 |
+
this.animalSystem.animals.set(`animal_${Date.now()}`, {
|
| 1389 |
+
type: animalType,
|
| 1390 |
+
mesh: animalMesh,
|
| 1391 |
+
position: position,
|
| 1392 |
+
targetVillager: null
|
| 1393 |
+
});
|
| 1394 |
+
}
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
addWarrior() {
|
| 1398 |
+
console.log('Adding warrior');
|
| 1399 |
+
|
| 1400 |
+
// Create warrior at random position
|
| 1401 |
+
const position = [
|
| 1402 |
+
(Math.random() - 0.5) * 10,
|
| 1403 |
+
0,
|
| 1404 |
+
(Math.random() - 0.5) * 10
|
| 1405 |
+
];
|
| 1406 |
+
|
| 1407 |
+
this.createWarrior(position);
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
createWarrior(position) {
|
| 1411 |
+
// Create a warrior (similar to villager but with different color and weapon)
|
| 1412 |
+
const warriorGroup = new THREE.Group();
|
| 1413 |
+
|
| 1414 |
+
// Body
|
| 1415 |
+
const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 0.8, 8);
|
| 1416 |
+
const bodyMaterial = new THREE.MeshLambertMaterial({
|
| 1417 |
+
color: 0x4169E1 // Blue for warriors
|
| 1418 |
+
});
|
| 1419 |
+
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
| 1420 |
+
body.position.y = 0.4;
|
| 1421 |
+
body.castShadow = true;
|
| 1422 |
+
body.receiveShadow = true;
|
| 1423 |
+
warriorGroup.add(body);
|
| 1424 |
+
|
| 1425 |
+
// Head
|
| 1426 |
+
const headGeometry = new THREE.SphereGeometry(0.25, 16, 16);
|
| 1427 |
+
const headMaterial = new THREE.MeshLambertMaterial({
|
| 1428 |
+
color: 0xFFD700 // Gold color for head
|
| 1429 |
+
});
|
| 1430 |
+
const head = new THREE.Mesh(headGeometry, headMaterial);
|
| 1431 |
+
head.position.y = 0.9;
|
| 1432 |
+
head.castShadow = true;
|
| 1433 |
+
head.receiveShadow = true;
|
| 1434 |
+
warriorGroup.add(head);
|
| 1435 |
+
|
| 1436 |
+
// Weapon (sword)
|
| 1437 |
+
const swordGeometry = new THREE.BoxGeometry(0.05, 1, 0.05);
|
| 1438 |
+
const swordMaterial = new THREE.MeshLambertMaterial({
|
| 1439 |
+
color: 0xC0C0C0 // Silver color for sword
|
| 1440 |
+
});
|
| 1441 |
+
const sword = new THREE.Mesh(swordGeometry, swordMaterial);
|
| 1442 |
+
sword.position.set(0.4, 0.8, 0);
|
| 1443 |
+
sword.rotation.z = Math.PI / 4;
|
| 1444 |
+
warriorGroup.add(sword);
|
| 1445 |
+
|
| 1446 |
+
// Create warrior label (sprite)
|
| 1447 |
+
const canvas = document.createElement('canvas');
|
| 1448 |
+
const context = canvas.getContext('2d');
|
| 1449 |
+
canvas.width = 256;
|
| 1450 |
+
canvas.height = 128;
|
| 1451 |
+
|
| 1452 |
+
context.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
| 1453 |
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
| 1454 |
+
|
| 1455 |
+
context.fillStyle = 'white';
|
| 1456 |
+
context.font = '32px Arial';
|
| 1457 |
+
context.textAlign = 'center';
|
| 1458 |
+
context.textBaseline = 'middle';
|
| 1459 |
+
context.fillText('Warrior', canvas.width / 2, canvas.height / 2);
|
| 1460 |
+
|
| 1461 |
+
const texture = new THREE.CanvasTexture(canvas);
|
| 1462 |
+
const spriteMaterial = new THREE.SpriteMaterial({
|
| 1463 |
+
map: texture,
|
| 1464 |
+
transparent: true
|
| 1465 |
+
});
|
| 1466 |
+
const label = new THREE.Sprite(spriteMaterial);
|
| 1467 |
+
label.scale.set(3, 1.5, 1);
|
| 1468 |
+
label.position.y = 1.5;
|
| 1469 |
+
label.visible = this.showTitles; // Set initial visibility based on showTitles flag
|
| 1470 |
+
warriorGroup.add(label);
|
| 1471 |
+
|
| 1472 |
+
warriorGroup.position.set(position[0], position[1], position[2]);
|
| 1473 |
+
warriorGroup.position.y = 0;
|
| 1474 |
+
warriorGroup.castShadow = true;
|
| 1475 |
+
warriorGroup.receiveShadow = true;
|
| 1476 |
+
|
| 1477 |
+
this.scene.add(warriorGroup);
|
| 1478 |
+
this.warriorSystem.warriors.set(`warrior_${Date.now()}`, {
|
| 1479 |
+
mesh: warriorGroup,
|
| 1480 |
+
position: position,
|
| 1481 |
+
target: null
|
| 1482 |
+
});
|
| 1483 |
+
|
| 1484 |
+
this.updateVillagerCount(); // Update count to include warriors
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
dispatchWarriors() {
|
| 1488 |
+
console.log('Dispatching warriors');
|
| 1489 |
+
this.warriorSystem.dispatched = true;
|
| 1490 |
+
|
| 1491 |
+
// Make warriors patrol or attack animals/beasts
|
| 1492 |
+
for (const [id, warrior] of this.warriorSystem.warriors) {
|
| 1493 |
+
// Set a random patrol point
|
| 1494 |
+
const patrolPoint = [
|
| 1495 |
+
(Math.random() - 0.5) * 30,
|
| 1496 |
+
0,
|
| 1497 |
+
(Math.random() - 0.5) * 30
|
| 1498 |
+
];
|
| 1499 |
+
|
| 1500 |
+
warrior.target = patrolPoint;
|
| 1501 |
+
}
|
| 1502 |
+
}
|
| 1503 |
+
|
| 1504 |
+
onWindowResize() {
|
| 1505 |
+
if (this.camera && this.renderer) {
|
| 1506 |
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
| 1507 |
+
this.camera.updateProjectionMatrix();
|
| 1508 |
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 1509 |
+
}
|
| 1510 |
+
}
|
| 1511 |
+
|
| 1512 |
+
onKeyDown(event) {
|
| 1513 |
+
// WASD controls have been disabled to prevent interference with LLM chat input
|
| 1514 |
+
// All camera movement should now be handled by OrbitControls only
|
| 1515 |
+
console.log('Key pressed (WASD controls disabled):', event.code);
|
| 1516 |
+
}
|
| 1517 |
+
|
| 1518 |
+
onMouseClick(event) {
|
| 1519 |
+
const mouse = new THREE.Vector2();
|
| 1520 |
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
| 1521 |
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
| 1522 |
+
|
| 1523 |
+
const raycaster = new THREE.Raycaster();
|
| 1524 |
+
raycaster.setFromCamera(mouse, this.camera);
|
| 1525 |
+
|
| 1526 |
+
const villagerMeshes = Array.from(this.villagerMeshes.values());
|
| 1527 |
+
|
| 1528 |
+
// Use recursive intersection to handle groups
|
| 1529 |
+
const intersects = raycaster.intersectObjects(villagerMeshes, true);
|
| 1530 |
+
|
| 1531 |
+
if (intersects.length > 0) {
|
| 1532 |
+
// Find the parent group that has the villager data
|
| 1533 |
+
let selectedMesh = intersects[0].object;
|
| 1534 |
+
while (selectedMesh && !selectedMesh.userData.villager) {
|
| 1535 |
+
selectedMesh = selectedMesh.parent;
|
| 1536 |
+
}
|
| 1537 |
+
|
| 1538 |
+
if (selectedMesh && selectedMesh.userData.villager) {
|
| 1539 |
+
this.selectedVillager = selectedMesh.userData.villager;
|
| 1540 |
+
this.updateVillagerInfo();
|
| 1541 |
+
}
|
| 1542 |
+
}
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
updateVillagerCount() {
|
| 1546 |
+
const count = this.villagerMeshes.size;
|
| 1547 |
+
if (this.uiElements.villagerCountDisplay) {
|
| 1548 |
+
this.uiElements.villagerCountDisplay.textContent = count;
|
| 1549 |
+
}
|
| 1550 |
+
if (this.uiElements.villagerCountStat) {
|
| 1551 |
+
this.uiElements.villagerCountStat.textContent = count;
|
| 1552 |
+
}
|
| 1553 |
+
}
|
| 1554 |
+
|
| 1555 |
+
updateBuildingCount() {
|
| 1556 |
+
const count = this.buildingMeshes.size;
|
| 1557 |
+
if (this.uiElements.buildingCount) {
|
| 1558 |
+
this.uiElements.buildingCount.textContent = count;
|
| 1559 |
+
}
|
| 1560 |
+
}
|
| 1561 |
+
|
| 1562 |
+
updateResourceCount() {
|
| 1563 |
+
const count = this.resourceMeshes.size;
|
| 1564 |
+
if (this.uiElements.resourceCount) {
|
| 1565 |
+
this.uiElements.resourceCount.textContent = count;
|
| 1566 |
+
}
|
| 1567 |
+
}
|
| 1568 |
+
|
| 1569 |
+
updateVillagerInfo() {
|
| 1570 |
+
const villagerList = this.uiElements.villagerList;
|
| 1571 |
+
|
| 1572 |
+
if (!villagerList) return;
|
| 1573 |
+
|
| 1574 |
+
if (!this.selectedVillager) {
|
| 1575 |
+
villagerList.innerHTML = '<p>No villager selected</p>';
|
| 1576 |
+
return;
|
| 1577 |
+
}
|
| 1578 |
+
|
| 1579 |
+
const villager = this.selectedVillager;
|
| 1580 |
+
villagerList.innerHTML = `
|
| 1581 |
+
<div class="villager-item selected">
|
| 1582 |
+
<div><strong>${villager.id}</strong></div>
|
| 1583 |
+
<div>State: <span class="state-indicator state-${villager.state}"></span>${villager.state}</div>
|
| 1584 |
+
<div>Position: (${villager.position[0].toFixed(1)}, ${villager.position[1].toFixed(1)}, ${villager.position[2].toFixed(1)})</div>
|
| 1585 |
+
<div>Energy: ${villager.energy.toFixed(1)}%</div>
|
| 1586 |
+
<div>Hunger: ${villager.hunger.toFixed(1)}%</div>
|
| 1587 |
+
<div>Social Need: ${villager.socialNeed.toFixed(1)}%</div>
|
| 1588 |
+
<div>Path Points: ${villager.path.length}</div>
|
| 1589 |
+
</div>
|
| 1590 |
+
`;
|
| 1591 |
+
}
|
| 1592 |
+
|
| 1593 |
+
updatePathVisualization(villager) {
|
| 1594 |
+
const villagerId = villager.id;
|
| 1595 |
+
|
| 1596 |
+
// Remove existing path line
|
| 1597 |
+
if (this.pathLines.has(villagerId)) {
|
| 1598 |
+
this.scene.remove(this.pathLines.get(villagerId));
|
| 1599 |
+
this.pathLines.delete(villagerId);
|
| 1600 |
+
}
|
| 1601 |
+
|
| 1602 |
+
// Create new path line if villager has a path
|
| 1603 |
+
if (villager.path.length > 1) {
|
| 1604 |
+
const geometry = new THREE.BufferGeometry();
|
| 1605 |
+
const positions = [];
|
| 1606 |
+
|
| 1607 |
+
// Add current position
|
| 1608 |
+
positions.push(villager.position[0], 0.1, villager.position[2]);
|
| 1609 |
+
|
| 1610 |
+
// Add path points
|
| 1611 |
+
for (const point of villager.path) {
|
| 1612 |
+
positions.push(point[0], 0.1, point[2]);
|
| 1613 |
+
}
|
| 1614 |
+
|
| 1615 |
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
| 1616 |
+
|
| 1617 |
+
const material = new THREE.LineBasicMaterial({
|
| 1618 |
+
color: this.getStateColor(villager.state),
|
| 1619 |
+
linewidth: 3
|
| 1620 |
+
});
|
| 1621 |
+
|
| 1622 |
+
const line = new THREE.Line(geometry, material);
|
| 1623 |
+
line.visible = this.showPaths;
|
| 1624 |
+
|
| 1625 |
+
this.pathLines.set(villagerId, line);
|
| 1626 |
+
this.scene.add(line);
|
| 1627 |
+
}
|
| 1628 |
+
}
|
| 1629 |
+
|
| 1630 |
+
updateGameTime() {
|
| 1631 |
+
if (this.aiSystem && this.aiSystem.routineManager) {
|
| 1632 |
+
const time = this.aiSystem.routineManager.currentTime;
|
| 1633 |
+
const hours = Math.floor(time);
|
| 1634 |
+
const minutes = Math.floor((time - hours) * 60);
|
| 1635 |
+
if (this.uiElements.gameTime) {
|
| 1636 |
+
this.uiElements.gameTime.textContent = `${hours}:${minutes.toString().padStart(2, '0')}`;
|
| 1637 |
+
}
|
| 1638 |
+
}
|
| 1639 |
+
}
|
| 1640 |
+
|
| 1641 |
+
updateFPS() {
|
| 1642 |
+
this.frameCount++;
|
| 1643 |
+
const currentTime = performance.now();
|
| 1644 |
+
|
| 1645 |
+
if (currentTime - this.lastTime >= 1000) {
|
| 1646 |
+
this.fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime));
|
| 1647 |
+
this.frameCount = 0;
|
| 1648 |
+
this.lastTime = currentTime;
|
| 1649 |
+
if (this.uiElements.fps) {
|
| 1650 |
+
this.uiElements.fps.textContent = this.fps;
|
| 1651 |
+
}
|
| 1652 |
+
}
|
| 1653 |
+
}
|
| 1654 |
+
|
| 1655 |
+
updateVillagerMeshes() {
|
| 1656 |
+
for (const [villagerId, mesh] of this.villagerMeshes) {
|
| 1657 |
+
const villager = mesh.userData.villager;
|
| 1658 |
+
|
| 1659 |
+
// Update position
|
| 1660 |
+
mesh.position.set(villager.position[0], villager.position[1], villager.position[2]);
|
| 1661 |
+
mesh.position.y = 0;
|
| 1662 |
+
|
| 1663 |
+
// Update color based on state
|
| 1664 |
+
// Update the body color (first child)
|
| 1665 |
+
if (mesh.children.length > 0) {
|
| 1666 |
+
const body = mesh.children[0];
|
| 1667 |
+
const newColor = this.getStateColor(villager.state);
|
| 1668 |
+
if (body.material.color.getHex() !== newColor) {
|
| 1669 |
+
body.material.color.setHex(newColor);
|
| 1670 |
+
}
|
| 1671 |
+
}
|
| 1672 |
+
}
|
| 1673 |
+
}
|
| 1674 |
+
|
| 1675 |
+
updatePathVisualizations() {
|
| 1676 |
+
for (const [villagerId, mesh] of this.villagerMeshes) {
|
| 1677 |
+
const villager = mesh.userData.villager;
|
| 1678 |
+
this.updatePathVisualization(villager);
|
| 1679 |
+
}
|
| 1680 |
+
}
|
| 1681 |
+
|
| 1682 |
+
updateAnimals() {
|
| 1683 |
+
// Update animal positions and behaviors
|
| 1684 |
+
for (const [id, animal] of this.animalSystem.animals) {
|
| 1685 |
+
// Simple movement logic - move towards random points
|
| 1686 |
+
if (Math.random() < 0.02) { // 2% chance to change direction
|
| 1687 |
+
animal.targetPosition = [
|
| 1688 |
+
animal.mesh.position.x + (Math.random() - 0.5) * 10,
|
| 1689 |
+
animal.mesh.position.y,
|
| 1690 |
+
animal.mesh.position.z + (Math.random() - 0.5) * 10
|
| 1691 |
+
];
|
| 1692 |
+
}
|
| 1693 |
+
|
| 1694 |
+
// Move towards target position
|
| 1695 |
+
if (animal.targetPosition) {
|
| 1696 |
+
const speed = 0.05;
|
| 1697 |
+
const dx = animal.targetPosition[0] - animal.mesh.position.x;
|
| 1698 |
+
const dz = animal.targetPosition[2] - animal.mesh.position.z;
|
| 1699 |
+
|
| 1700 |
+
if (Math.abs(dx) > 0.1) {
|
| 1701 |
+
animal.mesh.position.x += Math.sign(dx) * speed;
|
| 1702 |
+
}
|
| 1703 |
+
if (Math.abs(dz) > 0.1) {
|
| 1704 |
+
animal.mesh.position.z += Math.sign(dz) * speed;
|
| 1705 |
+
}
|
| 1706 |
+
}
|
| 1707 |
+
|
| 1708 |
+
// Rotate to face movement direction
|
| 1709 |
+
if (animal.targetPosition) {
|
| 1710 |
+
const dx = animal.targetPosition[0] - animal.mesh.position.x;
|
| 1711 |
+
const dz = animal.targetPosition[2] - animal.mesh.position.z;
|
| 1712 |
+
animal.mesh.rotation.y = Math.atan2(dx, dz);
|
| 1713 |
+
}
|
| 1714 |
+
}
|
| 1715 |
+
}
|
| 1716 |
+
|
| 1717 |
+
updateWarriors() {
|
| 1718 |
+
// Update warrior positions and behaviors
|
| 1719 |
+
for (const [id, warrior] of this.warriorSystem.warriors) {
|
| 1720 |
+
// If warriors are dispatched, make them patrol
|
| 1721 |
+
if (this.warriorSystem.dispatched) {
|
| 1722 |
+
// Check if warrior has reached target
|
| 1723 |
+
if (warrior.target) {
|
| 1724 |
+
const dx = warrior.target[0] - warrior.mesh.position.x;
|
| 1725 |
+
const dz = warrior.target[2] - warrior.mesh.position.z;
|
| 1726 |
+
|
| 1727 |
+
// If close to target, set new target
|
| 1728 |
+
if (Math.abs(dx) < 1 && Math.abs(dz) < 1) {
|
| 1729 |
+
warrior.target = [
|
| 1730 |
+
(Math.random() - 0.5) * 30,
|
| 1731 |
+
0,
|
| 1732 |
+
(Math.random() - 0.5) * 30
|
| 1733 |
+
];
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
+
// Move towards target
|
| 1737 |
+
const speed = 0.1;
|
| 1738 |
+
if (Math.abs(dx) > 0.1) {
|
| 1739 |
+
warrior.mesh.position.x += Math.sign(dx) * speed;
|
| 1740 |
+
}
|
| 1741 |
+
if (Math.abs(dz) > 0.1) {
|
| 1742 |
+
warrior.mesh.position.z += Math.sign(dz) * speed;
|
| 1743 |
+
}
|
| 1744 |
+
|
| 1745 |
+
// Rotate to face movement direction
|
| 1746 |
+
warrior.mesh.rotation.y = Math.atan2(dx, dz);
|
| 1747 |
+
}
|
| 1748 |
+
}
|
| 1749 |
+
}
|
| 1750 |
+
}
|
| 1751 |
+
|
| 1752 |
+
animate() {
|
| 1753 |
+
requestAnimationFrame(() => this.animate());
|
| 1754 |
+
|
| 1755 |
+
const deltaTime = this.clock.getDelta() * this.timeSpeed;
|
| 1756 |
+
|
| 1757 |
+
// Update AI system
|
| 1758 |
+
if (this.aiSystem) {
|
| 1759 |
+
this.aiSystem.update(deltaTime);
|
| 1760 |
+
}
|
| 1761 |
+
|
| 1762 |
+
// Update 3D visualization
|
| 1763 |
+
this.updateVillagerMeshes();
|
| 1764 |
+
this.updatePathVisualizations();
|
| 1765 |
+
this.updateAnimals();
|
| 1766 |
+
this.updateWarriors();
|
| 1767 |
+
|
| 1768 |
+
// Update UI
|
| 1769 |
+
this.updateGameTime();
|
| 1770 |
+
this.updateFPS();
|
| 1771 |
+
this.updateVillagerInfo();
|
| 1772 |
+
|
| 1773 |
+
// Update controls
|
| 1774 |
+
if (this.controls) {
|
| 1775 |
+
this.controls.update();
|
| 1776 |
+
}
|
| 1777 |
+
|
| 1778 |
+
// Render scene
|
| 1779 |
+
if (this.renderer && this.scene && this.camera) {
|
| 1780 |
+
this.renderer.render(this.scene, this.camera);
|
| 1781 |
+
}
|
| 1782 |
+
}
|
| 1783 |
+
}
|
| 1784 |
+
|
| 1785 |
+
// Initialize the application when the page loads
|
| 1786 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1787 |
+
// Get the Hugging Face token from the window object
|
| 1788 |
+
// This could be set by a server-side process that has access to the HF_TOKEN environment variable
|
| 1789 |
+
const hfToken = window.HF_TOKEN || null;
|
| 1790 |
+
|
| 1791 |
+
// Log token status for debugging
|
| 1792 |
+
console.log('HF_TOKEN from window.HF_TOKEN:', hfToken ? 'Set' : 'Not set');
|
| 1793 |
+
if (hfToken) {
|
| 1794 |
+
console.log('HF_TOKEN length:', hfToken.length);
|
| 1795 |
+
}
|
| 1796 |
+
|
| 1797 |
+
window.app = new VillageVisualizationApp(hfToken);
|
| 1798 |
+
});
|
index.html
CHANGED
|
@@ -1,19 +1,398 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Medieval Village AI System - Three.js Visualization</title>
|
| 7 |
+
<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
font-family: Arial, sans-serif;
|
| 13 |
+
background-color: #2c3e50;
|
| 14 |
+
color: white;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
#container {
|
| 19 |
+
position: absolute;
|
| 20 |
+
top: 0;
|
| 21 |
+
left: 0;
|
| 22 |
+
width: 100%;
|
| 23 |
+
height: 100%;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
#ui-panel {
|
| 27 |
+
position: absolute;
|
| 28 |
+
top: 20px;
|
| 29 |
+
left: 20px;
|
| 30 |
+
background: rgba(0, 0, 0, 0.8);
|
| 31 |
+
padding: 10px;
|
| 32 |
+
border-radius: 10px;
|
| 33 |
+
min-width: 200px;
|
| 34 |
+
max-width: 250px;
|
| 35 |
+
height: calc(100vh - 40px);
|
| 36 |
+
overflow-y: auto;
|
| 37 |
+
z-index: 101;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
#stats-panel {
|
| 42 |
+
position: absolute;
|
| 43 |
+
top: 20px;
|
| 44 |
+
right: 20px;
|
| 45 |
+
background: rgba(0, 0, 0, 0.8);
|
| 46 |
+
padding: 20px;
|
| 47 |
+
border-radius: 10px;
|
| 48 |
+
min-width: 200px;
|
| 49 |
+
z-index: 100;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
#villager-info {
|
| 53 |
+
position: absolute;
|
| 54 |
+
bottom: 20px;
|
| 55 |
+
right: 20px;
|
| 56 |
+
background: rgba(0, 0, 0, 0.8);
|
| 57 |
+
padding: 20px;
|
| 58 |
+
border-radius: 10px;
|
| 59 |
+
min-width: 300px;
|
| 60 |
+
max-height: 200px;
|
| 61 |
+
overflow-y: auto;
|
| 62 |
+
z-index: 100;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.control-group {
|
| 66 |
+
margin-bottom: 8px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.control-group label {
|
| 70 |
+
font-size: 12px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.control-group input, .control-group button {
|
| 74 |
+
padding: 3px;
|
| 75 |
+
font-size: 10px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.control-group h4 {
|
| 79 |
+
font-size: 14px;
|
| 80 |
+
margin: 8px 0 4px 0;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.control-group button {
|
| 84 |
+
background-color: #3498db;
|
| 85 |
+
color: white;
|
| 86 |
+
cursor: pointer;
|
| 87 |
+
transition: background-color 0.3s;
|
| 88 |
+
border: none;
|
| 89 |
+
border-radius: 3px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.control-group button:hover {
|
| 93 |
+
background-color: #2980b9;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.weather-btn, .animal-btn, .warrior-btn {
|
| 97 |
+
background-color: #3498db;
|
| 98 |
+
color: white;
|
| 99 |
+
cursor: pointer;
|
| 100 |
+
transition: background-color 0.3s;
|
| 101 |
+
margin: 1px;
|
| 102 |
+
padding: 2px;
|
| 103 |
+
font-size: 12px;
|
| 104 |
+
width: 30px;
|
| 105 |
+
height: 30px;
|
| 106 |
+
display: inline-block;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.disaster-btn {
|
| 110 |
+
background-color: #e74c3c;
|
| 111 |
+
color: white;
|
| 112 |
+
cursor: pointer;
|
| 113 |
+
transition: background-color 0.3s;
|
| 114 |
+
margin: 1px;
|
| 115 |
+
padding: 2px;
|
| 116 |
+
font-size: 12px;
|
| 117 |
+
width: 30px;
|
| 118 |
+
height: 30px;
|
| 119 |
+
display: inline-block;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.weather-btn:hover, .disaster-btn:hover, .animal-btn:hover, .warrior-btn:hover {
|
| 123 |
+
background-color: #2980b9;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.disaster-btn {
|
| 127 |
+
background-color: #e74c3c;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.disaster-btn:hover {
|
| 131 |
+
background-color: #c0392b;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.animal-btn {
|
| 135 |
+
background-color: #27ae60;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.animal-btn:hover {
|
| 139 |
+
background-color: #229954;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.warrior-btn {
|
| 143 |
+
background-color: #f39c12;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.warrior-btn:hover {
|
| 147 |
+
background-color: #d68910;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.villager-item {
|
| 151 |
+
padding: 10px;
|
| 152 |
+
margin-bottom: 10px;
|
| 153 |
+
background: rgba(255, 255, 255, 0.1);
|
| 154 |
+
border-radius: 5px;
|
| 155 |
+
cursor: pointer;
|
| 156 |
+
transition: background-color 0.3s;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.villager-item:hover {
|
| 160 |
+
background: rgba(255, 255, 255, 0.2);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.villager-item.selected {
|
| 164 |
+
background: rgba(52, 152, 219, 0.3);
|
| 165 |
+
border: 2px solid #3498db;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.state-indicator {
|
| 169 |
+
display: inline-block;
|
| 170 |
+
width: 12px;
|
| 171 |
+
height: 12px;
|
| 172 |
+
border-radius: 50%;
|
| 173 |
+
margin-right: 8px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.state-sleep { background-color: #7f8c8d; }
|
| 177 |
+
.state-work { background-color: #e74c3c; }
|
| 178 |
+
.state-eat { background-color: #f39c12; }
|
| 179 |
+
.state-socialize { background-color: #9b59b6; }
|
| 180 |
+
.state-idle { background-color: #95a5a6; }
|
| 181 |
+
|
| 182 |
+
#instructions {
|
| 183 |
+
position: absolute;
|
| 184 |
+
bottom: 250px;
|
| 185 |
+
right: 20px;
|
| 186 |
+
background: rgba(0, 0, 0, 0.8);
|
| 187 |
+
padding: 15px;
|
| 188 |
+
border-radius: 10px;
|
| 189 |
+
max-width: 300px;
|
| 190 |
+
font-size: 12px;
|
| 191 |
+
z-index: 100;
|
| 192 |
+
}
|
| 193 |
+
</style>
|
| 194 |
+
</head>
|
| 195 |
+
<body>
|
| 196 |
+
<div id="container"></div>
|
| 197 |
+
|
| 198 |
+
<!-- UI Controls Panel -->
|
| 199 |
+
<div id="ui-panel">
|
| 200 |
+
<h3>Village Controls</h3>
|
| 201 |
+
|
| 202 |
+
<div class="control-group">
|
| 203 |
+
<label for="villager-count">Villager Count: <span id="villager-count-display">0</span></label>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<div class="control-group">
|
| 207 |
+
<label for="add-villager-btn">Add Villager</label>
|
| 208 |
+
<button id="add-villager-btn" title="Add New Villager">👨🌾</button>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<div class="control-group">
|
| 212 |
+
<label for="reset-btn">Reset Simulation</label>
|
| 213 |
+
<button id="reset-btn" title="Reset All">🔄</button>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<div class="control-group">
|
| 217 |
+
<label for="time-speed">Time Speed: <span id="time-speed-display">1.0x</span></label>
|
| 218 |
+
<input type="range" id="time-speed" min="0.1" max="5" step="0.1" value="1.0">
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<div class="control-group">
|
| 222 |
+
<label for="show-paths">Show Movement Paths</label>
|
| 223 |
+
<input type="checkbox" id="show-paths" checked>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<div class="control-group">
|
| 227 |
+
<label for="show-titles">Show Villager Titles</label>
|
| 228 |
+
<input type="checkbox" id="show-titles" checked>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<!-- Weather Controls -->
|
| 232 |
+
<div class="control-group">
|
| 233 |
+
<h4>Weather Controls</h4>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<div class="control-group">
|
| 237 |
+
<label for="fog-control">Fog Intensity</label>
|
| 238 |
+
<input type="range" id="fog-control" min="0" max="100" value="50">
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div class="control-group">
|
| 242 |
+
<button id="weather-sun" class="weather-btn" title="Sunny Weather">☀️</button>
|
| 243 |
+
<button id="weather-rain" class="weather-btn" title="Rain Weather">🌧️</button>
|
| 244 |
+
<button id="weather-snow" class="weather-btn" title="Snow Weather">❄️</button>
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<!-- Disaster Controls -->
|
| 248 |
+
<div class="control-group">
|
| 249 |
+
<h4>Disaster Controls</h4>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<div class="control-group">
|
| 253 |
+
<button id="disaster-fire" class="disaster-btn" title="Fire Disaster">🔥</button>
|
| 254 |
+
<button id="disaster-hurricane" class="disaster-btn" title="Hurricane Disaster">🌪️</button>
|
| 255 |
+
<button id="disaster-flood" class="disaster-btn" title="Flood Disaster">🌊</button>
|
| 256 |
+
<button id="disaster-earthquake" class="disaster-btn" title="Earthquake Disaster">🌍</button>
|
| 257 |
+
<button id="disaster-plague" class="disaster-btn" title="Plague Disaster">🦠</button>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<!-- Animal/Beast Controls -->
|
| 261 |
+
<div class="control-group">
|
| 262 |
+
<h4>Animal/Beast Controls</h4>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
+
<div class="control-group">
|
| 266 |
+
<button id="spawn-wolf" class="animal-btn" title="Spawn Wolf">🐺</button>
|
| 267 |
+
<button id="spawn-bear" class="animal-btn" title="Spawn Bear">🐻</button>
|
| 268 |
+
<button id="spawn-dragon" class="animal-btn" title="Spawn Dragon">🐉</button>
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<!-- Warrior Controls -->
|
| 272 |
+
<div class="control-group">
|
| 273 |
+
<h4>Warrior Controls</h4>
|
| 274 |
+
</div>
|
| 275 |
+
|
| 276 |
+
<div class="control-group">
|
| 277 |
+
<button id="add-warrior" class="warrior-btn" title="Add Warrior">⚔️</button>
|
| 278 |
+
<button id="dispatch-warriors" class="warrior-btn" title="Dispatch Warriors">🛡️</button>
|
| 279 |
+
</div>
|
| 280 |
+
|
| 281 |
+
<!-- LLM Controls -->
|
| 282 |
+
<div class="control-group">
|
| 283 |
+
<h4>LLM Controls <span id="llm-status-indicator" style="width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-left: 10px;"></span></h4>
|
| 284 |
+
</div>
|
| 285 |
+
|
| 286 |
+
<div class="control-group">
|
| 287 |
+
<label for="llm-model">Select LLM Model:</label>
|
| 288 |
+
<select id="llm-model" style="width: 100%; padding: 2px; font-size: 11px;">
|
| 289 |
+
<option value="meta-llama/Llama-3.1-8B-Instruct">Llama-3.1-8B-Instruct</option>
|
| 290 |
+
<option value="google/gemma-3-270m-it">Gemma-3-270m-it</option>
|
| 291 |
+
<option value="google/gemma-3-4b-it">Gemma-3-4b-it</option>
|
| 292 |
+
<option value="google/gemma-3-27b-it">Gemma-3-27b-it</option>
|
| 293 |
+
<option value="Qwen/Qwen3-4B-Instruct-2507">Qwen3-4B-Instruct</option>
|
| 294 |
+
<option value="Qwen/Qwen3-8B">Qwen3-8B</option>
|
| 295 |
+
<option value="mistralai/Mistral-7B-Instruct-v0.3">Mistral-7B-Instruct</option>
|
| 296 |
+
<option value="HuggingFaceH4/zephyr-7b-beta">Zephyr-7b-beta</option>
|
| 297 |
+
<option value="TinyLlama/TinyLlama-1.1B-Chat-v1.0">TinyLlama-1.1B-Chat</option>
|
| 298 |
+
<option value="microsoft/Phi-3-mini-4k-instruct">Phi-3-mini-4k</option>
|
| 299 |
+
<option value="stabilityai/stablelm-2-1_6b">StableLM-2-1_6b</option>
|
| 300 |
+
<option value="NousResearch/Hermes-2-Pro-Llama-3-8B">Hermes-2-Pro-Llama-3</option>
|
| 301 |
+
<option value="CohereForAI/c4ai-command-r-v01">C4AI-Command-R</option>
|
| 302 |
+
<option value="nvidia/Nemotron-Research-Reasoning-Qwen-1.5B">Nemotron-Qwen-1.5B</option>
|
| 303 |
+
<option value="inclusionAI/AReaL-boba-2-8B">AReaL-boba-2-8B</option>
|
| 304 |
+
</select>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div class="control-group">
|
| 308 |
+
<label for="llm-query">Ask the LLM:</label>
|
| 309 |
+
<input type="text" id="llm-query" placeholder="Enter your question..." style="width: 100%; padding: 2px; font-size: 11px;">
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<div class="control-group">
|
| 313 |
+
<button id="llm-submit" style="width: 100%; padding: 3px; font-size: 11px;">Submit Query</button>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<div class="control-group">
|
| 317 |
+
<label for="llm-response">LLM Response:</label>
|
| 318 |
+
<div id="llm-response" style="background: rgba(255, 255, 255, 0.1); padding: 5px; border-radius: 5px; min-height: 50px; max-height: 100px; overflow-y: auto; font-size: 11px;">
|
| 319 |
+
No response yet. Submit a query to get started.
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
<div class="control-group">
|
| 324 |
+
<div style="background: rgba(255, 255, 0, 0.1); padding: 5px; border-radius: 5px; font-size: 9px; margin-top: 5px;">
|
| 325 |
+
<strong>API Token Required:</strong> To use the LLM functionality, you need to set your Hugging Face API token.
|
| 326 |
+
Get one from <a href="https://huggingface.co/settings/tokens" target="_blank" style="color: #3498db;">Hugging Face</a>.
|
| 327 |
+
<br><br>
|
| 328 |
+
If running this application with a server that has access to the HF_TOKEN environment variable, the token will be automatically used.
|
| 329 |
+
<br><br>
|
| 330 |
+
For manual setup, you can set the token in the browser console with:
|
| 331 |
+
<code>window.HF_TOKEN = 'your-actual-token-here'</code>
|
| 332 |
+
<br>or<br>
|
| 333 |
+
<code>app.llmHandler.setApiToken('your-actual-token-here')</code>
|
| 334 |
+
<br><br>
|
| 335 |
+
Check the browser console for debugging information about the token status.
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<!-- Stats Panel -->
|
| 341 |
+
<div id="stats-panel">
|
| 342 |
+
<h3>Simulation Stats</h3>
|
| 343 |
+
<div id="stats-content">
|
| 344 |
+
<div>Time: <span id="game-time">0:00</span></div>
|
| 345 |
+
<div>FPS: <span id="fps">0</span></div>
|
| 346 |
+
<div>Villagers: <span id="villager-count-stat">0</span></div>
|
| 347 |
+
<div>Buildings: <span id="building-count">0</span></div>
|
| 348 |
+
<div>Resources: <span id="resource-count">0</span></div>
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
<!-- Villager Information Panel -->
|
| 353 |
+
<div id="villager-info">
|
| 354 |
+
<h3>Villager Information</h3>
|
| 355 |
+
<div id="villager-list">
|
| 356 |
+
<p>No villagers selected</p>
|
| 357 |
+
</div>
|
| 358 |
+
</div>
|
| 359 |
+
|
| 360 |
+
<!-- Instructions Panel -->
|
| 361 |
+
<div id="instructions">
|
| 362 |
+
<h4>Controls:</h4>
|
| 363 |
+
<ul>
|
| 364 |
+
<li><strong>Mouse:</strong> Look around</li>
|
| 365 |
+
<li><strong>WASD:</strong> Move camera</li>
|
| 366 |
+
<li><strong>Space:</strong> Up</li>
|
| 367 |
+
<li><strong>Shift:</strong> Down</li>
|
| 368 |
+
<li><strong>Click villager:</strong> Select for info</li>
|
| 369 |
+
</ul>
|
| 370 |
+
<h4>Legend:</h4>
|
| 371 |
+
<ul>
|
| 372 |
+
<li><span class="state-indicator state-sleep"></span>Sleep</li>
|
| 373 |
+
<li><span class="state-indicator state-work"></span>Work</li>
|
| 374 |
+
<li><span class="state-indicator state-eat"></span>Eat</li>
|
| 375 |
+
<li><span class="state-indicator state-socialize"></span>Socialize</li>
|
| 376 |
+
</ul>
|
| 377 |
+
</div>
|
| 378 |
+
|
| 379 |
+
<!-- Hugging Face Token -->
|
| 380 |
+
<script>
|
| 381 |
+
// In a server-side implementation, the HF_TOKEN environment variable would be injected here
|
| 382 |
+
// For example, a Node.js server could inject it like this:
|
| 383 |
+
// window.HF_TOKEN = process.env.HF_TOKEN;
|
| 384 |
+
//
|
| 385 |
+
// For client-side usage, users can set the token in the browser console:
|
| 386 |
+
// window.HF_TOKEN = "your-actual-hugging-face-token";
|
| 387 |
+
//
|
| 388 |
+
// For testing purposes, you can uncomment the line below and replace with your actual token:
|
| 389 |
+
// window.HF_TOKEN = "your-actual-hugging-face-token";
|
| 390 |
+
window.HF_TOKEN = null;
|
| 391 |
+
</script>
|
| 392 |
+
|
| 393 |
+
<!-- Three.js and Application Scripts -->
|
| 394 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js?v=1"></script>
|
| 395 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js?v=1"></script>
|
| 396 |
+
<script type="module" src="app_new.js?v=1"></script>
|
| 397 |
+
</body>
|
| 398 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio==4.36.1
|
simple_app.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Simple Three.js test application
|
| 2 |
+
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
|
| 3 |
+
|
| 4 |
+
class SimpleVillageApp {
|
| 5 |
+
constructor() {
|
| 6 |
+
this.scene = null;
|
| 7 |
+
this.camera = null;
|
| 8 |
+
this.renderer = null;
|
| 9 |
+
this.controls = null;
|
| 10 |
+
|
| 11 |
+
this.init();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
init() {
|
| 15 |
+
console.log('Initializing Simple Village App...');
|
| 16 |
+
this.initThreeJS();
|
| 17 |
+
this.createEnvironment();
|
| 18 |
+
this.animate();
|
| 19 |
+
console.log('Simple Village App initialized successfully');
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
initThreeJS() {
|
| 23 |
+
// Scene
|
| 24 |
+
this.scene = new THREE.Scene();
|
| 25 |
+
this.scene.background = new THREE.Color(0x87CEEB);
|
| 26 |
+
|
| 27 |
+
// Camera
|
| 28 |
+
this.camera = new THREE.PerspectiveCamera(
|
| 29 |
+
75,
|
| 30 |
+
window.innerWidth / window.innerHeight,
|
| 31 |
+
0.1,
|
| 32 |
+
1000
|
| 33 |
+
);
|
| 34 |
+
this.camera.position.set(20, 20, 20);
|
| 35 |
+
this.camera.lookAt(0, 0, 0);
|
| 36 |
+
|
| 37 |
+
// Renderer
|
| 38 |
+
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 39 |
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 40 |
+
this.renderer.shadowMap.enabled = true;
|
| 41 |
+
|
| 42 |
+
const container = document.getElementById('container');
|
| 43 |
+
if (container) {
|
| 44 |
+
container.appendChild(this.renderer.domElement);
|
| 45 |
+
console.log('Renderer added to DOM');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Lighting
|
| 49 |
+
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
|
| 50 |
+
this.scene.add(ambientLight);
|
| 51 |
+
|
| 52 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| 53 |
+
directionalLight.position.set(10, 10, 5);
|
| 54 |
+
directionalLight.castShadow = true;
|
| 55 |
+
this.scene.add(directionalLight);
|
| 56 |
+
|
| 57 |
+
// Ground plane
|
| 58 |
+
const groundGeometry = new THREE.PlaneGeometry(100, 100);
|
| 59 |
+
const groundMaterial = new THREE.MeshLambertMaterial({
|
| 60 |
+
color: 0x228B22,
|
| 61 |
+
transparent: true,
|
| 62 |
+
opacity: 0.8
|
| 63 |
+
});
|
| 64 |
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
| 65 |
+
ground.rotation.x = -Math.PI / 2;
|
| 66 |
+
ground.receiveShadow = true;
|
| 67 |
+
this.scene.add(ground);
|
| 68 |
+
|
| 69 |
+
// Grid helper
|
| 70 |
+
const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222);
|
| 71 |
+
this.scene.add(gridHelper);
|
| 72 |
+
|
| 73 |
+
window.addEventListener('resize', () => this.onWindowResize());
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
createEnvironment() {
|
| 77 |
+
// Create some simple buildings
|
| 78 |
+
this.createBuilding(5, 0, 5, 0x8B4513);
|
| 79 |
+
this.createBuilding(-5, 0, -5, 0x696969);
|
| 80 |
+
this.createBuilding(0, 0, 0, 0xFFD700);
|
| 81 |
+
|
| 82 |
+
// Create some villagers
|
| 83 |
+
this.createVillager(0, 0, 0, 0xff0000);
|
| 84 |
+
this.createVillager(5, 0, 5, 0x00ff00);
|
| 85 |
+
this.createVillager(-3, 0, -3, 0x0000ff);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
createBuilding(x, y, z, color) {
|
| 89 |
+
const geometry = new THREE.BoxGeometry(3, 3, 3);
|
| 90 |
+
const material = new THREE.MeshLambertMaterial({ color: color });
|
| 91 |
+
const mesh = new THREE.Mesh(geometry, material);
|
| 92 |
+
mesh.position.set(x, y + 1.5, z);
|
| 93 |
+
mesh.castShadow = true;
|
| 94 |
+
mesh.receiveShadow = true;
|
| 95 |
+
this.scene.add(mesh);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
createVillager(x, y, z, color) {
|
| 99 |
+
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
|
| 100 |
+
const material = new THREE.MeshLambertMaterial({ color: color });
|
| 101 |
+
const mesh = new THREE.Mesh(geometry, material);
|
| 102 |
+
mesh.position.set(x, y + 0.5, z);
|
| 103 |
+
mesh.castShadow = true;
|
| 104 |
+
mesh.receiveShadow = true;
|
| 105 |
+
this.scene.add(mesh);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
onWindowResize() {
|
| 109 |
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
| 110 |
+
this.camera.updateProjectionMatrix();
|
| 111 |
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
animate() {
|
| 115 |
+
requestAnimationFrame(() => this.animate());
|
| 116 |
+
|
| 117 |
+
// Simple rotation for testing
|
| 118 |
+
this.scene.rotation.y += 0.001;
|
| 119 |
+
|
| 120 |
+
this.renderer.render(this.scene, this.camera);
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// Initialize the application when the page loads
|
| 125 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 126 |
+
new SimpleVillageApp();
|
| 127 |
+
});
|
src/ai/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Medieval Village AI System
|
| 2 |
+
|
| 3 |
+
This directory contains the AI system implementation for a medieval village simulator. The system includes various components for managing villager behavior, movement, and interactions.
|
| 4 |
+
|
| 5 |
+
## Components
|
| 6 |
+
|
| 7 |
+
### Pathfinding (`pathfinding.js`)
|
| 8 |
+
Implements A* pathfinding using navigation meshes for efficient village-scale navigation in 3D space.
|
| 9 |
+
|
| 10 |
+
Key features:
|
| 11 |
+
- Navigation mesh support for complex 3D environments
|
| 12 |
+
- Path calculation between any two points in the village
|
| 13 |
+
- Entity movement along calculated paths
|
| 14 |
+
|
| 15 |
+
### Behavior Trees (`behavior.js`)
|
| 16 |
+
Implements a behavior tree system for complex villager decision-making.
|
| 17 |
+
|
| 18 |
+
Key features:
|
| 19 |
+
- Behavior tree nodes (Selector, Sequence, Action, Condition)
|
| 20 |
+
- Villager behavior management for daily routines
|
| 21 |
+
- Extensible behavior system for custom actions
|
| 22 |
+
|
| 23 |
+
### Crowd Simulation (`crowd.js`)
|
| 24 |
+
Implements crowd simulation with collision avoidance for multiple villagers.
|
| 25 |
+
|
| 26 |
+
Key features:
|
| 27 |
+
- Steering behaviors for natural movement
|
| 28 |
+
- Spatial partitioning for efficient neighbor queries
|
| 29 |
+
- Collision avoidance to prevent villagers from walking through each other
|
| 30 |
+
|
| 31 |
+
### Daily Routines (`routines.js`)
|
| 32 |
+
Manages villager daily routines including work, rest, and social interactions.
|
| 33 |
+
|
| 34 |
+
Key features:
|
| 35 |
+
- Schedule-based routine system
|
| 36 |
+
- Activity management (sleep, work, eat, socialize)
|
| 37 |
+
- Need-based systems (energy, hunger, social)
|
| 38 |
+
|
| 39 |
+
### Environmental Interaction (`environment.js`)
|
| 40 |
+
Handles how villagers interact with their environment, including resource gathering and building usage.
|
| 41 |
+
|
| 42 |
+
Key features:
|
| 43 |
+
- Resource management system
|
| 44 |
+
- Building interaction system
|
| 45 |
+
- Inventory system for villagers
|
| 46 |
+
|
| 47 |
+
### Performance Optimization (`optimization.js`)
|
| 48 |
+
Implements various optimization techniques for managing multiple AI entities efficiently.
|
| 49 |
+
|
| 50 |
+
Key features:
|
| 51 |
+
- Entity manager with update queuing
|
| 52 |
+
- Spatial partitioning for efficient queries
|
| 53 |
+
- Object pooling to reduce garbage collection
|
| 54 |
+
|
| 55 |
+
### Main System (`main.js`)
|
| 56 |
+
Integrates all components into a cohesive AI system for the village simulator.
|
| 57 |
+
|
| 58 |
+
## Integration with Three.js
|
| 59 |
+
|
| 60 |
+
The AI system is designed to integrate with Three.js for 3D rendering:
|
| 61 |
+
|
| 62 |
+
1. Pathfinding uses Three.js Vector3 for positions
|
| 63 |
+
2. Crowd simulation uses Three.js Vector3 for positions and velocities
|
| 64 |
+
3. Environmental interactions use Three.js Vector3 for positions
|
| 65 |
+
4. The main system takes a Three.js scene object for integration
|
| 66 |
+
|
| 67 |
+
## Usage Example
|
| 68 |
+
|
| 69 |
+
```javascript
|
| 70 |
+
import VillageAISystem from './ai/main.js';
|
| 71 |
+
import * as THREE from 'three';
|
| 72 |
+
|
| 73 |
+
// Create a Three.js scene
|
| 74 |
+
const scene = new THREE.Scene();
|
| 75 |
+
|
| 76 |
+
// Initialize the AI system
|
| 77 |
+
const aiSystem = new VillageAISystem(scene);
|
| 78 |
+
|
| 79 |
+
// Create villagers
|
| 80 |
+
const villager1 = aiSystem.createVillager('villager1', new THREE.Vector3(0, 0, 0));
|
| 81 |
+
const villager2 = aiSystem.createVillager('villager2', new THREE.Vector3(5, 0, 5));
|
| 82 |
+
|
| 83 |
+
// In your animation loop
|
| 84 |
+
function animate(deltaTime) {
|
| 85 |
+
// Update the AI system
|
| 86 |
+
aiSystem.update(deltaTime);
|
| 87 |
+
|
| 88 |
+
// Render the scene
|
| 89 |
+
renderer.render(scene, camera);
|
| 90 |
+
|
| 91 |
+
requestAnimationFrame(animate);
|
| 92 |
+
}
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Performance Considerations
|
| 96 |
+
|
| 97 |
+
1. **Entity Updates**: Uses a queuing system to limit the number of entities updated per frame
|
| 98 |
+
2. **Spatial Partitioning**: Implements spatial partitioning for efficient neighbor queries
|
| 99 |
+
3. **Object Pooling**: Reduces garbage collection through object pooling
|
| 100 |
+
4. **Level of Detail**: In a full implementation, you would add LOD systems to reduce AI complexity for distant entities
|
| 101 |
+
|
| 102 |
+
## Extending the System
|
| 103 |
+
|
| 104 |
+
The system is designed to be extensible:
|
| 105 |
+
|
| 106 |
+
1. Add new behavior tree nodes for custom actions
|
| 107 |
+
2. Extend the villager class for specialized villager types
|
| 108 |
+
3. Add new resource and building types
|
| 109 |
+
4. Implement additional optimization techniques as needed
|
src/ai/behavior.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Behavior Tree Implementation for Villager AI
|
| 3 |
+
*
|
| 4 |
+
* This implementation uses a behavior tree approach to manage complex villager behaviors
|
| 5 |
+
* including decision-making for daily routines, work, rest, and social interactions.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
// Base Node class for behavior tree
|
| 9 |
+
class Node {
|
| 10 |
+
constructor() {
|
| 11 |
+
this.status = 'INVALID'; // INVALID, SUCCESS, FAILURE, RUNNING
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
execute(agent, deltaTime) {
|
| 15 |
+
throw new Error('Execute method must be implemented');
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Composite node that can have children
|
| 20 |
+
class CompositeNode extends Node {
|
| 21 |
+
constructor() {
|
| 22 |
+
super();
|
| 23 |
+
this.children = [];
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
addChild(child) {
|
| 27 |
+
this.children.push(child);
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Selector node - tries children in order until one succeeds
|
| 32 |
+
class SelectorNode extends CompositeNode {
|
| 33 |
+
constructor() {
|
| 34 |
+
super();
|
| 35 |
+
this.currentChildIndex = 0;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
execute(agent, deltaTime) {
|
| 39 |
+
// If we were running a child previously, continue with it
|
| 40 |
+
if (this.status === 'RUNNING' && this.currentChildIndex < this.children.length) {
|
| 41 |
+
const result = this.children[this.currentChildIndex].execute(agent, deltaTime);
|
| 42 |
+
if (result === 'RUNNING') {
|
| 43 |
+
return 'RUNNING';
|
| 44 |
+
} else if (result === 'SUCCESS') {
|
| 45 |
+
this.status = 'SUCCESS';
|
| 46 |
+
this.currentChildIndex = 0;
|
| 47 |
+
return 'SUCCESS';
|
| 48 |
+
}
|
| 49 |
+
// If child failed, try next child
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Try children in order
|
| 53 |
+
for (let i = 0; i < this.children.length; i++) {
|
| 54 |
+
const result = this.children[i].execute(agent, deltaTime);
|
| 55 |
+
if (result === 'RUNNING') {
|
| 56 |
+
this.status = 'RUNNING';
|
| 57 |
+
this.currentChildIndex = i;
|
| 58 |
+
return 'RUNNING';
|
| 59 |
+
} else if (result === 'SUCCESS') {
|
| 60 |
+
this.status = 'SUCCESS';
|
| 61 |
+
this.currentChildIndex = 0;
|
| 62 |
+
return 'SUCCESS';
|
| 63 |
+
}
|
| 64 |
+
// If child failed, try next child
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// All children failed
|
| 68 |
+
this.status = 'FAILURE';
|
| 69 |
+
this.currentChildIndex = 0;
|
| 70 |
+
return 'FAILURE';
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Sequence node - executes children in order until one fails
|
| 75 |
+
class SequenceNode extends CompositeNode {
|
| 76 |
+
constructor() {
|
| 77 |
+
super();
|
| 78 |
+
this.currentChildIndex = 0;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
execute(agent, deltaTime) {
|
| 82 |
+
// If we were running a child previously, continue with it
|
| 83 |
+
if (this.status === 'RUNNING' && this.currentChildIndex < this.children.length) {
|
| 84 |
+
const result = this.children[this.currentChildIndex].execute(agent, deltaTime);
|
| 85 |
+
if (result === 'RUNNING') {
|
| 86 |
+
return 'RUNNING';
|
| 87 |
+
} else if (result === 'FAILURE') {
|
| 88 |
+
this.status = 'FAILURE';
|
| 89 |
+
this.currentChildIndex = 0;
|
| 90 |
+
return 'FAILURE';
|
| 91 |
+
}
|
| 92 |
+
// If child succeeded, try next child
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Try children in order
|
| 96 |
+
for (let i = this.currentChildIndex; i < this.children.length; i++) {
|
| 97 |
+
const result = this.children[i].execute(agent, deltaTime);
|
| 98 |
+
if (result === 'RUNNING') {
|
| 99 |
+
this.status = 'RUNNING';
|
| 100 |
+
this.currentChildIndex = i;
|
| 101 |
+
return 'RUNNING';
|
| 102 |
+
} else if (result === 'FAILURE') {
|
| 103 |
+
this.status = 'FAILURE';
|
| 104 |
+
this.currentChildIndex = 0;
|
| 105 |
+
return 'FAILURE';
|
| 106 |
+
}
|
| 107 |
+
// If child succeeded, continue to next child
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// All children succeeded
|
| 111 |
+
this.status = 'SUCCESS';
|
| 112 |
+
this.currentChildIndex = 0;
|
| 113 |
+
return 'SUCCESS';
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Leaf node for specific actions
|
| 118 |
+
class ActionNode extends Node {
|
| 119 |
+
constructor(action) {
|
| 120 |
+
super();
|
| 121 |
+
this.action = action;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
execute(agent, deltaTime) {
|
| 125 |
+
return this.action(agent, deltaTime);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Condition node to check specific conditions
|
| 130 |
+
class ConditionNode extends Node {
|
| 131 |
+
constructor(condition) {
|
| 132 |
+
super();
|
| 133 |
+
this.condition = condition;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
execute(agent, deltaTime) {
|
| 137 |
+
return this.condition(agent, deltaTime) ? 'SUCCESS' : 'FAILURE';
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Villager behavior tree implementation
|
| 142 |
+
class VillagerBehaviorTree {
|
| 143 |
+
constructor() {
|
| 144 |
+
this.root = this.createBehaviorTree();
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
createBehaviorTree() {
|
| 148 |
+
// Root selector - decides what the villager should do
|
| 149 |
+
const root = new SelectorNode();
|
| 150 |
+
|
| 151 |
+
// Check if it's time to sleep
|
| 152 |
+
const sleepSequence = new SequenceNode();
|
| 153 |
+
sleepSequence.addChild(new ConditionNode((agent) => agent.isTired()));
|
| 154 |
+
sleepSequence.addChild(new ActionNode((agent) => agent.sleep()));
|
| 155 |
+
|
| 156 |
+
// Check if it's time to work
|
| 157 |
+
const workSequence = new SequenceNode();
|
| 158 |
+
workSequence.addChild(new ConditionNode((agent) => agent.shouldWork()));
|
| 159 |
+
workSequence.addChild(new ActionNode((agent) => agent.work()));
|
| 160 |
+
|
| 161 |
+
// Check if it's time to socialize
|
| 162 |
+
const socializeSequence = new SequenceNode();
|
| 163 |
+
socializeSequence.addChild(new ConditionNode((agent) => agent.shouldSocialize()));
|
| 164 |
+
socializeSequence.addChild(new ActionNode((agent) => agent.socialize()));
|
| 165 |
+
|
| 166 |
+
// Default action - idle
|
| 167 |
+
const idleAction = new ActionNode((agent) => agent.idle());
|
| 168 |
+
|
| 169 |
+
// Add all sequences to root
|
| 170 |
+
root.addChild(sleepSequence);
|
| 171 |
+
root.addChild(workSequence);
|
| 172 |
+
root.addChild(socializeSequence);
|
| 173 |
+
root.addChild(idleAction);
|
| 174 |
+
|
| 175 |
+
return root;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
update(agent, deltaTime) {
|
| 179 |
+
this.root.execute(agent, deltaTime);
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
export { VillagerBehaviorTree, Node, CompositeNode, SelectorNode, SequenceNode, ActionNode, ConditionNode };
|
src/ai/crowd.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// No direct THREE.js import needed
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Crowd Simulation with Collision Avoidance for Villagers
|
| 5 |
+
*
|
| 6 |
+
* This implementation uses a combination of steering behaviors and spatial partitioning
|
| 7 |
+
* to efficiently manage crowd simulation with collision avoidance for multiple villagers.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
class CrowdManager {
|
| 11 |
+
constructor() {
|
| 12 |
+
this.villagers = [];
|
| 13 |
+
this.spatialGrid = new SpatialGrid(100, 100, 5); // 100x100 grid with 5 unit cells
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Add a villager to the crowd simulation
|
| 18 |
+
* @param {Villager} villager - The villager to add
|
| 19 |
+
*/
|
| 20 |
+
addVillager(villager) {
|
| 21 |
+
this.villagers.push(villager);
|
| 22 |
+
this.spatialGrid.insert(villager);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Update all villagers in the crowd simulation
|
| 27 |
+
* @param {number} deltaTime - Time since last frame
|
| 28 |
+
*/
|
| 29 |
+
update(deltaTime) {
|
| 30 |
+
// Update spatial grid
|
| 31 |
+
this.spatialGrid.clear();
|
| 32 |
+
for (const villager of this.villagers) {
|
| 33 |
+
this.spatialGrid.insert(villager);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Update each villager
|
| 37 |
+
for (const villager of this.villagers) {
|
| 38 |
+
// Get nearby villagers for collision avoidance
|
| 39 |
+
const nearby = this.spatialGrid.query(villager.position, 10);
|
| 40 |
+
|
| 41 |
+
// Calculate steering forces
|
| 42 |
+
const avoidanceForce = this.calculateAvoidanceForce(villager, nearby);
|
| 43 |
+
const steeringForce = this.calculateSteeringForce(villager);
|
| 44 |
+
|
| 45 |
+
// Apply forces to velocity (using arrays)
|
| 46 |
+
this.addScaledVector(villager.velocity, avoidanceForce, deltaTime);
|
| 47 |
+
this.addScaledVector(villager.velocity, steeringForce, deltaTime);
|
| 48 |
+
|
| 49 |
+
// Limit velocity
|
| 50 |
+
const speed = this.calculateVectorLength(villager.velocity);
|
| 51 |
+
if (speed > villager.maxSpeed) {
|
| 52 |
+
this.normalizeVector(villager.velocity);
|
| 53 |
+
this.scaleVectorInPlace(villager.velocity, villager.maxSpeed);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Update position
|
| 57 |
+
const moveVector = this.scaleVector(villager.velocity, deltaTime);
|
| 58 |
+
this.addVectors(villager.position, moveVector);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Calculate avoidance force to prevent collisions
|
| 64 |
+
* @param {Villager} villager - The villager to calculate for
|
| 65 |
+
* @param {Array<Villager>} nearby - Nearby villagers
|
| 66 |
+
* @returns {Array} Avoidance force as [x, y, z]
|
| 67 |
+
*/
|
| 68 |
+
calculateAvoidanceForce(villager, nearby) {
|
| 69 |
+
const force = [0, 0, 0];
|
| 70 |
+
|
| 71 |
+
for (const neighbor of nearby) {
|
| 72 |
+
if (neighbor === villager) continue;
|
| 73 |
+
|
| 74 |
+
const distance = this.calculateDistance(villager.position, neighbor.position);
|
| 75 |
+
if (distance < villager.avoidanceRadius && distance > 0) {
|
| 76 |
+
// Calculate avoidance direction (away from neighbor)
|
| 77 |
+
const direction = this.subtractVectors(villager.position, neighbor.position);
|
| 78 |
+
this.normalizeVector(direction);
|
| 79 |
+
|
| 80 |
+
// Scale force by inverse of distance (stronger when closer)
|
| 81 |
+
const strength = (villager.avoidanceRadius - distance) / villager.avoidanceRadius;
|
| 82 |
+
this.addScaledVector(force, direction, strength);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return this.scaleVector(force, villager.avoidanceWeight);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Calculate distance between two positions
|
| 91 |
+
*/
|
| 92 |
+
calculateDistance(pos1, pos2) {
|
| 93 |
+
const dx = pos1[0] - pos2[0];
|
| 94 |
+
const dy = pos1[1] - pos2[1];
|
| 95 |
+
const dz = pos1[2] - pos2[2];
|
| 96 |
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/**
|
| 100 |
+
* Subtract two vectors
|
| 101 |
+
*/
|
| 102 |
+
subtractVectors(vec1, vec2) {
|
| 103 |
+
return [
|
| 104 |
+
vec1[0] - vec2[0],
|
| 105 |
+
vec1[1] - vec2[1],
|
| 106 |
+
vec1[2] - vec2[2]
|
| 107 |
+
];
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* Normalize a vector
|
| 112 |
+
*/
|
| 113 |
+
normalizeVector(vec) {
|
| 114 |
+
const length = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
|
| 115 |
+
if (length > 0) {
|
| 116 |
+
vec[0] /= length;
|
| 117 |
+
vec[1] /= length;
|
| 118 |
+
vec[2] /= length;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Add scaled vector to another vector
|
| 124 |
+
*/
|
| 125 |
+
addScaledVector(target, source, scale) {
|
| 126 |
+
target[0] += source[0] * scale;
|
| 127 |
+
target[1] += source[1] * scale;
|
| 128 |
+
target[2] += source[2] * scale;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Scale a vector
|
| 133 |
+
*/
|
| 134 |
+
scaleVector(vec, scale) {
|
| 135 |
+
return [
|
| 136 |
+
vec[0] * scale,
|
| 137 |
+
vec[1] * scale,
|
| 138 |
+
vec[2] * scale
|
| 139 |
+
];
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* Calculate vector length
|
| 144 |
+
*/
|
| 145 |
+
calculateVectorLength(vec) {
|
| 146 |
+
return Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Scale vector in place
|
| 151 |
+
*/
|
| 152 |
+
scaleVectorInPlace(vec, scale) {
|
| 153 |
+
vec[0] *= scale;
|
| 154 |
+
vec[1] *= scale;
|
| 155 |
+
vec[2] *= scale;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Add two vectors
|
| 160 |
+
*/
|
| 161 |
+
addVectors(target, source) {
|
| 162 |
+
target[0] += source[0];
|
| 163 |
+
target[1] += source[1];
|
| 164 |
+
target[2] += source[2];
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/**
|
| 168 |
+
* Calculate steering force to follow path
|
| 169 |
+
* @param {Villager} villager - The villager to calculate for
|
| 170 |
+
* @returns {Array} Steering force as [x, y, z]
|
| 171 |
+
*/
|
| 172 |
+
calculateSteeringForce(villager) {
|
| 173 |
+
if (villager.path.length === 0) return [0, 0, 0];
|
| 174 |
+
|
| 175 |
+
const target = villager.path[0];
|
| 176 |
+
const direction = this.subtractVectors(target, villager.position);
|
| 177 |
+
this.normalizeVector(direction);
|
| 178 |
+
const desired = this.scaleVector(direction, villager.maxSpeed);
|
| 179 |
+
const steer = this.subtractVectors(desired, villager.velocity);
|
| 180 |
+
|
| 181 |
+
return this.scaleVector(steer, villager.steeringWeight);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/**
|
| 186 |
+
* Spatial Grid for efficient neighbor queries
|
| 187 |
+
*/
|
| 188 |
+
class SpatialGrid {
|
| 189 |
+
constructor(width, height, cellSize) {
|
| 190 |
+
this.width = width;
|
| 191 |
+
this.height = height;
|
| 192 |
+
this.cellSize = cellSize;
|
| 193 |
+
this.grid = [];
|
| 194 |
+
|
| 195 |
+
// Initialize grid
|
| 196 |
+
const cols = Math.ceil(width / cellSize);
|
| 197 |
+
const rows = Math.ceil(height / cellSize);
|
| 198 |
+
for (let i = 0; i < cols * rows; i++) {
|
| 199 |
+
this.grid.push([]);
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/**
|
| 204 |
+
* Get cell index for a position
|
| 205 |
+
* @param {Array} position - Position to get cell for [x, y, z]
|
| 206 |
+
* @returns {number} Cell index
|
| 207 |
+
*/
|
| 208 |
+
getCellIndex(position) {
|
| 209 |
+
const col = Math.floor(position[0] / this.cellSize);
|
| 210 |
+
const row = Math.floor(position[2] / this.cellSize);
|
| 211 |
+
return row * Math.ceil(this.width / this.cellSize) + col;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
/**
|
| 215 |
+
* Insert an object into the grid
|
| 216 |
+
* @param {Object} obj - Object to insert
|
| 217 |
+
*/
|
| 218 |
+
insert(obj) {
|
| 219 |
+
const index = this.getCellIndex(obj.position);
|
| 220 |
+
if (index >= 0 && index < this.grid.length) {
|
| 221 |
+
this.grid[index].push(obj);
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
/**
|
| 226 |
+
* Clear the grid
|
| 227 |
+
*/
|
| 228 |
+
clear() {
|
| 229 |
+
for (let i = 0; i < this.grid.length; i++) {
|
| 230 |
+
this.grid[i] = [];
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/**
|
| 235 |
+
* Query objects within a radius
|
| 236 |
+
* @param {Array} position - Center position [x, y, z]
|
| 237 |
+
* @param {number} radius - Query radius
|
| 238 |
+
* @returns {Array<Object>} Objects within radius
|
| 239 |
+
*/
|
| 240 |
+
query(position, radius) {
|
| 241 |
+
const results = [];
|
| 242 |
+
const col = Math.floor(position[0] / this.cellSize);
|
| 243 |
+
const row = Math.floor(position[2] / this.cellSize);
|
| 244 |
+
const radiusCells = Math.ceil(radius / this.cellSize);
|
| 245 |
+
|
| 246 |
+
const cols = Math.ceil(this.width / this.cellSize);
|
| 247 |
+
const rows = Math.ceil(this.height / this.cellSize);
|
| 248 |
+
|
| 249 |
+
// Check surrounding cells
|
| 250 |
+
for (let r = Math.max(0, row - radiusCells); r <= Math.min(rows - 1, row + radiusCells); r++) {
|
| 251 |
+
for (let c = Math.max(0, col - radiusCells); c <= Math.min(cols - 1, col + radiusCells); c++) {
|
| 252 |
+
const index = r * cols + c;
|
| 253 |
+
if (index >= 0 && index < this.grid.length) {
|
| 254 |
+
results.push(...this.grid[index]);
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
return results;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
export default CrowdManager;
|
src/ai/environment.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Villager } from './routines.js';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Environmental Interaction System for Villager AI
|
| 5 |
+
*
|
| 6 |
+
* This implementation handles how villagers interact with their environment,
|
| 7 |
+
* including resource gathering and building usage.
|
| 8 |
+
*/
|
| 9 |
+
class EnvironmentInteractionSystem {
|
| 10 |
+
constructor() {
|
| 11 |
+
this.resources = new Map(); // Map of resource IDs to resource objects
|
| 12 |
+
this.buildings = new Map(); // Map of building IDs to building objects
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Add a resource to the environment
|
| 17 |
+
* @param {string} id - Resource identifier
|
| 18 |
+
* @param {Resource} resource - Resource object
|
| 19 |
+
*/
|
| 20 |
+
addResource(id, resource) {
|
| 21 |
+
this.resources.set(id, resource);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Add a building to the environment
|
| 26 |
+
* @param {string} id - Building identifier
|
| 27 |
+
* @param {Building} building - Building object
|
| 28 |
+
*/
|
| 29 |
+
addBuilding(id, building) {
|
| 30 |
+
this.buildings.set(id, building);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Find the nearest resource of a specific type
|
| 35 |
+
* @param {Array} position - Search position [x, y, z]
|
| 36 |
+
* @param {string} type - Resource type
|
| 37 |
+
* @returns {Resource|null} Nearest resource or null if none found
|
| 38 |
+
*/
|
| 39 |
+
findNearestResource(position, type) {
|
| 40 |
+
let nearest = null;
|
| 41 |
+
let minDistance = Infinity;
|
| 42 |
+
|
| 43 |
+
for (const [id, resource] of this.resources) {
|
| 44 |
+
if (resource.type === type && resource.amount > 0) {
|
| 45 |
+
const distance = this.calculateDistance(position, resource.position);
|
| 46 |
+
if (distance < minDistance) {
|
| 47 |
+
minDistance = distance;
|
| 48 |
+
nearest = resource;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return nearest;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Find the nearest building of a specific type
|
| 58 |
+
* @param {Array} position - Search position [x, y, z]
|
| 59 |
+
* @param {string} type - Building type
|
| 60 |
+
* @returns {Building|null} Nearest building or null if none found
|
| 61 |
+
*/
|
| 62 |
+
findNearestBuilding(position, type) {
|
| 63 |
+
let nearest = null;
|
| 64 |
+
let minDistance = Infinity;
|
| 65 |
+
|
| 66 |
+
for (const [id, building] of this.buildings) {
|
| 67 |
+
if (building.type === type) {
|
| 68 |
+
const distance = this.calculateDistance(position, building.position);
|
| 69 |
+
if (distance < minDistance) {
|
| 70 |
+
minDistance = distance;
|
| 71 |
+
nearest = building;
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
return nearest;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Calculate distance between two points
|
| 81 |
+
* @param {Array} pos1 - First position [x, y, z]
|
| 82 |
+
* @param {Array} pos2 - Second position [x, y, z]
|
| 83 |
+
* @returns {number} Distance between points
|
| 84 |
+
*/
|
| 85 |
+
calculateDistance(pos1, pos2) {
|
| 86 |
+
return Math.sqrt(
|
| 87 |
+
Math.pow(pos1[0] - pos2[0], 2) +
|
| 88 |
+
Math.pow(pos1[1] - pos2[1], 2) +
|
| 89 |
+
Math.pow(pos1[2] - pos2[2], 2)
|
| 90 |
+
);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Gather a resource
|
| 95 |
+
* @param {Villager} villager - Villager gathering the resource
|
| 96 |
+
* @param {Resource} resource - Resource to gather
|
| 97 |
+
* @returns {boolean} True if successful
|
| 98 |
+
*/
|
| 99 |
+
gatherResource(villager, resource) {
|
| 100 |
+
if (!resource || resource.amount <= 0) return false;
|
| 101 |
+
|
| 102 |
+
// Check if villager is close enough to gather
|
| 103 |
+
if (this.calculateDistance(villager.position, resource.position) > resource.gatherRadius) {
|
| 104 |
+
return false;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Gather the resource
|
| 108 |
+
const amount = Math.min(resource.gatherAmount, resource.amount);
|
| 109 |
+
resource.amount -= amount;
|
| 110 |
+
villager.inventory.add(resource.type, amount);
|
| 111 |
+
|
| 112 |
+
return true;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Use a building
|
| 117 |
+
* @param {Villager} villager - Villager using the building
|
| 118 |
+
* @param {Building} building - Building to use
|
| 119 |
+
* @returns {boolean} True if successful
|
| 120 |
+
*/
|
| 121 |
+
useBuilding(villager, building) {
|
| 122 |
+
if (!building) return false;
|
| 123 |
+
|
| 124 |
+
// Check if villager is close enough to use
|
| 125 |
+
if (this.calculateDistance(villager.position, building.position) > building.useRadius) {
|
| 126 |
+
return false;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Use the building
|
| 130 |
+
return building.use(villager);
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/**
|
| 135 |
+
* Resource class
|
| 136 |
+
*/
|
| 137 |
+
class Resource {
|
| 138 |
+
constructor(id, type, position, amount, gatherRadius = 2, gatherAmount = 1) {
|
| 139 |
+
this.id = id;
|
| 140 |
+
this.type = type; // 'wood', 'stone', 'food', etc.
|
| 141 |
+
this.position = position;
|
| 142 |
+
this.amount = amount;
|
| 143 |
+
this.gatherRadius = gatherRadius;
|
| 144 |
+
this.gatherAmount = gatherAmount;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/**
|
| 149 |
+
* Building class
|
| 150 |
+
*/
|
| 151 |
+
class Building {
|
| 152 |
+
constructor(id, type, position, useRadius = 3) {
|
| 153 |
+
this.id = id;
|
| 154 |
+
this.type = type; // 'house', 'workshop', 'market', etc.
|
| 155 |
+
this.position = position;
|
| 156 |
+
this.useRadius = useRadius;
|
| 157 |
+
this.occupancy = 0;
|
| 158 |
+
this.maxOccupancy = 1;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Use the building
|
| 163 |
+
* @param {Villager} villager - Villager using the building
|
| 164 |
+
* @returns {boolean} True if successful
|
| 165 |
+
*/
|
| 166 |
+
use(villager) {
|
| 167 |
+
if (this.occupancy >= this.maxOccupancy) return false;
|
| 168 |
+
|
| 169 |
+
this.occupancy++;
|
| 170 |
+
// Simulate building usage
|
| 171 |
+
setTimeout(() => {
|
| 172 |
+
this.occupancy--;
|
| 173 |
+
}, 5000); // Occupied for 5 seconds
|
| 174 |
+
|
| 175 |
+
return true;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* Villager inventory system
|
| 181 |
+
*/
|
| 182 |
+
class Inventory {
|
| 183 |
+
constructor() {
|
| 184 |
+
this.items = new Map(); // Map of item types to quantities
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/**
|
| 188 |
+
* Add items to inventory
|
| 189 |
+
* @param {string} type - Item type
|
| 190 |
+
* @param {number} amount - Amount to add
|
| 191 |
+
*/
|
| 192 |
+
add(type, amount) {
|
| 193 |
+
if (this.items.has(type)) {
|
| 194 |
+
this.items.set(type, this.items.get(type) + amount);
|
| 195 |
+
} else {
|
| 196 |
+
this.items.set(type, amount);
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* Remove items from inventory
|
| 202 |
+
* @param {string} type - Item type
|
| 203 |
+
* @param {number} amount - Amount to remove
|
| 204 |
+
* @returns {boolean} True if successful
|
| 205 |
+
*/
|
| 206 |
+
remove(type, amount) {
|
| 207 |
+
if (!this.items.has(type)) return false;
|
| 208 |
+
|
| 209 |
+
const current = this.items.get(type);
|
| 210 |
+
if (current < amount) return false;
|
| 211 |
+
|
| 212 |
+
this.items.set(type, current - amount);
|
| 213 |
+
if (this.items.get(type) === 0) {
|
| 214 |
+
this.items.delete(type);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
return true;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/**
|
| 221 |
+
* Get item count
|
| 222 |
+
* @param {string} type - Item type
|
| 223 |
+
* @returns {number} Item count
|
| 224 |
+
*/
|
| 225 |
+
getCount(type) {
|
| 226 |
+
return this.items.get(type) || 0;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// Add inventory to Villager class
|
| 231 |
+
Villager.prototype.inventory = new Inventory();
|
| 232 |
+
|
| 233 |
+
export { EnvironmentInteractionSystem, Resource, Building, Inventory, Villager };
|
src/ai/llmHandler.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// llmHandler.js - Module for handling LLM interactions
|
| 2 |
+
class LLMHandler {
|
| 3 |
+
constructor(apiToken = null) {
|
| 4 |
+
// List of free Hugging Face models
|
| 5 |
+
this.freeLLMs = [
|
| 6 |
+
"meta-llama/Llama-3.1-8B-Instruct",
|
| 7 |
+
"google/gemma-3-270m-it",
|
| 8 |
+
"google/gemma-3-4b-it",
|
| 9 |
+
"google/gemma-3-27b-it",
|
| 10 |
+
"Qwen/Qwen3-4B-Instruct-2507",
|
| 11 |
+
"Qwen/Qwen3-8B",
|
| 12 |
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
| 13 |
+
"HuggingFaceH4/zephyr-7b-beta",
|
| 14 |
+
"TinyLlama/TinyLlama-1.1B-Chat-v1.0",
|
| 15 |
+
"microsoft/Phi-3-mini-4k-instruct",
|
| 16 |
+
"stabilityai/stablelm-2-1_6b",
|
| 17 |
+
"NousResearch/Hermes-2-Pro-Llama-3-8B",
|
| 18 |
+
"CohereForAI/c4ai-command-r-v01",
|
| 19 |
+
"nvidia/Nemotron-Research-Reasoning-Qwen-1.5B",
|
| 20 |
+
"inclusionAI/AReaL-boba-2-8B"
|
| 21 |
+
];
|
| 22 |
+
|
| 23 |
+
// Default selected model
|
| 24 |
+
this.selectedModel = this.freeLLMs[0];
|
| 25 |
+
|
| 26 |
+
// Hugging Face Inference API endpoint
|
| 27 |
+
this.apiEndpoint = "https://api-inference.huggingface.co/models/";
|
| 28 |
+
|
| 29 |
+
// API token (can be set in constructor or externally)
|
| 30 |
+
this.apiToken = apiToken;
|
| 31 |
+
|
| 32 |
+
// Flag to indicate if we should use simulated responses
|
| 33 |
+
// If we have a token, we'll use real responses by default
|
| 34 |
+
this.useSimulatedResponses = !apiToken;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Set the API token for Hugging Face Inference API
|
| 39 |
+
* @param {string} token - The API token
|
| 40 |
+
*/
|
| 41 |
+
setApiToken(token) {
|
| 42 |
+
this.apiToken = token;
|
| 43 |
+
// When a real API token is set, disable simulated responses
|
| 44 |
+
this.useSimulatedResponses = !token;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Set whether to use simulated responses
|
| 49 |
+
* @param {boolean} useSimulated - Whether to use simulated responses
|
| 50 |
+
*/
|
| 51 |
+
setUseSimulatedResponses(useSimulated) {
|
| 52 |
+
this.useSimulatedResponses = useSimulated;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Set the selected model
|
| 57 |
+
* @param {string} model - The model identifier
|
| 58 |
+
*/
|
| 59 |
+
setSelectedModel(model) {
|
| 60 |
+
if (this.freeLLMs.includes(model)) {
|
| 61 |
+
this.selectedModel = model;
|
| 62 |
+
} else {
|
| 63 |
+
console.warn(`Model ${model} is not in the list of free LLMs`);
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Send a query to the selected LLM
|
| 69 |
+
* @param {string} query - The query to send to the LLM
|
| 70 |
+
* @returns {Promise<string>} - The response from the LLM
|
| 71 |
+
*/
|
| 72 |
+
async sendQuery(query) {
|
| 73 |
+
// If we don't have an API token, throw an error
|
| 74 |
+
if (!this.apiToken) {
|
| 75 |
+
throw new Error("API token is not set. Please set your Hugging Face API token to use the LLM functionality.");
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// If we're using simulated responses, return a simulated response
|
| 79 |
+
if (this.useSimulatedResponses) {
|
| 80 |
+
console.log("Using simulated response for query:", query);
|
| 81 |
+
// Simulate API delay
|
| 82 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 83 |
+
return this.simulateLLMResponse(query);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Prepare the API request
|
| 87 |
+
const url = this.apiEndpoint + this.selectedModel;
|
| 88 |
+
const payload = {
|
| 89 |
+
inputs: query,
|
| 90 |
+
parameters: {
|
| 91 |
+
max_new_tokens: 200,
|
| 92 |
+
temperature: 0.7,
|
| 93 |
+
top_p: 0.9,
|
| 94 |
+
do_sample: true
|
| 95 |
+
}
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
// Make the API request
|
| 99 |
+
try {
|
| 100 |
+
const response = await fetch(url, {
|
| 101 |
+
method: 'POST',
|
| 102 |
+
headers: {
|
| 103 |
+
'Authorization': `Bearer ${this.apiToken}`,
|
| 104 |
+
'Content-Type': 'application/json'
|
| 105 |
+
},
|
| 106 |
+
body: JSON.stringify(payload)
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
// Check if the request was successful
|
| 110 |
+
if (!response.ok) {
|
| 111 |
+
const errorData = await response.json();
|
| 112 |
+
throw new Error(`API request failed with status ${response.status}: ${errorData.error || 'Unknown error'}`);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Parse the response
|
| 116 |
+
const data = await response.json();
|
| 117 |
+
|
| 118 |
+
// Extract the generated text from the response
|
| 119 |
+
if (Array.isArray(data) && data.length > 0 && data[0].generated_text) {
|
| 120 |
+
return data[0].generated_text;
|
| 121 |
+
} else if (data.generated_text) {
|
| 122 |
+
return data.generated_text;
|
| 123 |
+
} else {
|
| 124 |
+
throw new Error("Unexpected response format from the API");
|
| 125 |
+
}
|
| 126 |
+
} catch (error) {
|
| 127 |
+
console.error("Error sending query to LLM:", error);
|
| 128 |
+
throw error;
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* Simulate an LLM response (for testing purposes)
|
| 134 |
+
* @param {string} query - The query to simulate a response for
|
| 135 |
+
* @returns {string} - A simulated response
|
| 136 |
+
*/
|
| 137 |
+
simulateLLMResponse(query) {
|
| 138 |
+
const lowerQuery = query.toLowerCase();
|
| 139 |
+
|
| 140 |
+
if (lowerQuery.includes("villager") && lowerQuery.includes("behavior")) {
|
| 141 |
+
return "Villagers in the medieval village simulation exhibit complex behaviors based on their needs and the time of day. They cycle through states like sleeping, working, eating, and socializing. Their decisions are influenced by factors such as energy levels, hunger, and social needs.";
|
| 142 |
+
} else if (lowerQuery.includes("resource") && lowerQuery.includes("management")) {
|
| 143 |
+
return "Resource management in the village is critical for sustainability. Villagers collect resources like wood and stone from designated areas. Proper allocation of resources to buildings and villagers ensures the village's growth and resilience against disasters.";
|
| 144 |
+
} else if (lowerQuery.includes("disaster") || lowerQuery.includes("emergency")) {
|
| 145 |
+
return "The village simulation includes various disasters like fires, floods, and plagues. These events test the village's resilience and require strategic planning to mitigate their effects. Warriors can be dispatched to help protect the village from certain threats.";
|
| 146 |
+
} else if (lowerQuery.includes("ai") || lowerQuery.includes("artificial intelligence")) {
|
| 147 |
+
return "This simulation uses several AI techniques including finite state machines for villager behavior, pathfinding algorithms for navigation, and rule-based systems for decision making. The emergent behaviors arise from the interaction of these systems.";
|
| 148 |
+
} else if (lowerQuery.includes("building") || lowerQuery.includes("structure")) {
|
| 149 |
+
return "The village features various building types, each with unique functions: houses for living, workshops for crafting, markets for trading, and specialized buildings like universities and hospitals. Buildings are placed strategically to optimize villager workflows.";
|
| 150 |
+
} else {
|
| 151 |
+
return `I've received your query about "${query}". In a full implementation with API access, I would provide a detailed response based on the selected LLM model. For now, try asking about villagers, resources, disasters, AI systems, or buildings in the village.`;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/**
|
| 156 |
+
* Get the list of free LLMs
|
| 157 |
+
* @returns {string[]} - Array of free LLM model identifiers
|
| 158 |
+
*/
|
| 159 |
+
getFreeLLMs() {
|
| 160 |
+
return this.freeLLMs;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/**
|
| 164 |
+
* Get the currently selected model
|
| 165 |
+
* @returns {string} - The currently selected model identifier
|
| 166 |
+
*/
|
| 167 |
+
getSelectedModel() {
|
| 168 |
+
return this.selectedModel;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/**
|
| 172 |
+
* Check if the API token is set
|
| 173 |
+
* @returns {boolean} - Whether the API token is set
|
| 174 |
+
*/
|
| 175 |
+
isApiTokenSet() {
|
| 176 |
+
return !!this.apiToken;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* Get the API token (for debugging purposes)
|
| 181 |
+
* @returns {string|null} - The API token or null if not set
|
| 182 |
+
*/
|
| 183 |
+
getApiToken() {
|
| 184 |
+
return this.apiToken;
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// Export the LLMHandler class
|
| 189 |
+
export default LLMHandler;
|
src/ai/main.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import VillagePathfinder from './pathfinding.js';
|
| 2 |
+
import { VillagerBehaviorTree } from './behavior.js';
|
| 3 |
+
import CrowdManager from './crowd.js';
|
| 4 |
+
import { DailyRoutineManager, Villager } from './routines.js';
|
| 5 |
+
import { EnvironmentInteractionSystem, Resource, Building } from './environment.js';
|
| 6 |
+
import { AIEntityManager } from './optimization.js';
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Main AI System for Medieval Village Simulator
|
| 10 |
+
*
|
| 11 |
+
* This file integrates all AI components into a cohesive system for managing
|
| 12 |
+
* villager AI in a medieval town simulator.
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
class VillageAISystem {
|
| 16 |
+
constructor(scene) {
|
| 17 |
+
this.scene = scene;
|
| 18 |
+
this.pathfinder = new VillagePathfinder();
|
| 19 |
+
this.crowdManager = new CrowdManager();
|
| 20 |
+
this.routineManager = new DailyRoutineManager();
|
| 21 |
+
this.environmentSystem = new EnvironmentInteractionSystem();
|
| 22 |
+
this.entityManager = new AIEntityManager();
|
| 23 |
+
|
| 24 |
+
// Initialize systems
|
| 25 |
+
this.initSystems();
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Initialize all AI systems
|
| 30 |
+
*/
|
| 31 |
+
initSystems() {
|
| 32 |
+
// Initialize pathfinding with a simple plane for now
|
| 33 |
+
// In a real implementation, this would use a navigation mesh
|
| 34 |
+
// Note: We're not using THREE.js directly in the AI system anymore
|
| 35 |
+
// The nav mesh is just a placeholder object
|
| 36 |
+
this.pathfinder.initNavMesh({});
|
| 37 |
+
|
| 38 |
+
// Create some sample resources
|
| 39 |
+
this.createSampleResources();
|
| 40 |
+
|
| 41 |
+
// Create some sample buildings
|
| 42 |
+
this.createSampleBuildings();
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Create sample resources for the village
|
| 47 |
+
*/
|
| 48 |
+
createSampleResources() {
|
| 49 |
+
const woodResource = new Resource('wood1', 'wood', [10, 0, 10], 100);
|
| 50 |
+
const stoneResource = new Resource('stone1', 'stone', [-10, 0, -10], 100);
|
| 51 |
+
const foodResource = new Resource('food1', 'food', [0, 0, 15], 100);
|
| 52 |
+
|
| 53 |
+
this.environmentSystem.addResource('wood1', woodResource);
|
| 54 |
+
this.environmentSystem.addResource('stone1', stoneResource);
|
| 55 |
+
this.environmentSystem.addResource('food1', foodResource);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Create sample buildings for the village
|
| 60 |
+
*/
|
| 61 |
+
createSampleBuildings() {
|
| 62 |
+
// Create a proper square village layout with buildings around a central square
|
| 63 |
+
const squareSize = 20; // Size of the central square
|
| 64 |
+
const buildingSpacing = 8; // Distance between buildings
|
| 65 |
+
let buildingId = 1;
|
| 66 |
+
|
| 67 |
+
// Define building types and their positions around the square
|
| 68 |
+
const buildingConfigs = [
|
| 69 |
+
{ type: 'university', x: -squareSize, z: -squareSize, maxOccupancy: 8, useRadius: 6 },
|
| 70 |
+
{ type: 'store', x: squareSize, z: -squareSize, maxOccupancy: 4, useRadius: 4 },
|
| 71 |
+
{ type: 'bank', x: -squareSize, z: squareSize, maxOccupancy: 3, useRadius: 4 },
|
| 72 |
+
{ type: 'hospital', x: squareSize, z: squareSize, maxOccupancy: 5, useRadius: 5 },
|
| 73 |
+
{ type: 'market', x: 0, z: -squareSize - buildingSpacing, maxOccupancy: 10, useRadius: 8 },
|
| 74 |
+
{ type: 'restaurant', x: 0, z: squareSize + buildingSpacing, maxOccupancy: 6, useRadius: 5 },
|
| 75 |
+
{ type: 'workshop', x: -squareSize - buildingSpacing, z: 0, maxOccupancy: 4, useRadius: 4 },
|
| 76 |
+
{ type: 'house', x: squareSize + buildingSpacing, z: 0, maxOccupancy: 3, useRadius: 3 }
|
| 77 |
+
];
|
| 78 |
+
|
| 79 |
+
// Add buildings around the square
|
| 80 |
+
buildingConfigs.forEach(config => {
|
| 81 |
+
const building = new Building(`building${buildingId}`, config.type, [config.x, 0, config.z]);
|
| 82 |
+
building.maxOccupancy = config.maxOccupancy;
|
| 83 |
+
building.useRadius = config.useRadius;
|
| 84 |
+
this.environmentSystem.addBuilding(`building${buildingId}`, building);
|
| 85 |
+
buildingId++;
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
// Add houses around the perimeter
|
| 89 |
+
const housePositions = [
|
| 90 |
+
[-squareSize, 0, -squareSize + buildingSpacing],
|
| 91 |
+
[-squareSize + buildingSpacing, 0, -squareSize],
|
| 92 |
+
[squareSize - buildingSpacing, 0, -squareSize],
|
| 93 |
+
[squareSize, 0, -squareSize + buildingSpacing],
|
| 94 |
+
[-squareSize, 0, squareSize - buildingSpacing],
|
| 95 |
+
[-squareSize + buildingSpacing, 0, squareSize],
|
| 96 |
+
[squareSize - buildingSpacing, 0, squareSize],
|
| 97 |
+
[squareSize, 0, squareSize - buildingSpacing]
|
| 98 |
+
];
|
| 99 |
+
|
| 100 |
+
housePositions.forEach((pos, index) => {
|
| 101 |
+
const house = new Building(`house${index + 1}`, 'house', pos);
|
| 102 |
+
house.maxOccupancy = 3;
|
| 103 |
+
house.useRadius = 3;
|
| 104 |
+
this.environmentSystem.addBuilding(`house${index + 1}`, house);
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
// Add some resources scattered around
|
| 108 |
+
const resources = [
|
| 109 |
+
new Resource('wood1', 'wood', [-squareSize - 5, 0, -squareSize - 5], 100),
|
| 110 |
+
new Resource('wood2', 'wood', [squareSize + 5, 0, squareSize + 5], 100),
|
| 111 |
+
new Resource('stone1', 'stone', [-squareSize - 5, 0, squareSize + 5], 100),
|
| 112 |
+
new Resource('stone2', 'stone', [squareSize + 5, 0, -squareSize - 5], 100),
|
| 113 |
+
new Resource('food1', 'food', [0, 0, -squareSize - 10], 100),
|
| 114 |
+
new Resource('food2', 'food', [0, 0, squareSize + 10], 100)
|
| 115 |
+
];
|
| 116 |
+
|
| 117 |
+
resources.forEach(resource => {
|
| 118 |
+
this.environmentSystem.addResource(resource.id, resource);
|
| 119 |
+
});
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Create a villager and add it to the simulation
|
| 124 |
+
* @param {string} id - Villager ID
|
| 125 |
+
* @param {Array} position - Initial position [x, y, z]
|
| 126 |
+
* @returns {Villager} Created villager
|
| 127 |
+
*/
|
| 128 |
+
createVillager(id, position) {
|
| 129 |
+
// Convert THREE.Vector3 to array if needed
|
| 130 |
+
let posArray = position;
|
| 131 |
+
if (position && typeof position.x !== 'undefined') {
|
| 132 |
+
posArray = [position.x, position.y, position.z];
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const villager = new Villager(id, posArray);
|
| 136 |
+
|
| 137 |
+
// Add to systems
|
| 138 |
+
this.crowdManager.addVillager(villager);
|
| 139 |
+
this.entityManager.addEntity(villager);
|
| 140 |
+
|
| 141 |
+
// Create a schedule for the villager
|
| 142 |
+
const workBuilding = this.environmentSystem.findNearestBuilding(posArray, 'workshop');
|
| 143 |
+
const homeBuilding = this.environmentSystem.findNearestBuilding(posArray, 'house');
|
| 144 |
+
const marketBuilding = this.environmentSystem.findNearestBuilding(posArray, 'market');
|
| 145 |
+
|
| 146 |
+
const workLocation = workBuilding ? workBuilding.position : [0, 0, 0];
|
| 147 |
+
const homeLocation = homeBuilding ? homeBuilding.position : [0, 0, 0];
|
| 148 |
+
const socialLocations = marketBuilding ? [marketBuilding.position] : [[0, 0, 0]];
|
| 149 |
+
|
| 150 |
+
this.routineManager.createDefaultSchedule(id, workLocation, homeLocation, socialLocations);
|
| 151 |
+
|
| 152 |
+
return villager;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/**
|
| 156 |
+
* Update the AI system
|
| 157 |
+
* @param {number} deltaTime - Time since last frame
|
| 158 |
+
*/
|
| 159 |
+
update(deltaTime) {
|
| 160 |
+
// Update routine manager
|
| 161 |
+
this.routineManager.update(deltaTime);
|
| 162 |
+
|
| 163 |
+
// Update entity manager (includes optimization techniques)
|
| 164 |
+
this.entityManager.update(deltaTime);
|
| 165 |
+
|
| 166 |
+
// Update crowd manager
|
| 167 |
+
this.crowdManager.update(deltaTime);
|
| 168 |
+
|
| 169 |
+
// Update each villager
|
| 170 |
+
for (const villager of this.crowdManager.villagers) {
|
| 171 |
+
// Update villager with routine manager and pathfinder
|
| 172 |
+
villager.update(this.routineManager, this.pathfinder, deltaTime);
|
| 173 |
+
|
| 174 |
+
// Handle environmental interactions
|
| 175 |
+
if (villager.state === 'work' && villager.path.length === 0) {
|
| 176 |
+
// Try to gather resources
|
| 177 |
+
const nearestWood = this.environmentSystem.findNearestResource(villager.position, 'wood');
|
| 178 |
+
if (nearestWood && this.environmentSystem.calculateDistance(villager.position, nearestWood.position) <= nearestWood.gatherRadius) {
|
| 179 |
+
this.environmentSystem.gatherResource(villager, nearestWood);
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
if (villager.state === 'socialize' && villager.path.length === 0) {
|
| 184 |
+
// Try to use market
|
| 185 |
+
const market = this.environmentSystem.findNearestBuilding(villager.position, 'market');
|
| 186 |
+
if (market) {
|
| 187 |
+
this.environmentSystem.useBuilding(villager, market);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Move along path if there is one
|
| 192 |
+
if (villager.path.length > 0) {
|
| 193 |
+
this.pathfinder.moveAlongPath(villager, villager.path, villager.maxSpeed, deltaTime);
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
export default VillageAISystem;
|
src/ai/optimization.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Performance Optimization Techniques for Multiple AI Entities
|
| 3 |
+
*
|
| 4 |
+
* This implementation includes various optimization techniques to efficiently
|
| 5 |
+
* manage multiple AI entities in a village simulation.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
class AIEntityManager {
|
| 9 |
+
constructor() {
|
| 10 |
+
this.entities = [];
|
| 11 |
+
this.activeEntities = new Set(); // Set of currently active entities
|
| 12 |
+
this.updateQueue = []; // Queue for entity updates
|
| 13 |
+
this.maxUpdatesPerFrame = 10; // Maximum entities to update per frame
|
| 14 |
+
this.spatialPartitioning = new SpatialPartitioning(100, 100, 10); // 100x100 grid with 10 unit cells
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Add an entity to the manager
|
| 19 |
+
* @param {Object} entity - Entity to add
|
| 20 |
+
*/
|
| 21 |
+
addEntity(entity) {
|
| 22 |
+
this.entities.push(entity);
|
| 23 |
+
this.spatialPartitioning.insert(entity);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Remove an entity from the manager
|
| 28 |
+
* @param {Object} entity - Entity to remove
|
| 29 |
+
*/
|
| 30 |
+
removeEntity(entity) {
|
| 31 |
+
const index = this.entities.indexOf(entity);
|
| 32 |
+
if (index !== -1) {
|
| 33 |
+
this.entities.splice(index, 1);
|
| 34 |
+
}
|
| 35 |
+
this.spatialPartitioning.remove(entity);
|
| 36 |
+
this.activeEntities.delete(entity);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Update all entities with optimization techniques
|
| 41 |
+
* @param {number} deltaTime - Time since last frame
|
| 42 |
+
*/
|
| 43 |
+
update(deltaTime) {
|
| 44 |
+
// Update spatial partitioning
|
| 45 |
+
this.spatialPartitioning.update();
|
| 46 |
+
|
| 47 |
+
// Reset active entities
|
| 48 |
+
this.activeEntities.clear();
|
| 49 |
+
|
| 50 |
+
// Determine active entities (those near the camera or player)
|
| 51 |
+
this.determineActiveEntities();
|
| 52 |
+
|
| 53 |
+
// Update entity queue
|
| 54 |
+
this.updateEntityQueue();
|
| 55 |
+
|
| 56 |
+
// Update entities
|
| 57 |
+
const updatesThisFrame = Math.min(this.maxUpdatesPerFrame, this.updateQueue.length);
|
| 58 |
+
for (let i = 0; i < updatesThisFrame; i++) {
|
| 59 |
+
const entity = this.updateQueue.shift();
|
| 60 |
+
if (entity) {
|
| 61 |
+
// Note: Entity updates are handled by the main system
|
| 62 |
+
// This is just for optimization management
|
| 63 |
+
this.updateQueue.push(entity); // Put back at end of queue
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/**
|
| 69 |
+
* Determine which entities are active based on proximity to camera
|
| 70 |
+
*/
|
| 71 |
+
determineActiveEntities() {
|
| 72 |
+
// In a real implementation, this would use the camera position
|
| 73 |
+
// For now, we'll mark all entities as active
|
| 74 |
+
for (const entity of this.entities) {
|
| 75 |
+
this.activeEntities.add(entity);
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Update the entity queue with active entities
|
| 81 |
+
*/
|
| 82 |
+
updateEntityQueue() {
|
| 83 |
+
// Add newly active entities to queue
|
| 84 |
+
for (const entity of this.activeEntities) {
|
| 85 |
+
if (!this.updateQueue.includes(entity)) {
|
| 86 |
+
this.updateQueue.push(entity);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Remove inactive entities from queue
|
| 91 |
+
this.updateQueue = this.updateQueue.filter(entity => this.activeEntities.has(entity));
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Get nearby entities for a position
|
| 96 |
+
* @param {Array} position - Position to check [x, y, z]
|
| 97 |
+
* @param {number} radius - Radius to check
|
| 98 |
+
* @returns {Array} Nearby entities
|
| 99 |
+
*/
|
| 100 |
+
getNearbyEntities(position, radius) {
|
| 101 |
+
return this.spatialPartitioning.query(position, radius);
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* Spatial Partitioning for efficient entity queries
|
| 107 |
+
*/
|
| 108 |
+
class SpatialPartitioning {
|
| 109 |
+
constructor(width, height, cellSize) {
|
| 110 |
+
this.width = width;
|
| 111 |
+
this.height = height;
|
| 112 |
+
this.cellSize = cellSize;
|
| 113 |
+
this.grid = new Map(); // Map of cell keys to arrays of entities
|
| 114 |
+
this.entityToCell = new Map(); // Map of entities to their current cell
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* Get cell key for a position
|
| 119 |
+
* @param {Array} position - Position to get cell for [x, y, z]
|
| 120 |
+
* @returns {string} Cell key
|
| 121 |
+
*/
|
| 122 |
+
getCellKey(position) {
|
| 123 |
+
const col = Math.floor(position[0] / this.cellSize);
|
| 124 |
+
const row = Math.floor(position[2] / this.cellSize);
|
| 125 |
+
return `${col},${row}`;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* Insert an entity into the spatial partitioning
|
| 130 |
+
* @param {Object} entity - Entity to insert
|
| 131 |
+
*/
|
| 132 |
+
insert(entity) {
|
| 133 |
+
const key = this.getCellKey(entity.position);
|
| 134 |
+
if (!this.grid.has(key)) {
|
| 135 |
+
this.grid.set(key, []);
|
| 136 |
+
}
|
| 137 |
+
this.grid.get(key).push(entity);
|
| 138 |
+
this.entityToCell.set(entity, key);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* Remove an entity from the spatial partitioning
|
| 143 |
+
* @param {Object} entity - Entity to remove
|
| 144 |
+
*/
|
| 145 |
+
remove(entity) {
|
| 146 |
+
const key = this.entityToCell.get(entity);
|
| 147 |
+
if (key && this.grid.has(key)) {
|
| 148 |
+
const cell = this.grid.get(key);
|
| 149 |
+
const index = cell.indexOf(entity);
|
| 150 |
+
if (index !== -1) {
|
| 151 |
+
cell.splice(index, 1);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
this.entityToCell.delete(entity);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Update an entity's position in the spatial partitioning
|
| 159 |
+
* @param {Object} entity - Entity to update
|
| 160 |
+
*/
|
| 161 |
+
updateEntity(entity) {
|
| 162 |
+
const oldKey = this.entityToCell.get(entity);
|
| 163 |
+
const newKey = this.getCellKey(entity.position);
|
| 164 |
+
|
| 165 |
+
if (oldKey !== newKey) {
|
| 166 |
+
// Remove from old cell
|
| 167 |
+
if (oldKey && this.grid.has(oldKey)) {
|
| 168 |
+
const oldCell = this.grid.get(oldKey);
|
| 169 |
+
const index = oldCell.indexOf(entity);
|
| 170 |
+
if (index !== -1) {
|
| 171 |
+
oldCell.splice(index, 1);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Add to new cell
|
| 176 |
+
if (!this.grid.has(newKey)) {
|
| 177 |
+
this.grid.set(newKey, []);
|
| 178 |
+
}
|
| 179 |
+
this.grid.get(newKey).push(entity);
|
| 180 |
+
this.entityToCell.set(entity, newKey);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/**
|
| 185 |
+
* Update all entities in the spatial partitioning
|
| 186 |
+
*/
|
| 187 |
+
update() {
|
| 188 |
+
for (const entity of this.entityToCell.keys()) {
|
| 189 |
+
this.updateEntity(entity);
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/**
|
| 194 |
+
* Query entities within a radius
|
| 195 |
+
* @param {Array} position - Center position [x, y, z]
|
| 196 |
+
* @param {number} radius - Query radius
|
| 197 |
+
* @returns {Array} Entities within radius
|
| 198 |
+
*/
|
| 199 |
+
query(position, radius) {
|
| 200 |
+
const results = [];
|
| 201 |
+
const col = Math.floor(position[0] / this.cellSize);
|
| 202 |
+
const row = Math.floor(position[2] / this.cellSize);
|
| 203 |
+
const radiusCells = Math.ceil(radius / this.cellSize);
|
| 204 |
+
|
| 205 |
+
// Check surrounding cells
|
| 206 |
+
for (let r = row - radiusCells; r <= row + radiusCells; r++) {
|
| 207 |
+
for (let c = col - radiusCells; c <= col + radiusCells; c++) {
|
| 208 |
+
const key = `${c},${r}`;
|
| 209 |
+
if (this.grid.has(key)) {
|
| 210 |
+
const cell = this.grid.get(key);
|
| 211 |
+
for (const entity of cell) {
|
| 212 |
+
if (this.calculateDistance(position, entity.position) <= radius) {
|
| 213 |
+
results.push(entity);
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
return results;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/**
|
| 224 |
+
* Calculate distance between two positions
|
| 225 |
+
* @param {Array} pos1 - First position [x, y, z]
|
| 226 |
+
* @param {Array} pos2 - Second position [x, y, z]
|
| 227 |
+
* @returns {number} Distance between positions
|
| 228 |
+
*/
|
| 229 |
+
calculateDistance(pos1, pos2) {
|
| 230 |
+
return Math.sqrt(
|
| 231 |
+
Math.pow(pos1[0] - pos2[0], 2) +
|
| 232 |
+
Math.pow(pos1[1] - pos2[1], 2) +
|
| 233 |
+
Math.pow(pos1[2] - pos2[2], 2)
|
| 234 |
+
);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/**
|
| 239 |
+
* Object pooling for AI entities to reduce garbage collection
|
| 240 |
+
*/
|
| 241 |
+
class ObjectPool {
|
| 242 |
+
constructor(createFn, resetFn, initialSize = 10) {
|
| 243 |
+
this.createFn = createFn;
|
| 244 |
+
this.resetFn = resetFn;
|
| 245 |
+
this.pool = [];
|
| 246 |
+
|
| 247 |
+
// Pre-populate pool
|
| 248 |
+
for (let i = 0; i < initialSize; i++) {
|
| 249 |
+
this.pool.push(this.createFn());
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/**
|
| 254 |
+
* Get an object from the pool
|
| 255 |
+
* @returns {Object} Object from pool or new object if pool is empty
|
| 256 |
+
*/
|
| 257 |
+
acquire() {
|
| 258 |
+
if (this.pool.length > 0) {
|
| 259 |
+
return this.pool.pop();
|
| 260 |
+
}
|
| 261 |
+
return this.createFn();
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/**
|
| 265 |
+
* Return an object to the pool
|
| 266 |
+
* @param {Object} object - Object to return
|
| 267 |
+
*/
|
| 268 |
+
release(object) {
|
| 269 |
+
this.resetFn(object);
|
| 270 |
+
this.pool.push(object);
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
export { AIEntityManager, SpatialPartitioning, ObjectPool };
|
src/ai/pathfinding.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Note: three-pathfinding is not available via CDN, using basic pathfinding instead
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* A* Pathfinding Implementation for Village Navigation
|
| 5 |
+
*
|
| 6 |
+
* This implementation uses a navigation mesh approach for efficient pathfinding
|
| 7 |
+
* in complex 3D environments like medieval villages.
|
| 8 |
+
*/
|
| 9 |
+
class VillagePathfinder {
|
| 10 |
+
constructor() {
|
| 11 |
+
// Simple pathfinding implementation without external library
|
| 12 |
+
this.zoneName = 'village';
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Initialize the navigation mesh
|
| 17 |
+
* @param {Object} navMeshGeometry - The navigation mesh geometry
|
| 18 |
+
*/
|
| 19 |
+
initNavMesh(navMeshGeometry) {
|
| 20 |
+
// Store the navigation mesh for future use
|
| 21 |
+
this.navMesh = navMeshGeometry;
|
| 22 |
+
console.log('Navigation mesh initialized');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Find a path between two points
|
| 27 |
+
* @param {Array} start - Start position [x, y, z]
|
| 28 |
+
* @param {Array} end - End position [x, y, z]
|
| 29 |
+
* @returns {Array<Array>} Path as array of points [[x, y, z], ...]
|
| 30 |
+
*/
|
| 31 |
+
findPath(start, end) {
|
| 32 |
+
// Simple direct path for now
|
| 33 |
+
// In a real implementation, this would use A* algorithm
|
| 34 |
+
return [
|
| 35 |
+
[start[0], start[1], start[2]],
|
| 36 |
+
[end[0], end[1], end[2]]
|
| 37 |
+
];
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Move an entity along a path
|
| 42 |
+
* @param {Object} entity - The entity to move
|
| 43 |
+
* @param {Array<Array>} path - Path to follow [[x, y, z], ...]
|
| 44 |
+
* @param {number} speed - Movement speed
|
| 45 |
+
* @param {number} deltaTime - Time since last frame
|
| 46 |
+
*/
|
| 47 |
+
moveAlongPath(entity, path, speed, deltaTime) {
|
| 48 |
+
if (path.length === 0) return;
|
| 49 |
+
|
| 50 |
+
const target = path[0];
|
| 51 |
+
const direction = this.normalize([
|
| 52 |
+
target[0] - entity.position[0],
|
| 53 |
+
target[1] - entity.position[1],
|
| 54 |
+
target[2] - entity.position[2]
|
| 55 |
+
]);
|
| 56 |
+
const moveDistance = speed * deltaTime;
|
| 57 |
+
|
| 58 |
+
// Move towards the target
|
| 59 |
+
entity.position[0] += direction[0] * moveDistance;
|
| 60 |
+
entity.position[1] += direction[1] * moveDistance;
|
| 61 |
+
entity.position[2] += direction[2] * moveDistance;
|
| 62 |
+
|
| 63 |
+
// Check if we've reached the target point
|
| 64 |
+
const distance = Math.sqrt(
|
| 65 |
+
Math.pow(entity.position[0] - target[0], 2) +
|
| 66 |
+
Math.pow(entity.position[1] - target[1], 2) +
|
| 67 |
+
Math.pow(entity.position[2] - target[2], 2)
|
| 68 |
+
);
|
| 69 |
+
|
| 70 |
+
if (distance < 0.1) {
|
| 71 |
+
path.shift(); // Remove the reached point
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/**
|
| 76 |
+
* Normalize a vector
|
| 77 |
+
* @param {Array} vector - Vector to normalize [x, y, z]
|
| 78 |
+
* @returns {Array} Normalized vector [x, y, z]
|
| 79 |
+
*/
|
| 80 |
+
normalize(vector) {
|
| 81 |
+
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
|
| 82 |
+
if (length === 0) return [0, 0, 0];
|
| 83 |
+
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
export default VillagePathfinder;
|
src/ai/routines.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// No direct THREE.js import needed
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Daily Routine System for Villager AI
|
| 5 |
+
*
|
| 6 |
+
* This implementation manages villager daily routines including work, rest, and social interactions.
|
| 7 |
+
* It uses a schedule-based approach with some randomness to create natural-looking behavior.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
class DailyRoutineManager {
|
| 11 |
+
constructor() {
|
| 12 |
+
this.schedules = new Map(); // Map of villager IDs to schedules
|
| 13 |
+
this.currentTime = 0; // In-game time in hours (0-24)
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Set a schedule for a villager
|
| 18 |
+
* @param {string} villagerId - Villager identifier
|
| 19 |
+
* @param {Array} schedule - Array of schedule entries
|
| 20 |
+
*/
|
| 21 |
+
setSchedule(villagerId, schedule) {
|
| 22 |
+
this.schedules.set(villagerId, schedule);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Update the routine manager
|
| 27 |
+
* @param {number} deltaTime - Time since last update in hours
|
| 28 |
+
*/
|
| 29 |
+
update(deltaTime) {
|
| 30 |
+
this.currentTime += deltaTime;
|
| 31 |
+
if (this.currentTime >= 24) {
|
| 32 |
+
this.currentTime -= 24; // Wrap around to next day
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Get current activity for a villager
|
| 38 |
+
* @param {string} villagerId - Villager identifier
|
| 39 |
+
* @returns {Object} Current activity
|
| 40 |
+
*/
|
| 41 |
+
getCurrentActivity(villagerId) {
|
| 42 |
+
const schedule = this.schedules.get(villagerId);
|
| 43 |
+
if (!schedule) return null;
|
| 44 |
+
|
| 45 |
+
// Find the current activity based on time
|
| 46 |
+
for (let i = schedule.length - 1; i >= 0; i--) {
|
| 47 |
+
if (this.currentTime >= schedule[i].startTime) {
|
| 48 |
+
return schedule[i];
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// If no activity found, return the last one (for times before first activity)
|
| 53 |
+
return schedule[schedule.length - 1];
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Create a default schedule for a villager
|
| 58 |
+
* @param {string} villagerId - Villager identifier
|
| 59 |
+
* @param {Object} workLocation - Work location
|
| 60 |
+
* @param {Object} homeLocation - Home location
|
| 61 |
+
* @param {Array} socialLocations - Social locations
|
| 62 |
+
*/
|
| 63 |
+
createDefaultSchedule(villagerId, workLocation, homeLocation, socialLocations) {
|
| 64 |
+
const schedule = [
|
| 65 |
+
{ startTime: 0, activity: 'sleep', location: homeLocation },
|
| 66 |
+
{ startTime: 7, activity: 'eat', location: homeLocation },
|
| 67 |
+
{ startTime: 8, activity: 'work', location: workLocation },
|
| 68 |
+
{ startTime: 12, activity: 'eat', location: workLocation },
|
| 69 |
+
{ startTime: 13, activity: 'work', location: workLocation },
|
| 70 |
+
{ startTime: 17, activity: 'socialize', location: socialLocations[0] },
|
| 71 |
+
{ startTime: 19, activity: 'eat', location: homeLocation },
|
| 72 |
+
{ startTime: 20, activity: 'sleep', location: homeLocation }
|
| 73 |
+
];
|
| 74 |
+
|
| 75 |
+
this.setSchedule(villagerId, schedule);
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Villager class with routine-based behavior
|
| 81 |
+
*/
|
| 82 |
+
class Villager {
|
| 83 |
+
constructor(id, position) {
|
| 84 |
+
this.id = id;
|
| 85 |
+
this.position = position;
|
| 86 |
+
this.velocity = [0, 0, 0]; // Simple array instead of THREE.Vector3
|
| 87 |
+
this.maxSpeed = 2;
|
| 88 |
+
this.energy = 100;
|
| 89 |
+
this.hunger = 0;
|
| 90 |
+
this.socialNeed = 0;
|
| 91 |
+
this.state = 'idle'; // idle, working, sleeping, eating, socializing
|
| 92 |
+
this.path = [];
|
| 93 |
+
this.avoidanceRadius = 2;
|
| 94 |
+
this.avoidanceWeight = 1.5;
|
| 95 |
+
this.steeringWeight = 1.0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Calculate distance between two points
|
| 100 |
+
* @param {Array} pos1 - First position [x, y, z]
|
| 101 |
+
* @param {Array} pos2 - Second position [x, y, z]
|
| 102 |
+
* @returns {number} Distance between points
|
| 103 |
+
*/
|
| 104 |
+
calculateDistance(pos1, pos2) {
|
| 105 |
+
return Math.sqrt(
|
| 106 |
+
Math.pow(pos1[0] - pos2[0], 2) +
|
| 107 |
+
Math.pow(pos1[1] - pos2[1], 2) +
|
| 108 |
+
Math.pow(pos1[2] - pos2[2], 2)
|
| 109 |
+
);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Update villager state
|
| 114 |
+
* @param {DailyRoutineManager} routineManager - Routine manager
|
| 115 |
+
* @param {VillagePathfinder} pathfinder - Pathfinding system
|
| 116 |
+
* @param {number} deltaTime - Time since last update
|
| 117 |
+
*/
|
| 118 |
+
update(routineManager, pathfinder, deltaTime) {
|
| 119 |
+
// Update needs
|
| 120 |
+
this.updateNeeds(deltaTime);
|
| 121 |
+
|
| 122 |
+
// Get current activity
|
| 123 |
+
const activity = routineManager.getCurrentActivity(this.id);
|
| 124 |
+
|
| 125 |
+
// Update state based on activity
|
| 126 |
+
if (activity) {
|
| 127 |
+
this.state = activity.activity;
|
| 128 |
+
|
| 129 |
+
// If we have a location for this activity, pathfind to it
|
| 130 |
+
if (activity.location && this.calculateDistance(this.position, activity.location) > 1) {
|
| 131 |
+
this.path = pathfinder.findPath(this.position, activity.location);
|
| 132 |
+
} else {
|
| 133 |
+
this.path = [];
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Perform state-specific actions
|
| 138 |
+
switch (this.state) {
|
| 139 |
+
case 'sleep':
|
| 140 |
+
this.sleep(deltaTime);
|
| 141 |
+
break;
|
| 142 |
+
case 'work':
|
| 143 |
+
this.work(deltaTime);
|
| 144 |
+
break;
|
| 145 |
+
case 'eat':
|
| 146 |
+
this.eat(deltaTime);
|
| 147 |
+
break;
|
| 148 |
+
case 'socialize':
|
| 149 |
+
this.socialize(deltaTime);
|
| 150 |
+
break;
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/**
|
| 155 |
+
* Update villager needs over time
|
| 156 |
+
* @param {number} deltaTime - Time since last update
|
| 157 |
+
*/
|
| 158 |
+
updateNeeds(deltaTime) {
|
| 159 |
+
this.energy = Math.max(0, this.energy - 0.1 * deltaTime);
|
| 160 |
+
this.hunger = Math.min(100, this.hunger + 0.05 * deltaTime);
|
| 161 |
+
this.socialNeed = Math.min(100, this.socialNeed + 0.03 * deltaTime);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/**
|
| 165 |
+
* Check if villager is tired
|
| 166 |
+
* @returns {boolean} True if tired
|
| 167 |
+
*/
|
| 168 |
+
isTired() {
|
| 169 |
+
return this.energy < 20;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* Check if villager should work
|
| 174 |
+
* @returns {boolean} True if should work
|
| 175 |
+
*/
|
| 176 |
+
shouldWork() {
|
| 177 |
+
return this.energy > 30 && this.hunger < 80;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* Check if villager should socialize
|
| 182 |
+
* @returns {boolean} True if should socialize
|
| 183 |
+
*/
|
| 184 |
+
shouldSocialize() {
|
| 185 |
+
return this.energy > 40 && this.socialNeed > 50;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Sleep action
|
| 190 |
+
* @param {number} deltaTime - Time since last update
|
| 191 |
+
*/
|
| 192 |
+
sleep(deltaTime) {
|
| 193 |
+
this.energy = Math.min(100, this.energy + 0.5 * deltaTime);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/**
|
| 197 |
+
* Work action
|
| 198 |
+
* @param {number} deltaTime - Time since last update
|
| 199 |
+
*/
|
| 200 |
+
work(deltaTime) {
|
| 201 |
+
this.energy = Math.max(0, this.energy - 0.2 * deltaTime);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/**
|
| 205 |
+
* Eat action
|
| 206 |
+
* @param {number} deltaTime - Time since last update
|
| 207 |
+
*/
|
| 208 |
+
eat(deltaTime) {
|
| 209 |
+
this.hunger = Math.max(0, this.hunger - 0.8 * deltaTime);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/**
|
| 213 |
+
* Socialize action
|
| 214 |
+
* @param {number} deltaTime - Time since last update
|
| 215 |
+
*/
|
| 216 |
+
socialize(deltaTime) {
|
| 217 |
+
this.socialNeed = Math.max(0, this.socialNeed - 0.6 * deltaTime);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/**
|
| 221 |
+
* Idle action
|
| 222 |
+
*/
|
| 223 |
+
idle() {
|
| 224 |
+
// Do nothing
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
export { DailyRoutineManager, Villager };
|
src/ai/routines_simple.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Daily Routine System for Villager AI (Simplified Version)
|
| 3 |
+
*
|
| 4 |
+
* This implementation manages villager daily routines including work, rest, and social interactions.
|
| 5 |
+
* Uses simple arrays for positions instead of THREE.Vector3 to avoid import issues.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
class DailyRoutineManager {
|
| 9 |
+
constructor() {
|
| 10 |
+
this.schedules = new Map(); // Map of villager IDs to schedules
|
| 11 |
+
this.currentTime = 0; // In-game time in hours (0-24)
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Set a schedule for a villager
|
| 16 |
+
* @param {string} villagerId - Villager identifier
|
| 17 |
+
* @param {Array} schedule - Array of schedule entries
|
| 18 |
+
*/
|
| 19 |
+
setSchedule(villagerId, schedule) {
|
| 20 |
+
this.schedules.set(villagerId, schedule);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Update the routine manager
|
| 25 |
+
* @param {number} deltaTime - Time since last update in hours
|
| 26 |
+
*/
|
| 27 |
+
update(deltaTime) {
|
| 28 |
+
this.currentTime += deltaTime;
|
| 29 |
+
if (this.currentTime >= 24) {
|
| 30 |
+
this.currentTime -= 24; // Wrap around to next day
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Get current activity for a villager
|
| 36 |
+
* @param {string} villagerId - Villager identifier
|
| 37 |
+
* @returns {Object} Current activity
|
| 38 |
+
*/
|
| 39 |
+
getCurrentActivity(villagerId) {
|
| 40 |
+
const schedule = this.schedules.get(villagerId);
|
| 41 |
+
if (!schedule) return null;
|
| 42 |
+
|
| 43 |
+
// Find the current activity based on time
|
| 44 |
+
for (let i = schedule.length - 1; i >= 0; i--) {
|
| 45 |
+
if (this.currentTime >= schedule[i].startTime) {
|
| 46 |
+
return schedule[i];
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// If no activity found, return the last one (for times before first activity)
|
| 51 |
+
return schedule[schedule.length - 1];
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Create a default schedule for a villager
|
| 56 |
+
* @param {string} villagerId - Villager identifier
|
| 57 |
+
* @param {Array} workLocation - Work location [x, y, z]
|
| 58 |
+
* @param {Array} homeLocation - Home location [x, y, z]
|
| 59 |
+
* @param {Array} socialLocations - Social locations [[x, y, z], ...]
|
| 60 |
+
*/
|
| 61 |
+
createDefaultSchedule(villagerId, workLocation, homeLocation, socialLocations) {
|
| 62 |
+
const schedule = [
|
| 63 |
+
{ startTime: 0, activity: 'sleep', location: homeLocation },
|
| 64 |
+
{ startTime: 7, activity: 'eat', location: homeLocation },
|
| 65 |
+
{ startTime: 8, activity: 'work', location: workLocation },
|
| 66 |
+
{ startTime: 12, activity: 'eat', location: workLocation },
|
| 67 |
+
{ startTime: 13, activity: 'work', location: workLocation },
|
| 68 |
+
{ startTime: 17, activity: 'socialize', location: socialLocations[0] },
|
| 69 |
+
{ startTime: 19, activity: 'eat', location: homeLocation },
|
| 70 |
+
{ startTime: 20, activity: 'sleep', location: homeLocation }
|
| 71 |
+
];
|
| 72 |
+
|
| 73 |
+
this.setSchedule(villagerId, schedule);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Villager class with routine-based behavior (Simplified Version)
|
| 79 |
+
*/
|
| 80 |
+
class Villager {
|
| 81 |
+
constructor(id, position) {
|
| 82 |
+
this.id = id;
|
| 83 |
+
this.position = position; // [x, y, z] array
|
| 84 |
+
this.velocity = [0, 0, 0]; // [x, y, z] array
|
| 85 |
+
this.maxSpeed = 2;
|
| 86 |
+
this.energy = 100;
|
| 87 |
+
this.hunger = 0;
|
| 88 |
+
this.socialNeed = 0;
|
| 89 |
+
this.state = 'idle'; // idle, working, sleeping, eating, socializing
|
| 90 |
+
this.path = [];
|
| 91 |
+
this.avoidanceRadius = 2;
|
| 92 |
+
this.avoidanceWeight = 1.5;
|
| 93 |
+
this.steeringWeight = 1.0;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Update villager state
|
| 98 |
+
* @param {DailyRoutineManager} routineManager - Routine manager
|
| 99 |
+
* @param {VillagePathfinder} pathfinder - Pathfinding system
|
| 100 |
+
* @param {number} deltaTime - Time since last update
|
| 101 |
+
*/
|
| 102 |
+
update(routineManager, pathfinder, deltaTime) {
|
| 103 |
+
// Update needs
|
| 104 |
+
this.updateNeeds(deltaTime);
|
| 105 |
+
|
| 106 |
+
// Get current activity
|
| 107 |
+
const activity = routineManager.getCurrentActivity(this.id);
|
| 108 |
+
|
| 109 |
+
// Update state based on activity
|
| 110 |
+
if (activity) {
|
| 111 |
+
this.state = activity.activity;
|
| 112 |
+
|
| 113 |
+
// If we have a location for this activity, pathfind to it
|
| 114 |
+
if (activity.location && this.distanceTo(activity.location) > 1) {
|
| 115 |
+
this.path = pathfinder.findPath(this.position, activity.location);
|
| 116 |
+
} else {
|
| 117 |
+
this.path = [];
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Perform state-specific actions
|
| 122 |
+
switch (this.state) {
|
| 123 |
+
case 'sleep':
|
| 124 |
+
this.sleep(deltaTime);
|
| 125 |
+
break;
|
| 126 |
+
case 'work':
|
| 127 |
+
this.work(deltaTime);
|
| 128 |
+
break;
|
| 129 |
+
case 'eat':
|
| 130 |
+
this.eat(deltaTime);
|
| 131 |
+
break;
|
| 132 |
+
case 'socialize':
|
| 133 |
+
this.socialize(deltaTime);
|
| 134 |
+
break;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Calculate distance to another position
|
| 140 |
+
* @param {Array} other - Other position [x, y, z]
|
| 141 |
+
* @returns {number} Distance
|
| 142 |
+
*/
|
| 143 |
+
distanceTo(other) {
|
| 144 |
+
const dx = this.position[0] - other[0];
|
| 145 |
+
const dy = this.position[1] - other[1];
|
| 146 |
+
const dz = this.position[2] - other[2];
|
| 147 |
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Update villager needs over time
|
| 152 |
+
* @param {number} deltaTime - Time since last update
|
| 153 |
+
*/
|
| 154 |
+
updateNeeds(deltaTime) {
|
| 155 |
+
this.energy = Math.max(0, this.energy - 0.1 * deltaTime);
|
| 156 |
+
this.hunger = Math.min(100, this.hunger + 0.05 * deltaTime);
|
| 157 |
+
this.socialNeed = Math.min(100, this.socialNeed + 0.03 * deltaTime);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/**
|
| 161 |
+
* Check if villager is tired
|
| 162 |
+
* @returns {boolean} True if tired
|
| 163 |
+
*/
|
| 164 |
+
isTired() {
|
| 165 |
+
return this.energy < 20;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Check if villager should work
|
| 170 |
+
* @returns {boolean} True if should work
|
| 171 |
+
*/
|
| 172 |
+
shouldWork() {
|
| 173 |
+
return this.energy > 30 && this.hunger < 80;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* Check if villager should socialize
|
| 178 |
+
* @returns {boolean} True if should socialize
|
| 179 |
+
*/
|
| 180 |
+
shouldSocialize() {
|
| 181 |
+
return this.energy > 40 && this.socialNeed > 50;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/**
|
| 185 |
+
* Sleep action
|
| 186 |
+
* @param {number} deltaTime - Time since last update
|
| 187 |
+
*/
|
| 188 |
+
sleep(deltaTime) {
|
| 189 |
+
this.energy = Math.min(100, this.energy + 0.5 * deltaTime);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/**
|
| 193 |
+
* Work action
|
| 194 |
+
* @param {number} deltaTime - Time since last update
|
| 195 |
+
*/
|
| 196 |
+
work(deltaTime) {
|
| 197 |
+
this.energy = Math.max(0, this.energy - 0.2 * deltaTime);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* Eat action
|
| 202 |
+
* @param {number} deltaTime - Time since last update
|
| 203 |
+
*/
|
| 204 |
+
eat(deltaTime) {
|
| 205 |
+
this.hunger = Math.max(0, this.hunger - 0.8 * deltaTime);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/**
|
| 209 |
+
* Socialize action
|
| 210 |
+
* @param {number} deltaTime - Time since last update
|
| 211 |
+
*/
|
| 212 |
+
socialize(deltaTime) {
|
| 213 |
+
this.socialNeed = Math.max(0, this.socialNeed - 0.6 * deltaTime);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/**
|
| 217 |
+
* Idle action
|
| 218 |
+
*/
|
| 219 |
+
idle() {
|
| 220 |
+
// Do nothing
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
export { DailyRoutineManager, Villager };
|