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 🌸✨
1321 lines
48 KiB
HTML
1321 lines
48 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 Earth — Living Planet</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
overflow: hidden;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
|
|
background: #000;
|
|
}
|
|
#canvas-container { position: fixed; inset: 0; }
|
|
|
|
/* TOP BAR */
|
|
.top-bar {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0;
|
|
height: 50px;
|
|
background: linear-gradient(180deg, rgba(0,0,0,0.9) 0%, transparent 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 20px;
|
|
z-index: 100;
|
|
gap: 16px;
|
|
}
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
.logo-icon {
|
|
font-size: 24px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); }
|
|
50% { transform: scale(1.1); }
|
|
}
|
|
|
|
.resources {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-left: auto;
|
|
}
|
|
.resource {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
background: rgba(255,255,255,0.1);
|
|
padding: 6px 12px;
|
|
border-radius: 15px;
|
|
color: white;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* EVOLUTION PANEL */
|
|
.evo-panel {
|
|
position: fixed;
|
|
top: 60px;
|
|
left: 20px;
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
z-index: 100;
|
|
width: 240px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.evo-title {
|
|
color: white;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.evo-era {
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
margin-bottom: 8px;
|
|
}
|
|
.evo-year {
|
|
color: rgba(255,255,255,0.6);
|
|
font-size: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.evo-bar {
|
|
height: 6px;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
margin-bottom: 8px;
|
|
}
|
|
.evo-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #4CAF50, #8BC34A);
|
|
transition: width 0.5s;
|
|
}
|
|
.evo-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
.evo-stat {
|
|
background: rgba(255,255,255,0.05);
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
}
|
|
.evo-stat-icon { font-size: 16px; }
|
|
.evo-stat-value { color: white; font-weight: 600; font-size: 14px; }
|
|
.evo-stat-label { color: rgba(255,255,255,0.5); font-size: 9px; }
|
|
.evo-stat-change {
|
|
font-size: 10px;
|
|
margin-top: 2px;
|
|
}
|
|
.evo-stat-change.up { color: #4CAF50; }
|
|
.evo-stat-change.down { color: #f44336; }
|
|
|
|
/* WORLD STATS */
|
|
.world-panel {
|
|
position: fixed;
|
|
top: 60px;
|
|
right: 20px;
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
z-index: 100;
|
|
width: 200px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.world-title {
|
|
color: white;
|
|
font-size: 12px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.world-stat {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 6px 0;
|
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
}
|
|
.world-stat:last-child { border: none; }
|
|
.world-stat-label { color: rgba(255,255,255,0.6); font-size: 11px; }
|
|
.world-stat-value { color: white; font-size: 12px; font-weight: 500; }
|
|
|
|
/* BUILD BAR */
|
|
.build-bar {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 6px;
|
|
background: rgba(0,0,0,0.9);
|
|
padding: 10px 16px;
|
|
border-radius: 30px;
|
|
z-index: 100;
|
|
}
|
|
.build-btn {
|
|
width: 48px;
|
|
height: 48px;
|
|
background: rgba(255,255,255,0.1);
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
}
|
|
.build-btn:hover { background: rgba(255,255,255,0.2); transform: translateY(-3px); }
|
|
.build-btn.active { background: #FF1D6C; }
|
|
.build-btn-key {
|
|
position: absolute;
|
|
top: 2px;
|
|
right: 4px;
|
|
font-size: 9px;
|
|
color: rgba(255,255,255,0.4);
|
|
}
|
|
|
|
/* CONTROLS */
|
|
.controls {
|
|
position: fixed;
|
|
bottom: 90px;
|
|
right: 20px;
|
|
display: flex;
|
|
gap: 6px;
|
|
z-index: 100;
|
|
}
|
|
.ctrl-btn {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: rgba(0,0,0,0.85);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
border-radius: 10px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.ctrl-btn:hover { background: rgba(255,255,255,0.15); }
|
|
.ctrl-btn.active { background: #FF1D6C; }
|
|
|
|
/* SPEED CONTROL */
|
|
.speed-control {
|
|
position: fixed;
|
|
bottom: 90px;
|
|
left: 20px;
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 20px;
|
|
padding: 8px 16px;
|
|
z-index: 100;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.speed-label { color: rgba(255,255,255,0.6); font-size: 11px; }
|
|
.speed-btns { display: flex; gap: 4px; }
|
|
.speed-btn {
|
|
width: 32px;
|
|
height: 28px;
|
|
background: rgba(255,255,255,0.1);
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: white;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
}
|
|
.speed-btn.active { background: #FF1D6C; }
|
|
|
|
/* EVENTS LOG */
|
|
.events {
|
|
position: fixed;
|
|
bottom: 150px;
|
|
left: 20px;
|
|
width: 240px;
|
|
z-index: 100;
|
|
pointer-events: none;
|
|
}
|
|
.event {
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 8px;
|
|
padding: 8px 12px;
|
|
margin-bottom: 4px;
|
|
color: white;
|
|
font-size: 11px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
animation: slideIn 0.3s ease;
|
|
border-left: 3px solid #4CAF50;
|
|
}
|
|
.event.disaster { border-left-color: #f44336; }
|
|
.event.discovery { border-left-color: #2196F3; }
|
|
.event.evolution { border-left-color: #9C27B0; }
|
|
@keyframes slideIn { from { transform: translateX(-50px); opacity: 0; } }
|
|
|
|
/* LOADING */
|
|
.loading {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a15 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
transition: opacity 1s;
|
|
}
|
|
.loading.hidden { opacity: 0; pointer-events: none; }
|
|
.loading-planet {
|
|
font-size: 80px;
|
|
animation: spin 4s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.loading-title { color: white; font-size: 24px; margin-top: 20px; font-weight: 300; }
|
|
.loading-sub { color: rgba(255,255,255,0.5); font-size: 14px; margin-top: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="loading" id="loading">
|
|
<div class="loading-planet">🌍</div>
|
|
<div class="loading-title">Creating Your Planet</div>
|
|
<div class="loading-sub">Generating terrain, oceans, and life...</div>
|
|
</div>
|
|
|
|
<div id="canvas-container"></div>
|
|
|
|
<!-- Top Bar -->
|
|
<div class="top-bar">
|
|
<div class="logo">
|
|
<span class="logo-icon">🌍</span>
|
|
<span>BlackRoad Earth</span>
|
|
</div>
|
|
<div class="resources">
|
|
<div class="resource"><span>👥</span><span id="resPop">0</span></div>
|
|
<div class="resource"><span>🏠</span><span id="resCities">0</span></div>
|
|
<div class="resource"><span>🌳</span><span id="resTrees">0</span></div>
|
|
<div class="resource"><span>🔬</span><span id="resTech">0</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Evolution Panel -->
|
|
<div class="evo-panel">
|
|
<div class="evo-title">🧬 Evolution</div>
|
|
<div class="evo-era" id="eraName">Primordial Era</div>
|
|
<div class="evo-year">Year <span id="yearNum">0</span></div>
|
|
<div class="evo-bar">
|
|
<div class="evo-fill" id="eraProgress" style="width: 0%"></div>
|
|
</div>
|
|
<div style="color: rgba(255,255,255,0.5); font-size: 10px;">Next: <span id="nextEra">Microbial Era</span></div>
|
|
<div class="evo-stats">
|
|
<div class="evo-stat">
|
|
<div class="evo-stat-icon">🌡️</div>
|
|
<div class="evo-stat-value" id="statTemp">15°C</div>
|
|
<div class="evo-stat-label">Temperature</div>
|
|
<div class="evo-stat-change" id="tempChange"></div>
|
|
</div>
|
|
<div class="evo-stat">
|
|
<div class="evo-stat-icon">💨</div>
|
|
<div class="evo-stat-value" id="statO2">21%</div>
|
|
<div class="evo-stat-label">Oxygen</div>
|
|
<div class="evo-stat-change" id="o2Change"></div>
|
|
</div>
|
|
<div class="evo-stat">
|
|
<div class="evo-stat-icon">🌊</div>
|
|
<div class="evo-stat-value" id="statSea">70%</div>
|
|
<div class="evo-stat-label">Ocean</div>
|
|
<div class="evo-stat-change" id="seaChange"></div>
|
|
</div>
|
|
<div class="evo-stat">
|
|
<div class="evo-stat-icon">🧬</div>
|
|
<div class="evo-stat-value" id="statBio">0%</div>
|
|
<div class="evo-stat-label">Biodiversity</div>
|
|
<div class="evo-stat-change" id="bioChange"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- World Stats -->
|
|
<div class="world-panel">
|
|
<div class="world-title">🌍 World Status</div>
|
|
<div class="world-stat">
|
|
<span class="world-stat-label">Season</span>
|
|
<span class="world-stat-value" id="seasonVal">Spring</span>
|
|
</div>
|
|
<div class="world-stat">
|
|
<span class="world-stat-label">Day/Night</span>
|
|
<span class="world-stat-value" id="dayNightVal">☀️ Day</span>
|
|
</div>
|
|
<div class="world-stat">
|
|
<span class="world-stat-label">Weather</span>
|
|
<span class="world-stat-value" id="weatherVal">Clear</span>
|
|
</div>
|
|
<div class="world-stat">
|
|
<span class="world-stat-label">Life Forms</span>
|
|
<span class="world-stat-value" id="lifeVal">0</span>
|
|
</div>
|
|
<div class="world-stat">
|
|
<span class="world-stat-label">Ecosystems</span>
|
|
<span class="world-stat-value" id="ecoVal">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Speed Control -->
|
|
<div class="speed-control">
|
|
<span class="speed-label">Time:</span>
|
|
<div class="speed-btns">
|
|
<button class="speed-btn" data-speed="0">⏸</button>
|
|
<button class="speed-btn active" data-speed="1">1x</button>
|
|
<button class="speed-btn" data-speed="5">5x</button>
|
|
<button class="speed-btn" data-speed="20">20x</button>
|
|
<button class="speed-btn" data-speed="100">⏩</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Build Bar -->
|
|
<div class="build-bar">
|
|
<button class="build-btn" data-build="tree" title="Plant Forest">🌲<span class="build-btn-key">1</span></button>
|
|
<button class="build-btn" data-build="life" title="Seed Life">🦠<span class="build-btn-key">2</span></button>
|
|
<button class="build-btn" data-build="animal" title="Create Animals">🐾<span class="build-btn-key">3</span></button>
|
|
<button class="build-btn" data-build="village" title="Found Village">🏘️<span class="build-btn-key">4</span></button>
|
|
<button class="build-btn" data-build="city" title="Build City">🏙️<span class="build-btn-key">5</span></button>
|
|
<button class="build-btn" data-build="wonder" title="Create Wonder">🏛️<span class="build-btn-key">6</span></button>
|
|
<button class="build-btn" data-build="meteor" title="Meteor Strike">☄️<span class="build-btn-key">7</span></button>
|
|
<button class="build-btn" data-build="terraform" title="Terraform">🌋<span class="build-btn-key">8</span></button>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls">
|
|
<button class="ctrl-btn active" id="btnRotate" title="Auto Rotate">🔄</button>
|
|
<button class="ctrl-btn" id="btnClouds" title="Toggle Clouds">☁️</button>
|
|
<button class="ctrl-btn" id="btnGrid" title="Show Grid">🌐</button>
|
|
</div>
|
|
|
|
<!-- Events -->
|
|
<div class="events" id="events"></div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// ==================== SIMPLEX NOISE ====================
|
|
class SimplexNoise {
|
|
constructor(seed = Math.random() * 10000) {
|
|
this.p = new Uint8Array(256);
|
|
for (let i = 0; i < 256; i++) this.p[i] = i;
|
|
let n = seed;
|
|
for (let i = 255; i > 0; i--) {
|
|
n = (n * 16807) % 2147483647;
|
|
const j = Math.floor((n / 2147483647) * (i + 1));
|
|
[this.p[i], this.p[j]] = [this.p[j], this.p[i]];
|
|
}
|
|
this.perm = new Uint8Array(512);
|
|
for (let i = 0; i < 512; i++) this.perm[i] = this.p[i & 255];
|
|
}
|
|
|
|
noise3D(x, y, z) {
|
|
const F3 = 1/3, G3 = 1/6;
|
|
const s = (x + y + z) * F3;
|
|
const i = Math.floor(x + s), j = Math.floor(y + s), k = Math.floor(z + s);
|
|
const t = (i + j + k) * G3;
|
|
const X0 = i - t, Y0 = j - t, Z0 = k - t;
|
|
const x0 = x - X0, y0 = y - Y0, z0 = z - Z0;
|
|
|
|
let i1, j1, k1, i2, j2, k2;
|
|
if (x0 >= y0) {
|
|
if (y0 >= z0) { i1=1;j1=0;k1=0;i2=1;j2=1;k2=0; }
|
|
else if (x0 >= z0) { i1=1;j1=0;k1=0;i2=1;j2=0;k2=1; }
|
|
else { i1=0;j1=0;k1=1;i2=1;j2=0;k2=1; }
|
|
} else {
|
|
if (y0 < z0) { i1=0;j1=0;k1=1;i2=0;j2=1;k2=1; }
|
|
else if (x0 < z0) { i1=0;j1=1;k1=0;i2=0;j2=1;k2=1; }
|
|
else { i1=0;j1=1;k1=0;i2=1;j2=1;k2=0; }
|
|
}
|
|
|
|
const x1 = x0 - i1 + G3, y1 = y0 - j1 + G3, z1 = z0 - k1 + G3;
|
|
const x2 = x0 - i2 + 2*G3, y2 = y0 - j2 + 2*G3, z2 = z0 - k2 + 2*G3;
|
|
const x3 = x0 - 1 + 3*G3, y3 = y0 - 1 + 3*G3, z3 = z0 - 1 + 3*G3;
|
|
|
|
const ii = i & 255, jj = j & 255, kk = k & 255;
|
|
|
|
const grad = (hash, x, y, z) => {
|
|
const h = hash & 15;
|
|
const u = h < 8 ? x : y;
|
|
const v = h < 4 ? y : (h === 12 || h === 14 ? x : z);
|
|
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
|
|
};
|
|
|
|
let n0 = 0, n1 = 0, n2 = 0, n3 = 0;
|
|
let t0 = 0.6 - x0*x0 - y0*y0 - z0*z0;
|
|
if (t0 >= 0) { t0 *= t0; n0 = t0 * t0 * grad(this.perm[ii+this.perm[jj+this.perm[kk]]], x0, y0, z0); }
|
|
let t1 = 0.6 - x1*x1 - y1*y1 - z1*z1;
|
|
if (t1 >= 0) { t1 *= t1; n1 = t1 * t1 * grad(this.perm[ii+i1+this.perm[jj+j1+this.perm[kk+k1]]], x1, y1, z1); }
|
|
let t2 = 0.6 - x2*x2 - y2*y2 - z2*z2;
|
|
if (t2 >= 0) { t2 *= t2; n2 = t2 * t2 * grad(this.perm[ii+i2+this.perm[jj+j2+this.perm[kk+k2]]], x2, y2, z2); }
|
|
let t3 = 0.6 - x3*x3 - y3*y3 - z3*z3;
|
|
if (t3 >= 0) { t3 *= t3; n3 = t3 * t3 * grad(this.perm[ii+1+this.perm[jj+1+this.perm[kk+1]]], x3, y3, z3); }
|
|
|
|
return 32 * (n0 + n1 + n2 + n3);
|
|
}
|
|
|
|
fbm(x, y, z, octaves = 6) {
|
|
let sum = 0, amp = 1, freq = 1, max = 0;
|
|
for (let i = 0; i < octaves; i++) {
|
|
sum += this.noise3D(x * freq, y * freq, z * freq) * amp;
|
|
max += amp;
|
|
amp *= 0.5;
|
|
freq *= 2;
|
|
}
|
|
return sum / max;
|
|
}
|
|
}
|
|
|
|
// ==================== EVOLUTION ERAS ====================
|
|
const ERAS = [
|
|
{ name: 'Primordial Era', duration: 500, color: 0x1a0a0a, life: false, plants: false, animals: false, civ: false },
|
|
{ name: 'Microbial Era', duration: 1000, color: 0x1a1a0a, life: true, plants: false, animals: false, civ: false },
|
|
{ name: 'Plant Era', duration: 1500, color: 0x0a1a0a, life: true, plants: true, animals: false, civ: false },
|
|
{ name: 'Animal Era', duration: 2000, color: 0x0a2a0a, life: true, plants: true, animals: true, civ: false },
|
|
{ name: 'Tribal Era', duration: 2500, color: 0x1a2a1a, life: true, plants: true, animals: true, civ: true },
|
|
{ name: 'Ancient Era', duration: 3000, color: 0x2a3a2a, life: true, plants: true, animals: true, civ: true },
|
|
{ name: 'Medieval Era', duration: 3500, color: 0x3a4a3a, life: true, plants: true, animals: true, civ: true },
|
|
{ name: 'Industrial Era', duration: 4000, color: 0x4a5a4a, life: true, plants: true, animals: true, civ: true },
|
|
{ name: 'Modern Era', duration: 5000, color: 0x5a6a5a, life: true, plants: true, animals: true, civ: true },
|
|
{ name: 'Space Era', duration: 10000, color: 0x6a7a8a, life: true, plants: true, animals: true, civ: true },
|
|
];
|
|
|
|
// ==================== GAME STATE ====================
|
|
const WORLD = {
|
|
year: 0,
|
|
era: 0,
|
|
eraProgress: 0,
|
|
speed: 1,
|
|
|
|
temperature: 15,
|
|
oxygen: 5,
|
|
seaLevel: 75,
|
|
biodiversity: 0,
|
|
|
|
population: 0,
|
|
cities: 0,
|
|
tech: 0,
|
|
|
|
season: 0, // 0-3
|
|
dayProgress: 0,
|
|
isNight: false,
|
|
weather: 'clear',
|
|
|
|
lifeForms: 0,
|
|
ecosystems: 0
|
|
};
|
|
|
|
// ==================== THREE.JS ====================
|
|
let scene, camera, renderer;
|
|
let earth, atmosphere, clouds, ocean;
|
|
let trees = [], animals = [], cities = [], particles = [];
|
|
let noise;
|
|
let time = 0;
|
|
let autoRotate = true;
|
|
let showClouds = true;
|
|
let showGrid = false;
|
|
|
|
// Camera
|
|
let isDragging = false;
|
|
let prevMouse = { x: 0, y: 0 };
|
|
let spherical = { theta: 0, phi: Math.PI / 3, radius: 350 };
|
|
|
|
const EARTH_RADIUS = 100;
|
|
|
|
function init() {
|
|
noise = new SimplexNoise(42);
|
|
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x000510);
|
|
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
|
|
updateCamera();
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.2;
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
// Build world
|
|
createStars();
|
|
createSun();
|
|
createEarth();
|
|
createOcean();
|
|
createAtmosphere();
|
|
createClouds();
|
|
|
|
// Events
|
|
setupEvents();
|
|
|
|
// Start
|
|
setTimeout(() => {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
logEvent('🌍 Planet formed from cosmic dust', 'evolution');
|
|
animate();
|
|
}, 1500);
|
|
}
|
|
|
|
function createStars() {
|
|
const geo = new THREE.BufferGeometry();
|
|
const count = 10000;
|
|
const pos = new Float32Array(count * 3);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const r = 800 + Math.random() * 400;
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.acos(Math.random() * 2 - 1);
|
|
pos[i*3] = r * Math.sin(phi) * Math.cos(theta);
|
|
pos[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
|
|
pos[i*3+2] = r * Math.cos(phi);
|
|
}
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
const stars = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0xffffff, size: 1.5 }));
|
|
scene.add(stars);
|
|
}
|
|
|
|
function createSun() {
|
|
const sunGeo = new THREE.SphereGeometry(30, 32, 32);
|
|
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffff80 });
|
|
const sun = new THREE.Mesh(sunGeo, sunMat);
|
|
sun.position.set(500, 200, 300);
|
|
scene.add(sun);
|
|
|
|
// Sun glow
|
|
const glowGeo = new THREE.SphereGeometry(50, 32, 32);
|
|
const glowMat = new THREE.MeshBasicMaterial({
|
|
color: 0xffaa00,
|
|
transparent: true,
|
|
opacity: 0.3
|
|
});
|
|
const glow = new THREE.Mesh(glowGeo, glowMat);
|
|
glow.position.copy(sun.position);
|
|
scene.add(glow);
|
|
|
|
// Sun light
|
|
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
|
sunLight.position.copy(sun.position);
|
|
scene.add(sunLight);
|
|
|
|
scene.add(new THREE.AmbientLight(0x404060, 0.4));
|
|
}
|
|
|
|
function createEarth() {
|
|
const segments = 128;
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS, segments, segments);
|
|
const pos = geo.attributes.position.array;
|
|
const colors = new Float32Array(pos.length);
|
|
|
|
// Store terrain data
|
|
window.terrainData = [];
|
|
|
|
for (let i = 0; i < pos.length; i += 3) {
|
|
const x = pos[i], y = pos[i+1], z = pos[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;
|
|
|
|
// Generate terrain height
|
|
const continental = noise.fbm(nx*2, ny*2, nz*2, 4) * 0.6;
|
|
const mountains = Math.pow(Math.abs(noise.fbm(nx*4, ny*4, nz*4, 5)), 1.5) * 0.4;
|
|
const detail = noise.fbm(nx*8, ny*8, nz*8, 3) * 0.1;
|
|
|
|
let elevation = continental + mountains + detail;
|
|
|
|
// Create more land near certain latitudes
|
|
const lat = Math.asin(ny);
|
|
const landBias = Math.cos(lat * 2) * 0.2;
|
|
elevation += landBias;
|
|
|
|
// Normalize elevation
|
|
elevation = Math.max(-0.3, Math.min(0.5, elevation));
|
|
|
|
// Apply elevation to vertex
|
|
const heightScale = elevation > 0 ? elevation * 8 : elevation * 2;
|
|
pos[i] = nx * (EARTH_RADIUS + heightScale);
|
|
pos[i+1] = ny * (EARTH_RADIUS + heightScale);
|
|
pos[i+2] = nz * (EARTH_RADIUS + heightScale);
|
|
|
|
// Store for later use
|
|
window.terrainData.push({ nx, ny, nz, elevation });
|
|
|
|
// Color based on elevation and latitude
|
|
let color = new THREE.Color();
|
|
|
|
if (elevation < 0) {
|
|
// Ocean floor - will be covered by water
|
|
color.setHex(0x1a3a5a);
|
|
} else if (elevation < 0.05) {
|
|
// Beach
|
|
color.setHex(0xf4d03f);
|
|
} else if (Math.abs(lat) > 1.2) {
|
|
// Polar ice
|
|
color.setHex(0xffffff);
|
|
} else if (elevation > 0.35) {
|
|
// Snow peaks
|
|
color.setHex(0xffffff);
|
|
} else if (elevation > 0.25) {
|
|
// Mountains
|
|
color.setHex(0x808080);
|
|
} else if (Math.abs(lat) < 0.4 && elevation < 0.15) {
|
|
// Tropical
|
|
color.setHex(0x228B22);
|
|
} else if (Math.abs(lat) > 0.8) {
|
|
// Tundra
|
|
color.setHex(0x90a090);
|
|
} else {
|
|
// Temperate
|
|
color.setHex(0x3d7a3d);
|
|
}
|
|
|
|
// Add noise variation
|
|
const variation = noise.noise3D(nx*10, ny*10, nz*10) * 0.1;
|
|
color.r = Math.max(0, Math.min(1, color.r + variation));
|
|
color.g = Math.max(0, Math.min(1, color.g + variation));
|
|
color.b = Math.max(0, Math.min(1, color.b + variation));
|
|
|
|
colors[i] = color.r;
|
|
colors[i+1] = color.g;
|
|
colors[i+2] = color.b;
|
|
}
|
|
|
|
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
geo.computeVertexNormals();
|
|
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
vertexColors: true,
|
|
roughness: 0.8,
|
|
metalness: 0.1
|
|
});
|
|
|
|
earth = new THREE.Mesh(geo, mat);
|
|
scene.add(earth);
|
|
}
|
|
|
|
function createOcean() {
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS - 0.5, 96, 96);
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color: 0x1a6aa8,
|
|
transparent: true,
|
|
opacity: 0.85,
|
|
roughness: 0.2,
|
|
metalness: 0.3
|
|
});
|
|
|
|
ocean = new THREE.Mesh(geo, mat);
|
|
scene.add(ocean);
|
|
}
|
|
|
|
function createAtmosphere() {
|
|
// Outer glow
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS + 15, 64, 64);
|
|
const mat = new THREE.ShaderMaterial({
|
|
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)), 2.0);
|
|
gl_FragColor = vec4(0.3, 0.6, 1.0, intensity * 0.5);
|
|
}
|
|
`,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.BackSide,
|
|
transparent: true
|
|
});
|
|
|
|
atmosphere = new THREE.Mesh(geo, mat);
|
|
scene.add(atmosphere);
|
|
}
|
|
|
|
function createClouds() {
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS + 3, 64, 64);
|
|
|
|
// Create cloud texture procedurally
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 512;
|
|
canvas.height = 256;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Generate cloud pattern
|
|
const imageData = ctx.createImageData(512, 256);
|
|
for (let y = 0; y < 256; y++) {
|
|
for (let x = 0; x < 512; x++) {
|
|
const nx = x / 512 * 4 - 2;
|
|
const ny = y / 256 * 2 - 1;
|
|
const nz = Math.sqrt(Math.max(0, 1 - nx*nx*0.1 - ny*ny));
|
|
|
|
let cloud = noise.fbm(nx * 3, ny * 3, nz * 3, 4);
|
|
cloud = Math.max(0, (cloud + 0.3) * 1.5);
|
|
|
|
const idx = (y * 512 + x) * 4;
|
|
imageData.data[idx] = 255;
|
|
imageData.data[idx+1] = 255;
|
|
imageData.data[idx+2] = 255;
|
|
imageData.data[idx+3] = cloud * 200;
|
|
}
|
|
}
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
texture.wrapS = THREE.RepeatWrapping;
|
|
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
|
|
const mat = new THREE.MeshBasicMaterial({
|
|
map: texture,
|
|
transparent: true,
|
|
opacity: 0.6,
|
|
depthWrite: false
|
|
});
|
|
|
|
clouds = new THREE.Mesh(geo, mat);
|
|
scene.add(clouds);
|
|
}
|
|
|
|
// ==================== LIFE SPAWNING ====================
|
|
function spawnTree(lat, lng) {
|
|
if (!ERAS[WORLD.era].plants) return;
|
|
|
|
const pos = latLngToVector3(lat, lng, EARTH_RADIUS + 1);
|
|
const group = new THREE.Group();
|
|
|
|
// Trunk
|
|
const trunk = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(0.15, 0.25, 2, 5),
|
|
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
|
|
);
|
|
trunk.position.y = 1;
|
|
group.add(trunk);
|
|
|
|
// Leaves - biome based on latitude
|
|
let leafColor = Math.abs(lat) > 60 ? 0x2d5a27 : (Math.abs(lat) < 25 ? 0x228B22 : 0x3d7a3d);
|
|
const leaves = new THREE.Mesh(
|
|
new THREE.ConeGeometry(1.2, 2.5, 6),
|
|
new THREE.MeshStandardMaterial({ color: leafColor })
|
|
);
|
|
leaves.position.y = 3;
|
|
group.add(leaves);
|
|
|
|
group.position.copy(pos);
|
|
group.lookAt(0, 0, 0);
|
|
group.rotateX(Math.PI / 2);
|
|
group.scale.setScalar(0.5 + Math.random() * 0.3);
|
|
|
|
group.userData = { type: 'tree', lat, lng, age: 0, sway: Math.random() * Math.PI * 2 };
|
|
trees.push(group);
|
|
scene.add(group);
|
|
|
|
WORLD.biodiversity = Math.min(100, WORLD.biodiversity + 0.01);
|
|
WORLD.oxygen = Math.min(25, WORLD.oxygen + 0.001);
|
|
}
|
|
|
|
function spawnAnimal(lat, lng) {
|
|
if (!ERAS[WORLD.era].animals) return;
|
|
|
|
const pos = latLngToVector3(lat, lng, EARTH_RADIUS + 1.5);
|
|
const group = new THREE.Group();
|
|
|
|
const colors = [0xD2B48C, 0x8B4513, 0x808080, 0xFFFFFF, 0x000000];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
const mat = new THREE.MeshStandardMaterial({ color });
|
|
|
|
const body = new THREE.Mesh(new THREE.SphereGeometry(0.3, 6, 6), mat);
|
|
body.scale.set(1, 0.7, 1.2);
|
|
group.add(body);
|
|
|
|
const head = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), mat);
|
|
head.position.set(0, 0.15, 0.3);
|
|
group.add(head);
|
|
|
|
group.position.copy(pos);
|
|
group.lookAt(0, 0, 0);
|
|
group.rotateX(Math.PI / 2);
|
|
group.scale.setScalar(0.4);
|
|
|
|
group.userData = {
|
|
type: 'animal',
|
|
lat,
|
|
lng,
|
|
targetLat: lat,
|
|
targetLng: lng,
|
|
speed: 0.001 + Math.random() * 0.002,
|
|
hop: Math.random() * Math.PI * 2
|
|
};
|
|
animals.push(group);
|
|
scene.add(group);
|
|
|
|
WORLD.biodiversity = Math.min(100, WORLD.biodiversity + 0.05);
|
|
WORLD.lifeForms++;
|
|
}
|
|
|
|
function spawnCity(lat, lng, size = 1) {
|
|
if (!ERAS[WORLD.era].civ) return;
|
|
|
|
const pos = latLngToVector3(lat, lng, EARTH_RADIUS + 0.5);
|
|
const group = new THREE.Group();
|
|
|
|
const numBuildings = 3 + Math.floor(size * 5);
|
|
for (let i = 0; i < numBuildings; i++) {
|
|
const height = 0.5 + Math.random() * size * 2;
|
|
const building = new THREE.Mesh(
|
|
new THREE.BoxGeometry(0.3, height, 0.3),
|
|
new THREE.MeshStandardMaterial({
|
|
color: 0x808080,
|
|
emissive: WORLD.isNight ? 0xffaa00 : 0x000000,
|
|
emissiveIntensity: WORLD.isNight ? 0.5 : 0
|
|
})
|
|
);
|
|
building.position.set(
|
|
(Math.random() - 0.5) * size,
|
|
height / 2,
|
|
(Math.random() - 0.5) * size
|
|
);
|
|
group.add(building);
|
|
}
|
|
|
|
group.position.copy(pos);
|
|
group.lookAt(0, 0, 0);
|
|
group.rotateX(Math.PI / 2);
|
|
|
|
group.userData = { type: 'city', lat, lng, size, population: 100 * size };
|
|
cities.push(group);
|
|
scene.add(group);
|
|
|
|
WORLD.cities++;
|
|
WORLD.population += Math.floor(100 * size);
|
|
WORLD.tech += Math.floor(10 * size);
|
|
}
|
|
|
|
function latLngToVector3(lat, lng, radius) {
|
|
const phi = (90 - lat) * Math.PI / 180;
|
|
const theta = (lng + 180) * Math.PI / 180;
|
|
return new THREE.Vector3(
|
|
-radius * Math.sin(phi) * Math.cos(theta),
|
|
radius * Math.cos(phi),
|
|
radius * Math.sin(phi) * Math.sin(theta)
|
|
);
|
|
}
|
|
|
|
// ==================== EVOLUTION ====================
|
|
function evolve() {
|
|
const era = ERAS[WORLD.era];
|
|
|
|
// Progress through era
|
|
WORLD.eraProgress += WORLD.speed * 0.1;
|
|
WORLD.year += WORLD.speed;
|
|
|
|
// Era transition
|
|
if (WORLD.eraProgress >= 100 && WORLD.era < ERAS.length - 1) {
|
|
WORLD.era++;
|
|
WORLD.eraProgress = 0;
|
|
const newEra = ERAS[WORLD.era];
|
|
logEvent(`🧬 ${newEra.name} begins!`, 'evolution');
|
|
|
|
// Era bonuses
|
|
if (newEra.plants && !era.plants) {
|
|
logEvent('🌱 First plants emerge from the oceans', 'evolution');
|
|
for (let i = 0; i < 20; i++) {
|
|
spawnTree(
|
|
(Math.random() - 0.5) * 120,
|
|
(Math.random() - 0.5) * 360
|
|
);
|
|
}
|
|
}
|
|
if (newEra.animals && !era.animals) {
|
|
logEvent('🦠 Complex life evolves', 'evolution');
|
|
for (let i = 0; i < 10; i++) {
|
|
spawnAnimal(
|
|
(Math.random() - 0.5) * 100,
|
|
(Math.random() - 0.5) * 360
|
|
);
|
|
}
|
|
}
|
|
if (newEra.civ && !era.civ) {
|
|
logEvent('🏘️ First civilizations emerge', 'evolution');
|
|
spawnCity((Math.random() - 0.5) * 60, (Math.random() - 0.5) * 360, 0.5);
|
|
}
|
|
}
|
|
|
|
// Natural growth
|
|
if (Math.random() < 0.01 * WORLD.speed && era.plants && trees.length < 500) {
|
|
spawnTree(
|
|
(Math.random() - 0.5) * 120,
|
|
(Math.random() - 0.5) * 360
|
|
);
|
|
}
|
|
|
|
if (Math.random() < 0.005 * WORLD.speed && era.animals && animals.length < 200) {
|
|
spawnAnimal(
|
|
(Math.random() - 0.5) * 100,
|
|
(Math.random() - 0.5) * 360
|
|
);
|
|
}
|
|
|
|
if (Math.random() < 0.001 * WORLD.speed && era.civ && cities.length < 50) {
|
|
spawnCity(
|
|
(Math.random() - 0.5) * 80,
|
|
(Math.random() - 0.5) * 360,
|
|
0.3 + Math.random() * 0.7
|
|
);
|
|
}
|
|
|
|
// City growth
|
|
cities.forEach(city => {
|
|
city.userData.population += WORLD.speed * 0.5;
|
|
city.userData.size = Math.min(3, city.userData.size + 0.0001 * WORLD.speed);
|
|
});
|
|
|
|
// Update world stats
|
|
WORLD.temperature = 15 + Math.sin(time * 0.01) * 3 + (WORLD.era * 0.5);
|
|
WORLD.oxygen = Math.min(25, 5 + trees.length * 0.02);
|
|
WORLD.seaLevel = 75 - WORLD.era * 0.5;
|
|
WORLD.ecosystems = Math.floor(trees.length / 20) + Math.floor(animals.length / 10);
|
|
|
|
WORLD.population = cities.reduce((sum, c) => sum + Math.floor(c.userData.population), 0);
|
|
|
|
// Random events
|
|
if (Math.random() < 0.0001 * WORLD.speed) {
|
|
triggerEvent();
|
|
}
|
|
|
|
// Day/night cycle
|
|
WORLD.dayProgress += WORLD.speed * 0.5;
|
|
if (WORLD.dayProgress >= 100) {
|
|
WORLD.dayProgress = 0;
|
|
WORLD.isNight = !WORLD.isNight;
|
|
}
|
|
|
|
// Seasons
|
|
WORLD.season = Math.floor((WORLD.year % 400) / 100);
|
|
}
|
|
|
|
function triggerEvent() {
|
|
const events = [
|
|
{ msg: '☄️ Meteor impact causes extinction event!', type: 'disaster', effect: () => {
|
|
const kill = Math.floor(animals.length * 0.3);
|
|
for (let i = 0; i < kill; i++) {
|
|
if (animals.length > 0) {
|
|
const a = animals.pop();
|
|
scene.remove(a);
|
|
}
|
|
}
|
|
WORLD.biodiversity *= 0.7;
|
|
}},
|
|
{ msg: '🌋 Volcanic eruption reshapes the land', type: 'disaster', effect: () => {
|
|
WORLD.temperature += 2;
|
|
}},
|
|
{ msg: '🔬 Scientific breakthrough!', type: 'discovery', effect: () => {
|
|
WORLD.tech += 50;
|
|
}},
|
|
{ msg: '🌿 New species discovered!', type: 'discovery', effect: () => {
|
|
WORLD.biodiversity += 5;
|
|
WORLD.lifeForms += 10;
|
|
}},
|
|
{ msg: '🏛️ Golden age of civilization', type: 'discovery', effect: () => {
|
|
WORLD.population *= 1.1;
|
|
}},
|
|
];
|
|
|
|
const event = events[Math.floor(Math.random() * events.length)];
|
|
logEvent(event.msg, event.type);
|
|
event.effect();
|
|
}
|
|
|
|
// ==================== BUILD ACTIONS ====================
|
|
function build(type) {
|
|
const lat = (Math.random() - 0.5) * 100;
|
|
const lng = (Math.random() - 0.5) * 360;
|
|
|
|
switch (type) {
|
|
case 'tree':
|
|
for (let i = 0; i < 10; i++) {
|
|
spawnTree(lat + (Math.random() - 0.5) * 20, lng + (Math.random() - 0.5) * 20);
|
|
}
|
|
logEvent('🌲 Forest planted');
|
|
break;
|
|
case 'life':
|
|
WORLD.lifeForms += 100;
|
|
WORLD.biodiversity += 5;
|
|
logEvent('🦠 Life seeded in the oceans', 'evolution');
|
|
break;
|
|
case 'animal':
|
|
for (let i = 0; i < 5; i++) {
|
|
spawnAnimal(lat + (Math.random() - 0.5) * 30, lng + (Math.random() - 0.5) * 30);
|
|
}
|
|
logEvent('🐾 Animals introduced');
|
|
break;
|
|
case 'village':
|
|
spawnCity(lat, lng, 0.5);
|
|
logEvent('🏘️ Village founded');
|
|
break;
|
|
case 'city':
|
|
spawnCity(lat, lng, 1.5);
|
|
logEvent('🏙️ City built');
|
|
break;
|
|
case 'wonder':
|
|
spawnCity(lat, lng, 3);
|
|
WORLD.tech += 100;
|
|
logEvent('🏛️ Wonder of the world constructed!', 'discovery');
|
|
break;
|
|
case 'meteor':
|
|
triggerEvent();
|
|
break;
|
|
case 'terraform':
|
|
WORLD.temperature += (Math.random() - 0.5) * 5;
|
|
WORLD.seaLevel += (Math.random() - 0.5) * 10;
|
|
logEvent('🌋 Terraforming in progress', 'evolution');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ==================== UI ====================
|
|
function updateUI() {
|
|
const era = ERAS[WORLD.era];
|
|
const nextEra = ERAS[WORLD.era + 1];
|
|
|
|
document.getElementById('eraName').textContent = era.name;
|
|
document.getElementById('yearNum').textContent = Math.floor(WORLD.year).toLocaleString();
|
|
document.getElementById('eraProgress').style.width = WORLD.eraProgress + '%';
|
|
document.getElementById('nextEra').textContent = nextEra ? nextEra.name : 'Final Era';
|
|
|
|
document.getElementById('statTemp').textContent = WORLD.temperature.toFixed(1) + '°C';
|
|
document.getElementById('statO2').textContent = WORLD.oxygen.toFixed(1) + '%';
|
|
document.getElementById('statSea').textContent = WORLD.seaLevel.toFixed(0) + '%';
|
|
document.getElementById('statBio').textContent = WORLD.biodiversity.toFixed(0) + '%';
|
|
|
|
document.getElementById('resPop').textContent = WORLD.population.toLocaleString();
|
|
document.getElementById('resCities').textContent = cities.length;
|
|
document.getElementById('resTrees').textContent = trees.length;
|
|
document.getElementById('resTech').textContent = WORLD.tech;
|
|
|
|
const seasons = ['🌸 Spring', '☀️ Summer', '🍂 Autumn', '❄️ Winter'];
|
|
document.getElementById('seasonVal').textContent = seasons[WORLD.season];
|
|
document.getElementById('dayNightVal').textContent = WORLD.isNight ? '🌙 Night' : '☀️ Day';
|
|
document.getElementById('weatherVal').textContent = WORLD.weather === 'clear' ? '☀️ Clear' : '🌧️ Rain';
|
|
document.getElementById('lifeVal').textContent = WORLD.lifeForms.toLocaleString();
|
|
document.getElementById('ecoVal').textContent = WORLD.ecosystems;
|
|
}
|
|
|
|
function logEvent(msg, type = '') {
|
|
const container = document.getElementById('events');
|
|
const el = document.createElement('div');
|
|
el.className = 'event ' + type;
|
|
el.textContent = msg;
|
|
container.insertBefore(el, container.firstChild);
|
|
|
|
// Keep only last 5 events
|
|
while (container.children.length > 5) {
|
|
container.removeChild(container.lastChild);
|
|
}
|
|
|
|
setTimeout(() => el.remove(), 8000);
|
|
}
|
|
|
|
// ==================== EVENTS ====================
|
|
function setupEvents() {
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
renderer.domElement.addEventListener('mousedown', e => {
|
|
isDragging = true;
|
|
prevMouse = { x: e.clientX, y: e.clientY };
|
|
});
|
|
|
|
renderer.domElement.addEventListener('mousemove', e => {
|
|
if (!isDragging) return;
|
|
const dx = e.clientX - prevMouse.x;
|
|
const dy = e.clientY - prevMouse.y;
|
|
spherical.theta -= dx * 0.005;
|
|
spherical.phi = Math.max(0.2, Math.min(Math.PI - 0.2, spherical.phi + dy * 0.005));
|
|
updateCamera();
|
|
prevMouse = { x: e.clientX, y: e.clientY };
|
|
});
|
|
|
|
renderer.domElement.addEventListener('mouseup', () => isDragging = false);
|
|
renderer.domElement.addEventListener('mouseleave', () => isDragging = false);
|
|
|
|
renderer.domElement.addEventListener('wheel', e => {
|
|
spherical.radius = Math.max(150, Math.min(600, spherical.radius + e.deltaY * 0.3));
|
|
updateCamera();
|
|
});
|
|
|
|
// Touch
|
|
renderer.domElement.addEventListener('touchstart', e => {
|
|
isDragging = true;
|
|
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
}, { passive: true });
|
|
|
|
renderer.domElement.addEventListener('touchmove', e => {
|
|
if (!isDragging) return;
|
|
const dx = e.touches[0].clientX - prevMouse.x;
|
|
const dy = e.touches[0].clientY - prevMouse.y;
|
|
spherical.theta -= dx * 0.005;
|
|
spherical.phi = Math.max(0.2, Math.min(Math.PI - 0.2, spherical.phi + dy * 0.005));
|
|
updateCamera();
|
|
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
}, { passive: true });
|
|
|
|
renderer.domElement.addEventListener('touchend', () => isDragging = false);
|
|
|
|
// Keyboard
|
|
document.addEventListener('keydown', e => {
|
|
const num = parseInt(e.key);
|
|
if (num >= 1 && num <= 8) {
|
|
const types = ['tree', 'life', 'animal', 'village', 'city', 'wonder', 'meteor', 'terraform'];
|
|
build(types[num - 1]);
|
|
}
|
|
});
|
|
|
|
// Build buttons
|
|
document.querySelectorAll('.build-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => build(btn.dataset.build));
|
|
});
|
|
|
|
// Speed buttons
|
|
document.querySelectorAll('.speed-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
WORLD.speed = parseInt(btn.dataset.speed);
|
|
});
|
|
});
|
|
|
|
// Control buttons
|
|
document.getElementById('btnRotate').addEventListener('click', function() {
|
|
autoRotate = !autoRotate;
|
|
this.classList.toggle('active', autoRotate);
|
|
});
|
|
|
|
document.getElementById('btnClouds').addEventListener('click', function() {
|
|
showClouds = !showClouds;
|
|
clouds.visible = showClouds;
|
|
this.classList.toggle('active', showClouds);
|
|
});
|
|
|
|
document.getElementById('btnGrid').addEventListener('click', function() {
|
|
showGrid = !showGrid;
|
|
this.classList.toggle('active', showGrid);
|
|
// Could add grid overlay here
|
|
});
|
|
}
|
|
|
|
function updateCamera() {
|
|
camera.position.x = spherical.radius * Math.sin(spherical.phi) * Math.sin(spherical.theta);
|
|
camera.position.y = spherical.radius * Math.cos(spherical.phi);
|
|
camera.position.z = spherical.radius * Math.sin(spherical.phi) * Math.cos(spherical.theta);
|
|
camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
// ==================== ANIMATION ====================
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
time += 0.016;
|
|
|
|
// Auto rotate
|
|
if (autoRotate && !isDragging) {
|
|
spherical.theta += 0.001;
|
|
updateCamera();
|
|
}
|
|
|
|
// Evolve world
|
|
if (WORLD.speed > 0) {
|
|
evolve();
|
|
}
|
|
|
|
// Rotate earth
|
|
if (earth) earth.rotation.y += 0.0005 * WORLD.speed;
|
|
if (clouds) clouds.rotation.y += 0.0006 * WORLD.speed;
|
|
|
|
// Animate trees
|
|
trees.forEach(tree => {
|
|
tree.userData.sway += 0.02;
|
|
tree.rotation.z = Math.sin(tree.userData.sway) * 0.02;
|
|
});
|
|
|
|
// Animate animals
|
|
animals.forEach(animal => {
|
|
const d = animal.userData;
|
|
|
|
// Move toward target
|
|
if (Math.random() < 0.01) {
|
|
d.targetLat = d.lat + (Math.random() - 0.5) * 10;
|
|
d.targetLng = d.lng + (Math.random() - 0.5) * 10;
|
|
}
|
|
|
|
d.lat += (d.targetLat - d.lat) * d.speed;
|
|
d.lng += (d.targetLng - d.lng) * d.speed;
|
|
|
|
// Keep in bounds
|
|
d.lat = Math.max(-80, Math.min(80, d.lat));
|
|
|
|
// Update position
|
|
const pos = latLngToVector3(d.lat, d.lng, EARTH_RADIUS + 1.5);
|
|
animal.position.copy(pos);
|
|
animal.lookAt(0, 0, 0);
|
|
animal.rotateX(Math.PI / 2);
|
|
|
|
// Hop animation
|
|
d.hop += 0.1;
|
|
animal.position.addScaledVector(animal.position.clone().normalize(), Math.abs(Math.sin(d.hop)) * 0.3);
|
|
});
|
|
|
|
// City lights at night
|
|
if (WORLD.isNight) {
|
|
cities.forEach(city => {
|
|
city.children.forEach(building => {
|
|
if (building.material) {
|
|
building.material.emissive.setHex(0xffaa00);
|
|
building.material.emissiveIntensity = 0.5 + Math.sin(time * 5) * 0.1;
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
cities.forEach(city => {
|
|
city.children.forEach(building => {
|
|
if (building.material) {
|
|
building.material.emissive.setHex(0x000000);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Update UI
|
|
updateUI();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Start
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|