Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
89907acf6a fix: [UI] Add first Nexus operator panel for Evennia room snapshot (closes #728)
- New component: nexus/components/evennia-room-panel.js (IIFE module)
- Renders room title + description, exits, objects, occupants
- Handles three honest states: offline, awaiting, in-room
- Listens for evennia.room_snapshot and evennia.actor_located WS events
- Integrates into handleHermesMessage dispatch + WS connect/disconnect lifecycle
- Styled to match existing Nexus HUD design system (JetBrains Mono, primary/secondary colors)
- aria-live='polite' for accessibility
- Syntax validated: node --check passed on all JS files
2026-04-10 20:11:32 -04:00
5 changed files with 573 additions and 128 deletions

View File

@@ -1,169 +1,132 @@
# Legacy Matrix Audit — Migration Table
# Legacy Matrix Audit
Purpose:
Preserve quality work from `/Users/apayne/the-matrix` before the Nexus browser shell is rebuilt.
Preserve useful work from `/Users/apayne/the-matrix` before the Nexus browser shell is rebuilt.
Canonical rule:
- `Timmy_Foundation/the-nexus` is the only canonical 3D repo.
- `/Users/apayne/the-matrix` is legacy source material, not a parallel product.
- This document is the authoritative migration table for issue #685.
## Verified Legacy State
## Verified Legacy Matrix State
Local legacy repo: `/Users/apayne/the-matrix`
Local legacy repo:
- `/Users/apayne/the-matrix`
Observed facts:
- Vite browser app, vanilla JS + Three.js 0.171.0
- 24 JS modules under `js/`
- Smoke suite: 87 passed, 0 failed
- Package scripts: dev, build, preview, test
- PWA manifest + service worker
- Vite config with code-splitting (Three.js in separate chunk)
- Quality-tier system for hardware detection
- WebSocket client with reconnection, heartbeat, mock mode
- Full avatar FPS movement + PiP camera
- Sub-world portal system with zone triggers
- Vite browser app exists
- `npm test` passes with `87 passed, 0 failed`
- 23 JS modules under `js/`
- package scripts include `dev`, `build`, `preview`, and `test`
## Migration Table
## Known historical Nexus snapshot
Decision key:
- **CARRY** = transplant concepts and patterns into Nexus vNext
- **ARCHIVE** = keep as reference, do not directly transplant
- **DROP** = do not preserve unless re-justified
Useful in-repo reference point:
- `0518a1c3ae3c1d0afeb24dea9772102f5a3d9a66`
### Core Modules
That snapshot still contains browser-world root files such as:
- `index.html`
- `app.js`
- `style.css`
- `package.json`
- `tests/`
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/main.js` | 180 | App bootstrap, render loop, WebGL context recovery | **CARRY** | Architectural pattern. Shows clean init/teardown lifecycle, context-loss recovery, visibility pause. Nexus needs this loop but should not copy the monolithic wiring. |
| `js/world.js` | 95 | Scene, camera, renderer, grid, lights | **CARRY** | Foundational. Quality-tier-aware renderer setup, grid floor, lighting. Nexus already has a world but should adopt the tier-aware antialiasing and pixel-ratio capping. |
| `js/config.js` | 68 | Connection config via URL params + env vars | **ARCHIVE** | Pattern reference only. Nexus config should route through Hermes harness, not Vite env vars. The URL-override pattern (ws, token, mock) is worth remembering. |
| `js/quality.js` | 90 | Hardware detection, quality tier (low/medium/high) | **CARRY** | Directly useful. DPR capping, core/memory/screen heuristics, WebGL renderer sniffing. Nexus needs this for graceful degradation on Mac/iPad. |
| `js/storage.js` | 39 | Safe localStorage with in-memory fallback | **CARRY** | Small, robust, sandbox-proof. Nexus should use this or equivalent. Prevents crashes in sandboxed iframes. |
## Rescue Candidates
### Agent System
### Carry forward into Nexus vNext
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/agent-defs.js` | 30 | Agent identity data (id, label, color, role, position) | **CARRY** | Seed data model. Nexus agents should be defined similarly — data-driven, not hardcoded in render logic. Color hex helper is trivial but useful. |
| `js/agents.js` | 523 | Agent 3D objects, movement, state, connection lines, hot-add/remove | **CARRY** | Core visual system. Shared geometries (perf), movement interpolation, wallet-health stress glow, auto-placement algorithm, connection-line pulse. All valuable. Needs integration with real agent state from Hermes. |
| `js/behaviors.js` | 413 | Autonomous agent behavior state machine | **ARCHIVE** | Pattern reference. The personality-weighted behavior selection, conversation pairing, and artifact-placement system are well-designed. But Nexus behaviors should be driven by Hermes, not a client-side simulation. Keep the architecture, drop the fake-autonomy. |
| `js/presence.js` | 139 | Agent presence HUD (online/offline, uptime, state) | **CARRY** | Valuable UX. Live "who's here" panel with uptime tickers and state indicators. Needs real backend state, not mock assumptions. |
1. `agent-defs.js`
- agent identity definitions
- useful as seed data/model for visible entities in the world
### Visitor & Interaction
2. `agents.js`
- agent objects, state machine, connection lines
- useful for visualizing Timmy / subagents / system processes in a world-native way
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/visitor.js` | 141 | Visitor enter/leave protocol, chat input | **CARRY** | Session lifecycle. Device detection, visibility-based leave/return, chat input wiring. Directly applicable to Nexus visitor tracking. |
| `js/avatar.js` | 360 | FPS movement, PiP dual-camera, touch input | **CARRY** | Visitor embodiment. WASD + arrow movement, first/third person swap, PiP canvas, touch joystick, right-click mouse-look. Strong work. Needs tuning for Nexus world bounds. |
| `js/interaction.js` | 296 | Raycasting, click-to-select agents, info popup | **CARRY** | Essential for any browser world. OrbitControls, pointer/tap detection, agent popup with state/role, TALK button. The popup-anchoring-to-3D-position logic is particularly well done. |
| `js/zones.js` | 161 | Proximity trigger zones (portal enter/exit, events) | **CARRY** | Spatial event system. Portal traversal, event triggers, once-only zones. Nexus portals (#672) need this exact pattern. |
3. `avatar.js`
- visitor embodiment, movement, camera handling
- strongly aligned with "training ground" and "walk the world" goals
### Chat & Communication
4. `ui.js`
- HUD, chat surfaces, overlays
- useful if rebuilt against real harness data instead of stale fake state
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/bark.js` | 141 | Speech bubble system with typing animation | **CARRY** | Timmy's voice in-world. Typing animation, queue, auto-dismiss, emotion tags, demo bark lines. Strong expressive presence. The demo lines ("The Tower watches. The Tower remembers.") are good seed content. |
| `js/ui.js` | 285 | Chat panel, agent list, HUD, streaming tokens | **CARRY** | Chat infrastructure. Rolling chat buffer, per-agent localStorage history, streaming token display with cursor animation, HTML escaping. Needs reconnection to Hermes chat instead of WS mock. |
| `js/transcript.js` | 183 | Conversation transcript logger, export | **ARCHIVE** | Pattern reference. The rolling buffer, structured JSON entries, TXT/JSON download, HUD badge are all solid. But transcript authority should live in Hermes, not browser localStorage. Keep the UX pattern, rebuild storage layer. |
5. `websocket.js`
- browser-side live bridge patterns
- useful if retethered to Hermes-facing transport
### Visual Effects
6. `transcript.js`
- local transcript capture pattern
- useful if durable truth still routes through Hermes and browser cache remains secondary
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/effects.js` | 195 | Matrix rain particles + starfield | **CARRY** | Atmospheric foundation. Quality-tier particle counts, frame-skip optimization, adaptive draw-range (FPS-budget recovery), bounding-sphere pre-compute. This is production-grade particle work. |
| `js/ambient.js` | 212 | Mood-driven atmosphere (lighting, fog, rain, stars) | **CARRY** | Scene mood engine. Smooth eased transitions between mood states (calm, focused, excited, contemplative, stressed), per-mood lighting/fog/rain/star parameters. Directly supports Nexus atmosphere. |
| `js/satflow.js` | 261 | Lightning payment particle flow | **CARRY** | Economy visualization. Bezier-arc particles, staggered travel, burst-on-arrival, pooling. If Nexus shows any payment/economy flow, this is the pattern. |
7. `ambient.js`
- mood / atmosphere system
- directly supports wizardly presentation without changing system authority
### Economy & Scene
8. `satflow.js`
- visual economy / payment flow motifs
- useful if Timmy's economy/agent interactions become a real visible layer
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/economy.js` | 100 | Wallet/treasury HUD panel | **ARCHIVE** | UI pattern reference. Clean sats formatting, per-agent balance rows, health-colored dots, recent transactions. Worth rebuilding when backed by real sovereign metrics. |
| `js/scene-objects.js` | 718 | Dynamic 3D object registry, portals, sub-worlds | **CARRY** | Critical. Geometry/material factories, animation system (rotate/bob/pulse/orbit), portal visual (torus ring + glow disc + zone), sub-world load/unload, text sprites, compound groups. This is the most complex and valuable module. Nexus portals (#672) should build on this. |
9. `economy.js`
- treasury / wallet panel ideas
- useful if later backed by real sovereign metrics
### Backend Bridge
10. `presence.js`
- who-is-here / online-state UI
- useful for showing human + agent + process presence in the world
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `js/websocket.js` | 598 | WebSocket client, message dispatcher, mock mode | **ARCHIVE** | Pattern reference only. Reconnection with exponential backoff, heartbeat/zombie detection, rich message dispatch (40+ message types), streaming chat support. The architecture is sound but must be reconnected to Hermes transport, not copied wholesale. The message-type catalog is the most valuable reference artifact. |
| `js/demo.js` | ~300 | Demo autopilot (mock mode simulation) | **DROP** | Fake activity simulation. Deliberately creates the illusion of live data. Do not preserve. If Nexus needs a demo mode, build a clearly-labeled one that doesn't pretend to be real. |
11. `interaction.js`
- clicking, inspecting, selecting world entities
- likely needed in any real browser-facing Nexus shell
### Testing & Build
12. `quality.js`
- hardware-aware quality tiering
- useful for local-first graceful degradation on Mac hardware
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `test/smoke.mjs` | 235 | Automated browser smoke test suite | **CARRY** | Testing discipline. Module inventory check, export verification, HTML structure validation, Vite build test, bundle-size budget, PWA manifest check. Nexus should adopt this pattern (adapted for its own module structure). |
| `vite.config.js` | 53 | Build config with code splitting, SW generation | **ARCHIVE** | Build tooling reference. manualChunks for Three.js, SW precache generation plugin. Relevant if Nexus re-commits to Vite. |
| `sw.js` | ~40 | Service worker with precache | **ARCHIVE** | PWA reference. Relevant only if Nexus pursues offline-first PWA. |
| `manifest.json` | ~20 | PWA manifest | **ARCHIVE** | PWA reference. |
13. `bark.js`
- prominent speech / bark system
- strong fit for Timmy's expressive presence in-world
### Server-Side (Python)
14. `world.js`, `effects.js`, `scene-objects.js`, `zones.js`
- broad visual foundation work
- should be mined for patterns, not blindly transplanted
| File | Lines | Capability | Decision | Why for Nexus |
|------|-------|------------|----------|---------------|
| `server/bridge.py` | ~900 | WebSocket bridge server | **ARCHIVE** | Reference. Hermes replaces this role. Keep for protocol schema reference. |
| `server/gateway.py` | ~400 | HTTP gateway | **ARCHIVE** | Reference. |
| `server/ollama_client.py` | ~280 | Ollama integration | **ARCHIVE** | Reference. Relevant if Nexus needs local model calls. |
| `server/research.py` | ~450 | Research pipeline | **ARCHIVE** | Reference. |
| `server/webhooks.py` | ~350 | Webhook handler | **ARCHIVE** | Reference. |
| `server/test_*.py` | ~5 files | Server test suites | **ARCHIVE** | Testing patterns worth studying. |
15. `test/smoke.mjs`
- browser smoke discipline
- should inform rebuilt validation in canonical Nexus repo
## Summary by Decision
### Archive as reference, not direct carry-forward
### CARRY FORWARD (17 modules)
These modules contain patterns, algorithms, or entire implementations that should move into the Nexus browser shell:
- demo/autopilot assumptions that pretend fake backend activity is real
- any websocket schema that no longer matches Hermes truth
- Vite-specific plumbing that is only useful if we consciously recommit to Vite
- `quality.js` — hardware detection
- `storage.js` — safe persistence
- `world.js` — scene foundation
- `agent-defs.js` — agent data model
- `agents.js` — agent visualization + movement
- `presence.js` — online presence HUD
- `visitor.js` — session lifecycle
- `avatar.js` — FPS embodiment
- `interaction.js` — click/select/raycast
- `zones.js` — spatial triggers
- `bark.js` — speech bubbles
- `ui.js` — chat/HUD
- `effects.js` — particle effects
- `ambient.js` — mood atmosphere
- `satflow.js` — payment flow particles
- `scene-objects.js` — dynamic objects + portals
- `test/smoke.mjs` — smoke test discipline
### Deliberately drop unless re-justified
### ARCHIVE AS REFERENCE (9 modules/files)
Keep for patterns, protocol schemas, and architectural reference. Do not directly transplant:
- `config.js` — config pattern (use Hermes instead)
- `behaviors.js` — behavior architecture (use Hermes-driven state)
- `transcript.js` — transcript UX (use Hermes storage)
- `economy.js` — economy UI pattern (use real metrics)
- `websocket.js` — message protocol catalog + reconnection patterns
- `vite.config.js` — build tooling
- `sw.js`, `manifest.json` — PWA reference
- `server/*.py` — server protocol schemas
### DELIBERATELY DROP (2)
Do not preserve unless re-justified:
- `demo.js` — fake activity simulation; creates false impression of live system
- `main.js` monolithic wiring — the init pattern carries, the specific module wiring does not
- anything that presents mock data as if it were live
- anything that duplicates a better Hermes-native telemetry path
- anything that turns the browser into the system of record
## Concern Separation for Nexus vNext
When rebuilding inside `the-nexus`, keep these concerns in separate modules:
When rebuilding inside `the-nexus`, keep concerns separated:
1. **World shell** — scene, camera, renderer, grid, lights, fog
2. **Effects layer** — rain, stars, ambient mood transitions
3. **Agent visualization** — 3D objects, labels, connection lines, movement
4. **Visitor embodiment** — avatar, FPS controls, PiP camera
5. **Interaction layer** — raycasting, selection, zones, portal traversal
6. **Communication surface** — bark, chat panel, streaming tokens
7. **Presence & HUD** — who's-online, economy panel, transcript controls
8. **Harness bridge** — WebSocket/API transport to Hermes (NOT a copy of websocket.js)
9. **Quality & config** — hardware detection, runtime configuration
10. **Smoke tests** — automated validation
1. World shell / rendering
- scene, camera, movement, atmosphere
2. Presence and embodiment
- avatar, agent placement, selection, bark/chat surfaces
3. Harness bridge
- websocket / API bridge from Hermes truth into browser state
4. Visualization panels
- metrics, presence, economy, portal states, transcripts
5. Validation
- smoke tests, screenshot proof, provenance checks
6. Game portal layer
- Morrowind / portal-specific interaction surfaces
Do not collapse all of this into one giant app file again.
Do not let visual shell code become telemetry authority.

14
app.js
View File

@@ -5,6 +5,7 @@ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
import { SessionRooms } from './nexus/components/session-rooms.js';
import { EvenniaRoomPanel } from './nexus/components/evennia-room-panel.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -707,6 +708,7 @@ async function init() {
createAshStorm();
SpatialMemory.init(scene);
SessionRooms.init(scene, camera, null);
EvenniaRoomPanel.init();
updateLoad(90);
loadSession();
@@ -2074,6 +2076,7 @@ function connectHermes() {
addChatMessage('system', 'Hermes link established.');
updateWsHudStatus(true);
refreshWorkshopPanel();
EvenniaRoomPanel.setConnected(true);
};
// Initialize MemPalace
@@ -2102,6 +2105,7 @@ function connectHermes() {
hermesWs = null;
updateWsHudStatus(false);
refreshWorkshopPanel();
EvenniaRoomPanel.setConnected(false);
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
wsReconnectTimer = setTimeout(connectHermes, 5000);
};
@@ -2112,6 +2116,16 @@ function connectHermes() {
}
function handleHermesMessage(data) {
// ── Evennia room snapshot events (#728) ──
if (data.type === 'evennia.room_snapshot') {
EvenniaRoomPanel.onRoomSnapshot(data);
return;
}
if (data.type === 'evennia.actor_located') {
EvenniaRoomPanel.onActorLocated(data);
return;
}
if (data.type === 'chat') {
addChatMessage(data.agent || 'timmy', data.text);
} else if (data.type === 'tool_call') {

View File

@@ -125,6 +125,19 @@
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
<div id="agent-log-content" class="agent-log-content"></div>
</div>
<!-- Evennia Room Snapshot Operator Panel (#728) -->
<div id="evennia-room-panel" class="evennia-room-panel" aria-live="polite">
<div class="erp-header">
<span class="erp-icon"></span>
<span class="erp-title">EVENNIA ROOM</span>
<span class="erp-status-dot erp-offline"></span>
</div>
<div class="erp-body erp-empty-state">
<div class="erp-empty-icon"></div>
<div class="erp-empty-label">DISCONNECTED</div>
<div class="erp-empty-hint">No link to Evennia world.</div>
</div>
</div>
</div>
<!-- Bottom: Chat Interface -->

View File

@@ -0,0 +1,229 @@
// ═══════════════════════════════════════════════════════
// EVENNIA ROOM SNAPSHOT OPERATOR PANEL (Issue #728)
// ═══════════════════════════════════════════════════════
//
// Renders the current Evennia room state in the Nexus HUD.
// Consumes evennia.room_snapshot and evennia.actor_located
// events from the Hermes WebSocket bridge.
//
// States:
// offline — no WS connection
// awaiting — connected but no room data yet
// in-room — room snapshot loaded, render full panel
//
// Usage from app.js:
// EvenniaRoomPanel.init();
// EvenniaRoomPanel.onRoomSnapshot(data);
// EvenniaRoomPanel.onActorLocated(data);
// EvenniaRoomPanel.setConnected(bool);
// ═══════════════════════════════════════════════════════
export const EvenniaRoomPanel = (() => {
// ─── STATE ────────────────────────────────────────────
let _connected = false;
let _roomData = null; // latest evennia.room_snapshot payload
let _actorRoomId = null; // from evennia.actor_located
let _lastUpdate = null; // timestamp of last snapshot
let _panelEl = null; // DOM root
let _init = false;
// ─── DOM REFS ─────────────────────────────────────────
function _el(id) { return document.getElementById(id); }
// ─── INIT ─────────────────────────────────────────────
function init() {
_panelEl = _el('evennia-room-panel');
if (!_panelEl) {
console.warn('[EvenniaRoomPanel] Panel element not found in DOM.');
return;
}
_init = true;
_render();
console.log('[EvenniaRoomPanel] Initialized.');
}
// ─── EVENT HANDLERS ───────────────────────────────────
function onRoomSnapshot(data) {
_roomData = data;
_lastUpdate = data.timestamp || new Date().toISOString();
_actorRoomId = data.room_id || data.room_key || null;
_render();
}
function onActorLocated(data) {
_actorRoomId = data.room_id || data.room_key || null;
// If we get a location but no snapshot yet, show awaiting
if (!_roomData || (_roomData.room_id !== _actorRoomId && _roomData.room_key !== _actorRoomId)) {
_render();
}
}
function setConnected(connected) {
_connected = connected;
if (!connected) {
// Clear room data on disconnect — stale data is lying
_roomData = null;
_actorRoomId = null;
_lastUpdate = null;
}
_render();
}
// ─── RENDER ───────────────────────────────────────────
function _render() {
if (!_panelEl) return;
if (!_connected) {
_renderOffline();
} else if (!_roomData) {
_renderAwaiting();
} else {
_renderRoom();
}
}
function _renderOffline() {
_panelEl.innerHTML = `
<div class="erp-header">
<span class="erp-icon">◈</span>
<span class="erp-title">EVENNIA ROOM</span>
<span class="erp-status-dot erp-offline"></span>
</div>
<div class="erp-body erp-empty-state">
<div class="erp-empty-icon">⊘</div>
<div class="erp-empty-label">DISCONNECTED</div>
<div class="erp-empty-hint">No link to Evennia world.</div>
</div>
`;
_panelEl.classList.add('erp-state-offline');
_panelEl.classList.remove('erp-state-awaiting', 'erp-state-inroom');
}
function _renderAwaiting() {
_panelEl.innerHTML = `
<div class="erp-header">
<span class="erp-icon">◈</span>
<span class="erp-title">EVENNIA ROOM</span>
<span class="erp-status-dot erp-online"></span>
</div>
<div class="erp-body erp-empty-state">
<div class="erp-empty-icon erp-pulse">◎</div>
<div class="erp-empty-label">AWAITING SNAPSHOT</div>
<div class="erp-empty-hint">Connected. Waiting for room data&hellip;</div>
</div>
`;
_panelEl.classList.add('erp-state-awaiting');
_panelEl.classList.remove('erp-state-offline', 'erp-state-inroom');
}
function _renderRoom() {
const room = _roomData;
const title = _esc(room.title || room.room_name || room.room_key || 'Unknown Room');
const desc = _esc(room.desc || 'No description available.');
const exits = Array.isArray(room.exits) ? room.exits : [];
const objects = Array.isArray(room.objects) ? room.objects : [];
const occupants = Array.isArray(room.occupants) ? room.occupants : [];
const roomId = _esc(room.room_id || room.room_key || '—');
const timeStr = _formatTime(_lastUpdate);
// Build exits list
let exitsHtml = '';
if (exits.length > 0) {
exitsHtml = exits.map(e => {
const name = _esc(e.key || e.name || '?');
const dest = _esc(e.destination_name || e.destination_id || e.destination_key || '');
return `<div class="erp-exit-row">
<span class="erp-exit-arrow">→</span>
<span class="erp-exit-name">${name}</span>
${dest ? `<span class="erp-exit-dest">${dest}</span>` : ''}
</div>`;
}).join('');
} else {
exitsHtml = '<div class="erp-none">No visible exits.</div>';
}
// Build objects list
let objectsHtml = '';
if (objects.length > 0) {
objectsHtml = objects.map(o => {
const name = _esc(o.key || o.id || '?');
const desc = _esc(o.short_desc || '');
return `<div class="erp-object-row">
<span class="erp-object-icon">▪</span>
<span class="erp-object-name">${name}</span>
${desc ? `<span class="erp-object-desc">${desc}</span>` : ''}
</div>`;
}).join('');
} else {
objectsHtml = '<div class="erp-none">No visible objects.</div>';
}
// Build occupants list
let occupantsHtml = '';
if (occupants.length > 0) {
occupantsHtml = occupants.map(o => {
const name = _esc(typeof o === 'string' ? o : (o.name || o.key || '?'));
return `<span class="erp-occupant">${name}</span>`;
}).join('');
}
_panelEl.innerHTML = `
<div class="erp-header">
<span class="erp-icon">◈</span>
<span class="erp-title">${title}</span>
<span class="erp-status-dot erp-online"></span>
</div>
<div class="erp-body">
<div class="erp-room-id">${roomId}</div>
<div class="erp-desc">${desc}</div>
<div class="erp-section">
<div class="erp-section-label">EXITS</div>
<div class="erp-exits">${exitsHtml}</div>
</div>
<div class="erp-section">
<div class="erp-section-label">OBJECTS</div>
<div class="erp-objects">${objectsHtml}</div>
</div>
${occupantsHtml ? `
<div class="erp-section">
<div class="erp-section-label">OCCUPANTS</div>
<div class="erp-occupants">${occupantsHtml}</div>
</div>` : ''}
<div class="erp-footer">
<span class="erp-time">${timeStr}</span>
</div>
</div>
`;
_panelEl.classList.add('erp-state-inroom');
_panelEl.classList.remove('erp-state-offline', 'erp-state-awaiting');
}
// ─── UTILS ────────────────────────────────────────────
function _esc(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function _formatTime(isoStr) {
if (!isoStr) return '—';
try {
const d = new Date(isoStr);
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
} catch {
return '—';
}
}
// ─── PUBLIC API ───────────────────────────────────────
return { init, onRoomSnapshot, onActorLocated, setConnected };
})();

226
style.css
View File

@@ -1580,3 +1580,229 @@ canvas#nexus-canvas {
text-transform: uppercase;
}
/* ═══════════════════════════════════════════════════════
EVENNIA ROOM SNAPSHOT OPERATOR PANEL (#728)
═══════════════════════════════════════════════════════ */
.evennia-room-panel {
width: 280px;
background: rgba(5, 5, 16, 0.85);
backdrop-filter: blur(12px);
border: 1px solid rgba(74, 240, 192, 0.18);
border-left: 3px solid var(--color-primary, #4af0c0);
border-radius: 6px;
font-family: var(--font-body, 'JetBrains Mono', monospace);
font-size: 11px;
color: var(--color-text, #e0f0ff);
pointer-events: auto;
overflow: hidden;
transition: border-color 0.3s ease;
}
.evennia-room-panel.erp-state-offline {
border-left-color: var(--color-danger, #ff4466);
}
.evennia-room-panel.erp-state-awaiting {
border-left-color: var(--color-warning, #ffaa22);
}
.evennia-room-panel.erp-state-inroom {
border-left-color: var(--color-primary, #4af0c0);
}
.erp-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
background: rgba(74, 240, 192, 0.04);
}
.erp-icon {
font-size: 12px;
color: var(--color-primary, #4af0c0);
}
.erp-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 1.2px;
color: var(--color-primary, #4af0c0);
flex: 1;
}
.erp-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.erp-status-dot.erp-offline {
background: var(--color-danger, #ff4466);
box-shadow: 0 0 6px var(--color-danger, #ff4466);
}
.erp-status-dot.erp-online {
background: var(--color-primary, #4af0c0);
box-shadow: 0 0 6px var(--color-primary, #4af0c0);
}
.erp-body {
padding: 8px 10px;
max-height: 320px;
overflow-y: auto;
}
/* Empty / offline states */
.erp-empty-state {
text-align: center;
padding: 18px 10px;
}
.erp-empty-icon {
font-size: 22px;
color: rgba(74, 240, 192, 0.25);
margin-bottom: 6px;
}
.erp-empty-icon.erp-pulse {
animation: erpPulse 2s ease-in-out infinite;
}
@keyframes erpPulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.9; }
}
.erp-empty-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--color-text-muted, #8a9ab8);
margin-bottom: 4px;
}
.erp-empty-hint {
font-size: 10px;
color: rgba(138, 154, 184, 0.55);
}
/* Room content */
.erp-room-id {
font-size: 9px;
color: rgba(138, 154, 184, 0.4);
letter-spacing: 0.8px;
text-transform: uppercase;
margin-bottom: 6px;
}
.erp-desc {
font-size: 11px;
color: rgba(224, 240, 255, 0.8);
line-height: 1.45;
margin-bottom: 10px;
border-left: 2px solid rgba(74, 240, 192, 0.15);
padding-left: 8px;
}
.erp-section {
margin-bottom: 8px;
}
.erp-section-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--color-primary, #4af0c0);
margin-bottom: 4px;
opacity: 0.7;
}
.erp-none {
font-size: 10px;
color: rgba(138, 154, 184, 0.35);
font-style: italic;
padding: 2px 0;
}
/* Exits */
.erp-exit-row {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
font-size: 11px;
}
.erp-exit-arrow {
color: var(--color-secondary, #7b5cff);
font-weight: 700;
}
.erp-exit-name {
color: var(--color-secondary, #7b5cff);
font-weight: 600;
}
.erp-exit-dest {
color: rgba(138, 154, 184, 0.45);
font-size: 10px;
}
/* Objects */
.erp-object-row {
display: flex;
align-items: baseline;
gap: 5px;
padding: 2px 0;
font-size: 11px;
}
.erp-object-icon {
color: var(--color-gold, #ffd700);
font-size: 8px;
}
.erp-object-name {
color: rgba(224, 240, 255, 0.75);
font-weight: 500;
}
.erp-object-desc {
color: rgba(138, 154, 184, 0.45);
font-size: 10px;
}
/* Occupants */
.erp-occupants {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.erp-occupant {
font-size: 10px;
padding: 2px 6px;
background: rgba(74, 240, 192, 0.08);
border: 1px solid rgba(74, 240, 192, 0.15);
border-radius: 3px;
color: var(--color-primary, #4af0c0);
}
/* Footer */
.erp-footer {
margin-top: 6px;
padding-top: 4px;
border-top: 1px solid rgba(74, 240, 192, 0.06);
text-align: right;
}
.erp-time {
font-size: 9px;
color: rgba(138, 154, 184, 0.3);
letter-spacing: 0.5px;
}