The Phase 2 data-layer PRs modified these to import from data/ and core/, but those directories were removed in the Manus revert. Restore to the self-contained split-commit versions. panels/ and effects/ subdirectories were Phase 2 extractions not used by the main import chain (app.js -> modules/panels.js, not panels/).
189 lines
6.7 KiB
JavaScript
189 lines
6.7 KiB
JavaScript
// === 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';
|
|
|
|
// === 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 url = `https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}¤t=temperature_2m,weather_code,wind_speed_10m,cloud_cover&temperature_unit=fahrenheit&wind_speed_unit=mph&forecast_days=1`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error('weather fetch failed');
|
|
const data = await res.json();
|
|
const cur = data.current;
|
|
const code = cur.weather_code;
|
|
const { condition, icon } = weatherCodeToLabel(code);
|
|
const cloudcover = typeof cur.cloud_cover === 'number' ? cur.cloud_cover : 50;
|
|
weatherState = { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon, cloudcover };
|
|
applyWeatherToScene(weatherState);
|
|
const cloudOpacity = 0.05 + (cloudcover / 100) * 0.55;
|
|
cloudMaterial.uniforms.uDensity.value = 0.3 + (cloudcover / 100) * 0.7;
|
|
cloudMaterial.opacity = cloudOpacity;
|
|
updateWeatherHUD(weatherState);
|
|
} catch {
|
|
const descEl = document.getElementById('weather-desc');
|
|
if (descEl) descEl.textContent = 'Lempster NH';
|
|
}
|
|
}
|
|
|
|
export function initWeather() {
|
|
fetchWeather();
|
|
setInterval(fetchWeather, WEATHER_REFRESH_MS);
|
|
}
|