diff --git a/app.js b/app.js index 5b1d70a..e94f10b 100644 --- a/app.js +++ b/app.js @@ -29,8 +29,11 @@ let keys = {}; let mouseDown = false; let batcaveTerminals = []; let portals = []; // Registry of active portals +let visionPoints = []; // Registry of vision points let activePortal = null; // Portal currently in proximity +let activeVisionPoint = null; // Vision point currently in proximity let portalOverlayActive = false; +let visionOverlayActive = false; let particles, dustParticles; let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; @@ -98,6 +101,15 @@ async function init() { console.error('Failed to load portals.json:', e); addChatMessage('error', 'Portal registry offline. Check logs.'); } + + // Load Vision Points + try { + const response = await fetch('./vision.json'); + const visionData = await response.json(); + createVisionPoints(visionData); + } catch (e) { + console.error('Failed to load vision.json:', e); + } updateLoad(80); createParticles(); @@ -418,6 +430,53 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) { batcaveTerminals.push({ group, scanMat, borderMat }); } +// ═══ VISION SYSTEM ═══ +function createVisionPoints(data) { + data.forEach(config => { + const vp = createVisionPoint(config); + visionPoints.push(vp); + }); +} + +function createVisionPoint(config) { + const group = new THREE.Group(); + group.position.set(config.position.x, config.position.y, config.position.z); + + const color = new THREE.Color(config.color); + + // Floating Crystal + const crystalGeo = new THREE.OctahedronGeometry(0.6, 0); + const crystalMat = new THREE.MeshPhysicalMaterial({ + color: color, + emissive: color, + emissiveIntensity: 1, + roughness: 0, + metalness: 1, + transmission: 0.5, + thickness: 1, + }); + const crystal = new THREE.Mesh(crystalGeo, crystalMat); + crystal.position.y = 2.5; + group.add(crystal); + + // Glow Ring + const ringGeo = new THREE.TorusGeometry(0.8, 0.02, 16, 64); + const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 }); + const ring = new THREE.Mesh(ringGeo, ringMat); + ring.position.y = 2.5; + ring.rotation.x = Math.PI / 2; + group.add(ring); + + // Light + const light = new THREE.PointLight(color, 1, 10); + light.position.set(0, 2.5, 0); + group.add(light); + + scene.add(group); + + return { config, group, crystal, ring, light }; +} + // ═══ PORTAL SYSTEM ═══ function createPortals(data) { data.forEach(config => { @@ -762,6 +821,7 @@ function setupControls() { if (e.key === 'Escape') { document.getElementById('chat-input').blur(); if (portalOverlayActive) closePortalOverlay(); + if (visionOverlayActive) closeVisionOverlay(); } if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) { cycleNavMode(); @@ -769,6 +829,9 @@ function setupControls() { if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) { activatePortal(activePortal); } + if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) { + activateVisionPoint(activeVisionPoint); + } }); document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; @@ -830,6 +893,7 @@ function setupControls() { document.getElementById('chat-send').addEventListener('click', sendChatMessage); document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay); + document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay); } function sendChatMessage() { @@ -932,6 +996,51 @@ function closePortalOverlay() { document.getElementById('portal-overlay').style.display = 'none'; } +// ═══ VISION INTERACTION ═══ +function checkVisionProximity() { + if (visionOverlayActive) return; + + let closest = null; + let minDist = Infinity; + + visionPoints.forEach(vp => { + const dist = playerPos.distanceTo(vp.group.position); + if (dist < 3.5 && dist < minDist) { + minDist = dist; + closest = vp; + } + }); + + activeVisionPoint = closest; + const hint = document.getElementById('vision-hint'); + if (activeVisionPoint) { + document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title; + hint.style.display = 'flex'; + } else { + hint.style.display = 'none'; + } +} + +function activateVisionPoint(vp) { + visionOverlayActive = true; + const overlay = document.getElementById('vision-overlay'); + const titleDisplay = document.getElementById('vision-title-display'); + const contentDisplay = document.getElementById('vision-content-display'); + const statusDot = document.getElementById('vision-status-dot'); + + titleDisplay.textContent = vp.config.title.toUpperCase(); + contentDisplay.textContent = vp.config.content; + statusDot.style.background = vp.config.color; + statusDot.style.boxShadow = `0 0 10px ${vp.config.color}`; + + overlay.style.display = 'flex'; +} + +function closeVisionOverlay() { + visionOverlayActive = false; + document.getElementById('vision-overlay').style.display = 'none'; +} + // ═══ GAME LOOP ═══ function gameLoop() { requestAnimationFrame(gameLoop); @@ -1007,6 +1116,7 @@ function gameLoop() { // Proximity check checkPortalProximity(); + checkVisionProximity(); const sky = scene.getObjectByName('skybox'); if (sky) sky.material.uniforms.uTime.value = elapsed; @@ -1032,6 +1142,16 @@ function gameLoop() { portal.pSystem.geometry.attributes.position.needsUpdate = true; }); + // Animate Vision Points + visionPoints.forEach(vp => { + vp.crystal.rotation.y = elapsed * 0.8; + vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2; + vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2; + vp.ring.rotation.z = elapsed * 0.5; + vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05); + vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3; + }); + if (particles?.material?.uniforms) { particles.material.uniforms.uTime.value = elapsed; } diff --git a/index.html b/index.html index 01b08e1..dfdf76a 100644 --- a/index.html +++ b/index.html @@ -108,6 +108,25 @@
Enter
+ + + + + +