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" +}