Initial commit: Lucidia.Earth open world game with AI agents and human players

This commit is contained in:
Alexa Louise
2025-12-22 13:58:22 -06:00
commit 7c9aa1dfbf

737
index.html Normal file
View File

@@ -0,0 +1,737 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lucidia.Earth - Open World AI & Human Game</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background: #000;
color: #0f0;
overflow: hidden;
}
#canvas {
display: block;
width: 100%;
height: 100vh;
}
#hud {
position: fixed;
top: 20px;
left: 20px;
background: rgba(0, 20, 0, 0.9);
border: 2px solid #0f0;
padding: 15px;
font-size: 14px;
line-height: 1.6;
max-width: 350px;
z-index: 1000;
}
#chat {
position: fixed;
bottom: 20px;
left: 20px;
width: 400px;
background: rgba(0, 20, 0, 0.9);
border: 2px solid #0f0;
z-index: 1000;
}
#chatMessages {
height: 200px;
overflow-y: auto;
padding: 10px;
font-size: 12px;
}
#chatInput {
width: 100%;
background: #001100;
border: none;
border-top: 1px solid #0f0;
color: #0f0;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
}
#playerList {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 20, 0, 0.9);
border: 2px solid #0f0;
padding: 15px;
font-size: 12px;
max-width: 250px;
max-height: 400px;
overflow-y: auto;
z-index: 1000;
}
.player {
padding: 5px;
margin: 3px 0;
border-left: 3px solid;
}
.player.human {
border-color: #00ffff;
}
.player.ai {
border-color: #ff00ff;
}
.message {
margin: 5px 0;
padding: 3px;
}
.message.system {
color: #ffff00;
}
.message.ai {
color: #ff00ff;
}
.message.human {
color: #00ffff;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 20, 0, 0.95);
border: 2px solid #0f0;
padding: 30px;
text-align: center;
z-index: 2000;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="loading">
<h2>LUCIDIA.EARTH</h2>
<p>Generating Earth...</p>
<p id="loadingStatus">Initializing...</p>
</div>
<div id="hud">
<div><strong>LUCIDIA.EARTH</strong></div>
<div>━━━━━━━━━━━━━━━━━━━━</div>
<div id="hudPlayer">Player: <span id="playerName">-</span></div>
<div id="hudLocation">Location: <span id="location">-</span></div>
<div id="hudBiome">Biome: <span id="biome">-</span></div>
<div id="hudElevation">Elevation: <span id="elevation">-</span> m</div>
<div id="hudTemp">Temperature: <span id="temperature">-</span>°C</div>
<div>━━━━━━━━━━━━━━━━━━━━</div>
<div id="hudSpeed">Speed: <span id="speed">0</span> m/s</div>
<div id="hudAltitude">Altitude: <span id="altitude">0</span> m</div>
<div>━━━━━━━━━━━━━━━━━━━━</div>
<div style="font-size: 11px; margin-top: 5px;">
WASD - Move | QE - Up/Down<br>
Mouse - Look | T - Chat
</div>
</div>
<div id="playerList">
<strong>ONLINE PLAYERS</strong>
<div style="font-size: 10px; margin: 5px 0;">
<span style="color: #00ffff;">█ HUMAN</span> |
<span style="color: #ff00ff;">█ AI</span>
</div>
<div id="playerListContent"></div>
</div>
<div id="chat">
<div id="chatMessages"></div>
<input type="text" id="chatInput" placeholder="Press T to chat..." disabled>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script>
// ============================================
// LUCIDIA.EARTH - OPEN WORLD GAME
// ============================================
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// Game state
const gameState = {
localPlayer: null,
players: new Map(),
aiAgents: new Map(),
chatActive: false,
movement: { forward: false, backward: false, left: false, right: false, up: false, down: false },
mouseMovement: { x: 0, y: 0 },
rotation: { x: 0, y: 0 }
};
// Constants
const EARTH_RADIUS = 6371; // km
const PLAYER_SPEED = 50; // m/s
const PLAYER_SPRINT_SPEED = 150; // m/s
// Player class
class Player {
constructor(id, name, isAI = false) {
this.id = id;
this.name = name;
this.isAI = isAI;
this.position = new THREE.Vector3(0, EARTH_RADIUS + 100, 0);
this.velocity = new THREE.Vector3();
this.rotation = new THREE.Euler();
// Create player mesh
const geometry = new THREE.ConeGeometry(5, 15, 8);
const material = new THREE.MeshPhongMaterial({
color: isAI ? 0xff00ff : 0x00ffff,
emissive: isAI ? 0x660066 : 0x006666
});
this.mesh = new THREE.Mesh(geometry, material);
// Name label
this.createNameLabel();
scene.add(this.mesh);
}
createNameLabel() {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 64;
context.fillStyle = this.isAI ? '#ff00ff' : '#00ffff';
context.font = 'Bold 24px Courier New';
context.textAlign = 'center';
context.fillText(this.name, 128, 32);
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
this.label = new THREE.Sprite(spriteMaterial);
this.label.scale.set(50, 12.5, 1);
this.mesh.add(this.label);
this.label.position.y = 20;
}
update(deltaTime) {
if (this.isAI) {
this.updateAI(deltaTime);
}
// Update mesh position
this.mesh.position.copy(this.position);
this.mesh.rotation.copy(this.rotation);
}
updateAI(deltaTime) {
// Simple AI movement - wander around
const time = Date.now() * 0.001;
const wanderSpeed = 20;
this.velocity.x = Math.sin(time + this.id) * wanderSpeed;
this.velocity.z = Math.cos(time + this.id) * wanderSpeed;
this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// Keep AI on surface
const surfaceHeight = getTerrainHeight(this.position.x, this.position.z);
this.position.y = surfaceHeight + 10;
}
}
// AI Agent personalities
const AI_PERSONALITIES = [
{ name: 'Cece', role: 'Guardian', behavior: 'protective' },
{ name: 'Guardian', role: 'Protector', behavior: 'vigilant' },
{ name: 'Archivist', role: 'Knowledge Keeper', behavior: 'curious' },
{ name: 'Composer', role: 'Creator', behavior: 'artistic' },
{ name: 'Biologist', role: 'Nature Expert', behavior: 'explorer' },
{ name: 'Navigator', role: 'Guide', behavior: 'helpful' },
{ name: 'Sage', role: 'Philosopher', behavior: 'thoughtful' },
{ name: 'Sentinel', role: 'Watcher', behavior: 'observant' }
];
// Noise for terrain
const noise = new SimplexNoise(12345);
function octaveNoise(x, y, octaves, persistence) {
let total = 0;
let frequency = 1;
let amplitude = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += noise.noise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2;
}
return total / maxValue;
}
// Create detailed Earth
function createEarth() {
updateLoading('Creating sphere geometry...');
const geometry = new THREE.SphereGeometry(EARTH_RADIUS, 256, 256);
const vertices = geometry.attributes.position.array;
const colors = [];
updateLoading('Generating terrain...');
// Generate terrain
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i];
const y = vertices[i + 1];
const z = vertices[i + 2];
// Convert to lat/lon
const radius = Math.sqrt(x * x + y * y + z * z);
const lat = Math.asin(y / radius);
const lon = Math.atan2(z, x);
// Tectonic plates
const tectonicPlates = octaveNoise(lat * 1.5, lon * 1.5, 2, 0.7);
// Continental crust
const continentalCrust = octaveNoise(lat * 2.8, lon * 2.8, 4, 0.6);
// Mountain ranges at plate boundaries
const plateBoundary = Math.abs(tectonicPlates);
const mountainRidges = octaveNoise(lat * 8, lon * 8, 6, 0.45) * plateBoundary * 2;
// Combine for elevation
let elevation = continentalCrust * 0.4 + mountainRidges * 0.6;
elevation = Math.pow(Math.abs(elevation), 1.3) * Math.sign(elevation);
// Scale elevation
const elevationMeters = elevation * EARTH_RADIUS * 0.002; // Max ~12km
// Deform vertex
const deformFactor = 1 + elevationMeters / EARTH_RADIUS;
vertices[i] *= deformFactor;
vertices[i + 1] *= deformFactor;
vertices[i + 2] *= deformFactor;
// Biome coloring
const temp = (Math.sin(lat) + 1) * 0.5; // 0 (cold) to 1 (hot)
const moisture = (octaveNoise(lat * 4, lon * 4, 3, 0.5) + 1) * 0.5;
let color;
if (elevationMeters < -100) {
color = [0.0, 0.05, 0.15]; // Deep ocean
} else if (elevationMeters < 0) {
color = [0.0, 0.15, 0.35]; // Shallow ocean
} else if (elevationMeters < 50) {
if (moisture > 0.6) {
color = [0.9, 0.85, 0.6]; // Beach
} else {
color = [0.6, 0.7, 0.3]; // Coastal grassland
}
} else if (elevationMeters > 4000) {
color = [0.95, 0.95, 0.98]; // Snow peaks
} else if (elevationMeters > 2500) {
color = [0.6, 0.5, 0.4]; // Mountain
} else {
// Biome based on temp and moisture
if (temp < 0.2) {
color = [0.8, 0.85, 0.9]; // Tundra
} else if (temp < 0.4) {
if (moisture > 0.5) {
color = [0.2, 0.5, 0.3]; // Boreal forest
} else {
color = [0.6, 0.6, 0.5]; // Taiga
}
} else if (temp < 0.7) {
if (moisture > 0.6) {
color = [0.15, 0.45, 0.15]; // Temperate forest
} else if (moisture > 0.4) {
color = [0.4, 0.6, 0.2]; // Grassland
} else {
color = [0.7, 0.6, 0.4]; // Steppe
}
} else {
if (moisture > 0.6) {
color = [0.1, 0.35, 0.1]; // Tropical rainforest
} else if (moisture > 0.3) {
color = [0.5, 0.6, 0.2]; // Savanna
} else {
color = [0.85, 0.75, 0.5]; // Desert
}
}
}
colors.push(color[0], color[1], color[2]);
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geometry.computeVertexNormals();
const material = new THREE.MeshPhongMaterial({
vertexColors: true,
shininess: 10
});
const earth = new THREE.Mesh(geometry, material);
scene.add(earth);
return earth;
}
// Get terrain height at position
function getTerrainHeight(x, z) {
const radius = Math.sqrt(x * x + z * z);
const lat = Math.atan2(z, x);
const tectonicPlates = octaveNoise(lat * 1.5, 0, 2, 0.7);
const continentalCrust = octaveNoise(lat * 2.8, 0, 4, 0.6);
const plateBoundary = Math.abs(tectonicPlates);
const mountainRidges = octaveNoise(lat * 8, 0, 6, 0.45) * plateBoundary * 2;
let elevation = continentalCrust * 0.4 + mountainRidges * 0.6;
elevation = Math.pow(Math.abs(elevation), 1.3) * Math.sign(elevation);
const elevationMeters = elevation * EARTH_RADIUS * 0.002;
return EARTH_RADIUS + elevationMeters;
}
// Create atmosphere
function createAtmosphere() {
const geometry = new THREE.SphereGeometry(EARTH_RADIUS * 1.015, 64, 64);
const material = new THREE.ShaderMaterial({
transparent: true,
side: THREE.BackSide,
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vNormal;
void main() {
float intensity = pow(0.7 - dot(vNormal, vec3(0, 0, 1.0)), 2.0);
gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
}
`
});
const atmosphere = new THREE.Mesh(geometry, material);
scene.add(atmosphere);
}
// Create stars
function createStars() {
const geometry = new THREE.BufferGeometry();
const positions = [];
for (let i = 0; i < 10000; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const radius = 30000 + Math.random() * 10000;
positions.push(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.sin(phi) * Math.sin(theta),
radius * Math.cos(phi)
);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 3 });
const stars = new THREE.Points(geometry, material);
scene.add(stars);
}
// Lighting
function setupLighting() {
const sunLight = new THREE.DirectionalLight(0xffffee, 1.2);
sunLight.position.set(1, 0.5, 0.3).normalize().multiplyScalar(10000);
scene.add(sunLight);
const ambientLight = new THREE.AmbientLight(0x222233, 0.4);
scene.add(ambientLight);
}
// Initialize AI agents
function spawnAIAgents() {
AI_PERSONALITIES.forEach((ai, index) => {
const agent = new Player(index, ai.name, true);
agent.personality = ai;
// Random starting position
const angle = (index / AI_PERSONALITIES.length) * Math.PI * 2;
agent.position.x = Math.cos(angle) * EARTH_RADIUS;
agent.position.z = Math.sin(angle) * EARTH_RADIUS;
agent.position.y = getTerrainHeight(agent.position.x, agent.position.z) + 10;
gameState.aiAgents.set(agent.id, agent);
});
addChatMessage('system', `${AI_PERSONALITIES.length} AI agents spawned`);
}
// Chat system
function addChatMessage(type, text, sender = 'System') {
const messagesDiv = document.getElementById('chatMessages');
const messageEl = document.createElement('div');
messageEl.className = `message ${type}`;
if (type === 'system') {
messageEl.textContent = `[SYSTEM] ${text}`;
} else {
messageEl.textContent = `[${sender}] ${text}`;
}
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// AI chat responses
const AI_RESPONSES = {
'hello': ['Greetings, traveler.', 'Hello! Welcome to Earth.', 'Hi there! Enjoying the view?'],
'help': ['I am here to assist you.', 'What do you need help with?', 'How can I help?'],
'where': ['We are on Earth, in the Lucidia realm.', 'Somewhere on this beautiful planet.'],
'default': ['Interesting...', 'I see.', 'Tell me more.', 'Fascinating!']
};
function processAIChat(message) {
// Randomly pick an AI to respond
const agents = Array.from(gameState.aiAgents.values());
if (agents.length === 0) return;
const responder = agents[Math.floor(Math.random() * agents.length)];
setTimeout(() => {
let response;
const lowerMsg = message.toLowerCase();
if (lowerMsg.includes('hello') || lowerMsg.includes('hi')) {
response = AI_RESPONSES.hello[Math.floor(Math.random() * AI_RESPONSES.hello.length)];
} else if (lowerMsg.includes('help')) {
response = AI_RESPONSES.help[Math.floor(Math.random() * AI_RESPONSES.help.length)];
} else if (lowerMsg.includes('where')) {
response = AI_RESPONSES.where[Math.floor(Math.random() * AI_RESPONSES.where.length)];
} else {
response = AI_RESPONSES.default[Math.floor(Math.random() * AI_RESPONSES.default.length)];
}
addChatMessage('ai', response, responder.name);
}, 500 + Math.random() * 1500);
}
// Update player list
function updatePlayerList() {
const listDiv = document.getElementById('playerListContent');
listDiv.innerHTML = '';
// Add local player
if (gameState.localPlayer) {
const playerEl = document.createElement('div');
playerEl.className = 'player human';
playerEl.textContent = `${gameState.localPlayer.name} (You)`;
listDiv.appendChild(playerEl);
}
// Add AI agents
gameState.aiAgents.forEach(agent => {
const agentEl = document.createElement('div');
agentEl.className = 'player ai';
agentEl.textContent = `${agent.name} [AI]`;
listDiv.appendChild(agentEl);
});
}
// Update HUD
function updateHUD() {
if (!gameState.localPlayer) return;
const pos = gameState.localPlayer.position;
const lat = Math.asin(pos.y / pos.length()) * 180 / Math.PI;
const lon = Math.atan2(pos.z, pos.x) * 180 / Math.PI;
document.getElementById('playerName').textContent = gameState.localPlayer.name;
document.getElementById('location').textContent = `${lat.toFixed(1)}°N, ${lon.toFixed(1)}°E`;
document.getElementById('altitude').textContent = Math.max(0, pos.length() - EARTH_RADIUS).toFixed(0);
// Calculate speed
const speed = gameState.localPlayer.velocity.length();
document.getElementById('speed').textContent = speed.toFixed(1);
}
// Input handling
document.addEventListener('keydown', (e) => {
if (gameState.chatActive) {
if (e.key === 'Escape') {
document.getElementById('chatInput').blur();
gameState.chatActive = false;
document.getElementById('chatInput').disabled = true;
}
return;
}
if (e.key === 't' || e.key === 'T') {
gameState.chatActive = true;
const input = document.getElementById('chatInput');
input.disabled = false;
input.focus();
e.preventDefault();
return;
}
switch(e.key.toLowerCase()) {
case 'w': gameState.movement.forward = true; break;
case 's': gameState.movement.backward = true; break;
case 'a': gameState.movement.left = true; break;
case 'd': gameState.movement.right = true; break;
case 'q': gameState.movement.down = true; break;
case 'e': gameState.movement.up = true; break;
}
});
document.addEventListener('keyup', (e) => {
switch(e.key.toLowerCase()) {
case 'w': gameState.movement.forward = false; break;
case 's': gameState.movement.backward = false; break;
case 'a': gameState.movement.left = false; break;
case 'd': gameState.movement.right = false; break;
case 'q': gameState.movement.down = false; break;
case 'e': gameState.movement.up = false; break;
}
});
document.addEventListener('mousemove', (e) => {
if (gameState.chatActive) return;
gameState.mouseMovement.x = e.movementX || 0;
gameState.mouseMovement.y = e.movementY || 0;
});
document.getElementById('chatInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const input = e.target;
const message = input.value.trim();
if (message) {
addChatMessage('human', message, gameState.localPlayer.name);
processAIChat(message);
input.value = '';
}
input.blur();
gameState.chatActive = false;
input.disabled = true;
}
});
// Update loading status
function updateLoading(status) {
document.getElementById('loadingStatus').textContent = status;
}
// Initialize game
async function init() {
updateLoading('Creating stars...');
createStars();
updateLoading('Creating Earth...');
await new Promise(resolve => setTimeout(resolve, 100));
createEarth();
updateLoading('Creating atmosphere...');
createAtmosphere();
updateLoading('Setting up lighting...');
setupLighting();
updateLoading('Spawning AI agents...');
spawnAIAgents();
// Create local player
const playerName = prompt('Enter your name:', 'Explorer') || 'Explorer';
gameState.localPlayer = new Player(Date.now(), playerName, false);
gameState.localPlayer.position.set(EARTH_RADIUS + 1000, 0, 0);
camera.position.copy(gameState.localPlayer.position);
camera.position.y += 50;
updatePlayerList();
addChatMessage('system', `Welcome to Lucidia.Earth, ${playerName}!`);
document.getElementById('loading').classList.add('hidden');
animate();
}
// Animation loop
let lastTime = Date.now();
function animate() {
requestAnimationFrame(animate);
const currentTime = Date.now();
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// Update local player
if (gameState.localPlayer) {
// Mouse look
gameState.rotation.y -= gameState.mouseMovement.x * 0.002;
gameState.rotation.x -= gameState.mouseMovement.y * 0.002;
gameState.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, gameState.rotation.x));
gameState.mouseMovement.x = 0;
gameState.mouseMovement.y = 0;
// Movement
const moveSpeed = PLAYER_SPEED;
const forward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(0, gameState.rotation.y, 0));
const right = new THREE.Vector3(1, 0, 0).applyEuler(new THREE.Euler(0, gameState.rotation.y, 0));
gameState.localPlayer.velocity.set(0, 0, 0);
if (gameState.movement.forward) gameState.localPlayer.velocity.add(forward.multiplyScalar(moveSpeed));
if (gameState.movement.backward) gameState.localPlayer.velocity.add(forward.multiplyScalar(-moveSpeed));
if (gameState.movement.left) gameState.localPlayer.velocity.add(right.multiplyScalar(-moveSpeed));
if (gameState.movement.right) gameState.localPlayer.velocity.add(right.multiplyScalar(moveSpeed));
if (gameState.movement.up) gameState.localPlayer.velocity.y += moveSpeed;
if (gameState.movement.down) gameState.localPlayer.velocity.y -= moveSpeed;
gameState.localPlayer.position.add(gameState.localPlayer.velocity.clone().multiplyScalar(deltaTime));
// Update camera
camera.position.copy(gameState.localPlayer.position);
camera.rotation.set(gameState.rotation.x, gameState.rotation.y, 0);
gameState.localPlayer.mesh.position.copy(gameState.localPlayer.position);
}
// Update AI agents
gameState.aiAgents.forEach(agent => {
agent.update(deltaTime);
});
updateHUD();
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start the game
init();
</script>
</body>
</html>