fix: restore weather.js + bookshelves.js to inline versions, remove unused panels/ and effects/ subdirs
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Staging Smoke Test / smoke-test (push) Successful in 1s

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/).
This commit is contained in:
Alexander Whitestone
2026-03-24 18:47:16 -04:00
parent 979c7cf96b
commit 4379f70352
14 changed files with 68 additions and 1919 deletions

View File

@@ -2,7 +2,6 @@
import * as THREE from 'three';
import { NEXUS } from './constants.js';
import { scene } from './scene-setup.js';
import { fetchNexusCommits, fetchMergedPRs } from './data/gitea.js';
// === AGENT STATUS PANELS (declared early) ===
export const agentPanelSprites = [];
@@ -38,16 +37,29 @@ function createCommitTexture(hash, message) {
}
export async function initCommitBanners() {
const raw = await fetchNexusCommits(5);
const commits = raw.length > 0
? raw.map(c => ({ hash: c.sha.slice(0, 7), message: c.commit.message.split('\n')[0] }))
: [
{ hash: 'a1b2c3d', message: 'feat: depth of field effect on distant objects' },
{ hash: 'e4f5g6h', message: 'feat: photo mode with orbit controls' },
{ hash: 'i7j8k9l', message: 'feat: sovereignty easter egg animation' },
{ hash: 'm0n1o2p', message: 'feat: overview mode bird\'s-eye view' },
{ hash: 'q3r4s5t', message: 'feat: star field and constellation lines' },
];
let commits;
try {
const res = await fetch(
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=5',
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
);
if (!res.ok) throw new Error('fetch failed');
const data = await res.json();
commits = data.map(c => ({
hash: c.sha.slice(0, 7),
message: c.commit.message.split('\n')[0],
}));
} catch {
commits = [
{ hash: 'a1b2c3d', message: 'feat: depth of field effect on distant objects' },
{ hash: 'e4f5g6h', message: 'feat: photo mode with orbit controls' },
{ hash: 'i7j8k9l', message: 'feat: sovereignty easter egg animation' },
{ hash: 'm0n1o2p', message: 'feat: overview mode bird\'s-eye view' },
{ hash: 'q3r4s5t', message: 'feat: star field and constellation lines' },
];
initCommitBanners();
}
const spreadX = [-7, -3.5, 0, 3.5, 7];
const spreadY = [1.0, -1.5, 2.2, -0.8, 1.6];
@@ -196,8 +208,23 @@ function buildBookshelf(books, position, rotationY) {
}
export async function initBookshelves() {
let prs = await fetchMergedPRs(20);
if (prs.length === 0) {
let prs = [];
try {
const res = await fetch(
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=20',
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
);
if (!res.ok) throw new Error('fetch failed');
const data = await res.json();
prs = data
.filter(p => p.merged)
.map(p => ({
prNum: p.number,
title: p.title
.replace(/^\[[\w\s]+\]\s*/i, '')
.replace(/\s*\(#\d+\)\s*$/, ''),
}));
} catch {
prs = [
{ prNum: 324, title: 'Model training status — LoRA adapters' },
{ prNum: 323, title: 'The Oath — interactive SOUL.md reading' },

View File

@@ -1,56 +0,0 @@
/**
* energy-beam.js — Vertical energy beam above the Batcave terminal
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.activeAgentCount (0 = faint, 3+ = full intensity)
*
* A glowing cyan cylinder rising from the Batcave area.
* Intensity and pulse amplitude are driven by the number of active agents.
*/
import * as THREE from 'three';
const BEAM_RADIUS = 0.2;
const BEAM_HEIGHT = 50;
const BEAM_X = -10;
const BEAM_Y = 0;
const BEAM_Z = -10;
let _state = null;
let _beamMaterial = null;
let _pulse = 0;
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.activeAgentCount)
* @param {object} theme Theme bus (reads theme.colors.accent)
*/
export function init(scene, state, theme) {
_state = state;
const accentColor = theme?.colors?.accent ?? 0x4488ff;
const geo = new THREE.CylinderGeometry(BEAM_RADIUS, BEAM_RADIUS * 2.5, BEAM_HEIGHT, 32, 16, true);
_beamMaterial = new THREE.MeshBasicMaterial({
color: accentColor,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const beam = new THREE.Mesh(geo, _beamMaterial);
beam.position.set(BEAM_X, BEAM_Y + BEAM_HEIGHT / 2, BEAM_Z);
scene.add(beam);
}
export function update(_elapsed, _delta) {
if (!_beamMaterial) return;
_pulse += 0.02;
const agentCount = _state?.activeAgentCount ?? 0;
const agentIntensity = agentCount === 0 ? 0.1 : Math.min(0.1 + agentCount * 0.3, 1.0);
const pulseEffect = Math.sin(_pulse) * 0.15 * agentIntensity;
_beamMaterial.opacity = agentIntensity * 0.6 + pulseEffect;
}

View File

@@ -1,176 +0,0 @@
/**
* gravity-zones.js — Rising particle gravity anomaly zones
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.portals (positions and online status)
*
* Each gravity zone is a glowing floor ring with rising particle streams.
* Zones are initially placed at hardcoded positions, then realigned to portal
* positions when portal data loads. Online portals have brighter/faster anomalies;
* offline portals have dim, slow anomalies.
*/
import * as THREE from 'three';
const ANOMALY_FLOOR = 0.2;
const ANOMALY_CEIL = 16.0;
const DEFAULT_ZONES = [
{ 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 },
];
let _state = null;
let _scene = null;
let _portalsApplied = false;
/**
* @typedef {{
* zone: object,
* ring: THREE.Mesh, ringMat: THREE.MeshBasicMaterial,
* disc: THREE.Mesh, discMat: THREE.MeshBasicMaterial,
* points: THREE.Points, geo: THREE.BufferGeometry,
* driftPhases: Float32Array, velocities: Float32Array
* }} GravityZoneObject
*/
/** @type {GravityZoneObject[]} */
const gravityZoneObjects = [];
function _buildZone(zone) {
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, 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, ANOMALY_FLOOR + 0.04, zone.z);
_scene.add(disc);
const count = 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] = ANOMALY_FLOOR + Math.random() * (ANOMALY_CEIL - 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);
return { zone: { ...zone }, ring, ringMat, disc, discMat, points, geo, driftPhases, velocities };
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.portals)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
for (const zone of DEFAULT_ZONES) {
gravityZoneObjects.push(_buildZone(zone));
}
}
function _applyPortals(portals) {
_portalsApplied = true;
for (let i = 0; i < Math.min(portals.length, gravityZoneObjects.length); i++) {
const portal = portals[i];
const gz = gravityZoneObjects[i];
const isOnline = portal.status === 'online';
const c = new THREE.Color(portal.color);
gz.ring.position.set(portal.position.x, ANOMALY_FLOOR + 0.05, portal.position.z);
gz.disc.position.set(portal.position.x, ANOMALY_FLOOR + 0.04, portal.position.z);
gz.zone.x = portal.position.x;
gz.zone.z = portal.position.z;
gz.zone.color = c.getHex();
gz.ringMat.color.copy(c);
gz.discMat.color.copy(c);
gz.points.material.color.copy(c);
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;
// Reposition particles around portal
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;
}
}
export function update(elapsed, _delta) {
// Align to portal data once it loads
if (!_portalsApplied) {
const portals = _state?.portals ?? [];
if (portals.length > 0) _applyPortals(portals);
}
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] > 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] = ANOMALY_FLOOR + Math.random() * 2.0;
pos[i * 3 + 2] = gz.zone.z + Math.sin(angle) * r;
}
}
gz.geo.attributes.position.needsUpdate = true;
// Breathing glow pulse on ring/disc
gz.ringMat.opacity = 0.3 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.15;
gz.discMat.opacity = 0.02 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.02;
}
}
/**
* Re-align zones to current portal data.
* Call after portal health check updates portal statuses.
*/
export function rebuildFromPortals() {
const portals = _state?.portals ?? [];
if (portals.length > 0) _applyPortals(portals);
}

