diff --git a/app.js b/app.js index b396094..b1afcbd 100644 --- a/app.js +++ b/app.js @@ -394,6 +394,15 @@ let overviewT = 0; // 0 = normal view, 1 = overview const NORMAL_CAM = new THREE.Vector3(0, 6, 11); const OVERVIEW_CAM = new THREE.Vector3(0, 200, 0.1); // overhead; tiny Z offset avoids gimbal lock +// Easter egg room camera state +const EGG_ROOM_Y = -28; +const EGG_CAM = new THREE.Vector3(0, EGG_ROOM_Y + 8, 12); +let eggRoomMode = false; +let eggRoomT = 0; + +/** Reusable vector for three-way camera blend. */ +const _camBlend = new THREE.Vector3(); + const overviewIndicator = document.getElementById('overview-indicator'); document.addEventListener('keydown', (e) => { @@ -656,6 +665,133 @@ for (let i = 0; i < RUNE_COUNT; i++) { runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 }); } +// === EASTER EGG ROOM === +// A secret chamber far below the platform revealed by the Konami walk pattern: +// ↑ ↑ ↓ ↓ ← → ← → Press [E] to exit. + +const eggRoomGroup = new THREE.Group(); +eggRoomGroup.position.set(0, EGG_ROOM_Y, 0); + +// Hexagonal floor +const eggFloorGeo = new THREE.CylinderGeometry(6.5, 6.5, 0.15, 6); +const eggFloorMat = new THREE.MeshStandardMaterial({ + color: 0x1a0030, + emissive: new THREE.Color(0x9900ff).multiplyScalar(0.2), + metalness: 0.8, + roughness: 0.2, +}); +eggRoomGroup.add(new THREE.Mesh(eggFloorGeo, eggFloorMat)); + +// Glowing hex rim +const eggRimGeo = new THREE.TorusGeometry(6.5, 0.12, 6, 6); +const eggRimMat = new THREE.MeshBasicMaterial({ color: 0xcc44ff, transparent: true, opacity: 0.85 }); +const eggRim = new THREE.Mesh(eggRimGeo, eggRimMat); +eggRim.rotation.x = Math.PI / 2; +eggRoomGroup.add(eggRim); + +// 6 glowing pillars arranged in hexagon +const EGG_PILLAR_R = 5.5; +for (let i = 0; i < 6; i++) { + const a = (i / 6) * Math.PI * 2; + const px = Math.cos(a) * EGG_PILLAR_R; + const pz = Math.sin(a) * EGG_PILLAR_R; + const pillarGeo = new THREE.CylinderGeometry(0.18, 0.24, 4.5, 6); + const pillarMat = new THREE.MeshStandardMaterial({ + color: 0x0a0020, + emissive: new THREE.Color(0x6611aa).multiplyScalar(0.6), + metalness: 0.9, + roughness: 0.1, + }); + const pillar = new THREE.Mesh(pillarGeo, pillarMat); + pillar.position.set(px, 2.25, pz); + eggRoomGroup.add(pillar); + // Cap glow at each pillar top + const capLight = new THREE.PointLight(0xcc44ff, 0.35, 5); + capLight.position.set(px, 4.6, pz); + eggRoomGroup.add(capLight); +} + +// Central floating crystal (icosahedron) +const eggCrystalGeo = new THREE.IcosahedronGeometry(1.2, 1); +const eggCrystalMat = new THREE.MeshPhysicalMaterial({ + color: new THREE.Color(0xdd66ff), + transparent: true, + opacity: 0.72, + roughness: 0.0, + metalness: 0.0, + transmission: 0.5, + thickness: 0.8, + emissive: new THREE.Color(0x7700cc), + emissiveIntensity: 0.5, + side: THREE.DoubleSide, +}); +const eggCrystal = new THREE.Mesh(eggCrystalGeo, eggCrystalMat); +eggCrystal.position.set(0, 2.5, 0); +eggRoomGroup.add(eggCrystal); + +// Crystal glow light +const eggCrystalLight = new THREE.PointLight(0xcc44ff, 1.2, 14); +eggCrystalLight.position.set(0, 2.5, 0); +eggRoomGroup.add(eggCrystalLight); + +// "TIMMY'S VAULT" label sprite +function createVaultLabelTexture() { + const W = 512, H = 80; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, W, H); + ctx.font = 'bold 34px "Courier New", monospace'; + ctx.fillStyle = '#dd66ff'; + ctx.shadowColor = '#cc44ff'; + ctx.shadowBlur = 22; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('\u2736 TIMMY\'S VAULT \u2736', W / 2, H / 2); + return new THREE.CanvasTexture(canvas); +} + +const eggVaultLabel = new THREE.Sprite(new THREE.SpriteMaterial({ + map: createVaultLabelTexture(), + transparent: true, + opacity: 0.92, + depthWrite: false, + blending: THREE.AdditiveBlending, +})); +eggVaultLabel.scale.set(9, 1.4, 1); +eggVaultLabel.position.set(0, 5.6, 0); +eggRoomGroup.add(eggVaultLabel); + +// Room ambient fill light +const eggRoomLight = new THREE.PointLight(0x9900ff, 0.7, 28); +eggRoomLight.position.set(0, 6, 0); +eggRoomGroup.add(eggRoomLight); + +scene.add(eggRoomGroup); + +/** + * Activates the Easter egg hidden room. + */ +function triggerEasterEggRoom() { + eggRoomMode = true; + const msg = document.getElementById('easter-egg-msg'); + if (msg) { + msg.classList.remove('visible'); + void msg.offsetWidth; + msg.classList.add('visible'); + } +} + +/** + * Exits the Easter egg room and returns to normal view. + */ +function exitEasterEggRoom() { + eggRoomMode = false; + const msg = document.getElementById('easter-egg-msg'); + if (msg) msg.classList.remove('visible'); +} + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -668,14 +804,20 @@ function animate() { requestAnimationFrame(animate); const elapsed = clock.getElapsedTime(); - // 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); + // Smooth camera transitions — overview and egg room + const overviewTargetT = overviewMode ? 1 : 0; + overviewT += (overviewTargetT - overviewT) * 0.04; - // Slow auto-rotation — suppressed during overview and photo mode - const rotationScale = photoMode ? 0 : (1 - overviewT); + const eggTargetT = eggRoomMode ? 1 : 0; + eggRoomT += (eggTargetT - eggRoomT) * 0.04; + + // Blend: normal → overview, then slide toward egg room below + _camBlend.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT); + camera.position.lerpVectors(_camBlend, EGG_CAM, eggRoomT); + camera.lookAt(0, eggRoomT * EGG_ROOM_Y, 0); + + // Slow auto-rotation — suppressed during overview, photo, or egg room mode + const rotationScale = photoMode ? 0 : (1 - overviewT) * (1 - eggRoomT); targetRotX += (mouseY * 0.3 - targetRotX) * 0.02; targetRotY += (mouseX * 0.3 - targetRotY) * 0.02; @@ -770,6 +912,13 @@ function animate() { rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2; } + // Animate Easter egg room — crystal spin, glow pulse, rim flicker + eggCrystal.rotation.y = elapsed * 0.85; + eggCrystal.rotation.x = Math.sin(elapsed * 0.45) * 0.35; + eggCrystal.position.y = 2.5 + Math.sin(elapsed * 1.1) * 0.3; + eggCrystalLight.intensity = 0.9 + Math.sin(elapsed * 2.1) * 0.4; + eggRimMat.opacity = 0.6 + Math.sin(elapsed * 1.5) * 0.25; + composer.render(); } @@ -924,6 +1073,41 @@ document.addEventListener('keydown', (e) => { sovereigntyBufferTimer = setTimeout(() => { sovereigntyBuffer = ''; }, 3000); }); +// === EASTER EGG WALK PATTERN === +// Konami code: ↑ ↑ ↓ ↓ ← → ← → +const EGG_WALK_PATTERN = [ + 'ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', + 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', +]; +let eggWalkBuffer = []; +let eggWalkTimer = /** @type {ReturnType|null} */ (null); + +document.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => { + const WALK_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']); + if (!WALK_KEYS.has(e.key)) return; + e.preventDefault(); + + eggWalkBuffer.push(e.key); + if (eggWalkBuffer.length > EGG_WALK_PATTERN.length) { + eggWalkBuffer = eggWalkBuffer.slice(-EGG_WALK_PATTERN.length); + } + + if (eggWalkBuffer.join(',') === EGG_WALK_PATTERN.join(',')) { + eggWalkBuffer = []; + if (!eggRoomMode) triggerEasterEggRoom(); + } + + if (eggWalkTimer) clearTimeout(eggWalkTimer); + eggWalkTimer = setTimeout(() => { eggWalkBuffer = []; }, 4000); +}); + +// Press [E] to exit the Easter egg room +document.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => { + if ((e.key === 'e' || e.key === 'E') && eggRoomMode) { + exitEasterEggRoom(); + } +}); + window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); diff --git a/index.html b/index.html index 69d6b65..46a977f 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,11 @@
⚡ SOVEREIGNTY ⚡
+
+ 🔮 TIMMY'S VAULT DISCOVERED 🔮 + [E] to exit +
+