Compare commits
9 Commits
mimo/build
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
229edf16e2 | ||
| aab3e607eb | |||
| fe56ece1ad | |||
| bf477382ba | |||
| fba972f8be | |||
| 6786e65f3d | |||
| 62a6581827 | |||
| 797f32a7fe | |||
| 80eb4ff7ea |
137
app.js
137
app.js
@@ -1,4 +1,4 @@
|
||||
import * as THREE from 'three';
|
||||
import ResonanceVisualizer from './nexus/components/resonance-visualizer.js';\nimport * as THREE from 'three';
|
||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||
@@ -58,11 +58,6 @@ let performanceTier = 'high';
|
||||
let hermesWs = null;
|
||||
let wsReconnectTimer = null;
|
||||
let wsConnected = false;
|
||||
// ═══ EVENNIA ROOM STATE ═══
|
||||
let evenniaRoom = null; // {title, desc, exits[], objects[], occupants[], timestamp, roomKey}
|
||||
let evenniaConnected = false;
|
||||
let evenniaStaleTimer = null;
|
||||
const EVENNIA_STALE_MS = 60000; // mark stale after 60s without update
|
||||
let recentToolOutputs = [];
|
||||
let workshopPanelCtx = null;
|
||||
let workshopPanelTexture = null;
|
||||
@@ -602,7 +597,7 @@ class PSELayer {
|
||||
|
||||
let pseLayer;
|
||||
|
||||
let metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||||
let resonanceViz, metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||||
let agentFSMs = {};
|
||||
|
||||
function setupGOFAI() {
|
||||
@@ -671,7 +666,7 @@ async function init() {
|
||||
scene = new THREE.Scene();
|
||||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||||
|
||||
setupGOFAI();
|
||||
setupGOFAI();\n resonanceViz = new ResonanceVisualizer(scene);
|
||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.copy(playerPos);
|
||||
|
||||
@@ -760,8 +755,6 @@ async function init() {
|
||||
enterPrompt.addEventListener('click', () => {
|
||||
enterPrompt.classList.add('fade-out');
|
||||
document.getElementById('hud').style.display = 'block';
|
||||
const erpPanel = document.getElementById('evennia-room-panel');
|
||||
if (erpPanel) erpPanel.style.display = 'block';
|
||||
setTimeout(() => { enterPrompt.remove(); }, 600);
|
||||
}, { once: true });
|
||||
|
||||
@@ -2176,134 +2169,10 @@ function handleHermesMessage(data) {
|
||||
else addChatMessage(msg.agent, msg.text, false);
|
||||
});
|
||||
}
|
||||
} else if (data.type && data.type.startsWith('evennia.')) {
|
||||
handleEvenniaEvent(data);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// EVENNIA ROOM SNAPSHOT PANEL (Issue #728)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
function handleEvenniaEvent(data) {
|
||||
const evtType = data.type;
|
||||
|
||||
if (evtType === 'evennia.room_snapshot') {
|
||||
evenniaRoom = {
|
||||
roomKey: data.room_key || data.room_id || '',
|
||||
title: data.title || 'Unknown Room',
|
||||
desc: data.desc || '',
|
||||
exits: data.exits || [],
|
||||
objects: data.objects || [],
|
||||
occupants: data.occupants || [],
|
||||
timestamp: data.timestamp || new Date().toISOString()
|
||||
};
|
||||
evenniaConnected = true;
|
||||
renderEvenniaRoomPanel();
|
||||
resetEvenniaStaleTimer();
|
||||
} else if (evtType === 'evennia.player_move') {
|
||||
// Movement may indicate current room changed; update location text
|
||||
if (data.to_room) {
|
||||
const locEl = document.getElementById('hud-location-text');
|
||||
if (locEl) locEl.textContent = data.to_room;
|
||||
}
|
||||
} else if (evtType === 'evennia.session_bound') {
|
||||
evenniaConnected = true;
|
||||
renderEvenniaRoomPanel();
|
||||
} else if (evtType === 'evennia.player_join' || evtType === 'evennia.player_leave') {
|
||||
// Refresh occupant display if we have room data
|
||||
if (evenniaRoom) renderEvenniaRoomPanel();
|
||||
}
|
||||
}
|
||||
|
||||
function resetEvenniaStaleTimer() {
|
||||
if (evenniaStaleTimer) clearTimeout(evenniaStaleTimer);
|
||||
const dot = document.getElementById('erp-live-dot');
|
||||
const status = document.getElementById('erp-status');
|
||||
if (dot) dot.className = 'erp-live-dot connected';
|
||||
if (status) { status.textContent = 'LIVE'; status.className = 'erp-status online'; }
|
||||
evenniaStaleTimer = setTimeout(() => {
|
||||
if (dot) dot.className = 'erp-live-dot stale';
|
||||
if (status) { status.textContent = 'STALE'; status.className = 'erp-status stale'; }
|
||||
}, EVENNIA_STALE_MS);
|
||||
}
|
||||
|
||||
function renderEvenniaRoomPanel() {
|
||||
const panel = document.getElementById('evennia-room-panel');
|
||||
if (!panel) return;
|
||||
panel.style.display = 'block';
|
||||
|
||||
const emptyEl = document.getElementById('erp-empty');
|
||||
const roomEl = document.getElementById('erp-room');
|
||||
|
||||
if (!evenniaRoom) {
|
||||
if (emptyEl) emptyEl.style.display = 'flex';
|
||||
if (roomEl) roomEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
if (roomEl) roomEl.style.display = 'block';
|
||||
|
||||
const titleEl = document.getElementById('erp-room-title');
|
||||
const descEl = document.getElementById('erp-room-desc');
|
||||
if (titleEl) titleEl.textContent = evenniaRoom.title;
|
||||
if (descEl) descEl.textContent = evenniaRoom.desc;
|
||||
|
||||
renderEvenniaList('erp-exits', evenniaRoom.exits, (item) => {
|
||||
const name = item.key || item.destination_id || item.name || '?';
|
||||
const dest = item.destination_key || item.destination_id || '';
|
||||
return { icon: '→', label: name, extra: dest && dest !== name ? dest : '' };
|
||||
});
|
||||
|
||||
renderEvenniaList('erp-objects', evenniaRoom.objects, (item) => {
|
||||
const name = item.short_desc || item.key || item.id || item.name || '?';
|
||||
return { icon: '◇', label: name };
|
||||
});
|
||||
|
||||
renderEvenniaList('erp-occupants', evenniaRoom.occupants, (item) => {
|
||||
const name = item.character || item.name || item.account || '?';
|
||||
return { icon: '◉', label: name };
|
||||
});
|
||||
|
||||
const tsEl = document.getElementById('erp-footer-ts');
|
||||
const roomKeyEl = document.getElementById('erp-footer-room');
|
||||
if (tsEl) {
|
||||
try {
|
||||
const d = new Date(evenniaRoom.timestamp);
|
||||
tsEl.textContent = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||
} catch(e) { tsEl.textContent = '—'; }
|
||||
}
|
||||
if (roomKeyEl) roomKeyEl.textContent = evenniaRoom.roomKey;
|
||||
}
|
||||
|
||||
function renderEvenniaList(containerId, items, mapFn) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'erp-section-empty';
|
||||
empty.textContent = 'none';
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const mapped = mapFn(item);
|
||||
const row = document.createElement('div');
|
||||
row.className = 'erp-item';
|
||||
row.innerHTML = `<span class="erp-item-icon">${mapped.icon}</span><span>${mapped.label}</span>`;
|
||||
if (mapped.extra) {
|
||||
row.innerHTML += `<span class="erp-item-dest">${mapped.extra}</span>`;
|
||||
}
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
// MNEMOSYNE — LIVE MEMORY BRIDGE
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
|
||||
38
index.html
38
index.html
@@ -102,44 +102,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Evennia Room Snapshot Panel -->
|
||||
<div id="evennia-room-panel" class="evennia-room-panel" style="display:none;">
|
||||
<div class="erp-header">
|
||||
<div class="erp-header-left">
|
||||
<div class="erp-live-dot" id="erp-live-dot"></div>
|
||||
<span class="erp-title">EVENNIA — ROOM SNAPSHOT</span>
|
||||
</div>
|
||||
<span class="erp-status" id="erp-status">OFFLINE</span>
|
||||
</div>
|
||||
<div class="erp-body" id="erp-body">
|
||||
<div class="erp-empty" id="erp-empty">
|
||||
<span class="erp-empty-icon">⊘</span>
|
||||
<span class="erp-empty-text">No Evennia connection</span>
|
||||
<span class="erp-empty-sub">Waiting for room data...</span>
|
||||
</div>
|
||||
<div class="erp-room" id="erp-room" style="display:none;">
|
||||
<div class="erp-room-title" id="erp-room-title"></div>
|
||||
<div class="erp-room-desc" id="erp-room-desc"></div>
|
||||
<div class="erp-section">
|
||||
<div class="erp-section-header">EXITS</div>
|
||||
<div class="erp-exits" id="erp-exits"></div>
|
||||
</div>
|
||||
<div class="erp-section">
|
||||
<div class="erp-section-header">OBJECTS</div>
|
||||
<div class="erp-objects" id="erp-objects"></div>
|
||||
</div>
|
||||
<div class="erp-section">
|
||||
<div class="erp-section-header">OCCUPANTS</div>
|
||||
<div class="erp-occupants" id="erp-occupants"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="erp-footer">
|
||||
<span class="erp-footer-ts" id="erp-footer-ts">—</span>
|
||||
<span class="erp-footer-room" id="erp-footer-room"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Left: Debug -->
|
||||
<div id="debug-overlay" class="hud-debug"></div>
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
|
||||
class MemoryOptimizer {
|
||||
constructor(options = {}) {
|
||||
this.threshold = options.threshold || 0.8;
|
||||
this.decayRate = options.decayRate || 0.05;
|
||||
this.threshold = options.threshold || 0.3;
|
||||
this.decayRate = options.decayRate || 0.01;
|
||||
this.lastRun = Date.now();
|
||||
}
|
||||
optimize(memory) {
|
||||
console.log('Optimizing memory...');
|
||||
// Heuristic-based pruning
|
||||
return memory.filter(m => m.strength > this.threshold);
|
||||
optimize(memories) {
|
||||
const now = Date.now();
|
||||
const elapsed = (now - this.lastRun) / 1000;
|
||||
this.lastRun = now;
|
||||
return memories.map(m => {
|
||||
const decay = (m.importance || 1) * this.decayRate * elapsed;
|
||||
return { ...m, strength: Math.max(0, (m.strength || 1) - decay) };
|
||||
}).filter(m => m.strength > this.threshold || m.locked);
|
||||
}
|
||||
}
|
||||
export default MemoryOptimizer;
|
||||
|
||||
@@ -243,24 +243,108 @@ async def playback(log_path: Path, ws_url: str):
|
||||
await ws.send(json.dumps(event))
|
||||
|
||||
|
||||
async def inject_event(event_type: str, ws_url: str, **kwargs):
|
||||
"""Inject a single Evennia event into the Nexus WS gateway. Dev/test use."""
|
||||
from nexus.evennia_event_adapter import (
|
||||
actor_located, command_issued, command_result,
|
||||
room_snapshot, session_bound,
|
||||
)
|
||||
|
||||
builders = {
|
||||
"room_snapshot": lambda: room_snapshot(
|
||||
kwargs.get("room_key", "Gate"),
|
||||
kwargs.get("title", "Gate"),
|
||||
kwargs.get("desc", "The entrance gate."),
|
||||
exits=kwargs.get("exits"),
|
||||
objects=kwargs.get("objects"),
|
||||
),
|
||||
"actor_located": lambda: actor_located(
|
||||
kwargs.get("actor_id", "Timmy"),
|
||||
kwargs.get("room_key", "Gate"),
|
||||
kwargs.get("room_name"),
|
||||
),
|
||||
"command_result": lambda: command_result(
|
||||
kwargs.get("session_id", "dev-inject"),
|
||||
kwargs.get("actor_id", "Timmy"),
|
||||
kwargs.get("command_text", "look"),
|
||||
kwargs.get("output_text", "You see the Gate."),
|
||||
success=kwargs.get("success", True),
|
||||
),
|
||||
"command_issued": lambda: command_issued(
|
||||
kwargs.get("session_id", "dev-inject"),
|
||||
kwargs.get("actor_id", "Timmy"),
|
||||
kwargs.get("command_text", "look"),
|
||||
),
|
||||
"session_bound": lambda: session_bound(
|
||||
kwargs.get("session_id", "dev-inject"),
|
||||
kwargs.get("account", "Timmy"),
|
||||
kwargs.get("character", "Timmy"),
|
||||
),
|
||||
}
|
||||
|
||||
if event_type not in builders:
|
||||
print(f"[inject] Unknown event type: {event_type}", flush=True)
|
||||
print(f"[inject] Available: {', '.join(builders)}", flush=True)
|
||||
sys.exit(1)
|
||||
|
||||
event = builders[event_type]()
|
||||
payload = json.dumps(event)
|
||||
|
||||
if websockets is None:
|
||||
print(f"[inject] websockets not installed, printing event:\n{payload}", flush=True)
|
||||
return
|
||||
|
||||
try:
|
||||
async with websockets.connect(ws_url, open_timeout=5) as ws:
|
||||
await ws.send(payload)
|
||||
print(f"[inject] Sent {event_type} -> {ws_url}", flush=True)
|
||||
print(f"[inject] Payload: {payload}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"[inject] Failed to send to {ws_url}: {e}", flush=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Evennia -> Nexus WebSocket Bridge")
|
||||
sub = parser.add_subparsers(dest="mode")
|
||||
|
||||
|
||||
live = sub.add_parser("live", help="Live tail Evennia logs and stream to Nexus")
|
||||
live.add_argument("--log-dir", default="/root/workspace/timmy-academy/server/logs", help="Evennia logs directory")
|
||||
live.add_argument("--ws", default="ws://127.0.0.1:8765", help="Nexus WebSocket URL")
|
||||
|
||||
|
||||
replay = sub.add_parser("playback", help="Replay a telemetry JSONL file")
|
||||
replay.add_argument("log_path", help="Path to Evennia telemetry JSONL")
|
||||
replay.add_argument("--ws", default="ws://127.0.0.1:8765", help="Nexus WebSocket URL")
|
||||
|
||||
|
||||
inject = sub.add_parser("inject", help="Inject a single Evennia event (dev/test)")
|
||||
inject.add_argument("event_type", choices=["room_snapshot", "actor_located", "command_result", "command_issued", "session_bound"])
|
||||
inject.add_argument("--ws", default="ws://127.0.0.1:8765", help="Nexus WebSocket URL")
|
||||
inject.add_argument("--room-key", default="Gate", help="Room key (room_snapshot, actor_located)")
|
||||
inject.add_argument("--title", default="Gate", help="Room title (room_snapshot)")
|
||||
inject.add_argument("--desc", default="The entrance gate.", help="Room description (room_snapshot)")
|
||||
inject.add_argument("--actor-id", default="Timmy", help="Actor ID")
|
||||
inject.add_argument("--command-text", default="look", help="Command text (command_result, command_issued)")
|
||||
inject.add_argument("--output-text", default="You see the Gate.", help="Command output (command_result)")
|
||||
inject.add_argument("--session-id", default="dev-inject", help="Hermes session ID")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
if args.mode == "live":
|
||||
asyncio.run(live_bridge(args.log_dir, args.ws))
|
||||
elif args.mode == "playback":
|
||||
asyncio.run(playback(Path(args.log_path).expanduser(), args.ws))
|
||||
elif args.mode == "inject":
|
||||
asyncio.run(inject_event(
|
||||
args.event_type,
|
||||
args.ws,
|
||||
room_key=args.room_key,
|
||||
title=args.title,
|
||||
desc=args.desc,
|
||||
actor_id=args.actor_id,
|
||||
command_text=args.command_text,
|
||||
output_text=args.output_text,
|
||||
session_id=args.session_id,
|
||||
))
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
14
nexus/mnemosyne/reasoner.py
Normal file
14
nexus/mnemosyne/reasoner.py
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
class Reasoner:
|
||||
def __init__(self, rules):
|
||||
self.rules = rules
|
||||
def evaluate(self, entries):
|
||||
return [r['action'] for r in self.rules if self._check(r['condition'], entries)]
|
||||
def _check(self, cond, entries):
|
||||
if cond.startswith('count'):
|
||||
# e.g. count(type=anomaly)>3
|
||||
p = cond.replace('count(', '').split(')')
|
||||
key, val = p[0].split('=')
|
||||
count = sum(1 for e in entries if e.get(key) == val)
|
||||
return eval(f"{count}{p[1]}")
|
||||
return False
|
||||
22
nexus/mnemosyne/resonance_linker.py
Normal file
22
nexus/mnemosyne/resonance_linker.py
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
"""Resonance Linker — Finds second-degree connections in the holographic graph."""
|
||||
|
||||
class ResonanceLinker:
|
||||
def __init__(self, archive):
|
||||
self.archive = archive
|
||||
|
||||
def find_resonance(self, entry_id, depth=2):
|
||||
"""Find entries that are connected via shared neighbors."""
|
||||
if entry_id not in self.archive._entries: return []
|
||||
|
||||
entry = self.archive._entries[entry_id]
|
||||
neighbors = set(entry.links)
|
||||
resonance = {}
|
||||
|
||||
for neighbor_id in neighbors:
|
||||
if neighbor_id in self.archive._entries:
|
||||
for second_neighbor in self.archive._entries[neighbor_id].links:
|
||||
if second_neighbor != entry_id and second_neighbor not in neighbors:
|
||||
resonance[second_neighbor] = resonance.get(second_neighbor, 0) + 1
|
||||
|
||||
return sorted(resonance.items(), key=lambda x: x[1], reverse=True)
|
||||
6
nexus/mnemosyne/rules.json
Normal file
6
nexus/mnemosyne/rules.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"condition": "count(type=anomaly)>3",
|
||||
"action": "alert"
|
||||
}
|
||||
]
|
||||
200
style.css
200
style.css
@@ -2077,203 +2077,3 @@ canvas#nexus-canvas {
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* ═══ EVENNIA ROOM SNAPSHOT PANEL (Issue #728) ═══ */
|
||||
.evennia-room-panel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 80px;
|
||||
width: 300px;
|
||||
background: rgba(5, 5, 16, 0.85);
|
||||
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||
border-right: 3px solid #4af0c0;
|
||||
border-radius: var(--panel-radius);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--color-text);
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.erp-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.12);
|
||||
background: rgba(74, 240, 192, 0.03);
|
||||
}
|
||||
|
||||
.erp-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.erp-live-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-muted);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.erp-live-dot.connected {
|
||||
background: var(--color-primary);
|
||||
animation: blink 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.erp-live-dot.stale {
|
||||
background: var(--color-warning);
|
||||
animation: blink 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.erp-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.erp-status {
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(138, 154, 184, 0.1);
|
||||
}
|
||||
|
||||
.erp-status.online {
|
||||
color: var(--color-primary);
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
}
|
||||
|
||||
.erp-status.stale {
|
||||
color: var(--color-warning);
|
||||
background: rgba(255, 170, 34, 0.1);
|
||||
}
|
||||
|
||||
.erp-body {
|
||||
padding: 8px 12px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Empty/offline state */
|
||||
.erp-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.erp-empty-icon {
|
||||
font-size: 20px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.erp-empty-text {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.erp-empty-sub {
|
||||
font-size: 10px;
|
||||
color: rgba(138, 154, 184, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Room content */
|
||||
.erp-room-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.erp-room-desc {
|
||||
font-size: 11px;
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.erp-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.erp-section-header {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-secondary);
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid rgba(123, 92, 255, 0.15);
|
||||
}
|
||||
|
||||
.erp-item {
|
||||
font-size: 11px;
|
||||
color: var(--color-text);
|
||||
padding: 2px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.erp-item-icon {
|
||||
color: var(--color-primary);
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.erp-item-dest {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.erp-objects .erp-item-icon {
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
.erp-occupants .erp-item-icon {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.erp-section-empty {
|
||||
font-size: 10px;
|
||||
color: rgba(138, 154, 184, 0.4);
|
||||
font-style: italic;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.erp-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border-top: 1px solid rgba(74, 240, 192, 0.1);
|
||||
background: rgba(74, 240, 192, 0.02);
|
||||
}
|
||||
|
||||
.erp-footer-ts {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.erp-footer-room {
|
||||
font-size: 10px;
|
||||
color: var(--color-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user