// 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 = '

No villager selected

'; return; } const villager = this.selectedVillager; villagerList.innerHTML = `
${villager.id}
State: ${villager.state}
Position: (${villager.position[0].toFixed(1)}, ${villager.position[1].toFixed(1)}, ${villager.position[2].toFixed(1)})
Energy: ${villager.energy.toFixed(1)}%
Hunger: ${villager.hunger.toFixed(1)}%
Social Need: ${villager.socialNeed.toFixed(1)}%
Path Points: ${villager.path.length}
`; } 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); });