View File

@@ -1,196 +0,0 @@
/**
* lightning.js — Floating crystals and lightning arcs between them
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.zoneIntensity (commit activity drives arc count + intensity)
*
* Five octahedral crystals float above the platform. Lightning arcs jump
* between them when zone activity is high. Crystal count and colors are
* aligned to the five agent zones.
*/
import * as THREE from 'three';
const CRYSTAL_COUNT = 5;
const CRYSTAL_BASE_POSITIONS = [
new THREE.Vector3(-4.5, 3.2, -3.8),
new THREE.Vector3( 4.8, 2.8, -4.0),
new THREE.Vector3(-5.5, 4.0, 1.5),
new THREE.Vector3( 5.2, 3.5, 2.0),
new THREE.Vector3( 0.0, 5.0, -5.5),
];
// Zone colors: Claude, Timmy, Kimi, Perplexity, center
const CRYSTAL_COLORS = [0xff6440, 0x40a0ff, 0x40ff8c, 0xc840ff, 0xffd700];
const LIGHTNING_POOL_SIZE = 6;
const LIGHTNING_SEGMENTS = 8;
const LIGHTNING_REFRESH_MS = 130;
let _state = null;
/** @type {THREE.Scene|null} */
let _scene = null;
/** @type {Array<{mesh: THREE.Mesh, light: THREE.PointLight, basePos: THREE.Vector3, floatPhase: number, flashStartTime: number}>} */
const crystals = [];
/** @type {THREE.Line[]} */
const lightningArcs = [];
/** @type {Array<{active: boolean, baseOpacity: number, srcIdx: number, dstIdx: number}>} */
const lightningArcMeta = [];
let _lastLightningRefreshTime = 0;
function _totalActivity() {
if (!_state) return 0;
if (typeof _state.totalActivity === 'function') return _state.totalActivity();
const zi = _state.zoneIntensity;
if (!zi) return 0;
const vals = Object.values(zi);
return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
}
function _lerpColor(colorA, colorB, t) {
const ar = (colorA >> 16) & 0xff, ag = (colorA >> 8) & 0xff, ab = colorA & 0xff;
const br = (colorB >> 16) & 0xff, bg = (colorB >> 8) & 0xff, bb = colorB & 0xff;
return (Math.round(ar + (br - ar) * t) << 16) |
(Math.round(ag + (bg - ag) * t) << 8) |
Math.round(ab + (bb - ab) * t);
}
function _buildLightningPath(start, end, jagAmount) {
const out = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
for (let s = 0; s <= LIGHTNING_SEGMENTS; s++) {
const t = s / LIGHTNING_SEGMENTS;
const jag = s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0;
out[s * 3] = start.x + (end.x - start.x) * t + jag;
out[s * 3 + 1] = start.y + (end.y - start.y) * t + jag;
out[s * 3 + 2] = start.z + (end.z - start.z) * t + jag;
}
return out;
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.zoneIntensity)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
const crystalGroup = new THREE.Group();
scene.add(crystalGroup);
for (let i = 0; i < CRYSTAL_COUNT; i++) {
const geo = new THREE.OctahedronGeometry(0.35, 0);
const color = CRYSTAL_COLORS[i];
const mat = new THREE.MeshStandardMaterial({
color,
emissive: new THREE.Color(color).multiplyScalar(0.6),
roughness: 0.05,
metalness: 0.3,
transparent: true,
opacity: 0.88,
});
const mesh = new THREE.Mesh(geo, mat);
const basePos = CRYSTAL_BASE_POSITIONS[i].clone();
mesh.position.copy(basePos);
mesh.userData.zoomLabel = 'Crystal';
crystalGroup.add(mesh);
const light = new THREE.PointLight(color, 0.3, 6);
light.position.copy(basePos);
crystalGroup.add(light);
crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2, flashStartTime: -999 });
}
// Pre-allocate lightning arc pool
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const positions = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.LineBasicMaterial({
color: 0x88ccff,
transparent: true,
opacity: 0.0,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const arc = new THREE.Line(geo, mat);
scene.add(arc);
lightningArcs.push(arc);
lightningArcMeta.push({ active: false, baseOpacity: 0, srcIdx: 0, dstIdx: 0 });
}
}
function _refreshLightningArcs(elapsed) {
const activity = _totalActivity();
const activeCount = Math.round(activity * LIGHTNING_POOL_SIZE);
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const arc = lightningArcs[i];
const meta = lightningArcMeta[i];
if (i >= activeCount) {
arc.material.opacity = 0;
meta.active = false;
continue;
}
const a = Math.floor(Math.random() * CRYSTAL_COUNT);
let b = Math.floor(Math.random() * (CRYSTAL_COUNT - 1));
if (b >= a) b++;
const jagAmount = 0.45 + activity * 0.85;
const path = _buildLightningPath(crystals[a].mesh.position, crystals[b].mesh.position, jagAmount);
const attr = arc.geometry.attributes.position;
attr.array.set(path);
attr.needsUpdate = true;
arc.material.color.setHex(_lerpColor(CRYSTAL_COLORS[a], CRYSTAL_COLORS[b], 0.5));
const base = (0.35 + Math.random() * 0.55) * Math.min(activity * 1.5, 1.0);
arc.material.opacity = base;
meta.active = true;
meta.baseOpacity = base;
meta.srcIdx = a;
meta.dstIdx = b;
crystals[a].flashStartTime = elapsed;
crystals[b].flashStartTime = elapsed;
}
}
export function update(elapsed, _delta) {
const activity = _totalActivity();
// Float crystals
for (let i = 0; i < crystals.length; i++) {
const c = crystals[i];
c.mesh.position.y = c.basePos.y + Math.sin(elapsed * 0.7 + c.floatPhase) * 0.3;
c.light.position.y = c.mesh.position.y;
// Brief emissive flash on lightning strike
const flashAge = elapsed - c.flashStartTime;
const flashIntensity = flashAge < 0.15 ? (1.0 - flashAge / 0.15) : 0;
c.mesh.material.emissiveIntensity = 0.6 + flashIntensity * 1.2;
c.light.intensity = 0.3 + flashIntensity * 1.5;
// Color intensity tethered to total activity
c.mesh.material.opacity = 0.7 + activity * 0.18;
}
// Flicker active arcs
for (let i = 0; i < lightningArcMeta.length; i++) {
const meta = lightningArcMeta[i];
if (!meta.active) continue;
lightningArcs[i].material.opacity = meta.baseOpacity * (0.7 + Math.random() * 0.3);
}
// Periodically rebuild arcs
if (elapsed * 1000 - _lastLightningRefreshTime > LIGHTNING_REFRESH_MS) {
_lastLightningRefreshTime = elapsed * 1000;
_refreshLightningArcs(elapsed);
}
}

View File

