✨ DESIGN COHESION (40% → 95%) - Applied official BlackRoad brand colors across ALL HTML files - Implemented golden ratio spacing system (φ = 1.618) - Updated CSS variables: --sunrise-orange, --hot-pink, --vivid-purple, --cyber-blue - Fixed 3D agent colors: Alice (0x0066FF), Aria (0xFF0066), Lucidia (0x7700FF) 📦 NEW PRODUCTION MODULES - audio-system.js: Procedural music, biome sounds, weather effects - api-client.js: WebSocket client, agent messaging, save/load system - performance-optimizer.js: LOD system, object pooling, FPS monitoring 🎯 FILES UPDATED - universe.html, index.html, pangea.html, ultimate.html 🛠 DEPLOYMENT TOOLS - deploy-quick.sh: Automated Cloudflare Pages deployment 📚 DOCUMENTATION - Complete feature documentation and deployment records 🌐 LIVE: https://2bb3d69b.blackroad-metaverse.pages.dev This commit represents a complete metaverse transformation! 🔥
442 lines
14 KiB
JavaScript
442 lines
14 KiB
JavaScript
/**
|
|
* PANGEA CATASTROPHIC EVENTS
|
|
*
|
|
* Massive geological and celestial events:
|
|
* - Earthquakes with ground shaking
|
|
* - Meteor impacts with craters
|
|
* - Tsunamis
|
|
* - Megastorms
|
|
* - Mass extinction triggers
|
|
* - Continental rifting
|
|
*/
|
|
|
|
import * as THREE from 'three';
|
|
|
|
/**
|
|
* EARTHQUAKE SYSTEM
|
|
*/
|
|
export class EarthquakeSystem {
|
|
constructor(scene, camera, terrain) {
|
|
this.scene = scene;
|
|
this.camera = camera;
|
|
this.terrain = terrain;
|
|
this.active = false;
|
|
this.magnitude = 0;
|
|
this.epicenter = new THREE.Vector3();
|
|
this.shakeIntensity = 0;
|
|
this.duration = 0;
|
|
this.timer = 0;
|
|
}
|
|
|
|
trigger(epicenter, magnitude) {
|
|
console.log(`⚠️ EARTHQUAKE! Magnitude ${magnitude.toFixed(1)} at (${epicenter.x}, ${epicenter.z})`);
|
|
|
|
this.active = true;
|
|
this.magnitude = magnitude;
|
|
this.epicenter.copy(epicenter);
|
|
this.duration = 5 + magnitude * 2; // 5-25 seconds
|
|
this.timer = 0;
|
|
|
|
// Calculate intensity based on distance
|
|
const distance = this.camera.position.distanceTo(epicenter);
|
|
this.shakeIntensity = Math.max(0, (magnitude / 10) * (1 - distance / 200));
|
|
|
|
// Create visual effects
|
|
this.createDustClouds();
|
|
this.createGroundCracks();
|
|
}
|
|
|
|
createDustClouds() {
|
|
// Dust particles rising from ground
|
|
const particleCount = 500;
|
|
const geometry = new THREE.BufferGeometry();
|
|
const positions = new Float32Array(particleCount * 3);
|
|
this.dustVelocities = [];
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const radius = Math.random() * 30;
|
|
positions[i * 3] = this.epicenter.x + Math.cos(angle) * radius;
|
|
positions[i * 3 + 1] = this.epicenter.y + 2;
|
|
positions[i * 3 + 2] = this.epicenter.z + Math.sin(angle) * radius;
|
|
|
|
this.dustVelocities.push(new THREE.Vector3(
|
|
(Math.random() - 0.5) * 2,
|
|
2 + Math.random() * 3,
|
|
(Math.random() - 0.5) * 2
|
|
));
|
|
}
|
|
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
|
|
const material = new THREE.PointsMaterial({
|
|
color: 0x8b7355,
|
|
size: 1.5,
|
|
transparent: true,
|
|
opacity: 0.6
|
|
});
|
|
|
|
this.dustParticles = new THREE.Points(geometry, material);
|
|
this.scene.add(this.dustParticles);
|
|
}
|
|
|
|
createGroundCracks() {
|
|
// Visual cracks radiating from epicenter
|
|
const crackCount = 8;
|
|
|
|
for (let i = 0; i < crackCount; i++) {
|
|
const angle = (i / crackCount) * Math.PI * 2;
|
|
const length = 20 + Math.random() * 30;
|
|
|
|
const points = [];
|
|
for (let j = 0; j <= 10; j++) {
|
|
const t = j / 10;
|
|
const dist = t * length;
|
|
const x = this.epicenter.x + Math.cos(angle) * dist;
|
|
const z = this.epicenter.z + Math.sin(angle) * dist;
|
|
const y = this.terrain.getElevation(x, z) + 0.5;
|
|
|
|
// Add some randomness to crack path
|
|
const offset = (Math.random() - 0.5) * 3;
|
|
points.push(new THREE.Vector3(
|
|
x + Math.cos(angle + Math.PI/2) * offset,
|
|
y,
|
|
z + Math.sin(angle + Math.PI/2) * offset
|
|
));
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
const material = new THREE.LineBasicMaterial({
|
|
color: 0x2d1a0f,
|
|
linewidth: 2
|
|
});
|
|
|
|
const crack = new THREE.Line(geometry, material);
|
|
this.scene.add(crack);
|
|
|
|
// Remove after earthquake
|
|
setTimeout(() => {
|
|
this.scene.remove(crack);
|
|
}, this.duration * 1000);
|
|
}
|
|
}
|
|
|
|
update(delta) {
|
|
if (!this.active) return;
|
|
|
|
this.timer += delta;
|
|
|
|
// Camera shake
|
|
if (this.shakeIntensity > 0.01) {
|
|
const shake = this.shakeIntensity * Math.sin(this.timer * 20);
|
|
this.camera.position.x += (Math.random() - 0.5) * shake;
|
|
this.camera.position.y += (Math.random() - 0.5) * shake;
|
|
this.camera.position.z += (Math.random() - 0.5) * shake;
|
|
}
|
|
|
|
// Update dust
|
|
if (this.dustParticles) {
|
|
const positions = this.dustParticles.geometry.attributes.position.array;
|
|
for (let i = 0; i < this.dustVelocities.length; i++) {
|
|
const idx = i * 3;
|
|
positions[idx] += this.dustVelocities[i].x * delta;
|
|
positions[idx + 1] += this.dustVelocities[i].y * delta;
|
|
positions[idx + 2] += this.dustVelocities[i].z * delta;
|
|
|
|
// Gravity
|
|
this.dustVelocities[i].y -= 1 * delta;
|
|
}
|
|
this.dustParticles.geometry.attributes.position.needsUpdate = true;
|
|
|
|
// Fade out
|
|
this.dustParticles.material.opacity = Math.max(0, 0.6 - (this.timer / this.duration) * 0.6);
|
|
}
|
|
|
|
// End earthquake
|
|
if (this.timer >= this.duration) {
|
|
this.end();
|
|
}
|
|
}
|
|
|
|
end() {
|
|
this.active = false;
|
|
if (this.dustParticles) {
|
|
this.scene.remove(this.dustParticles);
|
|
this.dustParticles = null;
|
|
}
|
|
console.log('Earthquake ended');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* METEOR IMPACT SYSTEM
|
|
*/
|
|
export class MeteorImpactSystem {
|
|
constructor(scene, terrain) {
|
|
this.scene = scene;
|
|
this.terrain = terrain;
|
|
this.meteors = [];
|
|
this.impacts = [];
|
|
}
|
|
|
|
spawnMeteor(targetPosition, size = 1.0) {
|
|
console.log(`☄️ METEOR INCOMING! Size: ${size.toFixed(1)}`);
|
|
|
|
// Start high in sky
|
|
const startPosition = new THREE.Vector3(
|
|
targetPosition.x + (Math.random() - 0.5) * 100,
|
|
200 + Math.random() * 100,
|
|
targetPosition.z + (Math.random() - 0.5) * 100
|
|
);
|
|
|
|
// Create meteor mesh
|
|
const geometry = new THREE.SphereGeometry(size * 5, 16, 16);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0x4a2a1a,
|
|
emissive: 0xff4500,
|
|
emissiveIntensity: 0.8,
|
|
roughness: 0.9
|
|
});
|
|
|
|
const meteor = new THREE.Mesh(geometry, material);
|
|
meteor.position.copy(startPosition);
|
|
meteor.castShadow = true;
|
|
this.scene.add(meteor);
|
|
|
|
// Create trail
|
|
const trailGeometry = new THREE.CylinderGeometry(0.5, size * 2, 20, 8);
|
|
const trailMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xff6600,
|
|
transparent: true,
|
|
opacity: 0.6
|
|
});
|
|
const trail = new THREE.Mesh(trailGeometry, trailMaterial);
|
|
this.scene.add(trail);
|
|
|
|
// Glow light
|
|
const light = new THREE.PointLight(0xff4500, 50, 100);
|
|
meteor.add(light);
|
|
|
|
this.meteors.push({
|
|
mesh: meteor,
|
|
trail,
|
|
light,
|
|
target: targetPosition.clone(),
|
|
velocity: new THREE.Vector3(),
|
|
size,
|
|
age: 0
|
|
});
|
|
}
|
|
|
|
update(delta) {
|
|
// Update falling meteors
|
|
for (let i = this.meteors.length - 1; i >= 0; i--) {
|
|
const meteor = this.meteors[i];
|
|
meteor.age += delta;
|
|
|
|
// Accelerate toward target
|
|
const direction = new THREE.Vector3()
|
|
.subVectors(meteor.target, meteor.mesh.position)
|
|
.normalize();
|
|
|
|
const speed = 50 + meteor.age * 20; // Accelerating
|
|
meteor.velocity.add(direction.multiplyScalar(speed * delta));
|
|
|
|
// Update position
|
|
meteor.mesh.position.add(meteor.velocity.clone().multiplyScalar(delta));
|
|
|
|
// Update trail
|
|
const trailMid = new THREE.Vector3().addVectors(
|
|
meteor.mesh.position,
|
|
meteor.velocity.clone().multiplyScalar(-0.5)
|
|
);
|
|
meteor.trail.position.copy(trailMid);
|
|
meteor.trail.lookAt(meteor.mesh.position);
|
|
meteor.trail.rotateX(Math.PI / 2);
|
|
|
|
// Check for impact
|
|
const groundHeight = this.terrain.getElevation(
|
|
meteor.mesh.position.x,
|
|
meteor.mesh.position.z
|
|
);
|
|
|
|
if (meteor.mesh.position.y <= groundHeight + 5) {
|
|
this.createImpact(meteor.mesh.position, meteor.size);
|
|
this.scene.remove(meteor.mesh);
|
|
this.scene.remove(meteor.trail);
|
|
this.meteors.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Update impact effects
|
|
for (let i = this.impacts.length - 1; i >= 0; i--) {
|
|
const impact = this.impacts[i];
|
|
impact.age += delta;
|
|
|
|
// Expand shockwave
|
|
impact.shockwave.scale.multiplyScalar(1 + delta * 5);
|
|
impact.shockwave.material.opacity = Math.max(0, 1 - impact.age / 3);
|
|
|
|
// Fade dust
|
|
if (impact.dust) {
|
|
impact.dust.material.opacity = Math.max(0, 0.8 - impact.age / 5);
|
|
}
|
|
|
|
// Remove old impacts
|
|
if (impact.age > 5) {
|
|
this.scene.remove(impact.shockwave);
|
|
if (impact.dust) this.scene.remove(impact.dust);
|
|
if (impact.crater) this.scene.remove(impact.crater);
|
|
this.impacts.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
createImpact(position, size) {
|
|
console.log(`💥 IMPACT! Magnitude: ${size.toFixed(1)}`);
|
|
|
|
// Shockwave ring
|
|
const shockwaveGeometry = new THREE.RingGeometry(1, 2, 32);
|
|
const shockwaveMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xff6600,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
opacity: 1
|
|
});
|
|
const shockwave = new THREE.Mesh(shockwaveGeometry, shockwaveMaterial);
|
|
shockwave.rotation.x = -Math.PI / 2;
|
|
shockwave.position.copy(position);
|
|
shockwave.position.y = this.terrain.getElevation(position.x, position.z) + 0.5;
|
|
this.scene.add(shockwave);
|
|
|
|
// Explosion flash
|
|
const flash = new THREE.PointLight(0xffffff, 200 * size, 200 * size);
|
|
flash.position.copy(position);
|
|
this.scene.add(flash);
|
|
setTimeout(() => this.scene.remove(flash), 100);
|
|
|
|
// Dust cloud
|
|
const dustCount = 1000 * size;
|
|
const dustGeometry = new THREE.BufferGeometry();
|
|
const dustPositions = new Float32Array(dustCount * 3);
|
|
|
|
for (let i = 0; i < dustCount; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const radius = Math.random() * size * 10;
|
|
dustPositions[i * 3] = position.x + Math.cos(angle) * radius;
|
|
dustPositions[i * 3 + 1] = position.y + Math.random() * size * 20;
|
|
dustPositions[i * 3 + 2] = position.z + Math.sin(angle) * radius;
|
|
}
|
|
|
|
dustGeometry.setAttribute('position', new THREE.BufferAttribute(dustPositions, 3));
|
|
const dustMaterial = new THREE.PointsMaterial({
|
|
color: 0x5a4a3a,
|
|
size: 2,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
const dust = new THREE.Points(dustGeometry, dustMaterial);
|
|
this.scene.add(dust);
|
|
|
|
// Crater (simplified)
|
|
const craterGeometry = new THREE.CylinderGeometry(
|
|
size * 8,
|
|
size * 5,
|
|
size * 3,
|
|
16,
|
|
1,
|
|
true
|
|
);
|
|
const craterMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x2d1a0f,
|
|
roughness: 1.0
|
|
});
|
|
const crater = new THREE.Mesh(craterGeometry, craterMaterial);
|
|
crater.position.copy(position);
|
|
crater.position.y = this.terrain.getElevation(position.x, position.z) - size * 1.5;
|
|
crater.receiveShadow = true;
|
|
this.scene.add(crater);
|
|
|
|
this.impacts.push({
|
|
shockwave,
|
|
dust,
|
|
crater,
|
|
age: 0,
|
|
size
|
|
});
|
|
}
|
|
|
|
randomImpact(size = 1.0) {
|
|
const x = (Math.random() - 0.5) * 200;
|
|
const z = (Math.random() - 0.5) * 200;
|
|
const y = this.terrain.getElevation(x, z);
|
|
this.spawnMeteor(new THREE.Vector3(x, y, z), size);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EVENT MANAGER
|
|
* Coordinates all catastrophic events
|
|
*/
|
|
export class CatastrophicEventManager {
|
|
constructor(scene, camera, terrain) {
|
|
this.scene = scene;
|
|
this.camera = camera;
|
|
this.terrain = terrain;
|
|
|
|
this.earthquakeSystem = new EarthquakeSystem(scene, camera, terrain);
|
|
this.meteorSystem = new MeteorImpactSystem(scene, terrain);
|
|
|
|
// Event probabilities
|
|
this.eventTimer = 0;
|
|
this.nextEventTime = 30 + Math.random() * 60;
|
|
}
|
|
|
|
update(delta) {
|
|
this.earthquakeSystem.update(delta);
|
|
this.meteorSystem.update(delta);
|
|
|
|
// Random events
|
|
this.eventTimer += delta;
|
|
if (this.eventTimer >= this.nextEventTime) {
|
|
this.triggerRandomEvent();
|
|
this.nextEventTime = 30 + Math.random() * 60;
|
|
this.eventTimer = 0;
|
|
}
|
|
}
|
|
|
|
triggerRandomEvent() {
|
|
const events = ['earthquake', 'meteor'];
|
|
const event = events[Math.floor(Math.random() * events.length)];
|
|
|
|
switch (event) {
|
|
case 'earthquake':
|
|
this.triggerEarthquake();
|
|
break;
|
|
case 'meteor':
|
|
this.triggerMeteorStrike();
|
|
break;
|
|
}
|
|
}
|
|
|
|
triggerEarthquake() {
|
|
const x = this.camera.position.x + (Math.random() - 0.5) * 100;
|
|
const z = this.camera.position.z + (Math.random() - 0.5) * 100;
|
|
const y = this.terrain.getElevation(x, z);
|
|
const magnitude = 4 + Math.random() * 4; // 4-8 magnitude
|
|
|
|
this.earthquakeSystem.trigger(new THREE.Vector3(x, y, z), magnitude);
|
|
}
|
|
|
|
triggerMeteorStrike() {
|
|
const size = 0.5 + Math.random() * 2; // 0.5-2.5 size
|
|
this.meteorSystem.randomImpact(size);
|
|
}
|
|
|
|
isActive() {
|
|
return this.earthquakeSystem.active || this.meteorSystem.meteors.length > 0;
|
|
}
|
|
}
|
|
|
|
export default { EarthquakeSystem, MeteorImpactSystem, CatastrophicEventManager };
|