/** * GMIIE Metaverse Hub — Three.js dimension galaxy * Live feeds: /api/metaverse/dimensions, /api/pulse, /api/gmiie/live/digest */ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; (function (global) { let scene, camera, renderer, controls, raycaster, mouse; let dimensionMeshes = []; let particles; let selectedDim = null; let registryData = null; let gamificationData = null; let marketData = null; let animId = null; let xrSession = null; const DONK_GAMIFICATION = 'https://donkai.org/api/metaverse/gamification'; const DONK_MARKET = 'https://donkai.org/api/metaverse/commerce/market'; const DONK_TRADE = 'https://donkai.org/api/metaverse/commerce/trade'; function $(id) { return document.getElementById(id); } async function fetchJson(url) { const r = await fetch(url, { signal: AbortSignal.timeout(15000) }); if (!r.ok) throw new Error(url + ' ' + r.status); return r.json(); } function statusBadge(status) { const s = (status || 'STUB').toUpperCase(); if (s === 'LIVE') return 'LIVE'; if (s === 'PARTIAL') return 'PARTIAL'; return 'STUB'; } function geometryForType(type, color) { const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.35, metalness: 0.6, roughness: 0.25, }); let geo; switch (type) { case 'torus': geo = new THREE.TorusGeometry(1.2, 0.35, 16, 48); break; case 'icosahedron': geo = new THREE.IcosahedronGeometry(1.4, 1); break; case 'octahedron': geo = new THREE.OctahedronGeometry(1.3, 0); break; case 'dodecahedron': geo = new THREE.DodecahedronGeometry(1.2, 0); break; case 'cone': geo = new THREE.ConeGeometry(1, 2, 8); break; case 'box': geo = new THREE.BoxGeometry(1.8, 1.8, 1.8); break; default: geo = new THREE.SphereGeometry(1.2, 32, 32); } return new THREE.Mesh(geo, mat); } function createLabel(text, color) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 512; canvas.height = 128; ctx.fillStyle = 'rgba(0,0,0,0.75)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = color; ctx.lineWidth = 4; ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); ctx.font = 'bold 36px JetBrains Mono, monospace'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'center'; ctx.fillText(text, canvas.width / 2, 72); const tex = new THREE.CanvasTexture(canvas); const mat = new THREE.SpriteMaterial({ map: tex, transparent: true }); const sprite = new THREE.Sprite(mat); sprite.scale.set(4, 1, 1); return sprite; } function buildGalaxy(dimensions) { dimensionMeshes.forEach((m) => { scene.remove(m.group); }); dimensionMeshes = []; dimensions.forEach((dim, i) => { const angle = ((dim.orbit_angle_deg ?? i * 24) * Math.PI) / 180; const radius = dim.orbit_radius ?? 12 + i * 1.5; const x = Math.cos(angle) * radius; const z = Math.sin(angle) * radius; const y = Math.sin(i * 0.7) * 2; const group = new THREE.Group(); group.position.set(x, y, z); group.userData = dim; const mesh = geometryForType(dim.geometry_type, dim.color || '#d4af37'); mesh.userData = dim; group.add(mesh); const label = createLabel(dim.short_name || dim.name, dim.color || '#fff'); label.position.set(0, 2.2, 0); group.add(label); if (dim.live_score != null) { const ring = new THREE.Mesh( new THREE.RingGeometry(1.8, 2.1, 32), new THREE.MeshBasicMaterial({ color: dim.color, transparent: true, opacity: 0.5, side: THREE.DoubleSide }) ); ring.rotation.x = Math.PI / 2; group.add(ring); } scene.add(group); dimensionMeshes.push({ group, mesh, dim }); }); } function createParticles() { const count = 1200; const geo = new THREE.BufferGeometry(); const pos = new Float32Array(count * 3); for (let i = 0; i < count; i++) { pos[i * 3] = (Math.random() - 0.5) * 120; pos[i * 3 + 1] = (Math.random() - 0.5) * 60; pos[i * 3 + 2] = (Math.random() - 0.5) * 120; } geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const mat = new THREE.PointsMaterial({ color: 0xd4af37, size: 0.15, transparent: true, opacity: 0.6 }); particles = new THREE.Points(geo, mat); scene.add(particles); } function updateHud(data) { const nigEl = $('mv-nig-score'); const readEl = $('mv-nig-reading'); const tsEl = $('mv-ts'); if (nigEl && data?.nig) { const score = data.nig.score ?? data.nig_composite ?? '—'; nigEl.textContent = score >= 0 ? '+' + score : String(score); } else if (nigEl && data?.dimensions?.[0]?.nig_composite != null) { const s = data.dimensions[0].nig_composite; nigEl.textContent = s >= 0 ? '+' + s : String(s); } if (readEl && data?.nig?.reading) readEl.textContent = data.nig.reading; else if (readEl && data?.dimensions?.[0]?.nig_reading) readEl.textContent = data.dimensions[0].nig_reading; if (tsEl) tsEl.textContent = new Date().toLocaleTimeString(); } function gamificationForNpc(npc) { if (!npc || !gamificationData?.agents) return null; const agentId = npc.donk_agent_id || npc.id; return gamificationData.agents.find(function (a) { return a.agent_id === agentId || a.agent_id === npc.id; }); } function renderPanel(dim, detail) { const panel = $('mv-panel'); if (!panel) return; const articles = (detail?.articles || []).slice(0, 4); const npc = detail?.npc; const gami = gamificationForNpc(npc); const xpBlock = gami ? '
Donk XP L' + gami.level + ' · ' + gami.xp + ' XP · ' + (gami.growth_stage || 'seedling') + '
' : ''; panel.innerHTML = '
' + '' + '
' + statusBadge(dim.status) + '