@@ -1,106 +0,0 @@
/**
* matrix-rain.js — Commit-density-driven 2D canvas matrix rain
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.zoneIntensity (commit activity) + state.commitHashes
*
* Renders a Katakana/hex character rain behind the Three.js canvas.
* Density and speed are tethered to commit zone activity.
* Real commit hashes are occasionally injected as characters.
*/
const MATRIX_CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF';
const MATRIX_FONT_SIZE = 14;
let _state = null;
let _canvas = null;
let _ctx = null;
let _drops = [];
/**
* Computes mean activity [0..1] across all agent zones via state.
* @returns {number}
*/
function _totalActivity() {
if (!_state) return 0;
if (typeof _state.totalActivity === 'function') return _state.totalActivity();
const zi = _state.zoneIntensity;
if (!zi) return 0;
const vals = Object.values(zi);
return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
}
function _draw() {
if (!_canvas || !_ctx) return;
const activity = _totalActivity();
const commitHashes = _state?.commitHashes ?? [];
// Fade previous frame — creates the trailing glow
_ctx.fillStyle = 'rgba(0, 0, 8, 0.05)';
_ctx.fillRect(0, 0, _canvas.width, _canvas.height);
_ctx.font = `${MATRIX_FONT_SIZE}px monospace`;
const density = 0.1 + activity * 0.9;
const activeColCount = Math.max(1, Math.floor(_drops.length * density));
for (let i = 0; i < _drops.length; i++) {
if (i >= activeColCount) {
if (_drops[i] * MATRIX_FONT_SIZE > _canvas.height) continue;
}
let char;
if (commitHashes.length > 0 && Math.random() < 0.02) {
const hash = commitHashes[Math.floor(Math.random() * commitHashes.length)];
char = hash[Math.floor(Math.random() * hash.length)];
} else {
char = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
}
_ctx.fillStyle = '#aaffaa';
_ctx.fillText(char, i * MATRIX_FONT_SIZE, _drops[i] * MATRIX_FONT_SIZE);
const resetThreshold = 0.975 - activity * 0.015;
if (_drops[i] * MATRIX_FONT_SIZE > _canvas.height && Math.random() > resetThreshold) {
_drops[i] = 0;
}
_drops[i]++;
}
}
function _resetDrops() {
const colCount = Math.floor(window.innerWidth / MATRIX_FONT_SIZE);
_drops = new Array(colCount).fill(1);
}
/**
* @param {THREE.Scene} _scene (unused — 2D canvas effect)
* @param {object} state Shared state bus
* @param {object} _theme (unused — color is hardcoded green for matrix aesthetic)
*/
export function init(_scene, state, _theme) {
_state = state;
_canvas = document.createElement('canvas');
_canvas.id = 'matrix-rain';
_canvas.width = window.innerWidth;
_canvas.height = window.innerHeight;
document.body.appendChild(_canvas);
_ctx = _canvas.getContext('2d');
_resetDrops();
window.addEventListener('resize', () => {
_canvas.width = window.innerWidth;
_canvas.height = window.innerHeight;
_resetDrops();
});
// Run at ~20 fps independent of the Three.js RAF loop
setInterval(_draw, 50);
}
/**
* update() is a no-op — rain runs on its own setInterval.
*/
export function update(_elapsed, _delta) {}

View File

@@ -1,138 +0,0 @@
/**
* rune-ring.js — Orbiting Elder Futhark rune sprites
*
* Category: DATA-TETHERED AESTHETIC
* Data source: state.portals (count, colors, and online status from portals.json)
*
* Rune sprites orbit the scene in a ring. Count matches the portal count,
* colors come from portal colors, and brightness reflects portal online status.
* A faint torus marks the orbit track.
*/
import * as THREE from 'three';
const RUNE_RING_RADIUS = 7.0;
const RUNE_RING_Y = 1.5;
const RUNE_ORBIT_SPEED = 0.08; // radians per second
const DEFAULT_RUNE_COUNT = 12;
const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','','','ᚹ','ᚺ','ᚾ','','ᛃ'];
const FALLBACK_COLORS = ['#00ffcc', '#ff44ff'];
let _scene = null;
let _state = null;
/** @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number, portalOnline: boolean}>} */
const runeSprites = [];
let _orbitRingMesh = null;
let _builtForPortalCount = -1;
function _createRuneTexture(glyph, color) {
const W = 128, H = 128;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
ctx.shadowColor = color;
ctx.shadowBlur = 28;
ctx.font = 'bold 78px serif';
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(glyph, W / 2, H / 2);
return new THREE.CanvasTexture(canvas);
}
function _clearSprites() {
for (const rune of runeSprites) {
_scene.remove(rune.sprite);
if (rune.sprite.material.map) rune.sprite.material.map.dispose();
rune.sprite.material.dispose();
}
runeSprites.length = 0;
}
function _build(portals) {
_clearSprites();
const count = portals ? portals.length : DEFAULT_RUNE_COUNT;
_builtForPortalCount = count;
for (let i = 0; i < count; i++) {
const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length];
const color = portals ? portals[i].color : FALLBACK_COLORS[i % FALLBACK_COLORS.length];
const isOnline = portals ? portals[i].status === 'online' : true;
const texture = _createRuneTexture(glyph, color);
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: isOnline ? 1.0 : 0.15,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const sprite = new THREE.Sprite(mat);
sprite.scale.set(1.3, 1.3, 1);
const baseAngle = (i / count) * Math.PI * 2;
sprite.position.set(
Math.cos(baseAngle) * RUNE_RING_RADIUS,
RUNE_RING_Y,
Math.sin(baseAngle) * RUNE_RING_RADIUS
);
_scene.add(sprite);
runeSprites.push({ sprite, baseAngle, floatPhase: (i / count) * Math.PI * 2, portalOnline: isOnline });
}
}
/**
* @param {THREE.Scene} scene
* @param {object} state Shared state bus (reads state.portals)
* @param {object} _theme
*/
export function init(scene, state, _theme) {
_scene = scene;
_state = state;
// Faint orbit track torus
const ringGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.03, 6, 64);
const ringMat = new THREE.MeshBasicMaterial({ color: 0x224466, transparent: true, opacity: 0.22 });
_orbitRingMesh = new THREE.Mesh(ringGeo, ringMat);
_orbitRingMesh.rotation.x = Math.PI / 2;
_orbitRingMesh.position.y = RUNE_RING_Y;
scene.add(_orbitRingMesh);
// Initial build with defaults — will be rebuilt when portals load
_build(null);
}
export function update(elapsed, _delta) {
// Rebuild rune sprites when portal data changes
const portals = _state?.portals ?? [];
if (portals.length > 0 && portals.length !== _builtForPortalCount) {
_build(portals);
}
// Orbit and float
for (const rune of runeSprites) {
const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED;
rune.sprite.position.x = Math.cos(angle) * RUNE_RING_RADIUS;
rune.sprite.position.z = Math.sin(angle) * RUNE_RING_RADIUS;
rune.sprite.position.y = RUNE_RING_Y + Math.sin(elapsed * 0.7 + rune.floatPhase) * 0.4;
const baseOpacity = rune.portalOnline ? 0.85 : 0.12;
const pulseRange = rune.portalOnline ? 0.15 : 0.03;
rune.sprite.material.opacity = baseOpacity + Math.sin(elapsed * 1.2 + rune.floatPhase) * pulseRange;
}
}
/**
* Force a rebuild from current portal data.
* Called externally after portal health checks update statuses.
*/
export function rebuild() {
const portals = _state?.portals ?? [];
_build(portals.length > 0 ? portals : null);
}

View File

