Complete deployment of unified Light Trinity system: 🔴 RedLight: Template & brand system (18 HTML templates) 💚 GreenLight: Project & collaboration (14 layers, 103 templates) 💛 YellowLight: Infrastructure & deployment 🌈 Trinity: Unified compliance & testing Includes: - 12 documentation files - 8 shell scripts - 18 HTML brand templates - Trinity compliance workflow Built by: Cece + Alexa Date: December 23, 2025 Source: blackroad-os/blackroad-os-infra 🌸✨
1983 lines
73 KiB
HTML
1983 lines
73 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BlackRoad OS — Living World</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
overflow: hidden;
|
|
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
#canvas-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Crisp UI */
|
|
.ui {
|
|
position: fixed;
|
|
z-index: 100;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.ui > * {
|
|
pointer-events: auto;
|
|
}
|
|
|
|
/* Logo */
|
|
.logo {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 10px 18px;
|
|
border-radius: 40px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
z-index: 100;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.logo-icon::after {
|
|
content: '';
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #000;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.logo-text {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: #1a1a1a;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
/* Stats bar */
|
|
.stats-bar {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 4px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 8px 12px;
|
|
border-radius: 40px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
z-index: 100;
|
|
}
|
|
|
|
.stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
background: rgba(0,0,0,0.03);
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
/* Time */
|
|
.time-panel {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 12px 20px;
|
|
border-radius: 16px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
text-align: right;
|
|
z-index: 100;
|
|
}
|
|
|
|
.time-display {
|
|
font-size: 28px;
|
|
font-weight: 300;
|
|
color: #1a1a1a;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.weather-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-top: 2px;
|
|
font-size: 13px;
|
|
color: #666;
|
|
}
|
|
|
|
/* Controls */
|
|
.controls {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 6px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 8px;
|
|
border-radius: 40px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
z-index: 100;
|
|
}
|
|
|
|
.ctrl-btn {
|
|
width: 44px;
|
|
height: 44px;
|
|
border: none;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.ctrl-btn:hover {
|
|
background: rgba(0,0,0,0.05);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.ctrl-btn.active {
|
|
background: #FF1D6C;
|
|
box-shadow: 0 2px 10px rgba(255,29,108,0.3);
|
|
}
|
|
|
|
/* Minimap */
|
|
.minimap {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
width: 160px;
|
|
height: 160px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 16px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
overflow: hidden;
|
|
z-index: 100;
|
|
}
|
|
|
|
.minimap canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Compass */
|
|
.compass {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
width: 60px;
|
|
height: 60px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 50%;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
.compass-needle {
|
|
width: 4px;
|
|
height: 30px;
|
|
background: linear-gradient(to bottom, #FF1D6C 50%, #333 50%);
|
|
border-radius: 2px;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
/* Info panel */
|
|
.info-panel {
|
|
position: fixed;
|
|
bottom: 90px;
|
|
left: 20px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 16px 20px;
|
|
border-radius: 16px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(10px);
|
|
z-index: 100;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.info-title {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: #999;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 12px;
|
|
}
|
|
|
|
.info-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.info-value {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.info-label {
|
|
font-size: 9px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #999;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* Loading */
|
|
.loading {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #87CEEB 0%, #90EE90 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
transition: opacity 0.8s ease, visibility 0.8s ease;
|
|
}
|
|
|
|
.loading.hidden {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.loading-icon {
|
|
font-size: 64px;
|
|
animation: float 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-15px); }
|
|
}
|
|
|
|
.loading-text {
|
|
margin-top: 20px;
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
color: #2d5a27;
|
|
}
|
|
|
|
.loading-bar {
|
|
width: 200px;
|
|
height: 4px;
|
|
background: rgba(255,255,255,0.5);
|
|
border-radius: 2px;
|
|
margin-top: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.loading-progress {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #FF1D6C, #F5A623);
|
|
width: 0%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Loading -->
|
|
<div class="loading" id="loading">
|
|
<div class="loading-icon">🌍</div>
|
|
<div class="loading-text">Building your world...</div>
|
|
<div class="loading-bar">
|
|
<div class="loading-progress" id="loadingProgress"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Canvas -->
|
|
<div id="canvas-container"></div>
|
|
|
|
<!-- Logo -->
|
|
<div class="logo">
|
|
<div class="logo-icon"></div>
|
|
<span class="logo-text">BlackRoad World</span>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-bar">
|
|
<div class="stat">
|
|
<span class="stat-icon">🏠</span>
|
|
<span class="stat-value" id="houseCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🌳</span>
|
|
<span class="stat-value" id="treeCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🐰</span>
|
|
<span class="stat-value" id="animalCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🌸</span>
|
|
<span class="stat-value" id="flowerCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🤖</span>
|
|
<span class="stat-value" id="agentCount">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time -->
|
|
<div class="time-panel">
|
|
<div class="time-display" id="timeDisplay">12:00</div>
|
|
<div class="weather-row">
|
|
<span id="weatherIcon">☀️</span>
|
|
<span id="weatherTemp">72°F</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls">
|
|
<button class="ctrl-btn" id="btnDay" title="Day">☀️</button>
|
|
<button class="ctrl-btn" id="btnSunset" title="Sunset">🌅</button>
|
|
<button class="ctrl-btn" id="btnNight" title="Night">🌙</button>
|
|
<button class="ctrl-btn" id="btnRain" title="Rain">🌧️</button>
|
|
<button class="ctrl-btn" id="btnSnow" title="Snow">❄️</button>
|
|
<button class="ctrl-btn active" id="btnRotate" title="Auto Rotate">🔄</button>
|
|
<button class="ctrl-btn" id="btnFast" title="Speed">⚡</button>
|
|
</div>
|
|
|
|
<!-- Compass -->
|
|
<div class="compass">
|
|
<div class="compass-needle" id="compassNeedle"></div>
|
|
</div>
|
|
|
|
<!-- Info Panel -->
|
|
<div class="info-panel">
|
|
<div class="info-title">World Status</div>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-value" id="happiness">98%</div>
|
|
<div class="info-label">Happiness</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-value" id="nature">100%</div>
|
|
<div class="info-label">Nature</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-value" id="energy">95%</div>
|
|
<div class="info-label">Energy</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-value" id="magic">87%</div>
|
|
<div class="info-label">Magic</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Minimap -->
|
|
<div class="minimap">
|
|
<canvas id="minimapCanvas" width="160" height="160"></canvas>
|
|
</div>
|
|
|
|
<!-- Three.js -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
|
<script>
|
|
// ============ CONFIG ============
|
|
const CONFIG = {
|
|
worldRadius: 100,
|
|
continents: 7,
|
|
treesPerContinent: 80,
|
|
flowersPerContinent: 150,
|
|
housesPerContinent: 15,
|
|
animalsPerContinent: 25,
|
|
agentsPerContinent: 8,
|
|
birdsTotal: 60,
|
|
fishTotal: 80,
|
|
butterfliesTotal: 50,
|
|
cloudsTotal: 30,
|
|
boatsTotal: 12
|
|
};
|
|
|
|
// ============ COLORS ============
|
|
const COLORS = {
|
|
// Sky
|
|
dayTop: 0x4A90D9,
|
|
dayBottom: 0x87CEEB,
|
|
sunsetTop: 0xFF6B6B,
|
|
sunsetBottom: 0xFFD93D,
|
|
nightTop: 0x0B0B2B,
|
|
nightBottom: 0x1a1a3e,
|
|
|
|
// Terrain
|
|
grass: 0x4CAF50,
|
|
grassLight: 0x66BB6A,
|
|
grassDark: 0x388E3C,
|
|
sand: 0xF5DEB3,
|
|
dirt: 0x8B7355,
|
|
rock: 0x808080,
|
|
snow: 0xFFFAFA,
|
|
|
|
// Water
|
|
waterShallow: 0x40C4FF,
|
|
waterDeep: 0x0277BD,
|
|
waterFoam: 0xE1F5FE,
|
|
|
|
// Buildings
|
|
wallWhite: 0xFAFAFA,
|
|
wallCream: 0xFFF8E7,
|
|
wallPink: 0xFFE4E9,
|
|
wallBlue: 0xE3F2FD,
|
|
roofRed: 0xE53935,
|
|
roofBlue: 0x1976D2,
|
|
roofGreen: 0x43A047,
|
|
roofPink: 0xFF1D6C,
|
|
roofOrange: 0xF5A623,
|
|
wood: 0x6D4C41,
|
|
|
|
// Nature
|
|
treeTrunk: 0x5D4037,
|
|
leafGreen: 0x2E7D32,
|
|
leafLight: 0x81C784,
|
|
leafPine: 0x1B5E20,
|
|
leafCherry: 0xF8BBD9,
|
|
leafAutumn: 0xFF8A65,
|
|
|
|
// Accents
|
|
pink: 0xFF1D6C,
|
|
amber: 0xF5A623,
|
|
blue: 0x2979FF,
|
|
violet: 0x9C27B0
|
|
};
|
|
|
|
// ============ SCENE ============
|
|
let scene, camera, renderer;
|
|
let world, waterSphere, atmosphere;
|
|
let continents = [];
|
|
let trees = [], flowers = [], houses = [], animals = [], agents = [];
|
|
let birds = [], fish = [], butterflies = [], clouds = [], boats = [];
|
|
let rain = null, snow = null;
|
|
|
|
let time = 0;
|
|
let worldTime = 12;
|
|
let weather = 'sunny';
|
|
let timeOfDay = 'day';
|
|
let autoRotate = true;
|
|
let speedMultiplier = 1;
|
|
|
|
let isDragging = false;
|
|
let previousMouseX = 0, previousMouseY = 0;
|
|
let cameraTheta = 0;
|
|
let cameraPhi = Math.PI / 4;
|
|
let cameraDistance = 280;
|
|
|
|
// ============ INIT ============
|
|
function init() {
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
|
|
updateCamera();
|
|
|
|
// Renderer - HIGH QUALITY
|
|
renderer = new THREE.WebGLRenderer({
|
|
antialias: true,
|
|
powerPreference: "high-performance"
|
|
});
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
renderer.outputEncoding = THREE.sRGBEncoding;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.2;
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
// Build world
|
|
createSky();
|
|
createLights();
|
|
createPlanet();
|
|
createWater();
|
|
createContinents();
|
|
createClouds();
|
|
createBirds();
|
|
createFish();
|
|
createBoats();
|
|
createWeatherSystems();
|
|
|
|
// Events
|
|
window.addEventListener('resize', onResize);
|
|
renderer.domElement.addEventListener('mousedown', onMouseDown);
|
|
renderer.domElement.addEventListener('mousemove', onMouseMove);
|
|
renderer.domElement.addEventListener('mouseup', onMouseUp);
|
|
renderer.domElement.addEventListener('wheel', onWheel);
|
|
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
renderer.domElement.addEventListener('touchend', onTouchEnd);
|
|
|
|
// Controls
|
|
document.getElementById('btnDay').addEventListener('click', () => setTimeOfDay('day'));
|
|
document.getElementById('btnSunset').addEventListener('click', () => setTimeOfDay('sunset'));
|
|
document.getElementById('btnNight').addEventListener('click', () => setTimeOfDay('night'));
|
|
document.getElementById('btnRain').addEventListener('click', () => toggleWeather('rain'));
|
|
document.getElementById('btnSnow').addEventListener('click', () => toggleWeather('snow'));
|
|
document.getElementById('btnRotate').addEventListener('click', toggleRotate);
|
|
document.getElementById('btnFast').addEventListener('click', toggleSpeed);
|
|
|
|
// Start
|
|
simulateLoading();
|
|
}
|
|
|
|
function simulateLoading() {
|
|
let progress = 0;
|
|
const progressBar = document.getElementById('loadingProgress');
|
|
|
|
const interval = setInterval(() => {
|
|
progress += Math.random() * 15;
|
|
if (progress >= 100) {
|
|
progress = 100;
|
|
clearInterval(interval);
|
|
setTimeout(() => {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
animate();
|
|
}, 300);
|
|
}
|
|
progressBar.style.width = progress + '%';
|
|
}, 100);
|
|
}
|
|
|
|
// ============ SKY ============
|
|
function createSky() {
|
|
const skyGeo = new THREE.SphereGeometry(800, 32, 32);
|
|
const skyMat = new THREE.ShaderMaterial({
|
|
uniforms: {
|
|
topColor: { value: new THREE.Color(COLORS.dayTop) },
|
|
bottomColor: { value: new THREE.Color(COLORS.dayBottom) },
|
|
offset: { value: 20 },
|
|
exponent: { value: 0.6 }
|
|
},
|
|
vertexShader: `
|
|
varying vec3 vWorldPosition;
|
|
void main() {
|
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
vWorldPosition = worldPosition.xyz;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform vec3 topColor;
|
|
uniform vec3 bottomColor;
|
|
uniform float offset;
|
|
uniform float exponent;
|
|
varying vec3 vWorldPosition;
|
|
void main() {
|
|
float h = normalize(vWorldPosition + offset).y;
|
|
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
|
|
}
|
|
`,
|
|
side: THREE.BackSide
|
|
});
|
|
|
|
const sky = new THREE.Mesh(skyGeo, skyMat);
|
|
scene.add(sky);
|
|
window.skyMaterial = skyMat;
|
|
}
|
|
|
|
// ============ LIGHTS ============
|
|
function createLights() {
|
|
// Ambient
|
|
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
|
|
scene.add(ambient);
|
|
window.ambientLight = ambient;
|
|
|
|
// Sun
|
|
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
sun.position.set(200, 300, 200);
|
|
sun.castShadow = true;
|
|
sun.shadow.mapSize.width = 4096;
|
|
sun.shadow.mapSize.height = 4096;
|
|
sun.shadow.camera.near = 100;
|
|
sun.shadow.camera.far = 800;
|
|
sun.shadow.camera.left = -200;
|
|
sun.shadow.camera.right = 200;
|
|
sun.shadow.camera.top = 200;
|
|
sun.shadow.camera.bottom = -200;
|
|
sun.shadow.bias = -0.0005;
|
|
scene.add(sun);
|
|
window.sunLight = sun;
|
|
|
|
// Hemisphere
|
|
const hemi = new THREE.HemisphereLight(0x87CEEB, 0x4CAF50, 0.3);
|
|
scene.add(hemi);
|
|
window.hemiLight = hemi;
|
|
|
|
// Sun sphere (visible sun)
|
|
const sunGeo = new THREE.SphereGeometry(15, 32, 32);
|
|
const sunMat = new THREE.MeshBasicMaterial({ color: 0xFFFF00 });
|
|
const sunMesh = new THREE.Mesh(sunGeo, sunMat);
|
|
sunMesh.position.copy(sun.position);
|
|
scene.add(sunMesh);
|
|
window.sunMesh = sunMesh;
|
|
}
|
|
|
|
// ============ PLANET ============
|
|
function createPlanet() {
|
|
// Core sphere (land base)
|
|
const planetGeo = new THREE.SphereGeometry(CONFIG.worldRadius, 128, 128);
|
|
const planetMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.grass,
|
|
roughness: 0.9,
|
|
metalness: 0.0,
|
|
flatShading: false
|
|
});
|
|
|
|
// Add terrain variation
|
|
const positions = planetGeo.attributes.position.array;
|
|
for (let i = 0; i < positions.length; i += 3) {
|
|
const x = positions[i];
|
|
const y = positions[i + 1];
|
|
const z = positions[i + 2];
|
|
|
|
// Normalize to get direction
|
|
const len = Math.sqrt(x*x + y*y + z*z);
|
|
const nx = x/len, ny = y/len, nz = z/len;
|
|
|
|
// Add noise-based height
|
|
const noise =
|
|
Math.sin(nx * 8 + ny * 5) * 0.5 +
|
|
Math.sin(ny * 12 + nz * 7) * 0.3 +
|
|
Math.sin(nz * 6 + nx * 9) * 0.4;
|
|
|
|
const height = CONFIG.worldRadius + noise * 3;
|
|
|
|
positions[i] = nx * height;
|
|
positions[i + 1] = ny * height;
|
|
positions[i + 2] = nz * height;
|
|
}
|
|
|
|
planetGeo.computeVertexNormals();
|
|
|
|
world = new THREE.Mesh(planetGeo, planetMat);
|
|
world.receiveShadow = true;
|
|
world.castShadow = true;
|
|
scene.add(world);
|
|
}
|
|
|
|
// ============ WATER ============
|
|
function createWater() {
|
|
const waterGeo = new THREE.SphereGeometry(CONFIG.worldRadius - 1, 128, 128);
|
|
const waterMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.waterShallow,
|
|
transparent: true,
|
|
opacity: 0.85,
|
|
roughness: 0.1,
|
|
metalness: 0.2
|
|
});
|
|
|
|
waterSphere = new THREE.Mesh(waterGeo, waterMat);
|
|
scene.add(waterSphere);
|
|
}
|
|
|
|
// ============ CONTINENTS ============
|
|
function createContinents() {
|
|
// Define continent positions on the sphere
|
|
const continentData = [
|
|
{ name: "Lucidia Prime", lat: 45, lng: 0, size: 1.2 },
|
|
{ name: "Memory Shores", lat: 30, lng: 90, size: 1.0 },
|
|
{ name: "Agent Valley", lat: -20, lng: 45, size: 0.9 },
|
|
{ name: "Quantum Isles", lat: 60, lng: -60, size: 0.8 },
|
|
{ name: "Creativity Coast", lat: -45, lng: -90, size: 1.1 },
|
|
{ name: "Ethics Haven", lat: 10, lng: 180, size: 0.85 },
|
|
{ name: "Unity Land", lat: -60, lng: 120, size: 0.95 }
|
|
];
|
|
|
|
continentData.forEach((data, index) => {
|
|
const continent = createContinent(data, index);
|
|
continents.push(continent);
|
|
});
|
|
|
|
updateCounts();
|
|
}
|
|
|
|
function createContinent(data, index) {
|
|
const group = new THREE.Group();
|
|
|
|
// Convert lat/lng to position on sphere
|
|
const phi = (90 - data.lat) * Math.PI / 180;
|
|
const theta = (data.lng + 180) * Math.PI / 180;
|
|
|
|
const baseX = CONFIG.worldRadius * Math.sin(phi) * Math.cos(theta);
|
|
const baseY = CONFIG.worldRadius * Math.cos(phi);
|
|
const baseZ = CONFIG.worldRadius * Math.sin(phi) * Math.sin(theta);
|
|
|
|
const centerPos = new THREE.Vector3(baseX, baseY, baseZ);
|
|
const normal = centerPos.clone().normalize();
|
|
|
|
// Create continent landmass
|
|
const landGeo = new THREE.CircleGeometry(25 * data.size, 32);
|
|
const landMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.grass,
|
|
roughness: 0.85,
|
|
side: THREE.DoubleSide
|
|
});
|
|
const land = new THREE.Mesh(landGeo, landMat);
|
|
land.position.copy(centerPos.clone().multiplyScalar(1.01));
|
|
land.lookAt(centerPos.clone().multiplyScalar(2));
|
|
land.receiveShadow = true;
|
|
group.add(land);
|
|
|
|
// Beach ring
|
|
const beachGeo = new THREE.RingGeometry(23 * data.size, 27 * data.size, 32);
|
|
const beachMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.sand,
|
|
roughness: 0.9,
|
|
side: THREE.DoubleSide
|
|
});
|
|
const beach = new THREE.Mesh(beachGeo, beachMat);
|
|
beach.position.copy(centerPos.clone().multiplyScalar(1.008));
|
|
beach.lookAt(centerPos.clone().multiplyScalar(2));
|
|
group.add(beach);
|
|
|
|
// Add trees
|
|
for (let i = 0; i < CONFIG.treesPerContinent * data.size; i++) {
|
|
const tree = createTree();
|
|
const pos = getRandomPositionOnContinent(centerPos, normal, 20 * data.size);
|
|
tree.position.copy(pos);
|
|
tree.lookAt(pos.clone().multiplyScalar(2));
|
|
tree.rotateX(Math.PI / 2);
|
|
tree.scale.setScalar(0.6 + Math.random() * 0.5);
|
|
trees.push(tree);
|
|
group.add(tree);
|
|
}
|
|
|
|
// Add flowers
|
|
for (let i = 0; i < CONFIG.flowersPerContinent * data.size; i++) {
|
|
const flower = createFlower();
|
|
const pos = getRandomPositionOnContinent(centerPos, normal, 22 * data.size);
|
|
flower.position.copy(pos);
|
|
flower.lookAt(pos.clone().multiplyScalar(2));
|
|
flower.rotateX(Math.PI / 2);
|
|
flower.scale.setScalar(0.3 + Math.random() * 0.3);
|
|
flowers.push(flower);
|
|
group.add(flower);
|
|
}
|
|
|
|
// Add houses
|
|
for (let i = 0; i < CONFIG.housesPerContinent * data.size; i++) {
|
|
const house = createHouse();
|
|
const pos = getRandomPositionOnContinent(centerPos, normal, 18 * data.size);
|
|
house.position.copy(pos);
|
|
house.lookAt(pos.clone().multiplyScalar(2));
|
|
house.rotateX(Math.PI / 2);
|
|
house.rotateZ(Math.random() * Math.PI * 2);
|
|
house.scale.setScalar(0.5 + Math.random() * 0.3);
|
|
houses.push(house);
|
|
group.add(house);
|
|
}
|
|
|
|
// Add animals
|
|
for (let i = 0; i < CONFIG.animalsPerContinent * data.size; i++) {
|
|
const animal = createAnimal();
|
|
const pos = getRandomPositionOnContinent(centerPos, normal, 20 * data.size);
|
|
animal.position.copy(pos);
|
|
animal.userData.continentCenter = centerPos.clone();
|
|
animal.userData.continentNormal = normal.clone();
|
|
animal.userData.continentSize = data.size;
|
|
animals.push(animal);
|
|
group.add(animal);
|
|
}
|
|
|
|
// Add agents
|
|
for (let i = 0; i < CONFIG.agentsPerContinent * data.size; i++) {
|
|
const agent = createAgent(index * 10 + i);
|
|
const pos = getRandomPositionOnContinent(centerPos, normal, 15 * data.size);
|
|
agent.position.copy(pos.clone().multiplyScalar(1.05));
|
|
agent.userData.continentCenter = centerPos.clone();
|
|
agent.userData.continentNormal = normal.clone();
|
|
agents.push(agent);
|
|
group.add(agent);
|
|
}
|
|
|
|
// Add butterflies
|
|
for (let i = 0; i < 8 * data.size; i++) {
|
|
const butterfly = createButterfly();
|
|
const pos = getRandomPositionOnContinent(centerPos, normal, 22 * data.size);
|
|
butterfly.position.copy(pos.clone().multiplyScalar(1.08));
|
|
butterfly.userData.continentCenter = centerPos.clone();
|
|
butterfly.userData.continentNormal = normal.clone();
|
|
butterflies.push(butterfly);
|
|
group.add(butterfly);
|
|
}
|
|
|
|
scene.add(group);
|
|
return { group, data, centerPos, normal };
|
|
}
|
|
|
|
function getRandomPositionOnContinent(center, normal, maxDist) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = Math.random() * maxDist;
|
|
|
|
// Create local coordinate system
|
|
const tangent = new THREE.Vector3(1, 0, 0);
|
|
if (Math.abs(normal.y) < 0.9) {
|
|
tangent.crossVectors(normal, new THREE.Vector3(0, 1, 0)).normalize();
|
|
} else {
|
|
tangent.crossVectors(normal, new THREE.Vector3(1, 0, 0)).normalize();
|
|
}
|
|
const bitangent = new THREE.Vector3().crossVectors(normal, tangent);
|
|
|
|
const offset = tangent.clone().multiplyScalar(Math.cos(angle) * dist)
|
|
.add(bitangent.clone().multiplyScalar(Math.sin(angle) * dist));
|
|
|
|
const pos = center.clone().add(offset);
|
|
return pos.normalize().multiplyScalar(CONFIG.worldRadius + 1);
|
|
}
|
|
|
|
// ============ TREES ============
|
|
function createTree() {
|
|
const group = new THREE.Group();
|
|
const type = Math.floor(Math.random() * 4);
|
|
|
|
// Trunk
|
|
const trunkGeo = new THREE.CylinderGeometry(0.2, 0.35, 2, 8);
|
|
const trunkMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.treeTrunk,
|
|
roughness: 0.9
|
|
});
|
|
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
|
|
trunk.position.y = 1;
|
|
trunk.castShadow = true;
|
|
group.add(trunk);
|
|
|
|
if (type === 0) {
|
|
// Pine
|
|
for (let i = 0; i < 4; i++) {
|
|
const size = 1.8 - i * 0.35;
|
|
const coneGeo = new THREE.ConeGeometry(size, 1.8, 8);
|
|
const coneMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.leafPine,
|
|
flatShading: true
|
|
});
|
|
const cone = new THREE.Mesh(coneGeo, coneMat);
|
|
cone.position.y = 2.5 + i * 1.2;
|
|
cone.castShadow = true;
|
|
group.add(cone);
|
|
}
|
|
} else if (type === 1) {
|
|
// Oak
|
|
const leavesGeo = new THREE.IcosahedronGeometry(2, 1);
|
|
const leavesMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.leafGreen,
|
|
flatShading: true
|
|
});
|
|
const leaves = new THREE.Mesh(leavesGeo, leavesMat);
|
|
leaves.position.y = 3.5;
|
|
leaves.scale.set(1.2, 1, 1.2);
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
} else if (type === 2) {
|
|
// Cherry blossom
|
|
const leavesGeo = new THREE.IcosahedronGeometry(1.8, 1);
|
|
const leavesMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.leafCherry,
|
|
flatShading: true
|
|
});
|
|
const leaves = new THREE.Mesh(leavesGeo, leavesMat);
|
|
leaves.position.y = 3.2;
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
} else {
|
|
// Autumn tree
|
|
const leavesGeo = new THREE.IcosahedronGeometry(1.6, 1);
|
|
const leavesMat = new THREE.MeshStandardMaterial({
|
|
color: COLORS.leafAutumn,
|
|
flatShading: true
|
|
});
|
|
const leaves = new THREE.Mesh(leavesGeo, leavesMat);
|
|
leaves.position.y = 3.3;
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
}
|
|
|
|
group.userData = { type: 'tree', swayPhase: Math.random() * Math.PI * 2 };
|
|
return group;
|
|
}
|
|
|
|
// ============ FLOWERS ============
|
|
function createFlower() {
|
|
const group = new THREE.Group();
|
|
const colors = [0xFF69B4, 0xFFD700, 0xFF6347, 0x9370DB, 0x00CED1, 0xFFFFFF, 0xFF1D6C, 0xF5A623];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
// Stem
|
|
const stemGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.6, 6);
|
|
const stemMat = new THREE.MeshStandardMaterial({ color: 0x228B22 });
|
|
const stem = new THREE.Mesh(stemGeo, stemMat);
|
|
stem.position.y = 0.3;
|
|
group.add(stem);
|
|
|
|
// Petals
|
|
const petalGeo = new THREE.SphereGeometry(0.15, 8, 8);
|
|
const petalMat = new THREE.MeshStandardMaterial({ color });
|
|
for (let i = 0; i < 6; i++) {
|
|
const petal = new THREE.Mesh(petalGeo, petalMat);
|
|
petal.scale.set(1, 0.3, 0.6);
|
|
const angle = (i / 6) * Math.PI * 2;
|
|
petal.position.set(Math.cos(angle) * 0.15, 0.6, Math.sin(angle) * 0.15);
|
|
petal.rotation.y = angle;
|
|
petal.rotation.z = 0.4;
|
|
group.add(petal);
|
|
}
|
|
|
|
// Center
|
|
const centerGeo = new THREE.SphereGeometry(0.08, 8, 8);
|
|
const centerMat = new THREE.MeshStandardMaterial({ color: 0xFFD700 });
|
|
const center = new THREE.Mesh(centerGeo, centerMat);
|
|
center.position.y = 0.6;
|
|
group.add(center);
|
|
|
|
group.userData = { type: 'flower', swayPhase: Math.random() * Math.PI * 2 };
|
|
return group;
|
|
}
|
|
|
|
// ============ HOUSES ============
|
|
function createHouse() {
|
|
const group = new THREE.Group();
|
|
|
|
const wallColors = [COLORS.wallWhite, COLORS.wallCream, COLORS.wallPink, COLORS.wallBlue];
|
|
const roofColors = [COLORS.roofRed, COLORS.roofBlue, COLORS.roofGreen, COLORS.roofPink, COLORS.roofOrange];
|
|
|
|
const wallColor = wallColors[Math.floor(Math.random() * wallColors.length)];
|
|
const roofColor = roofColors[Math.floor(Math.random() * roofColors.length)];
|
|
|
|
// Walls
|
|
const wallGeo = new THREE.BoxGeometry(2.5, 2, 2.5);
|
|
const wallMat = new THREE.MeshStandardMaterial({ color: wallColor, roughness: 0.8 });
|
|
const walls = new THREE.Mesh(wallGeo, wallMat);
|
|
walls.position.y = 1;
|
|
walls.castShadow = true;
|
|
walls.receiveShadow = true;
|
|
group.add(walls);
|
|
|
|
// Roof
|
|
const roofGeo = new THREE.ConeGeometry(2.2, 1.5, 4);
|
|
const roofMat = new THREE.MeshStandardMaterial({ color: roofColor, roughness: 0.7 });
|
|
const roof = new THREE.Mesh(roofGeo, roofMat);
|
|
roof.position.y = 2.7;
|
|
roof.rotation.y = Math.PI / 4;
|
|
roof.castShadow = true;
|
|
group.add(roof);
|
|
|
|
// Door
|
|
const doorGeo = new THREE.BoxGeometry(0.5, 1, 0.1);
|
|
const doorMat = new THREE.MeshStandardMaterial({ color: COLORS.wood });
|
|
const door = new THREE.Mesh(doorGeo, doorMat);
|
|
door.position.set(0, 0.5, 1.3);
|
|
group.add(door);
|
|
|
|
// Windows
|
|
const winGeo = new THREE.BoxGeometry(0.4, 0.4, 0.1);
|
|
const winMat = new THREE.MeshStandardMaterial({
|
|
color: 0x87CEEB,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
emissive: 0x444400,
|
|
emissiveIntensity: 0
|
|
});
|
|
|
|
[[-0.7, 1.3], [0.7, 1.3]].forEach(([x, y]) => {
|
|
const win = new THREE.Mesh(winGeo, winMat.clone());
|
|
win.position.set(x, y, 1.3);
|
|
group.add(win);
|
|
});
|
|
|
|
// Chimney
|
|
const chimGeo = new THREE.BoxGeometry(0.4, 1, 0.4);
|
|
const chimMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
|
|
const chimney = new THREE.Mesh(chimGeo, chimMat);
|
|
chimney.position.set(0.8, 3.2, 0);
|
|
chimney.castShadow = true;
|
|
group.add(chimney);
|
|
|
|
group.userData = { type: 'house', windows: group.children.filter(c => c.material?.emissive) };
|
|
return group;
|
|
}
|
|
|
|
// ============ ANIMALS ============
|
|
function createAnimal() {
|
|
const group = new THREE.Group();
|
|
const types = [
|
|
{ color: 0xFFFFFF, name: 'bunny' },
|
|
{ color: 0x8B4513, name: 'squirrel' },
|
|
{ color: 0xFF6600, name: 'fox' },
|
|
{ color: 0xFFB6C1, name: 'pig' },
|
|
{ color: 0xD2B48C, name: 'dog' },
|
|
{ color: 0x808080, name: 'cat' }
|
|
];
|
|
const type = types[Math.floor(Math.random() * types.length)];
|
|
|
|
const bodyMat = new THREE.MeshStandardMaterial({ color: type.color, roughness: 0.8 });
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.SphereGeometry(0.35, 12, 12);
|
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|
body.scale.set(1, 0.8, 1.2);
|
|
group.add(body);
|
|
|
|
// Head
|
|
const headGeo = new THREE.SphereGeometry(0.25, 12, 12);
|
|
const head = new THREE.Mesh(headGeo, bodyMat);
|
|
head.position.set(0, 0.2, 0.35);
|
|
group.add(head);
|
|
|
|
// Eyes
|
|
const eyeGeo = new THREE.SphereGeometry(0.05, 8, 8);
|
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
[[-0.08, 0.28, 0.55], [0.08, 0.28, 0.55]].forEach(([x, y, z]) => {
|
|
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
|
eye.position.set(x, y, z);
|
|
group.add(eye);
|
|
});
|
|
|
|
// Nose
|
|
const noseGeo = new THREE.SphereGeometry(0.03, 6, 6);
|
|
const noseMat = new THREE.MeshBasicMaterial({ color: 0xFF69B4 });
|
|
const nose = new THREE.Mesh(noseGeo, noseMat);
|
|
nose.position.set(0, 0.2, 0.6);
|
|
group.add(nose);
|
|
|
|
// Ears
|
|
const earGeo = new THREE.ConeGeometry(0.06, 0.2, 6);
|
|
[[-0.1, 0.45, 0.3], [0.1, 0.45, 0.3]].forEach(([x, y, z]) => {
|
|
const ear = new THREE.Mesh(earGeo, bodyMat);
|
|
ear.position.set(x, y, z);
|
|
group.add(ear);
|
|
});
|
|
|
|
// Legs
|
|
const legGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.2, 6);
|
|
[[-0.15, -0.25, -0.2], [0.15, -0.25, -0.2], [-0.15, -0.25, 0.2], [0.15, -0.25, 0.2]].forEach(([x, y, z]) => {
|
|
const leg = new THREE.Mesh(legGeo, bodyMat);
|
|
leg.position.set(x, y, z);
|
|
group.add(leg);
|
|
});
|
|
|
|
group.userData = {
|
|
type: 'animal',
|
|
animalType: type,
|
|
speed: 0.01 + Math.random() * 0.015,
|
|
hopPhase: Math.random() * Math.PI * 2,
|
|
wanderAngle: Math.random() * Math.PI * 2,
|
|
idleTime: 0
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ AGENTS ============
|
|
function createAgent(index) {
|
|
const group = new THREE.Group();
|
|
const colors = [COLORS.pink, COLORS.blue, COLORS.amber, COLORS.violet];
|
|
const color = colors[index % colors.length];
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.CylinderGeometry(0.25, 0.35, 0.7, 12);
|
|
const bodyMat = new THREE.MeshStandardMaterial({
|
|
color,
|
|
metalness: 0.5,
|
|
roughness: 0.3
|
|
});
|
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|
body.position.y = 0.35;
|
|
group.add(body);
|
|
|
|
// Head
|
|
const headGeo = new THREE.SphereGeometry(0.28, 16, 16);
|
|
const headMat = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
metalness: 0.3,
|
|
roughness: 0.5
|
|
});
|
|
const head = new THREE.Mesh(headGeo, headMat);
|
|
head.position.y = 0.95;
|
|
group.add(head);
|
|
|
|
// Eye (BlackRoad style)
|
|
const eyeGeo = new THREE.CircleGeometry(0.15, 32);
|
|
const eyeMat = new THREE.MeshBasicMaterial({ color: COLORS.pink });
|
|
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
|
eye.position.set(0, 1, 0.25);
|
|
group.add(eye);
|
|
|
|
// Pupil
|
|
const pupilGeo = new THREE.CircleGeometry(0.06, 16);
|
|
const pupilMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
const pupil = new THREE.Mesh(pupilGeo, pupilMat);
|
|
pupil.position.set(0, 1, 0.26);
|
|
group.add(pupil);
|
|
|
|
// Antenna
|
|
const antGeo = new THREE.CylinderGeometry(0.015, 0.015, 0.2, 6);
|
|
const antMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
|
const antenna = new THREE.Mesh(antGeo, antMat);
|
|
antenna.position.set(0, 1.3, 0);
|
|
group.add(antenna);
|
|
|
|
// Antenna ball
|
|
const ballGeo = new THREE.SphereGeometry(0.05, 8, 8);
|
|
const ballMat = new THREE.MeshBasicMaterial({ color });
|
|
const ball = new THREE.Mesh(ballGeo, ballMat);
|
|
ball.position.set(0, 1.45, 0);
|
|
group.add(ball);
|
|
|
|
// Hover ring
|
|
const ringGeo = new THREE.TorusGeometry(0.35, 0.03, 8, 32);
|
|
const ringMat = new THREE.MeshBasicMaterial({
|
|
color,
|
|
transparent: true,
|
|
opacity: 0.5
|
|
});
|
|
const ring = new THREE.Mesh(ringGeo, ringMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = -0.05;
|
|
group.add(ring);
|
|
|
|
group.userData = {
|
|
type: 'agent',
|
|
hoverPhase: Math.random() * Math.PI * 2,
|
|
speed: 0.008 + Math.random() * 0.008,
|
|
wanderAngle: Math.random() * Math.PI * 2
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ BIRDS ============
|
|
function createBirds() {
|
|
for (let i = 0; i < CONFIG.birdsTotal; i++) {
|
|
const bird = createBird();
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const height = CONFIG.worldRadius + 20 + Math.random() * 40;
|
|
bird.position.set(
|
|
Math.cos(angle) * height * 0.8,
|
|
Math.sin(Math.random() * Math.PI - Math.PI/2) * height * 0.5,
|
|
Math.sin(angle) * height * 0.8
|
|
);
|
|
birds.push(bird);
|
|
scene.add(bird);
|
|
}
|
|
}
|
|
|
|
function createBird() {
|
|
const group = new THREE.Group();
|
|
const colors = [0x4169E1, 0xFF6347, 0xFFD700, 0x32CD32, 0xFF69B4];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
const mat = new THREE.MeshStandardMaterial({ color });
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.SphereGeometry(0.2, 8, 8);
|
|
const body = new THREE.Mesh(bodyGeo, mat);
|
|
body.scale.set(1, 0.7, 1.3);
|
|
group.add(body);
|
|
|
|
// Wings
|
|
const wingGeo = new THREE.BoxGeometry(1, 0.05, 0.3);
|
|
const wings = new THREE.Mesh(wingGeo, mat);
|
|
wings.position.y = 0.05;
|
|
wings.userData = { flapPhase: Math.random() * Math.PI * 2 };
|
|
group.add(wings);
|
|
|
|
// Beak
|
|
const beakGeo = new THREE.ConeGeometry(0.05, 0.15, 6);
|
|
const beakMat = new THREE.MeshStandardMaterial({ color: 0xFFA500 });
|
|
const beak = new THREE.Mesh(beakGeo, beakMat);
|
|
beak.position.set(0, 0, 0.25);
|
|
beak.rotation.x = Math.PI / 2;
|
|
group.add(beak);
|
|
|
|
group.userData = {
|
|
type: 'bird',
|
|
wings,
|
|
orbitAngle: Math.random() * Math.PI * 2,
|
|
orbitRadius: CONFIG.worldRadius + 25 + Math.random() * 30,
|
|
orbitSpeed: 0.003 + Math.random() * 0.004,
|
|
orbitTilt: (Math.random() - 0.5) * Math.PI * 0.5,
|
|
flapSpeed: 0.2 + Math.random() * 0.15
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ FISH ============
|
|
function createFish() {
|
|
for (let i = 0; i < CONFIG.fishTotal; i++) {
|
|
const fishMesh = createFish3D();
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const depth = CONFIG.worldRadius - 5 - Math.random() * 10;
|
|
const tilt = (Math.random() - 0.5) * Math.PI * 0.6;
|
|
fishMesh.position.set(
|
|
Math.cos(angle) * Math.cos(tilt) * depth,
|
|
Math.sin(tilt) * depth,
|
|
Math.sin(angle) * Math.cos(tilt) * depth
|
|
);
|
|
fish.push(fishMesh);
|
|
scene.add(fishMesh);
|
|
}
|
|
}
|
|
|
|
function createFish3D() {
|
|
const group = new THREE.Group();
|
|
const colors = [0xFF6347, 0xFFD700, 0x00CED1, 0xFF69B4, 0x9370DB, 0x32CD32];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
const mat = new THREE.MeshStandardMaterial({ color });
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.SphereGeometry(0.25, 10, 10);
|
|
const body = new THREE.Mesh(bodyGeo, mat);
|
|
body.scale.set(1, 0.6, 1.4);
|
|
group.add(body);
|
|
|
|
// Tail
|
|
const tailGeo = new THREE.ConeGeometry(0.2, 0.35, 4);
|
|
const tail = new THREE.Mesh(tailGeo, mat);
|
|
tail.position.z = -0.35;
|
|
tail.rotation.x = Math.PI / 2;
|
|
tail.rotation.z = Math.PI / 4;
|
|
group.add(tail);
|
|
|
|
// Eye
|
|
const eyeGeo = new THREE.SphereGeometry(0.04, 6, 6);
|
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
|
eye.position.set(0.1, 0.05, 0.2);
|
|
group.add(eye);
|
|
|
|
group.userData = {
|
|
type: 'fish',
|
|
swimAngle: Math.random() * Math.PI * 2,
|
|
swimSpeed: 0.005 + Math.random() * 0.008,
|
|
swimTilt: (Math.random() - 0.5) * 0.5,
|
|
wobblePhase: Math.random() * Math.PI * 2
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ CLOUDS ============
|
|
function createClouds() {
|
|
for (let i = 0; i < CONFIG.cloudsTotal; i++) {
|
|
const cloud = createCloud();
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const tilt = (Math.random() - 0.5) * Math.PI * 0.4;
|
|
const height = CONFIG.worldRadius + 40 + Math.random() * 30;
|
|
cloud.position.set(
|
|
Math.cos(angle) * Math.cos(tilt) * height,
|
|
Math.sin(tilt) * height + 20,
|
|
Math.sin(angle) * Math.cos(tilt) * height
|
|
);
|
|
cloud.lookAt(0, 0, 0);
|
|
clouds.push(cloud);
|
|
scene.add(cloud);
|
|
}
|
|
}
|
|
|
|
function createCloud() {
|
|
const group = new THREE.Group();
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
roughness: 1
|
|
});
|
|
|
|
const puffs = [
|
|
[0, 0, 0, 2.5],
|
|
[-2, 0.3, 0, 2],
|
|
[2, 0.2, 0, 2],
|
|
[0, 1, 0, 1.8],
|
|
[-1.2, 0.8, 0.5, 1.5],
|
|
[1.2, 0.7, -0.5, 1.5]
|
|
];
|
|
|
|
puffs.forEach(([x, y, z, r]) => {
|
|
const puffGeo = new THREE.SphereGeometry(r, 10, 10);
|
|
const puff = new THREE.Mesh(puffGeo, mat);
|
|
puff.position.set(x, y, z);
|
|
group.add(puff);
|
|
});
|
|
|
|
group.userData = {
|
|
type: 'cloud',
|
|
orbitAngle: Math.random() * Math.PI * 2,
|
|
orbitSpeed: 0.0003 + Math.random() * 0.0003,
|
|
bobPhase: Math.random() * Math.PI * 2
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ BOATS ============
|
|
function createBoats() {
|
|
for (let i = 0; i < CONFIG.boatsTotal; i++) {
|
|
const boat = createBoat();
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = CONFIG.worldRadius + 2;
|
|
boat.position.set(
|
|
Math.cos(angle) * dist,
|
|
0,
|
|
Math.sin(angle) * dist
|
|
);
|
|
boat.lookAt(0, 0, 0);
|
|
boat.rotateY(Math.PI / 2);
|
|
boats.push(boat);
|
|
scene.add(boat);
|
|
}
|
|
}
|
|
|
|
function createBoat() {
|
|
const group = new THREE.Group();
|
|
|
|
// Hull
|
|
const hullGeo = new THREE.BoxGeometry(1.5, 0.4, 0.6);
|
|
const hullMat = new THREE.MeshStandardMaterial({ color: COLORS.wood });
|
|
const hull = new THREE.Mesh(hullGeo, hullMat);
|
|
group.add(hull);
|
|
|
|
// Sail
|
|
const sailGeo = new THREE.PlaneGeometry(0.8, 1.2);
|
|
const sailMat = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
side: THREE.DoubleSide
|
|
});
|
|
const sail = new THREE.Mesh(sailGeo, sailMat);
|
|
sail.position.set(0, 0.8, 0);
|
|
sail.rotation.y = Math.PI / 4;
|
|
group.add(sail);
|
|
|
|
// Mast
|
|
const mastGeo = new THREE.CylinderGeometry(0.03, 0.03, 1.4, 6);
|
|
const mastMat = new THREE.MeshStandardMaterial({ color: 0x4a3728 });
|
|
const mast = new THREE.Mesh(mastGeo, mastMat);
|
|
mast.position.y = 0.7;
|
|
group.add(mast);
|
|
|
|
group.userData = {
|
|
type: 'boat',
|
|
orbitAngle: Math.random() * Math.PI * 2,
|
|
orbitSpeed: 0.001 + Math.random() * 0.001,
|
|
bobPhase: Math.random() * Math.PI * 2
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ BUTTERFLIES ============
|
|
function createButterfly() {
|
|
const group = new THREE.Group();
|
|
const colors = [0xFF69B4, 0x9370DB, 0xFFD700, 0x00CED1, 0xFF6347];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
opacity: 0.85
|
|
});
|
|
|
|
// Wings
|
|
const wingGeo = new THREE.CircleGeometry(0.2, 8);
|
|
const leftWing = new THREE.Mesh(wingGeo, mat);
|
|
leftWing.position.x = -0.12;
|
|
leftWing.rotation.y = -0.4;
|
|
group.add(leftWing);
|
|
|
|
const rightWing = new THREE.Mesh(wingGeo, mat);
|
|
rightWing.position.x = 0.12;
|
|
rightWing.rotation.y = 0.4;
|
|
group.add(rightWing);
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.25, 6);
|
|
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|
body.rotation.x = Math.PI / 2;
|
|
group.add(body);
|
|
|
|
group.userData = {
|
|
type: 'butterfly',
|
|
leftWing,
|
|
rightWing,
|
|
flapPhase: Math.random() * Math.PI * 2,
|
|
wanderAngle: Math.random() * Math.PI * 2,
|
|
speed: 0.015 + Math.random() * 0.01
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
// ============ WEATHER ============
|
|
function createWeatherSystems() {
|
|
// Rain
|
|
const rainCount = 10000;
|
|
const rainGeo = new THREE.BufferGeometry();
|
|
const rainPos = new Float32Array(rainCount * 3);
|
|
const rainVel = new Float32Array(rainCount);
|
|
|
|
for (let i = 0; i < rainCount; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const tilt = (Math.random() - 0.5) * Math.PI;
|
|
const dist = CONFIG.worldRadius + 10 + Math.random() * 80;
|
|
rainPos[i * 3] = Math.cos(angle) * Math.cos(tilt) * dist;
|
|
rainPos[i * 3 + 1] = Math.sin(tilt) * dist;
|
|
rainPos[i * 3 + 2] = Math.sin(angle) * Math.cos(tilt) * dist;
|
|
rainVel[i] = 0.8 + Math.random() * 0.5;
|
|
}
|
|
|
|
rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPos, 3));
|
|
const rainMat = new THREE.PointsMaterial({
|
|
color: 0x8888ff,
|
|
size: 0.3,
|
|
transparent: true,
|
|
opacity: 0.6
|
|
});
|
|
|
|
rain = new THREE.Points(rainGeo, rainMat);
|
|
rain.userData = { velocities: rainVel };
|
|
rain.visible = false;
|
|
scene.add(rain);
|
|
|
|
// Snow
|
|
const snowCount = 6000;
|
|
const snowGeo = new THREE.BufferGeometry();
|
|
const snowPos = new Float32Array(snowCount * 3);
|
|
const snowVel = new Float32Array(snowCount * 2);
|
|
|
|
for (let i = 0; i < snowCount; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const tilt = (Math.random() - 0.5) * Math.PI;
|
|
const dist = CONFIG.worldRadius + 10 + Math.random() * 60;
|
|
snowPos[i * 3] = Math.cos(angle) * Math.cos(tilt) * dist;
|
|
snowPos[i * 3 + 1] = Math.sin(tilt) * dist;
|
|
snowPos[i * 3 + 2] = Math.sin(angle) * Math.cos(tilt) * dist;
|
|
snowVel[i * 2] = 0.15 + Math.random() * 0.1;
|
|
snowVel[i * 2 + 1] = (Math.random() - 0.5) * 0.01;
|
|
}
|
|
|
|
snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPos, 3));
|
|
const snowMat = new THREE.PointsMaterial({
|
|
color: 0xffffff,
|
|
size: 0.5,
|
|
transparent: true,
|
|
opacity: 0.9
|
|
});
|
|
|
|
snow = new THREE.Points(snowGeo, snowMat);
|
|
snow.userData = { velocities: snowVel };
|
|
snow.visible = false;
|
|
scene.add(snow);
|
|
}
|
|
|
|
// ============ CAMERA ============
|
|
function updateCamera() {
|
|
camera.position.x = Math.sin(cameraTheta) * Math.cos(cameraPhi) * cameraDistance;
|
|
camera.position.y = Math.sin(cameraPhi) * cameraDistance;
|
|
camera.position.z = Math.cos(cameraTheta) * Math.cos(cameraPhi) * cameraDistance;
|
|
camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
// ============ EVENTS ============
|
|
function onResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function onMouseDown(e) {
|
|
isDragging = true;
|
|
previousMouseX = e.clientX;
|
|
previousMouseY = e.clientY;
|
|
}
|
|
|
|
function onMouseMove(e) {
|
|
if (!isDragging) return;
|
|
const dx = e.clientX - previousMouseX;
|
|
const dy = e.clientY - previousMouseY;
|
|
cameraTheta -= dx * 0.005;
|
|
cameraPhi = Math.max(0.1, Math.min(Math.PI / 2 - 0.1, cameraPhi + dy * 0.005));
|
|
updateCamera();
|
|
previousMouseX = e.clientX;
|
|
previousMouseY = e.clientY;
|
|
}
|
|
|
|
function onMouseUp() {
|
|
isDragging = false;
|
|
}
|
|
|
|
function onWheel(e) {
|
|
cameraDistance = Math.max(150, Math.min(500, cameraDistance + e.deltaY * 0.2));
|
|
updateCamera();
|
|
}
|
|
|
|
function onTouchStart(e) {
|
|
if (e.touches.length === 1) {
|
|
isDragging = true;
|
|
previousMouseX = e.touches[0].clientX;
|
|
previousMouseY = e.touches[0].clientY;
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
function onTouchMove(e) {
|
|
if (!isDragging || e.touches.length !== 1) return;
|
|
const dx = e.touches[0].clientX - previousMouseX;
|
|
const dy = e.touches[0].clientY - previousMouseY;
|
|
cameraTheta -= dx * 0.005;
|
|
cameraPhi = Math.max(0.1, Math.min(Math.PI / 2 - 0.1, cameraPhi + dy * 0.005));
|
|
updateCamera();
|
|
previousMouseX = e.touches[0].clientX;
|
|
previousMouseY = e.touches[0].clientY;
|
|
e.preventDefault();
|
|
}
|
|
|
|
function onTouchEnd() {
|
|
isDragging = false;
|
|
}
|
|
|
|
// ============ CONTROLS ============
|
|
function setTimeOfDay(tod) {
|
|
timeOfDay = tod;
|
|
const sky = window.skyMaterial;
|
|
|
|
document.querySelectorAll('#btnDay, #btnSunset, #btnNight').forEach(b => b.classList.remove('active'));
|
|
|
|
if (tod === 'day') {
|
|
sky.uniforms.topColor.value.setHex(COLORS.dayTop);
|
|
sky.uniforms.bottomColor.value.setHex(COLORS.dayBottom);
|
|
window.sunLight.intensity = 1.2;
|
|
window.ambientLight.intensity = 0.4;
|
|
window.sunMesh.visible = true;
|
|
document.getElementById('btnDay').classList.add('active');
|
|
} else if (tod === 'sunset') {
|
|
sky.uniforms.topColor.value.setHex(COLORS.sunsetTop);
|
|
sky.uniforms.bottomColor.value.setHex(COLORS.sunsetBottom);
|
|
window.sunLight.intensity = 0.8;
|
|
window.sunLight.color.setHex(0xFFAA55);
|
|
window.ambientLight.intensity = 0.3;
|
|
window.sunMesh.material.color.setHex(0xFF6600);
|
|
window.sunMesh.visible = true;
|
|
document.getElementById('btnSunset').classList.add('active');
|
|
} else {
|
|
sky.uniforms.topColor.value.setHex(COLORS.nightTop);
|
|
sky.uniforms.bottomColor.value.setHex(COLORS.nightBottom);
|
|
window.sunLight.intensity = 0.15;
|
|
window.sunLight.color.setHex(0x4444AA);
|
|
window.ambientLight.intensity = 0.15;
|
|
window.sunMesh.visible = false;
|
|
document.getElementById('btnNight').classList.add('active');
|
|
|
|
// Light up windows
|
|
houses.forEach(house => {
|
|
house.traverse(child => {
|
|
if (child.material?.emissive) {
|
|
child.material.emissiveIntensity = 0.8;
|
|
child.material.emissive.setHex(0xFFAA44);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (tod !== 'night') {
|
|
houses.forEach(house => {
|
|
house.traverse(child => {
|
|
if (child.material?.emissive) {
|
|
child.material.emissiveIntensity = 0;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
function toggleWeather(type) {
|
|
if (weather === type) {
|
|
weather = 'sunny';
|
|
rain.visible = false;
|
|
snow.visible = false;
|
|
document.getElementById('btnRain').classList.remove('active');
|
|
document.getElementById('btnSnow').classList.remove('active');
|
|
} else {
|
|
weather = type;
|
|
rain.visible = type === 'rain';
|
|
snow.visible = type === 'snow';
|
|
document.getElementById('btnRain').classList.toggle('active', type === 'rain');
|
|
document.getElementById('btnSnow').classList.toggle('active', type === 'snow');
|
|
}
|
|
}
|
|
|
|
function toggleRotate() {
|
|
autoRotate = !autoRotate;
|
|
document.getElementById('btnRotate').classList.toggle('active', autoRotate);
|
|
}
|
|
|
|
function toggleSpeed() {
|
|
speedMultiplier = speedMultiplier === 1 ? 3 : 1;
|
|
document.getElementById('btnFast').classList.toggle('active', speedMultiplier > 1);
|
|
}
|
|
|
|
// ============ UPDATE COUNTS ============
|
|
function updateCounts() {
|
|
document.getElementById('houseCount').textContent = houses.length;
|
|
document.getElementById('treeCount').textContent = trees.length;
|
|
document.getElementById('animalCount').textContent = animals.length;
|
|
document.getElementById('flowerCount').textContent = flowers.length;
|
|
document.getElementById('agentCount').textContent = agents.length;
|
|
}
|
|
|
|
// ============ ANIMATION ============
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const delta = 0.016 * speedMultiplier;
|
|
time += delta;
|
|
|
|
// Auto rotate
|
|
if (autoRotate && !isDragging) {
|
|
cameraTheta += 0.001 * speedMultiplier;
|
|
updateCamera();
|
|
}
|
|
|
|
// Rotate planet slowly
|
|
world.rotation.y += 0.0002 * speedMultiplier;
|
|
waterSphere.rotation.y += 0.0002 * speedMultiplier;
|
|
|
|
// Update compass
|
|
document.getElementById('compassNeedle').style.transform =
|
|
`rotate(${-cameraTheta * 180 / Math.PI}deg)`;
|
|
|
|
// Update all entities
|
|
updateAnimals(delta);
|
|
updateAgents(delta);
|
|
updateBirds(delta);
|
|
updateFish(delta);
|
|
updateButterflies(delta);
|
|
updateClouds(delta);
|
|
updateBoats(delta);
|
|
updateTrees(delta);
|
|
updateFlowers(delta);
|
|
updateWeather(delta);
|
|
updateUI();
|
|
updateMinimap();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
function updateAnimals(delta) {
|
|
animals.forEach(animal => {
|
|
const d = animal.userData;
|
|
d.idleTime += delta;
|
|
|
|
if (d.idleTime > 2) {
|
|
d.idleTime = 0;
|
|
d.wanderAngle += (Math.random() - 0.5) * 1.5;
|
|
}
|
|
|
|
// Move on continent surface
|
|
if (d.continentCenter) {
|
|
const forward = new THREE.Vector3(Math.cos(d.wanderAngle), 0, Math.sin(d.wanderAngle));
|
|
forward.applyAxisAngle(d.continentNormal, 0);
|
|
|
|
const newPos = animal.position.clone().add(forward.multiplyScalar(d.speed * speedMultiplier));
|
|
const distFromCenter = newPos.distanceTo(d.continentCenter);
|
|
|
|
if (distFromCenter < 20 * d.continentSize) {
|
|
animal.position.copy(newPos.normalize().multiplyScalar(CONFIG.worldRadius + 1));
|
|
animal.lookAt(animal.position.clone().add(forward));
|
|
} else {
|
|
d.wanderAngle += Math.PI;
|
|
}
|
|
}
|
|
|
|
// Hopping
|
|
d.hopPhase += 0.15 * speedMultiplier;
|
|
const hop = Math.abs(Math.sin(d.hopPhase)) * 0.15;
|
|
animal.position.normalize().multiplyScalar(CONFIG.worldRadius + 1 + hop);
|
|
});
|
|
}
|
|
|
|
function updateAgents(delta) {
|
|
agents.forEach(agent => {
|
|
const d = agent.userData;
|
|
|
|
// Hover
|
|
d.hoverPhase += 0.03 * speedMultiplier;
|
|
const hover = Math.sin(d.hoverPhase) * 0.1;
|
|
|
|
// Move
|
|
d.wanderAngle += (Math.random() - 0.5) * 0.05 * speedMultiplier;
|
|
|
|
if (d.continentCenter) {
|
|
const forward = new THREE.Vector3(Math.cos(d.wanderAngle), 0, Math.sin(d.wanderAngle));
|
|
const newPos = agent.position.clone().add(forward.multiplyScalar(d.speed * speedMultiplier));
|
|
agent.position.copy(newPos.normalize().multiplyScalar(CONFIG.worldRadius + 3 + hover));
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateBirds(delta) {
|
|
birds.forEach(bird => {
|
|
const d = bird.userData;
|
|
|
|
// Orbit
|
|
d.orbitAngle += d.orbitSpeed * speedMultiplier;
|
|
bird.position.x = Math.cos(d.orbitAngle) * Math.cos(d.orbitTilt) * d.orbitRadius;
|
|
bird.position.y = Math.sin(d.orbitTilt) * d.orbitRadius;
|
|
bird.position.z = Math.sin(d.orbitAngle) * Math.cos(d.orbitTilt) * d.orbitRadius;
|
|
|
|
// Face direction
|
|
bird.lookAt(
|
|
Math.cos(d.orbitAngle + 0.1) * d.orbitRadius,
|
|
bird.position.y,
|
|
Math.sin(d.orbitAngle + 0.1) * d.orbitRadius
|
|
);
|
|
|
|
// Flap wings
|
|
if (d.wings) {
|
|
d.wings.userData.flapPhase += d.flapSpeed * speedMultiplier;
|
|
d.wings.rotation.z = Math.sin(d.wings.userData.flapPhase) * 0.5;
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateFish(delta) {
|
|
fish.forEach(f => {
|
|
const d = f.userData;
|
|
|
|
d.swimAngle += d.swimSpeed * speedMultiplier;
|
|
const depth = CONFIG.worldRadius - 8;
|
|
f.position.x = Math.cos(d.swimAngle) * Math.cos(d.swimTilt) * depth;
|
|
f.position.y = Math.sin(d.swimTilt) * depth;
|
|
f.position.z = Math.sin(d.swimAngle) * Math.cos(d.swimTilt) * depth;
|
|
|
|
f.lookAt(
|
|
Math.cos(d.swimAngle + 0.1) * depth,
|
|
f.position.y,
|
|
Math.sin(d.swimAngle + 0.1) * depth
|
|
);
|
|
|
|
// Wobble
|
|
d.wobblePhase += 0.1 * speedMultiplier;
|
|
f.rotation.z = Math.sin(d.wobblePhase) * 0.1;
|
|
});
|
|
}
|
|
|
|
function updateButterflies(delta) {
|
|
butterflies.forEach(b => {
|
|
const d = b.userData;
|
|
|
|
// Flap
|
|
d.flapPhase += 0.25 * speedMultiplier;
|
|
d.leftWing.rotation.y = -0.4 - Math.sin(d.flapPhase) * 0.6;
|
|
d.rightWing.rotation.y = 0.4 + Math.sin(d.flapPhase) * 0.6;
|
|
|
|
// Wander
|
|
d.wanderAngle += (Math.random() - 0.5) * 0.1 * speedMultiplier;
|
|
const forward = new THREE.Vector3(Math.cos(d.wanderAngle), 0, Math.sin(d.wanderAngle));
|
|
b.position.add(forward.multiplyScalar(d.speed * speedMultiplier));
|
|
b.position.normalize().multiplyScalar(CONFIG.worldRadius + 5 + Math.sin(time * 2) * 2);
|
|
b.rotation.y = d.wanderAngle;
|
|
});
|
|
}
|
|
|
|
function updateClouds(delta) {
|
|
clouds.forEach(cloud => {
|
|
const d = cloud.userData;
|
|
d.orbitAngle += d.orbitSpeed * speedMultiplier;
|
|
d.bobPhase += 0.005 * speedMultiplier;
|
|
|
|
const height = CONFIG.worldRadius + 50 + Math.sin(d.bobPhase) * 5;
|
|
cloud.position.x = Math.cos(d.orbitAngle) * height;
|
|
cloud.position.z = Math.sin(d.orbitAngle) * height;
|
|
});
|
|
}
|
|
|
|
function updateBoats(delta) {
|
|
boats.forEach(boat => {
|
|
const d = boat.userData;
|
|
d.orbitAngle += d.orbitSpeed * speedMultiplier;
|
|
d.bobPhase += 0.02 * speedMultiplier;
|
|
|
|
const dist = CONFIG.worldRadius + 1.5 + Math.sin(d.bobPhase) * 0.3;
|
|
boat.position.x = Math.cos(d.orbitAngle) * dist;
|
|
boat.position.z = Math.sin(d.orbitAngle) * dist;
|
|
boat.position.y = Math.sin(d.bobPhase * 2) * 0.2;
|
|
|
|
boat.lookAt(0, 0, 0);
|
|
boat.rotateY(Math.PI / 2);
|
|
boat.rotateZ(Math.sin(d.bobPhase * 3) * 0.1);
|
|
});
|
|
}
|
|
|
|
function updateTrees(delta) {
|
|
trees.forEach(tree => {
|
|
tree.userData.swayPhase += 0.01 * speedMultiplier;
|
|
tree.rotation.z = Math.sin(tree.userData.swayPhase) * 0.02;
|
|
});
|
|
}
|
|
|
|
function updateFlowers(delta) {
|
|
flowers.forEach(flower => {
|
|
flower.userData.swayPhase += 0.02 * speedMultiplier;
|
|
flower.rotation.z = Math.sin(flower.userData.swayPhase) * 0.04;
|
|
});
|
|
}
|
|
|
|
function updateWeather(delta) {
|
|
if (rain.visible) {
|
|
const pos = rain.geometry.attributes.position.array;
|
|
const vel = rain.userData.velocities;
|
|
|
|
for (let i = 0; i < pos.length / 3; i++) {
|
|
const dir = new THREE.Vector3(pos[i*3], pos[i*3+1], pos[i*3+2]).normalize();
|
|
pos[i*3] -= dir.x * vel[i] * speedMultiplier;
|
|
pos[i*3+1] -= dir.y * vel[i] * speedMultiplier;
|
|
pos[i*3+2] -= dir.z * vel[i] * speedMultiplier;
|
|
|
|
const dist = Math.sqrt(pos[i*3]**2 + pos[i*3+1]**2 + pos[i*3+2]**2);
|
|
if (dist < CONFIG.worldRadius + 5) {
|
|
const newDist = CONFIG.worldRadius + 60 + Math.random() * 30;
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const tilt = (Math.random() - 0.5) * Math.PI;
|
|
pos[i*3] = Math.cos(angle) * Math.cos(tilt) * newDist;
|
|
pos[i*3+1] = Math.sin(tilt) * newDist;
|
|
pos[i*3+2] = Math.sin(angle) * Math.cos(tilt) * newDist;
|
|
}
|
|
}
|
|
rain.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
|
|
if (snow.visible) {
|
|
const pos = snow.geometry.attributes.position.array;
|
|
const vel = snow.userData.velocities;
|
|
|
|
for (let i = 0; i < pos.length / 3; i++) {
|
|
const dir = new THREE.Vector3(pos[i*3], pos[i*3+1], pos[i*3+2]).normalize();
|
|
pos[i*3] -= dir.x * vel[i*2] * speedMultiplier;
|
|
pos[i*3+1] -= dir.y * vel[i*2] * speedMultiplier;
|
|
pos[i*3+2] -= dir.z * vel[i*2] * speedMultiplier;
|
|
|
|
// Drift
|
|
pos[i*3] += Math.sin(time + i) * vel[i*2+1] * speedMultiplier;
|
|
|
|
const dist = Math.sqrt(pos[i*3]**2 + pos[i*3+1]**2 + pos[i*3+2]**2);
|
|
if (dist < CONFIG.worldRadius + 3) {
|
|
const newDist = CONFIG.worldRadius + 50 + Math.random() * 20;
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const tilt = (Math.random() - 0.5) * Math.PI;
|
|
pos[i*3] = Math.cos(angle) * Math.cos(tilt) * newDist;
|
|
pos[i*3+1] = Math.sin(tilt) * newDist;
|
|
pos[i*3+2] = Math.sin(angle) * Math.cos(tilt) * newDist;
|
|
}
|
|
}
|
|
snow.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
function updateUI() {
|
|
// Time
|
|
worldTime = (worldTime + 0.0005 * speedMultiplier) % 24;
|
|
const hours = Math.floor(worldTime);
|
|
const mins = Math.floor((worldTime % 1) * 60);
|
|
document.getElementById('timeDisplay').textContent =
|
|
`${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
|
|
|
// Weather
|
|
const icons = { sunny: '☀️', rain: '🌧️', snow: '❄️' };
|
|
const temps = { sunny: '72°F', rain: '58°F', snow: '28°F' };
|
|
document.getElementById('weatherIcon').textContent = icons[weather];
|
|
document.getElementById('weatherTemp').textContent = temps[weather];
|
|
|
|
// Stats
|
|
document.getElementById('happiness').textContent = Math.floor(95 + Math.sin(time * 0.1) * 5) + '%';
|
|
document.getElementById('nature').textContent = Math.floor(90 + Math.sin(time * 0.15) * 10) + '%';
|
|
document.getElementById('energy').textContent = Math.floor(88 + Math.sin(time * 0.2) * 12) + '%';
|
|
document.getElementById('magic').textContent = Math.floor(85 + Math.sin(time * 0.12) * 15) + '%';
|
|
}
|
|
|
|
function updateMinimap() {
|
|
const canvas = document.getElementById('minimapCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.fillStyle = '#4FA4E8';
|
|
ctx.fillRect(0, 0, 160, 160);
|
|
|
|
// Draw continents
|
|
ctx.fillStyle = '#4CAF50';
|
|
continents.forEach(cont => {
|
|
const x = 80 + (cont.centerPos.x / CONFIG.worldRadius) * 40;
|
|
const y = 80 - (cont.centerPos.z / CONFIG.worldRadius) * 40;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 12 * cont.data.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
|
|
// Camera indicator
|
|
ctx.fillStyle = '#FF1D6C';
|
|
const camX = 80 + Math.sin(cameraTheta) * 50;
|
|
const camY = 80 - Math.cos(cameraTheta) * 50;
|
|
ctx.beginPath();
|
|
ctx.arc(camX, camY, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Initialize
|
|
init();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|