Medieval-Village-AI / app_new.js
6rz6
Add Medieval Village AI Emulator
a32dc8b
// Medieval Village AI System - Three.js Visualization Application
// Using globally available THREE from script tag in index.html
import VillageAISystem from './src/ai/main.js';
import LLMHandler from './src/ai/llmHandler.js';
class VillageVisualizationApp {
constructor(hfToken = null) {
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.aiSystem = null;
// 3D Objects
this.villagerMeshes = new Map();
this.buildingMeshes = new Map();
this.resourceMeshes = new Map();
this.pathLines = new Map();
// UI Elements
this.uiElements = {};
this.selectedVillager = null;
this.timeSpeed = 1.0;
this.showPaths = true;
this.showTitles = true;
// LLM System
this.llmHandler = new LLMHandler(hfToken);
this.llmConnected = false;
// Log token status for debugging
console.log('LLM Handler initialized with token:', hfToken ? 'Set' : 'Not set');
if (hfToken) {
console.log('Token length:', hfToken.length);
}
// Initialize LLM status indicator to red (disconnected)
this.updateLLMStatusIndicator(false);
// New systems
this.weatherSystem = {
fogIntensity: 50,
currentWeather: 'sun',
rainParticles: [],
snowParticles: []
};
this.disasterSystem = {
activeDisasters: new Map(),
fireEffects: [],
floodEffects: []
};
this.animalSystem = {
animals: new Map(),
beasts: new Map()
};
this.warriorSystem = {
warriors: new Map(),
dispatched: false
};
// Animation
this.clock = new THREE.Clock();
this.lastTime = 0;
this.frameCount = 0;
this.fps = 0;
this.init();
}
init() {
console.log('Initializing Village Visualization App...');
this.initThreeJS();
this.initAI();
this.initUI();
this.createEnvironment();
this.createInitialVillagers();
this.animate();
console.log('Village Visualization App initialized successfully');
}
initThreeJS() {
console.log('Initializing Three.js...');
// Scene
this.scene = new THREE.Scene();
// Create a more realistic sky gradient background
this.scene.background = new THREE.Color(0x87CEEB);
console.log('Scene created');
// Camera
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(15, 12, 15);
this.camera.lookAt(0, 0, 0);
console.log('Camera created');
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setClearColor(0x87CEEB, 1); // Set clear color to sky blue
this.renderer.gammaOutput = true;
this.renderer.gammaFactor = 2.2;
console.log('Renderer created');
// Add to DOM
const container = document.getElementById('container');
if (container) {
container.appendChild(this.renderer.domElement);
console.log('Renderer added to DOM');
} else {
console.error('Container element not found!');
}
// Lighting
this.addLighting();
// Ground plane
this.createGround();
// Grid helper
const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222);
this.scene.add(gridHelper);
// Controls
console.log('Initializing controls...');
console.log('THREE.OrbitControls:', typeof THREE !== 'undefined' ? THREE.OrbitControls : 'undefined');
try {
// Check if OrbitControls is available globally
if (typeof THREE !== 'undefined' && typeof THREE.OrbitControls !== 'undefined') {
console.log('Creating OrbitControls instance from global THREE object...');
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
console.log('OrbitControls instance created:', this.controls);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.enableZoom = true;
this.controls.enablePan = true;
console.log('OrbitControls initialized successfully');
} else {
console.warn('OrbitControls is not available');
this.controls = null;
}
} catch (error) {
console.warn('Error initializing OrbitControls:', error);
this.controls = null;
}
// Handle window resize
window.addEventListener('resize', () => this.onWindowResize());
window.addEventListener('keydown', (event) => this.onKeyDown(event));
// Handle mouse clicks for object selection
this.renderer.domElement.addEventListener('click', (event) => this.onMouseClick(event));
// Configure OrbitControls to work properly with object selection
if (this.controls) {
// Enable all controls but make sure they don't interfere with clicks
this.controls.enableRotate = true;
this.controls.enableZoom = true;
this.controls.enablePan = true;
// Disable keyboard navigation in OrbitControls to avoid conflicts
this.controls.enableKeys = false;
// Use damping for smoother controls
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}
}
addLighting() {
// Ambient light
const ambientLight = new THREE.AmbientLight(0x404040, 0.4);
this.scene.add(ambientLight);
// Directional light (sun)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 15, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -20;
directionalLight.shadow.camera.right = 20;
directionalLight.shadow.camera.top = 20;
directionalLight.shadow.camera.bottom = -20;
this.scene.add(directionalLight);
// Add a fill light
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
fillLight.position.set(-10, 5, -10);
this.scene.add(fillLight);
// Add a hemisphere light for more natural outdoor lighting
const hemisphereLight = new THREE.HemisphereLight(0x87CEEB, 0x3a5f3a, 0.2);
this.scene.add(hemisphereLight);
}
createGround() {
// Create a more detailed ground with texture
const groundGeometry = new THREE.PlaneGeometry(100, 100, 20, 20);
// Create a more realistic ground material
const groundMaterial = new THREE.MeshLambertMaterial({
color: 0x3a5f3a,
wireframe: false
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
// Add some variation to the ground
const vertices = ground.geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
// Add some noise to the y position for a more natural look
vertices[i + 1] = (Math.random() - 0.5) * 0.5;
}
ground.geometry.attributes.position.needsUpdate = true;
ground.geometry.computeVertexNormals();
this.scene.add(ground);
// Add fog to the scene for a more realistic atmosphere
this.scene.fog = new THREE.Fog(0x87CEEB, 20, 50);
}
initAI() {
this.aiSystem = new VillageAISystem(this.scene);
}
initUI() {
// Get UI elements
this.uiElements = {
addVillagerBtn: document.getElementById('add-villager-btn'),
resetBtn: document.getElementById('reset-btn'),
timeSpeed: document.getElementById('time-speed'),
timeSpeedDisplay: document.getElementById('time-speed-display'),
showPaths: document.getElementById('show-paths'),
showTitles: document.getElementById('show-titles'),
fogControl: document.getElementById('fog-control'),
weatherSun: document.getElementById('weather-sun'),
weatherRain: document.getElementById('weather-rain'),
weatherSnow: document.getElementById('weather-snow'),
disasterFire: document.getElementById('disaster-fire'),
disasterHurricane: document.getElementById('disaster-hurricane'),
disasterFlood: document.getElementById('disaster-flood'),
disasterEarthquake: document.getElementById('disaster-earthquake'),
disasterPlague: document.getElementById('disaster-plague'),
spawnWolf: document.getElementById('spawn-wolf'),
spawnBear: document.getElementById('spawn-bear'),
spawnDragon: document.getElementById('spawn-dragon'),
addWarrior: document.getElementById('add-warrior'),
dispatchWarriors: document.getElementById('dispatch-warriors'),
villagerCountDisplay: document.getElementById('villager-count-display'),
villagerCountStat: document.getElementById('villager-count-stat'),
gameTime: document.getElementById('game-time'),
fps: document.getElementById('fps'),
buildingCount: document.getElementById('building-count'),
resourceCount: document.getElementById('resource-count'),
villagerList: document.getElementById('villager-list'),
// LLM UI elements
llmModel: document.getElementById('llm-model'),
llmQuery: document.getElementById('llm-query'),
llmSubmit: document.getElementById('llm-submit'),
llmResponse: document.getElementById('llm-response')
};
// Add event listeners
if (this.uiElements.addVillagerBtn) {
this.uiElements.addVillagerBtn.addEventListener('click', () => this.addVillager());
}
if (this.uiElements.resetBtn) {
this.uiElements.resetBtn.addEventListener('click', () => this.resetSimulation());
}
if (this.uiElements.timeSpeed) {
this.uiElements.timeSpeed.addEventListener('input', (e) => this.updateTimeSpeed(e.target.value));
}
if (this.uiElements.showPaths) {
this.uiElements.showPaths.addEventListener('change', (e) => this.togglePaths(e.target.checked));
}
if (this.uiElements.showTitles) {
this.uiElements.showTitles.addEventListener('change', (e) => this.toggleTitles(e.target.checked));
}
// Weather controls
if (this.uiElements.fogControl) {
this.uiElements.fogControl.addEventListener('input', (e) => this.updateFog(e.target.value));
}
if (this.uiElements.weatherSun) {
this.uiElements.weatherSun.addEventListener('click', () => this.setWeather('sun'));
}
if (this.uiElements.weatherRain) {
this.uiElements.weatherRain.addEventListener('click', () => this.setWeather('rain'));
}
if (this.uiElements.weatherSnow) {
this.uiElements.weatherSnow.addEventListener('click', () => this.setWeather('snow'));
}
// Disaster controls
if (this.uiElements.disasterFire) {
this.uiElements.disasterFire.addEventListener('click', () => this.triggerDisaster('fire'));
}
if (this.uiElements.disasterHurricane) {
this.uiElements.disasterHurricane.addEventListener('click', () => this.triggerDisaster('hurricane'));
}
if (this.uiElements.disasterFlood) {
this.uiElements.disasterFlood.addEventListener('click', () => this.triggerDisaster('flood'));
}
if (this.uiElements.disasterEarthquake) {
this.uiElements.disasterEarthquake.addEventListener('click', () => this.triggerDisaster('earthquake'));
}
if (this.uiElements.disasterPlague) {
this.uiElements.disasterPlague.addEventListener('click', () => this.triggerDisaster('plague'));
}
// Animal/Beast controls
if (this.uiElements.spawnWolf) {
this.uiElements.spawnWolf.addEventListener('click', () => this.spawnAnimal('wolf'));
}
if (this.uiElements.spawnBear) {
this.uiElements.spawnBear.addEventListener('click', () => this.spawnAnimal('bear'));
}
if (this.uiElements.spawnDragon) {
this.uiElements.spawnDragon.addEventListener('click', () => this.spawnAnimal('dragon'));
}
// Warrior controls
if (this.uiElements.addWarrior) {
this.uiElements.addWarrior.addEventListener('click', () => this.addWarrior());
}
if (this.uiElements.dispatchWarriors) {
this.uiElements.dispatchWarriors.addEventListener('click', () => this.dispatchWarriors());
}
// LLM event listeners
if (this.uiElements.llmModel) {
this.uiElements.llmModel.addEventListener('change', (e) => this.handleLLMModelChange(e.target.value));
}
if (this.uiElements.llmQuery) {
this.uiElements.llmQuery.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleLLMQuerySubmit();
}
});
}
if (this.uiElements.llmSubmit) {
this.uiElements.llmSubmit.addEventListener('click', () => this.handleLLMQuerySubmit());
}
// Test LLM connection
this.testLLMConnection();
}
/**
* Update the LLM status indicator
* @param {boolean} connected - Whether the LLM is connected
*/
updateLLMStatusIndicator(connected) {
this.llmConnected = connected;
const indicator = document.getElementById('llm-status-indicator');
if (indicator) {
indicator.style.backgroundColor = connected ? 'green' : 'red';
}
}
/**
* Test the LLM connection with an initial prompt
*/
async testLLMConnection() {
// Log token status for debugging
console.log('Testing LLM connection, token set:', this.llmHandler.isApiTokenSet());
if (this.llmHandler.isApiTokenSet()) {
console.log('Token length:', this.llmHandler.getApiToken().length);
}
// Check if API token is set
if (!this.llmHandler.isApiTokenSet()) {
// Update status indicator to red (disconnected)
this.updateLLMStatusIndicator(false);
// Update UI with error message
if (this.uiElements.llmResponse) {
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.";
}
return;
}
// Update UI to show testing state
if (this.uiElements.llmResponse) {
this.uiElements.llmResponse.textContent = 'Testing LLM connection...';
}
try {
// Send an initial prompt to test the connection
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.";
const response = await this.llmHandler.sendQuery(initialPrompt);
// Update UI with response
if (this.uiElements.llmResponse) {
this.uiElements.llmResponse.textContent = response;
}
// Update status indicator to green (connected)
this.updateLLMStatusIndicator(true);
} catch (error) {
console.error('Error testing LLM connection:', error);
// Update status indicator to red (disconnected)
this.updateLLMStatusIndicator(false);
// Update UI with error message
if (this.uiElements.llmResponse) {
if (error.message.includes("API token is not set")) {
this.uiElements.llmResponse.textContent = "Please set your Hugging Face API token by setting the HF_TOKEN environment variable or using the browser console.";
} else {
this.uiElements.llmResponse.textContent = 'Error connecting to LLM: ' + error.message;
}
}
}
}
/**
* Handle LLM model change
* @param {string} model - The selected model
*/
handleLLMModelChange(model) {
console.log('LLM model changed to:', model);
this.llmHandler.setSelectedModel(model);
}
/**
* Handle LLM query submission
*/
async handleLLMQuerySubmit() {
if (!this.uiElements.llmQuery || !this.uiElements.llmResponse) return;
const query = this.uiElements.llmQuery.value.trim();
if (!query) return;
console.log('Submitting LLM query:', query);
console.log('LLM Handler token set:', this.llmHandler.isApiTokenSet());
if (this.llmHandler.isApiTokenSet()) {
console.log('Token length:', this.llmHandler.getApiToken().length);
}
// Update UI to show loading state
this.uiElements.llmResponse.textContent = 'Processing your query...';
this.uiElements.llmSubmit.disabled = true;
try {
// Send query to LLM handler
const response = await this.llmHandler.sendQuery(query);
// Update UI with response
this.uiElements.llmResponse.textContent = response;
// Update status indicator to green (connected)
this.updateLLMStatusIndicator(true);
} catch (error) {
console.error('Error processing LLM query:', error);
if (error.message.includes("API token is not set")) {
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";
// Update status indicator to red (disconnected)
this.updateLLMStatusIndicator(false);
} else {
this.uiElements.llmResponse.textContent = 'Error: ' + error.message;
// Update status indicator to red (disconnected) if it's a connection error
if (error.message.includes("API request failed")) {
this.updateLLMStatusIndicator(false);
}
}
} finally {
// Re-enable submit button
this.uiElements.llmSubmit.disabled = false;
}
}
createEnvironment() {
// Buildings are already created in the AI system
this.createBuildingMeshes();
this.createResourceMeshes();
this.createRoads();
this.createTrees();
console.log('Environment created with roads and trees');
}
createBuildingMeshes() {
if (this.aiSystem && this.aiSystem.environmentSystem) {
for (const [id, building] of this.aiSystem.environmentSystem.buildings) {
let mesh = null;
// Create unique geometry and materials for each building type
switch (building.type) {
case 'house':
// Cozy cottage with sloped roof
const houseGroup = new THREE.Group();
const houseBase = new THREE.Mesh(
new THREE.BoxGeometry(3, 2, 3),
new THREE.MeshLambertMaterial({ color: 0xD2691E })
);
houseBase.position.y = 1;
const houseRoof = new THREE.Mesh(
new THREE.ConeGeometry(2.5, 1.5, 4),
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
);
houseRoof.position.y = 2.75;
houseGroup.add(houseBase);
houseGroup.add(houseRoof);
mesh = houseGroup;
mesh.position.y = 0;
break;
case 'workshop':
// Industrial workshop with chimney
const workshopGroup = new THREE.Group();
const workshopBase = new THREE.Mesh(
new THREE.BoxGeometry(4, 3, 4),
new THREE.MeshLambertMaterial({ color: 0x708090 })
);
workshopBase.position.y = 1.5;
const chimney = new THREE.Mesh(
new THREE.CylinderGeometry(0.3, 0.3, 2),
new THREE.MeshLambertMaterial({ color: 0x696969 })
);
chimney.position.set(1.5, 3, 0);
workshopGroup.add(workshopBase);
workshopGroup.add(chimney);
mesh = workshopGroup;
mesh.position.y = 0;
break;
case 'market':
// Large marketplace with dome
const marketGroup = new THREE.Group();
const marketBase = new THREE.Mesh(
new THREE.CylinderGeometry(5, 5, 2, 32),
new THREE.MeshLambertMaterial({ color: 0xFFD700 })
);
marketBase.position.y = 1;
const marketDome = new THREE.Mesh(
new THREE.SphereGeometry(3, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2),
new THREE.MeshLambertMaterial({ color: 0xFFA500 })
);
marketDome.position.y = 3.5;
marketGroup.add(marketBase);
marketGroup.add(marketDome);
mesh = marketGroup;
mesh.position.y = 0;
break;
case 'university':
// Academic building with tower and columns
const universityGroup = new THREE.Group();
const uniBase = new THREE.Mesh(
new THREE.BoxGeometry(6, 4, 5),
new THREE.MeshLambertMaterial({ color: 0x4169E1 })
);
uniBase.position.y = 2;
const uniTower = new THREE.Mesh(
new THREE.CylinderGeometry(1.5, 1.5, 6),
new THREE.MeshLambertMaterial({ color: 0x1E90FF })
);
uniTower.position.set(2, 5, 0);
// Add columns
for (let i = -2; i <= 2; i += 2) {
const column = new THREE.Mesh(
new THREE.CylinderGeometry(0.3, 0.3, 3),
new THREE.MeshLambertMaterial({ color: 0xF5F5F5 })
);
column.position.set(i, 1.5, 2);
universityGroup.add(column);
}
universityGroup.add(uniBase);
universityGroup.add(uniTower);
mesh = universityGroup;
mesh.position.y = 0;
break;
case 'store':
// Modern store with large windows
const storeGroup = new THREE.Group();
const storeBase = new THREE.Mesh(
new THREE.BoxGeometry(5, 3, 4),
new THREE.MeshLambertMaterial({ color: 0x32CD32 })
);
storeBase.position.y = 1.5;
// Add windows
const windowMaterial = new THREE.MeshLambertMaterial({ color: 0x87CEEB });
for (let i = -1.5; i <= 1.5; i += 1.5) {
const window = new THREE.Mesh(
new THREE.PlaneGeometry(1, 1),
windowMaterial
);
window.position.set(i, 1.5, 2.01);
storeGroup.add(window);
}
storeGroup.add(storeBase);
mesh = storeGroup;
mesh.position.y = 0;
break;
case 'bank':
// Impressive bank building
const bankGroup = new THREE.Group();
const bankBase = new THREE.Mesh(
new THREE.BoxGeometry(6, 5, 5),
new THREE.MeshLambertMaterial({ color: 0xC0C0C0 })
);
bankBase.position.y = 2.5;
// Add pillars
for (let i = -2; i <= 2; i += 2) {
const pillar = new THREE.Mesh(
new THREE.CylinderGeometry(0.4, 0.4, 4),
new THREE.MeshLambertMaterial({ color: 0xF5F5F5 })
);
pillar.position.set(i, 2, 2.5);
bankGroup.add(pillar);
}
bankGroup.add(bankBase);
mesh = bankGroup;
mesh.position.y = 0;
break;
case 'hospital':
// Medical facility with cross
const hospitalGroup = new THREE.Group();
const hospitalBase = new THREE.Mesh(
new THREE.BoxGeometry(7, 5, 5),
new THREE.MeshLambertMaterial({ color: 0xFF0000 })
);
hospitalBase.position.y = 2.5;
// Medical cross
const crossVertical = new THREE.Mesh(
new THREE.BoxGeometry(0.3, 2, 0.3),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
);
crossVertical.position.set(0, 4.5, 2.5);
const crossHorizontal = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 0.3, 0.3),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
);
crossHorizontal.position.set(0, 4.5, 2.5);
hospitalGroup.add(hospitalBase);
hospitalGroup.add(crossVertical);
hospitalGroup.add(crossHorizontal);
mesh = hospitalGroup;
mesh.position.y = 0;
break;
case 'restaurant':
// Fancy restaurant with unique shape
const restaurantGroup = new THREE.Group();
const restaurantBase = new THREE.Mesh(
new THREE.CylinderGeometry(4, 4, 3, 32),
new THREE.MeshLambertMaterial({ color: 0xFF6347 })
);
restaurantBase.position.y = 1.5;
const restaurantRoof = new THREE.Mesh(
new THREE.ConeGeometry(3.5, 2, 8),
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
);
restaurantRoof.position.y = 3.5;
restaurantGroup.add(restaurantBase);
restaurantGroup.add(restaurantRoof);
mesh = restaurantGroup;
mesh.position.y = 0;
break;
default:
// Default building
mesh = new THREE.Mesh(
new THREE.BoxGeometry(3, 3, 3),
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
);
mesh.position.y = 1.5;
}
mesh.position.set(building.position[0], building.position[1], building.position[2]);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.buildingMeshes.set(id, mesh);
this.scene.add(mesh);
}
}
this.updateBuildingCount();
}
createResourceMeshes() {
const resourceGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2);
const materials = {
wood: new THREE.MeshLambertMaterial({ color: 0x8B4513 }),
stone: new THREE.MeshLambertMaterial({ color: 0x708090 }),
food: new THREE.MeshLambertMaterial({ color: 0x32CD32 })
};
if (this.aiSystem && this.aiSystem.environmentSystem) {
for (const [id, resource] of this.aiSystem.environmentSystem.resources) {
const material = materials[resource.type] || materials.wood;
const mesh = new THREE.Mesh(resourceGeometry, material);
mesh.position.set(resource.position[0], resource.position[1], resource.position[2]);
mesh.position.y = 1;
mesh.castShadow = true;
mesh.receiveShadow = true;
this.resourceMeshes.set(id, mesh);
this.scene.add(mesh);
}
}
this.updateResourceCount();
}
/**
* Create roads for the village
*/
createRoads() {
console.log('Creating roads...');
// Create a simple crossroad in the center - very visible
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
// Horizontal road - very wide and visible
const horizontalRoad = new THREE.Mesh(
new THREE.BoxGeometry(50, 0.5, 6),
roadMaterial
);
horizontalRoad.position.set(0, 0.25, 0);
horizontalRoad.receiveShadow = true;
this.scene.add(horizontalRoad);
console.log('Added horizontal road at center');
// Vertical road - very wide and visible
const verticalRoad = new THREE.Mesh(
new THREE.BoxGeometry(6, 0.5, 50),
roadMaterial
);
verticalRoad.position.set(0, 0.25, 0);
verticalRoad.receiveShadow = true;
this.scene.add(verticalRoad);
console.log('Added vertical road at center');
// Add bright yellow road markings
const markingMaterial = new THREE.MeshLambertMaterial({ color: 0xFFFF00 });
// Horizontal road markings
for (let x = -20; x <= 20; x += 4) {
if (Math.abs(x) > 2) { // Skip center
const marking = new THREE.Mesh(
new THREE.BoxGeometry(2, 0.3, 0.5),
markingMaterial
);
marking.position.set(x, 0.4, 0);
this.scene.add(marking);
}
}
// Vertical road markings
for (let z = -20; z <= 20; z += 4) {
if (Math.abs(z) > 2) { // Skip center
const marking = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.3, 2),
markingMaterial
);
marking.position.set(0, 0.4, z);
this.scene.add(marking);
}
}
console.log('Roads creation completed');
}
/**
* Create trees for the village
*/
createTrees() {
console.log('Creating trees...');
// Create very visible trees at key positions
const treePositions = [
[-15, 0, -15], [15, 0, -15], [-15, 0, 15], [15, 0, 15],
[-25, 0, 0], [25, 0, 0], [0, 0, -25], [0, 0, 25]
];
treePositions.forEach((pos, index) => {
// Create a very visible tree
const treeGroup = new THREE.Group();
// Large trunk
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(0.8, 1, 6, 8),
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
);
trunk.position.y = 3;
trunk.castShadow = true;
trunk.receiveShadow = true;
treeGroup.add(trunk);
// Large foliage - multiple layers for visibility
const foliage1 = new THREE.Mesh(
new THREE.SphereGeometry(5, 8, 6),
new THREE.MeshLambertMaterial({ color: 0x228B22 })
);
foliage1.position.y = 7;
foliage1.castShadow = true;
foliage1.receiveShadow = true;
treeGroup.add(foliage1);
const foliage2 = new THREE.Mesh(
new THREE.SphereGeometry(3, 8, 6),
new THREE.MeshLambertMaterial({ color: 0x32CD32 })
);
foliage2.position.y = 10;
foliage2.castShadow = true;
foliage2.receiveShadow = true;
treeGroup.add(foliage2);
treeGroup.position.set(pos[0], 0, pos[2]);
this.scene.add(treeGroup);
console.log(`Added tree ${index + 1} at:`, pos);
});
console.log('Trees creation completed');
}
createInitialVillagers() {
const positions = [
[0, 0, 0],
[5, 0, 5],
[-3, 0, -3]
];
positions.forEach((position, index) => {
this.createVillager(`villager${index + 1}`, position);
});
}
createVillager(id, position) {
if (this.aiSystem) {
const villager = this.aiSystem.createVillager(id, position);
this.createVillagerMesh(villager);
this.updateVillagerCount();
return villager;
}
}
createVillagerMesh(villager) {
// Create a more detailed villager with a body and head
const villagerGroup = new THREE.Group();
// Body
const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 0.8, 8);
const bodyMaterial = new THREE.MeshLambertMaterial({
color: this.getStateColor(villager.state)
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.4;
body.castShadow = true;
body.receiveShadow = true;
villagerGroup.add(body);
// Head
const headGeometry = new THREE.SphereGeometry(0.25, 16, 16);
const headMaterial = new THREE.MeshLambertMaterial({
color: 0xffd700 // Gold color for head
});
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 0.9;
head.castShadow = true;
head.receiveShadow = true;
villagerGroup.add(head);
// Create villager label (sprite)
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 128;
context.fillStyle = 'rgba(0, 0, 0, 0.8)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'white';
context.font = '32px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(villager.id, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
transparent: true
});
const label = new THREE.Sprite(spriteMaterial);
label.scale.set(3, 1.5, 1);
label.position.y = 1.5;
label.visible = this.showTitles; // Set initial visibility based on showTitles flag
villagerGroup.add(label);
villagerGroup.position.set(villager.position[0], villager.position[1], villager.position[2]);
villagerGroup.position.y = 0;
villagerGroup.castShadow = true;
villagerGroup.receiveShadow = true;
villagerGroup.userData.villager = villager;
this.villagerMeshes.set(villager.id, villagerGroup);
this.scene.add(villagerGroup);
}
getStateColor(state) {
const colors = {
sleep: 0x7f8c8d,
work: 0xe74c3c,
eat: 0xf39c12,
socialize: 0x9b59b6,
idle: 0x95a5a6
};
return colors[state] || colors.idle;
}
addVillager() {
const villagerCount = this.villagerMeshes.size;
const id = `villager${villagerCount + 1}`;
const position = [
(Math.random() - 0.5) * 40,
0,
(Math.random() - 0.5) * 40
];
this.createVillager(id, position);
}
resetSimulation() {
// Clear all villagers
for (const [id, mesh] of this.villagerMeshes) {
this.scene.remove(mesh);
}
this.villagerMeshes.clear();
// Clear path lines
for (const [id, line] of this.pathLines) {
this.scene.remove(line);
}
this.pathLines.clear();
// Reset AI system
this.aiSystem = new VillageAISystem(this.scene);
this.createEnvironment();
// Clear selection
this.selectedVillager = null;
this.updateVillagerInfo();
this.updateVillagerCount();
}
updateTimeSpeed(speed) {
this.timeSpeed = parseFloat(speed);
if (this.uiElements.timeSpeedDisplay) {
this.uiElements.timeSpeedDisplay.textContent = `${speed}x`;
}
}
togglePaths(show) {
console.log('Toggling paths:', show);
this.showPaths = show;
for (const [id, line] of this.pathLines) {
line.visible = show;
}
}
toggleTitles(show) {
console.log('Toggling titles:', show);
this.showTitles = show;
for (const [id, mesh] of this.villagerMeshes) {
// Find the text sprite (label) in the mesh children
// The label is the third child (index 2) - body, head, label
if (mesh.children.length >= 3) {
const label = mesh.children[2]; // Label is the third child
if (label instanceof THREE.Sprite) {
label.visible = show;
}
}
}
}
updateFog(intensity) {
console.log('Updating fog intensity:', intensity);
this.weatherSystem.fogIntensity = intensity;
// Update fog in the scene
if (this.scene.fog) {
// Convert intensity (0-100) to fog density
const near = 10 + (100 - intensity); // More intensity = less near distance
const far = 30 + (100 - intensity) * 2; // More intensity = less far distance
this.scene.fog.near = near;
this.scene.fog.far = far;
}
}
setWeather(weatherType) {
console.log('Setting weather to:', weatherType);
this.weatherSystem.currentWeather = weatherType;
// Update scene based on weather
switch (weatherType) {
case 'sun':
this.scene.background = new THREE.Color(0x87CEEB); // Sky blue
if (this.scene.fog) {
this.scene.fog.color = new THREE.Color(0x87CEEB);
}
break;
case 'rain':
this.scene.background = new THREE.Color(0x778899); // Gray
if (this.scene.fog) {
this.scene.fog.color = new THREE.Color(0x778899);
}
this.createRainEffect();
break;
case 'snow':
this.scene.background = new THREE.Color(0xE0E6EF); // Light gray
if (this.scene.fog) {
this.scene.fog.color = new THREE.Color(0xE0E6EF);
}
this.createSnowEffect();
break;
}
}
createRainEffect() {
// Clear existing rain particles
this.weatherSystem.rainParticles.forEach(particle => {
this.scene.remove(particle);
});
this.weatherSystem.rainParticles = [];
// Create new rain particles
const rainCount = 1000;
const rainGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(rainCount * 3);
for (let i = 0; i < rainCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 100; // x
positions[i + 1] = Math.random() * 50 + 10; // y
positions[i + 2] = (Math.random() - 0.5) * 100; // z
}
rainGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const rainMaterial = new THREE.PointsMaterial({
color: 0xAAAAFF,
size: 0.1,
transparent: true
});
const rainSystem = new THREE.Points(rainGeometry, rainMaterial);
this.scene.add(rainSystem);
this.weatherSystem.rainParticles.push(rainSystem);
}
createSnowEffect() {
// Clear existing snow particles
this.weatherSystem.snowParticles.forEach(particle => {
this.scene.remove(particle);
});
this.weatherSystem.snowParticles = [];
// Create new snow particles
const snowCount = 1000;
const snowGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(snowCount * 3);
for (let i = 0; i < snowCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 100; // x
positions[i + 1] = Math.random() * 50 + 10; // y
positions[i + 2] = (Math.random() - 0.5) * 100; // z
}
snowGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const snowMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 0.2,
transparent: true
});
const snowSystem = new THREE.Points(snowGeometry, snowMaterial);
this.scene.add(snowSystem);
this.weatherSystem.snowParticles.push(snowSystem);
}
triggerDisaster(disasterType) {
console.log('Triggering disaster:', disasterType);
// Create disaster effect
switch (disasterType) {
case 'fire':
this.createFireEffect();
break;
case 'hurricane':
this.createHurricaneEffect();
break;
case 'flood':
this.createFloodEffect();
break;
case 'earthquake':
this.createEarthquakeEffect();
break;
case 'plague':
this.createPlagueEffect();
break;
}
// Add to active disasters
this.disasterSystem.activeDisasters.set(disasterType, {
startTime: Date.now(),
intensity: 1.0
});
}
createFireEffect() {
// Create fire particles at random building locations
if (this.buildingMeshes.size > 0) {
const buildingArray = Array.from(this.buildingMeshes.values());
const building = buildingArray[Math.floor(Math.random() * buildingArray.length)];
const fireGeometry = new THREE.SphereGeometry(1, 8, 8);
const fireMaterial = new THREE.MeshBasicMaterial({
color: 0xFF4500,
transparent: true,
opacity: 0.7
});
const fireEffect = new THREE.Mesh(fireGeometry, fireMaterial);
fireEffect.position.copy(building.position);
fireEffect.position.y = 3;
this.scene.add(fireEffect);
this.disasterSystem.fireEffects.push(fireEffect);
// Remove fire after some time
setTimeout(() => {
this.scene.remove(fireEffect);
const index = this.disasterSystem.fireEffects.indexOf(fireEffect);
if (index > -1) {
this.disasterSystem.fireEffects.splice(index, 1);
}
}, 5000);
}
}
createHurricaneEffect() {
// Create a rotating wind effect in the center of the village
const tornadoGeometry = new THREE.CylinderGeometry(0.5, 2, 20, 8);
const tornadoMaterial = new THREE.MeshBasicMaterial({
color: 0x888888,
transparent: true,
opacity: 0.5
});
const tornado = new THREE.Mesh(tornadoGeometry, tornadoMaterial);
tornado.position.set(0, 10, 0);
this.scene.add(tornado);
// Animate tornado
let tornadoTime = 0;
const animateTornado = () => {
tornadoTime += 0.1;
tornado.rotation.y = tornadoTime;
tornado.position.x = Math.sin(tornadoTime) * 5;
tornado.position.z = Math.cos(tornadoTime) * 5;
if (tornadoTime < 20) { // Run for 20 seconds
requestAnimationFrame(animateTornado);
} else {
this.scene.remove(tornado);
}
};
animateTornado();
}
createFloodEffect() {
// Create a water plane that rises
const waterGeometry = new THREE.PlaneGeometry(100, 100);
const waterMaterial = new THREE.MeshBasicMaterial({
color: 0x4169E1,
transparent: true,
opacity: 0.6
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
water.position.y = 0.1;
this.scene.add(water);
this.disasterSystem.floodEffects.push(water);
// Animate water rising
let waterLevel = 0.1;
const raiseWater = () => {
waterLevel += 0.1;
water.position.y = waterLevel;
if (waterLevel < 3) { // Raise to 3 units
setTimeout(raiseWater, 200);
} else {
// Remove water after some time
setTimeout(() => {
this.scene.remove(water);
const index = this.disasterSystem.floodEffects.indexOf(water);
if (index > -1) {
this.disasterSystem.floodEffects.splice(index, 1);
}
}, 3000);
}
};
raiseWater();
}
createEarthquakeEffect() {
// Shake the camera
const originalCameraPosition = this.camera.position.clone();
let shakeIntensity = 0.5;
let shakeTime = 0;
const shakeCamera = () => {
shakeTime += 0.1;
shakeIntensity *= 0.95; // Decrease intensity over time
this.camera.position.x = originalCameraPosition.x + (Math.random() - 0.5) * shakeIntensity;
this.camera.position.y = originalCameraPosition.y + (Math.random() - 0.5) * shakeIntensity;
this.camera.position.z = originalCameraPosition.z + (Math.random() - 0.5) * shakeIntensity;
if (shakeTime < 5) { // Shake for 5 seconds
requestAnimationFrame(shakeCamera);
} else {
// Reset camera position
this.camera.position.copy(originalCameraPosition);
}
};
shakeCamera();
}
createPlagueEffect() {
// Change villager colors to show they're sick
for (const [id, mesh] of this.villagerMeshes) {
if (mesh.children.length > 0) {
const body = mesh.children[0];
if (body.material) {
body.material.color.setHex(0x808080); // Gray color for sick villagers
}
}
}
// Reset colors after some time
setTimeout(() => {
for (const [id, mesh] of this.villagerMeshes) {
const villager = mesh.userData.villager;
if (mesh.children.length > 0) {
const body = mesh.children[0];
if (body.material) {
body.material.color.setHex(this.getStateColor(villager.state));
}
}
}
}, 10000);
}
spawnAnimal(animalType) {
console.log('Spawning animal:', animalType);
// Create animal at random position
const position = [
(Math.random() - 0.5) * 40,
0,
(Math.random() - 0.5) * 40
];
this.createAnimal(animalType, position);
}
createAnimal(animalType, position) {
let animalMesh = null;
switch (animalType) {
case 'wolf':
animalMesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 0.5, 0.5),
new THREE.MeshLambertMaterial({ color: 0x696969 })
);
break;
case 'bear':
animalMesh = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1, 1),
new THREE.MeshLambertMaterial({ color: 0x8B4513 })
);
break;
case 'dragon':
// Create a more complex dragon
const dragonGroup = new THREE.Group();
// Body
const body = new THREE.Mesh(
new THREE.CylinderGeometry(0.5, 0.8, 2, 8),
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
);
body.rotation.z = Math.PI / 2;
dragonGroup.add(body);
// Head
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 8, 8),
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
);
head.position.x = 1.2;
dragonGroup.add(head);
// Wings
const leftWing = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 0.1, 0.5),
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
);
leftWing.position.set(-0.5, 0, 0.5);
leftWing.rotation.z = Math.PI / 4;
dragonGroup.add(leftWing);
const rightWing = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 0.1, 0.5),
new THREE.MeshLambertMaterial({ color: 0x8B0000 })
);
rightWing.position.set(-0.5, 0, -0.5);
rightWing.rotation.z = -Math.PI / 4;
dragonGroup.add(rightWing);
animalMesh = dragonGroup;
break;
}
if (animalMesh) {
animalMesh.position.set(position[0], position[1], position[2]);
animalMesh.position.y = 0.5;
animalMesh.castShadow = true;
animalMesh.receiveShadow = true;
this.scene.add(animalMesh);
this.animalSystem.animals.set(`animal_${Date.now()}`, {
type: animalType,
mesh: animalMesh,
position: position,
targetVillager: null
});
}
}
addWarrior() {
console.log('Adding warrior');
// Create warrior at random position
const position = [
(Math.random() - 0.5) * 10,
0,
(Math.random() - 0.5) * 10
];
this.createWarrior(position);
}
createWarrior(position) {
// Create a warrior (similar to villager but with different color and weapon)
const warriorGroup = new THREE.Group();
// Body
const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 0.8, 8);
const bodyMaterial = new THREE.MeshLambertMaterial({
color: 0x4169E1 // Blue for warriors
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.4;
body.castShadow = true;
body.receiveShadow = true;
warriorGroup.add(body);
// Head
const headGeometry = new THREE.SphereGeometry(0.25, 16, 16);
const headMaterial = new THREE.MeshLambertMaterial({
color: 0xFFD700 // Gold color for head
});
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 0.9;
head.castShadow = true;
head.receiveShadow = true;
warriorGroup.add(head);
// Weapon (sword)
const swordGeometry = new THREE.BoxGeometry(0.05, 1, 0.05);
const swordMaterial = new THREE.MeshLambertMaterial({
color: 0xC0C0C0 // Silver color for sword
});
const sword = new THREE.Mesh(swordGeometry, swordMaterial);
sword.position.set(0.4, 0.8, 0);
sword.rotation.z = Math.PI / 4;
warriorGroup.add(sword);
// Create warrior label (sprite)
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 128;
context.fillStyle = 'rgba(0, 0, 0, 0.8)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'white';
context.font = '32px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('Warrior', canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
transparent: true
});
const label = new THREE.Sprite(spriteMaterial);
label.scale.set(3, 1.5, 1);
label.position.y = 1.5;
label.visible = this.showTitles; // Set initial visibility based on showTitles flag
warriorGroup.add(label);
warriorGroup.position.set(position[0], position[1], position[2]);
warriorGroup.position.y = 0;
warriorGroup.castShadow = true;
warriorGroup.receiveShadow = true;
this.scene.add(warriorGroup);
this.warriorSystem.warriors.set(`warrior_${Date.now()}`, {
mesh: warriorGroup,
position: position,
target: null
});
this.updateVillagerCount(); // Update count to include warriors
}
dispatchWarriors() {
console.log('Dispatching warriors');
this.warriorSystem.dispatched = true;
// Make warriors patrol or attack animals/beasts
for (const [id, warrior] of this.warriorSystem.warriors) {
// Set a random patrol point
const patrolPoint = [
(Math.random() - 0.5) * 30,
0,
(Math.random() - 0.5) * 30
];
warrior.target = patrolPoint;
}
}
onWindowResize() {
if (this.camera && this.renderer) {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
}
onKeyDown(event) {
// WASD controls have been disabled to prevent interference with LLM chat input
// All camera movement should now be handled by OrbitControls only
console.log('Key pressed (WASD controls disabled):', event.code);
}
onMouseClick(event) {
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.camera);
const villagerMeshes = Array.from(this.villagerMeshes.values());
// Use recursive intersection to handle groups
const intersects = raycaster.intersectObjects(villagerMeshes, true);
if (intersects.length > 0) {
// Find the parent group that has the villager data
let selectedMesh = intersects[0].object;
while (selectedMesh && !selectedMesh.userData.villager) {
selectedMesh = selectedMesh.parent;
}
if (selectedMesh && selectedMesh.userData.villager) {
this.selectedVillager = selectedMesh.userData.villager;
this.updateVillagerInfo();
}
}
}
updateVillagerCount() {
const count = this.villagerMeshes.size;
if (this.uiElements.villagerCountDisplay) {
this.uiElements.villagerCountDisplay.textContent = count;
}
if (this.uiElements.villagerCountStat) {
this.uiElements.villagerCountStat.textContent = count;
}
}
updateBuildingCount() {
const count = this.buildingMeshes.size;
if (this.uiElements.buildingCount) {
this.uiElements.buildingCount.textContent = count;
}
}
updateResourceCount() {
const count = this.resourceMeshes.size;
if (this.uiElements.resourceCount) {
this.uiElements.resourceCount.textContent = count;
}
}
updateVillagerInfo() {
const villagerList = this.uiElements.villagerList;
if (!villagerList) return;
if (!this.selectedVillager) {
villagerList.innerHTML = '<p>No villager selected</p>';
return;
}
const villager = this.selectedVillager;
villagerList.innerHTML = `
<div class="villager-item selected">
<div><strong>${villager.id}</strong></div>
<div>State: <span class="state-indicator state-${villager.state}"></span>${villager.state}</div>
<div>Position: (${villager.position[0].toFixed(1)}, ${villager.position[1].toFixed(1)}, ${villager.position[2].toFixed(1)})</div>
<div>Energy: ${villager.energy.toFixed(1)}%</div>
<div>Hunger: ${villager.hunger.toFixed(1)}%</div>
<div>Social Need: ${villager.socialNeed.toFixed(1)}%</div>
<div>Path Points: ${villager.path.length}</div>
</div>
`;
}
updatePathVisualization(villager) {
const villagerId = villager.id;
// Remove existing path line
if (this.pathLines.has(villagerId)) {
this.scene.remove(this.pathLines.get(villagerId));
this.pathLines.delete(villagerId);
}
// Create new path line if villager has a path
if (villager.path.length > 1) {
const geometry = new THREE.BufferGeometry();
const positions = [];
// Add current position
positions.push(villager.position[0], 0.1, villager.position[2]);
// Add path points
for (const point of villager.path) {
positions.push(point[0], 0.1, point[2]);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const material = new THREE.LineBasicMaterial({
color: this.getStateColor(villager.state),
linewidth: 3
});
const line = new THREE.Line(geometry, material);
line.visible = this.showPaths;
this.pathLines.set(villagerId, line);
this.scene.add(line);
}
}
updateGameTime() {
if (this.aiSystem && this.aiSystem.routineManager) {
const time = this.aiSystem.routineManager.currentTime;
const hours = Math.floor(time);
const minutes = Math.floor((time - hours) * 60);
if (this.uiElements.gameTime) {
this.uiElements.gameTime.textContent = `${hours}:${minutes.toString().padStart(2, '0')}`;
}
}
}
updateFPS() {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.lastTime >= 1000) {
this.fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime));
this.frameCount = 0;
this.lastTime = currentTime;
if (this.uiElements.fps) {
this.uiElements.fps.textContent = this.fps;
}
}
}
updateVillagerMeshes() {
for (const [villagerId, mesh] of this.villagerMeshes) {
const villager = mesh.userData.villager;
// Update position
mesh.position.set(villager.position[0], villager.position[1], villager.position[2]);
mesh.position.y = 0;
// Update color based on state
// Update the body color (first child)
if (mesh.children.length > 0) {
const body = mesh.children[0];
const newColor = this.getStateColor(villager.state);
if (body.material.color.getHex() !== newColor) {
body.material.color.setHex(newColor);
}
}
}
}
updatePathVisualizations() {
for (const [villagerId, mesh] of this.villagerMeshes) {
const villager = mesh.userData.villager;
this.updatePathVisualization(villager);
}
}
updateAnimals() {
// Update animal positions and behaviors
for (const [id, animal] of this.animalSystem.animals) {
// Simple movement logic - move towards random points
if (Math.random() < 0.02) { // 2% chance to change direction
animal.targetPosition = [
animal.mesh.position.x + (Math.random() - 0.5) * 10,
animal.mesh.position.y,
animal.mesh.position.z + (Math.random() - 0.5) * 10
];
}
// Move towards target position
if (animal.targetPosition) {
const speed = 0.05;
const dx = animal.targetPosition[0] - animal.mesh.position.x;
const dz = animal.targetPosition[2] - animal.mesh.position.z;
if (Math.abs(dx) > 0.1) {
animal.mesh.position.x += Math.sign(dx) * speed;
}
if (Math.abs(dz) > 0.1) {
animal.mesh.position.z += Math.sign(dz) * speed;
}
}
// Rotate to face movement direction
if (animal.targetPosition) {
const dx = animal.targetPosition[0] - animal.mesh.position.x;
const dz = animal.targetPosition[2] - animal.mesh.position.z;
animal.mesh.rotation.y = Math.atan2(dx, dz);
}
}
}
updateWarriors() {
// Update warrior positions and behaviors
for (const [id, warrior] of this.warriorSystem.warriors) {
// If warriors are dispatched, make them patrol
if (this.warriorSystem.dispatched) {
// Check if warrior has reached target
if (warrior.target) {
const dx = warrior.target[0] - warrior.mesh.position.x;
const dz = warrior.target[2] - warrior.mesh.position.z;
// If close to target, set new target
if (Math.abs(dx) < 1 && Math.abs(dz) < 1) {
warrior.target = [
(Math.random() - 0.5) * 30,
0,
(Math.random() - 0.5) * 30
];
}
// Move towards target
const speed = 0.1;
if (Math.abs(dx) > 0.1) {
warrior.mesh.position.x += Math.sign(dx) * speed;
}
if (Math.abs(dz) > 0.1) {
warrior.mesh.position.z += Math.sign(dz) * speed;
}
// Rotate to face movement direction
warrior.mesh.rotation.y = Math.atan2(dx, dz);
}
}
}
}
animate() {
requestAnimationFrame(() => this.animate());
const deltaTime = this.clock.getDelta() * this.timeSpeed;
// Update AI system
if (this.aiSystem) {
this.aiSystem.update(deltaTime);
}
// Update 3D visualization
this.updateVillagerMeshes();
this.updatePathVisualizations();
this.updateAnimals();
this.updateWarriors();
// Update UI
this.updateGameTime();
this.updateFPS();
this.updateVillagerInfo();
// Update controls
if (this.controls) {
this.controls.update();
}
// Render scene
if (this.renderer && this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
}
}
}
// Initialize the application when the page loads
document.addEventListener('DOMContentLoaded', () => {
// Get the Hugging Face token from the window object
// This could be set by a server-side process that has access to the HF_TOKEN environment variable
const hfToken = window.HF_TOKEN || null;
// Log token status for debugging
console.log('HF_TOKEN from window.HF_TOKEN:', hfToken ? 'Set' : 'Not set');
if (hfToken) {
console.log('HF_TOKEN length:', hfToken.length);
}
window.app = new VillageVisualizationApp(hfToken);
});