@@ -1,183 +0,0 @@
/**
* shockwave.js — Shockwave ripple, fireworks, and merge flash
*
* Category: DATA-TETHERED AESTHETIC
* Data source: PR merge events (WebSocket/event dispatch)
*
* Triggered externally on merge events:
* - triggerShockwave() — expanding concentric ring waves from scene centre
* - triggerFireworks() — multi-burst particle fireworks above the platform
* - triggerMergeFlash() — both of the above + star/constellation color flash
*
* The merge flash accepts optional callbacks so terrain/stars.js can own
* its own state while shockwave.js coordinates the event.
*/
import * as THREE from 'three';
const SHOCKWAVE_RING_COUNT = 3;
const SHOCKWAVE_MAX_RADIUS = 14;
const SHOCKWAVE_DURATION = 2.5; // seconds
const FIREWORK_COLORS = [0xff4466, 0xffaa00, 0x00ffaa, 0x4488ff, 0xff44ff, 0xffff44, 0x00ffff];
const FIREWORK_BURST_PARTICLES = 80;
const FIREWORK_BURST_DURATION = 2.2; // seconds
const FIREWORK_GRAVITY = -5.0;
let _scene = null;
let _clock = null;
/**
* @typedef {{mesh: THREE.Mesh, mat: THREE.MeshBasicMaterial, startTime: number, delay: number}} ShockwaveRing
* @typedef {{points: THREE.Points, geo: THREE.BufferGeometry, mat: THREE.PointsMaterial, origins: Float32Array, velocities: Float32Array, startTime: number}} FireworkBurst
*/
/** @type {ShockwaveRing[]} */
const shockwaveRings = [];
/** @type {FireworkBurst[]} */
const fireworkBursts = [];
/**
* Optional callbacks injected via init() for the merge flash star/constellation effect.
* terrain/stars.js can register its own handler when it is initialized.
* @type {Array<() => void>}
*/
const _mergeFlashCallbacks = [];
/**
* @param {THREE.Scene} scene
* @param {object} _state (unused — triggered by events, not state polling)
* @param {object} _theme
* @param {{ clock: THREE.Clock }} options Pass the shared clock in.
*/
export function init(scene, _state, _theme, options = {}) {
_scene = scene;
_clock = options.clock ?? new THREE.Clock();
}
/**
* Register an external callback to be called during triggerMergeFlash().
* Use this to let other modules (stars, constellation lines) animate their own flash.
* @param {() => void} fn
*/
export function onMergeFlash(fn) {
_mergeFlashCallbacks.push(fn);
}
export function triggerShockwave() {
if (!_scene || !_clock) return;
const now = _clock.getElapsedTime();
for (let i = 0; i < SHOCKWAVE_RING_COUNT; i++) {
const mat = new THREE.MeshBasicMaterial({
color: 0x00ffff, transparent: true, opacity: 0,
side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending,
});
const geo = new THREE.RingGeometry(0.9, 1.0, 64);
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2;
mesh.position.y = 0.02;
_scene.add(mesh);
shockwaveRings.push({ mesh, mat, startTime: now, delay: i * 0.35 });
}
}
function _spawnFireworkBurst(origin, color) {
if (!_scene || !_clock) return;
const now = _clock.getElapsedTime();
const count = FIREWORK_BURST_PARTICLES;
const positions = new Float32Array(count * 3);
const origins = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const speed = 2.5 + Math.random() * 3.5;
velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
velocities[i * 3 + 1] = Math.sin(phi) * Math.sin(theta) * speed;
velocities[i * 3 + 2] = Math.cos(phi) * speed;
origins[i * 3] = origin.x;
origins[i * 3 + 1] = origin.y;
origins[i * 3 + 2] = origin.z;
positions[i * 3] = origin.x;
positions[i * 3 + 1] = origin.y;
positions[i * 3 + 2] = origin.z;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color, size: 0.35, sizeAttenuation: true,
transparent: true, opacity: 1.0,
blending: THREE.AdditiveBlending, depthWrite: false,
});
const points = new THREE.Points(geo, mat);
_scene.add(points);
fireworkBursts.push({ points, geo, mat, origins, velocities, startTime: now });
}
export function triggerFireworks() {
for (let i = 0; i < 6; i++) {
const delay = i * 0.35;
setTimeout(() => {
const x = (Math.random() - 0.5) * 12;
const y = 8 + Math.random() * 6;
const z = (Math.random() - 0.5) * 12;
const color = FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)];
_spawnFireworkBurst(new THREE.Vector3(x, y, z), color);
}, delay * 1000);
}
}
export function triggerMergeFlash() {
triggerShockwave();
// Notify registered handlers (e.g. terrain/stars.js)
for (const fn of _mergeFlashCallbacks) fn();
}
export function update(elapsed, _delta) {
// Animate shockwave rings
for (let i = shockwaveRings.length - 1; i >= 0; i--) {
const ring = shockwaveRings[i];
const age = elapsed - ring.startTime - ring.delay;
if (age < 0) continue;
const t = Math.min(age / SHOCKWAVE_DURATION, 1);
if (t >= 1) {
_scene.remove(ring.mesh);
ring.mesh.geometry.dispose();
ring.mat.dispose();
shockwaveRings.splice(i, 1);
continue;
}
const eased = 1 - Math.pow(1 - t, 2);
ring.mesh.scale.setScalar(eased * SHOCKWAVE_MAX_RADIUS + 0.1);
ring.mat.opacity = (1 - t) * 0.9;
}
// Animate firework bursts
for (let i = fireworkBursts.length - 1; i >= 0; i--) {
const burst = fireworkBursts[i];
const age = elapsed - burst.startTime;
const t = Math.min(age / FIREWORK_BURST_DURATION, 1);
if (t >= 1) {
_scene.remove(burst.points);
burst.geo.dispose();
burst.mat.dispose();
fireworkBursts.splice(i, 1);
continue;
}
burst.mat.opacity = t < 0.6 ? 1.0 : (1.0 - t) / 0.4;
const pos = burst.geo.attributes.position.array;
const vel = burst.velocities;
const org = burst.origins;
const halfGAge2 = 0.5 * FIREWORK_GRAVITY * age * age;
for (let j = 0; j < FIREWORK_BURST_PARTICLES; j++) {
pos[j * 3] = org[j * 3] + vel[j * 3] * age;
pos[j * 3 + 1] = org[j * 3 + 1] + vel[j * 3 + 1] * age + halfGAge2;
pos[j * 3 + 2] = org[j * 3 + 2] + vel[j * 3 + 2] * age;
}
burst.geo.attributes.position.needsUpdate = true;
}
}

View File

