| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Creature Life Simulator</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: #f0f4f8; |
| } |
| canvas { |
| border-radius: 1rem; |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
| background: linear-gradient(to bottom right, #e2e8f0, #cbd5e1); |
| } |
| </style> |
| </head> |
| <body class="p-4 flex flex-col items-center min-h-screen"> |
|
|
| <div class="container mx-auto p-8 bg-white rounded-3xl shadow-xl flex flex-col lg:flex-row items-center lg:items-start space-y-8 lg:space-y-0 lg:space-x-8"> |
| <div class="flex-1 w-full flex flex-col items-center space-y-6"> |
| <h1 class="text-4xl font-bold text-gray-800 tracking-wide">Creature Simulation</h1> |
| <canvas id="creatureCanvas" class="w-full h-[500px] lg:h-[700px] border-4 border-gray-200"></canvas> |
| |
| <div id="controls" class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4"> |
| <button id="newLifeButton" class="px-6 py-3 bg-indigo-600 text-white font-bold rounded-full shadow-lg hover:bg-indigo-700 transition-all duration-300"> |
| Create New Life |
| </button> |
| <button id="feedButton" class="px-6 py-3 bg-green-500 text-white font-bold rounded-full shadow-lg hover:bg-green-600 transition-all duration-300"> |
| Provide Food |
| </button> |
| </div> |
| </div> |
|
|
| <div class="w-full lg:w-1/3 flex flex-col space-y-8"> |
| <div class="bg-indigo-500 text-white p-6 rounded-2xl shadow-lg"> |
| <h2 class="text-2xl font-bold mb-2">Life Clock</h2> |
| <div id="lifeClock" class="text-right"> |
| <p class="text-xl font-medium">Time: <span id="time">--:--:--</span></p> |
| <p class="text-xl font-medium">Date: <span id="date">--/--/----</span></p> |
| <p class="text-5xl font-extrabold mt-4 animate-pulse">Age: <span id="ageDisplay">0</span></p> |
| </div> |
| </div> |
|
|
| <div class="bg-gray-100 p-6 rounded-2xl shadow-lg flex-1"> |
| <h2 class="text-2xl font-bold text-gray-800 mb-4">Creature's Mind & Body</h2> |
| <div id="thoughtPanel" class="text-gray-600 mb-6 min-h-[4rem] flex items-center justify-center text-center italic"> |
| <p>A new life is waiting to be born...</p> |
| </div> |
| <div id="statusPanel" class="space-y-3 text-sm text-gray-700"> |
| <p class="font-bold text-gray-800">Status:</p> |
| <div class="flex items-center space-x-2"> |
| <span class="font-medium">State:</span> |
| <span id="stateDisplay" class="font-bold text-indigo-600">Dormant</span> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <span class="font-medium">Health:</span> |
| <div class="w-3/4 bg-gray-300 rounded-full h-2.5"> |
| <div id="healthBar" class="bg-red-500 h-2.5 rounded-full transition-all duration-500" style="width: 100%;"></div> |
| </div> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <span class="font-medium">Hunger:</span> |
| <div class="w-3/4 bg-gray-300 rounded-full h-2.5"> |
| <div id="hungerBar" class="bg-yellow-500 h-2.5 rounded-full transition-all duration-500" style="width: 0%;"></div> |
| </div> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <span class="font-medium">Energy:</span> |
| <div class="w-3/4 bg-gray-300 rounded-full h-2.5"> |
| <div id="energyBar" class="bg-blue-500 h-2.5 rounded-full transition-all duration-500" style="width: 100%;"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="messageModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center p-4"> |
| <div class="bg-white p-8 rounded-xl shadow-2xl text-center w-full max-w-md"> |
| <p id="modalMessage" class="text-xl font-bold text-gray-800 mb-4"></p> |
| <button id="modalClose" class="px-6 py-2 bg-indigo-600 text-white rounded-full hover:bg-indigo-700 transition-all">OK</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| |
| const canvas = document.getElementById('creatureCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const newLifeButton = document.getElementById('newLifeButton'); |
| const feedButton = document.getElementById('feedButton'); |
| const ageDisplay = document.getElementById('ageDisplay'); |
| const timeDisplay = document.getElementById('time'); |
| const dateDisplay = document.getElementById('date'); |
| const thoughtPanel = document.getElementById('thoughtPanel'); |
| const stateDisplay = document.getElementById('stateDisplay'); |
| const healthBar = document.getElementById('healthBar'); |
| const hungerBar = document.getElementById('hungerBar'); |
| const energyBar = document.getElementById('energyBar'); |
| const messageModal = document.getElementById('messageModal'); |
| const modalMessage = document.getElementById('modalMessage'); |
| const modalCloseButton = document.getElementById('modalClose'); |
| |
| let creature = null; |
| let animationFrameId = null; |
| let gameTime = 0; |
| |
| |
| const SIMULATION_SPEED = 100; |
| const CELL_SIZE = 5; |
| const MAX_AGE_DAYS_MIN = 30; |
| const MAX_AGE_DAYS_MAX = 120; |
| const MAX_HEALTH = 100; |
| const MAX_HUNGER = 100; |
| const MAX_ENERGY = 100; |
| const HUNGER_GAIN_RATE = 0.05 + Math.random() * 0.1; |
| const ENERGY_DRAIN_RATE = 0.08 + Math.random() * 0.12; |
| |
| |
| const FOOD_COUNT = 50; |
| let foodParticles = []; |
| |
| |
| class Creature { |
| constructor() { |
| this.age = 0; |
| this.ageInDays = 0; |
| this.size = CELL_SIZE; |
| this.health = MAX_HEALTH; |
| this.hunger = 0; |
| this.energy = MAX_ENERGY; |
| this.state = 'cell'; |
| this.color = '#3b82f6'; |
| this.x = canvas.width / 2; |
| this.y = canvas.height / 2; |
| this.maxAge = (Math.floor(Math.random() * (MAX_AGE_DAYS_MAX - MAX_AGE_DAYS_MIN + 1)) + MAX_AGE_DAYS_MIN) * 100; |
| this.thought = "I am a single cell, waiting to be born..."; |
| } |
| |
| update() { |
| |
| if (this.state === 'dead' || this.state === 'dying') { |
| return; |
| } |
| |
| this.age++; |
| this.ageInDays = Math.floor(this.age / 100); |
| |
| |
| this.hunger += HUNGER_GAIN_RATE * (1 + Math.random()); |
| this.energy -= ENERGY_DRAIN_RATE * (1 + Math.random() * 0.5); |
| |
| |
| this.hunger = Math.min(this.hunger, MAX_HUNGER); |
| this.energy = Math.max(0, Math.min(this.energy, MAX_ENERGY)); |
| |
| |
| |
| let decisionMade = false; |
| |
| |
| if (this.hunger > 80 && Math.random() < 0.9) { |
| this.state = 'eating'; |
| this.thought = 'I must find nourishment to survive.'; |
| decisionMade = true; |
| } |
| |
| |
| if (!decisionMade && this.energy < 20 && Math.random() < 0.8) { |
| this.state = 'sleeping'; |
| this.thought = 'My body is tired. I need to rest.'; |
| decisionMade = true; |
| } |
| |
| |
| if (!decisionMade && this.ageInDays > 2 && Math.random() < 0.7) { |
| if (this.ageInDays < 50) { |
| this.state = 'growing'; |
| this.size += 0.05; |
| this.thought = 'I feel my body changing and growing.'; |
| } else { |
| this.state = 'exploring'; |
| this.thought = 'The world is so big. What is that over there?'; |
| } |
| decisionMade = true; |
| } |
| |
| |
| if (!decisionMade) { |
| this.state = 'wandering'; |
| this.thought = 'Just drifting through the void...'; |
| } |
| |
| |
| if (this.state === 'eating') { |
| |
| if (foodParticles.length > 0) { |
| const nearestFood = foodParticles.reduce((prev, curr) => |
| Math.hypot(this.x - curr.x, this.y - curr.y) < Math.hypot(this.x - prev.x, this.y - prev.y) ? curr : prev |
| ); |
| const dx = nearestFood.x - this.x; |
| const dy = nearestFood.y - this.y; |
| const dist = Math.hypot(dx, dy); |
| this.x += (dx / dist) * 2; |
| this.y += (dy / dist) * 2; |
| |
| |
| if (dist < this.size) { |
| this.hunger = Math.max(0, this.hunger - 30); |
| foodParticles.splice(foodParticles.indexOf(nearestFood), 1); |
| this.thought = 'Mmm, delicious food!'; |
| } |
| } else { |
| |
| this.hunger += 1; |
| this.state = 'wandering'; |
| this.thought = 'I am hungry, but there is nothing to eat!'; |
| } |
| } else if (this.state === 'sleeping') { |
| this.energy = Math.min(MAX_ENERGY, this.energy + 2); |
| this.hunger += 0.1; |
| this.thought = 'Zzz... zzz...'; |
| } else if (this.state === 'wandering' || this.state === 'exploring' || this.state === 'growing') { |
| |
| this.x += (Math.random() - 0.5) * 4; |
| this.y += (Math.random() - 0.5) * 4; |
| } |
| |
| |
| if (this.x < this.size || this.x > canvas.width - this.size) this.x = Math.min(Math.max(this.size, this.x), canvas.width - this.size); |
| if (this.y < this.size || this.y > canvas.height - this.size) this.y = Math.min(Math.max(this.size, this.y), canvas.height - this.size); |
| |
| |
| if (this.age > this.maxAge) { |
| this.state = 'dying'; |
| this.health -= 0.5; |
| this.thought = 'I feel weak. My time is coming to an end.'; |
| } else if (this.hunger > 95) { |
| this.health -= 1.0; |
| this.thought = 'My body is starving...'; |
| } else if (this.energy <= 0) { |
| this.health -= 0.5; |
| this.thought = 'I am completely exhausted...'; |
| } else { |
| this.health = Math.min(MAX_HEALTH, this.health + 0.1); |
| } |
| |
| if (this.health <= 0) { |
| this.state = 'dead'; |
| this.thought = `A new life has ended after ${this.ageInDays} days.`; |
| cancelAnimationFrame(animationFrameId); |
| showMessage("The creature has died of natural causes."); |
| } |
| } |
| |
| draw() { |
| |
| ctx.beginPath(); |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); |
| ctx.fillStyle = this.color; |
| ctx.shadowColor = 'rgba(0,0,0,0.2)'; |
| ctx.shadowBlur = 10; |
| ctx.fill(); |
| ctx.closePath(); |
| |
| |
| if (this.ageInDays <= 1) { |
| const alpha = 0.5 + Math.sin(this.age * 0.1) * 0.5; |
| ctx.fillStyle = `rgba(59, 130, 246, ${alpha})`; |
| ctx.shadowColor = `rgba(59, 130, 246, ${alpha})`; |
| ctx.fill(); |
| } |
| } |
| } |
| |
| |
| class Food { |
| constructor() { |
| this.x = Math.random() * canvas.width; |
| this.y = Math.random() * canvas.height; |
| this.size = 2 + Math.random() * 3; |
| this.color = '#10b981'; |
| } |
| draw() { |
| ctx.beginPath(); |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); |
| ctx.fillStyle = this.color; |
| ctx.fill(); |
| ctx.closePath(); |
| } |
| } |
| |
| |
| |
| function generateFood() { |
| for (let i = 0; i < FOOD_COUNT; i++) { |
| foodParticles.push(new Food()); |
| } |
| } |
| |
| function showMessage(message) { |
| modalMessage.textContent = message; |
| messageModal.classList.remove('hidden'); |
| messageModal.classList.add('flex'); |
| } |
| |
| function updateUI() { |
| if (creature) { |
| ageDisplay.textContent = creature.ageInDays; |
| stateDisplay.textContent = creature.state.charAt(0).toUpperCase() + creature.state.slice(1); |
| thoughtPanel.textContent = creature.thought; |
| healthBar.style.width = `${Math.max(0, creature.health)}%`; |
| hungerBar.style.width = `${creature.hunger}%`; |
| energyBar.style.width = `${creature.energy}%`; |
| |
| |
| healthBar.classList.toggle('bg-red-500', creature.health > 50); |
| healthBar.classList.toggle('bg-red-700', creature.health <= 50); |
| healthBar.classList.toggle('bg-red-900', creature.health <= 20); |
| |
| hungerBar.classList.toggle('bg-yellow-500', creature.hunger < 50); |
| hungerBar.classList.toggle('bg-yellow-700', creature.hunger >= 50); |
| |
| energyBar.classList.toggle('bg-blue-500', creature.energy > 50); |
| energyBar.classList.toggle('bg-blue-700', creature.energy <= 50); |
| |
| } else { |
| ageDisplay.textContent = '0'; |
| stateDisplay.textContent = 'Dormant'; |
| thoughtPanel.textContent = 'A new life is waiting to be born...'; |
| healthBar.style.width = '100%'; |
| hungerBar.style.width = '0%'; |
| energyBar.style.width = '100%'; |
| } |
| } |
| |
| function updateTime() { |
| const now = new Date(); |
| timeDisplay.textContent = now.toLocaleTimeString(); |
| dateDisplay.textContent = now.toLocaleDateString(); |
| } |
| |
| function gameLoop() { |
| if (creature) { |
| |
| creature.update(); |
| |
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| foodParticles.forEach(food => food.draw()); |
| |
| |
| creature.draw(); |
| } |
| |
| updateUI(); |
| animationFrameId = requestAnimationFrame(gameLoop); |
| } |
| |
| function startNewLife() { |
| if (creature && creature.state !== 'dead') { |
| |
| showMessage("A creature is already living. Let it live out its life or refresh the page."); |
| return; |
| } |
| |
| |
| if (animationFrameId) { |
| cancelAnimationFrame(animationFrameId); |
| } |
| canvas.width = canvas.clientWidth; |
| canvas.height = canvas.clientHeight; |
| creature = new Creature(); |
| foodParticles = []; |
| generateFood(); |
| |
| showMessage("A new life has been created! Watch it grow."); |
| gameLoop(); |
| } |
| |
| |
| newLifeButton.addEventListener('click', startNewLife); |
| feedButton.addEventListener('click', () => { |
| if (creature && creature.state !== 'dead') { |
| for(let i=0; i<10; i++) foodParticles.push(new Food()); |
| showMessage("More food has been added to the world!"); |
| } |
| }); |
| modalCloseButton.addEventListener('click', () => { |
| messageModal.classList.remove('flex'); |
| messageModal.classList.add('hidden'); |
| }); |
| |
| |
| updateTime(); |
| setInterval(updateTime, 1000); |
| |
| |
| window.addEventListener('resize', () => { |
| canvas.width = canvas.clientWidth; |
| canvas.height = canvas.clientHeight; |
| }); |
| |
| |
| canvas.width = canvas.clientWidth; |
| canvas.height = canvas.clientHeight; |
| }); |
| </script> |
| </body> |
| </html> |