Files
blackroad-os-web/.trinity/redlight/templates/blackroad-earth-game.html
Alexa Louise f9ec2879ba 🌈 Add Light Trinity system (RedLight + GreenLight + YellowLight)
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
🌸
2025-12-23 15:47:25 -06:00

2010 lines
70 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 World — Earth Edition</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: 60px;
background: linear-gradient(180deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 100%);
display: flex;
align-items: center;
padding: 0 20px;
z-index: 100;
gap: 20px;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #FF1D6C, #F5A623);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.logo-text {
color: white;
font-weight: 600;
font-size: 18px;
}
.logo-sub {
color: rgba(255,255,255,0.5);
font-size: 11px;
}
/* Resources */
.resources {
display: flex;
gap: 12px;
margin-left: auto;
}
.resource {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255,255,255,0.1);
padding: 8px 14px;
border-radius: 20px;
color: white;
font-size: 13px;
backdrop-filter: blur(10px);
}
.resource-icon { font-size: 16px; }
.resource-value { font-weight: 600; }
/* Level */
.level-display {
display: flex;
align-items: center;
gap: 10px;
color: white;
background: rgba(255,255,255,0.1);
padding: 8px 16px;
border-radius: 20px;
backdrop-filter: blur(10px);
}
.level-badge {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #FF1D6C, #F5A623);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
}
.xp-bar {
width: 80px;
height: 6px;
background: rgba(255,255,255,0.2);
border-radius: 3px;
overflow: hidden;
}
.xp-fill {
height: 100%;
background: linear-gradient(90deg, #FF1D6C, #F5A623);
transition: width 0.3s;
}
/* === BIOME INFO === */
.biome-panel {
position: fixed;
top: 80px;
left: 20px;
background: rgba(0,0,0,0.85);
border-radius: 16px;
padding: 16px;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
min-width: 220px;
}
.biome-title {
color: white;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.biome-desc {
color: rgba(255,255,255,0.6);
font-size: 12px;
margin-bottom: 12px;
}
.biome-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.biome-stat {
background: rgba(255,255,255,0.05);
padding: 8px;
border-radius: 8px;
text-align: center;
}
.biome-stat-icon { font-size: 18px; }
.biome-stat-value { color: white; font-weight: 600; font-size: 14px; }
.biome-stat-label { color: rgba(255,255,255,0.5); font-size: 10px; }
/* === MINIMAP === */
.minimap {
position: fixed;
bottom: 100px;
left: 20px;
width: 180px;
height: 180px;
background: rgba(0,0,0,0.85);
border-radius: 16px;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
overflow: hidden;
}
.minimap-canvas {
width: 100%;
height: 100%;
}
.minimap-label {
position: absolute;
bottom: 4px;
left: 8px;
color: rgba(255,255,255,0.5);
font-size: 10px;
}
/* === 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: 50px;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
.build-btn {
width: 52px;
height: 52px;
background: rgba(255,255,255,0.08);
border: none;
border-radius: 12px;
font-size: 22px;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
position: relative;
}
.build-btn:hover { background: rgba(255,255,255,0.15); transform: translateY(-3px); }
.build-btn.active { background: #FF1D6C; }
.build-btn.locked { opacity: 0.3; cursor: not-allowed; }
.build-btn-cost {
font-size: 8px;
color: #F5A623;
position: absolute;
bottom: 3px;
}
.build-btn-key {
position: absolute;
top: 2px;
right: 4px;
font-size: 9px;
color: rgba(255,255,255,0.4);
}
/* === CONTROLS === */
.controls {
position: fixed;
bottom: 100px;
right: 20px;
display: flex;
flex-direction: column;
gap: 6px;
z-index: 100;
}
.ctrl-btn {
width: 44px;
height: 44px;
background: rgba(0,0,0,0.85);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
font-size: 18px;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(10px);
}
.ctrl-btn:hover { background: rgba(255,255,255,0.15); }
.ctrl-btn.active { background: #FF1D6C; border-color: #FF1D6C; }
/* === TIME === */
.time-display {
position: fixed;
top: 80px;
right: 20px;
background: rgba(0,0,0,0.85);
padding: 12px 20px;
border-radius: 16px;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
text-align: center;
}
.time-text { color: white; font-size: 28px; font-weight: 300; }
.day-text { color: rgba(255,255,255,0.5); font-size: 12px; }
.weather-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 8px;
color: white;
font-size: 13px;
}
/* === TERRAIN INFO === */
.terrain-info {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
padding: 10px 20px;
border-radius: 50px;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
display: flex;
gap: 20px;
color: white;
font-size: 13px;
}
.terrain-stat {
display: flex;
align-items: center;
gap: 6px;
}
/* === NOTIFICATIONS === */
.notifications {
position: fixed;
top: 140px;
right: 20px;
width: 260px;
z-index: 200;
pointer-events: none;
}
.notification {
background: rgba(0,0,0,0.9);
border-radius: 10px;
padding: 10px 14px;
margin-bottom: 6px;
color: white;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
animation: slideIn 0.3s ease;
border-left: 3px solid #4CAF50;
}
.notification.warning { border-left-color: #F5A623; }
@keyframes slideIn { from { transform: translateX(100px); opacity: 0; } }
/* === LOADING === */
.loading {
position: fixed;
inset: 0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
transition: opacity 0.8s;
}
.loading.hidden { opacity: 0; pointer-events: none; }
.loading-title { font-size: 36px; color: white; margin-bottom: 8px; font-weight: 300; }
.loading-sub { color: rgba(255,255,255,0.5); margin-bottom: 30px; font-size: 14px; }
.loading-spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: #FF1D6C;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-progress {
width: 200px;
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
margin-top: 20px;
overflow: hidden;
}
.loading-progress-fill {
height: 100%;
background: linear-gradient(90deg, #FF1D6C, #F5A623);
width: 0%;
transition: width 0.3s;
}
/* === COMPASS === */
.compass {
position: fixed;
top: 200px;
right: 20px;
width: 60px;
height: 60px;
background: rgba(0,0,0,0.85);
border-radius: 50%;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
justify-content: center;
}
.compass-needle {
width: 4px;
height: 30px;
background: linear-gradient(180deg, #FF1D6C 50%, white 50%);
border-radius: 2px;
transition: transform 0.1s;
}
.compass-label {
position: absolute;
color: rgba(255,255,255,0.5);
font-size: 10px;
font-weight: 600;
}
.compass-n { top: 4px; }
.compass-s { bottom: 4px; }
.compass-e { right: 6px; }
.compass-w { left: 6px; }
</style>
</head>
<body>
<!-- Loading -->
<div class="loading" id="loading">
<div class="loading-title">🌍 BlackRoad Earth</div>
<div class="loading-sub">Generating terrain...</div>
<div class="loading-spinner"></div>
<div class="loading-progress">
<div class="loading-progress-fill" id="loadingProgress"></div>
</div>
</div>
<div id="canvas-container"></div>
<!-- Top Bar -->
<div class="top-bar">
<div class="logo">
<div class="logo-icon">🌍</div>
<div>
<div class="logo-text">BlackRoad Earth</div>
<div class="logo-sub">Living World Simulator</div>
</div>
</div>
<div class="level-display">
<div class="level-badge" id="levelBadge">1</div>
<div>
<div style="font-size: 11px;">Level <span id="levelNum">1</span></div>
<div class="xp-bar">
<div class="xp-fill" id="xpFill" style="width: 0%"></div>
</div>
</div>
</div>
<div class="resources">
<div class="resource">
<span class="resource-icon">🪵</span>
<span class="resource-value" id="resWood">100</span>
</div>
<div class="resource">
<span class="resource-icon">🪨</span>
<span class="resource-value" id="resStone">50</span>
</div>
<div class="resource">
<span class="resource-icon">🌾</span>
<span class="resource-value" id="resFood">200</span>
</div>
<div class="resource">
<span class="resource-icon">💎</span>
<span class="resource-value" id="resGems">25</span>
</div>
</div>
</div>
<!-- Terrain Info -->
<div class="terrain-info">
<div class="terrain-stat">
<span>🏔️</span>
<span>Elevation: <strong id="elevationVal">0m</strong></span>
</div>
<div class="terrain-stat">
<span>🌡️</span>
<span>Temp: <strong id="tempVal">22°C</strong></span>
</div>
<div class="terrain-stat">
<span>🌲</span>
<span>Population: <strong id="popVal">0</strong></span>
</div>
</div>
<!-- Biome Panel -->
<div class="biome-panel">
<div class="biome-title" id="biomeTitle">🌲 Temperate Forest</div>
<div class="biome-desc" id="biomeDesc">Lush forests with moderate climate</div>
<div class="biome-stats">
<div class="biome-stat">
<div class="biome-stat-icon">🌳</div>
<div class="biome-stat-value" id="statTrees">0</div>
<div class="biome-stat-label">Trees</div>
</div>
<div class="biome-stat">
<div class="biome-stat-icon">🏠</div>
<div class="biome-stat-value" id="statHouses">0</div>
<div class="biome-stat-label">Houses</div>
</div>
<div class="biome-stat">
<div class="biome-stat-icon">🐰</div>
<div class="biome-stat-value" id="statAnimals">0</div>
<div class="biome-stat-label">Animals</div>
</div>
<div class="biome-stat">
<div class="biome-stat-icon">🤖</div>
<div class="biome-stat-value" id="statAgents">0</div>
<div class="biome-stat-label">Agents</div>
</div>
</div>
</div>
<!-- Minimap -->
<div class="minimap">
<canvas class="minimap-canvas" id="minimapCanvas" width="180" height="180"></canvas>
<div class="minimap-label">Terrain Map</div>
</div>
<!-- Time Display -->
<div class="time-display">
<div class="time-text" id="timeText">12:00</div>
<div class="day-text">Day <span id="dayNum">1</span><span id="seasonText">Spring</span></div>
<div class="weather-row">
<span id="weatherIcon">☀️</span>
<span id="weatherText">Clear</span>
<span id="tempDisplay">22°C</span>
</div>
</div>
<!-- Compass -->
<div class="compass">
<div class="compass-needle" id="compassNeedle"></div>
<span class="compass-label compass-n">N</span>
<span class="compass-label compass-s">S</span>
<span class="compass-label compass-e">E</span>
<span class="compass-label compass-w">W</span>
</div>
<!-- Build Bar -->
<div class="build-bar">
<button class="build-btn" data-build="tree" title="Plant Tree">
🌲<span class="build-btn-cost">5🪵</span><span class="build-btn-key">1</span>
</button>
<button class="build-btn" data-build="flower" title="Plant Flowers">
🌸<span class="build-btn-cost">2🌾</span><span class="build-btn-key">2</span>
</button>
<button class="build-btn" data-build="house" title="Build House">
🏠<span class="build-btn-cost">20🪵</span><span class="build-btn-key">3</span>
</button>
<button class="build-btn" data-build="farm" title="Build Farm">
🌾<span class="build-btn-cost">15🪵</span><span class="build-btn-key">4</span>
</button>
<button class="build-btn" data-build="mine" title="Build Mine">
⛏️<span class="build-btn-cost">25🪵</span><span class="build-btn-key">5</span>
</button>
<button class="build-btn" data-build="tower" title="Watch Tower">
🗼<span class="build-btn-cost">30🪨</span><span class="build-btn-key">6</span>
</button>
<button class="build-btn" data-build="agent" title="Spawn Agent">
🤖<span class="build-btn-cost">50💎</span><span class="build-btn-key">7</span>
</button>
<button class="build-btn" data-build="bridge" title="Build Bridge">
🌉<span class="build-btn-cost">40🪵</span><span class="build-btn-key">8</span>
</button>
<button class="build-btn locked" data-build="castle" title="Castle (Lvl 5)">
🏰<span class="build-btn-cost">200🪨</span><span class="build-btn-key">9</span>
</button>
</div>
<!-- Controls -->
<div class="controls">
<button class="ctrl-btn active" id="btnRotate" title="Auto Rotate">🔄</button>
<button class="ctrl-btn" id="btnDay" title="Day">☀️</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" id="btnSpeed" title="Speed x3"></button>
</div>
<!-- Notifications -->
<div class="notifications" id="notifications"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ==================== SIMPLEX NOISE ====================
// Fast simplex noise for terrain generation
class SimplexNoise {
constructor(seed = Math.random()) {
this.p = new Uint8Array(256);
for (let i = 0; i < 256; i++) this.p[i] = i;
// Fisher-Yates shuffle with seed
let n = seed * 2147483647;
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];
}
noise2D(x, y) {
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const s = (x + y) * F2;
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const t = (i + j) * G2;
const X0 = i - t;
const Y0 = j - t;
const x0 = x - X0;
const y0 = y - Y0;
let i1, j1;
if (x0 > y0) { i1 = 1; j1 = 0; }
else { i1 = 0; j1 = 1; }
const x1 = x0 - i1 + G2;
const y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2;
const y2 = y0 - 1 + 2 * G2;
const ii = i & 255;
const jj = j & 255;
const grad = (hash, x, y) => {
const h = hash & 7;
const u = h < 4 ? x : y;
const v = h < 4 ? y : x;
return ((h & 1) ? -u : u) + ((h & 2) ? -2 * v : 2 * v);
};
let n0 = 0, n1 = 0, n2 = 0;
let t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0) {
t0 *= t0;
n0 = t0 * t0 * grad(this.perm[ii + this.perm[jj]], x0, y0);
}
let t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 >= 0) {
t1 *= t1;
n1 = t1 * t1 * grad(this.perm[ii + i1 + this.perm[jj + j1]], x1, y1);
}
let t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 >= 0) {
t2 *= t2;
n2 = t2 * t2 * grad(this.perm[ii + 1 + this.perm[jj + 1]], x2, y2);
}
return 70 * (n0 + n1 + n2);
}
// Fractal Brownian Motion for more natural terrain
fbm(x, y, octaves = 6, lacunarity = 2, gain = 0.5) {
let sum = 0;
let amp = 1;
let freq = 1;
let max = 0;
for (let i = 0; i < octaves; i++) {
sum += this.noise2D(x * freq, y * freq) * amp;
max += amp;
amp *= gain;
freq *= lacunarity;
}
return sum / max;
}
}
// ==================== GAME STATE ====================
const GAME = {
level: 1,
xp: 0,
xpNeeded: 100,
day: 1,
hour: 12,
minute: 0,
speed: 1,
season: 'spring',
resources: {
wood: 100,
stone: 50,
food: 200,
gems: 25
},
counts: {
trees: 0,
flowers: 0,
houses: 0,
animals: 0,
agents: 0
}
};
// ==================== BIOMES ====================
const BIOMES = {
ocean: { name: 'Ocean', icon: '🌊', color: 0x1a5fb4, desc: 'Deep waters', temp: 15 },
beach: { name: 'Beach', icon: '🏖️', color: 0xf9e79f, desc: 'Sandy shores', temp: 25 },
plains: { name: 'Plains', icon: '🌾', color: 0x7daa4a, desc: 'Open grasslands', temp: 20 },
forest: { name: 'Forest', icon: '🌲', color: 0x2d5a27, desc: 'Dense woodland', temp: 18 },
hills: { name: 'Hills', icon: '⛰️', color: 0x6b8e23, desc: 'Rolling highlands', temp: 15 },
mountain: { name: 'Mountain', icon: '🏔️', color: 0x808080, desc: 'Rocky peaks', temp: 5 },
snow: { name: 'Snow Peak', icon: '🗻', color: 0xffffff, desc: 'Frozen summit', temp: -10 },
desert: { name: 'Desert', icon: '🏜️', color: 0xdaa520, desc: 'Arid wasteland', temp: 35 },
swamp: { name: 'Swamp', icon: '🌿', color: 0x556b2f, desc: 'Murky wetlands', temp: 22 },
tundra: { name: 'Tundra', icon: '❄️', color: 0xb0c4de, desc: 'Frozen plains', temp: -5 }
};
// ==================== THREE.JS ====================
let scene, camera, renderer;
let terrain, water;
let trees = [], flowers = [], houses = [], animals = [], agents = [];
let clouds = [], birds = [], rain = null, snow = null;
let noise;
let time = 0;
let autoRotate = true;
let isNight = false;
let weatherState = 'clear';
// Camera
let isDragging = false;
let prevMouse = { x: 0, y: 0 };
let camAngle = 0, camHeight = 80, camDist = 150;
// Terrain settings
const TERRAIN = {
size: 300,
segments: 200,
maxHeight: 60,
waterLevel: 0
};
// Initialize terrain data structure
let terrainData = {
heights: [],
biomes: [],
width: TERRAIN.segments + 1,
height: TERRAIN.segments + 1
};
function init() {
noise = new SimplexNoise(12345);
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.003);
// Camera
camera = new THREE.PerspectiveCamera(60, 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.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
document.getElementById('canvas-container').appendChild(renderer.domElement);
// Generate world
setLoadingProgress(10);
createLights();
setLoadingProgress(20);
generateTerrain();
setLoadingProgress(50);
createWater();
setLoadingProgress(60);
populateWorld();
setLoadingProgress(80);
createAtmosphere();
setLoadingProgress(90);
createMinimap();
setLoadingProgress(100);
// Events
setupEvents();
updateUI();
// Start
setTimeout(() => {
document.getElementById('loading').classList.add('hidden');
notify('🌍 Welcome to BlackRoad Earth!');
animate();
}, 500);
}
function setLoadingProgress(percent) {
document.getElementById('loadingProgress').style.width = percent + '%';
}
function createLights() {
// Ambient
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
// Sun
window.sunLight = new THREE.DirectionalLight(0xfffaf0, 1.2);
window.sunLight.position.set(200, 200, 100);
window.sunLight.castShadow = true;
window.sunLight.shadow.mapSize.set(4096, 4096);
window.sunLight.shadow.camera.near = 1;
window.sunLight.shadow.camera.far = 600;
window.sunLight.shadow.camera.left = -200;
window.sunLight.shadow.camera.right = 200;
window.sunLight.shadow.camera.top = 200;
window.sunLight.shadow.camera.bottom = -200;
window.sunLight.shadow.bias = -0.0005;
scene.add(window.sunLight);
// Hemisphere
window.hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x7CBA3D, 0.5);
scene.add(window.hemiLight);
}
function generateTerrain() {
const { size, segments, maxHeight, waterLevel } = TERRAIN;
const geo = new THREE.PlaneGeometry(size, size, segments, segments);
const pos = geo.attributes.position.array;
const colors = new Float32Array(pos.length);
// Generate heightmap with multiple noise layers
for (let i = 0; i <= segments; i++) {
terrainData.heights[i] = [];
terrainData.biomes[i] = [];
for (let j = 0; j <= segments; j++) {
const x = (j / segments - 0.5) * size;
const z = (i / segments - 0.5) * size;
// Continental shape - large scale
const continental = noise.fbm(x * 0.003, z * 0.003, 3, 2, 0.5);
// Mountain ranges - medium scale with ridges
const mountains = Math.pow(Math.abs(noise.fbm(x * 0.008, z * 0.008, 4, 2.5, 0.6)), 1.5) * 2;
// Hills - small scale variation
const hills = noise.fbm(x * 0.02, z * 0.02, 3, 2, 0.5) * 0.3;
// Detail - fine texture
const detail = noise.fbm(x * 0.05, z * 0.05, 2, 2, 0.5) * 0.1;
// Combine
let height = continental * 0.6 + mountains * 0.8 + hills + detail;
// Create some flat areas for building
const flatness = noise.fbm(x * 0.01, z * 0.01, 2, 2, 0.5);
if (flatness > 0.2 && height > 0 && height < 0.4) {
height = height * 0.3 + 0.15;
}
// Edge falloff for island feel
const dist = Math.sqrt(x * x + z * z) / (size * 0.5);
if (dist > 0.7) {
const falloff = 1 - Math.pow((dist - 0.7) / 0.3, 2);
height *= Math.max(0, falloff);
height -= (1 - falloff) * 0.5;
}
// Scale to world height
height *= maxHeight;
terrainData.heights[i][j] = height;
// Determine biome
let biome;
if (height < waterLevel - 5) biome = 'ocean';
else if (height < waterLevel + 1) biome = 'beach';
else if (height < 8) {
const moisture = noise.fbm(x * 0.01 + 100, z * 0.01, 2, 2, 0.5);
if (moisture < -0.3) biome = 'desert';
else if (moisture > 0.4) biome = 'swamp';
else biome = 'plains';
}
else if (height < 20) biome = 'forest';
else if (height < 35) biome = 'hills';
else if (height < 50) biome = 'mountain';
else biome = 'snow';
terrainData.biomes[i][j] = biome;
}
}
// Apply heights and colors to geometry
for (let i = 0; i < pos.length; i += 3) {
const idx = i / 3;
const row = Math.floor(idx / (segments + 1));
const col = idx % (segments + 1);
const height = terrainData.heights[row]?.[col] || 0;
const biome = terrainData.biomes[row]?.[col] || 'plains';
pos[i + 2] = height;
// Color based on biome
const biomeData = BIOMES[biome];
const color = new THREE.Color(biomeData.color);
// Add variation
const variation = noise.noise2D(pos[i] * 0.1, pos[i + 1] * 0.1) * 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();
// Material
const mat = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.85,
metalness: 0.05,
flatShading: false
});
terrain = new THREE.Mesh(geo, mat);
terrain.rotation.x = -Math.PI / 2;
terrain.receiveShadow = true;
terrain.castShadow = true;
scene.add(terrain);
}
function createWater() {
// Main ocean
const waterGeo = new THREE.PlaneGeometry(600, 600, 100, 100);
const waterMat = new THREE.MeshStandardMaterial({
color: 0x2196F3,
transparent: true,
opacity: 0.75,
roughness: 0.1,
metalness: 0.3
});
water = new THREE.Mesh(waterGeo, waterMat);
water.rotation.x = -Math.PI / 2;
water.position.y = TERRAIN.waterLevel;
water.receiveShadow = true;
scene.add(water);
// Add some wave animation data
water.userData = {
originalPositions: waterGeo.attributes.position.array.slice()
};
}
function populateWorld() {
const { segments } = TERRAIN;
// Populate based on biomes
for (let i = 0; i < segments; i += 4) {
for (let j = 0; j < segments; j += 4) {
const biome = terrainData.biomes[i]?.[j];
const height = terrainData.heights[i]?.[j] || 0;
if (!biome || height < TERRAIN.waterLevel + 1) continue;
const worldX = (j / segments - 0.5) * TERRAIN.size;
const worldZ = (i / segments - 0.5) * TERRAIN.size;
// Trees based on biome
if (Math.random() < getTreeDensity(biome)) {
const tree = createTree(biome);
tree.position.set(
worldX + (Math.random() - 0.5) * 10,
height,
worldZ + (Math.random() - 0.5) * 10
);
trees.push(tree);
scene.add(tree);
}
// Flowers in certain biomes
if ((biome === 'plains' || biome === 'forest') && Math.random() < 0.3) {
for (let f = 0; f < 3; f++) {
const flower = createFlower();
flower.position.set(
worldX + (Math.random() - 0.5) * 15,
height,
worldZ + (Math.random() - 0.5) * 15
);
flowers.push(flower);
scene.add(flower);
}
}
// Animals
if (Math.random() < 0.05 && biome !== 'ocean' && biome !== 'snow') {
const animal = createAnimal(biome);
animal.position.set(worldX, height + 0.3, worldZ);
animals.push(animal);
scene.add(animal);
}
}
}
// Add some initial houses in good spots
for (let i = 0; i < 12; i++) {
const pos = findBuildableSpot();
if (pos) {
const house = createHouse();
house.position.copy(pos);
houses.push(house);
scene.add(house);
}
}
// Initial agents
for (let i = 0; i < 8; i++) {
const pos = findBuildableSpot();
if (pos) {
const agent = createAgent();
agent.position.set(pos.x, pos.y + 1, pos.z);
agents.push(agent);
scene.add(agent);
}
}
// Birds
for (let i = 0; i < 25; i++) {
const bird = createBird();
birds.push(bird);
scene.add(bird);
}
updateCounts();
}
function getTreeDensity(biome) {
switch (biome) {
case 'forest': return 0.6;
case 'hills': return 0.3;
case 'plains': return 0.15;
case 'swamp': return 0.4;
case 'mountain': return 0.1;
case 'tundra': return 0.05;
default: return 0;
}
}
function createTree(biome) {
const group = new THREE.Group();
// Different tree types based on biome
let trunkColor = 0x8B4513;
let leafColor, leafType;
switch (biome) {
case 'forest':
leafColor = 0x228B22;
leafType = Math.random() > 0.5 ? 'pine' : 'oak';
break;
case 'hills':
case 'mountain':
leafColor = 0x2d5a27;
leafType = 'pine';
break;
case 'plains':
leafColor = 0x32CD32;
leafType = 'oak';
break;
case 'swamp':
leafColor = 0x556b2f;
leafType = 'willow';
trunkColor = 0x4a3728;
break;
case 'desert':
leafColor = 0x228B22;
leafType = 'palm';
break;
default:
leafColor = 0x32CD32;
leafType = 'oak';
}
const scale = 0.6 + Math.random() * 0.6;
// Trunk
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(0.15 * scale, 0.25 * scale, 2 * scale, 6),
new THREE.MeshStandardMaterial({ color: trunkColor })
);
trunk.position.y = scale;
trunk.castShadow = true;
group.add(trunk);
// Leaves based on type
if (leafType === 'pine') {
for (let i = 0; i < 4; i++) {
const cone = new THREE.Mesh(
new THREE.ConeGeometry((1.5 - i * 0.3) * scale, 1.5 * scale, 6),
new THREE.MeshStandardMaterial({ color: leafColor })
);
cone.position.y = (2 + i * 1) * scale;
cone.castShadow = true;
group.add(cone);
}
} else if (leafType === 'oak') {
const leaves = new THREE.Mesh(
new THREE.IcosahedronGeometry(1.5 * scale, 1),
new THREE.MeshStandardMaterial({ color: leafColor })
);
leaves.position.y = 3 * scale;
leaves.castShadow = true;
group.add(leaves);
} else if (leafType === 'palm') {
trunk.geometry = new THREE.CylinderGeometry(0.1 * scale, 0.2 * scale, 3 * scale, 6);
trunk.position.y = 1.5 * scale;
for (let i = 0; i < 6; i++) {
const frond = new THREE.Mesh(
new THREE.ConeGeometry(0.2 * scale, 2 * scale, 4),
new THREE.MeshStandardMaterial({ color: leafColor })
);
frond.position.y = 3 * scale;
frond.rotation.z = Math.PI / 4;
frond.rotation.y = (i / 6) * Math.PI * 2;
group.add(frond);
}
} else if (leafType === 'willow') {
const leaves = new THREE.Mesh(
new THREE.SphereGeometry(1.2 * scale, 8, 8),
new THREE.MeshStandardMaterial({ color: leafColor })
);
leaves.scale.y = 1.5;
leaves.position.y = 2.5 * scale;
leaves.castShadow = true;
group.add(leaves);
}
group.userData = { type: 'tree', sway: Math.random() * Math.PI * 2 };
return group;
}
function createFlower() {
const group = new THREE.Group();
const colors = [0xFF69B4, 0xFFD700, 0xFF6347, 0x9370DB, 0x00CED1, 0xFFFFFF, 0xFF1D6C];
const color = colors[Math.floor(Math.random() * colors.length)];
const stem = new THREE.Mesh(
new THREE.CylinderGeometry(0.02, 0.02, 0.4, 4),
new THREE.MeshStandardMaterial({ color: 0x228B22 })
);
stem.position.y = 0.2;
group.add(stem);
for (let i = 0; i < 5; i++) {
const petal = new THREE.Mesh(
new THREE.SphereGeometry(0.1, 6, 6),
new THREE.MeshStandardMaterial({ color })
);
petal.scale.set(1, 0.3, 0.5);
petal.position.y = 0.4;
petal.position.x = Math.cos((i / 5) * Math.PI * 2) * 0.1;
petal.position.z = Math.sin((i / 5) * Math.PI * 2) * 0.1;
group.add(petal);
}
const center = new THREE.Mesh(
new THREE.SphereGeometry(0.06, 6, 6),
new THREE.MeshStandardMaterial({ color: 0xFFD700 })
);
center.position.y = 0.4;
group.add(center);
group.scale.setScalar(0.5 + Math.random() * 0.5);
group.userData = { type: 'flower', sway: Math.random() * Math.PI * 2 };
return group;
}
function createHouse() {
const group = new THREE.Group();
const roofColors = [0xE74C3C, 0x3498DB, 0xFF1D6C, 0xF5A623, 0x9C27B0, 0x27ae60];
const scale = 0.8 + Math.random() * 0.4;
// Walls
const walls = new THREE.Mesh(
new THREE.BoxGeometry(3 * scale, 2.5 * scale, 3 * scale),
new THREE.MeshStandardMaterial({ color: 0xFAF0E6 })
);
walls.position.y = 1.25 * scale;
walls.castShadow = true;
walls.receiveShadow = true;
group.add(walls);
// Roof
const roof = new THREE.Mesh(
new THREE.ConeGeometry(2.5 * scale, 1.5 * scale, 4),
new THREE.MeshStandardMaterial({ color: roofColors[Math.floor(Math.random() * roofColors.length)] })
);
roof.position.y = 3.25 * scale;
roof.rotation.y = Math.PI / 4;
roof.castShadow = true;
group.add(roof);
// Door
const door = new THREE.Mesh(
new THREE.BoxGeometry(0.5 * scale, 1 * scale, 0.1),
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
);
door.position.set(0, 0.5 * scale, 1.55 * scale);
group.add(door);
// Windows
const windowMat = new THREE.MeshStandardMaterial({ color: 0x87CEEB, emissive: 0x000000 });
[[-0.7, 1.5, 1.55], [0.7, 1.5, 1.55]].forEach(([x, y, z]) => {
const win = new THREE.Mesh(
new THREE.BoxGeometry(0.4 * scale, 0.4 * scale, 0.1),
windowMat.clone()
);
win.position.set(x * scale, y * scale, z * scale);
group.add(win);
});
// Chimney
const chimney = new THREE.Mesh(
new THREE.BoxGeometry(0.4 * scale, 1 * scale, 0.4 * scale),
new THREE.MeshStandardMaterial({ color: 0x8B0000 })
);
chimney.position.set(0.8 * scale, 3.5 * scale, 0);
chimney.castShadow = true;
group.add(chimney);
group.rotation.y = Math.random() * Math.PI * 2;
group.userData = { type: 'house' };
return group;
}
function createAnimal(biome) {
const group = new THREE.Group();
let color, scale = 0.4 + Math.random() * 0.3;
switch (biome) {
case 'forest': color = [0x8B4513, 0xFFFFFF, 0x808080][Math.floor(Math.random() * 3)]; break;
case 'plains': color = [0xD2B48C, 0xFFFFFF, 0x000000][Math.floor(Math.random() * 3)]; break;
case 'hills': color = [0x808080, 0xFFFFFF][Math.floor(Math.random() * 2)]; break;
case 'swamp': color = 0x228B22; break;
default: color = 0xD2B48C;
}
const mat = new THREE.MeshStandardMaterial({ color });
// Body
const body = new THREE.Mesh(new THREE.SphereGeometry(0.3 * scale, 8, 8), mat);
body.scale.set(1, 0.7, 1.2);
group.add(body);
// Head
const head = new THREE.Mesh(new THREE.SphereGeometry(0.2 * scale, 8, 8), mat);
head.position.set(0, 0.15 * scale, 0.3 * scale);
group.add(head);
// Eyes
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
[[-0.06, 0.22, 0.45], [0.06, 0.22, 0.45]].forEach(([x, y, z]) => {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.03 * scale, 6, 6), eyeMat);
eye.position.set(x * scale, y * scale, z * scale);
group.add(eye);
});
// Ears
[[-0.08, 0.32, 0.25], [0.08, 0.32, 0.25]].forEach(([x, y, z]) => {
const ear = new THREE.Mesh(new THREE.ConeGeometry(0.05 * scale, 0.15 * scale, 6), mat);
ear.position.set(x * scale, y * scale, z * scale);
group.add(ear);
});
group.userData = {
type: 'animal',
targetX: group.position.x,
targetZ: group.position.z,
speed: 0.02 + Math.random() * 0.02,
hop: Math.random() * Math.PI * 2,
idle: 0
};
return group;
}
function createAgent() {
const group = new THREE.Group();
const colors = [0xFF1D6C, 0x2979FF, 0xF5A623, 0x9C27B0];
const color = colors[Math.floor(Math.random() * colors.length)];
// Body
const body = new THREE.Mesh(
new THREE.CylinderGeometry(0.25, 0.35, 0.7, 8),
new THREE.MeshStandardMaterial({ color, metalness: 0.5, roughness: 0.3 })
);
body.position.y = 0.35;
group.add(body);
// Head
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.25, 12, 12),
new THREE.MeshStandardMaterial({ color: 0xffffff, metalness: 0.3 })
);
head.position.y = 0.85;
group.add(head);
// Eye
const eye = new THREE.Mesh(
new THREE.CircleGeometry(0.12, 16),
new THREE.MeshBasicMaterial({ color: 0xFF1D6C })
);
eye.position.set(0, 0.9, 0.23);
group.add(eye);
// Antenna
const antenna = new THREE.Mesh(
new THREE.CylinderGeometry(0.02, 0.02, 0.2, 6),
new THREE.MeshStandardMaterial({ color: 0x333333 })
);
antenna.position.set(0, 1.15, 0);
group.add(antenna);
const ball = new THREE.Mesh(
new THREE.SphereGeometry(0.05, 8, 8),
new THREE.MeshBasicMaterial({ color: 0xFF1D6C })
);
ball.position.set(0, 1.3, 0);
ball.userData = { glow: 0 };
group.add(ball);
// Hover ring
const ring = new THREE.Mesh(
new THREE.TorusGeometry(0.35, 0.03, 6, 24),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 })
);
ring.rotation.x = Math.PI / 2;
ring.position.y = -0.1;
group.add(ring);
group.userData = {
type: 'agent',
targetX: 0,
targetZ: 0,
speed: 0.03 + Math.random() * 0.02,
hover: Math.random() * Math.PI * 2,
ball
};
return group;
}
function createBird() {
const group = new THREE.Group();
const colors = [0x4169E1, 0xFF6347, 0xFFD700, 0x32CD32, 0x9370DB];
const mat = new THREE.MeshStandardMaterial({ color: colors[Math.floor(Math.random() * colors.length)] });
const body = new THREE.Mesh(new THREE.SphereGeometry(0.15, 6, 6), mat);
body.scale.set(1, 0.7, 1.3);
group.add(body);
const wing = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.04, 0.2), mat);
wing.position.y = 0.05;
wing.userData = { flap: Math.random() * Math.PI * 2 };
group.add(wing);
const beak = new THREE.Mesh(
new THREE.ConeGeometry(0.04, 0.12, 4),
new THREE.MeshStandardMaterial({ color: 0xFFA500 })
);
beak.position.set(0, 0, 0.18);
beak.rotation.x = Math.PI / 2;
group.add(beak);
group.position.set(
(Math.random() - 0.5) * 200,
20 + Math.random() * 30,
(Math.random() - 0.5) * 200
);
group.userData = {
type: 'bird',
angle: Math.random() * Math.PI * 2,
radius: 30 + Math.random() * 60,
speed: 0.004 + Math.random() * 0.008,
wing
};
return group;
}
function createAtmosphere() {
// Clouds
for (let i = 0; i < 20; i++) {
const cloud = createCloud();
clouds.push(cloud);
scene.add(cloud);
}
// Rain system
const rainCount = 8000;
const rainGeo = new THREE.BufferGeometry();
const rainPos = new Float32Array(rainCount * 3);
const rainVel = new Float32Array(rainCount);
for (let i = 0; i < rainCount; i++) {
rainPos[i * 3] = (Math.random() - 0.5) * 300;
rainPos[i * 3 + 1] = Math.random() * 100;
rainPos[i * 3 + 2] = (Math.random() - 0.5) * 300;
rainVel[i] = 0.5 + Math.random() * 0.5;
}
rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPos, 3));
rain = new THREE.Points(rainGeo, new THREE.PointsMaterial({ color: 0x8888ff, size: 0.15, transparent: true, opacity: 0.6 }));
rain.userData = { velocities: rainVel };
rain.visible = false;
scene.add(rain);
// Snow system
const snowCount = 5000;
const snowGeo = new THREE.BufferGeometry();
const snowPos = new Float32Array(snowCount * 3);
const snowVel = new Float32Array(snowCount * 2);
for (let i = 0; i < snowCount; i++) {
snowPos[i * 3] = (Math.random() - 0.5) * 300;
snowPos[i * 3 + 1] = Math.random() * 100;
snowPos[i * 3 + 2] = (Math.random() - 0.5) * 300;
snowVel[i * 2] = 0.1 + Math.random() * 0.1;
snowVel[i * 2 + 1] = (Math.random() - 0.5) * 0.02;
}
snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPos, 3));
snow = new THREE.Points(snowGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.3, transparent: true, opacity: 0.8 }));
snow.userData = { velocities: snowVel };
snow.visible = false;
scene.add(snow);
}
function createCloud() {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.85 });
const puffs = 4 + Math.floor(Math.random() * 4);
for (let i = 0; i < puffs; i++) {
const puff = new THREE.Mesh(
new THREE.SphereGeometry(2 + Math.random() * 2, 8, 8),
mat
);
puff.position.set(
(Math.random() - 0.5) * 6,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 4
);
group.add(puff);
}
group.position.set(
(Math.random() - 0.5) * 300,
40 + Math.random() * 30,
(Math.random() - 0.5) * 300
);
group.userData = { speed: 0.05 + Math.random() * 0.1 };
return group;
}
function findBuildableSpot() {
const { segments, size, waterLevel } = TERRAIN;
for (let attempt = 0; attempt < 50; attempt++) {
const i = Math.floor(Math.random() * segments);
const j = Math.floor(Math.random() * segments);
const height = terrainData.heights[i]?.[j];
const biome = terrainData.biomes[i]?.[j];
if (height > waterLevel + 2 && height < 30 &&
(biome === 'plains' || biome === 'forest' || biome === 'hills')) {
const x = (j / segments - 0.5) * size;
const z = (i / segments - 0.5) * size;
return new THREE.Vector3(x, height, z);
}
}
return null;
}
function getHeightAt(x, z) {
const { segments, size } = TERRAIN;
const i = Math.floor((z / size + 0.5) * segments);
const j = Math.floor((x / size + 0.5) * segments);
return terrainData.heights[i]?.[j] || 0;
}
function getBiomeAt(x, z) {
const { segments, size } = TERRAIN;
const i = Math.floor((z / size + 0.5) * segments);
const j = Math.floor((x / size + 0.5) * segments);
return terrainData.biomes[i]?.[j] || 'plains';
}
function createMinimap() {
const canvas = document.getElementById('minimapCanvas');
const ctx = canvas.getContext('2d');
const { segments } = TERRAIN;
const scale = 180 / segments;
for (let i = 0; i < segments; i++) {
for (let j = 0; j < segments; j++) {
const biome = terrainData.biomes[i]?.[j] || 'ocean';
const color = BIOMES[biome].color;
ctx.fillStyle = '#' + color.toString(16).padStart(6, '0');
ctx.fillRect(j * scale, i * scale, scale + 1, scale + 1);
}
}
}
// ==================== BUILD SYSTEM ====================
const BUILD_COSTS = {
tree: { wood: 5 },
flower: { food: 2 },
house: { wood: 20, stone: 10 },
farm: { wood: 15, stone: 5 },
mine: { wood: 25, stone: 10 },
tower: { stone: 30 },
agent: { gems: 50 },
bridge: { wood: 40 },
castle: { wood: 100, stone: 200 }
};
function canAfford(type) {
const costs = BUILD_COSTS[type];
if (!costs) return false;
for (const [res, amount] of Object.entries(costs)) {
if (GAME.resources[res] < amount) return false;
}
return true;
}
function spendResources(type) {
const costs = BUILD_COSTS[type];
for (const [res, amount] of Object.entries(costs)) {
GAME.resources[res] -= amount;
}
updateUI();
}
function build(type) {
if (!canAfford(type)) {
notify('⚠️ Not enough resources!', 'warning');
return;
}
if (type === 'castle' && GAME.level < 5) {
notify('🔒 Unlock at Level 5', 'warning');
return;
}
const pos = findBuildableSpot();
if (!pos) {
notify('⚠️ No suitable location!', 'warning');
return;
}
spendResources(type);
switch (type) {
case 'tree':
const tree = createTree(getBiomeAt(pos.x, pos.z));
tree.position.copy(pos);
trees.push(tree);
scene.add(tree);
notify('🌲 Tree planted!');
addXP(5);
break;
case 'flower':
for (let i = 0; i < 5; i++) {
const flower = createFlower();
flower.position.set(pos.x + (Math.random() - 0.5) * 5, pos.y, pos.z + (Math.random() - 0.5) * 5);
flowers.push(flower);
scene.add(flower);
}
notify('🌸 Flowers planted!');
addXP(3);
break;
case 'house':
const house = createHouse();
house.position.copy(pos);
houses.push(house);
scene.add(house);
notify('🏠 House built!');
addXP(25);
break;
case 'farm':
notify('🌾 Farm built! +5 food/cycle');
addXP(20);
break;
case 'mine':
notify('⛏️ Mine built! +3 stone/cycle');
addXP(20);
break;
case 'tower':
notify('🗼 Watch tower built!');
addXP(30);
break;
case 'agent':
const agent = createAgent();
agent.position.set(pos.x, pos.y + 1, pos.z);
agents.push(agent);
scene.add(agent);
notify('🤖 Agent deployed!');
addXP(50);
break;
case 'bridge':
notify('🌉 Bridge constructed!');
addXP(35);
break;
case 'castle':
notify('🏰 Castle built!');
addXP(200);
break;
}
updateCounts();
}
function addXP(amount) {
GAME.xp += amount;
while (GAME.xp >= GAME.xpNeeded) {
GAME.xp -= GAME.xpNeeded;
GAME.level++;
GAME.xpNeeded = Math.floor(GAME.xpNeeded * 1.5);
GAME.resources.wood += 50;
GAME.resources.stone += 30;
GAME.resources.gems += 15;
notify(`🎉 Level ${GAME.level}! Rewards added.`);
if (GAME.level >= 5) {
document.querySelector('[data-build="castle"]')?.classList.remove('locked');
}
}
updateUI();
}
function updateCounts() {
GAME.counts.trees = trees.length;
GAME.counts.flowers = flowers.length;
GAME.counts.houses = houses.length;
GAME.counts.animals = animals.length;
GAME.counts.agents = agents.length;
document.getElementById('statTrees').textContent = GAME.counts.trees;
document.getElementById('statHouses').textContent = GAME.counts.houses;
document.getElementById('statAnimals').textContent = GAME.counts.animals;
document.getElementById('statAgents').textContent = GAME.counts.agents;
document.getElementById('popVal').textContent = GAME.counts.trees + GAME.counts.houses + GAME.counts.animals + GAME.counts.agents;
}
// ==================== TIME & WEATHER ====================
function updateTime() {
GAME.minute += 0.5 * GAME.speed;
if (GAME.minute >= 60) {
GAME.minute = 0;
GAME.hour++;
GAME.resources.food = Math.min(999, GAME.resources.food + 2);
GAME.resources.wood = Math.min(999, GAME.resources.wood + 1);
GAME.resources.stone = Math.min(999, GAME.resources.stone + 1);
if (GAME.hour >= 24) {
GAME.hour = 0;
GAME.day++;
if (GAME.day % 30 === 1) {
const seasons = ['Spring', 'Summer', 'Autumn', 'Winter'];
GAME.season = seasons[(Math.floor(GAME.day / 30)) % 4];
}
GAME.resources.gems += 2;
notify(`☀️ Day ${GAME.day}!`);
}
if (GAME.hour === 20) setNight(true);
else if (GAME.hour === 6) setNight(false);
}
updateUI();
}
function setNight(night) {
isNight = night;
if (night) {
scene.background = new THREE.Color(0x0a0a1a);
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.004);
window.sunLight.intensity = 0.1;
window.sunLight.color.setHex(0x4444aa);
window.hemiLight.intensity = 0.1;
} else {
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.003);
window.sunLight.intensity = 1.2;
window.sunLight.color.setHex(0xfffaf0);
window.hemiLight.intensity = 0.5;
}
document.getElementById('btnDay').classList.toggle('active', !night);
document.getElementById('btnNight').classList.toggle('active', night);
}
function setWeather(type) {
rain.visible = type === 'rain';
snow.visible = type === 'snow';
weatherState = type;
document.getElementById('btnRain').classList.toggle('active', type === 'rain');
document.getElementById('btnSnow').classList.toggle('active', type === 'snow');
if (type === 'rain') notify('🌧️ Rain started!');
else if (type === 'snow') notify('❄️ Snow falling!');
else notify('☀️ Weather cleared!');
}
function updateUI() {
document.getElementById('resWood').textContent = GAME.resources.wood;
document.getElementById('resStone').textContent = GAME.resources.stone;
document.getElementById('resFood').textContent = GAME.resources.food;
document.getElementById('resGems').textContent = GAME.resources.gems;
document.getElementById('levelBadge').textContent = GAME.level;
document.getElementById('levelNum').textContent = GAME.level;
document.getElementById('xpFill').style.width = (GAME.xp / GAME.xpNeeded * 100) + '%';
const h = GAME.hour.toString().padStart(2, '0');
const m = Math.floor(GAME.minute).toString().padStart(2, '0');
document.getElementById('timeText').textContent = `${h}:${m}`;
document.getElementById('dayNum').textContent = GAME.day;
document.getElementById('seasonText').textContent = GAME.season;
document.getElementById('weatherIcon').textContent = weatherState === 'rain' ? '🌧️' : weatherState === 'snow' ? '❄️' : (isNight ? '🌙' : '☀️');
document.getElementById('weatherText').textContent = weatherState === 'rain' ? 'Rainy' : weatherState === 'snow' ? 'Snowy' : (isNight ? 'Clear Night' : 'Clear');
const temp = GAME.season === 'Winter' ? -5 : GAME.season === 'Summer' ? 28 : 18;
document.getElementById('tempDisplay').textContent = temp + '°C';
document.getElementById('tempVal').textContent = temp + '°C';
}
function notify(message, type = 'success') {
const container = document.getElementById('notifications');
const el = document.createElement('div');
el.className = 'notification ' + type;
el.textContent = message;
container.appendChild(el);
setTimeout(() => el.remove(), 3000);
}
// ==================== 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;
camAngle += dx * 0.005;
camHeight = Math.max(20, Math.min(150, camHeight - dy * 0.3));
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 => {
camDist = Math.max(50, Math.min(350, camDist + e.deltaY * 0.15));
updateCamera();
});
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;
camAngle += dx * 0.005;
camHeight = Math.max(20, Math.min(150, camHeight - dy * 0.3));
updateCamera();
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}, { passive: true });
renderer.domElement.addEventListener('touchend', () => isDragging = false);
document.addEventListener('keydown', e => {
const num = parseInt(e.key);
if (num >= 1 && num <= 9) {
const types = ['tree', 'flower', 'house', 'farm', 'mine', 'tower', 'agent', 'bridge', 'castle'];
build(types[num - 1]);
}
});
document.querySelectorAll('.build-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (!btn.classList.contains('locked')) {
build(btn.dataset.build);
}
});
});
document.getElementById('btnRotate').addEventListener('click', function() {
autoRotate = !autoRotate;
this.classList.toggle('active', autoRotate);
});
document.getElementById('btnDay').addEventListener('click', () => setNight(false));
document.getElementById('btnNight').addEventListener('click', () => setNight(true));
document.getElementById('btnRain').addEventListener('click', () => setWeather(weatherState === 'rain' ? 'clear' : 'rain'));
document.getElementById('btnSnow').addEventListener('click', () => setWeather(weatherState === 'snow' ? 'clear' : 'snow'));
document.getElementById('btnSpeed').addEventListener('click', function() {
GAME.speed = GAME.speed === 1 ? 3 : 1;
this.classList.toggle('active', GAME.speed > 1);
});
}
function updateCamera() {
camera.position.x = Math.sin(camAngle) * camDist;
camera.position.z = Math.cos(camAngle) * camDist;
camera.position.y = camHeight;
camera.lookAt(0, 10, 0);
// Compass
const needle = document.getElementById('compassNeedle');
if (needle) needle.style.transform = `rotate(${-camAngle * 180 / Math.PI}deg)`;
// Elevation display
const centerHeight = getHeightAt(0, 0);
document.getElementById('elevationVal').textContent = Math.floor(centerHeight * 10) + 'm';
}
// ==================== ANIMATION ====================
function animate() {
requestAnimationFrame(animate);
time += 0.016;
if (autoRotate && !isDragging) {
camAngle += 0.001;
updateCamera();
}
updateTime();
// Trees sway
trees.forEach(t => {
if (t.userData.sway !== undefined) {
t.userData.sway += 0.01;
t.rotation.z = Math.sin(t.userData.sway) * 0.015;
}
});
// Flowers sway
flowers.forEach(f => {
if (f.userData.sway !== undefined) {
f.userData.sway += 0.015;
f.rotation.z = Math.sin(f.userData.sway) * 0.03;
}
});
// Animals
animals.forEach(a => {
const d = a.userData;
d.idle += 0.016;
if (d.idle > 2 + Math.random() * 2) {
d.idle = 0;
const angle = Math.random() * Math.PI * 2;
const dist = 5 + Math.random() * 15;
d.targetX = a.position.x + Math.cos(angle) * dist;
d.targetZ = a.position.z + Math.sin(angle) * dist;
// Keep on land
const maxDist = 120;
const targetDist = Math.sqrt(d.targetX ** 2 + d.targetZ ** 2);
if (targetDist > maxDist) {
d.targetX *= maxDist / targetDist;
d.targetZ *= maxDist / targetDist;
}
}
const dx = d.targetX - a.position.x;
const dz = d.targetZ - a.position.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist > 0.5) {
a.position.x += (dx / dist) * d.speed;
a.position.z += (dz / dist) * d.speed;
a.rotation.y = Math.atan2(dx, dz);
d.hop += 0.12;
const h = getHeightAt(a.position.x, a.position.z);
a.position.y = h + 0.3 + Math.abs(Math.sin(d.hop)) * 0.15;
}
});
// Agents
agents.forEach(ag => {
const d = ag.userData;
d.hover += 0.03;
const h = getHeightAt(ag.position.x, ag.position.z);
ag.position.y = h + 1.2 + Math.sin(d.hover) * 0.2;
if (Math.random() < 0.003) {
const angle = Math.random() * Math.PI * 2;
const dist = 20 + Math.random() * 30;
d.targetX = Math.cos(angle) * dist;
d.targetZ = Math.sin(angle) * dist;
}
const dx = d.targetX - ag.position.x;
const dz = d.targetZ - ag.position.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist > 1) {
ag.position.x += (dx / dist) * d.speed;
ag.position.z += (dz / dist) * d.speed;
ag.rotation.y = Math.atan2(dx, dz);
}
if (d.ball) {
d.ball.userData.glow += 0.08;
d.ball.material.color.setHSL((Math.sin(d.ball.userData.glow) + 1) * 0.15, 1, 0.5);
}
});
// Birds
birds.forEach(b => {
const d = b.userData;
d.angle += d.speed;
b.position.x = Math.cos(d.angle) * d.radius;
b.position.z = Math.sin(d.angle) * d.radius;
b.rotation.y = d.angle + Math.PI / 2;
if (d.wing) {
d.wing.userData.flap += 0.2;
d.wing.rotation.z = Math.sin(d.wing.userData.flap) * 0.35;
}
});
// Clouds
clouds.forEach(c => {
c.position.x += c.userData.speed;
if (c.position.x > 200) c.position.x = -200;
});
// Water waves
if (water.userData.originalPositions) {
const pos = water.geometry.attributes.position.array;
const orig = water.userData.originalPositions;
for (let i = 0; i < pos.length; i += 3) {
const x = orig[i];
const y = orig[i + 1];
pos[i + 2] = Math.sin(x * 0.05 + time) * 0.3 + Math.sin(y * 0.05 + time * 0.8) * 0.2;
}
water.geometry.attributes.position.needsUpdate = true;
}
// Rain
if (rain.visible) {
const pos = rain.geometry.attributes.position.array;
const vel = rain.userData.velocities;
for (let i = 0; i < pos.length / 3; i++) {
pos[i * 3 + 1] -= vel[i];
if (pos[i * 3 + 1] < 0) {
pos[i * 3 + 1] = 100;
pos[i * 3] = (Math.random() - 0.5) * 300;
pos[i * 3 + 2] = (Math.random() - 0.5) * 300;
}
}
rain.geometry.attributes.position.needsUpdate = true;
}
// Snow
if (snow.visible) {
const pos = snow.geometry.attributes.position.array;
const vel = snow.userData.velocities;
for (let i = 0; i < pos.length / 3; i++) {
pos[i * 3 + 1] -= vel[i * 2];
pos[i * 3] += vel[i * 2 + 1];
if (pos[i * 3 + 1] < 0) {
pos[i * 3 + 1] = 100;
pos[i * 3] = (Math.random() - 0.5) * 300;
pos[i * 3 + 2] = (Math.random() - 0.5) * 300;
}
}
snow.geometry.attributes.position.needsUpdate = true;
}
renderer.render(scene, camera);
}
// Start
init();
</script>
</body>
</html>