From 940241ce5d9d146bfecfa4a31114e624ed3cdcb7 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 23:08:04 -0400 Subject: [PATCH] feat: re-implement gravity anomaly zones (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GRAVITY_ANOMALY_FLOOR/CEIL constants - Add createGravityZones() — three cylindrical zones with rising particles, floor ring indicators, and translucent disc fills - Add rebuildGravityZones() — repositions zones to match loaded portal positions and tints them to each portal's color/status - Animate particles floating upward with horizontal drift; reset to floor when they exceed ceiling height - Ring/disc opacity pulses subtly via sine wave each frame - respects performanceTier particle budget via particleCount() Refs #478 --- app.js | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/app.js b/app.js index 9a03e40..536fd5f 100644 --- a/app.js +++ b/app.js @@ -39,6 +39,7 @@ let thoughtStreamMesh; let harnessPulseMesh; let powerMeterBars = []; let particles, dustParticles; +let gravityZoneObjects = []; let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; @@ -101,6 +102,7 @@ async function init() { const response = await fetch('./portals.json'); const portalData = await response.json(); createPortals(portalData); + rebuildGravityZones(); } catch (e) { console.error('Failed to load portals.json:', e); addChatMessage('error', 'Portal registry offline. Check logs.'); @@ -118,6 +120,7 @@ async function init() { updateLoad(80); createParticles(); createDustParticles(); + createGravityZones(); updateLoad(85); createAmbientStructures(); createAgentPresences(); @@ -954,6 +957,100 @@ function createDustParticles() { scene.add(dustParticles); } +// ═══ GRAVITY ANOMALY ZONES ═══ +const GRAVITY_ANOMALY_FLOOR = 0.2; +const GRAVITY_ANOMALY_CEIL = 16.0; + +const GRAVITY_ZONE_DEFS = [ + { x: -8, z: -6, radius: 3.5, color: 0x00ffcc, particleCount: 180 }, + { x: 10, z: 4, radius: 3.0, color: 0xaa44ff, particleCount: 160 }, + { x: -3, z: 9, radius: 2.5, color: 0xff8844, particleCount: 140 }, +]; + +function createGravityZones() { + for (const zone of GRAVITY_ZONE_DEFS) { + const ringGeo = new THREE.RingGeometry(zone.radius - 0.15, zone.radius + 0.15, 64); + const ringMat = new THREE.MeshBasicMaterial({ + color: zone.color, transparent: true, opacity: 0.4, + side: THREE.DoubleSide, depthWrite: false, + }); + const ring = new THREE.Mesh(ringGeo, ringMat); + ring.rotation.x = -Math.PI / 2; + ring.position.set(zone.x, GRAVITY_ANOMALY_FLOOR + 0.05, zone.z); + scene.add(ring); + + const discGeo = new THREE.CircleGeometry(zone.radius - 0.15, 64); + const discMat = new THREE.MeshBasicMaterial({ + color: zone.color, transparent: true, opacity: 0.04, + side: THREE.DoubleSide, depthWrite: false, + }); + const disc = new THREE.Mesh(discGeo, discMat); + disc.rotation.x = -Math.PI / 2; + disc.position.set(zone.x, GRAVITY_ANOMALY_FLOOR + 0.04, zone.z); + scene.add(disc); + + const count = particleCount(zone.particleCount); + const positions = new Float32Array(count * 3); + const driftPhases = new Float32Array(count); + const velocities = new Float32Array(count); + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const r = Math.sqrt(Math.random()) * zone.radius; + positions[i * 3] = zone.x + Math.cos(angle) * r; + positions[i * 3 + 1] = GRAVITY_ANOMALY_FLOOR + Math.random() * (GRAVITY_ANOMALY_CEIL - GRAVITY_ANOMALY_FLOOR); + positions[i * 3 + 2] = zone.z + Math.sin(angle) * r; + driftPhases[i] = Math.random() * Math.PI * 2; + velocities[i] = 0.03 + Math.random() * 0.04; + } + + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + + const mat = new THREE.PointsMaterial({ + color: zone.color, size: 0.10, sizeAttenuation: true, + transparent: true, opacity: 0.7, depthWrite: false, + }); + const points = new THREE.Points(geo, mat); + scene.add(points); + + gravityZoneObjects.push({ zone: { ...zone, particleCount: count }, ring, ringMat, disc, discMat, points, geo, driftPhases, velocities }); + } +} + +function rebuildGravityZones() { + if (portals.length === 0) return; + for (let i = 0; i < Math.min(portals.length, gravityZoneObjects.length); i++) { + const portal = portals[i]; + const gz = gravityZoneObjects[i]; + const portalColor = new THREE.Color(portal.config.color); + const isOnline = portal.config.status !== 'offline'; + + gz.ring.position.set(portal.config.position.x, GRAVITY_ANOMALY_FLOOR + 0.05, portal.config.position.z); + gz.disc.position.set(portal.config.position.x, GRAVITY_ANOMALY_FLOOR + 0.04, portal.config.position.z); + + gz.zone.x = portal.config.position.x; + gz.zone.z = portal.config.position.z; + + gz.ringMat.color.copy(portalColor); + gz.discMat.color.copy(portalColor); + gz.points.material.color.copy(portalColor); + + gz.ringMat.opacity = isOnline ? 0.4 : 0.08; + gz.discMat.opacity = isOnline ? 0.04 : 0.01; + gz.points.material.opacity = isOnline ? 0.7 : 0.15; + + const pos = gz.geo.attributes.position.array; + for (let j = 0; j < gz.zone.particleCount; j++) { + const angle = Math.random() * Math.PI * 2; + const r = Math.sqrt(Math.random()) * gz.zone.radius; + pos[j * 3] = gz.zone.x + Math.cos(angle) * r; + pos[j * 3 + 2] = gz.zone.z + Math.sin(angle) * r; + } + gz.geo.attributes.position.needsUpdate = true; + } +} + // ═══ AMBIENT STRUCTURES ═══ function createAmbientStructures() { const crystalMat = new THREE.MeshPhysicalMaterial({ @@ -1429,6 +1526,27 @@ function gameLoop() { portal.pSystem.geometry.attributes.position.needsUpdate = true; }); + // Animate Gravity Anomaly Zones + for (const gz of gravityZoneObjects) { + const pos = gz.geo.attributes.position.array; + const count = gz.zone.particleCount; + for (let i = 0; i < count; i++) { + pos[i * 3 + 1] += gz.velocities[i]; + pos[i * 3] += Math.sin(elapsed * 0.5 + gz.driftPhases[i]) * 0.003; + pos[i * 3 + 2] += Math.cos(elapsed * 0.5 + gz.driftPhases[i]) * 0.003; + if (pos[i * 3 + 1] > GRAVITY_ANOMALY_CEIL) { + const angle = Math.random() * Math.PI * 2; + const r = Math.sqrt(Math.random()) * gz.zone.radius; + pos[i * 3] = gz.zone.x + Math.cos(angle) * r; + pos[i * 3 + 1] = GRAVITY_ANOMALY_FLOOR + Math.random() * 2.0; + pos[i * 3 + 2] = gz.zone.z + Math.sin(angle) * r; + } + } + gz.geo.attributes.position.needsUpdate = true; + gz.ringMat.opacity = 0.3 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.1; + gz.discMat.opacity = 0.02 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.015; + } + // Animate Vision Points visionPoints.forEach(vp => { vp.crystal.rotation.y = elapsed * 0.8; -- 2.43.0