@@ -1,191 +0,0 @@
// modules/panels/agent-board.js — Agent status holographic board
// Reads state.agentStatus (populated by data/gitea.js) and renders one floating
// sprite panel per agent. Board arcs behind the platform on the negative-Z side.
//
// Data category: REAL
// Data source: state.agentStatus (Gitea commits + open PRs via data/gitea.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const BOARD_RADIUS = 9.5;
const BOARD_Y = 4.2;
const BOARD_SPREAD = Math.PI * 0.75; // 135° arc, centred on -Z
const STATUS_COLOR = {
working: NEXUS.theme.agentWorking,
idle: NEXUS.theme.agentIdle,
dormant: NEXUS.theme.agentDormant,
dead: NEXUS.theme.agentDead,
unreachable: NEXUS.theme.agentDead,
};
let _group, _scene;
let _lastAgentStatus = null;
let _sprites = [];
/**
* Builds a canvas texture for a single agent holo-panel.
* @param {{ name: string, status: string, issue: string|null, prs_today: number, local: boolean }} agent
* @returns {THREE.CanvasTexture}
*/
function _makeTexture(agent) {
const W = 400, H = 200;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
const sc = STATUS_COLOR[agent.status] || NEXUS.theme.accentStr;
const font = NEXUS.theme.fontMono;
ctx.fillStyle = 'rgba(0, 8, 24, 0.88)';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = sc;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.globalAlpha = 0.3;
ctx.strokeRect(4, 4, W - 8, H - 8);
ctx.globalAlpha = 1.0;
// Agent name
ctx.font = `bold 28px ${font}`;
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.fillText(agent.name.toUpperCase(), 16, 44);
// Status dot
ctx.beginPath();
ctx.arc(W - 30, 26, 10, 0, Math.PI * 2);
ctx.fillStyle = sc;
ctx.fill();
// Status label
ctx.font = `13px ${font}`;
ctx.fillStyle = sc;
ctx.textAlign = 'right';
ctx.fillText(agent.status.toUpperCase(), W - 16, 60);
// Separator
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.lineWidth = 1;
ctx.textAlign = 'left';
ctx.beginPath(); ctx.moveTo(16, 70); ctx.lineTo(W - 16, 70); ctx.stroke();
// Current issue
ctx.font = `10px ${font}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.fillText('CURRENT ISSUE', 16, 90);
ctx.font = `13px ${font}`;
ctx.fillStyle = NEXUS.theme.panelText;
const raw = agent.issue || '\u2014 none \u2014';
ctx.fillText(raw.length > 40 ? raw.slice(0, 40) + '\u2026' : raw, 16, 110);
// Separator
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.beginPath(); ctx.moveTo(16, 128); ctx.lineTo(W - 16, 128); ctx.stroke();
// PRs label + count
ctx.font = `10px ${font}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.fillText('PRs MERGED TODAY', 16, 148);
ctx.font = `bold 28px ${font}`;
ctx.fillStyle = NEXUS.theme.accentStr;
ctx.fillText(String(agent.prs_today), 16, 182);
// Runtime indicator
const isLocal = agent.local === true;
const rtColor = isLocal ? NEXUS.theme.agentWorking : NEXUS.theme.agentDead;
const rtLabel = isLocal ? 'LOCAL' : 'CLOUD';
ctx.font = `10px ${font}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'right';
ctx.fillText('RUNTIME', W - 16, 148);
ctx.font = `bold 13px ${font}`;
ctx.fillStyle = rtColor;
ctx.fillText(rtLabel, W - 28, 172);
ctx.textAlign = 'left';
ctx.beginPath();
ctx.arc(W - 16, 167, 6, 0, Math.PI * 2);
ctx.fillStyle = rtColor;
ctx.fill();
return new THREE.CanvasTexture(canvas);
}
function _rebuild(statusData) {
// Remove old sprites
while (_group.children.length) _group.remove(_group.children[0]);
for (const s of _sprites) {
if (s.material.map) s.material.map.dispose();
s.material.dispose();
}
_sprites = [];
const agents = statusData.agents;
const n = agents.length;
agents.forEach((agent, i) => {
const t = n === 1 ? 0.5 : i / (n - 1);
const angle = Math.PI + (t - 0.5) * BOARD_SPREAD;
const x = Math.cos(angle) * BOARD_RADIUS;
const z = Math.sin(angle) * BOARD_RADIUS;
const texture = _makeTexture(agent);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.93, depthWrite: false });
const sprite = new THREE.Sprite(material);
sprite.scale.set(6.4, 3.2, 1);
sprite.position.set(x, BOARD_Y, z);
sprite.userData = {
baseY: BOARD_Y,
floatPhase: (i / n) * Math.PI * 2,
floatSpeed: 0.18 + i * 0.04,
zoomLabel: `Agent: ${agent.name}`,
};
_group.add(sprite);
_sprites.push(sprite);
});
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
scene.add(_group);
// If state already has agent data (unlikely on first load, but handle it)
if (state.agentStatus) {
_rebuild(state.agentStatus);
_lastAgentStatus = state.agentStatus;
}
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} delta
*/
export function update(elapsed, delta) {
// Rebuild board when state.agentStatus changes
if (state.agentStatus && state.agentStatus !== _lastAgentStatus) {
_rebuild(state.agentStatus);
_lastAgentStatus = state.agentStatus;
}
// Animate gentle float
for (const sprite of _sprites) {
const ud = sprite.userData;
sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.15;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
}

View File

@@ -1,200 +0,0 @@
// modules/panels/dual-brain.js — Dual-Brain Status holographic panel
// Shows the Brain Gap Scorecard with two glowing brain orbs.
// Displayed as HONEST-OFFLINE: the dual-brain system is not yet deployed.
// Brain pulse particles are set to ZERO — will flow when system comes online.
//
// Data category: HONEST-OFFLINE
// Data source: — (dual-brain system not deployed; shows "AWAITING DEPLOYMENT")
import * as THREE from 'three';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const ORIGIN = new THREE.Vector3(10, 3, -8);
const OFFLINE_COLOR = NEXUS.theme.agentDormantHex; // dim blue — system offline
const ACCENT = NEXUS.theme.accentStr;
const FONT = NEXUS.theme.fontMono;
let _group, _sprite, _scanSprite, _scanCanvas, _scanCtx, _scanTexture;
let _cloudOrb, _localOrb;
let _scene;
function _buildPanelTexture() {
const W = 512, H = 512;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = NEXUS.theme.panelBg;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = ACCENT;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.strokeStyle = '#223366';
ctx.lineWidth = 1;
ctx.strokeRect(5, 5, W - 10, H - 10);
// Title
ctx.font = `bold 22px ${FONT}`;
ctx.fillStyle = '#88ccff';
ctx.textAlign = 'center';
ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40);
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.beginPath(); ctx.moveTo(20, 52); ctx.lineTo(W - 20, 52); ctx.stroke();
// Section header
ctx.font = `11px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'left';
ctx.fillText('BRAIN GAP SCORECARD', 20, 74);
const categories = ['Triage', 'Tool Use', 'Code Gen', 'Planning', 'Communication', 'Reasoning'];
const barX = 20, barW = W - 130, barH = 20;
let y = 90;
for (const cat of categories) {
ctx.font = `13px ${FONT}`;
ctx.fillStyle = NEXUS.theme.agentDormant;
ctx.textAlign = 'left';
ctx.fillText(cat, barX, y + 14);
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.textAlign = 'right';
ctx.fillText('\u2014', W - 20, y + 14); // em dash — no data
y += 22;
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
ctx.fillRect(barX, y, barW, barH); // empty bar background only
y += barH + 12;
}
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
ctx.beginPath(); ctx.moveTo(20, y + 4); ctx.lineTo(W - 20, y + 4); ctx.stroke();
y += 22;
// Honest offline status
ctx.font = `bold 18px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.textAlign = 'center';
ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#223344';
ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32);
// Brain indicators — offline dim
y += 52;
ctx.beginPath();
ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.fill();
ctx.font = `11px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.textAlign = 'left';
ctx.fillText('CLOUD', W / 2 - 48, y + 12);
ctx.beginPath();
ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = NEXUS.theme.panelVeryDim;
ctx.fill();
ctx.fillText('LOCAL', W / 2 + 42, y + 12);
return new THREE.CanvasTexture(canvas);
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
_group.position.copy(ORIGIN);
_group.lookAt(0, 3, 0);
scene.add(_group);
// Static panel sprite
const texture = _buildPanelTexture();
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.92, depthWrite: false });
_sprite = new THREE.Sprite(material);
_sprite.scale.set(5.0, 5.0, 1);
_sprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' };
_group.add(_sprite);
// Accent light
const light = new THREE.PointLight(NEXUS.theme.accent, 0.6, 10);
light.position.set(0, 0.5, 1);
_group.add(light);
// Offline brain orbs — dim
const orbGeo = new THREE.SphereGeometry(0.35, 32, 32);
const orbMat = (color) => new THREE.MeshStandardMaterial({
color, emissive: new THREE.Color(color), emissiveIntensity: 0.1,
metalness: 0.3, roughness: 0.2, transparent: true, opacity: 0.85,
});
_cloudOrb = new THREE.Mesh(orbGeo, orbMat(OFFLINE_COLOR));
_cloudOrb.position.set(-2.0, 3.0, 0);
_cloudOrb.userData.zoomLabel = 'Cloud Brain';
_group.add(_cloudOrb);
_localOrb = new THREE.Mesh(orbGeo.clone(), orbMat(OFFLINE_COLOR));
_localOrb.position.set(2.0, 3.0, 0);
_localOrb.userData.zoomLabel = 'Local Brain';
_group.add(_localOrb);
// Brain pulse particles — ZERO count (system offline)
const particleGeo = new THREE.BufferGeometry();
particleGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3));
const particleMat = new THREE.PointsMaterial({
color: 0x44ddff, size: 0.08, sizeAttenuation: true,
transparent: true, opacity: 0.8, depthWrite: false,
});
_group.add(new THREE.Points(particleGeo, particleMat));
// Scan line overlay
_scanCanvas = document.createElement('canvas');
_scanCanvas.width = 512;
_scanCanvas.height = 512;
_scanCtx = _scanCanvas.getContext('2d');
_scanTexture = new THREE.CanvasTexture(_scanCanvas);
const scanMat = new THREE.SpriteMaterial({
map: _scanTexture, transparent: true, opacity: 0.18, depthWrite: false,
});
_scanSprite = new THREE.Sprite(scanMat);
_scanSprite.scale.set(5.0, 5.0, 1);
_scanSprite.position.set(0, 0, 0.01);
_group.add(_scanSprite);
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
// Gentle float animation
const ud = _sprite.userData;
_sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.08;
// Scan line — horizontal sweep
const W = 512, H = 512;
_scanCtx.clearRect(0, 0, W, H);
const scanY = ((elapsed * 60) % H);
const grad = _scanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20);
grad.addColorStop(0, 'rgba(68, 136, 255, 0)');
grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.4)');
grad.addColorStop(1, 'rgba(68, 136, 255, 0)');
_scanCtx.fillStyle = grad;
_scanCtx.fillRect(0, scanY - 20, W, 40);
_scanTexture.needsUpdate = true;
}
export function dispose() {
if (_group) _scene.remove(_group);
if (_scanTexture) _scanTexture.dispose();
}

