Initial commit: Lucidia.Earth open world game with AI agents and human players
This commit is contained in:
737
index.html
Normal file
737
index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user