feat: add sovereignty meter — 3D holographic arc gauge (#470)
Some checks failed
CI / validate (pull_request) Failing after 4s
Some checks failed
CI / validate (pull_request) Failing after 4s
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 <noreply@anthropic.com>
This commit is contained in:
113
app.js
113
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;
|
||||
|
||||
5
sovereignty-status.json
Normal file
5
sovereignty-status.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"score": 75,
|
||||
"label": "Stable",
|
||||
"assessment_type": "MANUAL"
|
||||
}
|
||||
Reference in New Issue
Block a user