Spaces:
Running
Running
// 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 = '<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> | |
`; | |
} | |
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); | |
}); |