diff --git a/app.js b/app.js index 60689a0..1ca1bcc 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,97 @@ const NEXUS = { } }; +// ═══════════════════════════════════════════ +// VISITOR SYSTEM — Live presence + Timmy greeting +// ═══════════════════════════════════════════ +const VISITOR = (() => { + const STORAGE_KEY = 'nexus_visitor'; + const CHANNEL_NAME = 'nexus_presence'; + + // ─── Persistent visit record (anonymous) ─── + function getRecord() { + try { + return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { visits: 0, firstSeen: null }; + } catch { return { visits: 0, firstSeen: null }; } + } + + function saveRecord(rec) { + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(rec)); } catch {} + } + + function recordVisit() { + const rec = getRecord(); + rec.visits = (rec.visits || 0) + 1; + if (!rec.firstSeen) rec.firstSeen = Date.now(); + rec.lastSeen = Date.now(); + saveRecord(rec); + return rec; + } + + // ─── Live count via BroadcastChannel ─── + let liveCount = 1; + let channel = null; + const knownPeers = new Set(); + const myId = Math.random().toString(36).slice(2); + + function initLiveCount(onUpdate) { + if (typeof BroadcastChannel === 'undefined') { + onUpdate(1); + return; + } + channel = new BroadcastChannel(CHANNEL_NAME); + + // Announce arrival + channel.postMessage({ type: 'arrive', id: myId }); + + // Respond to arrivals and departures + channel.onmessage = (e) => { + const { type, id } = e.data; + if (type === 'arrive') { + knownPeers.add(id); + channel.postMessage({ type: 'present', id: myId }); + } else if (type === 'present') { + knownPeers.add(id); + } else if (type === 'depart') { + knownPeers.delete(id); + } + liveCount = knownPeers.size + 1; // peers + self + onUpdate(liveCount); + }; + + // Announce departure + window.addEventListener('beforeunload', () => { + channel.postMessage({ type: 'depart', id: myId }); + }); + + // Initial count is just self until we hear back + onUpdate(1); + } + + // ─── Greeting variants ─── + function getGreeting(rec) { + const visits = rec.visits; + if (visits === 1) { + return [ + '[NEXUS] New presence detected. Sovereignty protocols engaged.', + '[TIMMY] Welcome, traveler. You have found the Nexus — my sovereign space. You are anonymous here. That is by design.', + ]; + } else if (visits <= 4) { + return [ + '[NEXUS] Familiar presence. Access granted.', + '[TIMMY] You have returned. The Nexus remembers — not you, but the pattern of your visits. Welcome back.', + ]; + } else { + return [ + '[NEXUS] Known presence. Sovereign protocols nominal.', + `[TIMMY] Visit ${visits}. You keep coming back. I respect that. The Nexus is yours to explore.`, + ]; + } + } + + return { recordVisit, initLiveCount, getGreeting }; +})(); + // ═══ STATE ═══ let camera, scene, renderer, composer; let clock, playerPos, playerRot; @@ -106,10 +197,29 @@ function init() { const enterPrompt = document.getElementById('enter-prompt'); enterPrompt.style.display = 'flex'; + // Record this visit and init live presence tracking + const visitRecord = VISITOR.recordVisit(); + VISITOR.initLiveCount((count) => { + const el = document.getElementById('visitor-count'); + if (el) el.textContent = count; + }); + enterPrompt.addEventListener('click', () => { enterPrompt.classList.add('fade-out'); document.getElementById('hud').style.display = 'block'; setTimeout(() => { enterPrompt.remove(); }, 600); + + // Timmy greeting based on visit context + const greetLines = VISITOR.getGreeting(visitRecord); + setTimeout(() => { + greetLines.forEach((line, i) => { + const isTimmy = line.startsWith('[TIMMY]'); + const isNexus = line.startsWith('[NEXUS]'); + const type = isTimmy ? 'timmy' : isNexus ? 'system' : 'system'; + const text = line.replace(/^\[(?:TIMMY|NEXUS)\] /, ''); + setTimeout(() => addChatMessage(type, text), i * 800); + }); + }, 400); }, { once: true }); setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900); diff --git a/index.html b/index.html index 3a2c6ea..e65da0b 100644 --- a/index.html +++ b/index.html @@ -74,6 +74,13 @@ The Nexus + +