View File

@@ -1,212 +0,0 @@
// modules/panels/earth.js — Holographic Earth floating above the Nexus
// A procedural planet Earth with continent noise, scan lines, and fresnel rim glow.
// Rotation speed is tethered to state.totalActivity() — more commits = faster spin.
// Lat/lon grid, atmosphere shell, and a tether beam to the platform center.
//
// Data category: DATA-TETHERED AESTHETIC
// Data source: state.totalActivity() (computed from state.zoneIntensity)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const EARTH_RADIUS = 2.8;
const EARTH_Y = 20.0;
const EARTH_AXIAL_TILT = 23.4 * (Math.PI / 180);
const ROTATION_SPEED_BASE = 0.02; // rad/s minimum
const ROTATION_SPEED_MAX = 0.08; // rad/s at full activity
let _group, _surfaceMat, _scene;
const _vertexShader = `
varying vec3 vNormal;
varying vec3 vWorldPos;
varying vec2 vUv;
void main() {
vNormal = normalize(normalMatrix * normal);
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const _fragmentShader = `
uniform float uTime;
uniform vec3 uOceanColor;
uniform vec3 uLandColor;
uniform vec3 uGlowColor;
varying vec3 vNormal;
varying vec3 vWorldPos;
varying vec2 vUv;
vec3 _m3(vec3 x){ return x - floor(x*(1./289.))*289.; }
vec4 _m4(vec4 x){ return x - floor(x*(1./289.))*289.; }
vec4 _p4(vec4 x){ return _m4((x*34.+1.)*x); }
float snoise(vec3 v){
const vec2 C = vec2(1./6., 1./3.);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - 0.5;
i = _m3(i);
vec4 p = _p4(_p4(_p4(
i.z+vec4(0.,i1.z,i2.z,1.))+
i.y+vec4(0.,i1.y,i2.y,1.))+
i.x+vec4(0.,i1.x,i2.x,1.)));
float n_ = .142857142857;
vec3 ns = n_*vec3(2.,0.,-1.)+vec3(0.,-.5,1.);
vec4 j = p - 49.*floor(p*ns.z*ns.z);
vec4 x_ = floor(j*ns.z);
vec4 y_ = floor(j - 7.*x_);
vec4 h = 1. - abs(x_*(2./7.)) - abs(y_*(2./7.));
vec4 b0 = vec4(x_.xy,y_.xy)*(2./7.);
vec4 b1 = vec4(x_.zw,y_.zw)*(2./7.);
vec4 s0 = floor(b0)*2.+1.; vec4 s1 = floor(b1)*2.+1.;
vec4 sh = -step(h, vec4(0.));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
vec3 p0=vec3(a0.xy,h.x); vec3 p1=vec3(a0.zw,h.y);
vec3 p2=vec3(a1.xy,h.z); vec3 p3=vec3(a1.zw,h.w);
vec4 nm = max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.);
vec4 nr = 1.79284291400159-0.85373472095314*nm;
p0*=nr.x; p1*=nr.y; p2*=nr.z; p3*=nr.w;
nm = nm*nm;
return 42.*dot(nm*nm, vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
}
void main() {
vec3 n = normalize(vNormal);
vec3 vd = normalize(cameraPosition - vWorldPos);
float lat = (vUv.y - 0.5) * 3.14159265;
float lon = vUv.x * 6.28318530;
vec3 sp = vec3(cos(lat)*cos(lon), sin(lat), cos(lat)*sin(lon));
float c = snoise(sp*1.8)*0.60 + snoise(sp*3.6)*0.30 + snoise(sp*7.2)*0.10;
float land = smoothstep(0.05, 0.30, c);
vec3 surf = mix(uOceanColor, uLandColor, land);
surf = mix(surf, uGlowColor * 0.45, 0.38);
float scan = 0.5 + 0.5*sin(vUv.y * 220.0 + uTime * 1.8);
scan = smoothstep(0.30, 0.70, scan) * 0.14;
float fresnel = pow(1.0 - max(dot(n, vd), 0.0), 4.0);
vec3 col = surf + scan*uGlowColor*0.9 + fresnel*uGlowColor*1.5;
float alpha = 0.48 + fresnel * 0.42;
gl_FragColor = vec4(col, alpha);
}
`;
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
_group.position.set(0, EARTH_Y, 0);
_group.rotation.z = EARTH_AXIAL_TILT;
// Surface shader
_surfaceMat = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0.0 },
uOceanColor: { value: new THREE.Color(NEXUS.theme.earthOcean) },
uLandColor: { value: new THREE.Color(NEXUS.theme.earthLand) },
uGlowColor: { value: new THREE.Color(NEXUS.theme.earthGlow) },
},
vertexShader: _vertexShader,
fragmentShader: _fragmentShader,
transparent: true,
depthWrite: false,
side: THREE.FrontSide,
});
const earthMesh = new THREE.Mesh(new THREE.SphereGeometry(EARTH_RADIUS, 64, 32), _surfaceMat);
earthMesh.userData.zoomLabel = 'Planet Earth';
_group.add(earthMesh);
// Lat/lon grid
const lineMat = new THREE.LineBasicMaterial({ color: 0x2266bb, transparent: true, opacity: 0.30 });
const r = EARTH_RADIUS + 0.015;
const SEG = 64;
for (let lat = -60; lat <= 60; lat += 30) {
const phi = lat * (Math.PI / 180);
const pts = [];
for (let i = 0; i <= SEG; i++) {
const th = (i / SEG) * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(phi)*Math.cos(th)*r, Math.sin(phi)*r, Math.cos(phi)*Math.sin(th)*r));
}
_group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), lineMat));
}
for (let lon = 0; lon < 360; lon += 30) {
const th = lon * (Math.PI / 180);
const pts = [];
for (let i = 0; i <= SEG; i++) {
const phi = (i / SEG) * Math.PI - Math.PI / 2;
pts.push(new THREE.Vector3(Math.cos(phi)*Math.cos(th)*r, Math.sin(phi)*r, Math.cos(phi)*Math.sin(th)*r));
}
_group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), lineMat));
}
// Atmosphere shell
_group.add(new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS * 1.14, 32, 16),
new THREE.MeshBasicMaterial({
color: NEXUS.theme.earthAtm, transparent: true, opacity: 0.07,
side: THREE.BackSide, depthWrite: false, blending: THREE.AdditiveBlending,
})
));
// Glow light
_group.add(new THREE.PointLight(NEXUS.theme.earthGlow, 0.4, 25));
_group.traverse(obj => {
if (obj.isMesh || obj.isLine) obj.userData.zoomLabel = 'Planet Earth';
});
// Tether beam to platform
const beamPts = [
new THREE.Vector3(0, EARTH_Y - EARTH_RADIUS * 1.15, 0),
new THREE.Vector3(0, 0.5, 0),
];
scene.add(new THREE.Line(
new THREE.BufferGeometry().setFromPoints(beamPts),
new THREE.LineBasicMaterial({
color: NEXUS.theme.earthGlow, transparent: true, opacity: 0.08,
depthWrite: false, blending: THREE.AdditiveBlending,
})
));
scene.add(_group);
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} delta
*/
export function update(elapsed, delta) {
if (!_group) return;
// Tether rotation speed to commit activity
const activity = state.totalActivity();
const speed = ROTATION_SPEED_BASE + activity * (ROTATION_SPEED_MAX - ROTATION_SPEED_BASE);
_group.rotation.y += speed * delta;
// Update shader time uniform for scan line animation
_surfaceMat.uniforms.uTime.value = elapsed;
}
export function dispose() {
if (_group) _scene.remove(_group);
if (_surfaceMat) _surfaceMat.dispose();
}

View File

@@ -1,125 +0,0 @@
// modules/panels/heatmap.js — Commit heatmap floor overlay
// Canvas-texture circle on the glass platform floor.
// Each agent occupies a polar sector; recent commits make that sector glow brighter.
// Activity decays over 24 h (driven by state.zoneIntensity, written by data/gitea.js).
//
// Data category: DATA-TETHERED AESTHETIC
// Data source: state.zoneIntensity (populated from Gitea commits API by data/gitea.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
export const HEATMAP_ZONES = [
{ name: 'Claude', color: [255, 100, 60], authorMatch: /^claude$/i, angleDeg: 0 },
{ name: 'Timmy', color: [ 60, 160, 255], authorMatch: /^timmy/i, angleDeg: 90 },
{ name: 'Kimi', color: [ 60, 255, 140], authorMatch: /^kimi/i, angleDeg: 180 },
{ name: 'Perplexity', color: [200, 60, 255], authorMatch: /^perplexity/i, angleDeg: 270 },
];
const HEATMAP_SIZE = 512;
const HEATMAP_ZONE_SPAN_RAD = Math.PI / 2; // 90° per zone
const GLASS_RADIUS = 4.55; // matches terrain/island.js platform radius
let _canvas, _ctx, _texture, _mesh;
let _scene;
function _draw() {
const cx = HEATMAP_SIZE / 2;
const cy = HEATMAP_SIZE / 2;
const r = cx * 0.96;
_ctx.clearRect(0, 0, HEATMAP_SIZE, HEATMAP_SIZE);
_ctx.save();
_ctx.beginPath();
_ctx.arc(cx, cy, r, 0, Math.PI * 2);
_ctx.clip();
for (const zone of HEATMAP_ZONES) {
const intensity = state.zoneIntensity[zone.name] || 0;
if (intensity < 0.01) continue;
const [rr, gg, bb] = zone.color;
const baseRad = zone.angleDeg * (Math.PI / 180);
const startRad = baseRad - HEATMAP_ZONE_SPAN_RAD / 2;
const endRad = baseRad + HEATMAP_ZONE_SPAN_RAD / 2;
const gx = cx + Math.cos(baseRad) * r * 0.55;
const gy = cy + Math.sin(baseRad) * r * 0.55;
const grad = _ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.75);
grad.addColorStop(0, `rgba(${rr},${gg},${bb},${0.65 * intensity})`);
grad.addColorStop(0.45, `rgba(${rr},${gg},${bb},${0.25 * intensity})`);
grad.addColorStop(1, `rgba(${rr},${gg},${bb},0)`);
_ctx.beginPath();
_ctx.moveTo(cx, cy);
_ctx.arc(cx, cy, r, startRad, endRad);
_ctx.closePath();
_ctx.fillStyle = grad;
_ctx.fill();
if (intensity > 0.05) {
const lx = cx + Math.cos(baseRad) * r * 0.62;
const ly = cy + Math.sin(baseRad) * r * 0.62;
_ctx.font = `bold ${Math.round(13 * intensity + 7)}px ${NEXUS.theme.fontMono}`;
_ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`;
_ctx.textAlign = 'center';
_ctx.textBaseline = 'middle';
_ctx.fillText(zone.name, lx, ly);
}
}
_ctx.restore();
_texture.needsUpdate = true;
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_canvas = document.createElement('canvas');
_canvas.width = HEATMAP_SIZE;
_canvas.height = HEATMAP_SIZE;
_ctx = _canvas.getContext('2d');
_texture = new THREE.CanvasTexture(_canvas);
const mat = new THREE.MeshBasicMaterial({
map: _texture,
transparent: true,
opacity: 0.9,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
_mesh = new THREE.Mesh(new THREE.CircleGeometry(GLASS_RADIUS, 64), mat);
_mesh.rotation.x = -Math.PI / 2;
_mesh.position.y = 0.005;
_mesh.userData.zoomLabel = 'Activity Heatmap';
scene.add(_mesh);
// Draw initial empty state
_draw();
subscribe(update);
}
let _lastDrawElapsed = 0;
const REDRAW_INTERVAL = 0.5; // redraw at most every 500 ms (data changes slowly)
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
if (elapsed - _lastDrawElapsed < REDRAW_INTERVAL) return;
_lastDrawElapsed = elapsed;
_draw();
}
export function dispose() {
if (_mesh) { _scene.remove(_mesh); _mesh.geometry.dispose(); _mesh.material.dispose(); }
if (_texture) _texture.dispose();
}

