Spaces:
Running
Running
| // Using globally available THREE from script tag in index.html | |
| import VillageAISystem from './src/ai/main.js'; | |
| /** | |
| * Medieval Village AI System - Three.js Visualization Application | |
| * | |
| * This application demonstrates the village AI system with real-time 3D visualization | |
| * using Three.js. It shows villagers moving around, performing activities, and interacting | |
| * with their environment. | |
| */ | |
| class VillageVisualizationApp { | |
| constructor() { | |
| 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; | |
| // Animation | |
| this.clock = new THREE.Clock(); | |
| this.lastTime = 0; | |
| this.frameCount = 0; | |
| this.fps = 0; | |
| this.init(); | |
| } | |
| /** | |
| * Initialize the application | |
| */ | |
| 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'); | |
| } | |
| /** | |
| * Initialize Three.js scene, camera, and renderer | |
| */ | |
| initThreeJS() { | |
| console.log('Initializing Three.js...'); | |
| // Scene | |
| this.scene = new THREE.Scene(); | |
| this.scene.background = new THREE.Color(0x87CEEB); // Sky blue | |
| console.log('Scene created'); | |
| // Camera | |
| this.camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000 | |
| ); | |
| this.camera.position.set(20, 20, 20); | |
| 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; | |
| 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!'); | |
| } | |
| // 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; | |
| } | |
| // Lighting | |
| this.addLighting(); | |
| // Ground plane | |
| this.createGround(); | |
| // Grid helper | |
| const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222); | |
| this.scene.add(gridHelper); | |
| // Handle window resize | |
| window.addEventListener('resize', () => this.onWindowResize()); | |
| window.addEventListener('keydown', (event) => this.onKeyDown(event)); | |
| this.renderer.domElement.addEventListener('click', (event) => this.onMouseClick(event)); | |
| } | |
| /** | |
| * Add lighting to the scene | |
| */ | |
| addLighting() { | |
| // Ambient light | |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.6); | |
| this.scene.add(ambientLight); | |
| // Directional light (sun) | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(10, 10, 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; | |
| this.scene.add(directionalLight); | |
| } | |
| /** | |
| * Create ground plane | |
| */ | |
| createGround() { | |
| const groundGeometry = new THREE.PlaneGeometry(100, 100); | |
| const groundMaterial = new THREE.MeshLambertMaterial({ | |
| color: 0x228B22, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| this.scene.add(ground); | |
| } | |
| /** | |
| * Initialize the AI system | |
| */ | |
| initAI() { | |
| this.aiSystem = new VillageAISystem(this.scene); | |
| } | |
| /** | |
| * Initialize UI event listeners | |
| */ | |
| 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'), | |
| 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') | |
| }; | |
| // Add event listeners | |
| this.uiElements.addVillagerBtn.addEventListener('click', () => this.addVillager()); | |
| this.uiElements.resetBtn.addEventListener('click', () => this.resetSimulation()); | |
| this.uiElements.timeSpeed.addEventListener('input', (e) => this.updateTimeSpeed(e.target.value)); | |
| this.uiElements.showPaths.addEventListener('change', (e) => this.togglePaths(e.target.checked)); | |
| } | |
| /** | |
| * Create the 3D environment (buildings and resources) | |
| */ | |
| createEnvironment() { | |
| // Buildings are already created in the AI system | |
| // We need to create 3D meshes for them | |
| this.createBuildingMeshes(); | |
| this.createResourceMeshes(); | |
| } | |
| /** | |
| * Create 3D meshes for buildings | |
| */ | |
| createBuildingMeshes() { | |
| const buildingGeometry = new THREE.BoxGeometry(3, 3, 3); | |
| const materials = { | |
| house: new THREE.MeshLambertMaterial({ color: 0x8B4513 }), | |
| workshop: new THREE.MeshLambertMaterial({ color: 0x696969 }), | |
| market: new THREE.MeshLambertMaterial({ color: 0xFFD700 }) | |
| }; | |
| for (const [id, building] of this.aiSystem.environmentSystem.buildings) { | |
| const material = materials[building.type] || materials.house; | |
| const mesh = new THREE.Mesh(buildingGeometry, material); | |
| mesh.position.set(building.position[0], building.position[1], building.position[2]); | |
| mesh.position.y = 1.5; // Half height | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| // Add building label | |
| const label = this.createTextSprite(building.type); | |
| label.position.set(0, 2.5, 0); | |
| mesh.add(label); | |
| this.buildingMeshes.set(id, mesh); | |
| this.scene.add(mesh); | |
| } | |
| this.updateBuildingCount(); | |
| } | |
| /** | |
| * Create 3D meshes for resources | |
| */ | |
| 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 }) | |
| }; | |
| 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; // Half height | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| // Add resource label | |
| const label = this.createTextSprite(`${resource.type} (${resource.amount})`); | |
| label.position.set(0, 1.5, 0); | |
| mesh.add(label); | |
| this.resourceMeshes.set(id, mesh); | |
| this.scene.add(mesh); | |
| } | |
| this.updateResourceCount(); | |
| } | |
| /** | |
| * Create initial villagers | |
| */ | |
| createInitialVillagers() { | |
| // Create a few initial villagers at random positions | |
| const positions = [ | |
| [0, 0, 0], | |
| [5, 0, 5], | |
| [-3, 0, -3] | |
| ]; | |
| positions.forEach((position, index) => { | |
| this.createVillager(`villager${index + 1}`, position); | |
| }); | |
| } | |
| /** | |
| * Create a new villager | |
| */ | |
| createVillager(id, position) { | |
| const villager = this.aiSystem.createVillager(id, position); | |
| this.createVillagerMesh(villager); | |
| this.updateVillagerCount(); | |
| return villager; | |
| } | |
| /** | |
| * Create 3D mesh for a villager | |
| */ | |
| createVillagerMesh(villager) { | |
| // Create villager geometry (sphere) | |
| const geometry = new THREE.SphereGeometry(0.5, 16, 16); | |
| const material = new THREE.MeshLambertMaterial({ | |
| color: this.getStateColor(villager.state) | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.position.set(villager.position[0], villager.position[1], villager.position[2]); | |
| mesh.position.y = 0.5; // Half height | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| // Add villager ID label | |
| const label = this.createTextSprite(villager.id); | |
| label.position.set(0, 1.2, 0); | |
| mesh.add(label); | |
| // Store reference to villager in mesh | |
| mesh.userData.villager = villager; | |
| this.villagerMeshes.set(villager.id, mesh); | |
| this.scene.add(mesh); | |
| } | |
| /** | |
| * Get color for villager state | |
| */ | |
| getStateColor(state) { | |
| const colors = { | |
| sleep: 0x7f8c8d, // Gray | |
| work: 0xe74c3c, // Red | |
| eat: 0xf39c12, // Orange | |
| socialize: 0x9b59b6, // Purple | |
| idle: 0x95a5a6 // Light gray | |
| }; | |
| return colors[state] || colors.idle; | |
| } | |
| /** | |
| * Create a text sprite for labels | |
| */ | |
| createTextSprite(text) { | |
| 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(text, canvas.width / 2, canvas.height / 2); | |
| const texture = new THREE.CanvasTexture(canvas); | |
| const spriteMaterial = new THREE.SpriteMaterial({ | |
| map: texture, | |
| transparent: true | |
| }); | |
| const sprite = new THREE.Sprite(spriteMaterial); | |
| sprite.scale.set(3, 1.5, 1); | |
| return sprite; | |
| } | |
| /** | |
| * Add a new villager via UI | |
| */ | |
| addVillager() { | |
| const villagerCount = this.villagerMeshes.size; | |
| const id = `villager${villagerCount + 1}`; | |
| // Random position within bounds | |
| const position = [ | |
| (Math.random() - 0.5) * 40, | |
| 0, | |
| (Math.random() - 0.5) * 40 | |
| ]; | |
| this.createVillager(id, position); | |
| } | |
| /** | |
| * Reset the simulation | |
| */ | |
| 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(); | |
| } | |
| /** | |
| * Update time speed | |
| */ | |
| updateTimeSpeed(speed) { | |
| this.timeSpeed = parseFloat(speed); | |
| this.uiElements.timeSpeedDisplay.textContent = `${speed}x`; | |
| } | |
| /** | |
| * Toggle path visibility | |
| */ | |
| togglePaths(show) { | |
| this.showPaths = show; | |
| for (const [id, line] of this.pathLines) { | |
| line.visible = show; | |
| } | |
| } | |
| /** | |
| * Handle window resize | |
| */ | |
| onWindowResize() { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| /** | |
| * Handle keyboard input | |
| */ | |
| onKeyDown(event) { | |
| console.log('Key pressed:', event.code); | |
| const speed = 0.5; | |
| // Handle keyboard input directly for camera movement | |
| // Even when OrbitControls are available, we want to handle WASD keys | |
| switch (event.code) { | |
| case 'KeyW': | |
| this.camera.position.z -= speed; | |
| this.camera.lookAt(0, 0, 0); | |
| console.log('Moved camera forward'); | |
| break; | |
| case 'KeyS': | |
| this.camera.position.z += speed; | |
| this.camera.lookAt(0, 0, 0); | |
| console.log('Moved camera backward'); | |
| break; | |
| case 'KeyA': | |
| this.camera.position.x -= speed; | |
| this.camera.lookAt(0, 0, 0); | |
| console.log('Moved camera left'); | |
| break; | |
| case 'KeyD': | |
| this.camera.position.x += speed; | |
| this.camera.lookAt(0, 0, 0); | |
| console.log('Moved camera right'); | |
| break; | |
| case 'Space': | |
| this.camera.position.y += speed; | |
| this.camera.lookAt(0, 0, 0); | |
| console.log('Moved camera up'); | |
| break; | |
| case 'ShiftLeft': | |
| this.camera.position.y -= speed; | |
| this.camera.lookAt(0, 0, 0); | |
| console.log('Moved camera down'); | |
| break; | |
| } | |
| } | |
| /** | |
| * Handle mouse clicks for villager selection | |
| */ | |
| onMouseClick(event) { | |
| const mouse = { x: 0, y: 0 }; | |
| 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()); | |
| const intersects = raycaster.intersectObjects(villagerMeshes); | |
| if (intersects.length > 0) { | |
| const selectedMesh = intersects[0].object; | |
| this.selectedVillager = selectedMesh.userData.villager; | |
| this.updateVillagerInfo(); | |
| } | |
| } | |
| /** | |
| * Update villager count display | |
| */ | |
| updateVillagerCount() { | |
| const count = this.villagerMeshes.size; | |
| this.uiElements.villagerCountDisplay.textContent = count; | |
| this.uiElements.villagerCountStat.textContent = count; | |
| } | |
| /** | |
| * Update building count display | |
| */ | |
| updateBuildingCount() { | |
| const count = this.buildingMeshes.size; | |
| this.uiElements.buildingCount.textContent = count; | |
| } | |
| /** | |
| * Update resource count display | |
| */ | |
| updateResourceCount() { | |
| const count = this.resourceMeshes.size; | |
| this.uiElements.resourceCount.textContent = count; | |
| } | |
| /** | |
| * Update villager information panel | |
| */ | |
| updateVillagerInfo() { | |
| const villagerList = this.uiElements.villagerList; | |
| 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> | |
| `; | |
| } | |
| /** | |
| * Update path visualization for a villager | |
| */ | |
| 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); | |
| } | |
| } | |
| /** | |
| * Update game time display | |
| */ | |
| updateGameTime() { | |
| const time = this.aiSystem.routineManager.currentTime; | |
| const hours = Math.floor(time); | |
| const minutes = Math.floor((time - hours) * 60); | |
| this.uiElements.gameTime.textContent = `${hours}:${minutes.toString().padStart(2, '0')}`; | |
| } | |
| /** | |
| * Update FPS counter | |
| */ | |
| 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; | |
| this.uiElements.fps.textContent = this.fps; | |
| } | |
| } | |
| /** | |
| * Animation loop | |
| */ | |
| animate() { | |
| requestAnimationFrame(() => this.animate()); | |
| const deltaTime = this.clock.getDelta() * this.timeSpeed; | |
| // Update AI system | |
| this.aiSystem.update(deltaTime); | |
| // Update 3D visualization | |
| this.updateVillagerMeshes(); | |
| this.updatePathVisualizations(); | |
| // Update UI | |
| this.updateGameTime(); | |
| this.updateFPS(); | |
| this.updateVillagerInfo(); | |
| // Update controls | |
| if (this.controls) { | |
| // console.log('Updating controls'); | |
| this.controls.update(); | |
| } | |
| // Render scene | |
| if (this.renderer && this.scene && this.camera) { | |
| this.renderer.render(this.scene, this.camera); | |
| } else { | |
| console.error('Missing renderer, scene, or camera for rendering'); | |
| } | |
| } | |
| /** | |
| * Update villager mesh positions and colors | |
| */ | |
| 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.5; | |
| // Update color based on state | |
| const material = mesh.material; | |
| const newColor = this.getStateColor(villager.state); | |
| if (material.color.getHex() !== newColor) { | |
| material.color.setHex(newColor); | |
| } | |
| } | |
| } | |
| /** | |
| * Update all path visualizations | |
| */ | |
| updatePathVisualizations() { | |
| for (const [villagerId, mesh] of this.villagerMeshes) { | |
| const villager = mesh.userData.villager; | |
| this.updatePathVisualization(villager); | |
| } | |
| } | |
| } | |
| // Initialize the application when the page loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const app = new VillageVisualizationApp(); | |
| }); |