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