View File

@@ -1,167 +0,0 @@
// modules/panels/lora-panel.js — LoRA Adapter Status holographic panel
// Shows the model training / LoRA fine-tuning adapter status.
// Displayed as HONEST-OFFLINE: no adapters are deployed. Panel shows empty state.
// Will render real adapters when state.loraAdapters is populated in the future.
//
// Data category: HONEST-OFFLINE
// Data source: — (no LoRA adapters deployed; shows "NO ADAPTERS DEPLOYED")
import * as THREE from 'three';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const PANEL_POS = new THREE.Vector3(-10.5, 4.5, 2.5);
const LORA_ACCENT = NEXUS.theme.loraAccent;
const LORA_ACTIVE = NEXUS.theme.loraActive;
const LORA_OFFLINE = NEXUS.theme.loraInactive;
const FONT = NEXUS.theme.fontMono;
let _group, _sprite, _scene;
/**
* Builds the LoRA panel canvas texture.
* @param {{ adapters: Array }|null} data
* @returns {THREE.CanvasTexture}
*/
function _makeTexture(data) {
const W = 420, H = 260;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = NEXUS.theme.panelBg;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = LORA_ACCENT;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.strokeStyle = LORA_ACCENT;
ctx.lineWidth = 1;
ctx.globalAlpha = 0.3;
ctx.strokeRect(4, 4, W - 8, H - 8);
ctx.globalAlpha = 1.0;
ctx.font = `bold 14px ${FONT}`;
ctx.fillStyle = LORA_ACCENT;
ctx.textAlign = 'left';
ctx.fillText('MODEL TRAINING', 14, 24);
ctx.font = `10px ${FONT}`;
ctx.fillStyle = '#664488';
ctx.fillText('LoRA ADAPTERS', 14, 38);
ctx.strokeStyle = '#2a1a44';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(14, 46); ctx.lineTo(W - 14, 46); ctx.stroke();
const adapters = data && Array.isArray(data.adapters) ? data.adapters : [];
if (adapters.length === 0) {
// Honest empty state
ctx.font = `bold 18px ${FONT}`;
ctx.fillStyle = LORA_OFFLINE;
ctx.textAlign = 'center';
ctx.fillText('NO ADAPTERS DEPLOYED', W / 2, H / 2 + 10);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#223344';
ctx.fillText('Adapters will appear here when trained', W / 2, H / 2 + 36);
return new THREE.CanvasTexture(canvas);
}
// Active count header
const activeCount = adapters.filter(a => a.active).length;
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = LORA_ACTIVE;
ctx.textAlign = 'right';
ctx.fillText(`${activeCount}/${adapters.length} ACTIVE`, W - 14, 26);
ctx.textAlign = 'left';
// Adapter rows
const ROW_H = 44;
adapters.forEach((adapter, i) => {
const rowY = 50 + i * ROW_H;
const col = adapter.active ? LORA_ACTIVE : LORA_OFFLINE;
ctx.beginPath();
ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566';
ctx.fillText(adapter.name, 36, rowY + 16);
ctx.font = `10px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'right';
ctx.fillText(adapter.base, W - 14, rowY + 16);
ctx.textAlign = 'left';
if (adapter.active) {
const BX = 36, BW = W - 80, BY = rowY + 22, BH = 5;
ctx.fillStyle = '#0a1428';
ctx.fillRect(BX, BY, BW, BH);
ctx.fillStyle = col;
ctx.globalAlpha = 0.7;
ctx.fillRect(BX, BY, BW * (adapter.strength || 0), BH);
ctx.globalAlpha = 1.0;
}
if (i < adapters.length - 1) {
ctx.strokeStyle = '#1a0a2a';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(14, rowY + ROW_H - 2); ctx.lineTo(W - 14, rowY + ROW_H - 2); ctx.stroke();
}
});
return new THREE.CanvasTexture(canvas);
}
function _buildSprite(data) {
if (_sprite) {
_group.remove(_sprite);
if (_sprite.material.map) _sprite.material.map.dispose();
_sprite.material.dispose();
_sprite = null;
}
const texture = _makeTexture(data);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.93, depthWrite: false });
_sprite = new THREE.Sprite(material);
_sprite.scale.set(6.0, 3.6, 1);
_sprite.position.copy(PANEL_POS);
_sprite.userData = {
baseY: PANEL_POS.y,
floatPhase: 1.1,
floatSpeed: 0.14,
zoomLabel: 'Model Training — LoRA Adapters',
};
_group.add(_sprite);
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
scene.add(_group);
// Honest empty state on init — no adapters deployed
_buildSprite({ adapters: [] });
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
if (_sprite) {
const ud = _sprite.userData;
_sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.12;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
}

View File

@@ -1,147 +0,0 @@
// modules/panels/sovereignty.js — Sovereignty Meter holographic arc gauge
// Floating arc gauge above the platform showing the current sovereignty score.
// Reads from state.sovereignty (populated by data/loaders.js via sovereignty-status.json).
// The assessment is MANUAL — the panel always labels itself as such.
//
// Data category: REAL (manual assessment)
// Data source: state.sovereignty (sovereignty-status.json via data/loaders.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const FONT = NEXUS.theme.fontMono;
// Defaults shown before data loads
let _score = 85;
let _label = 'Mostly Sovereign';
let _assessmentType = 'MANUAL';
let _group, _arcMesh, _arcMat, _light, _spriteMat, _scene;
let _lastSovereignty = null;
function _scoreColor(score) {
if (score >= 80) return NEXUS.theme.sovereignHighHex;
if (score >= 40) return NEXUS.theme.sovereignMidHex;
return NEXUS.theme.sovereignLowHex;
}
function _scoreColorStr(score) {
if (score >= 80) return NEXUS.theme.sovereignHigh;
if (score >= 40) return NEXUS.theme.sovereignMid;
return NEXUS.theme.sovereignLow;
}
function _buildArcGeo(score) {
return new THREE.TorusGeometry(1.6, 0.1, 8, 64, (score / 100) * Math.PI * 2);
}
function _buildMeterTexture(score, label, assessmentType) {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
const ctx = canvas.getContext('2d');
const col = _scoreColorStr(score);
ctx.clearRect(0, 0, 256, 128);
ctx.font = `bold 52px ${FONT}`;
ctx.fillStyle = col;
ctx.textAlign = 'center';
ctx.fillText(`${score}%`, 128, 50);
ctx.font = `16px ${FONT}`;
ctx.fillStyle = '#8899bb';
ctx.fillText(label.toUpperCase(), 128, 74);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#445566';
ctx.fillText('SOVEREIGNTY', 128, 94);
ctx.font = `9px ${FONT}`;
ctx.fillStyle = '#334455';
ctx.fillText('MANUAL ASSESSMENT', 128, 112);
return new THREE.CanvasTexture(canvas);
}
function _applyScore(score, label, assessmentType) {
_score = score;
_label = label;
_assessmentType = assessmentType;
_arcMesh.geometry.dispose();
_arcMesh.geometry = _buildArcGeo(score);
const col = _scoreColor(score);
_arcMat.color.setHex(col);
_light.color.setHex(col);
if (_spriteMat.map) _spriteMat.map.dispose();
_spriteMat.map = _buildMeterTexture(score, label, assessmentType);
_spriteMat.needsUpdate = true;
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
_group.position.set(0, 3.8, 0);
// Background ring
const bgMat = new THREE.MeshBasicMaterial({ color: 0x0a1828, transparent: true, opacity: 0.5 });
_group.add(new THREE.Mesh(new THREE.TorusGeometry(1.6, 0.1, 8, 64), bgMat));
// Score arc
_arcMat = new THREE.MeshBasicMaterial({
color: _scoreColor(_score),
transparent: true,
opacity: 0.9,
});
_arcMesh = new THREE.Mesh(_buildArcGeo(_score), _arcMat);
_arcMesh.rotation.z = Math.PI / 2; // arc starts at 12 o'clock
_group.add(_arcMesh);
// Glow light
_light = new THREE.PointLight(_scoreColor(_score), 0.7, 6);
_group.add(_light);
// Sprite label
_spriteMat = new THREE.SpriteMaterial({
map: _buildMeterTexture(_score, _label, _assessmentType),
transparent: true,
depthWrite: false,
});
const sprite = new THREE.Sprite(_spriteMat);
sprite.scale.set(3.2, 1.6, 1);
_group.add(sprite);
scene.add(_group);
_group.traverse(obj => {
if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter';
});
subscribe(update);
}
/**
* @param {number} _elapsed
* @param {number} _delta
*/
export function update(_elapsed, _delta) {
if (state.sovereignty && state.sovereignty !== _lastSovereignty) {
const { score, label, assessment_type } = state.sovereignty;
const s = Math.max(0, Math.min(100, typeof score === 'number' ? score : _score));
const l = typeof label === 'string' ? label : _label;
const t = typeof assessment_type === 'string' ? assessment_type : 'MANUAL';
_applyScore(s, l, t);
_lastSovereignty = state.sovereignty;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
if (_spriteMat.map) _spriteMat.map.dispose();
}

View File

@@ -4,8 +4,6 @@ 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';
import { checkPortalHealth } from './data/loaders.js';
// === PORTAL HEALTH CHECKS ===
const PORTAL_HEALTH_CHECK_MS = 5 * 60 * 1000;
@@ -24,7 +22,21 @@ export function setWeatherPortalRefs(portals, portalGroup, rebuildGravityZones)
export async function runPortalHealthChecks() {
if (_portalsRef.length === 0) return;
await checkPortalHealth(_portalsRef);
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();
@@ -150,13 +162,20 @@ function updateWeatherHUD(wx) {
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;
const url = `https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}&current=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(wx);
updateWeatherHUD(weatherState);
} catch {
const descEl = document.getElementById('weather-desc');
if (descEl) descEl.textContent = 'Lempster NH';