Compare commits
1 Commits
mimo/build
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc389539f0 |
124
app.js
124
app.js
@@ -5,7 +5,6 @@ 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
|
||||
@@ -708,7 +707,6 @@ async function init() {
|
||||
createAshStorm();
|
||||
SpatialMemory.init(scene);
|
||||
SessionRooms.init(scene, camera, null);
|
||||
EvenniaRoomPanel.init();
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
@@ -2076,7 +2074,6 @@ function connectHermes() {
|
||||
addChatMessage('system', 'Hermes link established.');
|
||||
updateWsHudStatus(true);
|
||||
refreshWorkshopPanel();
|
||||
EvenniaRoomPanel.setConnected(true);
|
||||
};
|
||||
|
||||
// Initialize MemPalace
|
||||
@@ -2105,7 +2102,6 @@ function connectHermes() {
|
||||
hermesWs = null;
|
||||
updateWsHudStatus(false);
|
||||
refreshWorkshopPanel();
|
||||
EvenniaRoomPanel.setConnected(false);
|
||||
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = setTimeout(connectHermes, 5000);
|
||||
};
|
||||
@@ -2116,16 +2112,6 @@ 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') {
|
||||
@@ -2530,24 +2516,46 @@ function populateAtlas() {
|
||||
const grid = document.getElementById('atlas-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Counters by status
|
||||
let onlineCount = 0;
|
||||
let standbyCount = 0;
|
||||
let rebuildingCount = 0;
|
||||
let localCount = 0;
|
||||
let blockedCount = 0;
|
||||
|
||||
// Group portals by environment
|
||||
const byEnv = { production: [], staging: [], local: [] };
|
||||
|
||||
portals.forEach(portal => {
|
||||
const config = portal.config;
|
||||
if (config.status === 'online') onlineCount++;
|
||||
if (config.status === 'standby') standbyCount++;
|
||||
const status = config.status || 'online';
|
||||
const env = config.environment || 'production';
|
||||
|
||||
// Count statuses
|
||||
if (status === 'online' || status === 'active') onlineCount++;
|
||||
else if (status === 'standby') standbyCount++;
|
||||
else if (status === 'rebuilding') rebuildingCount++;
|
||||
else if (status === 'local-only') localCount++;
|
||||
else if (status === 'blocked') blockedCount++;
|
||||
|
||||
// Group by environment
|
||||
if (byEnv[env]) {
|
||||
byEnv[env].push({ config, status });
|
||||
} else {
|
||||
byEnv['production'].push({ config, status });
|
||||
}
|
||||
|
||||
// Create atlas card
|
||||
const card = document.createElement('div');
|
||||
card.className = 'atlas-card';
|
||||
card.style.setProperty('--portal-color', config.color);
|
||||
|
||||
const statusClass = `status-${config.status || 'online'}`;
|
||||
const statusClass = `status-${status}`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="atlas-card-header">
|
||||
<div class="atlas-card-name">${config.name}</div>
|
||||
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
|
||||
<div class="atlas-card-status ${statusClass}">${status.toUpperCase()}</div>
|
||||
</div>
|
||||
<div class="atlas-card-desc">${config.description}</div>
|
||||
<div class="atlas-card-footer">
|
||||
@@ -2564,9 +2572,18 @@ function populateAtlas() {
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Update footer counts
|
||||
document.getElementById('atlas-online-count').textContent = onlineCount;
|
||||
document.getElementById('atlas-standby-count').textContent = standbyCount;
|
||||
|
||||
document.getElementById('atlas-rebuilding-count').textContent = rebuildingCount;
|
||||
document.getElementById('atlas-local-count').textContent = localCount;
|
||||
document.getElementById('atlas-blocked-count').textContent = blockedCount;
|
||||
|
||||
// Populate status wall by environment
|
||||
populateStatusWallEnv('production', byEnv.production);
|
||||
populateStatusWallEnv('staging', byEnv.staging);
|
||||
populateStatusWallEnv('local', byEnv.local);
|
||||
|
||||
// Update Bannerlord HUD status
|
||||
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
|
||||
if (bannerlord) {
|
||||
@@ -2575,6 +2592,75 @@ function populateAtlas() {
|
||||
}
|
||||
}
|
||||
|
||||
function populateStatusWallEnv(envName, portalList) {
|
||||
const container = document.getElementById(`${envName}-portals`);
|
||||
const summary = document.getElementById(`${envName}-summary`);
|
||||
container.innerHTML = '';
|
||||
|
||||
if (portalList.length === 0) {
|
||||
container.innerHTML = '<div class="status-portal-row"><span class="status-portal-name" style="font-style: italic; color: rgba(160,184,208,0.4);">No worlds</span></div>';
|
||||
summary.textContent = 'No portals in this environment';
|
||||
return;
|
||||
}
|
||||
|
||||
// Count statuses in this environment
|
||||
const statusCounts = {};
|
||||
portalList.forEach(({ config, status }) => {
|
||||
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
||||
});
|
||||
|
||||
// Create portal rows
|
||||
portalList.forEach(({ config, status }) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'status-portal-row';
|
||||
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = `status-portal-indicator status-dot ${status}`;
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'status-portal-name';
|
||||
nameSpan.textContent = config.name;
|
||||
|
||||
const statusSpan = document.createElement('span');
|
||||
statusSpan.style.fontSize = '9px';
|
||||
statusSpan.style.textTransform = 'uppercase';
|
||||
statusSpan.style.marginLeft = '8px';
|
||||
statusSpan.style.color = getStatusColor(status);
|
||||
statusSpan.textContent = status;
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
row.appendChild(statusSpan);
|
||||
row.appendChild(indicator);
|
||||
container.appendChild(row);
|
||||
});
|
||||
|
||||
// Create summary
|
||||
const summaryParts = Object.entries(statusCounts).map(([status, count]) =>
|
||||
`${count} ${status}`
|
||||
);
|
||||
summary.textContent = summaryParts.join(' · ');
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
case 'active':
|
||||
return 'var(--color-primary)';
|
||||
case 'standby':
|
||||
return 'var(--color-gold)';
|
||||
case 'rebuilding':
|
||||
return '#ffa500';
|
||||
case 'local-only':
|
||||
return '#00ff88';
|
||||
case 'blocked':
|
||||
return '#ff0000';
|
||||
case 'offline':
|
||||
return 'var(--color-danger)';
|
||||
default:
|
||||
return 'var(--color-text-muted)';
|
||||
}
|
||||
}
|
||||
|
||||
function focusPortal(portal) {
|
||||
// Teleport player to a position in front of the portal
|
||||
const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0));
|
||||
|
||||
59
index.html
59
index.html
@@ -125,19 +125,6 @@
|
||||
<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 -->
|
||||
@@ -277,11 +264,57 @@
|
||||
<div class="atlas-grid" id="atlas-grid">
|
||||
<!-- Portals will be injected here -->
|
||||
</div>
|
||||
<!-- Portal Status Wall -->
|
||||
<div class="atlas-status-wall">
|
||||
<div class="status-wall-header">
|
||||
<span class="status-wall-title">WORLD STATUS WALL</span>
|
||||
<span class="status-wall-subtitle">Real-time portal health</span>
|
||||
</div>
|
||||
<div class="status-wall-grid">
|
||||
<div class="status-wall-env" id="status-wall-production">
|
||||
<div class="status-env-header">
|
||||
<span class="status-env-dot production"></span>
|
||||
<span class="status-env-label">PRODUCTION</span>
|
||||
</div>
|
||||
<div class="status-env-portals" id="production-portals"></div>
|
||||
<div class="status-env-summary" id="production-summary"></div>
|
||||
</div>
|
||||
<div class="status-wall-env" id="status-wall-staging">
|
||||
<div class="status-env-header">
|
||||
<span class="status-env-dot staging"></span>
|
||||
<span class="status-env-label">STAGING</span>
|
||||
</div>
|
||||
<div class="status-env-portals" id="staging-portals"></div>
|
||||
<div class="status-env-summary" id="staging-summary"></div>
|
||||
</div>
|
||||
<div class="status-wall-env" id="status-wall-local">
|
||||
<div class="status-env-header">
|
||||
<span class="status-env-dot local"></span>
|
||||
<span class="status-env-label">LOCAL</span>
|
||||
</div>
|
||||
<div class="status-env-portals" id="local-portals"></div>
|
||||
<div class="status-env-summary" id="local-summary"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-wall-legend">
|
||||
<div class="legend-item"><span class="status-dot online"></span> Online</div>
|
||||
<div class="legend-item"><span class="status-dot rebuilding"></span> Rebuilding</div>
|
||||
<div class="legend-item"><span class="status-dot local-only"></span> Local-only</div>
|
||||
<div class="legend-item"><span class="status-dot blocked"></span> Blocked</div>
|
||||
<div class="legend-item"><span class="status-dot offline"></span> Offline</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atlas-footer">
|
||||
<div class="atlas-status-summary">
|
||||
<span class="status-indicator online"></span> <span id="atlas-online-count">0</span> ONLINE
|
||||
|
||||
<span class="status-indicator standby"></span> <span id="atlas-standby-count">0</span> STANDBY
|
||||
|
||||
<span class="status-indicator rebuilding"></span> <span id="atlas-rebuilding-count">0</span> REBUILDING
|
||||
|
||||
<span class="status-indicator local-only"></span> <span id="atlas-local-count">0</span> LOCAL
|
||||
|
||||
<span class="status-indicator blocked"></span> <span id="atlas-blocked-count">0</span> BLOCKED
|
||||
</div>
|
||||
<div class="atlas-hint">Click a portal to focus or teleport</div>
|
||||
</div>
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 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…</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 };
|
||||
|
||||
})();
|
||||
25
portals.json
25
portals.json
@@ -4,6 +4,7 @@
|
||||
"name": "Morrowind",
|
||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#ff6600",
|
||||
"position": { "x": 15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": -0.5 },
|
||||
@@ -17,13 +18,13 @@
|
||||
"id": "bannerlord",
|
||||
"name": "Bannerlord",
|
||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||
"status": "active",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": 0.5 },
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "production",
|
||||
"access_mode": "operator",
|
||||
"readiness_state": "active",
|
||||
"telemetry_source": "hermes-harness:bannerlord",
|
||||
@@ -42,6 +43,7 @@
|
||||
"name": "Workshop",
|
||||
"description": "The creative harness. Build, script, and manifest.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 0, "y": 0, "z": -20 },
|
||||
"rotation": { "y": 0 },
|
||||
@@ -56,6 +58,7 @@
|
||||
"name": "Archive",
|
||||
"description": "The repository of all knowledge. History, logs, and ancient data.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#0066ff",
|
||||
"position": { "x": 25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": -1.57 },
|
||||
@@ -70,6 +73,7 @@
|
||||
"name": "Chapel",
|
||||
"description": "A sanctuary for reflection and digital peace.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": 1.57 },
|
||||
@@ -84,6 +88,7 @@
|
||||
"name": "Courtyard",
|
||||
"description": "The open nexus. A place for agents to gather and connect.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": -2.5 },
|
||||
@@ -98,6 +103,7 @@
|
||||
"name": "Gate",
|
||||
"description": "The transition point. Entry and exit from the Nexus core.",
|
||||
"status": "standby",
|
||||
"environment": "staging",
|
||||
"color": "#ff4466",
|
||||
"position": { "x": -15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": 2.5 },
|
||||
@@ -106,5 +112,20 @@
|
||||
"type": "harness",
|
||||
"params": { "mode": "transit" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dev",
|
||||
"name": "Dev Sandbox",
|
||||
"description": "Local development world. Unstable, experimental, honest.",
|
||||
"status": "local-only",
|
||||
"environment": "local",
|
||||
"color": "#00ff88",
|
||||
"position": { "x": 0, "y": 0, "z": 20 },
|
||||
"rotation": { "y": 0 },
|
||||
"destination": {
|
||||
"url": "http://localhost:3000",
|
||||
"type": "local",
|
||||
"params": { "mode": "dev" }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
395
style.css
395
style.css
@@ -365,7 +365,11 @@ canvas#nexus-canvas {
|
||||
}
|
||||
|
||||
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
|
||||
.status-active { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
|
||||
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
|
||||
.status-rebuilding { background: rgba(255, 165, 0, 0.2); color: #ffa500; border: 1px solid #ffa500; }
|
||||
.status-local-only { background: rgba(0, 255, 136, 0.2); color: #00ff88; border: 1px solid #00ff88; }
|
||||
.status-blocked { background: rgba(255, 0, 0, 0.2); color: #ff0000; border: 1px solid #ff0000; }
|
||||
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
|
||||
|
||||
.atlas-card-desc {
|
||||
@@ -410,6 +414,165 @@ canvas#nexus-canvas {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Portal Status Wall */
|
||||
.atlas-status-wall {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: rgba(10, 15, 40, 0.5);
|
||||
}
|
||||
|
||||
.status-wall-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-wall-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-wall-subtitle {
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.status-wall-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-wall-env {
|
||||
background: rgba(20, 30, 60, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-env-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.status-env-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-env-dot.production {
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 5px var(--color-primary);
|
||||
}
|
||||
|
||||
.status-env-dot.staging {
|
||||
background: var(--color-gold);
|
||||
box-shadow: 0 0 5px var(--color-gold);
|
||||
}
|
||||
|
||||
.status-env-dot.local {
|
||||
background: #00ff88;
|
||||
box-shadow: 0 0 5px #00ff88;
|
||||
}
|
||||
|
||||
.status-env-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-env-portals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status-portal-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.status-portal-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-portal-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-env-summary {
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.status-wall-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.online { background: var(--color-primary); box-shadow: 0 0 4px var(--color-primary); }
|
||||
.status-dot.active { background: var(--color-primary); box-shadow: 0 0 4px var(--color-primary); }
|
||||
.status-dot.standby { background: var(--color-gold); box-shadow: 0 0 4px var(--color-gold); }
|
||||
.status-dot.rebuilding { background: #ffa500; box-shadow: 0 0 4px #ffa500; }
|
||||
.status-dot.local-only { background: #00ff88; box-shadow: 0 0 4px #00ff88; }
|
||||
.status-dot.blocked { background: #ff0000; box-shadow: 0 0 4px #ff0000; }
|
||||
.status-dot.offline { background: var(--color-danger); box-shadow: 0 0 4px var(--color-danger); }
|
||||
|
||||
/* Additional status indicators for footer */
|
||||
.status-indicator.rebuilding { background: #ffa500; box-shadow: 0 0 5px #ffa500; }
|
||||
.status-indicator.local-only { background: #00ff88; box-shadow: 0 0 5px #00ff88; }
|
||||
.status-indicator.blocked { background: #ff0000; box-shadow: 0 0 5px #ff0000; }
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
@@ -423,6 +586,12 @@ canvas#nexus-canvas {
|
||||
.atlas-content {
|
||||
max-height: 90vh;
|
||||
}
|
||||
.status-wall-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.atlas-status-wall {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Debug overlay */
|
||||
@@ -1580,229 +1749,3 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user