Files
the-nexus/modules/weather.js

183 lines
6.2 KiB
JavaScript
Raw Normal View History

// === 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);
}