Files
blackroad-os-web/.trinity/redlight/templates/blackroad-world-v2.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

1983 lines
73 KiB
HTML

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