' + dim.name + '

' + (dim.live_score != null ? '
Live score ' + dim.live_score + '
' : '') + '
' + (npc ? '
NPC: ' + npc.name + ' — ' + statusBadge(npc.status) + '

' + npc.role + '

' + xpBlock + '
' : '') + '
' + (detail?.desk_pulse?.summary || detail?.desk_pulse?.headline || 'Ambient desk feed — connect via live digest.') + '
' + (articles.length ? '
Tagged signals
' + articles .map(function (a) { return ( '
' + (a.headline || a.topic) + 'R' + (a.ring || '?') + ' · ' + (a.signal_strength || '—') + '

' + (a.summary || '').slice(0, 180) + '…

' ); }) .join('') + '
' : '') + '
' + 'Enter Desk →' + 'Donk Federation →' + '
'; panel.classList.add('open'); document.body.classList.add('mv-panel-open'); $('mv-close')?.addEventListener('click', closePanel); } function closePanel() { $('mv-panel')?.classList.remove('open'); document.body.classList.remove('mv-panel-open'); selectedDim = null; dimensionMeshes.forEach(function (m) { m.mesh.material.emissiveIntensity = 0.35; }); } async function selectDimension(dim) { selectedDim = dim; dimensionMeshes.forEach(function (m) { m.mesh.material.emissiveIntensity = m.dim.id === dim.id ? 0.85 : 0.35; }); try { const detail = await fetchJson('/api/metaverse/dimension/' + encodeURIComponent(dim.id)); renderPanel(dim, detail); } catch (_) { renderPanel(dim, null); } } function onClick(event) { const rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const hits = raycaster.intersectObjects( dimensionMeshes.map(function (m) { return m.mesh; }) ); if (hits.length) { selectDimension(hits[0].object.userData); } } function animate() { animId = requestAnimationFrame(animate); const t = Date.now() * 0.001; dimensionMeshes.forEach(function (m, i) { m.group.rotation.y += 0.004; m.mesh.rotation.x = Math.sin(t + i) * 0.15; m.group.position.y += Math.sin(t * 0.8 + i) * 0.002; }); if (particles) particles.rotation.y += 0.0003; controls.update(); renderer.render(scene, camera); } async function enterXR() { if (!navigator.xr) { alert('WebXR not available in this browser.'); return; } try { const supported = await navigator.xr.isSessionSupported('immersive-vr'); if (!supported) { alert('Immersive VR not supported — try a WebXR-capable browser.'); return; } xrSession = await navigator.xr.requestSession('immersive-vr'); await renderer.xr.setSession(xrSession); $('mv-xr-btn').textContent = 'XR Active'; } catch (e) { alert('WebXR entry failed: ' + e.message); } } function gamificationApiUrl() { return (registryData && registryData.gamification_api) || DONK_GAMIFICATION; } async function loadDonkGamification() { try { gamificationData = await fetchJson(gamificationApiUrl()); } catch (_) { gamificationData = null; } } async function loadMarketSidebar() { const list = $('mv-market-list'); const skuSelect = $('mv-trade-sku'); const countEl = $('mv-market-count'); if (!list) return; try { marketData = await fetchJson(DONK_MARKET); const items = marketData.listings || []; if (countEl) countEl.textContent = items.length + ' SKUs'; list.innerHTML = items.length ? items .map(function (l) { return ( '
' + l.sku + ' ' + statusBadge(l.label) + '
' + l.name + ' · ' + l.price_atp + ' ATP
' ); }) .join('') : '

