Spaces:
Running
Running
| // 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); | |
| }); |