6rz6
Add Medieval Village AI Emulator
a32dc8b
// 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();
});