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

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}
`; } /** * 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(); });