diff --git a/app.js b/app.js index 9a03e40..81f2424 100644 --- a/app.js +++ b/app.js @@ -45,6 +45,15 @@ let chatOpen = true; let loadProgress = 0; let performanceTier = 'high'; +// ═══ TIMELAPSE STATE ═══ +const TIMELAPSE_DURATION_S = 30; +let timelapseActive = false; +let timelapseRealStart = 0; +let timelapseProgress = 0; +let timelapseNextCommitIdx = 0; +let timelapseCommits = []; +let timelapseWindow = { startMs: 0, endMs: 0 }; + // ═══ NAVIGATION SYSTEM ═══ const NAV_MODES = ['walk', 'orbit', 'fly']; let navModeIdx = 0; @@ -1069,10 +1078,14 @@ function setupControls() { document.getElementById('chat-input').blur(); if (portalOverlayActive) closePortalOverlay(); if (visionOverlayActive) closeVisionOverlay(); + if (timelapseActive) stopTimelapse(); } if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) { cycleNavMode(); } + if (e.key.toLowerCase() === 'l' && document.activeElement !== document.getElementById('chat-input')) { + if (timelapseActive) stopTimelapse(); else startTimelapse(); + } if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) { activatePortal(activePortal); } @@ -1141,6 +1154,13 @@ function setupControls() { document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay); document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay); + + const timelapseBtnEl = document.getElementById('timelapse-btn'); + if (timelapseBtnEl) { + timelapseBtnEl.addEventListener('click', () => { + if (timelapseActive) stopTimelapse(); else startTimelapse(); + }); + } } function sendChatMessage() { @@ -1463,6 +1483,25 @@ function gameLoop() { dustParticles.rotation.y = elapsed * 0.01; } + // ─── TIMELAPSE TICK ─── + if (timelapseActive) { + const realElapsed = elapsed - timelapseRealStart; + timelapseProgress = Math.min(realElapsed / TIMELAPSE_DURATION_S, 1.0); + const span = timelapseWindow.endMs - timelapseWindow.startMs; + const virtualMs = timelapseWindow.startMs + span * timelapseProgress; + + while ( + timelapseNextCommitIdx < timelapseCommits.length && + timelapseCommits[timelapseNextCommitIdx].ts <= virtualMs + ) { + fireTimelapseCommit(timelapseCommits[timelapseNextCommitIdx]); + timelapseNextCommitIdx++; + } + + updateTimelapseHUD(timelapseProgress, virtualMs); + if (timelapseProgress >= 1.0) stopTimelapse(); + } + for (let i = 0; i < 5; i++) { const stone = scene.getObjectByName('runestone_' + i); if (stone) { @@ -1665,6 +1704,91 @@ function simulateAgentThought() { addAgentLog(agentId, thought); } +// ═══ TIME-LAPSE MODE ═══ +async function loadTimelapseData() { + try { + const midnight = new Date(); + midnight.setHours(0, 0, 0, 0); + const res = await fetch( + 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50', + { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } + ); + if (!res.ok) throw new Error('fetch failed'); + const data = await res.json(); + timelapseCommits = data + .map(c => ({ + ts: new Date(c.commit?.author?.date || 0).getTime(), + author: c.commit?.author?.name || c.author?.login || 'unknown', + message: (c.commit?.message || '').split('\n')[0], + hash: (c.sha || '').slice(0, 7), + })) + .filter(c => c.ts >= midnight.getTime()) + .sort((a, b) => a.ts - b.ts); + timelapseWindow = { startMs: midnight.getTime(), endMs: Date.now() }; + } catch { + timelapseCommits = []; + const midnight = new Date(); + midnight.setHours(0, 0, 0, 0); + timelapseWindow = { startMs: midnight.getTime(), endMs: Date.now() }; + } +} + +function fireTimelapseCommit(commit) { + // Flash the nexus core + const core = scene.getObjectByName('nexus-core'); + if (core) { + core.material.emissiveIntensity = 8; + setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 300); + } + // Log the commit in agent stream + const shortMsg = commit.message.length > 40 + ? commit.message.slice(0, 37) + '...' + : commit.message; + addAgentLog('timmy', `[${commit.hash}] ${shortMsg}`); +} + +function updateTimelapseHUD(progress, virtualMs) { + const clockEl = document.getElementById('timelapse-clock'); + if (clockEl) { + const d = new Date(virtualMs); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + clockEl.textContent = `${hh}:${mm}`; + } + const barEl = document.getElementById('timelapse-bar'); + if (barEl) { + barEl.style.width = `${(progress * 100).toFixed(1)}%`; + } +} + +async function startTimelapse() { + if (timelapseActive) return; + addChatMessage('system', 'Loading time-lapse data...'); + await loadTimelapseData(); + timelapseActive = true; + timelapseRealStart = clock.elapsedTime; + timelapseProgress = 0; + timelapseNextCommitIdx = 0; + const indicator = document.getElementById('timelapse-indicator'); + if (indicator) indicator.classList.add('visible'); + const btn = document.getElementById('timelapse-btn'); + if (btn) btn.classList.add('active'); + const commitCount = timelapseCommits.length; + addChatMessage('system', `Time-lapse started. Replaying ${commitCount} commit${commitCount !== 1 ? 's' : ''} from today.`); +} + +function stopTimelapse() { + if (!timelapseActive) return; + timelapseActive = false; + const indicator = document.getElementById('timelapse-indicator'); + if (indicator) indicator.classList.remove('visible'); + const btn = document.getElementById('timelapse-btn'); + if (btn) btn.classList.remove('active'); + const barEl = document.getElementById('timelapse-bar'); + if (barEl) barEl.style.width = '0%'; + addChatMessage('system', 'Time-lapse complete.'); +} + function addAgentLog(agentId, text) { const container = document.getElementById('agent-log-content'); if (!container) return; diff --git a/index.html b/index.html index dd4d42d..270435d 100644 --- a/index.html +++ b/index.html @@ -104,10 +104,19 @@