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 + +
+ + 1 + PRESENT +
+
@@ -85,9 +92,6 @@
[NEXUS] Sovereign space initialized. Timmy is observing.
-
- [TIMMY] Welcome to the Nexus, Alexander. All systems nominal. -
diff --git a/style.css b/style.css index 519b05e..e67b167 100644 --- a/style.css +++ b/style.css @@ -202,6 +202,44 @@ canvas#nexus-canvas { to { transform: rotate(360deg); } } +/* Visitor presence badge */ +.visitor-badge { + position: absolute; + top: var(--space-3); + right: var(--space-4); + display: flex; + align-items: center; + gap: var(--space-1); + font-family: var(--font-display); + font-size: var(--text-xs); + font-weight: 500; + letter-spacing: 0.12em; + color: var(--color-primary); + text-shadow: 0 0 8px rgba(74, 240, 192, 0.3); + pointer-events: none; +} +.visitor-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-primary); + box-shadow: 0 0 6px var(--color-primary); + animation: visitor-pulse 2.5s ease-in-out infinite; +} +@keyframes visitor-pulse { + 0%, 100% { opacity: 0.5; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.3); } +} +#visitor-count { + font-size: var(--text-sm); + font-weight: 700; +} +.visitor-label { + color: var(--color-text-muted); + font-size: 10px; + letter-spacing: 0.15em; +} + /* Controls hint */ .hud-controls { position: absolute;