import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { BokehPass } from 'three/addons/postprocessing/BokehPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { LoadingManager } from 'three'; // === COLOR PALETTE === const NEXUS = { colors: { bg: 0x000008, starCore: 0xffffff, starDim: 0x8899cc, constellationLine: 0x334488, constellationFade: 0x112244, accent: 0x4488ff, } }; // === ASSET LOADER === const loadedAssets = new Map(); const loadingManager = new THREE.LoadingManager(() => { document.getElementById('loading-bar').style.width = '100%'; document.getElementById('loading').style.display = 'none'; animate(); }); loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => { const progress = (itemsLoaded / itemsTotal) * 100; document.getElementById('loading-bar').style.width = `${progress}%`; }; // Simulate loading a texture for demonstration const textureLoader = new THREE.TextureLoader(loadingManager); textureLoader.load('placeholder-texture.jpg', (texture) => { loadedAssets.set('placeholder-texture', texture); }); // === SCENE SETUP === const scene = new THREE.Scene(); scene.background = new THREE.Color(NEXUS.colors.bg); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); camera.position.set(0, 6, 11); const raycaster = new THREE.Raycaster(); const forwardVector = new THREE.Vector3(); // === LIGHTING === // Required for MeshStandardMaterial / MeshPhysicalMaterial used on the platform. const ambientLight = new THREE.AmbientLight(0x0a1428, 1.4); scene.add(ambientLight); const overheadLight = new THREE.PointLight(0x8899bb, 0.6, 60); overheadLight.position.set(0, 25, 0); scene.add(overheadLight); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // === STAR FIELD === const STAR_COUNT = 800; const STAR_SPREAD = 400; const CONSTELLATION_DISTANCE = 30; // max distance to draw a line between stars const starPositions = []; const starGeo = new THREE.BufferGeometry(); const posArray = new Float32Array(STAR_COUNT * 3); const sizeArray = new Float32Array(STAR_COUNT); for (let i = 0; i < STAR_COUNT; i++) { const x = (Math.random() - 0.5) * STAR_SPREAD; const y = (Math.random() - 0.5) * STAR_SPREAD; const z = (Math.random() - 0.5) * STAR_SPREAD; posArray[i * 3] = x; posArray[i * 3 + 1] = y; posArray[i * 3 + 2] = z; sizeArray[i] = Math.random() * 2.5 + 0.5; starPositions.push(new THREE.Vector3(x, y, z)); } starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); starGeo.setAttribute('size', new THREE.BufferAttribute(sizeArray, 1)); const starMaterial = new THREE.PointsMaterial({ color: NEXUS.colors.starCore, size: 0.6, sizeAttenuation: true, transparent: true, opacity: 0.9, }); const stars = new THREE.Points(starGeo, starMaterial); scene.add(stars); // === CONSTELLATION LINES === // Connect nearby stars with faint lines, limited to avoid clutter /** * Builds constellation line segments connecting nearby stars. * @returns {THREE.LineSegments} */ function buildConstellationLines() { const linePositions = []; const MAX_CONNECTIONS_PER_STAR = 3; const connectionCount = new Array(STAR_COUNT).fill(0); for (let i = 0; i < STAR_COUNT; i++) { if (connectionCount[i] >= MAX_CONNECTIONS_PER_STAR) continue; // Find nearest neighbors const neighbors = []; for (let j = i + 1; j < STAR_COUNT; j++) { if (connectionCount[j] >= MAX_CONNECTIONS_PER_STAR) continue; const dist = starPositions[i].distanceTo(starPositions[j]); if (dist < CONSTELLATION_DISTANCE) { neighbors.push({ j, dist }); } } // Sort by distance and connect closest ones neighbors.sort((/** @type {{j: number, dist: number}} */ a, /** @type {{j: number, dist: number}} */ b) => a.dist - b.dist); const toConnect = neighbors.slice(0, MAX_CONNECTIONS_PER_STAR - connectionCount[i]); for (const { j } of toConnect) { linePositions.push( starPositions[i].x, starPositions[i].y, starPositions[i].z, starPositions[j].x, starPositions[j].y, starPositions[j].z ); connectionCount[i]++; connectionCount[j]++; } } const lineGeo = new THREE.BufferGeometry(); lineGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePositions), 3)); const lineMat = new THREE.LineBasicMaterial({ color: NEXUS.colors.constellationLine, transparent: true, opacity: 0.18, }); return new THREE.LineSegments(lineGeo, lineMat); } const constellationLines = buildConstellationLines(); scene.add(constellationLines); // === GLASS PLATFORM === // Central floating platform with transparent glass-floor sections revealing the void (star field) below. const glassPlatformGroup = new THREE.Group(); // Dark metallic frame material const platformFrameMat = new THREE.MeshStandardMaterial({ color: 0x0a1828, metalness: 0.9, roughness: 0.1, emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.06), }); // Outer solid rim (flat ring) const platformRimGeo = new THREE.RingGeometry(4.7, 5.3, 64); const platformRim = new THREE.Mesh(platformRimGeo, platformFrameMat); platformRim.rotation.x = -Math.PI / 2; glassPlatformGroup.add(platformRim); // Raised border torus for visible 3-D thickness const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64); const borderTorus = new THREE.Mesh(borderTorusGeo, platformFrameMat); borderTorus.rotation.x = Math.PI / 2; glassPlatformGroup.add(borderTorus); // Glass tile material — highly transmissive to reveal the void below const glassTileMat = new THREE.MeshPhysicalMaterial({ color: new THREE.Color(NEXUS.colors.accent), transparent: true, opacity: 0.09, roughness: 0.0, metalness: 0.0, transmission: 0.92, thickness: 0.06, side: THREE.DoubleSide, depthWrite: false, }); // Edge glow — bright accent outline on each tile const glassEdgeBaseMat = new THREE.LineBasicMaterial({ color: NEXUS.colors.accent, transparent: true, opacity: 0.55, }); const GLASS_TILE_SIZE = 0.85; const GLASS_TILE_GAP = 0.14; const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP; const GLASS_RADIUS = 4.55; const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE); const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo); /** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */ const glassEdgeMaterials = []; for (let row = -5; row <= 5; row++) { for (let col = -5; col <= 5; col++) { const x = col * GLASS_TILE_STEP; const z = row * GLASS_TILE_STEP; const distFromCenter = Math.sqrt(x * x + z * z); if (distFromCenter > GLASS_RADIUS) continue; // Transparent glass tile const tile = new THREE.Mesh(tileGeo, glassTileMat.clone()); tile.rotation.x = -Math.PI / 2; tile.position.set(x, 0, z); glassPlatformGroup.add(tile); // Glowing edge lines const mat = glassEdgeBaseMat.clone(); const edges = new THREE.LineSegments(tileEdgeGeo, mat); edges.rotation.x = -Math.PI / 2; edges.position.set(x, 0.002, z); glassPlatformGroup.add(edges); glassEdgeMaterials.push({ mat, distFromCenter }); } } // Void shimmer — faint point light below the glass, emphasising the infinite depth const voidLight = new THREE.PointLight(NEXUS.colors.accent, 0.5, 14); voidLight.position.set(0, -3.5, 0); glassPlatformGroup.add(voidLight); scene.add(glassPlatformGroup); glassPlatformGroup.traverse(obj => { if (obj.isMesh) obj.userData.zoomLabel = 'Glass Platform'; }); // === PERLIN NOISE === // Classic Perlin noise used for procedural terrain generation. function createPerlinNoise() { const p = new Uint8Array(256); for (let i = 0; i < 256; i++) p[i] = i; // Fisher-Yates shuffle with a fixed seed sequence for reproducibility let seed = 42; function seededRand() { seed = (seed * 1664525 + 1013904223) & 0xffffffff; return (seed >>> 0) / 0xffffffff; } for (let i = 255; i > 0; i--) { const j = Math.floor(seededRand() * (i + 1)); const tmp = p[i]; p[i] = p[j]; p[j] = tmp; } const perm = new Uint8Array(512); for (let i = 0; i < 512; i++) perm[i] = p[i & 255]; function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(a, b, t) { return a + t * (b - a); } function grad(hash, x, y, z) { const h = hash & 15; const u = h < 8 ? x : y; const v = h < 4 ? y : (h === 12 || h === 14) ? x : z; return ((h & 1) ? -u : u) + ((h & 2) ? -v : v); } return function noise(x, y, z) { z = z || 0; const X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255; x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); const u = fade(x), v = fade(y), w = fade(z); const A = perm[X] + Y, AA = perm[A] + Z, AB = perm[A + 1] + Z; const B = perm[X + 1] + Y, BA = perm[B] + Z, BB = perm[B + 1] + Z; return lerp( lerp(lerp(grad(perm[AA], x, y, z ), grad(perm[BA], x-1, y, z ), u), lerp(grad(perm[AB], x, y-1, z ), grad(perm[BB], x-1, y-1, z ), u), v), lerp(lerp(grad(perm[AA + 1], x, y, z-1), grad(perm[BA + 1], x-1, y, z-1), u), lerp(grad(perm[AB + 1], x, y-1, z-1), grad(perm[BB + 1], x-1, y-1, z-1), u), v), w ); }; } const perlin = createPerlinNoise(); // === FLOATING ISLAND TERRAIN === // Procedural terrain below the glass platform, shaped like a floating rock island. // Heights generated via fBm (fractional Brownian motion) layered Perlin noise. (function buildFloatingIsland() { const ISLAND_RADIUS = 9.5; const SEGMENTS = 90; const SIZE = ISLAND_RADIUS * 2; const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEGMENTS, SEGMENTS); geo.rotateX(-Math.PI / 2); const pos = geo.attributes.position; const count = pos.count; for (let i = 0; i < count; i++) { const x = pos.getX(i); const z = pos.getZ(i); const dist = Math.sqrt(x * x + z * z) / ISLAND_RADIUS; // Island edge taper — smooth falloff toward rim const edgeFactor = Math.max(0, 1 - Math.pow(dist, 2.2)); // fBm: four octaves of Perlin noise const nx = x * 0.17, nz = z * 0.17; let h = 0; h += perlin(nx, nz ) * 1.000; h += perlin(nx * 2, nz * 2 ) * 0.500; h += perlin(nx * 4, nz * 4 ) * 0.250; h += perlin(nx * 8, nz * 8 ) * 0.125; h /= 1.875; // normalise to ~[-1, 1] const height = ((h + 1) * 0.5) * edgeFactor * 2.6; pos.setY(i, height); } geo.computeVertexNormals(); // Vertex colours: low=dark earth, mid=dusty stone, high=pale rock const colors = new Float32Array(count * 3); for (let i = 0; i < count; i++) { const t = Math.min(1, pos.getY(i) / 2.0); colors[i * 3] = 0.18 + t * 0.22; // R colors[i * 3 + 1] = 0.14 + t * 0.16; // G colors[i * 3 + 2] = 0.10 + t * 0.15; // B } geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); const topMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.88, metalness: 0.04, }); const topMesh = new THREE.Mesh(geo, topMat); // Underside — tapered cylinder giving the island its rocky underbelly const bottomGeo = new THREE.CylinderGeometry(ISLAND_RADIUS * 0.82, ISLAND_RADIUS * 0.35, 2.0, 64, 1); const bottomMat = new THREE.MeshStandardMaterial({ color: 0x0c0a08, roughness: 0.92, metalness: 0.03 }); const bottomMesh = new THREE.Mesh(bottomGeo, bottomMat); bottomMesh.position.y = -1.0; const islandGroup = new THREE.Group(); islandGroup.add(topMesh); islandGroup.add(bottomMesh); islandGroup.position.y = -2.8; // float below the glass platform scene.add(islandGroup); })(); // === COMMIT HEATMAP === // Canvas-texture overlay on the floor. Each agent occupies a polar sector; // recent commits make that sector glow brighter. Activity decays over 24 h. const HEATMAP_SIZE = 512; const HEATMAP_REFRESH_MS = 5 * 60 * 1000; // 5 min between API polls const HEATMAP_DECAY_MS = 24 * 60 * 60 * 1000; // 24 h full decay // Agent zones — angle in canvas degrees (0 = east/right, clockwise) 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_ZONE_SPAN_RAD = Math.PI / 2; // 90° per zone const heatmapCanvas = document.createElement('canvas'); heatmapCanvas.width = HEATMAP_SIZE; heatmapCanvas.height = HEATMAP_SIZE; const heatmapTexture = new THREE.CanvasTexture(heatmapCanvas); const heatmapMat = new THREE.MeshBasicMaterial({ map: heatmapTexture, transparent: true, opacity: 0.9, depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, }); const heatmapMesh = new THREE.Mesh( new THREE.CircleGeometry(GLASS_RADIUS, 64), heatmapMat ); heatmapMesh.rotation.x = -Math.PI / 2; heatmapMesh.position.y = 0.005; heatmapMesh.userData.zoomLabel = 'Activity Heatmap'; scene.add(heatmapMesh); // Per-zone intensity [0..1], updated by updateHeatmap() const zoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0])); /** * Redraws the heatmap canvas from current zoneIntensity values. */ function drawHeatmap() { const ctx = heatmapCanvas.getContext('2d'); const cx = HEATMAP_SIZE / 2; const cy = HEATMAP_SIZE / 2; const r = cx * 0.96; ctx.clearRect(0, 0, HEATMAP_SIZE, HEATMAP_SIZE); // Clip drawing to the circular platform boundary ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.clip(); for (const zone of HEATMAP_ZONES) { const intensity = 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; // Glow origin sits at 55% radius in the zone's direction 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(); // Zone label — only when active if (intensity > 0.05) { const labelX = cx + Math.cos(baseRad) * r * 0.62; const labelY = cy + Math.sin(baseRad) * r * 0.62; ctx.font = `bold ${Math.round(13 * intensity + 7)}px "Courier New", monospace`; ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(zone.name, labelX, labelY); } } ctx.restore(); heatmapTexture.needsUpdate = true; } /** * Fetches recent commits, maps them to agent zones via author, and redraws. */ async function updateHeatmap() { let commits = []; try { const res = await fetch( 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50', { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } ); if (res.ok) commits = await res.json(); } catch { /* silently use zero-activity baseline */ } const now = Date.now(); const rawWeights = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0])); for (const commit of commits) { const author = commit.commit?.author?.name || commit.author?.login || ''; const ts = new Date(commit.commit?.author?.date || 0).getTime(); const age = now - ts; if (age > HEATMAP_DECAY_MS) continue; const weight = 1 - age / HEATMAP_DECAY_MS; // linear decay for (const zone of HEATMAP_ZONES) { if (zone.authorMatch.test(author)) { rawWeights[zone.name] += weight; break; } } } // Normalise: 8 recent weighted commits = full brightness const MAX_WEIGHT = 8; for (const zone of HEATMAP_ZONES) { zoneIntensity[zone.name] = Math.min(rawWeights[zone.name] / MAX_WEIGHT, 1.0); } drawHeatmap(); } // Kick off and schedule periodic refresh updateHeatmap(); setInterval(updateHeatmap, HEATMAP_REFRESH_MS); // === MOUSE-DRIVEN ROTATION === let mouseX = 0; let mouseY = 0; let targetRotX = 0; let targetRotY = 0; document.addEventListener('mousemove', (/** @type {MouseEvent} */ e) => { mouseX = (e.clientX / window.innerWidth - 0.5) * 2; mouseY = (e.clientY / window.innerHeight - 0.5) * 2; }); // === OVERVIEW MODE (Tab — bird's-eye view of the whole Nexus) === let overviewMode = false; let overviewT = 0; // 0 = normal view, 1 = overview const NORMAL_CAM = new THREE.Vector3(0, 6, 11); const OVERVIEW_CAM = new THREE.Vector3(0, 200, 0.1); // overhead; tiny Z offset avoids gimbal lock const overviewIndicator = document.getElementById('overview-indicator'); document.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); overviewMode = !overviewMode; if (overviewMode) { overviewIndicator.classList.add('visible'); } else { overviewIndicator.classList.remove('visible'); } } }); // === ZOOM-TO-OBJECT === const _zoomRaycaster = new THREE.Raycaster(); const _zoomMouse = new THREE.Vector2(); const _zoomCamTarget = new THREE.Vector3(); const _zoomLookTarget = new THREE.Vector3(); let zoomT = 0; let zoomTargetT = 0; let zoomActive = false; const zoomIndicator = document.getElementById('zoom-indicator'); const zoomLabelEl = document.getElementById('zoom-label'); function getZoomLabel(/** @type {THREE.Object3D} */ obj) { let o = /** @type {THREE.Object3D|null} */ (obj); while (o) { if (o.userData && o.userData.zoomLabel) return o.userData.zoomLabel; o = o.parent; } return 'Object'; } function exitZoom() { zoomTargetT = 0; zoomActive = false; if (zoomIndicator) zoomIndicator.classList.remove('visible'); } renderer.domElement.addEventListener('dblclick', (/** @type {MouseEvent} */ e) => { if (overviewMode || photoMode) return; _zoomMouse.x = (e.clientX / window.innerWidth) * 2 - 1; _zoomMouse.y = -(e.clientY / window.innerHeight) * 2 + 1; _zoomRaycaster.setFromCamera(_zoomMouse, camera); const hits = _zoomRaycaster.intersectObjects(scene.children, true) .filter(h => !(h.object instanceof THREE.Points) && !(h.object instanceof THREE.Line)); if (!hits.length) { exitZoom(); return; } const hit = hits[0]; const label = getZoomLabel(hit.object); const dir = new THREE.Vector3().subVectors(camera.position, hit.point).normalize(); const flyDist = Math.max(1.5, Math.min(5, hit.distance * 0.45)); _zoomCamTarget.copy(hit.point).addScaledVector(dir, flyDist); _zoomLookTarget.copy(hit.point); zoomT = 0; zoomTargetT = 1; zoomActive = true; if (zoomLabelEl) zoomLabelEl.textContent = label; if (zoomIndicator) zoomIndicator.classList.add('visible'); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') exitZoom(); }); // === PHOTO MODE === let photoMode = false; // Warp effect state let isWarping = false; let warpStartTime = 0; const WARP_DURATION = 1.5; // seconds // Post-processing composer for depth of field (always-on, subtle) const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const bokehPass = new BokehPass(scene, camera, { focus: 5.0, aperture: 0.00015, maxblur: 0.004, }); composer.addPass(bokehPass); // Orbit controls for free camera movement in photo mode const orbitControls = new OrbitControls(camera, renderer.domElement); orbitControls.enableDamping = true; orbitControls.dampingFactor = 0.05; orbitControls.enabled = false; const photoIndicator = document.getElementById('photo-indicator'); const photoFocusDisplay = document.getElementById('photo-focus'); /** * Updates the photo mode focus distance display. */ function updateFocusDisplay() { if (photoFocusDisplay) { photoFocusDisplay.textContent = bokehPass.uniforms['focus'].value.toFixed(1); } } document.addEventListener('keydown', (e) => { if (e.key === 'p' || e.key === 'P') { photoMode = !photoMode; document.body.classList.toggle('photo-mode', photoMode); orbitControls.enabled = photoMode; if (photoIndicator) { photoIndicator.classList.toggle('visible', photoMode); } if (photoMode) { // Enhanced DoF in photo mode bokehPass.uniforms['aperture'].value = 0.0003; bokehPass.uniforms['maxblur'].value = 0.008; // Sync orbit target to current look-at orbitControls.target.set(0, 0, 0); orbitControls.update(); updateFocusDisplay(); } else { // Restore subtle ambient DoF bokehPass.uniforms['aperture'].value = 0.00015; bokehPass.uniforms['maxblur'].value = 0.004; } } // Adjust focus with [ ] while in photo mode if (photoMode) { const focusStep = 0.5; if (e.key === '[') { bokehPass.uniforms['focus'].value = Math.max(0.5, bokehPass.uniforms['focus'].value - focusStep); updateFocusDisplay(); } else if (e.key === ']') { bokehPass.uniforms['focus'].value = Math.min(200, bokehPass.uniforms['focus'].value + focusStep); updateFocusDisplay(); } } }); // === RESIZE HANDLER === window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }); // === SOVEREIGNTY METER === // Holographic arc gauge floating above the platform; reads from sovereignty-status.json const sovereigntyGroup = new THREE.Group(); sovereigntyGroup.position.set(0, 3.8, 0); // Background ring — full circle, dark frame const meterBgGeo = new THREE.TorusGeometry(1.6, 0.1, 8, 64); const meterBgMat = new THREE.MeshBasicMaterial({ color: 0x0a1828, transparent: true, opacity: 0.5 }); sovereigntyGroup.add(new THREE.Mesh(meterBgGeo, meterBgMat)); let sovereigntyScore = 85; let sovereigntyLabel = 'Mostly Sovereign'; function sovereigntyHexColor(score) { if (score >= 80) return 0x00ff88; if (score >= 40) return 0xffcc00; return 0xff4444; } function buildScoreArcGeo(score) { return new THREE.TorusGeometry(1.6, 0.1, 8, 64, (score / 100) * Math.PI * 2); } const scoreArcMat = new THREE.MeshBasicMaterial({ color: sovereigntyHexColor(sovereigntyScore), transparent: true, opacity: 0.9, }); const scoreArcMesh = new THREE.Mesh(buildScoreArcGeo(sovereigntyScore), scoreArcMat); scoreArcMesh.rotation.z = Math.PI / 2; // arc starts at 12 o'clock sovereigntyGroup.add(scoreArcMesh); // Glow light at gauge center const meterLight = new THREE.PointLight(sovereigntyHexColor(sovereigntyScore), 0.7, 6); sovereigntyGroup.add(meterLight); function buildMeterTexture(score, label) { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 128; const ctx = canvas.getContext('2d'); const hexStr = score >= 80 ? '#00ff88' : score >= 40 ? '#ffcc00' : '#ff4444'; ctx.clearRect(0, 0, 256, 128); ctx.font = 'bold 52px "Courier New", monospace'; ctx.fillStyle = hexStr; ctx.textAlign = 'center'; ctx.fillText(`${score}%`, 128, 58); ctx.font = '16px "Courier New", monospace'; ctx.fillStyle = '#8899bb'; ctx.fillText(label.toUpperCase(), 128, 82); ctx.font = '11px "Courier New", monospace'; ctx.fillStyle = '#445566'; ctx.fillText('SOVEREIGNTY', 128, 104); return new THREE.CanvasTexture(canvas); } const meterSpriteMat = new THREE.SpriteMaterial({ map: buildMeterTexture(sovereigntyScore, sovereigntyLabel), transparent: true, depthWrite: false, }); const meterSprite = new THREE.Sprite(meterSpriteMat); meterSprite.scale.set(3.2, 1.6, 1); sovereigntyGroup.add(meterSprite); scene.add(sovereigntyGroup); sovereigntyGroup.traverse(obj => { if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter'; }); async function loadSovereigntyStatus() { try { const res = await fetch('./sovereignty-status.json'); if (!res.ok) throw new Error('not found'); const data = await res.json(); const score = Math.max(0, Math.min(100, typeof data.score === 'number' ? data.score : 85)); const label = typeof data.label === 'string' ? data.label : ''; sovereigntyScore = score; sovereigntyLabel = label; scoreArcMesh.geometry.dispose(); scoreArcMesh.geometry = buildScoreArcGeo(score); const col = sovereigntyHexColor(score); scoreArcMat.color.setHex(col); meterLight.color.setHex(col); if (meterSpriteMat.map) meterSpriteMat.map.dispose(); meterSpriteMat.map = buildMeterTexture(score, label); meterSpriteMat.needsUpdate = true; } catch { // defaults already set above } } loadSovereigntyStatus(); // === RUNE RING === // 12 Elder Futhark rune sprites in a slow-orbiting ring around the center platform. const RUNE_COUNT = 12; const RUNE_RING_RADIUS = 7.0; const RUNE_RING_Y = 1.5; // base height above platform const RUNE_ORBIT_SPEED = 0.08; // radians per second const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚲ','ᚷ','ᚹ','ᚺ','ᚾ','ᛁ','ᛃ']; const RUNE_GLOW_COLORS = ['#00ffcc', '#ff44ff']; // alternating cyan / magenta /** * Creates a canvas texture for a single glowing rune glyph. * @param {string} glyph * @param {string} color * @returns {THREE.CanvasTexture} */ 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); // Outer glow 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); } // Faint torus marking the orbit height const runeOrbitRingGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.03, 6, 64); const runeOrbitRingMat = new THREE.MeshBasicMaterial({ color: 0x224466, transparent: true, opacity: 0.22, }); const runeOrbitRingMesh = new THREE.Mesh(runeOrbitRingGeo, runeOrbitRingMat); runeOrbitRingMesh.rotation.x = Math.PI / 2; runeOrbitRingMesh.position.y = RUNE_RING_Y; scene.add(runeOrbitRingMesh); /** * @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number}>} */ const runeSprites = []; for (let i = 0; i < RUNE_COUNT; i++) { const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length]; const color = RUNE_GLOW_COLORS[i % RUNE_GLOW_COLORS.length]; const texture = createRuneTexture(glyph, color); const runeMat = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.85, depthWrite: false, blending: THREE.AdditiveBlending, }); const sprite = new THREE.Sprite(runeMat); sprite.scale.set(1.3, 1.3, 1); const baseAngle = (i / RUNE_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 / RUNE_COUNT) * Math.PI * 2 }); } // === WARP TUNNEL EFFECT === const WarpShader = { uniforms: { 'tDiffuse': { value: null }, 'time': { value: 0.0 }, 'distortionStrength': { value: 0.0 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform float time; uniform float distortionStrength; varying vec2 vUv; void main() { vec2 uv = vUv; vec2 center = vec2(0.5, 0.5); // Simple swirling distortion vec2 dir = uv - center; float angle = atan(dir.y, dir.x); float radius = length(dir); angle += radius * distortionStrength * sin(time * 5.0 + radius * 10.0); radius *= 1.0 - distortionStrength * 0.1 * sin(time * 3.0 + radius * 5.0); uv = center + vec2(cos(angle), sin(angle)) * radius; gl_FragColor = texture2D(tDiffuse, uv); } `, }; let warpPass = new ShaderPass(WarpShader); warpPass.enabled = false; composer.addPass(warpPass); /** * Triggers the warp tunnel effect. */ function startWarp() { isWarping = true; warpStartTime = clock.getElapsedTime(); warpPass.enabled = true; warpPass.uniforms['time'].value = 0.0; warpPass.uniforms['distortionStrength'].value = 0.0; } // === ANIMATION LOOP === const clock = new THREE.Clock(); /** * Main animation loop — called each frame via requestAnimationFrame. * @returns {void} */ function animate() { // Only start animation after assets are loaded requestAnimationFrame(animate); const elapsed = clock.getElapsedTime(); // Smooth camera transition for overview mode const targetT = overviewMode ? 1 : 0; overviewT += (targetT - overviewT) * 0.04; const _basePos = new THREE.Vector3().lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT); // Zoom-to-object interpolation if (!photoMode) { zoomT += (zoomTargetT - zoomT) * 0.07; } if (zoomT > 0.001 && !photoMode && !overviewMode) { camera.position.lerpVectors(_basePos, _zoomCamTarget, zoomT); camera.lookAt(new THREE.Vector3(0, 0, 0).lerp(_zoomLookTarget, zoomT)); } else { camera.position.copy(_basePos); camera.lookAt(0, 0, 0); } // Slow auto-rotation — suppressed during overview and photo mode const rotationScale = photoMode ? 0 : (1 - overviewT); targetRotX += (mouseY * 0.3 - targetRotX) * 0.02; targetRotY += (mouseX * 0.3 - targetRotY) * 0.02; stars.rotation.x = (targetRotX + elapsed * 0.01) * rotationScale; stars.rotation.y = (targetRotY + elapsed * 0.015) * rotationScale; constellationLines.rotation.x = stars.rotation.x; constellationLines.rotation.y = stars.rotation.y; // Subtle pulse on constellation opacity constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; // Glass platform — ripple edge glow outward from centre for (const { mat, distFromCenter } of glassEdgeMaterials) { const phase = elapsed * 1.1 - distFromCenter * 0.18; mat.opacity = 0.25 + Math.sin(phase) * 0.22; } // Pulse the void light below voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2; // Heatmap floor: subtle breathing glow heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2; if (photoMode) { orbitControls.update(); } // Animate sovereignty meter — gentle hover float and glow pulse sovereigntyGroup.position.y = 3.8 + Math.sin(elapsed * 0.8) * 0.15; meterLight.intensity = 0.5 + Math.sin(elapsed * 1.8) * 0.25; // Animate floating commit banners const FADE_DUR = 1.5; commitBanners.forEach(banner => { const ud = banner.userData; if (ud.spawnTime === null) { if (elapsed < ud.startDelay) return; ud.spawnTime = elapsed; } const age = elapsed - ud.spawnTime; let opacity; if (age < FADE_DUR) { opacity = age / FADE_DUR; } else if (age < ud.lifetime - FADE_DUR) { opacity = 1; } else if (age < ud.lifetime) { opacity = (ud.lifetime - age) / FADE_DUR; } else { ud.spawnTime = elapsed + 3; opacity = 0; } banner.material.opacity = opacity * 0.85; banner.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.4; }); // Animate agent status panels — gentle float for (const sprite of agentPanelSprites) { const ud = sprite.userData; sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; } // Animate Timmy speech bubble — fade in, hold, fade out if (timmySpeechState) { const age = elapsed - timmySpeechState.startTime; let opacity; if (age < SPEECH_FADE_IN) { opacity = age / SPEECH_FADE_IN; } else if (age < SPEECH_DURATION - SPEECH_FADE_OUT) { opacity = 1.0; } else if (age < SPEECH_DURATION) { opacity = (SPEECH_DURATION - age) / SPEECH_FADE_OUT; } else { scene.remove(timmySpeechState.sprite); if (timmySpeechState.sprite.material.map) timmySpeechState.sprite.material.map.dispose(); timmySpeechState.sprite.material.dispose(); timmySpeechSprite = null; timmySpeechState = null; opacity = 0; } if (timmySpeechState) { timmySpeechState.sprite.material.opacity = opacity; timmySpeechState.sprite.position.y = TIMMY_SPEECH_POS.y + Math.sin(elapsed * 1.1) * 0.1; } } // Animate rune ring — orbit and vertical 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; rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2; } // Portal collision detection forwardVector.set(0, 0, -1).applyQuaternion(camera.quaternion); raycaster.set(camera.position, forwardVector); const intersects = raycaster.intersectObjects(portalGroup.children); if (intersects.length > 0) { const intersectedPortal = intersects[0].object; console.log(`Entered portal: ${intersectedPortal.name}`); if (!isWarping) { startWarp(); } } // Warp effect animation if (isWarping) { const warpElapsed = elapsed - warpStartTime; const progress = Math.min(warpElapsed / WARP_DURATION, 1.0); warpPass.uniforms['time'].value = elapsed; // Ease in and out distortion if (progress < 0.5) { warpPass.uniforms['distortionStrength'].value = progress * 2.0; // 0 to 1 } else { warpPass.uniforms['distortionStrength'].value = (1.0 - progress) * 2.0; // 1 to 0 } if (progress >= 1.0) { isWarping = false; warpPass.enabled = false; warpPass.uniforms['distortionStrength'].value = 0.0; } } composer.render(); } animate(); // === AMBIENT SOUNDTRACK === // Procedural ambient score synthesised in-browser via Web Audio API. // Research: Google MusicFX (musicfx.sandbox.google.com) uses AI text prompts to // generate music clips. Since MusicFX has no public API, we replicate the desired // "deep space / sovereign" aesthetic procedurally. // // Architecture (4 layers): // 1. Sub-drone — two slow-detuned sawtooth oscillators at ~55 Hz (octave below A2) // 2. Pad — four detuned triangle oscillators in a minor 7th chord // 3. Sparkle — random high-register sine plucks on a pentatonic scale // 4. Noise hiss — pink-ish filtered noise for texture // All routed through: gain → convolver reverb → limiter → destination. /** @type {AudioContext|null} */ let audioCtx = null; /** @type {GainNode|null} */ let masterGain = null; /** @type {boolean} */ let audioRunning = false; /** @type {Array} */ const audioSources = []; /** @type {number|null} */ let sparkleTimer = null; /** * Builds a simple impulse-response buffer for reverb (synthetic room). * @param {AudioContext} ctx * @param {number} duration seconds * @param {number} decay * @returns {AudioBuffer} */ function buildReverbIR(ctx, duration, decay) { const rate = ctx.sampleRate; const len = Math.ceil(rate * duration); const buf = ctx.createBuffer(2, len, rate); for (let ch = 0; ch < 2; ch++) { const d = buf.getChannelData(ch); for (let i = 0; i < len; i++) { d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay); } } return buf; } /** * Starts the ambient soundtrack. Safe to call multiple times (idempotent). */ function startAmbient() { if (audioRunning) return; audioCtx = new AudioContext(); masterGain = audioCtx.createGain(); masterGain.gain.value = 0; // Reverb const convolver = audioCtx.createConvolver(); convolver.buffer = buildReverbIR(audioCtx, 3.5, 2.8); // Limiter (DynamicsCompressor as brickwall) const limiter = audioCtx.createDynamicsCompressor(); limiter.threshold.value = -3; limiter.knee.value = 0; limiter.ratio.value = 20; limiter.attack.value = 0.001; limiter.release.value = 0.1; masterGain.connect(convolver); convolver.connect(limiter); limiter.connect(audioCtx.destination); // -- Layer 1: Sub-drone (two detuned saws) -- [[55.0, -6], [55.0, +6]].forEach(([freq, detune]) => { const osc = audioCtx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = freq; osc.detune.value = detune; const g = audioCtx.createGain(); g.gain.value = 0.07; osc.connect(g); g.connect(masterGain); osc.start(); audioSources.push(osc); }); // -- Layer 2: Pad (minor 7th chord: A2, C3, E3, G3) -- [110, 130.81, 164.81, 196].forEach((freq, i) => { const detunes = [-8, 4, -3, 7]; const osc = audioCtx.createOscillator(); osc.type = 'triangle'; osc.frequency.value = freq; osc.detune.value = detunes[i]; // Slow LFO for gentle swell const lfo = audioCtx.createOscillator(); lfo.frequency.value = 0.05 + i * 0.013; const lfoGain = audioCtx.createGain(); lfoGain.gain.value = 0.02; lfo.connect(lfoGain); const g = audioCtx.createGain(); g.gain.value = 0.06; lfoGain.connect(g.gain); osc.connect(g); g.connect(masterGain); osc.start(); lfo.start(); audioSources.push(osc, lfo); }); // -- Layer 3: Noise hiss (filtered white noise) -- const noiseLen = audioCtx.sampleRate * 2; const noiseBuf = audioCtx.createBuffer(1, noiseLen, audioCtx.sampleRate); const nd = noiseBuf.getChannelData(0); // Simple pink-ish noise via first-order IIR let b0 = 0; for (let i = 0; i < noiseLen; i++) { const white = Math.random() * 2 - 1; b0 = 0.99 * b0 + white * 0.01; nd[i] = b0 * 3.5; } const noiseNode = audioCtx.createBufferSource(); noiseNode.buffer = noiseBuf; noiseNode.loop = true; const noiseFilter = audioCtx.createBiquadFilter(); noiseFilter.type = 'bandpass'; noiseFilter.frequency.value = 800; noiseFilter.Q.value = 0.5; const noiseGain = audioCtx.createGain(); noiseGain.gain.value = 0.012; noiseNode.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(masterGain); noiseNode.start(); audioSources.push(noiseNode); // -- Layer 4: Sparkle plucks (pentatonic: A4 C5 E5 A5 C6) -- const sparkleNotes = [440, 523.25, 659.25, 880, 1046.5]; function scheduleSparkle() { if (!audioRunning || !audioCtx) return; const osc = audioCtx.createOscillator(); osc.type = 'sine'; osc.frequency.value = sparkleNotes[Math.floor(Math.random() * sparkleNotes.length)]; const env = audioCtx.createGain(); const now = audioCtx.currentTime; env.gain.setValueAtTime(0, now); env.gain.linearRampToValueAtTime(0.08, now + 0.02); env.gain.exponentialRampToValueAtTime(0.0001, now + 1.8); osc.connect(env); env.connect(masterGain); osc.start(now); osc.stop(now + 1.9); // Schedule next sparkle: 3-9 seconds const nextMs = 3000 + Math.random() * 6000; sparkleTimer = setTimeout(scheduleSparkle, nextMs); } sparkleTimer = setTimeout(scheduleSparkle, 1000 + Math.random() * 3000); // Fade in master gain masterGain.gain.setValueAtTime(0, audioCtx.currentTime); masterGain.gain.linearRampToValueAtTime(0.9, audioCtx.currentTime + 2.0); audioRunning = true; document.getElementById('audio-toggle').textContent = '🔇'; } /** * Stops and tears down the ambient soundtrack. */ function stopAmbient() { if (!audioRunning || !audioCtx) return; audioRunning = false; if (sparkleTimer !== null) { clearTimeout(sparkleTimer); sparkleTimer = null; } const gain = masterGain; const ctx = audioCtx; gain.gain.setValueAtTime(gain.gain.value, ctx.currentTime); gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.8); setTimeout(() => { audioSources.forEach(n => { try { n.stop(); } catch (_) {} }); audioSources.length = 0; ctx.close(); audioCtx = null; masterGain = null; }, 900); document.getElementById('audio-toggle').textContent = '🔊'; } document.getElementById('audio-toggle').addEventListener('click', () => { if (audioRunning) { stopAmbient(); } else { startAmbient(); } }); // === DEBUG MODE === let debugMode = false; document.getElementById('debug-toggle').addEventListener('click', () => { debugMode = !debugMode; document.getElementById('debug-toggle').style.backgroundColor = debugMode ? 'var(--color-text-muted)' : 'var(--color-secondary)'; console.log(`Debug mode ${debugMode ? 'enabled' : 'disabled'}`); if (debugMode) { // Example: Visualize all collision boxes and light sources // Replace with actual logic when available document.querySelectorAll('.collision-box').forEach((/** @type {HTMLElement} */ el) => el.style.outline = '2px solid red'); document.querySelectorAll('.light-source').forEach((/** @type {HTMLElement} */ el) => el.style.outline = '2px dashed yellow'); } else { document.querySelectorAll('.collision-box, .light-source').forEach((/** @type {HTMLElement} */ el) => { el.style.outline = 'none'; }); } }); // === WEBSOCKET CLIENT === import { wsClient } from './ws-client.js'; wsClient.connect(); window.addEventListener('player-joined', (/** @type {CustomEvent} */ event) => { console.log('Player joined:', event.detail); }); window.addEventListener('player-left', (/** @type {CustomEvent} */ event) => { console.log('Player left:', event.detail); }); // === SESSION EXPORT === /** @type {{ ts: number, speaker: string, text: string }[]} */ const sessionLog = []; const sessionStart = Date.now(); /** * Appends an entry to the in-memory session log. * @param {string} speaker * @param {string} text */ function logMessage(speaker, text) { sessionLog.push({ ts: Date.now(), speaker, text }); } /** * Formats the session log as Markdown and triggers a browser download. */ function exportSessionAsMarkdown() { const startStr = new Date(sessionStart).toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; const lines = [ '# Nexus Session Export', '', `**Session started:** ${startStr}`, `**Messages:** ${sessionLog.length}`, '', '---', '', ]; for (const entry of sessionLog) { const timeStr = new Date(entry.ts).toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; lines.push(`### ${entry.speaker} — ${timeStr}`); lines.push(''); lines.push(entry.text); lines.push(''); } if (sessionLog.length === 0) { lines.push('*No messages recorded this session.*'); lines.push(''); } const blob = new Blob([lines.join('\n')], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `nexus-session-${new Date(sessionStart).toISOString().slice(0, 10)}.md`; a.click(); URL.revokeObjectURL(url); } const exportBtn = document.getElementById('export-session'); if (exportBtn) { exportBtn.addEventListener('click', exportSessionAsMarkdown); } window.addEventListener('chat-message', (/** @type {CustomEvent} */ event) => { console.log('Chat message:', event.detail); if (typeof event.detail?.text === 'string') { logMessage(event.detail.speaker || 'TIMMY', event.detail.text); showTimmySpeech(event.detail.text); if (event.detail.text.toLowerCase().includes('sovereignty')) { triggerSovereigntyEasterEgg(); } } }); window.addEventListener('status-update', (/** @type {CustomEvent} */ event) => { console.log('[hermes] Status update:', event.detail); }); window.addEventListener('pr-notification', (/** @type {CustomEvent} */ event) => { console.log('[hermes] PR notification:', event.detail); if (event.detail && event.detail.action === 'merged') { triggerMergeFlash(); } }); // === SOVEREIGNTY EASTER EGG === const SOVEREIGNTY_WORD = 'sovereignty'; let sovereigntyBuffer = ''; let sovereigntyBufferTimer = /** @type {ReturnType|null} */ (null); const sovereigntyMsg = document.getElementById('sovereignty-msg'); /** * Triggers the sovereignty Easter egg: stars pulse gold, message flashes. */ function triggerSovereigntyEasterEgg() { // Flash constellation lines gold const originalLineColor = constellationLines.material.color.getHex(); constellationLines.material.color.setHex(0xffd700); constellationLines.material.opacity = 0.9; // Stars burst gold const originalStarColor = starMaterial.color.getHex(); const originalStarOpacity = starMaterial.opacity; starMaterial.color.setHex(0xffd700); starMaterial.opacity = 1.0; // Show overlay message if (sovereigntyMsg) { sovereigntyMsg.classList.remove('visible'); // Force reflow so animation restarts void sovereigntyMsg.offsetWidth; sovereigntyMsg.classList.add('visible'); } // Animate gold fade-out over 2.5s const startTime = performance.now(); const DURATION = 2500; function fadeBack() { const t = Math.min((performance.now() - startTime) / DURATION, 1); const eased = t * t; // ease in: slow start, fast end // Interpolate star color back const goldR = 1.0, goldG = 0.843, goldB = 0; const origColor = new THREE.Color(originalStarColor); starMaterial.color.setRGB( goldR + (origColor.r - goldR) * eased, goldG + (origColor.g - goldG) * eased, goldB + (origColor.b - goldB) * eased ); starMaterial.opacity = 1.0 + (originalStarOpacity - 1.0) * eased; // Interpolate line color back const origLineColor = new THREE.Color(originalLineColor); constellationLines.material.color.setRGB( 1.0 + (origLineColor.r - 1.0) * eased, 0.843 + (origLineColor.g - 0.843) * eased, 0 + origLineColor.b * eased ); if (t < 1) { requestAnimationFrame(fadeBack); } else { // Restore originals exactly starMaterial.color.setHex(originalStarColor); starMaterial.opacity = originalStarOpacity; constellationLines.material.color.setHex(originalLineColor); if (sovereigntyMsg) sovereigntyMsg.classList.remove('visible'); } } requestAnimationFrame(fadeBack); } /** * Triggers a visual flash effect for merge events: stars pulse bright, lines glow. */ function triggerMergeFlash() { // Flash constellation lines bright blue-green const originalLineColor = constellationLines.material.color.getHex(); constellationLines.material.color.setHex(0x00ffff); constellationLines.material.opacity = 1.0; // Stars burst bright blue-green const originalStarColor = starMaterial.color.getHex(); const originalStarOpacity = starMaterial.opacity; starMaterial.color.setHex(0x00ffff); starMaterial.opacity = 1.0; // Animate fade-out over 2.0s const startTime = performance.now(); const DURATION = 2000; // 2 seconds function fadeBack() { const t = Math.min((performance.now() - startTime) / DURATION, 1); const eased = t * t; // ease in: slow start, fast end // Interpolate star color back const mergeR = 0.0, mergeG = 1.0, mergeB = 1.0; // Cyan const origStarColor = new THREE.Color(originalStarColor); starMaterial.color.setRGB( mergeR + (origStarColor.r - mergeR) * eased, mergeG + (origStarColor.g - mergeG) * eased, mergeB + (origStarColor.b - mergeB) * eased ); starMaterial.opacity = 1.0 + (originalStarOpacity - 1.0) * eased; // Interpolate line color back const origLineColor = new THREE.Color(originalLineColor); constellationLines.material.color.setRGB( mergeR + (origLineColor.r - mergeR) * eased, mergeG + (origLineColor.g - mergeG) * eased, mergeB + (origLineColor.b - mergeB) * eased ); constellationLines.material.opacity = 1.0 + (0.18 - 1.0) * eased; // Assuming original opacity is 0.18 for lines. if (t < 1) { requestAnimationFrame(fadeBack); } else { // Restore originals exactly starMaterial.color.setHex(originalStarColor); starMaterial.opacity = originalStarOpacity; constellationLines.material.color.setHex(originalLineColor); constellationLines.material.opacity = 0.18; // Explicitly set to original } } requestAnimationFrame(fadeBack); } // Detect 'sovereignty' typed anywhere on the page (cheat-code style) document.addEventListener('keydown', (e) => { if (e.metaKey || e.ctrlKey || e.altKey) return; if (e.key.length !== 1) { // Non-printable key resets buffer sovereigntyBuffer = ''; return; } sovereigntyBuffer += e.key.toLowerCase(); // Keep only the last N chars needed if (sovereigntyBuffer.length > SOVEREIGNTY_WORD.length) { sovereigntyBuffer = sovereigntyBuffer.slice(-SOVEREIGNTY_WORD.length); } if (sovereigntyBuffer === SOVEREIGNTY_WORD) { sovereigntyBuffer = ''; triggerSovereigntyEasterEgg(); } // Reset buffer after 3s of inactivity if (sovereigntyBufferTimer) clearTimeout(sovereigntyBufferTimer); sovereigntyBufferTimer = setTimeout(() => { sovereigntyBuffer = ''; }, 3000); }); window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); // === COMMIT BANNERS === const commitBanners = []; const portalGroup = new THREE.Group(); scene.add(portalGroup); /** * Creates 3D representations of portals from the loaded data. */ function createPortals() { const portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100); portals.forEach(portal => { const portalMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(portal.color).convertSRGBToLinear(), transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, }); const portalMesh = new THREE.Mesh(portalGeo, portalMat); portalMesh.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z); portalMesh.rotation.y = portal.rotation.y; // Apply Y rotation portalMesh.rotation.x = Math.PI / 2; // Orient to stand vertically portalMesh.name = `portal-${portal.id}`; portalGroup.add(portalMesh); }); } // === PORTALS === /** @type {Array} */ let portals = []; async function loadPortals() { try { const res = await fetch('./portals.json'); if (!res.ok) throw new Error('Portals not found'); portals = await res.json(); console.log('Loaded portals:', portals); createPortals(); } catch (error) { console.error('Failed to load portals:', error); } } // === AGENT STATUS PANELS (declared early — populated after scene is ready) === /** @type {THREE.Sprite[]} */ const agentPanelSprites = []; /** * Creates a canvas texture for a commit banner. * @param {string} hash - Short commit hash * @param {string} message - Commit subject line * @returns {THREE.CanvasTexture} */ function createCommitTexture(hash, message) { const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 64; const ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgba(0, 0, 16, 0.75)'; ctx.fillRect(0, 0, 512, 64); ctx.strokeStyle = '#4488ff'; ctx.lineWidth = 1; ctx.strokeRect(0.5, 0.5, 511, 63); ctx.font = 'bold 11px "Courier New", monospace'; ctx.fillStyle = '#4488ff'; ctx.fillText(hash, 10, 20); ctx.font = '12px "Courier New", monospace'; ctx.fillStyle = '#ccd6f6'; const displayMsg = message.length > 54 ? message.slice(0, 54) + '\u2026' : message; ctx.fillText(displayMsg, 10, 46); return new THREE.CanvasTexture(canvas); } /** * Fetches recent commits and spawns floating banner sprites. */ async function initCommitBanners() { 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(/** @type {(c: any) => {hash: string, message: string}} */ 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' }, ]; // Load commit banners after assets are ready initCommitBanners(); } const spreadX = [-7, -3.5, 0, 3.5, 7]; const spreadY = [1.0, -1.5, 2.2, -0.8, 1.6]; const spreadZ = [-1.5, -2.5, -1.0, -2.0, -1.8]; commits.forEach((commit, i) => { const texture = createCommitTexture(commit.hash, commit.message); const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0, depthWrite: false, }); const sprite = new THREE.Sprite(material); sprite.scale.set(12, 1.5, 1); sprite.position.set( spreadX[i % spreadX.length], spreadY[i % spreadY.length], spreadZ[i % spreadZ.length] ); sprite.userData = { baseY: spreadY[i % spreadY.length], floatPhase: (i / commits.length) * Math.PI * 2, floatSpeed: 0.25 + i * 0.07, startDelay: i * 2.5, lifetime: 12 + i * 1.5, spawnTime: /** @type {number|null} */ (null), zoomLabel: `Commit: ${commit.hash}`, }; scene.add(sprite); commitBanners.push(sprite); }); } initCommitBanners(); loadPortals(); // === AGENT STATUS BOARD === const AGENT_STATUS_STUB = { agents: [ { name: 'claude', status: 'working', issue: 'Live agent status board (#199)', prs_today: 3 }, { name: 'gemini', status: 'idle', issue: null, prs_today: 1 }, { name: 'kimi', status: 'working', issue: 'Portal system YAML registry (#5)', prs_today: 2 }, { name: 'groq', status: 'idle', issue: null, prs_today: 0 }, { name: 'grok', status: 'dead', issue: null, prs_today: 0 }, ] }; const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dead: '#ff4444' }; /** * Builds a canvas texture for a single agent holo-panel. * @param {{ name: string, status: string, issue: string|null, prs_today: number }} agent * @returns {THREE.CanvasTexture} */ function createAgentPanelTexture(agent) { const W = 400, H = 200; const canvas = document.createElement('canvas'); canvas.width = W; canvas.height = H; const ctx = canvas.getContext('2d'); const sc = AGENT_STATUS_COLORS[agent.status] || '#4488ff'; // Dark background ctx.fillStyle = 'rgba(0, 8, 24, 0.88)'; ctx.fillRect(0, 0, W, H); // Outer border in status color ctx.strokeStyle = sc; ctx.lineWidth = 2; ctx.strokeRect(1, 1, W - 2, H - 2); // Faint inner border ctx.strokeStyle = sc; ctx.lineWidth = 1; ctx.globalAlpha = 0.3; ctx.strokeRect(4, 4, W - 8, H - 8); ctx.globalAlpha = 1.0; // Agent name ctx.font = 'bold 28px "Courier New", monospace'; ctx.fillStyle = '#ffffff'; 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 "Courier New", monospace'; ctx.fillStyle = sc; ctx.textAlign = 'right'; ctx.fillText(agent.status.toUpperCase(), W - 16, 60); ctx.textAlign = 'left'; // Separator ctx.strokeStyle = '#1a3a6a'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(16, 70); ctx.lineTo(W - 16, 70); ctx.stroke(); // Current issue label ctx.font = '10px "Courier New", monospace'; ctx.fillStyle = '#556688'; ctx.fillText('CURRENT ISSUE', 16, 90); ctx.font = '13px "Courier New", monospace'; ctx.fillStyle = '#ccd6f6'; const issueText = agent.issue || '\u2014 none \u2014'; const displayIssue = issueText.length > 40 ? issueText.slice(0, 40) + '\u2026' : issueText; ctx.fillText(displayIssue, 16, 110); // Separator ctx.strokeStyle = '#1a3a6a'; ctx.beginPath(); ctx.moveTo(16, 128); ctx.lineTo(W - 16, 128); ctx.stroke(); // PRs merged today ctx.font = '10px "Courier New", monospace'; ctx.fillStyle = '#556688'; ctx.fillText('PRs MERGED TODAY', 16, 148); ctx.font = 'bold 28px "Courier New", monospace'; ctx.fillStyle = '#4488ff'; ctx.fillText(String(agent.prs_today), 16, 182); return new THREE.CanvasTexture(canvas); } /** Group holding all agent panels so they can be toggled/repositioned together. */ const agentBoardGroup = new THREE.Group(); scene.add(agentBoardGroup); const BOARD_RADIUS = 9.5; // distance from scene origin const BOARD_Y = 4.2; // height above platform const BOARD_SPREAD = Math.PI * 0.75; // 135° total arc, centred on negative-Z axis /** * (Re)builds the agent panel sprites from fresh status data. * @param {{ agents: Array<{ name: string, status: string, issue: string|null, prs_today: number }> }} statusData */ function rebuildAgentPanels(statusData) { while (agentBoardGroup.children.length) agentBoardGroup.remove(agentBoardGroup.children[0]); agentPanelSprites.length = 0; const n = statusData.agents.length; statusData.agents.forEach((agent, i) => { const t = n === 1 ? 0.5 : i / (n - 1); // Spread in a semi-circle: angle=PI is directly behind (negative-Z) 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 = createAgentPanelTexture(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}`, }; agentBoardGroup.add(sprite); agentPanelSprites.push(sprite); }); } /** * Fetches live agent status, falling back to the stub when the endpoint is unavailable. * @returns {Promise} */ async function fetchAgentStatus() { try { const res = await fetch('/api/status.json'); if (!res.ok) throw new Error('status ' + res.status); return await res.json(); } catch { return AGENT_STATUS_STUB; } } async function refreshAgentBoard() { const data = await fetchAgentStatus(); rebuildAgentPanels(data); } // Initial render, then poll every 30 s refreshAgentBoard(); setInterval(refreshAgentBoard, 30000); // === TIMMY SPEECH BUBBLE === // When Timmy sends a chat message, a glowing floating text sprite appears near // his avatar position above the platform. Fades in quickly, holds for 5 s total, // then fades out. Only the most recent message is shown. const TIMMY_SPEECH_POS = new THREE.Vector3(0, 8.2, 1.5); const SPEECH_DURATION = 5.0; // total seconds visible (including fades) const SPEECH_FADE_IN = 0.35; const SPEECH_FADE_OUT = 0.7; /** @type {THREE.Sprite|null} */ let timmySpeechSprite = null; /** @type {{ startTime: number, sprite: THREE.Sprite }|null} */ let timmySpeechState = null; /** * Builds a canvas texture for a Timmy speech bubble. * @param {string} text * @returns {THREE.CanvasTexture} */ function createSpeechBubbleTexture(text) { const W = 512, H = 100; const canvas = document.createElement('canvas'); canvas.width = W; canvas.height = H; const ctx = canvas.getContext('2d'); // Semi-transparent dark background ctx.fillStyle = 'rgba(0, 6, 20, 0.85)'; ctx.fillRect(0, 0, W, H); // Neon blue glow border ctx.strokeStyle = '#66aaff'; ctx.lineWidth = 2; ctx.strokeRect(1, 1, W - 2, H - 2); // Inner subtle border ctx.strokeStyle = '#2244aa'; ctx.lineWidth = 1; ctx.strokeRect(4, 4, W - 8, H - 8); // "TIMMY:" label ctx.font = 'bold 12px "Courier New", monospace'; ctx.fillStyle = '#4488ff'; ctx.fillText('TIMMY:', 12, 22); // Message text — truncate to two lines if needed const LINE1_MAX = 42; const LINE2_MAX = 48; ctx.font = '15px "Courier New", monospace'; ctx.fillStyle = '#ddeeff'; if (text.length <= LINE1_MAX) { ctx.fillText(text, 12, 58); } else { ctx.fillText(text.slice(0, LINE1_MAX), 12, 46); const rest = text.slice(LINE1_MAX, LINE1_MAX + LINE2_MAX); ctx.font = '13px "Courier New", monospace'; ctx.fillStyle = '#aabbcc'; ctx.fillText(rest + (text.length > LINE1_MAX + LINE2_MAX ? '\u2026' : ''), 12, 76); } return new THREE.CanvasTexture(canvas); } /** * Shows a floating speech bubble near Timmy's avatar. * Immediately replaces any existing bubble. * @param {string} text */ function showTimmySpeech(text) { if (timmySpeechSprite) { scene.remove(timmySpeechSprite); if (timmySpeechSprite.material.map) timmySpeechSprite.material.map.dispose(); timmySpeechSprite.material.dispose(); timmySpeechSprite = null; timmySpeechState = null; } const texture = createSpeechBubbleTexture(text); const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0, depthWrite: false, }); const sprite = new THREE.Sprite(material); sprite.scale.set(8.5, 1.65, 1); sprite.position.copy(TIMMY_SPEECH_POS); scene.add(sprite); timmySpeechSprite = sprite; timmySpeechState = { startTime: clock.getElapsedTime(), sprite }; }