// === WEATHER SYSTEM + PORTAL HEALTH === import * as THREE from 'three'; import { scene, ambientLight } from './scene-setup.js'; import { cloudMaterial } from './platform.js'; import { rebuildRuneRing } from './effects.js'; import { S } from './state.js'; import { fetchWeatherData } from './data/weather.js'; // === PORTAL HEALTH CHECKS === const PORTAL_HEALTH_CHECK_MS = 5 * 60 * 1000; // Forward refs let _portalsRef = []; let _portalGroupRef = null; let _rebuildGravityZonesFn = null; export function setWeatherPortalRefs(portals, portalGroup, rebuildGravityZones) { _portalsRef = portals; _portalGroupRef = portalGroup; _rebuildGravityZonesFn = rebuildGravityZones; } export async function runPortalHealthChecks() { if (_portalsRef.length === 0) return; for (const portal of _portalsRef) { if (!portal.destination?.url) { portal.status = 'offline'; continue; } try { await fetch(portal.destination.url, { mode: 'no-cors', signal: AbortSignal.timeout(5000), }); portal.status = 'online'; } catch { portal.status = 'offline'; } } rebuildRuneRing(); if (_rebuildGravityZonesFn) _rebuildGravityZonesFn(); if (_portalGroupRef) { for (const child of _portalGroupRef.children) { const portalId = child.name.replace('portal-', ''); const portalData = _portalsRef.find(p => p.id === portalId); if (portalData) { const isOnline = portalData.status === 'online'; child.material.opacity = isOnline ? 0.7 : 0.15; } } } } export function initPortalHealthChecks() { setInterval(runPortalHealthChecks, PORTAL_HEALTH_CHECK_MS); } // === WEATHER SYSTEM === const WEATHER_LAT = 43.2897; const WEATHER_LON = -72.1479; const WEATHER_REFRESH_MS = 15 * 60 * 1000; let weatherState = null; export const PRECIP_COUNT = 1200; export const PRECIP_AREA = 18; export const PRECIP_HEIGHT = 20; export const PRECIP_FLOOR = -5; // Rain geometry export const rainGeo = new THREE.BufferGeometry(); const rainPositions = new Float32Array(PRECIP_COUNT * 3); export const rainVelocities = new Float32Array(PRECIP_COUNT); for (let i = 0; i < PRECIP_COUNT; i++) { rainPositions[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2; rainPositions[i * 3 + 1] = Math.random() * (PRECIP_HEIGHT - PRECIP_FLOOR) + PRECIP_FLOOR; rainPositions[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2; rainVelocities[i] = 0.18 + Math.random() * 0.12; } rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3)); const rainMat = new THREE.PointsMaterial({ color: 0x88aaff, size: 0.05, sizeAttenuation: true, transparent: true, opacity: 0.55, }); export const rainParticles = new THREE.Points(rainGeo, rainMat); rainParticles.visible = false; scene.add(rainParticles); // Snow geometry export const snowGeo = new THREE.BufferGeometry(); const snowPositions = new Float32Array(PRECIP_COUNT * 3); export const snowDrift = new Float32Array(PRECIP_COUNT); for (let i = 0; i < PRECIP_COUNT; i++) { snowPositions[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2; snowPositions[i * 3 + 1] = Math.random() * (PRECIP_HEIGHT - PRECIP_FLOOR) + PRECIP_FLOOR; snowPositions[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2; snowDrift[i] = Math.random() * Math.PI * 2; } snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPositions, 3)); const snowMat = new THREE.PointsMaterial({ color: 0xddeeff, size: 0.12, sizeAttenuation: true, transparent: true, opacity: 0.75, }); export const snowParticles = new THREE.Points(snowGeo, snowMat); snowParticles.visible = false; scene.add(snowParticles); function weatherCodeToLabel(code) { if (code === 0) return { condition: 'Clear', icon: '☀️' }; if (code <= 2) return { condition: 'Partly Cloudy', icon: '⛅' }; if (code === 3) return { condition: 'Overcast', icon: '☁️' }; if (code >= 45 && code <= 48) return { condition: 'Fog', icon: '🌫️' }; if (code >= 51 && code <= 57) return { condition: 'Drizzle', icon: '🌦️' }; if (code >= 61 && code <= 67) return { condition: 'Rain', icon: '🌧️' }; if (code >= 71 && code <= 77) return { condition: 'Snow', icon: '❄️' }; if (code >= 80 && code <= 82) return { condition: 'Showers', icon: '🌦️' }; if (code >= 85 && code <= 86) return { condition: 'Snow Showers', icon: '🌨️' }; if (code >= 95 && code <= 99) return { condition: 'Thunderstorm', icon: '⛈️' }; return { condition: 'Unknown', icon: '🌀' }; } function applyWeatherToScene(wx) { const code = wx.code; const isRain = (code >= 51 && code <= 67) || (code >= 80 && code <= 82) || (code >= 95 && code <= 99); const isSnow = (code >= 71 && code <= 77) || (code >= 85 && code <= 86); rainParticles.visible = isRain; snowParticles.visible = isSnow; if (isSnow) { ambientLight.color.setHex(0x1a2a40); ambientLight.intensity = 1.8; } else if (isRain) { ambientLight.color.setHex(0x0a1428); ambientLight.intensity = 1.2; } else if (code === 3 || (code >= 45 && code <= 48)) { ambientLight.color.setHex(0x0c1220); ambientLight.intensity = 1.1; } else { ambientLight.color.setHex(0x0a1428); ambientLight.intensity = 1.4; } } function updateWeatherHUD(wx) { const iconEl = document.getElementById('weather-icon'); const tempEl = document.getElementById('weather-temp'); const descEl = document.getElementById('weather-desc'); if (iconEl) iconEl.textContent = wx.icon; if (tempEl) tempEl.textContent = `${Math.round(wx.temp)}°F`; if (descEl) descEl.textContent = wx.condition; } export async function fetchWeather() { try { const wx = await fetchWeatherData(); weatherState = wx; applyWeatherToScene(wx); const cloudOpacity = 0.05 + (wx.cloudcover / 100) * 0.55; cloudMaterial.uniforms.uDensity.value = 0.3 + (wx.cloudcover / 100) * 0.7; cloudMaterial.opacity = cloudOpacity; updateWeatherHUD(wx); } catch { const descEl = document.getElementById('weather-desc'); if (descEl) descEl.textContent = 'Lempster NH'; } } export function initWeather() { fetchWeather(); setInterval(fetchWeather, WEATHER_REFRESH_MS); }