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 { 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); // === 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; platformRim.userData.inspectName = 'Glass Platform'; platformRim.userData.inspectDesc = 'Central floating platform — the sovereign foundation of the Nexus.'; 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; borderTorus.userData.inspectName = 'Platform Rim'; borderTorus.userData.inspectDesc = 'Raised metallic torus encircling the Nexus platform.'; 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); // === 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'); } } }); // === PHOTO MODE === let photoMode = false; // 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 scoreArcMesh.userData.inspectName = 'Sovereignty Meter'; scoreArcMesh.userData.inspectDesc = 'Holographic arc gauge tracking Timmy\'s sovereignty score across all systems.'; 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); meterSprite.userData.inspectName = 'Sovereignty Score'; meterSprite.userData.inspectDesc = 'Live sovereignty score display. Reads from sovereignty-status.json.'; sovereigntyGroup.add(meterSprite); scene.add(sovereigntyGroup); 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(); // === 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; camera.position.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT); 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; 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; } composer.render(); } animate(); // === 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); }); window.addEventListener('chat-message', (/** @type {CustomEvent} */ event) => { console.log('Chat message:', event.detail); if (typeof event.detail?.text === 'string' && event.detail.text.toLowerCase().includes('sovereignty')) { triggerSovereigntyEasterEgg(); } }); // === 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); } // 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 = []; // === 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), inspectName: `Commit ${commit.hash}`, inspectDesc: commit.message, }; scene.add(sprite); commitBanners.push(sprite); }); } initCommitBanners(); // === 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, inspectName: `Agent: ${agent.name.toUpperCase()}`, inspectDesc: `Status: ${agent.status} · PRs today: ${agent.prs_today}${agent.issue ? '\n' + agent.issue : ''}`, }; 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); // === OBJECT INSPECTION (right-click) === const inspectRaycaster = new THREE.Raycaster(); const inspectTooltip = document.getElementById('inspect-tooltip'); const inspectNameEl = document.getElementById('inspect-name'); const inspectDescEl = document.getElementById('inspect-desc'); /** * Shows the inspection tooltip near the cursor. * @param {number} x - Client X position * @param {number} y - Client Y position * @param {string} name - Object name * @param {string} desc - Object description */ function showInspectTooltip(x, y, name, desc) { inspectNameEl.textContent = name; inspectDescEl.textContent = desc; // Offset from cursor; keep within viewport const tx = Math.min(x + 14, window.innerWidth - 300); const ty = Math.min(y + 14, window.innerHeight - 80); inspectTooltip.style.left = tx + 'px'; inspectTooltip.style.top = ty + 'px'; inspectTooltip.classList.add('visible'); } renderer.domElement.addEventListener('contextmenu', (/** @type {MouseEvent} */ e) => { e.preventDefault(); const mouse = new THREE.Vector2( (e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1 ); inspectRaycaster.setFromCamera(mouse, camera); const hits = inspectRaycaster.intersectObjects(scene.children, true); const hit = hits.find(h => h.object.userData.inspectName); if (hit) { const ud = hit.object.userData; showInspectTooltip(e.clientX, e.clientY, ud.inspectName, ud.inspectDesc || ''); } else { inspectTooltip.classList.remove('visible'); } }); document.addEventListener('click', () => inspectTooltip.classList.remove('visible')); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') inspectTooltip.classList.remove('visible'); });