From 29508b9ce530f7b17ee84a00b61ffdf1f70e99cf Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 23:06:39 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20add=20sovereignty=20meter=20=E2=80=94?= =?UTF-8?q?=203D=20holographic=20arc=20gauge=20(#470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-implement the sovereignty meter from reference/v2-modular into the v0-golden baseline app.js. Adds a floating holographic torus arc gauge centered above the Nexus showing sovereignty score (0–100%) with color-coded status: green ≥80, yellow ≥40, red <40. - createSovereigntyMeter(): builds TorusGeometry arc, PointLight, and Canvas sprite showing score + label - loadSovereigntyStatus(): fetches sovereignty-status.json and updates geometry/colors/texture dynamically - Game loop animation: gentle bob (sin wave) + slow rotation - sovereignty-status.json stub with score=75 / label="Stable" Fixes #470 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 113 ++++++++++++++++++++++++++++++++++++++++ sovereignty-status.json | 5 ++ 2 files changed, 118 insertions(+) create mode 100644 sovereignty-status.json diff --git a/app.js b/app.js index 9a03e40..8c1d14f 100644 --- a/app.js +++ b/app.js @@ -39,6 +39,13 @@ let thoughtStreamMesh; let harnessPulseMesh; let powerMeterBars = []; let particles, dustParticles; +let sovereigntyGroup = null; +let sovereigntyScoreArcMesh = null; +let sovereigntyScoreArcMat = null; +let sovereigntyMeterLight = null; +let sovereigntySpriteMat = null; +let sovereigntyScore = 85; +let sovereigntyLabel = 'Mostly Sovereign'; let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; @@ -124,6 +131,8 @@ async function init() { createThoughtStream(); createHarnessPulse(); createSessionPowerMeter(); + createSovereigntyMeter(); + loadSovereigntyStatus(); updateLoad(90); composer = new EffectComposer(renderer); @@ -639,6 +648,104 @@ function createHarnessPulse() { scene.add(harnessPulseMesh); } +// ═══ SOVEREIGNTY METER ═══ +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); +} + +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, 50); + ctx.font = '16px "Courier New", monospace'; + ctx.fillStyle = '#8899bb'; + ctx.fillText(label.toUpperCase(), 128, 74); + ctx.font = '11px "Courier New", monospace'; + ctx.fillStyle = '#445566'; + ctx.fillText('SOVEREIGNTY', 128, 94); + ctx.font = '9px "Courier New", monospace'; + ctx.fillStyle = '#334455'; + ctx.fillText('MANUAL ASSESSMENT', 128, 112); + return new THREE.CanvasTexture(canvas); +} + +function createSovereigntyMeter() { + sovereigntyGroup = new THREE.Group(); + sovereigntyGroup.position.set(0, 3.8, 0); + + // Background ring + 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)); + + // Score arc + sovereigntyScoreArcMat = new THREE.MeshBasicMaterial({ + color: sovereigntyHexColor(sovereigntyScore), + transparent: true, + opacity: 0.9, + }); + sovereigntyScoreArcMesh = new THREE.Mesh(buildScoreArcGeo(sovereigntyScore), sovereigntyScoreArcMat); + sovereigntyScoreArcMesh.rotation.z = Math.PI / 2; + sovereigntyGroup.add(sovereigntyScoreArcMesh); + + // Glow light + sovereigntyMeterLight = new THREE.PointLight(sovereigntyHexColor(sovereigntyScore), 0.7, 6); + sovereigntyGroup.add(sovereigntyMeterLight); + + // Score/label sprite + sovereigntySpriteMat = new THREE.SpriteMaterial({ + map: buildMeterTexture(sovereigntyScore, sovereigntyLabel), + transparent: true, + depthWrite: false, + }); + const meterSprite = new THREE.Sprite(sovereigntySpriteMat); + 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; + + if (sovereigntyScoreArcMesh && sovereigntyScoreArcMat && sovereigntyMeterLight && sovereigntySpriteMat) { + sovereigntyScoreArcMesh.geometry.dispose(); + sovereigntyScoreArcMesh.geometry = buildScoreArcGeo(score); + const col = sovereigntyHexColor(score); + sovereigntyScoreArcMat.color.setHex(col); + sovereigntyMeterLight.color.setHex(col); + if (sovereigntySpriteMat.map) sovereigntySpriteMat.map.dispose(); + sovereigntySpriteMat.map = buildMeterTexture(score, label); + sovereigntySpriteMat.needsUpdate = true; + } + } catch { + // defaults already set + } +} + function createSessionPowerMeter() { const group = new THREE.Group(); group.position.set(0, 0, 3); @@ -1451,6 +1558,12 @@ function gameLoop() { bar.scale.x = active ? 1.2 : 1.0; }); + // Sovereignty meter — float and rotate + if (sovereigntyGroup) { + sovereigntyGroup.position.y = 3.8 + Math.sin(elapsed * 0.8) * 0.15; + sovereigntyGroup.rotation.y = elapsed * 0.2; + } + if (thoughtStreamMesh) { thoughtStreamMesh.material.uniforms.uTime.value = elapsed; thoughtStreamMesh.rotation.y = elapsed * 0.05; diff --git a/sovereignty-status.json b/sovereignty-status.json new file mode 100644 index 0000000..600fdbc --- /dev/null +++ b/sovereignty-status.json @@ -0,0 +1,5 @@ +{ + "score": 75, + "label": "Stable", + "assessment_type": "MANUAL" +} -- 2.43.0