diff --git a/app.js b/app.js index 3f9b2cd..48cad1a 100644 --- a/app.js +++ b/app.js @@ -232,6 +232,58 @@ glassPlatformGroup.add(voidLight); scene.add(glassPlatformGroup); +// === WALKING (WASD) === +const walkOffset = new THREE.Vector3(0, 0, 0); +const walkCamPos = new THREE.Vector3(); +const WALK_SPEED = 0.07; +const WALK_CLAMP = 4.0; +const walkKeys = { w: false, a: false, s: false, d: false }; + +document.addEventListener('keydown', (e) => { + if (e.key === 'w' || e.key === 'W') walkKeys.w = true; + if (e.key === 'a' || e.key === 'A') walkKeys.a = true; + if (e.key === 's' || e.key === 'S') walkKeys.s = true; + if (e.key === 'd' || e.key === 'D') walkKeys.d = true; +}); +document.addEventListener('keyup', (e) => { + if (e.key === 'w' || e.key === 'W') walkKeys.w = false; + if (e.key === 'a' || e.key === 'A') walkKeys.a = false; + if (e.key === 's' || e.key === 'S') walkKeys.s = false; + if (e.key === 'd' || e.key === 'D') walkKeys.d = false; +}); + +// === BREADCRUMB TRAIL === +const CRUMB_MAX = 80; +const CRUMB_MIN_DIST_SQ = 0.12; // ~0.35 units min gap between crumbs +/** @type {THREE.Vector3[]} */ +const crumbs = []; +let crumbLastX = 0; +let crumbLastZ = 0; + +// Faint line tracing the path walked +const trailGeo = new THREE.BufferGeometry(); +const trailMat = new THREE.LineBasicMaterial({ + color: NEXUS.colors.accent, + transparent: true, + opacity: 0.22, + depthWrite: false, +}); +const trailLine = new THREE.Line(trailGeo, trailMat); +scene.add(trailLine); + +// Glowing dots — individual breadcrumb markers +const crumbDotGeo = new THREE.BufferGeometry(); +const crumbDotMat = new THREE.PointsMaterial({ + color: NEXUS.colors.accent, + size: 0.22, + sizeAttenuation: true, + transparent: true, + opacity: 0.45, + depthWrite: false, +}); +const crumbDots = new THREE.Points(crumbDotGeo, crumbDotMat); +scene.add(crumbDots); + // === MOUSE-DRIVEN ROTATION === let mouseX = 0; let mouseY = 0; @@ -443,11 +495,39 @@ function animate() { requestAnimationFrame(animate); const elapsed = clock.getElapsedTime(); + // Update walking (only when in normal ground-level view) + if (!overviewMode && !photoMode) { + if (walkKeys.w) walkOffset.z -= WALK_SPEED; + if (walkKeys.s) walkOffset.z += WALK_SPEED; + if (walkKeys.a) walkOffset.x -= WALK_SPEED; + if (walkKeys.d) walkOffset.x += WALK_SPEED; + walkOffset.x = Math.max(-WALK_CLAMP, Math.min(WALK_CLAMP, walkOffset.x)); + walkOffset.z = Math.max(-WALK_CLAMP, Math.min(WALK_CLAMP, walkOffset.z)); + } + // Smooth camera transition for overview mode const targetT = overviewMode ? 1 : 0; overviewT += (targetT - overviewT) * 0.04; - camera.position.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT); - camera.lookAt(0, 0, 0); + walkCamPos.set(NORMAL_CAM.x + walkOffset.x, NORMAL_CAM.y, NORMAL_CAM.z + walkOffset.z); + camera.position.lerpVectors(walkCamPos, OVERVIEW_CAM, overviewT); + camera.lookAt(walkOffset.x * (1 - overviewT), 0, walkOffset.z * (1 - overviewT)); + + // Record breadcrumb when player has moved far enough + const crumbFx = walkOffset.x; + const crumbFz = walkOffset.z; + const crumbDx = crumbFx - crumbLastX; + const crumbDz = crumbFz - crumbLastZ; + if (crumbDx * crumbDx + crumbDz * crumbDz >= CRUMB_MIN_DIST_SQ) { + crumbLastX = crumbFx; + crumbLastZ = crumbFz; + crumbs.push(new THREE.Vector3(crumbFx, 0.015, crumbFz)); + if (crumbs.length > CRUMB_MAX) crumbs.shift(); + if (crumbs.length >= 2) trailGeo.setFromPoints(crumbs); + crumbDotGeo.setFromPoints(crumbs); + } + // Pulse the trail glow + trailMat.opacity = 0.14 + Math.sin(elapsed * 1.1) * 0.08; + crumbDotMat.opacity = 0.35 + Math.sin(elapsed * 1.9) * 0.13; // Slow auto-rotation — suppressed during overview and photo mode const rotationScale = photoMode ? 0 : (1 - overviewT);