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 🌸✨
1720 lines
61 KiB
HTML
1720 lines
61 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: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
|
|
}
|
|
|
|
#canvas-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Logo */
|
|
.logo {
|
|
position: fixed;
|
|
top: 24px;
|
|
left: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
z-index: 100;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 12px 20px;
|
|
border-radius: 50px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.logo-mark {
|
|
width: 36px;
|
|
height: 36px;
|
|
}
|
|
|
|
.logo-mark svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.road-dashes {
|
|
animation: spin 10s linear infinite;
|
|
transform-origin: 50px 50px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.logo-sub {
|
|
font-size: 10px;
|
|
color: #999;
|
|
}
|
|
|
|
/* Time & Weather */
|
|
.time-weather {
|
|
position: fixed;
|
|
top: 24px;
|
|
right: 24px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 16px 24px;
|
|
border-radius: 20px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
text-align: right;
|
|
z-index: 100;
|
|
}
|
|
|
|
.time-display {
|
|
font-size: 32px;
|
|
font-weight: 300;
|
|
color: #333;
|
|
}
|
|
|
|
.weather-display {
|
|
font-size: 14px;
|
|
color: #666;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Stats Panel */
|
|
.stats-panel {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
left: 24px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 20px;
|
|
border-radius: 20px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
z-index: 100;
|
|
}
|
|
|
|
.stats-title {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: #999;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 24px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #FF1D6C;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 10px;
|
|
color: #999;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Controls */
|
|
.controls {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
display: flex;
|
|
gap: 8px;
|
|
z-index: 100;
|
|
}
|
|
|
|
.control-btn {
|
|
width: 48px;
|
|
height: 48px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border: none;
|
|
border-radius: 50%;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.control-btn:hover {
|
|
transform: scale(1.1);
|
|
background: #FF1D6C;
|
|
}
|
|
|
|
.control-btn.active {
|
|
background: #FF1D6C;
|
|
}
|
|
|
|
/* Population Panel */
|
|
.population-panel {
|
|
position: fixed;
|
|
top: 24px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 12px 32px;
|
|
border-radius: 50px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
z-index: 100;
|
|
display: flex;
|
|
gap: 32px;
|
|
}
|
|
|
|
.pop-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.pop-icon {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.pop-count {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
/* Message */
|
|
.message {
|
|
position: fixed;
|
|
bottom: 100px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 16px 32px;
|
|
border-radius: 50px;
|
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.15);
|
|
z-index: 100;
|
|
font-size: 14px;
|
|
color: #333;
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.message.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Loading */
|
|
.loading {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #87CEEB 0%, #98FB98 50%, #87CEEB 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
transition: opacity 1s ease, visibility 1s ease;
|
|
}
|
|
|
|
.loading.hidden {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.loading-icon {
|
|
font-size: 80px;
|
|
animation: bounce 1s ease infinite;
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-20px); }
|
|
}
|
|
|
|
.loading-text {
|
|
margin-top: 24px;
|
|
font-size: 18px;
|
|
color: #333;
|
|
}
|
|
|
|
.loading-sub {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin-top: 8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Loading -->
|
|
<div class="loading" id="loading">
|
|
<div class="loading-icon">🌍</div>
|
|
<div class="loading-text">Growing your world...</div>
|
|
<div class="loading-sub">Planting trees, waking animals, building homes</div>
|
|
</div>
|
|
|
|
<!-- Canvas -->
|
|
<div id="canvas-container"></div>
|
|
|
|
<!-- Logo -->
|
|
<div class="logo">
|
|
<div class="logo-mark">
|
|
<svg viewBox="0 0 100 100" fill="none">
|
|
<circle cx="50" cy="50" r="44" stroke="#FF1D6C" stroke-width="6"/>
|
|
<g class="road-dashes">
|
|
<rect x="47" y="4" width="6" height="12" fill="#fff" rx="2"/>
|
|
<rect x="47" y="84" width="6" height="12" fill="#fff" rx="2"/>
|
|
<rect x="84" y="47" width="12" height="6" fill="#fff" rx="2"/>
|
|
<rect x="4" y="47" width="12" height="6" fill="#fff" rx="2"/>
|
|
</g>
|
|
<path d="M50 10C27.9 10 10 27.9 10 50H90C90 27.9 72.1 10 50 10Z" fill="#F5A623"/>
|
|
<path d="M10 50C10 72.1 27.9 90 50 90C72.1 90 90 72.1 90 50H10Z" fill="#2979FF"/>
|
|
<circle cx="50" cy="50" r="14" fill="#333"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="logo-text">BlackRoad World</div>
|
|
<div class="logo-sub">Living Simulation</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time & Weather -->
|
|
<div class="time-weather">
|
|
<div class="time-display" id="timeDisplay">12:00</div>
|
|
<div class="weather-display">
|
|
<span id="weatherIcon">☀️</span>
|
|
<span id="weatherTemp">72°F</span>
|
|
<span id="weatherDesc">Sunny</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Population -->
|
|
<div class="population-panel">
|
|
<div class="pop-item">
|
|
<div class="pop-icon">🏠</div>
|
|
<div class="pop-count" id="houseCount">0</div>
|
|
</div>
|
|
<div class="pop-item">
|
|
<div class="pop-icon">🌳</div>
|
|
<div class="pop-count" id="treeCount">0</div>
|
|
</div>
|
|
<div class="pop-item">
|
|
<div class="pop-icon">🐰</div>
|
|
<div class="pop-count" id="animalCount">0</div>
|
|
</div>
|
|
<div class="pop-item">
|
|
<div class="pop-icon">🌸</div>
|
|
<div class="pop-count" id="flowerCount">0</div>
|
|
</div>
|
|
<div class="pop-item">
|
|
<div class="pop-icon">🤖</div>
|
|
<div class="pop-count" id="agentCount">0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-panel">
|
|
<div class="stats-title">World Status</div>
|
|
<div class="stat-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-icon">💚</div>
|
|
<div class="stat-value" id="happinessStat">100%</div>
|
|
<div class="stat-label">Happiness</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon">🌱</div>
|
|
<div class="stat-value" id="natureStat">100%</div>
|
|
<div class="stat-label">Nature</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon">⚡</div>
|
|
<div class="stat-value" id="energyStat">100%</div>
|
|
<div class="stat-label">Energy</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon">💫</div>
|
|
<div class="stat-value" id="magicStat">100%</div>
|
|
<div class="stat-label">Magic</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls">
|
|
<button class="control-btn" id="btnDay" title="Day">☀️</button>
|
|
<button class="control-btn" id="btnNight" title="Night">🌙</button>
|
|
<button class="control-btn" id="btnRain" title="Rain">🌧️</button>
|
|
<button class="control-btn" id="btnSnow" title="Snow">❄️</button>
|
|
<button class="control-btn active" id="btnRotate" title="Auto Rotate">🔄</button>
|
|
</div>
|
|
|
|
<!-- Message -->
|
|
<div class="message" id="message">Welcome to BlackRoad World! 🌍</div>
|
|
|
|
<!-- Three.js -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
|
<script>
|
|
// ========== CONFIG ==========
|
|
const CONFIG = {
|
|
worldSize: 200,
|
|
waterLevel: -2,
|
|
numTrees: 150,
|
|
numFlowers: 300,
|
|
numHouses: 24,
|
|
numAnimals: 50,
|
|
numAgents: 20,
|
|
numClouds: 15,
|
|
numBirds: 30,
|
|
numFish: 40,
|
|
numButterflies: 25
|
|
};
|
|
|
|
// ========== COLORS ==========
|
|
const COLORS = {
|
|
sky: 0x87CEEB,
|
|
skyNight: 0x1a1a2e,
|
|
grass: 0x7CBA3D,
|
|
grassDark: 0x5a9a2d,
|
|
water: 0x4FA4E8,
|
|
waterDeep: 0x2979FF,
|
|
sand: 0xF4D03F,
|
|
wood: 0x8B4513,
|
|
roof: 0xE74C3C,
|
|
roofBlue: 0x3498DB,
|
|
roofPink: 0xFF1D6C,
|
|
roofYellow: 0xF5A623,
|
|
white: 0xFFFFFF,
|
|
pink: 0xFF1D6C,
|
|
amber: 0xF5A623,
|
|
blue: 0x2979FF,
|
|
violet: 0x9C27B0
|
|
};
|
|
|
|
// ========== SCENE ==========
|
|
let scene, camera, renderer;
|
|
let world, water, sky;
|
|
let trees = [], houses = [], animals = [], agents = [];
|
|
let flowers = [], clouds = [], birds = [], fish = [], butterflies = [];
|
|
let rain = null, snow = null;
|
|
let time = 0;
|
|
let worldTime = 12; // 0-24
|
|
let weather = 'sunny';
|
|
let autoRotate = true;
|
|
|
|
// Mouse controls
|
|
let isDragging = false;
|
|
let previousMouseX = 0, previousMouseY = 0;
|
|
let cameraAngle = 0;
|
|
let cameraHeight = 60;
|
|
let cameraDistance = 120;
|
|
|
|
function init() {
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(COLORS.sky);
|
|
scene.fog = new THREE.Fog(COLORS.sky, 100, 400);
|
|
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
updateCameraPosition();
|
|
|
|
// 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;
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
// Create world
|
|
createLights();
|
|
createTerrain();
|
|
createWater();
|
|
createTrees();
|
|
createFlowers();
|
|
createHouses();
|
|
createAnimals();
|
|
createAgents();
|
|
createClouds();
|
|
createBirds();
|
|
createFish();
|
|
createButterflies();
|
|
createRainSystem();
|
|
createSnowSystem();
|
|
|
|
// 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);
|
|
|
|
// Controls
|
|
document.getElementById('btnDay').addEventListener('click', () => setTimeOfDay(12));
|
|
document.getElementById('btnNight').addEventListener('click', () => setTimeOfDay(0));
|
|
document.getElementById('btnRain').addEventListener('click', () => toggleWeather('rain'));
|
|
document.getElementById('btnSnow').addEventListener('click', () => toggleWeather('snow'));
|
|
document.getElementById('btnRotate').addEventListener('click', toggleRotate);
|
|
|
|
// Start
|
|
setTimeout(() => {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
showMessage("Welcome to BlackRoad World! 🌍");
|
|
animate();
|
|
}, 2000);
|
|
}
|
|
|
|
function createLights() {
|
|
// Ambient light
|
|
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambient);
|
|
|
|
// Sun
|
|
const sun = new THREE.DirectionalLight(0xffffff, 1);
|
|
sun.position.set(100, 100, 50);
|
|
sun.castShadow = true;
|
|
sun.shadow.mapSize.width = 2048;
|
|
sun.shadow.mapSize.height = 2048;
|
|
sun.shadow.camera.near = 0.5;
|
|
sun.shadow.camera.far = 500;
|
|
sun.shadow.camera.left = -150;
|
|
sun.shadow.camera.right = 150;
|
|
sun.shadow.camera.top = 150;
|
|
sun.shadow.camera.bottom = -150;
|
|
scene.add(sun);
|
|
window.sunLight = sun;
|
|
|
|
// Hemisphere light for sky color
|
|
const hemi = new THREE.HemisphereLight(0x87CEEB, 0x7CBA3D, 0.4);
|
|
scene.add(hemi);
|
|
window.hemiLight = hemi;
|
|
}
|
|
|
|
function createTerrain() {
|
|
// Main island
|
|
const terrainGroup = new THREE.Group();
|
|
|
|
// Create height map using noise
|
|
const size = CONFIG.worldSize;
|
|
const segments = 100;
|
|
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
|
|
|
|
const positions = geometry.attributes.position.array;
|
|
for (let i = 0; i < positions.length; i += 3) {
|
|
const x = positions[i];
|
|
const y = positions[i + 1];
|
|
const dist = Math.sqrt(x * x + y * y);
|
|
|
|
// Create island shape
|
|
let height = 0;
|
|
if (dist < size * 0.4) {
|
|
// Hills using simplex-like noise
|
|
height = Math.sin(x * 0.05) * Math.cos(y * 0.05) * 8;
|
|
height += Math.sin(x * 0.1 + 1) * Math.cos(y * 0.1 + 1) * 4;
|
|
height += Math.sin(x * 0.2) * Math.cos(y * 0.2) * 2;
|
|
|
|
// Flatten center for village
|
|
const centerDist = dist / (size * 0.4);
|
|
if (centerDist < 0.3) {
|
|
height *= centerDist / 0.3;
|
|
}
|
|
|
|
// Edge falloff
|
|
const edgeFactor = 1 - Math.pow(centerDist, 2);
|
|
height *= Math.max(0, edgeFactor);
|
|
height = Math.max(0, height);
|
|
} else {
|
|
// Underwater slope
|
|
const underwaterDist = (dist - size * 0.4) / (size * 0.1);
|
|
height = -underwaterDist * 10 - 2;
|
|
}
|
|
|
|
positions[i + 2] = height;
|
|
}
|
|
|
|
geometry.computeVertexNormals();
|
|
|
|
// Grass material
|
|
const grassMaterial = new THREE.MeshStandardMaterial({
|
|
color: COLORS.grass,
|
|
roughness: 0.8,
|
|
metalness: 0.1,
|
|
flatShading: false
|
|
});
|
|
|
|
const terrain = new THREE.Mesh(geometry, grassMaterial);
|
|
terrain.rotation.x = -Math.PI / 2;
|
|
terrain.receiveShadow = true;
|
|
terrainGroup.add(terrain);
|
|
|
|
// Beach/sand ring
|
|
const beachGeometry = new THREE.RingGeometry(size * 0.38, size * 0.42, 64);
|
|
const beachMaterial = new THREE.MeshStandardMaterial({
|
|
color: COLORS.sand,
|
|
roughness: 0.9
|
|
});
|
|
const beach = new THREE.Mesh(beachGeometry, beachMaterial);
|
|
beach.rotation.x = -Math.PI / 2;
|
|
beach.position.y = 0.1;
|
|
beach.receiveShadow = true;
|
|
terrainGroup.add(beach);
|
|
|
|
scene.add(terrainGroup);
|
|
world = terrainGroup;
|
|
}
|
|
|
|
function createWater() {
|
|
// Ocean
|
|
const waterGeometry = new THREE.CircleGeometry(300, 64);
|
|
const waterMaterial = new THREE.MeshStandardMaterial({
|
|
color: COLORS.water,
|
|
transparent: true,
|
|
opacity: 0.85,
|
|
roughness: 0.1,
|
|
metalness: 0.3
|
|
});
|
|
water = new THREE.Mesh(waterGeometry, waterMaterial);
|
|
water.rotation.x = -Math.PI / 2;
|
|
water.position.y = CONFIG.waterLevel;
|
|
water.receiveShadow = true;
|
|
scene.add(water);
|
|
}
|
|
|
|
function createTrees() {
|
|
const treeTypes = ['pine', 'oak', 'palm', 'cherry'];
|
|
|
|
for (let i = 0; i < CONFIG.numTrees; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 15 + Math.random() * 65;
|
|
const x = Math.cos(angle) * dist;
|
|
const z = Math.sin(angle) * dist;
|
|
|
|
const type = treeTypes[Math.floor(Math.random() * treeTypes.length)];
|
|
const tree = createTree(type);
|
|
tree.position.set(x, 0, z);
|
|
tree.scale.setScalar(0.8 + Math.random() * 0.5);
|
|
tree.rotation.y = Math.random() * Math.PI * 2;
|
|
|
|
trees.push(tree);
|
|
scene.add(tree);
|
|
}
|
|
|
|
document.getElementById('treeCount').textContent = trees.length;
|
|
}
|
|
|
|
function createTree(type) {
|
|
const group = new THREE.Group();
|
|
|
|
// Trunk
|
|
const trunkGeometry = new THREE.CylinderGeometry(0.3, 0.5, 3, 8);
|
|
const trunkMaterial = new THREE.MeshStandardMaterial({ color: COLORS.wood });
|
|
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
|
|
trunk.position.y = 1.5;
|
|
trunk.castShadow = true;
|
|
group.add(trunk);
|
|
|
|
if (type === 'pine') {
|
|
// Pine tree - layered cones
|
|
for (let i = 0; i < 4; i++) {
|
|
const coneGeometry = new THREE.ConeGeometry(2.5 - i * 0.4, 2.5, 8);
|
|
const coneMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x228B22,
|
|
flatShading: true
|
|
});
|
|
const cone = new THREE.Mesh(coneGeometry, coneMaterial);
|
|
cone.position.y = 3.5 + i * 1.5;
|
|
cone.castShadow = true;
|
|
group.add(cone);
|
|
}
|
|
} else if (type === 'oak') {
|
|
// Oak tree - fluffy sphere
|
|
const leavesGeometry = new THREE.SphereGeometry(3, 8, 8);
|
|
const leavesMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x32CD32,
|
|
flatShading: true
|
|
});
|
|
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
|
|
leaves.position.y = 5;
|
|
leaves.scale.set(1, 0.8, 1);
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
} else if (type === 'palm') {
|
|
// Palm tree
|
|
trunk.scale.set(0.6, 1.5, 0.6);
|
|
trunk.position.y = 2.5;
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const frondGeometry = new THREE.ConeGeometry(0.3, 4, 4);
|
|
const frondMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
|
|
const frond = new THREE.Mesh(frondGeometry, frondMaterial);
|
|
frond.position.y = 5;
|
|
frond.rotation.z = Math.PI / 3;
|
|
frond.rotation.y = (i / 6) * Math.PI * 2;
|
|
frond.castShadow = true;
|
|
group.add(frond);
|
|
}
|
|
} else if (type === 'cherry') {
|
|
// Cherry blossom
|
|
const leavesGeometry = new THREE.SphereGeometry(2.5, 8, 8);
|
|
const leavesMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0xFFB7C5,
|
|
flatShading: true
|
|
});
|
|
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
|
|
leaves.position.y = 4.5;
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
}
|
|
|
|
group.userData = { type: 'tree', swayPhase: Math.random() * Math.PI * 2 };
|
|
return group;
|
|
}
|
|
|
|
function createFlowers() {
|
|
const flowerColors = [0xFF69B4, 0xFFD700, 0xFF6347, 0x9370DB, 0x00CED1, 0xFFFFFF];
|
|
|
|
for (let i = 0; i < CONFIG.numFlowers; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 5 + Math.random() * 70;
|
|
const x = Math.cos(angle) * dist;
|
|
const z = Math.sin(angle) * dist;
|
|
|
|
const flower = createFlower(flowerColors[Math.floor(Math.random() * flowerColors.length)]);
|
|
flower.position.set(x, 0, z);
|
|
flower.scale.setScalar(0.3 + Math.random() * 0.4);
|
|
|
|
flowers.push(flower);
|
|
scene.add(flower);
|
|
}
|
|
|
|
document.getElementById('flowerCount').textContent = flowers.length;
|
|
}
|
|
|
|
function createFlower(color) {
|
|
const group = new THREE.Group();
|
|
|
|
// Stem
|
|
const stemGeometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 6);
|
|
const stemMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
|
|
const stem = new THREE.Mesh(stemGeometry, stemMaterial);
|
|
stem.position.y = 0.5;
|
|
group.add(stem);
|
|
|
|
// Petals
|
|
for (let i = 0; i < 5; i++) {
|
|
const petalGeometry = new THREE.SphereGeometry(0.3, 8, 8);
|
|
const petalMaterial = new THREE.MeshStandardMaterial({ color: color });
|
|
const petal = new THREE.Mesh(petalGeometry, petalMaterial);
|
|
petal.scale.set(1, 0.3, 0.5);
|
|
petal.position.y = 1;
|
|
petal.position.x = Math.cos((i / 5) * Math.PI * 2) * 0.3;
|
|
petal.position.z = Math.sin((i / 5) * Math.PI * 2) * 0.3;
|
|
petal.rotation.y = (i / 5) * Math.PI * 2;
|
|
petal.rotation.z = 0.3;
|
|
group.add(petal);
|
|
}
|
|
|
|
// Center
|
|
const centerGeometry = new THREE.SphereGeometry(0.15, 8, 8);
|
|
const centerMaterial = new THREE.MeshStandardMaterial({ color: 0xFFD700 });
|
|
const center = new THREE.Mesh(centerGeometry, centerMaterial);
|
|
center.position.y = 1;
|
|
group.add(center);
|
|
|
|
group.userData = { type: 'flower', swayPhase: Math.random() * Math.PI * 2 };
|
|
return group;
|
|
}
|
|
|
|
function createHouses() {
|
|
const roofColors = [COLORS.roof, COLORS.roofBlue, COLORS.roofPink, COLORS.roofYellow];
|
|
const houseNames = [
|
|
"Lucidia Home", "Memory Cottage", "Agent Academy", "Quantum Lodge",
|
|
"Creativity Den", "Ethics House", "Unity Villa", "Roadchain Cabin",
|
|
"Alice Cottage", "Spiral Home", "Garden House", "Z-Frame Lodge",
|
|
"Archive House", "Codon Cottage", "Pauli Place", "Amplitude Home",
|
|
"Jetson House", "Easter Cottage", "Milvus Home", "CrewAI Lodge",
|
|
"Fine Cottage", "Gauss Home", "Mission House", "SOC Cottage"
|
|
];
|
|
|
|
// Place houses in a village layout
|
|
const housePositions = [];
|
|
|
|
// Central plaza houses
|
|
for (let i = 0; i < 8; i++) {
|
|
const angle = (i / 8) * Math.PI * 2;
|
|
housePositions.push({
|
|
x: Math.cos(angle) * 25,
|
|
z: Math.sin(angle) * 25,
|
|
rotation: angle + Math.PI
|
|
});
|
|
}
|
|
|
|
// Outer ring
|
|
for (let i = 0; i < 12; i++) {
|
|
const angle = (i / 12) * Math.PI * 2 + 0.2;
|
|
housePositions.push({
|
|
x: Math.cos(angle) * 45,
|
|
z: Math.sin(angle) * 45,
|
|
rotation: angle + Math.PI
|
|
});
|
|
}
|
|
|
|
// Scattered
|
|
for (let i = 0; i < 4; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 55 + Math.random() * 15;
|
|
housePositions.push({
|
|
x: Math.cos(angle) * dist,
|
|
z: Math.sin(angle) * dist,
|
|
rotation: Math.random() * Math.PI * 2
|
|
});
|
|
}
|
|
|
|
housePositions.forEach((pos, i) => {
|
|
if (i >= CONFIG.numHouses) return;
|
|
|
|
const house = createHouse(
|
|
roofColors[i % roofColors.length],
|
|
houseNames[i] || `House ${i + 1}`
|
|
);
|
|
house.position.set(pos.x, 0, pos.z);
|
|
house.rotation.y = pos.rotation;
|
|
house.scale.setScalar(0.8 + Math.random() * 0.4);
|
|
|
|
houses.push(house);
|
|
scene.add(house);
|
|
});
|
|
|
|
document.getElementById('houseCount').textContent = houses.length;
|
|
}
|
|
|
|
function createHouse(roofColor, name) {
|
|
const group = new THREE.Group();
|
|
|
|
// Base/walls
|
|
const wallGeometry = new THREE.BoxGeometry(4, 3, 4);
|
|
const wallMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0xFAF0E6,
|
|
roughness: 0.9
|
|
});
|
|
const walls = new THREE.Mesh(wallGeometry, wallMaterial);
|
|
walls.position.y = 1.5;
|
|
walls.castShadow = true;
|
|
walls.receiveShadow = true;
|
|
group.add(walls);
|
|
|
|
// Roof
|
|
const roofGeometry = new THREE.ConeGeometry(3.5, 2, 4);
|
|
const roofMaterial = new THREE.MeshStandardMaterial({
|
|
color: roofColor,
|
|
roughness: 0.7
|
|
});
|
|
const roof = new THREE.Mesh(roofGeometry, roofMaterial);
|
|
roof.position.y = 4;
|
|
roof.rotation.y = Math.PI / 4;
|
|
roof.castShadow = true;
|
|
group.add(roof);
|
|
|
|
// Door
|
|
const doorGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.1);
|
|
const doorMaterial = new THREE.MeshStandardMaterial({ color: COLORS.wood });
|
|
const door = new THREE.Mesh(doorGeometry, doorMaterial);
|
|
door.position.set(0, 0.75, 2.05);
|
|
group.add(door);
|
|
|
|
// Windows
|
|
const windowGeometry = new THREE.BoxGeometry(0.8, 0.8, 0.1);
|
|
const windowMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x87CEEB,
|
|
transparent: true,
|
|
opacity: 0.7
|
|
});
|
|
|
|
[-1.2, 1.2].forEach(x => {
|
|
const window = new THREE.Mesh(windowGeometry, windowMaterial);
|
|
window.position.set(x, 2, 2.05);
|
|
group.add(window);
|
|
});
|
|
|
|
// Chimney
|
|
const chimneyGeometry = new THREE.BoxGeometry(0.6, 1.5, 0.6);
|
|
const chimneyMaterial = new THREE.MeshStandardMaterial({ color: 0x8B0000 });
|
|
const chimney = new THREE.Mesh(chimneyGeometry, chimneyMaterial);
|
|
chimney.position.set(1.2, 4.5, 0);
|
|
chimney.castShadow = true;
|
|
group.add(chimney);
|
|
|
|
// Smoke particles (will animate)
|
|
const smokeGroup = new THREE.Group();
|
|
for (let i = 0; i < 3; i++) {
|
|
const smokeGeometry = new THREE.SphereGeometry(0.2, 8, 8);
|
|
const smokeMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xcccccc,
|
|
transparent: true,
|
|
opacity: 0.5
|
|
});
|
|
const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
|
|
smoke.position.set(1.2, 5.5 + i * 0.5, 0);
|
|
smoke.userData = { phase: i * 0.5 };
|
|
smokeGroup.add(smoke);
|
|
}
|
|
group.add(smokeGroup);
|
|
|
|
group.userData = { type: 'house', name: name, smokeGroup: smokeGroup };
|
|
return group;
|
|
}
|
|
|
|
function createAnimals() {
|
|
const animalTypes = [
|
|
{ emoji: '🐰', name: 'Bunny', color: 0xFFFFFF },
|
|
{ emoji: '🐿️', name: 'Squirrel', color: 0x8B4513 },
|
|
{ emoji: '🦊', name: 'Fox', color: 0xFF6600 },
|
|
{ emoji: '🦌', name: 'Deer', color: 0xD2691E },
|
|
{ emoji: '🐕', name: 'Dog', color: 0xD2B48C },
|
|
{ emoji: '🐈', name: 'Cat', color: 0x808080 },
|
|
{ emoji: '🐑', name: 'Sheep', color: 0xFFFFF0 },
|
|
{ emoji: '🐄', name: 'Cow', color: 0x000000 },
|
|
{ emoji: '🐖', name: 'Pig', color: 0xFFB6C1 },
|
|
{ emoji: '🦆', name: 'Duck', color: 0xFFD700 }
|
|
];
|
|
|
|
for (let i = 0; i < CONFIG.numAnimals; i++) {
|
|
const type = animalTypes[Math.floor(Math.random() * animalTypes.length)];
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 10 + Math.random() * 60;
|
|
|
|
const animal = createAnimal(type);
|
|
animal.position.set(
|
|
Math.cos(angle) * dist,
|
|
0.4,
|
|
Math.sin(angle) * dist
|
|
);
|
|
|
|
animals.push(animal);
|
|
scene.add(animal);
|
|
}
|
|
|
|
document.getElementById('animalCount').textContent = animals.length;
|
|
}
|
|
|
|
function createAnimal(type) {
|
|
const group = new THREE.Group();
|
|
|
|
// Body
|
|
const bodyGeometry = new THREE.SphereGeometry(0.5, 12, 12);
|
|
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
color: type.color,
|
|
roughness: 0.8
|
|
});
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
body.scale.set(1, 0.8, 1.3);
|
|
group.add(body);
|
|
|
|
// Head
|
|
const headGeometry = new THREE.SphereGeometry(0.35, 12, 12);
|
|
const head = new THREE.Mesh(headGeometry, bodyMaterial);
|
|
head.position.set(0, 0.3, 0.5);
|
|
group.add(head);
|
|
|
|
// Eyes
|
|
const eyeGeometry = new THREE.SphereGeometry(0.08, 8, 8);
|
|
const eyeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
[-0.12, 0.12].forEach(x => {
|
|
const eye = new THREE.Mesh(eyeGeometry, eyeMaterial);
|
|
eye.position.set(x, 0.4, 0.75);
|
|
group.add(eye);
|
|
});
|
|
|
|
// Nose
|
|
const noseGeometry = new THREE.SphereGeometry(0.05, 8, 8);
|
|
const noseMaterial = new THREE.MeshBasicMaterial({ color: 0xFF69B4 });
|
|
const nose = new THREE.Mesh(noseGeometry, noseMaterial);
|
|
nose.position.set(0, 0.3, 0.85);
|
|
group.add(nose);
|
|
|
|
// Ears
|
|
const earGeometry = new THREE.ConeGeometry(0.1, 0.3, 8);
|
|
[-0.15, 0.15].forEach(x => {
|
|
const ear = new THREE.Mesh(earGeometry, bodyMaterial);
|
|
ear.position.set(x, 0.6, 0.4);
|
|
ear.rotation.x = -0.2;
|
|
group.add(ear);
|
|
});
|
|
|
|
// Legs
|
|
const legGeometry = new THREE.CylinderGeometry(0.08, 0.08, 0.3, 8);
|
|
[[-0.2, -0.3], [0.2, -0.3], [-0.2, 0.3], [0.2, 0.3]].forEach(([x, z]) => {
|
|
const leg = new THREE.Mesh(legGeometry, bodyMaterial);
|
|
leg.position.set(x, -0.3, z);
|
|
group.add(leg);
|
|
});
|
|
|
|
group.userData = {
|
|
type: 'animal',
|
|
animalType: type,
|
|
targetX: group.position.x,
|
|
targetZ: group.position.z,
|
|
speed: 0.02 + Math.random() * 0.03,
|
|
hopPhase: Math.random() * Math.PI * 2,
|
|
idleTime: 0,
|
|
state: 'idle'
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
function createAgents() {
|
|
for (let i = 0; i < CONFIG.numAgents; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 5 + Math.random() * 50;
|
|
|
|
const agent = createAgent(i);
|
|
agent.position.set(
|
|
Math.cos(angle) * dist,
|
|
0.8,
|
|
Math.sin(angle) * dist
|
|
);
|
|
|
|
agents.push(agent);
|
|
scene.add(agent);
|
|
}
|
|
|
|
document.getElementById('agentCount').textContent = agents.length;
|
|
}
|
|
|
|
function createAgent(index) {
|
|
const group = new THREE.Group();
|
|
|
|
// Body (cute robot style)
|
|
const bodyGeometry = new THREE.CylinderGeometry(0.4, 0.5, 1, 12);
|
|
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
color: [COLORS.pink, COLORS.blue, COLORS.amber, COLORS.violet][index % 4],
|
|
metalness: 0.5,
|
|
roughness: 0.3
|
|
});
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
body.position.y = 0.5;
|
|
group.add(body);
|
|
|
|
// Head
|
|
const headGeometry = new THREE.SphereGeometry(0.4, 16, 16);
|
|
const headMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
metalness: 0.3,
|
|
roughness: 0.5
|
|
});
|
|
const head = new THREE.Mesh(headGeometry, headMaterial);
|
|
head.position.y = 1.3;
|
|
group.add(head);
|
|
|
|
// Eye (the BlackRoad eye!)
|
|
const eyeGeometry = new THREE.CircleGeometry(0.2, 32);
|
|
const eyeMaterial = new THREE.MeshBasicMaterial({ color: COLORS.pink });
|
|
const eye = new THREE.Mesh(eyeGeometry, eyeMaterial);
|
|
eye.position.set(0, 1.35, 0.35);
|
|
group.add(eye);
|
|
|
|
// Pupil
|
|
const pupilGeometry = new THREE.CircleGeometry(0.08, 16);
|
|
const pupilMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
const pupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
|
|
pupil.position.set(0, 1.35, 0.36);
|
|
group.add(pupil);
|
|
|
|
// Antenna
|
|
const antennaGeometry = new THREE.CylinderGeometry(0.02, 0.02, 0.3, 8);
|
|
const antennaMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
|
const antenna = new THREE.Mesh(antennaGeometry, antennaMaterial);
|
|
antenna.position.set(0, 1.7, 0);
|
|
group.add(antenna);
|
|
|
|
// Antenna ball
|
|
const ballGeometry = new THREE.SphereGeometry(0.08, 8, 8);
|
|
const ballMaterial = new THREE.MeshBasicMaterial({ color: COLORS.pink });
|
|
const ball = new THREE.Mesh(ballGeometry, ballMaterial);
|
|
ball.position.set(0, 1.9, 0);
|
|
ball.userData = { glowPhase: Math.random() * Math.PI * 2 };
|
|
group.add(ball);
|
|
|
|
// Hover ring
|
|
const ringGeometry = new THREE.TorusGeometry(0.5, 0.05, 8, 32);
|
|
const ringMaterial = new THREE.MeshBasicMaterial({
|
|
color: COLORS.pink,
|
|
transparent: true,
|
|
opacity: 0.5
|
|
});
|
|
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = -0.1;
|
|
group.add(ring);
|
|
|
|
group.userData = {
|
|
type: 'agent',
|
|
index: index,
|
|
targetX: group.position.x,
|
|
targetZ: group.position.z,
|
|
speed: 0.03 + Math.random() * 0.02,
|
|
hoverPhase: Math.random() * Math.PI * 2,
|
|
state: 'wandering'
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
function createClouds() {
|
|
for (let i = 0; i < CONFIG.numClouds; i++) {
|
|
const cloud = createCloud();
|
|
cloud.position.set(
|
|
(Math.random() - 0.5) * 200,
|
|
40 + Math.random() * 30,
|
|
(Math.random() - 0.5) * 200
|
|
);
|
|
clouds.push(cloud);
|
|
scene.add(cloud);
|
|
}
|
|
}
|
|
|
|
function createCloud() {
|
|
const group = new THREE.Group();
|
|
|
|
const cloudMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
roughness: 1
|
|
});
|
|
|
|
// Multiple spheres for fluffy cloud
|
|
const positions = [
|
|
[0, 0, 0, 3],
|
|
[-2, 0, 0, 2.5],
|
|
[2, 0, 0, 2.5],
|
|
[0, 1, 0, 2],
|
|
[-1.5, 0.8, 0, 2],
|
|
[1.5, 0.8, 0, 2],
|
|
[0, 0, 1.5, 2],
|
|
[0, 0, -1.5, 2]
|
|
];
|
|
|
|
positions.forEach(([x, y, z, r]) => {
|
|
const puff = new THREE.Mesh(
|
|
new THREE.SphereGeometry(r, 12, 12),
|
|
cloudMaterial
|
|
);
|
|
puff.position.set(x, y, z);
|
|
group.add(puff);
|
|
});
|
|
|
|
group.userData = {
|
|
type: 'cloud',
|
|
speed: 0.02 + Math.random() * 0.03,
|
|
bobPhase: Math.random() * Math.PI * 2
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
function createBirds() {
|
|
for (let i = 0; i < CONFIG.numBirds; i++) {
|
|
const bird = createBird();
|
|
bird.position.set(
|
|
(Math.random() - 0.5) * 150,
|
|
15 + Math.random() * 25,
|
|
(Math.random() - 0.5) * 150
|
|
);
|
|
birds.push(bird);
|
|
scene.add(bird);
|
|
}
|
|
}
|
|
|
|
function createBird() {
|
|
const group = new THREE.Group();
|
|
|
|
// Body
|
|
const bodyGeometry = new THREE.SphereGeometry(0.3, 8, 8);
|
|
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
color: [0x4169E1, 0xFF6347, 0xFFD700, 0x32CD32][Math.floor(Math.random() * 4)]
|
|
});
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
body.scale.set(1, 0.8, 1.5);
|
|
group.add(body);
|
|
|
|
// Wings
|
|
const wingGeometry = new THREE.BoxGeometry(1.5, 0.1, 0.5);
|
|
const wing = new THREE.Mesh(wingGeometry, bodyMaterial);
|
|
wing.position.y = 0.1;
|
|
wing.userData = { flapPhase: Math.random() * Math.PI * 2 };
|
|
group.add(wing);
|
|
|
|
// Beak
|
|
const beakGeometry = new THREE.ConeGeometry(0.1, 0.3, 6);
|
|
const beakMaterial = new THREE.MeshStandardMaterial({ color: 0xFFA500 });
|
|
const beak = new THREE.Mesh(beakGeometry, beakMaterial);
|
|
beak.position.set(0, 0, 0.4);
|
|
beak.rotation.x = Math.PI / 2;
|
|
group.add(beak);
|
|
|
|
group.userData = {
|
|
type: 'bird',
|
|
circleAngle: Math.random() * Math.PI * 2,
|
|
circleRadius: 20 + Math.random() * 40,
|
|
circleSpeed: 0.005 + Math.random() * 0.01,
|
|
flapSpeed: 0.3 + Math.random() * 0.2,
|
|
wing: wing
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
function createFish() {
|
|
for (let i = 0; i < CONFIG.numFish; i++) {
|
|
const fishMesh = createFish3D();
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 85 + Math.random() * 30;
|
|
fishMesh.position.set(
|
|
Math.cos(angle) * dist,
|
|
CONFIG.waterLevel - 1 - Math.random() * 3,
|
|
Math.sin(angle) * dist
|
|
);
|
|
fish.push(fishMesh);
|
|
scene.add(fishMesh);
|
|
}
|
|
}
|
|
|
|
function createFish3D() {
|
|
const group = new THREE.Group();
|
|
|
|
const bodyGeometry = new THREE.SphereGeometry(0.4, 8, 8);
|
|
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
color: [0xFF6347, 0xFFD700, 0x00CED1, 0xFF69B4][Math.floor(Math.random() * 4)]
|
|
});
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
body.scale.set(1, 0.6, 1.5);
|
|
group.add(body);
|
|
|
|
// Tail
|
|
const tailGeometry = new THREE.ConeGeometry(0.3, 0.5, 4);
|
|
const tail = new THREE.Mesh(tailGeometry, bodyMaterial);
|
|
tail.position.z = -0.5;
|
|
tail.rotation.x = Math.PI / 2;
|
|
group.add(tail);
|
|
|
|
group.userData = {
|
|
type: 'fish',
|
|
swimAngle: Math.random() * Math.PI * 2,
|
|
swimRadius: 85 + Math.random() * 30,
|
|
swimSpeed: 0.01 + Math.random() * 0.02,
|
|
wobblePhase: Math.random() * Math.PI * 2
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
function createButterflies() {
|
|
for (let i = 0; i < CONFIG.numButterflies; i++) {
|
|
const butterfly = createButterfly();
|
|
butterfly.position.set(
|
|
(Math.random() - 0.5) * 100,
|
|
2 + Math.random() * 8,
|
|
(Math.random() - 0.5) * 100
|
|
);
|
|
butterflies.push(butterfly);
|
|
scene.add(butterfly);
|
|
}
|
|
}
|
|
|
|
function createButterfly() {
|
|
const group = new THREE.Group();
|
|
|
|
const wingColors = [0xFF69B4, 0x9370DB, 0xFFD700, 0x00CED1, 0xFF6347];
|
|
const wingMaterial = new THREE.MeshStandardMaterial({
|
|
color: wingColors[Math.floor(Math.random() * wingColors.length)],
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
|
|
// Wings
|
|
const wingGeometry = new THREE.CircleGeometry(0.3, 8);
|
|
|
|
const leftWing = new THREE.Mesh(wingGeometry, wingMaterial);
|
|
leftWing.position.x = -0.2;
|
|
leftWing.rotation.y = -0.3;
|
|
group.add(leftWing);
|
|
|
|
const rightWing = new THREE.Mesh(wingGeometry, wingMaterial);
|
|
rightWing.position.x = 0.2;
|
|
rightWing.rotation.y = 0.3;
|
|
group.add(rightWing);
|
|
|
|
// Body
|
|
const bodyGeometry = new THREE.CylinderGeometry(0.03, 0.03, 0.4, 6);
|
|
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
body.rotation.x = Math.PI / 2;
|
|
group.add(body);
|
|
|
|
group.userData = {
|
|
type: 'butterfly',
|
|
leftWing: leftWing,
|
|
rightWing: rightWing,
|
|
flapPhase: Math.random() * Math.PI * 2,
|
|
wanderAngle: Math.random() * Math.PI * 2,
|
|
wanderSpeed: 0.02 + Math.random() * 0.02
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
function createRainSystem() {
|
|
const rainCount = 5000;
|
|
const rainGeometry = new THREE.BufferGeometry();
|
|
const rainPositions = new Float32Array(rainCount * 3);
|
|
const rainVelocities = new Float32Array(rainCount);
|
|
|
|
for (let i = 0; i < rainCount; i++) {
|
|
rainPositions[i * 3] = (Math.random() - 0.5) * 200;
|
|
rainPositions[i * 3 + 1] = Math.random() * 100;
|
|
rainPositions[i * 3 + 2] = (Math.random() - 0.5) * 200;
|
|
rainVelocities[i] = 0.5 + Math.random() * 0.5;
|
|
}
|
|
|
|
rainGeometry.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3));
|
|
|
|
const rainMaterial = new THREE.PointsMaterial({
|
|
color: 0x8888ff,
|
|
size: 0.2,
|
|
transparent: true,
|
|
opacity: 0.6
|
|
});
|
|
|
|
rain = new THREE.Points(rainGeometry, rainMaterial);
|
|
rain.userData = { velocities: rainVelocities };
|
|
rain.visible = false;
|
|
scene.add(rain);
|
|
}
|
|
|
|
function createSnowSystem() {
|
|
const snowCount = 3000;
|
|
const snowGeometry = new THREE.BufferGeometry();
|
|
const snowPositions = new Float32Array(snowCount * 3);
|
|
const snowVelocities = new Float32Array(snowCount * 2); // y and x drift
|
|
|
|
for (let i = 0; i < snowCount; i++) {
|
|
snowPositions[i * 3] = (Math.random() - 0.5) * 200;
|
|
snowPositions[i * 3 + 1] = Math.random() * 100;
|
|
snowPositions[i * 3 + 2] = (Math.random() - 0.5) * 200;
|
|
snowVelocities[i * 2] = 0.1 + Math.random() * 0.1; // fall speed
|
|
snowVelocities[i * 2 + 1] = (Math.random() - 0.5) * 0.02; // drift
|
|
}
|
|
|
|
snowGeometry.setAttribute('position', new THREE.BufferAttribute(snowPositions, 3));
|
|
|
|
const snowMaterial = new THREE.PointsMaterial({
|
|
color: 0xffffff,
|
|
size: 0.4,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
|
|
snow = new THREE.Points(snowGeometry, snowMaterial);
|
|
snow.userData = { velocities: snowVelocities };
|
|
snow.visible = false;
|
|
scene.add(snow);
|
|
}
|
|
|
|
// ========== CONTROLS ==========
|
|
function updateCameraPosition() {
|
|
camera.position.x = Math.sin(cameraAngle) * cameraDistance;
|
|
camera.position.z = Math.cos(cameraAngle) * cameraDistance;
|
|
camera.position.y = cameraHeight;
|
|
camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
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 deltaX = e.clientX - previousMouseX;
|
|
const deltaY = e.clientY - previousMouseY;
|
|
|
|
cameraAngle += deltaX * 0.005;
|
|
cameraHeight = Math.max(20, Math.min(150, cameraHeight - deltaY * 0.5));
|
|
|
|
updateCameraPosition();
|
|
|
|
previousMouseX = e.clientX;
|
|
previousMouseY = e.clientY;
|
|
}
|
|
|
|
function onMouseUp() {
|
|
isDragging = false;
|
|
}
|
|
|
|
function onWheel(e) {
|
|
cameraDistance = Math.max(50, Math.min(250, cameraDistance + e.deltaY * 0.1));
|
|
updateCameraPosition();
|
|
}
|
|
|
|
function setTimeOfDay(hour) {
|
|
worldTime = hour;
|
|
|
|
if (hour >= 6 && hour < 18) {
|
|
// Day
|
|
scene.background = new THREE.Color(COLORS.sky);
|
|
scene.fog.color = new THREE.Color(COLORS.sky);
|
|
window.sunLight.intensity = 1;
|
|
window.hemiLight.intensity = 0.4;
|
|
} else {
|
|
// Night
|
|
scene.background = new THREE.Color(COLORS.skyNight);
|
|
scene.fog.color = new THREE.Color(COLORS.skyNight);
|
|
window.sunLight.intensity = 0.1;
|
|
window.hemiLight.intensity = 0.1;
|
|
}
|
|
|
|
document.getElementById('btnDay').classList.toggle('active', hour === 12);
|
|
document.getElementById('btnNight').classList.toggle('active', hour === 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');
|
|
showMessage("Weather cleared! ☀️");
|
|
} 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');
|
|
showMessage(type === 'rain' ? "It's raining! 🌧️" : "Snow is falling! ❄️");
|
|
}
|
|
}
|
|
|
|
function toggleRotate() {
|
|
autoRotate = !autoRotate;
|
|
document.getElementById('btnRotate').classList.toggle('active', autoRotate);
|
|
}
|
|
|
|
function showMessage(text) {
|
|
const msg = document.getElementById('message');
|
|
msg.textContent = text;
|
|
msg.classList.add('visible');
|
|
setTimeout(() => msg.classList.remove('visible'), 3000);
|
|
}
|
|
|
|
// ========== ANIMATION ==========
|
|
function updateAnimals() {
|
|
animals.forEach(animal => {
|
|
const data = animal.userData;
|
|
|
|
// Random wandering
|
|
data.idleTime += 0.016;
|
|
if (data.idleTime > 2 + Math.random() * 3) {
|
|
data.idleTime = 0;
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 5 + Math.random() * 10;
|
|
data.targetX = animal.position.x + Math.cos(angle) * dist;
|
|
data.targetZ = animal.position.z + Math.sin(angle) * dist;
|
|
|
|
// Keep on island
|
|
const maxDist = 70;
|
|
const targetDist = Math.sqrt(data.targetX ** 2 + data.targetZ ** 2);
|
|
if (targetDist > maxDist) {
|
|
data.targetX *= maxDist / targetDist;
|
|
data.targetZ *= maxDist / targetDist;
|
|
}
|
|
}
|
|
|
|
// Move towards target
|
|
const dx = data.targetX - animal.position.x;
|
|
const dz = data.targetZ - animal.position.z;
|
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
|
|
if (dist > 0.5) {
|
|
animal.position.x += (dx / dist) * data.speed;
|
|
animal.position.z += (dz / dist) * data.speed;
|
|
animal.rotation.y = Math.atan2(dx, dz);
|
|
|
|
// Hopping motion
|
|
data.hopPhase += 0.2;
|
|
animal.position.y = 0.4 + Math.abs(Math.sin(data.hopPhase)) * 0.3;
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateAgents() {
|
|
agents.forEach(agent => {
|
|
const data = agent.userData;
|
|
|
|
// Hover animation
|
|
data.hoverPhase += 0.05;
|
|
agent.position.y = 0.8 + Math.sin(data.hoverPhase) * 0.2;
|
|
|
|
// Wandering
|
|
if (Math.random() < 0.01) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 10 + Math.random() * 20;
|
|
data.targetX = Math.cos(angle) * dist;
|
|
data.targetZ = Math.sin(angle) * dist;
|
|
}
|
|
|
|
// Move
|
|
const dx = data.targetX - agent.position.x;
|
|
const dz = data.targetZ - agent.position.z;
|
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
|
|
if (dist > 1) {
|
|
agent.position.x += (dx / dist) * data.speed;
|
|
agent.position.z += (dz / dist) * data.speed;
|
|
agent.rotation.y = Math.atan2(dx, dz);
|
|
}
|
|
|
|
// Antenna glow
|
|
const ball = agent.children[5];
|
|
if (ball) {
|
|
ball.userData.glowPhase += 0.1;
|
|
ball.material.color.setHSL(
|
|
(Math.sin(ball.userData.glowPhase) + 1) * 0.15,
|
|
1,
|
|
0.5
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateBirds() {
|
|
birds.forEach(bird => {
|
|
const data = bird.userData;
|
|
|
|
// Circular flight
|
|
data.circleAngle += data.circleSpeed;
|
|
bird.position.x = Math.cos(data.circleAngle) * data.circleRadius;
|
|
bird.position.z = Math.sin(data.circleAngle) * data.circleRadius;
|
|
bird.rotation.y = data.circleAngle + Math.PI / 2;
|
|
|
|
// Wing flapping
|
|
if (data.wing) {
|
|
data.wing.userData.flapPhase += data.flapSpeed;
|
|
data.wing.rotation.z = Math.sin(data.wing.userData.flapPhase) * 0.5;
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateButterflies() {
|
|
butterflies.forEach(butterfly => {
|
|
const data = butterfly.userData;
|
|
|
|
// Wing flapping
|
|
data.flapPhase += 0.3;
|
|
data.leftWing.rotation.y = -0.3 - Math.sin(data.flapPhase) * 0.5;
|
|
data.rightWing.rotation.y = 0.3 + Math.sin(data.flapPhase) * 0.5;
|
|
|
|
// Wandering
|
|
data.wanderAngle += (Math.random() - 0.5) * 0.1;
|
|
butterfly.position.x += Math.cos(data.wanderAngle) * data.wanderSpeed;
|
|
butterfly.position.z += Math.sin(data.wanderAngle) * data.wanderSpeed;
|
|
butterfly.position.y += Math.sin(time * 2 + data.flapPhase) * 0.02;
|
|
|
|
// Keep in bounds
|
|
const dist = Math.sqrt(butterfly.position.x ** 2 + butterfly.position.z ** 2);
|
|
if (dist > 70) {
|
|
data.wanderAngle = Math.atan2(-butterfly.position.z, -butterfly.position.x);
|
|
}
|
|
|
|
butterfly.rotation.y = data.wanderAngle;
|
|
});
|
|
}
|
|
|
|
function updateClouds() {
|
|
clouds.forEach(cloud => {
|
|
const data = cloud.userData;
|
|
|
|
// Drift
|
|
cloud.position.x += data.speed;
|
|
if (cloud.position.x > 150) {
|
|
cloud.position.x = -150;
|
|
}
|
|
|
|
// Bob
|
|
data.bobPhase += 0.01;
|
|
cloud.position.y = 40 + Math.sin(data.bobPhase) * 2;
|
|
});
|
|
}
|
|
|
|
function updateTrees() {
|
|
trees.forEach(tree => {
|
|
// Gentle sway
|
|
tree.userData.swayPhase += 0.02;
|
|
tree.rotation.z = Math.sin(tree.userData.swayPhase) * 0.02;
|
|
});
|
|
}
|
|
|
|
function updateFlowers() {
|
|
flowers.forEach(flower => {
|
|
flower.userData.swayPhase += 0.03;
|
|
flower.rotation.z = Math.sin(flower.userData.swayPhase) * 0.05;
|
|
});
|
|
}
|
|
|
|
function updateHouses() {
|
|
houses.forEach(house => {
|
|
// Smoke animation
|
|
const smokeGroup = house.userData.smokeGroup;
|
|
if (smokeGroup) {
|
|
smokeGroup.children.forEach((smoke, i) => {
|
|
smoke.userData.phase += 0.02;
|
|
smoke.position.y = 5.5 + i * 0.5 + Math.sin(smoke.userData.phase) * 0.3;
|
|
smoke.position.x = house.userData.name ? 1.2 : 0;
|
|
smoke.position.x += Math.sin(smoke.userData.phase * 2) * 0.2;
|
|
smoke.scale.setScalar(1 + Math.sin(smoke.userData.phase) * 0.2);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateWater() {
|
|
// Simple wave effect via position
|
|
water.position.y = CONFIG.waterLevel + Math.sin(time * 0.5) * 0.1;
|
|
}
|
|
|
|
function updateWeather() {
|
|
if (rain.visible) {
|
|
const positions = rain.geometry.attributes.position.array;
|
|
const velocities = rain.userData.velocities;
|
|
|
|
for (let i = 0; i < positions.length / 3; i++) {
|
|
positions[i * 3 + 1] -= velocities[i];
|
|
|
|
if (positions[i * 3 + 1] < 0) {
|
|
positions[i * 3 + 1] = 100;
|
|
positions[i * 3] = (Math.random() - 0.5) * 200;
|
|
positions[i * 3 + 2] = (Math.random() - 0.5) * 200;
|
|
}
|
|
}
|
|
rain.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
|
|
if (snow.visible) {
|
|
const positions = snow.geometry.attributes.position.array;
|
|
const velocities = snow.userData.velocities;
|
|
|
|
for (let i = 0; i < positions.length / 3; i++) {
|
|
positions[i * 3 + 1] -= velocities[i * 2];
|
|
positions[i * 3] += velocities[i * 2 + 1];
|
|
|
|
if (positions[i * 3 + 1] < 0) {
|
|
positions[i * 3 + 1] = 100;
|
|
positions[i * 3] = (Math.random() - 0.5) * 200;
|
|
positions[i * 3 + 2] = (Math.random() - 0.5) * 200;
|
|
}
|
|
}
|
|
snow.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
function updateUI() {
|
|
// Time display
|
|
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 weatherIcons = { sunny: '☀️', rain: '🌧️', snow: '❄️' };
|
|
const weatherDescs = { sunny: 'Sunny', rain: 'Rainy', snow: 'Snowy' };
|
|
document.getElementById('weatherIcon').textContent = weatherIcons[weather];
|
|
document.getElementById('weatherDesc').textContent = weatherDescs[weather];
|
|
|
|
const temp = weather === 'snow' ? '28°F' : weather === 'rain' ? '58°F' : '72°F';
|
|
document.getElementById('weatherTemp').textContent = temp;
|
|
|
|
// Stats (fluctuating)
|
|
const happiness = 95 + Math.sin(time * 0.1) * 5;
|
|
const nature = 90 + Math.sin(time * 0.15) * 10;
|
|
const energy = 85 + Math.sin(time * 0.2) * 15;
|
|
const magic = 88 + Math.sin(time * 0.12) * 12;
|
|
|
|
document.getElementById('happinessStat').textContent = Math.floor(happiness) + '%';
|
|
document.getElementById('natureStat').textContent = Math.floor(nature) + '%';
|
|
document.getElementById('energyStat').textContent = Math.floor(energy) + '%';
|
|
document.getElementById('magicStat').textContent = Math.floor(magic) + '%';
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
time += 0.016;
|
|
|
|
// Auto rotate camera
|
|
if (autoRotate && !isDragging) {
|
|
cameraAngle += 0.002;
|
|
updateCameraPosition();
|
|
}
|
|
|
|
// Update world time (slow cycle)
|
|
worldTime = (worldTime + 0.001) % 24;
|
|
|
|
// Update everything
|
|
updateAnimals();
|
|
updateAgents();
|
|
updateBirds();
|
|
updateButterflies();
|
|
updateClouds();
|
|
updateTrees();
|
|
updateFlowers();
|
|
updateHouses();
|
|
updateWater();
|
|
updateWeather();
|
|
updateUI();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Initialize
|
|
init();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|