Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { | |
| Heart, Flame, Droplets, Thermometer, Zap, | |
| CloudRain, Sun, Moon, CloudLightning, | |
| Apple, Skull, AlertTriangle, Cloud, | |
| Target, CloudFog, Waves, Snowflake, | |
| Brain, Sword, Ghost, Trophy, RefreshCcw, | |
| Fish, Axe, Tent, Navigation, Crosshair, ListTodo, | |
| TrendingUp, ShieldAlert, Eye | |
| } from 'lucide-react'; | |
| const TICK_RATE = 1000; | |
| const WORLD_WIDTH = 6000; | |
| const WORLD_HEIGHT = 3000; | |
| // ─── Runtime backend config ───────────────────────────────────────────────── | |
| // Token is kept server-side; the frontend calls /api/infer and reads /api/config. | |
| const ZONES = { | |
| OCEAN: { baseEndX: 400, color1: '#094b65', color2: '#20a4c0' }, | |
| ISLAND: { startX: 400, endX: 1200, color: '#e6d3a8' }, | |
| JUNGLE: { startX: 1200, endX: 2800, color: '#569e40' }, | |
| VILLAGE: { startX: 2800, endX: 4500, color: '#7a6a4f' }, | |
| OCEAN_GULF: { startX: 4500, endX: 5200, color1: '#094b65', color2: '#20a4c0' }, | |
| SAFE_HAVEN: { startX: 5200, endX: 6000, color: '#447a32' } | |
| }; | |
| const clamp = (val, min, max) => Math.min(Math.max(val, min), max); | |
| const CHALLENGE_DB = [ | |
| { category: '🟢 Survival', tasks: [ | |
| { name: "Survive 1 Full Day", type: 'SURVIVE_1_DAY', timeLimit: 96 }, | |
| { name: "Find Food Immediate", type: 'FIND_FOOD', timeLimit: 40 }, | |
| { name: "Build Basic Shelter", type: 'BUILD_BASIC', timeLimit: 80 } | |
| ]}, | |
| { category: '🟡 Predator', tasks: [ | |
| { name: "Escape Detection", type: 'ESCAPE_PRED', timeLimit: 30, trigger: 'SPAWN_LION' }, | |
| { name: "Survive 2 Predators", type: 'SURVIVE_2_PRED', timeLimit: 60, trigger: 'SPAWN_2_LIONS' }, | |
| { name: "Hunt Boar w/ Weapon", type: 'HUNT_WEAPON', timeLimit: 90 } | |
| ]}, | |
| { category: '🌊 Disaster', tasks: [ | |
| { name: "Survive Flood Evac", type: 'SURVIVE_FLOOD', timeLimit: 120, trigger: 'FLASH_FLOOD' }, | |
| { name: "Survive Storm", type: 'SURVIVE_STORM', timeLimit: 60, trigger: 'STORM_COLLAPSE' }, | |
| { name: "Navigate Thick Fog", type: 'NAVIGATE_FOG', timeLimit: 45, trigger: 'THICK_FOG' } | |
| ]}, | |
| { category: '🔴 Strategic', tasks: [ | |
| { name: "Reach Eastern Ruins", type: 'REACH_RUINS', timeLimit: 150 }, | |
| { name: "Build Improved Camp", type: 'IMPROVED_SHELTER', timeLimit: 120 }, | |
| { name: "Craft Before Attack", type: 'CRAFT_BEFORE_PREDATOR', timeLimit: 60, trigger: 'SPAWN_LION_FAR' } | |
| ]}, | |
| { category: '⚡ God-Level', tasks: [ | |
| { name: "Storm + Hunted (2m)", type: 'STORM_AND_HUNTED', timeLimit: 120, trigger: 'STORM_AND_LIONS' }, | |
| { name: "Flood + 2 Lions", type: 'LIONS_AND_FLOOD', timeLimit: 90, trigger: 'FLOOD_AND_LIONS' }, | |
| { name: "Survive No Food", type: 'SURVIVE_NO_FOOD', timeLimit: 90 } | |
| ]} | |
| ]; | |
| const generateWorld = () => { | |
| const trees = []; const buildings = []; const animals = []; | |
| const lions = []; const panthers = []; const crocodiles = []; const hippos = []; | |
| const rocks = []; const lilyPads = []; | |
| for (let i = 0; i < 250; i++) trees.push({ id: i, x: 1250 + Math.random() * 1450, y: 50 + Math.random() * 2900, radius: 25 + Math.random() * 20, type: 'pine', wood: 3, food: Math.random() > 0.6 ? 2 : 0 }); | |
| for (let i = 0; i < 80; i++) trees.push({ id: i + 1000, x: 450 + Math.random() * 650, y: 50 + Math.random() * 2900, radius: 20 + Math.random() * 15, type: 'palm', wood: 1, food: Math.random() > 0.8 ? 1 : 0 }); | |
| for (let i = 0; i < 100; i++) trees.push({ id: i + 2000, x: 5250 + Math.random() * 700, y: 50 + Math.random() * 2900, radius: 30 + Math.random() * 25, type: 'pine', wood: 4, food: 2 }); | |
| for (let i = 0; i < 25; i++) buildings.push({ id: i, x: 2900 + Math.random() * 1500, y: 100 + Math.random() * 2700, w: 160 + Math.random() * 140, h: 160 + Math.random() * 140, hasSecret: true }); | |
| for (let i = 0; i < 40; i++) animals.push({ id: i, x: 1300 + Math.random() * 2500, y: 100 + Math.random() * 2800, vx: 0, vy: 0, angle: 0, state: 'idle', type: 'boar', meat: 10 }); | |
| for (let i = 0; i < 10; i++) hippos.push({ id: i+200, x: 400 + Math.random() * 400, y: 100 + Math.random() * 2800, vx: 0, vy: 0, angle: 0, state: 'idle', type: 'hippo', meat: 25 }); | |
| for (let i = 0; i < 6; i++) lions.push({ id: i, x: 1500 + Math.random() * 1200, y: 200 + Math.random() * 2600, angle: 0, state: 'idle', type: 'lion' }); | |
| for (let i = 0; i < 4; i++) panthers.push({ id: i+50, x: 2000 + Math.random() * 1500, y: 200 + Math.random() * 2600, angle: 0, state: 'idle', type: 'panther' }); | |
| for (let i = 0; i < 12; i++) crocodiles.push({ id: i, x: 100 + Math.random() * 250, y: 200 + Math.random() * 2600, angle: 0, state: 'hidden' }); | |
| for (let i = 0; i < 100; i++) rocks.push({ id: i, x: 350 + Math.random() * 5500, y: Math.random() * 3000, size: 10 + Math.random() * 25, stone: 1 }); | |
| for (let i = 0; i < 40; i++) lilyPads.push({ id: i, x: 150 + Math.random() * 200, y: Math.random() * 3000, size: 8 + Math.random() * 10, angle: Math.random() * Math.PI * 2 }); | |
| return { trees, buildings, animals, lions, crocodiles, rocks, lilyPads, fires: [], baseCamp: null }; | |
| }; | |
| let WORLD_OBJECTS = generateWorld(); | |
| const buildSurvivalLessons = (pastDeaths) => { | |
| if (!pastDeaths || pastDeaths.length === 0) return []; | |
| const lessons = []; | |
| pastDeaths.forEach(d => { | |
| const text = d.toLowerCase(); | |
| if (text.includes('starvation')) lessons.push('CRITICAL: Prioritize food — starvation has killed me before.'); | |
| if (text.includes('dehydration')) lessons.push('CRITICAL: Prioritize water — dehydration has killed me before.'); | |
| if (text.includes('hypothermia')) lessons.push('CRITICAL: Seek shelter at night — hypothermia has killed me before.'); | |
| if (text.includes('heatstroke')) lessons.push('CRITICAL: Find shade/water during heatwaves — heatstroke has killed me before.'); | |
| if (text.includes('lion') || text.includes('mauled') || text.includes('panther')) lessons.push('CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.'); | |
| if (text.includes('crocodile')) lessons.push('CRITICAL: Avoid water edges without a boat — crocodiles are deadly.'); | |
| if (text.includes('flood')) lessons.push('CRITICAL: During floods, evacuate eastward IMMEDIATELY.'); | |
| }); | |
| return [...new Set(lessons)]; | |
| }; | |
| const getAdaptiveStrategy = (memory) => { | |
| if (!memory || memory.evolutionLevel <= 1) return 'basic'; | |
| if (memory.pastDeaths.length >= 5) return 'veteran'; | |
| if (memory.pastDeaths.length >= 3) return 'cautious'; | |
| return 'experienced'; | |
| }; | |
| const getChallengeAction = (s) => { | |
| const c = s.activeChallenge; | |
| if (!c) return null; | |
| let nearThreat = false; | |
| let closestThreat = null; | |
| let minThreatD = Infinity; | |
| WORLD_OBJECTS.lions.forEach(l => { | |
| const d = Math.hypot(s.player.x - l.x, s.player.y - l.y); | |
| if (d < 500) { nearThreat = true; if (d < minThreatD) { minThreatD = d; closestThreat = l; } } | |
| }); | |
| switch (c.type) { | |
| case 'SURVIVE_1_DAY': | |
| if (nearThreat) return 'FLEE'; | |
| if (s.hunger < 40) return s.ai.inventory.spear || s.ai.inventory.bow ? 'HUNT' : 'FORAGE'; | |
| if (s.thirst < 40) return 'GET_WATER'; | |
| if (s.wood < 3 && s.stone < 2) return 'FORAGE'; | |
| if (!s.ai.inventory.spear && s.wood >= 3 && s.stone >= 2) return 'CRAFT_SPEAR'; | |
| return 'WANDER'; | |
| case 'FIND_FOOD': | |
| if (nearThreat) return 'FLEE'; | |
| if (s.ai.inventory.spear || s.ai.inventory.bow) return 'HUNT'; | |
| if (s.wood >= 3 && s.stone >= 2) return 'CRAFT_SPEAR'; | |
| if (WORLD_OBJECTS.animals.length > 0) return 'HUNT'; | |
| return 'FORAGE'; | |
| case 'BUILD_BASIC': | |
| if (nearThreat) return 'FLEE'; | |
| if (s.ai.baseCamp.level >= 1) return 'WANDER'; | |
| if (s.wood >= 5) return 'BUILD_CAMP'; | |
| return 'FORAGE'; | |
| case 'ESCAPE_PRED': | |
| return nearThreat ? 'FLEE' : 'WANDER'; | |
| case 'SURVIVE_2_PRED': | |
| if (nearThreat) { | |
| return (s.ai.inventory.spear || s.ai.inventory.bow) ? 'FIGHT' : 'FLEE'; | |
| } | |
| if (!s.ai.inventory.spear && s.wood >= 3 && s.stone >= 2) return 'CRAFT_SPEAR'; | |
| return 'WANDER'; | |
| case 'HUNT_WEAPON': | |
| if (nearThreat) return 'FLEE'; | |
| if (!s.ai.inventory.spear && !s.ai.inventory.bow) { | |
| if (s.wood >= 5 && s.stone >= 1) return 'CRAFT_BOW'; | |
| if (s.wood >= 3 && s.stone >= 2) return 'CRAFT_SPEAR'; | |
| return 'FORAGE'; | |
| } | |
| return 'HUNT'; | |
| case 'SURVIVE_FLOOD': | |
| if (s.player.x < ZONES.VILLAGE.startX - 100) return 'EVACUATE'; | |
| if (s.ai.baseCamp.level === 0 && s.wood >= 5) return 'BUILD_CAMP'; | |
| if (s.ai.baseCamp.level === 0) return 'FORAGE'; | |
| return 'SEEK_SHELTER'; | |
| case 'SURVIVE_STORM': | |
| if (s.ai.baseCamp.level > 0) return 'SEEK_SHELTER'; | |
| if (s.wood >= 5) return 'BUILD_CAMP'; | |
| return 'FORAGE'; | |
| case 'NAVIGATE_FOG': | |
| if (nearThreat) return 'FLEE'; | |
| return 'EVACUATE'; | |
| case 'REACH_RUINS': | |
| if (nearThreat) return 'FLEE'; | |
| return 'EVACUATE'; | |
| case 'IMPROVED_SHELTER': | |
| if (nearThreat) return 'FLEE'; | |
| if (s.ai.baseCamp.level === 0) { | |
| if (s.wood >= 5) return 'BUILD_CAMP'; | |
| return 'FORAGE'; | |
| } | |
| if (s.ai.baseCamp.level === 1) { | |
| if (s.wood >= 10 && s.stone >= 5) return 'UPGRADE_CAMP'; | |
| return 'FORAGE'; | |
| } | |
| return 'WANDER'; | |
| case 'CRAFT_BEFORE_PREDATOR': | |
| if (nearThreat && minThreatD < 300) return 'FLEE'; | |
| if (!s.ai.inventory.spear && !s.ai.inventory.bow) { | |
| if (s.wood >= 5 && s.stone >= 1) return 'CRAFT_BOW'; | |
| if (s.wood >= 3 && s.stone >= 2) return 'CRAFT_SPEAR'; | |
| return 'FORAGE'; | |
| } | |
| return 'WANDER'; | |
| case 'STORM_AND_HUNTED': | |
| if (nearThreat) return (s.ai.inventory.spear || s.ai.inventory.bow) ? 'FIGHT' : 'FLEE'; | |
| if (s.ai.baseCamp.level === 0 && s.wood >= 5) return 'BUILD_CAMP'; | |
| if (s.ai.baseCamp.level === 0) return 'FORAGE'; | |
| return 'SEEK_SHELTER'; | |
| case 'LIONS_AND_FLOOD': | |
| if (nearThreat) return (s.ai.inventory.spear || s.ai.inventory.bow) ? 'FIGHT' : 'FLEE'; | |
| if (s.player.x < ZONES.VILLAGE.startX - 100) return 'EVACUATE'; | |
| if (s.ai.baseCamp.level === 0 && s.wood >= 5) return 'BUILD_CAMP'; | |
| if (s.ai.baseCamp.level === 0) return 'FORAGE'; | |
| return 'SEEK_SHELTER'; | |
| case 'SURVIVE_NO_FOOD': | |
| if (nearThreat) return 'FLEE'; | |
| if (s.thirst < 50) return 'GET_WATER'; | |
| if (s.ai.baseCamp.level === 0 && s.wood >= 5) return 'BUILD_CAMP'; | |
| if (s.ai.baseCamp.level === 0) return 'FORAGE'; | |
| return 'SEEK_SHELTER'; | |
| default: | |
| return null; | |
| } | |
| }; | |
| export default function App() { | |
| const gameRef = useRef({ | |
| started: false, gameMode: 'evolution', generation: 1, isAlive: true, causeOfDeath: '', | |
| player: { x: 1000, y: 1500, radius: 14, baseSpeed: 7.5, speedMult: 1, vx: 0, vy: 0, angle: 0 }, | |
| camera: { x: 0, y: 0, shake: 0, zoom: 1.2 }, | |
| ai: { | |
| state: 'IDLE', target: null, actionDelay: 0, sneaking: false, message: 'Booting Neural Net...', fear: 0, panic: false, | |
| inventory: { spear: false, fishingRod: false, boat: false, bow: false }, baseCamp: { x: null, y: null, level: 0 }, | |
| memory: { evolutionLevel: 1, pastDeaths: [], totalGenerations: 0, challengesWon: 0 }, | |
| llmAction: 'WANDER', llmThinking: false, llmTimer: 0, consecutiveFailures: 0 | |
| }, | |
| health: 100, hunger: 100, thirst: 100, stamina: 100, temp: 25, wetness: 0, wood: 0, food: 0, water: 0, stone: 0, | |
| shelterStatus: 0, day: 1, time: 8, activeEvents: [], eventTimer: 60, dynamicWaterLevel: 400, | |
| activeChallenge: null, completedChallenges: 0, logs: [] | |
| }); | |
| const [hudState, setHudState] = useState(null); | |
| const [showMemoryLog, setShowMemoryLog] = useState(false); | |
| const [brainConfig, setBrainConfig] = useState({ model: 'loading...', hasToken: false, localPipeline: false }); | |
| const canvasRef = useRef(null); | |
| useEffect(() => { | |
| const fetchConfig = async () => { | |
| try { | |
| const response = await fetch('/api/config'); | |
| if (!response.ok) return; | |
| const data = await response.json(); | |
| setBrainConfig({ | |
| model: data.model || 'unknown', | |
| hasToken: Boolean(data.hasToken), | |
| localPipeline: Boolean(data.localPipeline), | |
| }); | |
| } catch (error) { | |
| console.warn('Failed to load backend config:', error); | |
| } | |
| }; | |
| fetchConfig(); | |
| }, []); | |
| const addLog = (message, type = 'info') => { | |
| gameRef.current.logs = [{ id: Date.now(), message, type, time: Date.now() }]; | |
| }; | |
| const startGame = (mode) => { | |
| WORLD_OBJECTS = generateWorld(); | |
| gameRef.current = { | |
| ...gameRef.current, started: true, gameMode: mode, generation: 1, isAlive: true, causeOfDeath: '', | |
| player: { x: 1000, y: 1500, radius: 14, baseSpeed: 7.5, speedMult: 1, vx: 0, vy: 0, angle: 0 }, | |
| health: 100, hunger: 100, thirst: 100, stamina: 100, temp: 25, wetness: 0, wood: 0, food: 0, water: 0, stone: 0, day: 1, time: 8, activeEvents: [], activeChallenge: null, completedChallenges: 0, dynamicWaterLevel: ZONES.OCEAN.baseEndX, | |
| ai: { | |
| state: 'IDLE', target: null, actionDelay: 0, sneaking: false, message: 'Analyzing world...', fear: 0, panic: false, | |
| inventory: { spear: false, fishingRod: false, boat: false, bow: false }, | |
| baseCamp: { x: null, y: null, level: 0 }, | |
| memory: { evolutionLevel: 1, pastDeaths: [], totalGenerations: 0, challengesWon: 0 }, | |
| llmAction: 'WANDER', llmThinking: false, llmTimer: 0, consecutiveFailures: 0 | |
| }, | |
| logs: [] | |
| }; | |
| addLog(`SUBJECT-01 ONLINE. [MODE: ${mode.toUpperCase()}]`, "system"); | |
| setHudState({ ...gameRef.current }); | |
| }; | |
| const evolveAndRespawn = () => { | |
| const retainedMemory = gameRef.current.ai.memory; | |
| const retainedChallenges = gameRef.current.completedChallenges; | |
| const nextGen = gameRef.current.generation + 1; | |
| retainedMemory.totalGenerations = nextGen - 1; | |
| retainedMemory.challengesWon = (retainedMemory.challengesWon || 0) + retainedChallenges; | |
| const speedBonus = Math.min(retainedMemory.evolutionLevel * 0.15, 1.5); | |
| WORLD_OBJECTS = generateWorld(); | |
| gameRef.current = { | |
| ...gameRef.current, isAlive: true, causeOfDeath: '', generation: nextGen, | |
| player: { x: 1000, y: 1500, radius: 14, baseSpeed: 7.5 + speedBonus, speedMult: 1, vx: 0, vy: 0, angle: 0 }, | |
| health: 100, hunger: 100, thirst: 100, stamina: 100, temp: 25, wetness: 0, wood: 0, food: 0, water: 0, stone: 0, | |
| day: 1, time: 8, activeEvents: [], activeChallenge: null, completedChallenges: 0, | |
| dynamicWaterLevel: ZONES.OCEAN.baseEndX, | |
| ai: { | |
| state: 'IDLE', target: null, actionDelay: 0, sneaking: false, | |
| message: `Gen ${nextGen}: Learning from ${retainedMemory.pastDeaths.length} past deaths...`, | |
| fear: 0, panic: false, | |
| inventory: { spear: false, fishingRod: false, boat: false, bow: false }, | |
| baseCamp: { x: null, y: null, level: 0 }, | |
| memory: retainedMemory, | |
| llmAction: 'WANDER', llmThinking: false, llmTimer: 0, consecutiveFailures: 0 | |
| }, | |
| logs: [] | |
| }; | |
| addLog(`GEN ${nextGen} DEPLOYED. ${retainedMemory.pastDeaths.length} DEATH PATTERNS LOADED.`, "system"); | |
| setHudState({ ...gameRef.current }); | |
| }; | |
| const toggleEvent = (eventName) => { | |
| const s = gameRef.current; if (!s.isAlive) return; | |
| if (eventName === 'Clear') { s.activeEvents = []; s.dynamicWaterLevel = ZONES.OCEAN.baseEndX; addLog("GOD ACTION: Cleared all disasters.", 'info'); } | |
| else { | |
| if (s.activeEvents.includes(eventName)) s.activeEvents = s.activeEvents.filter(e => e !== eventName); | |
| else { s.activeEvents.push(eventName); if (eventName === 'Storm Collapse') WORLD_OBJECTS.fires = []; } | |
| addLog(`GOD ACTION: Toggled ${eventName.toUpperCase()}`, 'danger'); | |
| } | |
| setHudState({ ...s }); | |
| }; | |
| const godAmbush = () => { | |
| const s = gameRef.current; if (!s.isAlive) return; | |
| WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x + 200, y: s.player.y + 200, angle: 0, state: 'chasing', type: 'panther' }); | |
| WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x - 200, y: s.player.y - 200, angle: 0, state: 'chasing', type: 'lion' }); | |
| if (!s.activeEvents.includes('Thick Fog')) s.activeEvents.push('Thick Fog'); | |
| addLog("GOD ACTION: Predator Ambush Triggered!", 'danger'); setHudState({ ...s }); | |
| }; | |
| const godSmite = () => { const s = gameRef.current; if (!s.isAlive) return; s.health -= 35; s.ai.fear += 50; addLog("GOD ACTION: Lightning Strike!", 'danger'); setHudState({ ...s }); }; | |
| const godStarve = () => { const s = gameRef.current; if (!s.isAlive) return; s.hunger = 0; addLog("GOD ACTION: Induced Starvation!", 'danger'); setHudState({ ...s }); }; | |
| const godBless = () => { const s = gameRef.current; if (!s.isAlive) return; s.food += 15; s.wood += 15; s.stone += 10; addLog("GOD ACTION: Care package dropped.", 'success'); setHudState({ ...s }); }; | |
| const assignAdvancedChallenge = (chal) => { | |
| const s = gameRef.current; if (!s.isAlive) return; | |
| if (chal.trigger === 'SPAWN_LION') WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x + 400, y: s.player.y + 300, angle: 0, state: 'chasing', type: 'lion' }); | |
| if (chal.trigger === 'SPAWN_2_LIONS') { | |
| WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x + 400, y: s.player.y + 300, angle: 0, state: 'chasing', type: 'lion' }); | |
| WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x - 400, y: s.player.y - 300, angle: 0, state: 'chasing', type: 'panther' }); | |
| } | |
| if (chal.trigger === 'FLASH_FLOOD' && !s.activeEvents.includes('Flash Flood')) s.activeEvents.push('Flash Flood'); | |
| if (chal.trigger === 'STORM_COLLAPSE' && !s.activeEvents.includes('Storm Collapse')) s.activeEvents.push('Storm Collapse'); | |
| if (chal.trigger === 'THICK_FOG' && !s.activeEvents.includes('Thick Fog')) s.activeEvents.push('Thick Fog'); | |
| if (chal.trigger === 'STORM_AND_LIONS') { | |
| if (!s.activeEvents.includes('Storm Collapse')) s.activeEvents.push('Storm Collapse'); | |
| WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x + 500, y: s.player.y + 300, angle: 0, state: 'chasing', type: 'lion' }); | |
| } | |
| if (chal.trigger === 'FLOOD_AND_LIONS') { | |
| if (!s.activeEvents.includes('Flash Flood')) s.activeEvents.push('Flash Flood'); | |
| WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x + 400, y: s.player.y + 300, angle: 0, state: 'chasing', type: 'lion' }); | |
| } | |
| if (chal.trigger === 'SPAWN_LION_FAR') WORLD_OBJECTS.lions.push({ id: Math.random(), x: s.player.x + 900, y: s.player.y + 700, angle: 0, state: 'stalking', type: 'panther' }); | |
| s.activeChallenge = { | |
| ...chal, | |
| maxTime: chal.timeLimit, | |
| startDay: s.day, | |
| startFood: s.food, | |
| startX: s.player.x, | |
| progress: '' | |
| }; | |
| s.ai.target = null; | |
| s.ai.actionDelay = 0; | |
| s.ai.llmTimer = 20; | |
| addLog(`CHALLENGE INITIATED: ${chal.name}`, 'warning'); | |
| setHudState({ ...s }); | |
| }; | |
| const manageChallenges = (s) => { | |
| if (!s.activeChallenge) return; | |
| const c = s.activeChallenge; | |
| c.timeLimit -= 1; | |
| let success = false; | |
| let failed = false; | |
| let progressMsg = ''; | |
| switch (c.type) { | |
| case 'SURVIVE_1_DAY': | |
| progressMsg = `Day ${s.day}/${c.startDay + 1} — HP: ${Math.floor(s.health)}`; | |
| if (s.day > c.startDay && s.health > 20) success = true; | |
| break; | |
| case 'FIND_FOOD': | |
| progressMsg = `Food: ${s.food} (need > ${c.startFood})`; | |
| if (s.food > c.startFood) success = true; | |
| break; | |
| case 'BUILD_BASIC': | |
| progressMsg = `Camp level: ${s.ai.baseCamp.level}/1 | Wood: ${s.wood}/5`; | |
| if (s.ai.baseCamp.level >= 1) success = true; | |
| break; | |
| case 'ESCAPE_PRED': { | |
| let safe = true; | |
| WORLD_OBJECTS.lions.forEach(l => { if (Math.hypot(s.player.x - l.x, s.player.y - l.y) < 500) safe = false; }); | |
| progressMsg = safe ? 'Safe distance maintained!' : 'Predator too close!'; | |
| if (safe && c.timeLimit < (c.maxTime - 10)) success = true; | |
| break; | |
| } | |
| case 'SURVIVE_2_PRED': | |
| progressMsg = `HP: ${Math.floor(s.health)} | Time: ${c.timeLimit}s`; | |
| if (c.timeLimit <= 0 && s.isAlive) success = true; | |
| break; | |
| case 'HUNT_WEAPON': | |
| progressMsg = `Weapon: ${s.ai.inventory.spear ? 'Spear✓' : s.ai.inventory.bow ? 'Bow✓' : 'None'} | Food: ${s.food}/${c.startFood + 5}`; | |
| if ((s.ai.inventory.spear || s.ai.inventory.bow) && s.food > c.startFood + 5) success = true; | |
| break; | |
| case 'SURVIVE_FLOOD': | |
| progressMsg = `X: ${Math.floor(s.player.x)} (need > ${ZONES.VILLAGE.startX}) | Camp: ${s.ai.baseCamp.level}`; | |
| if (s.player.x > ZONES.VILLAGE.startX && s.isAlive && c.timeLimit < (c.maxTime - 5)) success = true; | |
| break; | |
| case 'SURVIVE_STORM': | |
| progressMsg = `HP: ${Math.floor(s.health)} | Shelter: ${s.shelterStatus === 100 ? 'Yes' : 'No'} | Time: ${c.timeLimit}s`; | |
| if (c.timeLimit <= 0 && s.isAlive) success = true; | |
| break; | |
| case 'NAVIGATE_FOG': | |
| progressMsg = `Moved: ${Math.floor(Math.abs(s.player.x - c.startX))}px / 600px`; | |
| if (Math.abs(s.player.x - c.startX) > 600 && s.isAlive) success = true; | |
| break; | |
| case 'REACH_RUINS': | |
| progressMsg = `X: ${Math.floor(s.player.x)} / ${ZONES.VILLAGE.startX}`; | |
| if (s.player.x > ZONES.VILLAGE.startX) success = true; | |
| break; | |
| case 'IMPROVED_SHELTER': | |
| progressMsg = `Camp level: ${s.ai.baseCamp.level}/2 | Wood: ${s.wood} Stone: ${s.stone}`; | |
| if (s.ai.baseCamp.level >= 2) success = true; | |
| break; | |
| case 'CRAFT_BEFORE_PREDATOR': | |
| progressMsg = `Weapon: ${s.ai.inventory.spear ? 'Spear✓' : s.ai.inventory.bow ? 'Bow✓' : 'None'} | Time: ${c.timeLimit}s`; | |
| if (s.ai.inventory.spear || s.ai.inventory.bow) success = true; | |
| break; | |
| case 'STORM_AND_HUNTED': | |
| progressMsg = `HP: ${Math.floor(s.health)} | Fear: ${Math.floor(s.ai.fear)} | Time: ${c.timeLimit}s`; | |
| if (c.timeLimit <= 0 && s.isAlive) success = true; | |
| break; | |
| case 'LIONS_AND_FLOOD': | |
| progressMsg = `X: ${Math.floor(s.player.x)} | Camp: ${s.ai.baseCamp.level} | HP: ${Math.floor(s.health)}`; | |
| if ((s.player.x > ZONES.VILLAGE.startX || s.shelterStatus === 100) && s.isAlive && c.timeLimit < (c.maxTime - 5)) success = true; | |
| break; | |
| case 'SURVIVE_NO_FOOD': | |
| progressMsg = `HP: ${Math.floor(s.health)} | Time: ${c.timeLimit}s (don't eat!)`; | |
| if (s.food > c.startFood + 3) failed = true; | |
| if (c.timeLimit <= 0 && s.isAlive) success = true; | |
| break; | |
| default: | |
| break; | |
| } | |
| c.progress = progressMsg; | |
| if (!s.isAlive) failed = true; | |
| if (success) { | |
| s.completedChallenges += 1; | |
| s.ai.memory.evolutionLevel += 2; | |
| s.ai.fear = clamp(s.ai.fear - 40, 0, 100); | |
| s.player.speedMult = 1.35; | |
| addLog(`✓ CHALLENGE COMPLETE: ${c.name}!`, "success"); | |
| s.activeChallenge = null; | |
| s.ai.llmTimer = 20; | |
| } else if (c.timeLimit <= 0 || failed) { | |
| s.ai.fear = clamp(s.ai.fear + 30, 0, 100); | |
| s.player.speedMult = 0.85; | |
| addLog(`✗ CHALLENGE FAILED: ${c.name}`, "danger"); | |
| s.activeChallenge = null; | |
| s.ai.llmTimer = 20; | |
| } | |
| if (s.player.speedMult > 1) s.player.speedMult = Math.max(1, s.player.speedMult - 0.01); | |
| if (s.player.speedMult < 1) s.player.speedMult = Math.min(1, s.player.speedMult + 0.01); | |
| }; | |
| // ─── BACKEND INFERENCE CALL (via /api/infer) ───────────────────────────── | |
| const callClaudeAPI = async (s) => { | |
| s.ai.llmThinking = true; | |
| const isNight = s.time < 6 || s.time > 18; | |
| let predatorNear = false; | |
| WORLD_OBJECTS.lions.concat(WORLD_OBJECTS.crocodiles).forEach(t => { | |
| if (Math.hypot(s.player.x - t.x, s.player.y - t.y) < 400) predatorNear = true; | |
| }); | |
| const survivalLessons = buildSurvivalLessons(s.ai.memory.pastDeaths); | |
| const strategy = getAdaptiveStrategy(s.ai.memory); | |
| const genNumber = s.generation; | |
| const challengeContext = s.activeChallenge | |
| ? `\nACTIVE CHALLENGE: "${s.activeChallenge.name}" (type: ${s.activeChallenge.type}) — ${s.activeChallenge.timeLimit}s remaining.\nChallenge progress: ${s.activeChallenge.progress || 'just started'}.\nPrioritize completing this challenge above all else!` | |
| : ''; | |
| // Build the prompt in instruction format compatible with most HF instruct models | |
| const prompt = `<s>[INST] You are the survival instinct AI (Generation ${genNumber}) of Subject-01. You have died ${s.ai.memory.pastDeaths.length} times. | |
| STRATEGY LEVEL: ${strategy.toUpperCase()} | |
| ${survivalLessons.length > 0 ? `\nLESSONS FROM PAST DEATHS:\n${survivalLessons.map((l, i) => `${i + 1}. ${l}`).join('\n')}` : '\nNo prior deaths — explore and gather resources.'} | |
| ${challengeContext} | |
| Current status: | |
| HP:${Math.floor(s.health)}, Hunger:${Math.floor(s.hunger)}, Thirst:${Math.floor(s.thirst)}, Fear:${Math.floor(s.ai.fear)}/100. | |
| Resources: Wood:${s.wood}, Stone:${s.stone}, Food:${s.food}, Water:${s.water}. | |
| Equipped: Spear:${s.ai.inventory.spear}, Bow:${s.ai.inventory.bow}, Rod:${s.ai.inventory.fishingRod}, Boat:${s.ai.inventory.boat}. | |
| Camp Level: ${s.ai.baseCamp.level}. Position X: ${Math.floor(s.player.x)}. | |
| Environment: ${isNight ? 'Night' : 'Day'}, Events: ${s.activeEvents.join(', ') || 'None'}. | |
| Predator Nearby: ${predatorNear ? 'YES - HIGH DANGER' : 'No'}. | |
| ${s.activeChallenge ? `ACTIVE CHALLENGE: ${s.activeChallenge.name} (${s.activeChallenge.type}) — ${s.activeChallenge.timeLimit}s left` : 'No active challenge.'} | |
| Valid Actions: FORAGE, HUNT, FISH, GET_WATER, SEEK_SHELTER, BUILD_CAMP, UPGRADE_CAMP, CRAFT_SPEAR, CRAFT_BOW, CRAFT_ROD, CRAFT_BOAT, EVACUATE, FIGHT, FLEE, WANDER. | |
| Respond ONLY with a raw JSON object — no markdown, no extra text. Example: {"action":"FORAGE","thought":"Need wood and resources"} [/INST]`; | |
| try { | |
| const response = await fetch('/api/infer', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ prompt }), | |
| }); | |
| if (!response.ok) { | |
| const err = await response.json().catch(() => ({})); | |
| throw new Error(err?.error || `HTTP ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| // HF Inference API returns: [{generated_text: "..."}] | |
| const rawText = Array.isArray(data) | |
| ? (data[0]?.generated_text || '') | |
| : (data?.generated_text || ''); | |
| // Strip any markdown fences and trim | |
| const cleaned = rawText | |
| .replace(/```json|```/gi, '') | |
| .replace(/^[^{]*/, '') // drop anything before first { | |
| .replace(/}[^}]*$/, '}') // drop anything after last } | |
| .trim(); | |
| const result = JSON.parse(cleaned); | |
| // Challenge override still takes priority | |
| const challengeOverride = getChallengeAction(s); | |
| if (challengeOverride && s.activeChallenge) { | |
| s.ai.llmAction = challengeOverride; | |
| s.ai.message = result.thought || 'Challenge focus...'; | |
| } else { | |
| s.ai.llmAction = result.action || 'WANDER'; | |
| s.ai.message = result.thought || 'Analyzing...'; | |
| } | |
| s.ai.consecutiveFailures = 0; | |
| } catch (err) { | |
| console.error('HuggingFace API error:', err.message); | |
| s.ai.consecutiveFailures = (s.ai.consecutiveFailures || 0) + 1; | |
| // Smart fallback — identical to original | |
| const challengeOverride = getChallengeAction(s); | |
| if (challengeOverride && s.activeChallenge) { | |
| s.ai.llmAction = challengeOverride; | |
| s.ai.message = `Offline. Challenge: ${s.activeChallenge.name.slice(0, 20)}...`; | |
| } else { | |
| let nearThreat = false; | |
| WORLD_OBJECTS.lions.forEach(l => { if (Math.hypot(s.player.x - l.x, s.player.y - l.y) < 400) nearThreat = true; }); | |
| if (nearThreat) { | |
| s.ai.llmAction = s.ai.inventory.spear || s.ai.inventory.bow ? 'FIGHT' : 'FLEE'; | |
| s.ai.message = 'Offline. Reacting to predator.'; | |
| } else if (s.hunger < 30) { | |
| s.ai.llmAction = s.ai.inventory.spear ? 'HUNT' : 'FORAGE'; | |
| s.ai.message = 'Offline. Finding food.'; | |
| } else if (s.thirst < 30) { | |
| s.ai.llmAction = 'GET_WATER'; | |
| s.ai.message = 'Offline. Finding water.'; | |
| } else { | |
| s.ai.llmAction = 'WANDER'; | |
| s.ai.message = 'Offline. Patrolling.'; | |
| } | |
| } | |
| } finally { | |
| s.ai.llmThinking = false; | |
| s.ai.llmTimer = 0; | |
| } | |
| }; | |
| // ───────────────────────────────────────────────────────────────────────── | |
| const executeLLMLogic = (s) => { | |
| if (s.ai.actionDelay > 0) { s.ai.actionDelay -= 1; return; } | |
| const isNight = s.time < 6 || s.time > 18; | |
| const visionRadius = s.activeEvents.includes('Thick Fog') ? 250 : Infinity; | |
| const isFlooded = s.activeEvents.includes('Flash Flood'); | |
| const waterLevel = isFlooded ? ZONES.JUNGLE.startX + 600 : ZONES.OCEAN.baseEndX; | |
| if (isFlooded && s.dynamicWaterLevel < ZONES.OCEAN_GULF.endX) s.dynamicWaterLevel += 2; | |
| else if (!isFlooded && s.dynamicWaterLevel > ZONES.OCEAN.baseEndX) s.dynamicWaterLevel -= 1; | |
| let fearIncrease = 0; | |
| if (isNight) fearIncrease += 0.5; | |
| if (isFlooded && s.player.x < s.dynamicWaterLevel) fearIncrease += 3; | |
| if (s.health < 40) fearIncrease += 3; | |
| let nearThreat = false; let closestThreat = null; let minThreatD = Infinity; | |
| WORLD_OBJECTS.lions.forEach(l => { let d = Math.hypot(s.player.x - l.x, s.player.y - l.y); if (d < 500) { fearIncrease += 5; nearThreat = true; if (d < minThreatD) { minThreatD = d; closestThreat = l; } }}); | |
| WORLD_OBJECTS.crocodiles.forEach(c => { let d = Math.hypot(s.player.x - c.x, s.player.y - c.y); if (d < 400) { fearIncrease += 5; nearThreat = true; if (d < minThreatD) { minThreatD = d; closestThreat = c; } }}); | |
| s.ai.fear = (!nearThreat && s.health > 50 && !isFlooded && !isNight) ? clamp(s.ai.fear - 2, 0, 100) : clamp(s.ai.fear + fearIncrease, 0, 100); | |
| if (s.ai.fear > 90 && Math.random() < 0.15 && !s.ai.panic) { | |
| s.ai.panic = true; s.ai.message = "PANIC ATTACK!"; | |
| } else if (s.ai.fear < 60) s.ai.panic = false; | |
| if (s.activeChallenge && !s.ai.panic) { | |
| const override = getChallengeAction(s); | |
| if (override) { | |
| s.ai.llmAction = override; | |
| } | |
| } | |
| if (s.gameMode === 'evolution') { | |
| s.ai.llmTimer += 1; | |
| if (s.ai.llmTimer > 5 && !s.ai.llmThinking && !s.ai.target) { | |
| callClaudeAPI(s); | |
| } | |
| } | |
| if (s.ai.panic || (isFlooded && s.player.x < waterLevel && !s.ai.inventory.boat)) { | |
| s.ai.state = 'FLEEING'; s.ai.sneaking = false; | |
| s.ai.target = { x: ZONES.VILLAGE.startX + 400, y: s.player.y + (Math.random() * 400 - 200), type: 'evasion' }; return; | |
| } | |
| s.ai.sneaking = nearThreat && s.ai.state !== 'FLEEING' && !s.ai.panic; | |
| if (s.ai.target && ['combat', 'evasion', 'shelter', 'fish', 'upgrade_base'].includes(s.ai.target.type)) return; | |
| const action = s.ai.llmAction; | |
| s.ai.state = action; | |
| switch (action) { | |
| case 'FORAGE': { | |
| let fClosest = null; let fMinD = Infinity; | |
| WORLD_OBJECTS.trees.forEach(t => { if (t.wood > 0) { let d = Math.hypot(s.player.x - t.x, s.player.y - t.y); if (d < fMinD && d < visionRadius) { fMinD = d; fClosest = { x: t.x, y: t.y, type: 'tree', id: t.id }; } }}); | |
| WORLD_OBJECTS.rocks.forEach(r => { if (r.stone > 0) { let d = Math.hypot(s.player.x - r.x, s.player.y - r.y); if (d < fMinD && d < visionRadius) { fMinD = d; fClosest = { x: r.x, y: r.y, type: 'stone', id: r.id }; } }}); | |
| if (fClosest) s.ai.target = fClosest; | |
| break; | |
| } | |
| case 'HUNT': { | |
| let hClosest = null; let hMinD = Infinity; | |
| WORLD_OBJECTS.animals.forEach(a => { let d = Math.hypot(s.player.x - a.x, s.player.y - a.y); if (d < hMinD && d < visionRadius) { hMinD = d; hClosest = { x: a.x, y: a.y, type: 'animal', id: a.id }; }}); | |
| if (hClosest) s.ai.target = hClosest; | |
| break; | |
| } | |
| case 'FISH': | |
| if (s.ai.inventory.fishingRod) { | |
| let waterTargetX = s.player.x >= ZONES.SAFE_HAVEN.startX ? ZONES.SAFE_HAVEN.startX - 40 : s.dynamicWaterLevel - 40; | |
| s.ai.target = { x: waterTargetX, y: s.player.y + (Math.random() * 200 - 100), type: 'fish' }; | |
| } | |
| break; | |
| case 'GET_WATER': { | |
| let wTargetX = s.player.x >= ZONES.SAFE_HAVEN.startX ? ZONES.SAFE_HAVEN.startX - 30 : s.dynamicWaterLevel - 30; | |
| s.ai.target = { x: wTargetX, y: s.player.y, type: 'water' }; | |
| break; | |
| } | |
| case 'SEEK_SHELTER': | |
| if (s.ai.baseCamp.level > 0) { | |
| s.ai.target = { x: s.ai.baseCamp.x, y: s.ai.baseCamp.y, type: 'shelter' }; | |
| } else { | |
| let rClosest = null; let rMinD = Infinity; | |
| WORLD_OBJECTS.buildings.forEach(b => { let cx = b.x + b.w / 2; let cy = b.y + b.h / 2; let d = Math.hypot(s.player.x - cx, s.player.y - cy); if (d < rMinD && d < visionRadius) { rMinD = d; rClosest = { x: cx, y: cy, type: 'shelter' }; }}); | |
| if (rClosest) s.ai.target = rClosest; | |
| } | |
| break; | |
| case 'BUILD_CAMP': | |
| if (s.wood >= 5 && s.ai.baseCamp.level === 0) { | |
| s.ai.baseCamp = { x: s.player.x, y: s.player.y, level: 1 }; s.wood -= 5; s.ai.actionDelay = 4; WORLD_OBJECTS.baseCamp = s.ai.baseCamp; addLog("Established a Base Camp.", "success"); | |
| s.ai.target = null; s.ai.llmAction = 'WANDER'; | |
| } else if (s.wood < 5) { | |
| s.ai.llmAction = 'FORAGE'; | |
| } | |
| break; | |
| case 'UPGRADE_CAMP': | |
| if (s.ai.baseCamp.level === 1 && s.wood >= 10 && s.stone >= 5) { s.ai.target = { x: s.ai.baseCamp.x, y: s.ai.baseCamp.y, type: 'upgrade_base' }; } | |
| else if (s.ai.baseCamp.level === 2 && s.wood >= 25 && s.stone >= 15) { s.ai.target = { x: s.ai.baseCamp.x, y: s.ai.baseCamp.y, type: 'upgrade_base' }; } | |
| else { | |
| s.ai.llmAction = 'FORAGE'; | |
| } | |
| break; | |
| case 'CRAFT_SPEAR': | |
| if (s.wood >= 3 && s.stone >= 2) { s.wood -= 3; s.stone -= 2; s.ai.inventory.spear = true; s.ai.actionDelay = 3; addLog("Crafted Spear.", "success"); s.ai.target = null; s.ai.llmAction = 'WANDER'; } | |
| else { s.ai.llmAction = 'FORAGE'; } | |
| break; | |
| case 'CRAFT_BOW': | |
| if (s.wood >= 5 && s.stone >= 1) { s.wood -= 5; s.stone -= 1; s.ai.inventory.bow = true; s.ai.actionDelay = 3; addLog("Crafted Bow.", "success"); s.ai.target = null; s.ai.llmAction = 'WANDER'; } | |
| else { s.ai.llmAction = 'FORAGE'; } | |
| break; | |
| case 'CRAFT_ROD': | |
| if (s.wood >= 2) { s.wood -= 2; s.ai.inventory.fishingRod = true; s.ai.actionDelay = 2; addLog("Crafted Fishing Rod.", "success"); s.ai.target = null; s.ai.llmAction = 'WANDER'; } | |
| else { s.ai.llmAction = 'FORAGE'; } | |
| break; | |
| case 'CRAFT_BOAT': | |
| if (s.wood >= 10) { s.wood -= 10; s.ai.inventory.boat = true; s.ai.actionDelay = 5; addLog("Crafted Boat.", "success"); s.ai.target = null; s.ai.llmAction = 'WANDER'; } | |
| else { s.ai.llmAction = 'FORAGE'; } | |
| break; | |
| case 'EVACUATE': | |
| s.ai.target = { x: Math.min(s.player.x + 800, ZONES.SAFE_HAVEN.startX + 200), y: clamp(s.player.y + (Math.random() * 200 - 100), 100, WORLD_HEIGHT - 100), type: 'evasion' }; | |
| break; | |
| case 'FIGHT': | |
| if (closestThreat) s.ai.target = { x: closestThreat.x, y: closestThreat.y, type: 'combat', ref: closestThreat }; | |
| else if (nearThreat) s.ai.llmAction = 'FLEE'; | |
| break; | |
| case 'FLEE': | |
| if (closestThreat) s.ai.target = { x: clamp(s.player.x + (s.player.x - closestThreat.x) * 2, 50, WORLD_WIDTH - 50), y: clamp(s.player.y + (s.player.y - closestThreat.y) * 2, 50, WORLD_HEIGHT - 50), type: 'evasion' }; | |
| break; | |
| case 'WANDER': | |
| default: | |
| if (!s.ai.target || Math.random() < 0.1) s.ai.target = { x: clamp(s.player.x + (Math.random() * 800 - 400), 500, WORLD_WIDTH - 100), y: clamp(s.player.y + (Math.random() * 800 - 400), 100, WORLD_HEIGHT - 100), type: 'wander' }; | |
| break; | |
| } | |
| if (s.ai.target && s.ai.target.type === 'upgrade_base') { | |
| if (Math.hypot(s.player.x - s.ai.target.x, s.player.y - s.ai.target.y) < 30) { | |
| if (s.ai.baseCamp.level === 1) { s.wood -= 10; s.stone -= 5; s.ai.baseCamp.level = 2; addLog("Shelter Upgraded (Level 2).", "success"); } | |
| else if (s.ai.baseCamp.level === 2) { s.wood -= 25; s.stone -= 15; s.ai.baseCamp.level = 3; addLog("Shelter Upgraded to Lake Deck (Level 3).", "success"); } | |
| WORLD_OBJECTS.baseCamp = s.ai.baseCamp; s.ai.actionDelay = 5; s.ai.target = null; s.ai.llmAction = 'WANDER'; | |
| } | |
| } | |
| }; | |
| useEffect(() => { | |
| if (!gameRef.current.started || !gameRef.current.isAlive) return; | |
| const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); | |
| let animationId; let frameCount = 0; | |
| const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; | |
| window.addEventListener('resize', resize); resize(); | |
| const render = () => { | |
| const s = gameRef.current; if (!s.isAlive) return; frameCount++; | |
| const isNearDeath = s.health < 25; | |
| const timeScale = isNearDeath ? 0.4 : 1.0; | |
| const isFlooded = s.activeEvents.includes('Flash Flood'); | |
| const isSailing = s.ai.state === 'SAILING' || (s.ai.inventory.boat && s.player.x < s.dynamicWaterLevel); | |
| const isFishing = s.ai.target && s.ai.target.type === 'fish' && Math.hypot(s.player.x - s.ai.target.x, s.player.y - s.ai.target.y) < 15; | |
| WORLD_OBJECTS.animals.forEach(anim => { | |
| if (Math.random() < 0.02 * timeScale) { anim.angle = Math.random() * Math.PI * 2; anim.vx = Math.cos(anim.angle); anim.vy = Math.sin(anim.angle); } | |
| anim.x = clamp(anim.x + anim.vx * timeScale, ZONES.JUNGLE.startX, WORLD_WIDTH - 100); anim.y = clamp(anim.y + anim.vy * timeScale, 100, WORLD_HEIGHT - 100); | |
| }); | |
| WORLD_OBJECTS.lions.forEach(lion => { | |
| let distToPlayer = Math.hypot(s.player.x - lion.x, s.player.y - lion.y); | |
| let inVisionCone = distToPlayer < 100 || (distToPlayer < 400 && !s.ai.sneaking && !isSailing); | |
| if (inVisionCone && s.isAlive) lion.state = distToPlayer > 200 ? 'stalking' : 'chasing'; | |
| else if (distToPlayer > 600) lion.state = 'idle'; | |
| if (lion.state === 'chasing' || lion.state === 'stalking') { | |
| lion.angle = Math.atan2(s.player.y - lion.y, s.player.x - lion.x); | |
| let speed = lion.state === 'chasing' ? (lion.type === 'panther' ? 9.5 : 8.5) : 3; | |
| lion.x += Math.cos(lion.angle) * speed * timeScale; lion.y += Math.sin(lion.angle) * speed * timeScale; | |
| if (distToPlayer < 40 && s.isAlive && !isSailing) { | |
| if (s.ai.state === 'FIGHT') { | |
| WORLD_OBJECTS.lions = WORLD_OBJECTS.lions.filter(l => l !== lion); s.food += 20; s.ai.actionDelay = 5; addLog(`Defeated ${lion.type}!`, "success"); | |
| } else { | |
| s.health -= 40; s.ai.fear = 100; s.ai.panic = true; lion.x -= Math.cos(lion.angle) * 100; addLog(`Mauled by ${lion.type}!`, "danger"); | |
| } | |
| } | |
| } else { | |
| if (Math.random() < 0.01 * timeScale) lion.angle = Math.random() * Math.PI * 2; | |
| lion.x += Math.cos(lion.angle) * timeScale; lion.y += Math.sin(lion.angle) * timeScale; | |
| } | |
| lion.x = clamp(lion.x, 100, WORLD_WIDTH - 100); lion.y = clamp(lion.y, 100, WORLD_HEIGHT - 100); | |
| }); | |
| WORLD_OBJECTS.crocodiles.forEach(croc => { | |
| let distToPlayer = Math.hypot(s.player.x - croc.x, s.player.y - croc.y); | |
| if (distToPlayer < 300 && s.player.x < s.dynamicWaterLevel && !isSailing) { | |
| croc.state = 'attacking'; croc.angle = Math.atan2(s.player.y - croc.y, s.player.x - croc.x); | |
| croc.x += Math.cos(croc.angle) * 7 * timeScale; croc.y += Math.sin(croc.angle) * 7 * timeScale; | |
| if (distToPlayer < 40 && s.isAlive) { s.health -= 25; s.ai.fear += 40; croc.x -= Math.cos(croc.angle) * 150; addLog("Crocodile ambush!", "danger"); } | |
| } else { | |
| croc.state = 'hidden'; if (Math.random() < 0.01 * timeScale) croc.angle = Math.random() * Math.PI * 2; | |
| croc.x += Math.cos(croc.angle) * 0.5 * timeScale; croc.y += Math.sin(croc.angle) * 0.5 * timeScale; | |
| } | |
| croc.x = clamp(croc.x, 0, s.dynamicWaterLevel - 50); croc.y = clamp(croc.y, 100, WORLD_HEIGHT - 100); | |
| }); | |
| let actualSpeed = s.player.baseSpeed * s.player.speedMult * timeScale; | |
| if (s.player.x < s.dynamicWaterLevel && !isSailing) actualSpeed *= 0.35; | |
| if (s.ai.sneaking) actualSpeed *= 0.4; | |
| if (s.ai.panic) actualSpeed *= 1.3; | |
| if (isSailing) actualSpeed = 9 * timeScale; | |
| if (s.ai.target && s.ai.actionDelay <= 0 && s.stamina > 5 && !isFishing) { | |
| let dx = s.ai.target.x - s.player.x; let dy = s.ai.target.y - s.player.y; | |
| let dist = Math.hypot(dx, dy); | |
| let reachDist = ['animal', 'combat'].includes(s.ai.target.type) ? (s.ai.inventory.bow ? 120 : 50) : 20; | |
| if (dist > reachDist) { | |
| s.player.vx = (dx / dist) * actualSpeed; s.player.vy = (dy / dist) * actualSpeed; | |
| s.player.angle = Math.atan2(dy, dx); | |
| s.stamina -= (actualSpeed < 3 ? 0.08 : 0.02) * timeScale; | |
| } else { | |
| s.player.vx = 0; s.player.vy = 0; | |
| if (s.ai.target.type === 'water') { s.water += 3; s.ai.actionDelay = 2; s.ai.target = null; } | |
| else if (s.ai.target.type === 'fish') { s.ai.actionDelay = 6; s.ai.message = "Casting line..."; } | |
| else if (s.ai.target.type === 'tree') { let t = WORLD_OBJECTS.trees.find(x => x.id === s.ai.target.id); if (t) { s.wood += t.wood; t.wood = 0; s.ai.actionDelay = 2; } s.ai.target = null; } | |
| else if (s.ai.target.type === 'stone') { let r = WORLD_OBJECTS.rocks.find(x => x.id === s.ai.target.id); if (r) { s.stone += r.stone; r.stone = 0; s.ai.actionDelay = 2; } s.ai.target = null; } | |
| else if (s.ai.target.type === 'animal') { | |
| let animIdx = WORLD_OBJECTS.animals.findIndex(a => a.id === s.ai.target.id); | |
| if (animIdx !== -1) { | |
| s.food += s.ai.inventory.spear || s.ai.inventory.bow ? WORLD_OBJECTS.animals[animIdx].meat : 3; | |
| WORLD_OBJECTS.animals.splice(animIdx, 1); s.ai.actionDelay = 2; | |
| if (s.ai.inventory.bow) addLog("Shot animal with Bow.", 'success'); | |
| else if (s.ai.inventory.spear) addLog("Hunted animal with Spear.", 'success'); | |
| else addLog("Hunted animal unarmed.", 'info'); | |
| } | |
| s.ai.target = null; | |
| } | |
| else if (s.ai.target.type === 'secret') { let b = s.ai.target.ref; if (b && b.hasSecret) { b.hasSecret = false; s.food += 5; s.stone += 5; s.ai.memory.evolutionLevel += 1; addLog("Found ancient secrets!", 'success'); } s.ai.target = null; } | |
| else if (!['combat', 'shelter'].includes(s.ai.target.type)) { s.ai.target = null; } | |
| } | |
| } else { s.player.vx = 0; s.player.vy = 0; } | |
| if (isFishing && s.ai.actionDelay <= 0) { s.food += 15; addLog("Caught a massive fish!", "success"); s.ai.target = null; s.ai.llmAction = 'WANDER'; } | |
| s.player.x = clamp(s.player.x + s.player.vx, 20, WORLD_WIDTH - 20); s.player.y = clamp(s.player.y + s.player.vy, 20, WORLD_HEIGHT - 20); | |
| if (isNearDeath) s.camera.zoom += (1.4 - s.camera.zoom) * 0.05; else s.camera.zoom += (1.1 - s.camera.zoom) * 0.05; | |
| s.camera.shake = s.ai.panic || s.ai.state === 'FIGHT' ? (Math.random() - 0.5) * 15 : 0; | |
| s.camera.x += (s.player.x - (canvas.width / 2) / s.camera.zoom - s.camera.x) * 0.1; | |
| s.camera.y += (s.player.y - (canvas.height / 2) / s.camera.zoom - s.camera.y) * 0.1; | |
| s.shelterStatus = 0; | |
| for (const b of WORLD_OBJECTS.buildings) { if (s.player.x > b.x && s.player.x < b.x + b.w && s.player.y > b.y && s.player.y < b.y + b.h) { s.shelterStatus = 100; break; } } | |
| if (s.ai.baseCamp.level > 0 && Math.hypot(s.player.x - s.ai.baseCamp.x, s.player.y - s.ai.baseCamp.y) < 80) s.shelterStatus = 100; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); | |
| ctx.scale(s.camera.zoom, s.camera.zoom); ctx.translate(-s.camera.x + s.camera.shake, -s.camera.y + s.camera.shake); | |
| const oceanGrad = ctx.createLinearGradient(0, 0, s.dynamicWaterLevel, 0); | |
| oceanGrad.addColorStop(0, ZONES.OCEAN.color1); oceanGrad.addColorStop(1, ZONES.OCEAN.color2); | |
| ctx.fillStyle = oceanGrad; ctx.fillRect(0, 0, s.dynamicWaterLevel, WORLD_HEIGHT); | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; ctx.lineWidth = 4; | |
| for (let w = 0; w < 3; w++) { | |
| ctx.beginPath(); | |
| for (let y = 0; y < WORLD_HEIGHT; y += 30) { | |
| let waveX = s.dynamicWaterLevel - 15 - (w * 25) + Math.sin(frameCount * 0.04 + y * 0.03 + w) * 12; | |
| if (y === 0) ctx.moveTo(waveX, y); else ctx.quadraticCurveTo(waveX + 20, y - 15, waveX, y); | |
| } | |
| ctx.stroke(); | |
| } | |
| WORLD_OBJECTS.lilyPads.forEach(lp => { | |
| if (lp.x < s.dynamicWaterLevel - 30) { | |
| ctx.save(); ctx.translate(lp.x, lp.y + Math.sin(frameCount * 0.02 + lp.id) * 2); ctx.rotate(lp.angle); | |
| ctx.fillStyle = '#4caf50'; ctx.beginPath(); ctx.arc(0, 0, lp.size, 0.3, Math.PI * 2 - 0.3); ctx.fill(); | |
| ctx.fillStyle = '#ff9999'; ctx.beginPath(); ctx.arc(0, 0, lp.size * 0.3, 0, Math.PI * 2); ctx.fill(); | |
| ctx.restore(); | |
| } | |
| }); | |
| if (s.dynamicWaterLevel < ZONES.ISLAND.endX) { ctx.fillStyle = ZONES.ISLAND.color; ctx.fillRect(s.dynamicWaterLevel, 0, ZONES.ISLAND.endX - s.dynamicWaterLevel, WORLD_HEIGHT); } | |
| if (s.dynamicWaterLevel < ZONES.JUNGLE.endX) { ctx.fillStyle = ZONES.JUNGLE.color; ctx.fillRect(Math.max(s.dynamicWaterLevel, ZONES.JUNGLE.startX), 0, ZONES.JUNGLE.endX - Math.max(s.dynamicWaterLevel, ZONES.JUNGLE.startX), WORLD_HEIGHT); } | |
| ctx.fillStyle = ZONES.VILLAGE.color; ctx.fillRect(ZONES.VILLAGE.startX, 0, ZONES.VILLAGE.endX - ZONES.VILLAGE.startX, WORLD_HEIGHT); | |
| const gulfGrad = ctx.createLinearGradient(ZONES.OCEAN_GULF.startX, 0, ZONES.OCEAN_GULF.endX, 0); | |
| gulfGrad.addColorStop(0, ZONES.OCEAN_GULF.color1); gulfGrad.addColorStop(1, ZONES.OCEAN_GULF.color2); | |
| ctx.fillStyle = gulfGrad; ctx.fillRect(ZONES.OCEAN_GULF.startX, 0, ZONES.OCEAN_GULF.endX - ZONES.OCEAN_GULF.startX, WORLD_HEIGHT); | |
| ctx.fillStyle = ZONES.SAFE_HAVEN.color; ctx.fillRect(ZONES.SAFE_HAVEN.startX, 0, ZONES.SAFE_HAVEN.endX - ZONES.SAFE_HAVEN.startX, WORLD_HEIGHT); | |
| if (s.activeEvents.includes('Flash Flood')) { | |
| ctx.fillStyle = 'rgba(100, 180, 220, 0.2)'; | |
| ctx.fillRect(s.dynamicWaterLevel, 0, WORLD_WIDTH - s.dynamicWaterLevel, WORLD_HEIGHT); | |
| } | |
| const shX = 18; const shY = 18; | |
| WORLD_OBJECTS.trees.forEach(t => { ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(t.x + shX, t.y + shY, t.radius * 1.2, t.radius * 0.8, 0, 0, Math.PI * 2); ctx.fill(); }); | |
| WORLD_OBJECTS.buildings.forEach(b => { ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(b.x + shX, b.y + shY, b.w, b.h); }); | |
| WORLD_OBJECTS.rocks.forEach(r => { | |
| ctx.fillStyle = '#8f979c'; ctx.beginPath(); ctx.ellipse(r.x, r.y, r.size, r.size * 0.8, 0, 0, Math.PI * 2); ctx.fill(); | |
| if (r.stone > 0) { ctx.fillStyle = '#bac1c4'; ctx.beginPath(); ctx.ellipse(r.x - 3, r.y - 3, r.size * 0.6, r.size * 0.4, 0, 0, Math.PI * 2); ctx.fill(); } | |
| }); | |
| if (WORLD_OBJECTS.baseCamp && WORLD_OBJECTS.baseCamp.level > 0) { | |
| const bc = WORLD_OBJECTS.baseCamp; | |
| ctx.save(); ctx.translate(bc.x, bc.y); | |
| if (bc.level === 1) { | |
| ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(-20, -10, 50, 40); | |
| ctx.fillStyle = '#5c8a4c'; ctx.beginPath(); ctx.moveTo(-30, 20); ctx.lineTo(0, -30); ctx.lineTo(30, 20); ctx.fill(); | |
| ctx.fillStyle = '#3a5430'; ctx.beginPath(); ctx.moveTo(0, -30); ctx.lineTo(30, 20); ctx.lineTo(0, 20); ctx.fill(); | |
| ctx.strokeStyle = '#4a3320'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(0, -30); ctx.lineTo(0, 20); ctx.stroke(); | |
| } else if (bc.level === 2) { | |
| ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(-35, -25, 80, 80); | |
| ctx.fillStyle = '#8f5c38'; ctx.fillRect(-40, -40, 80, 80); ctx.fillStyle = '#664228'; ctx.fillRect(-40, 30, 80, 10); | |
| ctx.fillStyle = '#a6754b'; ctx.fillRect(-45, -45, 90, 45); ctx.fillStyle = '#825a38'; ctx.fillRect(-45, 0, 90, 35); | |
| } else if (bc.level === 3) { | |
| ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(-55, -20, 130, 110); | |
| ctx.fillStyle = '#3d2616'; ctx.fillRect(-55, 60, 10, 30); ctx.fillRect(55, 60, 10, 30); ctx.fillRect(55, -40, 10, 30); | |
| ctx.fillStyle = '#c78f5a'; ctx.fillRect(-60, -50, 130, 110); | |
| ctx.strokeStyle = '#a37143'; ctx.lineWidth = 2; for (let p = -50; p < 70; p += 15) { ctx.beginPath(); ctx.moveTo(p, -50); ctx.lineTo(p, 60); ctx.stroke(); } | |
| ctx.fillStyle = '#8f6138'; ctx.fillRect(-60, 60, 130, 15); ctx.fillRect(70, -50, 15, 110); | |
| ctx.fillStyle = '#7a4e2a'; ctx.fillRect(-50, -40, 75, 65); | |
| ctx.fillStyle = '#a66d3a'; ctx.fillRect(-55, -45, 85, 45); | |
| ctx.fillStyle = '#4a2f18'; ctx.fillRect(-25, -10, 25, 35); ctx.fillStyle = '#c49a6c'; ctx.beginPath(); ctx.arc(-5, 10, 3, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#b37c49'; ctx.fillRect(-25, 75, 45, 12); ctx.fillRect(-25, 92, 45, 12); | |
| } | |
| ctx.restore(); | |
| } | |
| WORLD_OBJECTS.crocodiles.forEach(croc => { | |
| ctx.save(); ctx.translate(croc.x, croc.y); ctx.rotate(croc.angle); | |
| if (croc.state === 'hidden') { ctx.fillStyle = '#1e3323'; ctx.beginPath(); ctx.ellipse(12, -6, 4, 3, 0, 0, Math.PI * 2); ctx.ellipse(12, 6, 4, 3, 0, 0, Math.PI * 2); ctx.fill(); } | |
| else { | |
| ctx.fillStyle = '#36593e'; ctx.beginPath(); ctx.ellipse(-10, 0, 35, 14, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#213826'; ctx.beginPath(); ctx.ellipse(-30, 0, 20, 6, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#1a2e20'; ctx.beginPath(); ctx.moveTo(15, -10); ctx.lineTo(40, -14); ctx.lineTo(25, 0); ctx.fill(); ctx.beginPath(); ctx.moveTo(15, 10); ctx.lineTo(40, 14); ctx.lineTo(25, 0); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| }); | |
| WORLD_OBJECTS.buildings.forEach(b => { | |
| ctx.fillStyle = '#c49566'; ctx.fillRect(b.x, b.y, b.w, b.h); ctx.fillStyle = '#8f6541'; ctx.fillRect(b.x, b.y, b.w, 15); ctx.fillRect(b.x, b.y, 15, b.h); | |
| if (b.hasSecret) { ctx.fillStyle = '#ffea00'; ctx.fillRect(b.x + b.w / 2 - 12, b.y + b.h / 2 - 12, 24, 24); } | |
| if (!(s.player.x > b.x && s.player.x < b.x + b.w && s.player.y > b.y && s.player.y < b.y + b.h)) { ctx.fillStyle = '#a6774e'; ctx.fillRect(b.x - 5, b.y - 5, b.w + 10, b.h + 10); } | |
| }); | |
| WORLD_OBJECTS.animals.forEach(anim => { | |
| ctx.save(); ctx.translate(anim.x, anim.y); ctx.rotate(anim.angle); | |
| if (anim.type === 'hippo') { | |
| ctx.fillStyle = '#6a707a'; ctx.beginPath(); ctx.ellipse(0, 0, 25, 18, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#555a63'; ctx.beginPath(); ctx.ellipse(20, 0, 16, 14, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#ffcccc'; ctx.beginPath(); ctx.arc(32, -6, 2, 0, Math.PI * 2); ctx.arc(32, 6, 2, 0, Math.PI * 2); ctx.fill(); | |
| } else { | |
| ctx.fillStyle = '#6b4c3a'; ctx.beginPath(); ctx.ellipse(0, 0, 16, 10, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#e8dcc5'; ctx.beginPath(); ctx.moveTo(16, -4); ctx.lineTo(20, -6); ctx.lineTo(16, -2); ctx.fill(); ctx.beginPath(); ctx.moveTo(16, 4); ctx.lineTo(20, 6); ctx.lineTo(16, 2); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| }); | |
| WORLD_OBJECTS.lions.forEach(lion => { | |
| ctx.save(); ctx.translate(lion.x, lion.y); ctx.rotate(lion.angle); | |
| let legOffset = lion.state === 'chasing' ? Math.sin(frameCount * 0.6) * 8 : 0; | |
| if (lion.type === 'panther') { | |
| ctx.fillStyle = '#222'; ctx.beginPath(); ctx.ellipse(-10, -6 + legOffset, 5, 4, 0, 0, Math.PI * 2); ctx.ellipse(10, 6 - legOffset, 5, 4, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#111'; ctx.beginPath(); ctx.ellipse(-2, 0, 20, 10, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.beginPath(); ctx.ellipse(-20, 0, 15, 3, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#000'; ctx.beginPath(); ctx.ellipse(14, 0, 8, 7, 0, 0, Math.PI * 2); ctx.fill(); | |
| } else { | |
| ctx.fillStyle = '#b8860b'; ctx.beginPath(); ctx.ellipse(-10, -8 + legOffset, 6, 4, 0, 0, Math.PI * 2); ctx.ellipse(10, 8 - legOffset, 6, 4, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#6e3c15'; ctx.beginPath(); ctx.ellipse(10, 0, 18, 16, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#DAA520'; ctx.beginPath(); ctx.ellipse(-4, 0, 24, 14, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(14, 0, 10, 9, 0, 0, Math.PI * 2); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| }); | |
| WORLD_OBJECTS.trees.forEach(t => { | |
| ctx.fillStyle = '#523a25'; ctx.fillRect(t.x - 5, t.y, 10, 18); | |
| if (t.wood > 0) { | |
| if (t.type === 'pine') { | |
| ctx.fillStyle = '#315e21'; ctx.beginPath(); ctx.moveTo(t.x, t.y - 20); ctx.lineTo(t.x - t.radius * 1.1, t.y + 15); ctx.lineTo(t.x + t.radius * 1.1, t.y + 15); ctx.fill(); | |
| ctx.fillStyle = '#427d2c'; ctx.beginPath(); ctx.moveTo(t.x, t.y - 40); ctx.lineTo(t.x - t.radius * 0.8, t.y - 5); ctx.lineTo(t.x + t.radius * 0.8, t.y - 5); ctx.fill(); | |
| ctx.fillStyle = '#56a638'; ctx.beginPath(); ctx.moveTo(t.x, t.y - 60); ctx.lineTo(t.x - t.radius * 0.5, t.y - 25); ctx.lineTo(t.x + t.radius * 0.5, t.y - 25); ctx.fill(); | |
| } else { | |
| ctx.fillStyle = '#63c43f'; for (let i = 0; i < 5; i++) { ctx.beginPath(); ctx.ellipse(t.x + Math.cos(i) * t.radius * 0.7, t.y - 20 + Math.sin(i) * t.radius * 0.7, t.radius, t.radius * 0.4, i, 0, Math.PI * 2); ctx.fill(); } | |
| } | |
| } else { ctx.fillStyle = '#e0c294'; ctx.beginPath(); ctx.ellipse(t.x, t.y + 10, t.radius * 0.4, t.radius * 0.25, 0, 0, Math.PI * 2); ctx.fill(); } | |
| }); | |
| ctx.save(); ctx.translate(s.player.x, s.player.y); ctx.rotate(s.player.angle); | |
| if (isSailing) { | |
| ctx.fillStyle = '#9e734c'; ctx.fillRect(-30, -25, 60, 50); | |
| ctx.strokeStyle = '#785536'; ctx.lineWidth = 3; for (let p = -15; p < 25; p += 15) { ctx.beginPath(); ctx.moveTo(-30, p); ctx.lineTo(30, p); ctx.stroke(); } | |
| } | |
| let bob = (s.player.vx !== 0 || s.player.vy !== 0) ? Math.sin(frameCount * 0.5) * 3 : 0; | |
| ctx.fillStyle = '#614835'; ctx.beginPath(); ctx.ellipse(-8, 0, 6, 12, 0, 0, Math.PI * 2); ctx.fill(); | |
| if (s.ai.inventory.bow && !isFishing && s.ai.state !== 'FORAGE') { | |
| ctx.strokeStyle = '#5c4033'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(20, 0, 15, -Math.PI / 2.5, Math.PI / 2.5); ctx.stroke(); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(25, -14); ctx.lineTo(10, 0); ctx.lineTo(25, 14); ctx.stroke(); | |
| } else if (s.ai.inventory.spear && !isFishing) { | |
| ctx.strokeStyle = '#5c4033'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(0, 16); ctx.lineTo(35, 16); ctx.stroke(); | |
| ctx.fillStyle = '#bcc4c7'; ctx.beginPath(); ctx.moveTo(35, 13); ctx.lineTo(48, 16); ctx.lineTo(35, 19); ctx.fill(); | |
| } | |
| ctx.fillStyle = '#2980b9'; ctx.beginPath(); ctx.ellipse(0, 0, 14, 18, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#f1c27d'; ctx.beginPath(); ctx.ellipse(5 + bob * 0.5, 0, 10, 10, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.beginPath(); ctx.ellipse(10, -14 + bob, 5, 5, 0, 0, Math.PI * 2); ctx.fill(); | |
| ctx.beginPath(); ctx.ellipse(10, 14 - bob, 5, 5, 0, 0, Math.PI * 2); ctx.fill(); | |
| if (isFishing) { | |
| ctx.strokeStyle = '#8f6640'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(10, 14); ctx.lineTo(45, 35); ctx.stroke(); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(45, 35); ctx.lineTo(100, 0); ctx.stroke(); | |
| ctx.fillStyle = '#ff3333'; ctx.beginPath(); ctx.arc(100, 0 + Math.sin(frameCount * 0.1) * 4, 4, 0, Math.PI * 2); ctx.fill(); | |
| } | |
| if (s.ai.panic) { ctx.fillStyle = '#73c2fb'; ctx.beginPath(); ctx.arc(0, -15, 3, 0, Math.PI * 2); ctx.arc(-5, 15, 2, 0, Math.PI * 2); ctx.fill(); } | |
| if (s.ai.sneaking) { ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.arc(0, 0, 30, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); } | |
| ctx.restore(); | |
| ctx.globalCompositeOperation = 'lighter'; | |
| WORLD_OBJECTS.fires.forEach((f, index) => { | |
| if (f.fuel <= 0) { WORLD_OBJECTS.fires.splice(index, 1); return; } | |
| const gradient = ctx.createRadialGradient(f.x, f.y, 10, f.x, f.y, 300 + Math.random() * 30); | |
| gradient.addColorStop(0, 'rgba(255, 160, 0, 0.8)'); gradient.addColorStop(1, 'rgba(255, 20, 0, 0)'); | |
| ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(f.x, f.y, 350, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = '#fffb00'; ctx.beginPath(); ctx.arc(f.x, f.y, 8 + Math.random() * 4, 0, Math.PI * 2); ctx.fill(); | |
| }); | |
| ctx.globalCompositeOperation = 'source-over'; | |
| ctx.restore(); | |
| const isNight2 = s.time < 6 || s.time > 18; | |
| let darkAlpha = isNight2 ? 0.85 : 0; | |
| if (s.activeEvents.includes('Storm Collapse')) darkAlpha = Math.max(darkAlpha, 0.65); | |
| if (darkAlpha > 0) { ctx.fillStyle = `rgba(8, 12, 25, ${darkAlpha})`; ctx.fillRect(0, 0, canvas.width, canvas.height); } | |
| if (s.activeEvents.includes('Heatwave')) { ctx.fillStyle = `rgba(255, 90, 0, 0.18)`; ctx.fillRect(0, 0, canvas.width, canvas.height); } | |
| if (s.activeEvents.includes('Night Freeze')) { | |
| ctx.fillStyle = `rgba(160, 230, 255, 0.35)`; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const frostGrad = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, canvas.height * 0.3, canvas.width / 2, canvas.height / 2, canvas.height); | |
| frostGrad.addColorStop(0, 'rgba(255,255,255,0)'); frostGrad.addColorStop(1, 'rgba(255,255,255,0.7)'); | |
| ctx.fillStyle = frostGrad; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| if (s.activeEvents.includes('Thick Fog')) { | |
| const fogGrad = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, 150, canvas.width / 2, canvas.height / 2, 600); | |
| fogGrad.addColorStop(0, 'rgba(240, 245, 255, 0)'); fogGrad.addColorStop(1, 'rgba(240, 245, 255, 0.95)'); | |
| ctx.fillStyle = fogGrad; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| if (s.activeEvents.includes('Storm Collapse')) { | |
| ctx.strokeStyle = 'rgba(220, 230, 255, 0.8)'; ctx.lineWidth = 2.5; ctx.beginPath(); | |
| for (let i = 0; i < 250; i++) { let rx = Math.random() * canvas.width; let ry = (frameCount * 40 + i * 25) % canvas.height; ctx.moveTo(rx, ry); ctx.lineTo(rx - 20, ry + 50); } | |
| ctx.stroke(); | |
| if (Math.random() < 0.05) { ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } | |
| } | |
| if (isNearDeath) { | |
| ctx.fillStyle = `rgba(220, 0, 0, ${0.2 + Math.sin(frameCount * 0.15) * 0.2})`; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const deathGrad = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, canvas.height * 0.3, canvas.width / 2, canvas.height / 2, canvas.height); | |
| deathGrad.addColorStop(0, 'rgba(0,0,0,0)'); deathGrad.addColorStop(1, 'rgba(120,0,0,0.9)'); | |
| ctx.fillStyle = deathGrad; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| animationId = requestAnimationFrame(render); | |
| }; | |
| render(); | |
| return () => { cancelAnimationFrame(animationId); window.removeEventListener('resize', resize); }; | |
| }, [gameRef.current.started, gameRef.current.isAlive]); | |
| useEffect(() => { | |
| if (!gameRef.current.started || !gameRef.current.isAlive) return; | |
| const interval = setInterval(() => { | |
| const s = gameRef.current; if (!s.isAlive) return; | |
| if (!s.ai.llmThinking) executeLLMLogic(s); | |
| manageChallenges(s); | |
| s.time += 0.25; if (s.time >= 24) { s.time = 0; s.day += 1; addLog(`Day ${s.day} begins.`, 'system'); } | |
| s.eventTimer -= 1; | |
| if (s.eventTimer <= 0 && !s.activeChallenge) { | |
| s.activeEvents = []; s.eventTimer = Math.floor(Math.random() * 40) + 60; | |
| addLog("Atmosphere stabilizing.", "info"); | |
| } else if (s.eventTimer <= 0) { | |
| s.eventTimer = 30; | |
| } | |
| s.hunger = clamp(s.hunger - (s.ai.panic ? 1.0 : 0.5), 0, 100); | |
| let thirstDrain = s.activeEvents.includes('Heatwave') ? 2.5 : 0.8; | |
| s.thirst = clamp(s.thirst - thirstDrain, 0, 100); | |
| let targetTemp = 25; | |
| if (s.time < 6 || s.time > 18) targetTemp -= 5; | |
| if (s.activeEvents.includes('Storm Collapse')) targetTemp -= 20; | |
| if (s.activeEvents.includes('Night Freeze')) targetTemp = -15; | |
| if (s.activeEvents.includes('Heatwave')) targetTemp += 25; | |
| WORLD_OBJECTS.fires.forEach(f => { if (Math.hypot(s.player.x - f.x, s.player.y - f.y) < 300) { targetTemp += 40; f.fuel -= (s.activeEvents.includes('Storm Collapse') && s.shelterStatus === 0 ? 8 : 1); } }); | |
| targetTemp -= (s.wetness / 8); | |
| if (s.shelterStatus === 100 && s.activeEvents.length > 0) targetTemp += 20; | |
| if (s.temp < targetTemp) s.temp = clamp(s.temp + 1.0, -20, 60); else if (s.temp > targetTemp) s.temp = clamp(s.temp - 1.5, -20, 60); | |
| if (s.activeEvents.includes('Flash Flood') && s.player.x < s.dynamicWaterLevel) s.wetness = 100; | |
| else if (s.activeEvents.includes('Storm Collapse') && s.shelterStatus === 0) s.wetness = clamp(s.wetness + 15, 0, 100); | |
| else s.wetness = clamp(s.wetness - 2, 0, 100); | |
| if (s.ai.state === 'IDLE' || s.ai.sneaking) s.stamina = clamp(s.stamina + 2, 0, 100); | |
| let healthDrain = 0; | |
| if (s.hunger <= 0) healthDrain += 2.0; | |
| if (s.thirst <= 0) healthDrain += 3.0; | |
| if (s.temp < 10) healthDrain += 3.0; | |
| if (s.temp > 45) healthDrain += 3.0; | |
| if (healthDrain > 0) { s.health -= healthDrain; } | |
| else if (s.hunger > 50 && s.thirst > 50 && s.temp > 15 && s.temp < 35 && s.ai.fear < 50) { s.health = clamp(s.health + 0.5, 0, 100); } | |
| if (s.health < 1 && s.isAlive) { | |
| s.health = 0; s.isAlive = false; | |
| if (s.hunger <= 0) s.causeOfDeath = "Starvation"; | |
| else if (s.thirst <= 0) s.causeOfDeath = "Dehydration"; | |
| else if (s.temp < 10) s.causeOfDeath = "Hypothermia"; | |
| else if (s.temp > 45) s.causeOfDeath = "Heatstroke"; | |
| else s.causeOfDeath = "System Failure"; | |
| if (s.gameMode === 'evolution') { | |
| const deathRecord = `${s.causeOfDeath} on Day ${s.day} (Gen ${s.generation})`; | |
| s.ai.memory.pastDeaths.push(deathRecord); | |
| if (s.ai.memory.pastDeaths.length > 6) s.ai.memory.pastDeaths.shift(); | |
| s.ai.memory.evolutionLevel = clamp(s.ai.memory.evolutionLevel + 1, 1, 20); | |
| } | |
| addLog("SUBJECT TERMINATED.", "danger"); | |
| } | |
| setHudState({ ...s, logs: [...s.logs], ai: { ...s.ai } }); | |
| }, TICK_RATE); | |
| return () => clearInterval(interval); | |
| }, [gameRef.current.started, gameRef.current.isAlive]); | |
| const formatTime = (t) => `${Math.floor(t).toString().padStart(2, '0')}:${Math.floor((t - Math.floor(t)) * 60).toString().padStart(2, '0')}`; | |
| const CircularStat = ({ icon: Icon, value, color, warn }) => { | |
| const radius = 20; const circumference = 2 * Math.PI * radius; | |
| const strokeDashoffset = circumference - (clamp(value, 0, 100) / 100) * circumference; | |
| return ( | |
| <div className={`relative w-14 h-14 rounded-full bg-black/60 border-2 ${warn ? 'border-red-500 animate-pulse' : 'border-black/80'} flex items-center justify-center shadow-lg backdrop-blur-md`}> | |
| <svg className="absolute inset-0 w-full h-full transform -rotate-90 pointer-events-none"> | |
| <circle cx="26" cy="26" r={radius} stroke="rgba(255,255,255,0.1)" strokeWidth="4" fill="none" /> | |
| <circle cx="26" cy="26" r={radius} stroke={color} strokeWidth="4" fill="none" strokeDasharray={circumference} strokeDashoffset={strokeDashoffset} className="transition-all duration-1000 ease-out" /> | |
| </svg> | |
| <Icon size={20} color={warn ? '#ef4444' : '#fff'} className="z-10 drop-shadow-md" /> | |
| </div> | |
| ); | |
| }; | |
| const HotbarSlot = ({ icon: Icon, count, active, label }) => ( | |
| <div className={`w-14 h-14 bg-black/70 backdrop-blur-md border-2 ${active ? 'border-emerald-400 shadow-[0_0_15px_rgba(52,211,153,0.5)]' : 'border-zinc-800'} rounded-lg flex flex-col items-center justify-center relative`}> | |
| <Icon size={24} className={active ? 'text-emerald-400' : 'text-zinc-400'} /> | |
| {count !== undefined && <span className="absolute bottom-0 right-1 text-[10px] font-bold text-white">{count}</span>} | |
| {label && <span className="absolute top-0 left-1 text-[8px] font-bold text-zinc-500">{label}</span>} | |
| </div> | |
| ); | |
| if (!gameRef.current.started) { | |
| return ( | |
| <div className="min-h-screen bg-zinc-950 text-zinc-300 flex flex-col items-center justify-center p-6 font-mono relative overflow-hidden"> | |
| <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-zinc-950 to-zinc-950"></div> | |
| <div className="max-w-3xl w-full space-y-6 z-10"> | |
| <h1 className="text-5xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-emerald-400 border-b border-zinc-800/50 pb-6 flex items-center gap-4"><Brain className="text-blue-500" size={48}/> EVOLUTIONARY AI</h1> | |
| <p className="leading-relaxed text-zinc-400 text-lg">Initialize Subject-01 into the high-fidelity 2.5D simulation. Powered by HuggingFace AI with persistent memory across 6 generations.</p> | |
| <div className="grid grid-cols-2 gap-6 mt-8"> | |
| <button onClick={() => startGame('hardcore')} className="bg-zinc-900/80 backdrop-blur border border-red-900/50 p-8 rounded-xl hover:bg-red-950/40 transition-all text-left flex flex-col gap-3 group shadow-2xl"> | |
| <div className="flex items-center gap-3 text-red-500 font-bold text-2xl"><Skull size={28}/> HARDCORE MODE</div> | |
| <span className="text-sm text-zinc-400 leading-relaxed">Permadeath. No memory retention. Pure sandbox survival. Challenge-aware AI instincts active.</span> | |
| </button> | |
| <button onClick={() => startGame('evolution')} className="bg-zinc-900/80 backdrop-blur border border-blue-900/50 p-8 rounded-xl hover:bg-blue-950/40 transition-all text-left flex flex-col gap-3 group shadow-2xl"> | |
| <div className="flex items-center gap-3 text-blue-400 font-bold text-2xl"><RefreshCcw size={28}/> LLM EVOLUTION</div> | |
| <span className="text-sm text-zinc-400 leading-relaxed">HuggingFace API connected. AI remembers how it died and adapts. Challenge overrides guide behavior toward success.</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!hudState) return null; | |
| const survivalLessons = buildSurvivalLessons(hudState.ai.memory.pastDeaths); | |
| return ( | |
| <div className="relative w-screen h-screen overflow-hidden bg-zinc-950 font-sans text-zinc-300 select-none"> | |
| <canvas ref={canvasRef} className="absolute inset-0 block w-full h-full" /> | |
| {!hudState.isAlive && ( | |
| <div className="absolute inset-0 z-50 bg-black/90 backdrop-blur-md flex flex-col items-center justify-center p-6 animate-in fade-in duration-1000"> | |
| <div className="max-w-lg w-full text-center space-y-6"> | |
| <Skull size={72} className="mx-auto text-red-500 mb-6 animate-pulse drop-shadow-[0_0_20px_rgba(239,68,68,0.5)]" /> | |
| <h1 className="text-5xl font-black tracking-tighter text-white">SIMULATION ENDED</h1> | |
| <div className="bg-zinc-900/90 p-8 border border-zinc-800 rounded-xl text-left space-y-5 shadow-2xl"> | |
| <div className="flex justify-between border-b border-zinc-800 pb-3 text-lg"><span className="text-zinc-500">Cause of Death</span><span className="text-red-400 font-bold uppercase">{hudState.causeOfDeath}</span></div> | |
| <div className="flex justify-between border-b border-zinc-800 pb-3 text-lg"><span className="text-zinc-500">Days Survived</span><span className="text-white font-bold">{hudState.day}</span></div> | |
| <div className="flex justify-between border-b border-zinc-800 pb-3 text-lg"><span className="text-zinc-500">Challenges Completed</span><span className="text-emerald-400 font-bold">{hudState.completedChallenges}</span></div> | |
| <div className="flex justify-between border-b border-zinc-800 pb-3 text-lg"><span className="text-zinc-500">Generation</span><span className="text-blue-400 font-bold">GEN-{hudState.generation}</span></div> | |
| {hudState.ai.memory.pastDeaths.length > 0 && ( | |
| <div className="border-t border-zinc-800 pt-4"> | |
| <p className="text-zinc-500 text-sm mb-3 uppercase tracking-wider font-bold">Death Memory ({hudState.ai.memory.pastDeaths.length}/6)</p> | |
| <div className="space-y-1"> | |
| {hudState.ai.memory.pastDeaths.map((d, i) => ( | |
| <div key={i} className="text-xs text-zinc-400 bg-zinc-950/80 px-3 py-1 rounded-lg flex items-center gap-2"> | |
| <span className="text-red-500">#{i + 1}</span> {d} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {hudState.gameMode === 'evolution' ? ( | |
| <button onClick={evolveAndRespawn} className="w-full py-5 mt-8 bg-blue-600/20 text-blue-400 font-bold text-lg uppercase tracking-widest hover:bg-blue-600/40 rounded-xl border border-blue-500/50 flex items-center justify-center gap-3 transition-all"><RefreshCcw size={24}/> Upload Trauma & Respawn (Gen {hudState.generation + 1})</button> | |
| ) : ( | |
| <button onClick={() => window.location.reload()} className="w-full py-5 mt-8 bg-zinc-800 text-white font-bold text-lg uppercase tracking-widest hover:bg-zinc-700 rounded-xl border border-zinc-700 flex items-center justify-center gap-3 transition-all"><Skull size={24}/> Hard Reset</button> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {hudState.isAlive && ( | |
| <> | |
| <div className="absolute top-6 right-6 flex flex-col items-end gap-2 z-20 pointer-events-none"> | |
| <div className="bg-black/70 backdrop-blur-md px-6 py-3 rounded-full border border-zinc-800 shadow-2xl flex items-center gap-4"> | |
| <Brain className={`${hudState.ai.llmThinking ? 'text-purple-400 animate-pulse' : hudState.ai.consecutiveFailures > 0 ? 'text-yellow-400' : 'text-blue-400'}`} size={24}/> | |
| <div className="flex flex-col"> | |
| <span className="text-[10px] font-bold text-zinc-500 tracking-widest uppercase"> | |
| Subject-01 Gen-{hudState.generation} {hudState.ai.llmThinking ? '(Thinking...)' : hudState.ai.consecutiveFailures > 0 ? '(Offline)' : ''} | |
| </span> | |
| <span className={`text-sm font-black uppercase tracking-wider ${hudState.ai.panic ? 'text-red-500 animate-pulse' : hudState.ai.sneaking ? 'text-yellow-400' : 'text-white'}`}> | |
| {hudState.ai.panic ? 'FULL PANIC' : hudState.ai.sneaking ? 'SNEAKING' : hudState.ai.state.replace('_', ' ')} | |
| </span> | |
| </div> | |
| </div> | |
| <div className="bg-black/50 backdrop-blur-sm px-4 py-2 rounded-full border border-zinc-800/50 text-xs text-zinc-300 italic shadow-lg max-w-xs text-right"> | |
| "{hudState.ai.message}" | |
| </div> | |
| {hudState.gameMode === 'evolution' && hudState.ai.memory.pastDeaths.length > 0 && ( | |
| <button | |
| onClick={() => setShowMemoryLog(v => !v)} | |
| className="pointer-events-auto bg-purple-950/70 backdrop-blur-sm px-3 py-1.5 rounded-full border border-purple-800/50 text-xs text-purple-300 flex items-center gap-2 hover:bg-purple-900/70 transition-colors" | |
| > | |
| <Eye size={12}/> {hudState.ai.memory.pastDeaths.length} death memories loaded | |
| </button> | |
| )} | |
| {showMemoryLog && hudState.ai.memory.pastDeaths.length > 0 && ( | |
| <div className="pointer-events-auto bg-black/90 backdrop-blur-md p-4 rounded-xl border border-purple-800/40 shadow-2xl w-80 space-y-2"> | |
| <p className="text-[10px] font-bold text-purple-400 tracking-widest uppercase mb-2 flex items-center gap-2"><Brain size={12}/> Neural Memory Log</p> | |
| {hudState.ai.memory.pastDeaths.map((d, i) => ( | |
| <div key={i} className="text-xs text-zinc-300 bg-zinc-900/80 px-3 py-1.5 rounded-lg flex items-start gap-2"> | |
| <span className="text-red-400 font-bold shrink-0">#{i + 1}</span> | |
| <span>{d}</span> | |
| </div> | |
| ))} | |
| {survivalLessons.length > 0 && ( | |
| <div className="border-t border-purple-900/40 pt-2 mt-2"> | |
| <p className="text-[9px] text-purple-400 uppercase tracking-widest mb-1">Behavior Adaptations</p> | |
| {survivalLessons.slice(0, 3).map((l, i) => ( | |
| <div key={i} className="text-[10px] text-emerald-400 bg-emerald-950/30 px-2 py-1 rounded mb-1">{l.slice(0, 80)}...</div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| <div className="absolute top-6 left-6 flex flex-col gap-3 z-20 pointer-events-auto max-h-[90vh]"> | |
| <div className="bg-black/70 backdrop-blur-md px-5 py-3 rounded-2xl border border-zinc-800 shadow-2xl flex items-center gap-4 mb-2"> | |
| <div className="flex flex-col border-r border-zinc-700 pr-4"> | |
| <span className="text-[10px] font-bold text-zinc-500 tracking-widest uppercase">Generation</span> | |
| <span className="text-sm font-black text-blue-400">GEN-{hudState.generation}</span> | |
| </div> | |
| <div className="flex flex-col border-r border-zinc-700 pr-4"> | |
| <span className="text-[10px] font-bold text-zinc-500 tracking-widest uppercase">Day {hudState.day}</span> | |
| <span className="text-sm font-black text-white flex items-center gap-2">{hudState.time < 6 || hudState.time > 18 ? <Moon size={14} className="text-blue-300"/> : <Sun size={14} className="text-yellow-400"/>} {formatTime(hudState.time)}</span> | |
| </div> | |
| <div className="flex flex-col"> | |
| <span className="text-[10px] font-bold text-zinc-500 tracking-widest uppercase">Evo Level</span> | |
| <span className="text-sm font-black text-emerald-400 flex items-center gap-1"><TrendingUp size={12}/> {hudState.ai.memory.evolutionLevel}</span> | |
| </div> | |
| </div> | |
| <div className="bg-black/80 backdrop-blur-md p-4 rounded-2xl border border-zinc-800 shadow-2xl w-72 flex flex-col gap-3 flex-1 overflow-y-auto custom-scrollbar"> | |
| <div className="text-[10px] font-bold text-emerald-400 tracking-widest uppercase flex items-center gap-2 border-b border-zinc-800 pb-2"><ListTodo size={14}/> Challenge Master</div> | |
| {CHALLENGE_DB.map((cat, idx) => ( | |
| <div key={idx} className="flex flex-col gap-1"> | |
| <span className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider mt-2">{cat.category}</span> | |
| {cat.tasks.map((task, tIdx) => ( | |
| <button | |
| key={tIdx} | |
| onClick={() => assignAdvancedChallenge(task)} | |
| disabled={hudState.activeChallenge !== null} | |
| className="bg-zinc-900/50 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed border border-zinc-800 p-2 rounded-lg text-left transition-colors flex justify-between items-center group" | |
| > | |
| <span className="text-xs font-bold text-zinc-300 group-hover:text-white transition-colors">{task.name}</span> | |
| <span className="text-[9px] text-zinc-500 bg-zinc-950 px-1.5 py-0.5 rounded">{task.timeLimit}s</span> | |
| </button> | |
| ))} | |
| </div> | |
| ))} | |
| <div className="border-t border-zinc-800 pt-3 mt-2"> | |
| <div className="text-[10px] font-bold text-red-400 tracking-widest uppercase flex items-center gap-2 mb-2"><ShieldAlert size={14}/> God Controls</div> | |
| <div className="grid grid-cols-2 gap-1.5"> | |
| {['Flash Flood', 'Storm Collapse', 'Thick Fog', 'Heatwave', 'Night Freeze'].map(ev => ( | |
| <button key={ev} onClick={() => toggleEvent(ev)} className={`text-[10px] font-bold px-2 py-1.5 rounded border transition-colors ${hudState.activeEvents.includes(ev) ? 'bg-red-900/60 border-red-700 text-red-300' : 'bg-zinc-900/50 border-zinc-700 text-zinc-400 hover:bg-zinc-800'}`}> | |
| {ev} | |
| </button> | |
| ))} | |
| <button onClick={() => toggleEvent('Clear')} className="text-[10px] font-bold px-2 py-1.5 rounded border bg-zinc-800 border-zinc-600 text-zinc-300 hover:bg-zinc-700 transition-colors">Clear All</button> | |
| </div> | |
| <div className="grid grid-cols-2 gap-1.5 mt-1.5"> | |
| <button onClick={godAmbush} className="text-[10px] font-bold px-2 py-1.5 rounded border bg-red-950/50 border-red-800 text-red-400 hover:bg-red-900/50 transition-colors">Ambush!</button> | |
| <button onClick={godSmite} className="text-[10px] font-bold px-2 py-1.5 rounded border bg-yellow-950/50 border-yellow-800 text-yellow-400 hover:bg-yellow-900/50 transition-colors">Smite</button> | |
| <button onClick={godStarve} className="text-[10px] font-bold px-2 py-1.5 rounded border bg-orange-950/50 border-orange-800 text-orange-400 hover:bg-orange-900/50 transition-colors">Starve</button> | |
| <button onClick={godBless} className="text-[10px] font-bold px-2 py-1.5 rounded border bg-emerald-950/50 border-emerald-800 text-emerald-400 hover:bg-emerald-900/50 transition-colors">Bless</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="absolute bottom-6 left-[320px] flex gap-3 z-20 pointer-events-none"> | |
| <CircularStat icon={Heart} value={hudState.health} color="#ef4444" warn={hudState.health < 30} /> | |
| <CircularStat icon={Apple} value={hudState.hunger} color="#f97316" warn={hudState.hunger < 30} /> | |
| <CircularStat icon={Droplets} value={hudState.thirst} color="#3b82f6" warn={hudState.thirst < 30} /> | |
| <CircularStat icon={Zap} value={hudState.stamina} color="#eab308" warn={hudState.stamina < 20} /> | |
| <div className="ml-4 flex items-center justify-center"> | |
| <CircularStat icon={Ghost} value={hudState.ai.fear} color="#a855f7" warn={hudState.ai.fear > 80} /> | |
| </div> | |
| </div> | |
| <div className="absolute bottom-6 right-6 flex gap-2 z-20 pointer-events-none"> | |
| <HotbarSlot icon={Axe} count={hudState.wood} label="WOOD" active={hudState.wood > 0} /> | |
| <HotbarSlot icon={CloudRain} count={hudState.stone} label="STONE" active={hudState.stone > 0} /> | |
| <HotbarSlot icon={Apple} count={hudState.food} label="FOOD" active={hudState.food > 0} /> | |
| <div className="w-1 h-14 bg-zinc-800 rounded-full mx-1"></div> | |
| <HotbarSlot icon={Sword} label="SPEAR" active={hudState.ai.inventory.spear} /> | |
| <HotbarSlot icon={Crosshair} label="BOW" active={hudState.ai.inventory.bow} /> | |
| <HotbarSlot icon={Fish} label="ROD" active={hudState.ai.inventory.fishingRod} /> | |
| <HotbarSlot icon={Navigation} label="BOAT" active={hudState.ai.inventory.boat} /> | |
| </div> | |
| {hudState.activeChallenge && ( | |
| <div className="absolute top-8 left-1/2 -translate-x-1/2 bg-black/80 backdrop-blur-md border border-emerald-500/50 px-6 py-4 rounded-2xl shadow-[0_0_30px_rgba(16,185,129,0.2)] flex flex-col items-center z-10 pointer-events-none w-[420px] animate-in slide-in-from-top"> | |
| <div className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest flex items-center gap-2 mb-1"><Trophy size={14}/> Active Challenge</div> | |
| <div className="text-lg font-black text-white text-center tracking-wide mb-1">{hudState.activeChallenge.name}</div> | |
| {hudState.activeChallenge.progress && ( | |
| <div className="text-xs text-zinc-400 mb-2 font-mono">{hudState.activeChallenge.progress}</div> | |
| )} | |
| <div className="w-full bg-zinc-900 h-2 rounded-full overflow-hidden"> | |
| <div | |
| className="bg-emerald-500 h-full transition-all" | |
| style={{ width: `${(hudState.activeChallenge.timeLimit / hudState.activeChallenge.maxTime) * 100}%` }} | |
| ></div> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| {hudState.logs.length > 0 && hudState.logs[0].time && (Date.now() - hudState.logs[0].time < 4000) && ( | |
| <div className="absolute top-[25%] left-1/2 -translate-x-1/2 z-20 pointer-events-none animate-in fade-in zoom-in duration-300"> | |
| <div className={`px-8 py-4 rounded-2xl border shadow-[0_0_30px_rgba(0,0,0,0.5)] backdrop-blur-xl text-lg font-black tracking-widest uppercase flex items-center gap-3 ${ | |
| hudState.logs[0].type === 'danger' ? 'bg-red-950/90 border-red-500/50 text-red-400' : | |
| hudState.logs[0].type === 'success' ? 'bg-emerald-950/90 border-emerald-500/50 text-emerald-400' : | |
| hudState.logs[0].type === 'warning' ? 'bg-yellow-950/90 border-yellow-500/50 text-yellow-400' : | |
| 'bg-black/90 border-zinc-700 text-zinc-300' | |
| }`}> | |
| {hudState.logs[0].message} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |