diff --git a/app.js b/app.js index 60689a0..09c7a0f 100644 --- a/app.js +++ b/app.js @@ -34,6 +34,9 @@ let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; let loadProgress = 0; +let nostrPubkey = null; + +const NOSTR_STORAGE_KEY = 'nexus_nostr_pubkey'; // ═══ INIT ═══ function init() { @@ -95,6 +98,9 @@ function init() { setupControls(); window.addEventListener('resize', onResize); + // NIP-07 identity (after DOM is ready) + initNostrIdentity(); + // Debug overlay ref debugOverlay = document.getElementById('debug-overlay'); @@ -866,7 +872,8 @@ function addChatMessage(type, text) { const container = document.getElementById('chat-messages'); const div = document.createElement('div'); div.className = `chat-msg chat-msg-${type}`; - const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' }; + const userPrefix = nostrPubkey ? `[${formatPubkey(nostrPubkey)}]` : '[VISITOR]'; + const prefixes = { user: userPrefix, timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' }; div.innerHTML = `${prefixes[type] || '[???]'} ${text}`; container.appendChild(div); container.scrollTop = container.scrollHeight; @@ -980,5 +987,71 @@ function onResize() { composer.setSize(w, h); } +// ═══ NIP-07 NOSTR IDENTITY ═══ +function formatPubkey(hex) { + return hex.slice(0, 8) + '…' + hex.slice(-4); +} + +function setNostrIdentity(pubkey) { + nostrPubkey = pubkey; + localStorage.setItem(NOSTR_STORAGE_KEY, pubkey); + document.getElementById('nostr-anon').style.display = 'none'; + document.getElementById('nostr-connect-btn').style.display = 'none'; + document.getElementById('nostr-pubkey-display').textContent = formatPubkey(pubkey); + document.getElementById('nostr-connected').style.display = 'flex'; + addChatMessage('system', `Identity connected: ${formatPubkey(pubkey)}. Timmy recognizes you.`); +} + +function clearNostrIdentity() { + nostrPubkey = null; + localStorage.removeItem(NOSTR_STORAGE_KEY); + document.getElementById('nostr-connected').style.display = 'none'; + document.getElementById('nostr-anon').style.display = 'flex'; + if (window.nostr) { + document.getElementById('nostr-connect-btn').style.display = 'flex'; + } + addChatMessage('system', 'Identity disconnected. Visitor is anonymous.'); +} + +function initNostrIdentity() { + const connectBtn = document.getElementById('nostr-connect-btn'); + const disconnectBtn = document.getElementById('nostr-disconnect-btn'); + + connectBtn.addEventListener('click', async () => { + try { + connectBtn.disabled = true; + connectBtn.textContent = 'Connecting…'; + const pubkey = await window.nostr.getPublicKey(); + if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey)) { + setNostrIdentity(pubkey); + } else { + throw new Error('Invalid pubkey returned'); + } + } catch (err) { + addChatMessage('error', 'Identity request declined or extension unavailable.'); + connectBtn.disabled = false; + connectBtn.innerHTML = ' Connect Identity'; + } + }); + + disconnectBtn.addEventListener('click', () => { + clearNostrIdentity(); + }); + + // Check for saved identity in localStorage + const saved = localStorage.getItem(NOSTR_STORAGE_KEY); + if (saved && /^[0-9a-f]{64}$/i.test(saved)) { + nostrPubkey = saved; + document.getElementById('nostr-anon').style.display = 'none'; + document.getElementById('nostr-pubkey-display').textContent = formatPubkey(saved); + document.getElementById('nostr-connected').style.display = 'flex'; + } else if (window.nostr) { + // Extension available, offer connect + document.getElementById('nostr-anon').style.display = 'none'; + connectBtn.style.display = 'flex'; + } + // else: stays anonymous (default) +} + // ═══ START ═══ init(); diff --git a/index.html b/index.html index 3a2c6ea..6e6b908 100644 --- a/index.html +++ b/index.html @@ -86,7 +86,7 @@ [NEXUS] Sovereign space initialized. Timmy is observing.
- [TIMMY] Welcome to the Nexus, Alexander. All systems nominal. + [TIMMY] Welcome to the Nexus. Connect your Nostr identity or explore anonymously.
@@ -95,6 +95,21 @@
+ +
+ + +
+ Anonymous +
+
+
WASD move   Mouse look   Enter chat diff --git a/style.css b/style.css index 519b05e..780fcbb 100644 --- a/style.css +++ b/style.css @@ -330,6 +330,81 @@ canvas#nexus-canvas { background: rgba(74, 240, 192, 0.1); } +/* === NOSTR IDENTITY === */ +.nostr-identity { + position: absolute; + top: var(--space-3); + right: var(--space-4); + display: flex; + align-items: center; + gap: var(--space-2); + pointer-events: auto; +} +.nostr-btn { + background: none; + border: 1px solid var(--color-border); + border-radius: 4px; + font-family: var(--font-body); + font-size: var(--text-xs); + cursor: pointer; + transition: background var(--transition-ui), border-color var(--transition-ui); + display: flex; + align-items: center; + gap: var(--space-1); +} +.nostr-btn-connect { + padding: var(--space-1) var(--space-3); + color: var(--color-primary); + border-color: rgba(74, 240, 192, 0.3); +} +.nostr-btn-connect:hover { + background: rgba(74, 240, 192, 0.1); + border-color: rgba(74, 240, 192, 0.6); +} +.nostr-btn-disconnect { + padding: 2px var(--space-2); + color: var(--color-text-muted); + border-color: transparent; + font-size: 10px; + line-height: 1; +} +.nostr-btn-disconnect:hover { + color: var(--color-danger); + border-color: rgba(255, 68, 102, 0.3); +} +.nostr-connected { + display: flex; + align-items: center; + gap: var(--space-2); + background: rgba(74, 240, 192, 0.05); + border: 1px solid rgba(74, 240, 192, 0.25); + border-radius: 4px; + padding: var(--space-1) var(--space-3); + font-size: var(--text-xs); +} +.nostr-icon { + color: var(--color-primary); + font-style: normal; +} +.nostr-pubkey { + font-family: var(--font-body); + font-size: var(--text-xs); + color: var(--color-primary); + letter-spacing: 0.05em; +} +.nostr-anon { + font-size: var(--text-xs); + color: var(--color-text-muted); + display: flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-1) var(--space-2); +} +.nostr-icon-muted { + color: var(--color-text-muted); + font-style: normal; +} + /* === FOOTER === */ .nexus-footer { position: fixed;