refactor: split app.js (5416 lines) into 21 modules — hard cap 1000 lines/file
app.js: 5416 → 528 lines (entry point, animation loop, event wiring)
modules/state.js: shared mutable state object
modules/constants.js: color palette
modules/matrix-rain.js: matrix rain canvas effect
modules/scene-setup.js: scene, camera, renderer, lighting, stars
modules/platform.js: glass platform, perlin noise, floating island, clouds
modules/heatmap.js: commit heatmap
modules/sigil.js: Timmy sigil
modules/controls.js: mouse, overview, zoom, photo mode
modules/effects.js: energy beam, sovereignty meter, rune ring
modules/earth.js: holographic earth
modules/warp.js: warp tunnel, crystals, lightning
modules/dual-brain.js: dual-brain holographic panel
modules/audio.js: Web Audio, spatial, portal hums
modules/debug.js: debug mode, websocket, session export
modules/celebrations.js: easter egg, shockwave, fireworks
modules/portals.js: portal loading
modules/bookshelves.js: floating bookshelves, spine textures
modules/oath.js: The Oath interactive SOUL.md
modules/panels.js: agent status board, LoRA panel
modules/weather.js: weather system, portal health
modules/extras.js: gravity zones, speech, timelapse, bitcoin
Largest file: 528 lines (app.js). No file exceeds 1000.
All files pass node --check. No refactoring — mechanical split only.
2026-03-24 15:12:15 -04:00
|
|
|
// === 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';
|
2026-03-24 18:06:28 -04:00
|
|
|
import { fetchWeatherData } from './data/weather.js';
|
refactor: split app.js (5416 lines) into 21 modules — hard cap 1000 lines/file
app.js: 5416 → 528 lines (entry point, animation loop, event wiring)
modules/state.js: shared mutable state object
modules/constants.js: color palette
modules/matrix-rain.js: matrix rain canvas effect
modules/scene-setup.js: scene, camera, renderer, lighting, stars
modules/platform.js: glass platform, perlin noise, floating island, clouds
modules/heatmap.js: commit heatmap
modules/sigil.js: Timmy sigil
modules/controls.js: mouse, overview, zoom, photo mode
modules/effects.js: energy beam, sovereignty meter, rune ring
modules/earth.js: holographic earth
modules/warp.js: warp tunnel, crystals, lightning
modules/dual-brain.js: dual-brain holographic panel
modules/audio.js: Web Audio, spatial, portal hums
modules/debug.js: debug mode, websocket, session export
modules/celebrations.js: easter egg, shockwave, fireworks
modules/portals.js: portal loading
modules/bookshelves.js: floating bookshelves, spine textures
modules/oath.js: The Oath interactive SOUL.md
modules/panels.js: agent status board, LoRA panel
modules/weather.js: weather system, portal health
modules/extras.js: gravity zones, speech, timelapse, bitcoin
Largest file: 528 lines (app.js). No file exceeds 1000.
All files pass node --check. No refactoring — mechanical split only.
2026-03-24 15:12:15 -04:00
|
|
|
|
|
|
|
|
// === 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 {
|
2026-03-24 18:06:28 -04:00
|
|
|
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;
|
refactor: split app.js (5416 lines) into 21 modules — hard cap 1000 lines/file
app.js: 5416 → 528 lines (entry point, animation loop, event wiring)
modules/state.js: shared mutable state object
modules/constants.js: color palette
modules/matrix-rain.js: matrix rain canvas effect
modules/scene-setup.js: scene, camera, renderer, lighting, stars
modules/platform.js: glass platform, perlin noise, floating island, clouds
modules/heatmap.js: commit heatmap
modules/sigil.js: Timmy sigil
modules/controls.js: mouse, overview, zoom, photo mode
modules/effects.js: energy beam, sovereignty meter, rune ring
modules/earth.js: holographic earth
modules/warp.js: warp tunnel, crystals, lightning
modules/dual-brain.js: dual-brain holographic panel
modules/audio.js: Web Audio, spatial, portal hums
modules/debug.js: debug mode, websocket, session export
modules/celebrations.js: easter egg, shockwave, fireworks
modules/portals.js: portal loading
modules/bookshelves.js: floating bookshelves, spine textures
modules/oath.js: The Oath interactive SOUL.md
modules/panels.js: agent status board, LoRA panel
modules/weather.js: weather system, portal health
modules/extras.js: gravity zones, speech, timelapse, bitcoin
Largest file: 528 lines (app.js). No file exceeds 1000.
All files pass node --check. No refactoring — mechanical split only.
2026-03-24 15:12:15 -04:00
|
|
|
cloudMaterial.opacity = cloudOpacity;
|
2026-03-24 18:06:28 -04:00
|
|
|
updateWeatherHUD(wx);
|
refactor: split app.js (5416 lines) into 21 modules — hard cap 1000 lines/file
app.js: 5416 → 528 lines (entry point, animation loop, event wiring)
modules/state.js: shared mutable state object
modules/constants.js: color palette
modules/matrix-rain.js: matrix rain canvas effect
modules/scene-setup.js: scene, camera, renderer, lighting, stars
modules/platform.js: glass platform, perlin noise, floating island, clouds
modules/heatmap.js: commit heatmap
modules/sigil.js: Timmy sigil
modules/controls.js: mouse, overview, zoom, photo mode
modules/effects.js: energy beam, sovereignty meter, rune ring
modules/earth.js: holographic earth
modules/warp.js: warp tunnel, crystals, lightning
modules/dual-brain.js: dual-brain holographic panel
modules/audio.js: Web Audio, spatial, portal hums
modules/debug.js: debug mode, websocket, session export
modules/celebrations.js: easter egg, shockwave, fireworks
modules/portals.js: portal loading
modules/bookshelves.js: floating bookshelves, spine textures
modules/oath.js: The Oath interactive SOUL.md
modules/panels.js: agent status board, LoRA panel
modules/weather.js: weather system, portal health
modules/extras.js: gravity zones, speech, timelapse, bitcoin
Largest file: 528 lines (app.js). No file exceeds 1000.
All files pass node --check. No refactoring — mechanical split only.
2026-03-24 15:12:15 -04:00
|
|
|
} catch {
|
|
|
|
|
const descEl = document.getElementById('weather-desc');
|
|
|
|
|
if (descEl) descEl.textContent = 'Lempster NH';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function initWeather() {
|
|
|
|
|
fetchWeather();
|
|
|
|
|
setInterval(fetchWeather, WEATHER_REFRESH_MS);
|
|
|
|
|
}
|