Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
Adds a "Trust Panel" section inside the session panel (left sidebar) that is only visible when the authenticated user has elite trust tier. Users can paste an npub1... or hex pubkey and click "Vouch" to grant +20 trust points to a newcomer via POST /api/identity/vouch. - New vouch.js module handles tier check, pubkey decoding, event signing, and API call with proper error/success feedback - Exported signEvent() from nostr-identity.js for reusable Nostr signing - Panel hidden by default; shown only after /identity/me confirms elite tier Fixes #50 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
4.3 KiB
JavaScript
156 lines
4.3 KiB
JavaScript
import { initWorld, onWindowResize, disposeWorld } from './world.js';
|
|
import {
|
|
initAgents, updateAgents, getAgentCount,
|
|
disposeAgents, getAgentStates, applyAgentStates,
|
|
getTimmyGroup, applySlap, getCameraShakeStrength,
|
|
TIMMY_WORLD_POS,
|
|
} from './agents.js';
|
|
import { initEffects, updateEffects, disposeEffects } from './effects.js';
|
|
import { initUI, updateUI } from './ui.js';
|
|
import { initInteraction, disposeInteraction, registerSlapTarget } from './interaction.js';
|
|
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
|
|
import { initPaymentPanel } from './payment.js';
|
|
import { initSessionPanel } from './session.js';
|
|
import { initVouchPanel } from './vouch.js';
|
|
import { initNostrIdentity } from './nostr-identity.js';
|
|
import { warmup as warmupEdgeWorker, onReady as onEdgeWorkerReady } from './edge-worker-client.js';
|
|
import { setEdgeWorkerReady } from './ui.js';
|
|
import { initTimmyId } from './timmy-id.js';
|
|
import { AGENT_DEFS } from './agent-defs.js';
|
|
import { initNavigation, updateNavigation, disposeNavigation } from './navigation.js';
|
|
import { initHudLabels, updateHudLabels, disposeHudLabels } from './hud-labels.js';
|
|
|
|
let running = false;
|
|
let canvas = null;
|
|
let _lastTime = performance.now();
|
|
|
|
function buildWorld(firstInit, stateSnapshot) {
|
|
const { scene, camera, renderer } = initWorld(canvas);
|
|
canvas = renderer.domElement;
|
|
|
|
initEffects(scene);
|
|
initAgents(scene);
|
|
|
|
if (stateSnapshot) applyAgentStates(stateSnapshot);
|
|
|
|
// Navigation replaces OrbitControls
|
|
initNavigation(camera, renderer);
|
|
|
|
initInteraction(camera, renderer);
|
|
registerSlapTarget(getTimmyGroup(), applySlap);
|
|
|
|
// AR floating labels
|
|
initHudLabels(camera, AGENT_DEFS, TIMMY_WORLD_POS);
|
|
|
|
if (firstInit) {
|
|
initUI();
|
|
initWebSocket(scene);
|
|
initPaymentPanel();
|
|
initSessionPanel();
|
|
initVouchPanel();
|
|
void initNostrIdentity('/api');
|
|
warmupEdgeWorker();
|
|
onEdgeWorkerReady(() => setEdgeWorkerReady());
|
|
void initTimmyId();
|
|
}
|
|
|
|
const ac = new AbortController();
|
|
window.addEventListener('resize', () => onWindowResize(camera, renderer), { signal: ac.signal });
|
|
|
|
let frameCount = 0;
|
|
let lastFpsTime = performance.now();
|
|
let currentFps = 0;
|
|
|
|
running = true;
|
|
|
|
function animate() {
|
|
if (!running) return;
|
|
requestAnimationFrame(animate);
|
|
|
|
const now = performance.now();
|
|
const deltaMs = now - _lastTime;
|
|
_lastTime = now;
|
|
|
|
frameCount++;
|
|
if (now - lastFpsTime >= 1000) {
|
|
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
|
|
frameCount = 0;
|
|
lastFpsTime = now;
|
|
}
|
|
|
|
// FPS navigation
|
|
updateNavigation(deltaMs);
|
|
|
|
updateEffects(now);
|
|
updateAgents(now);
|
|
updateUI({
|
|
fps: currentFps,
|
|
agentCount: getAgentCount(),
|
|
jobCount: getJobCount(),
|
|
connectionState: getConnectionState(),
|
|
});
|
|
|
|
// Camera shake
|
|
const shakeStr = getCameraShakeStrength();
|
|
let sx = 0, sy = 0;
|
|
if (shakeStr > 0) {
|
|
const mag = shakeStr * 0.22;
|
|
sx = (Math.random() - 0.5) * mag;
|
|
sy = (Math.random() - 0.5) * mag * 0.45;
|
|
camera.position.x += sx;
|
|
camera.position.y += sy;
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
|
|
if (shakeStr > 0) {
|
|
camera.position.x -= sx;
|
|
camera.position.y -= sy;
|
|
}
|
|
|
|
// AR label positions (after render so NDC is current)
|
|
updateHudLabels(camera, renderer);
|
|
}
|
|
|
|
animate();
|
|
return { scene, renderer, ac };
|
|
}
|
|
|
|
function teardown({ scene, renderer, ac }) {
|
|
running = false;
|
|
ac.abort();
|
|
disposeNavigation();
|
|
disposeInteraction();
|
|
disposeHudLabels();
|
|
disposeEffects();
|
|
disposeAgents();
|
|
disposeWorld(renderer, scene);
|
|
}
|
|
|
|
function main() {
|
|
const $overlay = document.getElementById('webgl-recovery-overlay');
|
|
let handle = buildWorld(true, null);
|
|
|
|
canvas.addEventListener('webglcontextlost', event => {
|
|
event.preventDefault();
|
|
running = false;
|
|
if ($overlay) $overlay.style.display = 'flex';
|
|
});
|
|
|
|
canvas.addEventListener('webglcontextrestored', () => {
|
|
const snapshot = getAgentStates();
|
|
teardown(handle);
|
|
_lastTime = performance.now();
|
|
handle = buildWorld(false, snapshot);
|
|
if ($overlay) $overlay.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
main();
|
|
|
|
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register(import.meta.env.BASE_URL + 'sw.js').catch(() => {});
|
|
});
|
|
}
|