No listings

'; if (skuSelect) { skuSelect.innerHTML = items .map(function (l) { return ''; }) .join(''); } } catch (_) { list.innerHTML = '

Market offline — retry when donkai.org is up

'; } } async function previewDonkTrade() { const sku = $('mv-trade-sku')?.value; const result = $('mv-trade-result'); if (!sku || !result) return; result.textContent = 'Requesting…'; try { const r = await fetch(DONK_TRADE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sku: sku, buyer_agent: 'gmiie-metaverse-guest', preview: true }), signal: AbortSignal.timeout(15000), }); const data = await r.json().catch(function () { return {}; }); result.textContent = JSON.stringify( { http_status: r.status, label: data.label || (r.status === 402 ? 'PAYMENT_REQUIRED' : 'TRADE_PREVIEW'), sku: sku, price_atp: data.price_atp, facilitator: data.facilitator, message: data.message, settlement: 'STUB — Apostle ATP on-chain', }, null, 2 ); } catch (e) { result.textContent = 'Preview failed: ' + e.message; } } async function loadLiveFeeds() { try { registryData = await fetchJson('/api/metaverse/dimensions'); buildGalaxy(registryData.dimensions || []); updateHud(registryData); $('mv-mode').textContent = registryData.mode || 'registry'; $('mv-dim-count').textContent = (registryData.dimensions || []).length + ' dimensions'; } catch (e) { $('mv-mode').textContent = 'offline — static fallback'; try { const fallback = await fetchJson('/data/gmiie-dimensions.json'); registryData = fallback; buildGalaxy(fallback.dimensions || []); $('mv-dim-count').textContent = (fallback.dimensions || []).length + ' dimensions'; } catch (_) {} } try { const pulse = await fetchJson('/api/pulse'); const pulseEl = $('mv-pulse-snippet'); if (pulseEl) { pulseEl.textContent = (pulse.summary || pulse.lead_title || 'GMIIE Pulse synced.').slice(0, 220); } } catch (_) {} try { const digest = await fetchJson('/api/gmiie/live/digest'); const digestEl = $('mv-digest-snippet'); if (digestEl && digest.as_of_date) { digestEl.textContent = 'Digest ' + digest.as_of_date + ' · ' + (digest.jurisdiction_label || 'US-FED'); } } catch (_) {} } function initThree() { const canvas = $('mv-canvas'); scene = new THREE.Scene(); scene.background = new THREE.Color(0x060810); scene.fog = new THREE.FogExp2(0x060810, 0.012); camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 500); camera.position.set(0, 18, 42); renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.xr.enabled = true; controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.06; controls.maxDistance = 90; controls.minDistance = 8; scene.add(new THREE.AmbientLight(0xffffff, 0.35)); const key = new THREE.DirectionalLight(0xd4af37, 1.2); key.position.set(20, 40, 10); scene.add(key); const fill = new THREE.PointLight(0x5b9aff, 0.8, 80); fill.position.set(-15, 10, -20); scene.add(fill); const core = new THREE.Mesh( new THREE.SphereGeometry(2.5, 32, 32), new THREE.MeshStandardMaterial({ color: 0xd4af37, emissive: 0xb8953a, emissiveIntensity: 0.5, metalness: 0.8, roughness: 0.2 }) ); scene.add(core); const coreLabel = createLabel('GMIIE CORE', '#d4af37'); coreLabel.position.set(0, 4, 0); scene.add(coreLabel); createParticles(); raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); renderer.domElement.addEventListener('click', onClick); window.addEventListener('resize', function () { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); $('mv-xr-btn')?.addEventListener('click', enterXR); $('mv-trade-btn')?.addEventListener('click', previewDonkTrade); loadDonkGamification(); loadMarketSidebar(); const params = new URLSearchParams(location.search); const dimParam = params.get('dim'); loadLiveFeeds().then(function () { if (dimParam) { const hit = (registryData?.dimensions || []).find(function (d) { return d.id === dimParam; }); if (hit) selectDimension(hit); } }); animate(); } initThree(); global.GMIIMetaverse = { reload: function () { loadLiveFeeds(); loadDonkGamification(); loadMarketSidebar(); }, closePanel: closePanel, }; })(window);