diff --git a/package-lock.json b/package-lock.json index 50883f057..6182585a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "express-hello-world", "version": "1.0.0", "license": "MIT", "dependencies": { diff --git a/public/animations.js b/public/animations.js new file mode 100644 index 000000000..6c7fb3378 --- /dev/null +++ b/public/animations.js @@ -0,0 +1,312 @@ +// Mortéz World - Cinematic Animations +let camera, scene; +let isIntroPlaying = true; +let introComplete = false; + +const ANIMATION_CONFIG = { + introDuration: 8000, + titleFadeInStart: 2000, + titleFadeInDuration: 2000, + titleFadeOutStart: 6000, + titleFadeOutDuration: 1500, + buttonFadeInStart: 7000, + buttonFadeInDuration: 1000 +}; + +function initAnimations(sceneAPI) { + camera = sceneAPI.getCamera(); + scene = sceneAPI.getScene(); + + // Start intro sequence + playIntroSequence(); +} + +function playIntroSequence() { + const startTime = Date.now(); + const startPosition = new THREE.Vector3(0, 25, 120); + const endPosition = new THREE.Vector3(0, 15, 60); + const lookAtTarget = new THREE.Vector3(0, 10, 0); + + camera.position.copy(startPosition); + camera.lookAt(lookAtTarget); + + // Show loading overlay + const overlay = document.getElementById('intro-overlay'); + const title = document.getElementById('intro-title'); + const subtitle = document.getElementById('intro-subtitle'); + const enterButton = document.getElementById('enter-button'); + + if (overlay) overlay.style.display = 'flex'; + + function animateIntro() { + if (!isIntroPlaying) return; + + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / ANIMATION_CONFIG.introDuration, 1); + const eased = easeInOutQuad(progress); + + // Camera movement + camera.position.lerpVectors(startPosition, endPosition, eased); + camera.lookAt(lookAtTarget); + + // Title fade in + if (elapsed >= ANIMATION_CONFIG.titleFadeInStart && elapsed < ANIMATION_CONFIG.titleFadeInStart + ANIMATION_CONFIG.titleFadeInDuration) { + const titleProgress = (elapsed - ANIMATION_CONFIG.titleFadeInStart) / ANIMATION_CONFIG.titleFadeInDuration; + const titleOpacity = easeInOutQuad(titleProgress); + if (title) title.style.opacity = titleOpacity; + if (subtitle) subtitle.style.opacity = titleOpacity * 0.8; + } + + // Title fade out + if (elapsed >= ANIMATION_CONFIG.titleFadeOutStart) { + const fadeOutProgress = (elapsed - ANIMATION_CONFIG.titleFadeOutStart) / ANIMATION_CONFIG.titleFadeOutDuration; + const fadeOutOpacity = 1 - easeInOutQuad(Math.min(fadeOutProgress, 1)); + if (title) title.style.opacity = fadeOutOpacity; + if (subtitle) subtitle.style.opacity = fadeOutOpacity * 0.8; + } + + // Button fade in + if (elapsed >= ANIMATION_CONFIG.buttonFadeInStart) { + const buttonProgress = (elapsed - ANIMATION_CONFIG.buttonFadeInStart) / ANIMATION_CONFIG.buttonFadeInDuration; + const buttonOpacity = easeInOutQuad(Math.min(buttonProgress, 1)); + if (enterButton) { + enterButton.style.opacity = buttonOpacity; + enterButton.style.pointerEvents = buttonOpacity > 0.5 ? 'auto' : 'none'; + } + } + + // Lightning flash effect + if (elapsed > 3000 && elapsed < 3100) { + triggerLightningFlash(); + } + if (elapsed > 5500 && elapsed < 5600) { + triggerLightningFlash(); + } + + if (progress < 1) { + requestAnimationFrame(animateIntro); + } else { + // Intro complete, wait for user interaction + introComplete = true; + } + } + + animateIntro(); + + // Setup enter button + if (enterButton) { + enterButton.addEventListener('click', () => { + endIntroSequence(); + }); + + // Add hover effects + enterButton.addEventListener('mouseenter', () => { + if (window.audioAPI) { + window.audioAPI.playSound('hover'); + } + triggerButtonHoverEffect(enterButton); + }); + } + + // Allow skip with spacebar + document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && isIntroPlaying) { + endIntroSequence(); + } + }); +} + +function endIntroSequence() { + isIntroPlaying = false; + + const overlay = document.getElementById('intro-overlay'); + const uiContainer = document.getElementById('ui-container'); + + // Fade out overlay + if (overlay) { + overlay.style.opacity = '0'; + setTimeout(() => { + overlay.style.display = 'none'; + }, 1000); + } + + // Fade in UI + if (uiContainer) { + setTimeout(() => { + uiContainer.style.opacity = '1'; + }, 500); + } + + // Play entrance sound + if (window.audioAPI) { + window.audioAPI.playSound('enter'); + window.audioAPI.startAmbience(); + } + + // Show welcome message + if (window.interactionAPI) { + setTimeout(() => { + window.interactionAPI.showMessage("Explore the mansion. Click to interact."); + }, 1500); + } +} + +function triggerLightningFlash() { + const flash = document.getElementById('lightning-flash'); + if (flash) { + flash.style.opacity = '0.3'; + setTimeout(() => { + flash.style.opacity = '0'; + }, 100); + } + + // Increase scene lighting briefly + if (scene) { + scene.traverse((object) => { + if (object.isLight && object.type === 'DirectionalLight') { + const originalIntensity = object.intensity; + object.intensity = originalIntensity * 3; + setTimeout(() => { + object.intensity = originalIntensity; + }, 100); + } + }); + } + + // Play thunder sound + if (window.audioAPI) { + window.audioAPI.playSound('thunder'); + } +} + +function triggerButtonHoverEffect(button) { + // Create ripple effect + const ripple = document.createElement('div'); + ripple.className = 'button-ripple'; + button.appendChild(ripple); + + setTimeout(() => { + ripple.remove(); + }, 600); + + // Flicker lights in scene + if (scene) { + scene.traverse((object) => { + if (object.isLight && object.type === 'PointLight') { + const originalIntensity = object.intensity; + object.intensity = originalIntensity * 1.5; + setTimeout(() => { + object.intensity = originalIntensity; + }, 200); + } + }); + } +} + +function animateCameraTo(targetPosition, lookAtPosition, duration = 2000) { + const startPosition = camera.position.clone(); + const startLookAt = new THREE.Vector3(0, 10, 0); + const startTime = Date.now(); + + function animate() { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = easeInOutCubic(progress); + + camera.position.lerpVectors(startPosition, targetPosition, eased); + + const currentLookAt = new THREE.Vector3(); + currentLookAt.lerpVectors(startLookAt, lookAtPosition, eased); + camera.lookAt(currentLookAt); + + if (progress < 1) { + requestAnimationFrame(animate); + } + } + + animate(); +} + +function createFloatingAnimation(object, amplitude = 0.5, speed = 1) { + const startY = object.position.y; + const startTime = Date.now(); + + function animate() { + const elapsed = (Date.now() - startTime) / 1000; + object.position.y = startY + Math.sin(elapsed * speed) * amplitude; + requestAnimationFrame(animate); + } + + animate(); +} + +function createRotatingAnimation(object, speed = 0.5) { + function animate() { + object.rotation.y += speed * 0.01; + requestAnimationFrame(animate); + } + + animate(); +} + +function createPulsingLight(light, minIntensity, maxIntensity, speed = 1) { + const startTime = Date.now(); + + function animate() { + const elapsed = (Date.now() - startTime) / 1000; + const intensity = minIntensity + (maxIntensity - minIntensity) * (Math.sin(elapsed * speed) * 0.5 + 0.5); + light.intensity = intensity; + requestAnimationFrame(animate); + } + + animate(); +} + +function createScreenShake(intensity = 0.5, duration = 500) { + const startPosition = camera.position.clone(); + const startTime = Date.now(); + + function shake() { + const elapsed = Date.now() - startTime; + if (elapsed < duration) { + const progress = elapsed / duration; + const currentIntensity = intensity * (1 - progress); + + camera.position.x = startPosition.x + (Math.random() - 0.5) * currentIntensity; + camera.position.y = startPosition.y + (Math.random() - 0.5) * currentIntensity; + camera.position.z = startPosition.z + (Math.random() - 0.5) * currentIntensity; + + requestAnimationFrame(shake); + } else { + camera.position.copy(startPosition); + } + } + + shake(); +} + +// Easing functions +function easeInOutQuad(t) { + return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; +} + +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +function easeInOutQuart(t) { + return t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2; +} + +// Export API +window.animationAPI = { + init: initAnimations, + isIntroPlaying: () => isIntroPlaying, + endIntro: endIntroSequence, + animateCameraTo: animateCameraTo, + createFloatingAnimation: createFloatingAnimation, + createRotatingAnimation: createRotatingAnimation, + createPulsingLight: createPulsingLight, + createScreenShake: createScreenShake, + triggerLightningFlash: triggerLightningFlash +}; diff --git a/public/audio.js b/public/audio.js new file mode 100644 index 000000000..37d92efef --- /dev/null +++ b/public/audio.js @@ -0,0 +1,396 @@ +// Mortéz World - Audio System +let audioContext; +let sounds = {}; +let ambienceLoop; +let isAudioInitialized = false; + +const AUDIO_CONFIG = { + masterVolume: 0.7, + ambienceVolume: 0.3, + sfxVolume: 0.5, + spatialRolloff: 2 +}; + +function initAudio() { + // Create audio context on user interaction + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + + // Create procedural sounds + createProceduralSounds(); + + isAudioInitialized = true; +} + +function createProceduralSounds() { + // Thunder sound + sounds.thunder = createThunderSound(); + + // Hover sound + sounds.hover = createHoverSound(); + + // Click sound + sounds.click = createClickSound(); + + // Door creak sound + sounds.door = createDoorSound(); + + // Enter/transition sound + sounds.enter = createEnterSound(); + + // Rotate sound + sounds.rotate = createRotateSound(); +} + +function createThunderSound() { + return () => { + if (!audioContext) return; + + const duration = 2; + const now = audioContext.currentTime; + + // Create noise for thunder rumble + const bufferSize = audioContext.sampleRate * duration; + const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); + const data = buffer.getChannelData(0); + + // Generate brown noise for deep rumble + let lastOut = 0; + for (let i = 0; i < bufferSize; i++) { + const white = Math.random() * 2 - 1; + data[i] = (lastOut + (0.02 * white)) / 1.02; + lastOut = data[i]; + data[i] *= 3.5; + } + + const noise = audioContext.createBufferSource(); + noise.buffer = buffer; + + // Filter for low frequencies + const filter = audioContext.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.value = 200; + filter.Q.value = 1; + + // Envelope + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(AUDIO_CONFIG.sfxVolume * 0.8, now + 0.1); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + noise.connect(filter); + filter.connect(gainNode); + gainNode.connect(audioContext.destination); + + noise.start(now); + noise.stop(now + duration); + }; +} + +function createHoverSound() { + return () => { + if (!audioContext) return; + + const now = audioContext.currentTime; + const duration = 0.1; + + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, now); + oscillator.frequency.exponentialRampToValueAtTime(1200, now + duration); + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(AUDIO_CONFIG.sfxVolume * 0.1, now); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(now); + oscillator.stop(now + duration); + }; +} + +function createClickSound() { + return () => { + if (!audioContext) return; + + const now = audioContext.currentTime; + const duration = 0.15; + + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(400, now); + oscillator.frequency.exponentialRampToValueAtTime(200, now + duration); + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(AUDIO_CONFIG.sfxVolume * 0.2, now); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(now); + oscillator.stop(now + duration); + }; +} + +function createDoorSound() { + return () => { + if (!audioContext) return; + + const now = audioContext.currentTime; + const duration = 1.5; + + // Create creaking sound with multiple oscillators + const frequencies = [120, 180, 240]; + + frequencies.forEach((freq, index) => { + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sawtooth'; + oscillator.frequency.setValueAtTime(freq, now); + oscillator.frequency.linearRampToValueAtTime(freq * 0.9, now + duration); + + const gainNode = audioContext.createGain(); + const volume = AUDIO_CONFIG.sfxVolume * 0.15 * (1 - index * 0.2); + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(volume, now + 0.1); + gainNode.gain.linearRampToValueAtTime(volume * 0.7, now + duration * 0.5); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + const filter = audioContext.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.value = 800; + + oscillator.connect(filter); + filter.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(now + index * 0.1); + oscillator.stop(now + duration); + }); + }; +} + +function createEnterSound() { + return () => { + if (!audioContext) return; + + const now = audioContext.currentTime; + const duration = 2; + + // Deep resonant sound + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(80, now); + oscillator.frequency.exponentialRampToValueAtTime(60, now + duration); + + const oscillator2 = audioContext.createOscillator(); + oscillator2.type = 'sine'; + oscillator2.frequency.setValueAtTime(120, now); + oscillator2.frequency.exponentialRampToValueAtTime(90, now + duration); + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(AUDIO_CONFIG.sfxVolume * 0.3, now + 0.5); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + oscillator.connect(gainNode); + oscillator2.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(now); + oscillator2.start(now); + oscillator.stop(now + duration); + oscillator2.stop(now + duration); + }; +} + +function createRotateSound() { + return () => { + if (!audioContext) return; + + const now = audioContext.currentTime; + const duration = 2; + + // Mechanical rotation sound + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sawtooth'; + oscillator.frequency.setValueAtTime(100, now); + oscillator.frequency.linearRampToValueAtTime(150, now + duration * 0.5); + oscillator.frequency.linearRampToValueAtTime(100, now + duration); + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(AUDIO_CONFIG.sfxVolume * 0.2, now); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + const filter = audioContext.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.value = 500; + + oscillator.connect(filter); + filter.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(now); + oscillator.stop(now + duration); + }; +} + +function startAmbience() { + if (!audioContext || ambienceLoop) return; + + createAmbienceLoop(); +} + +function createAmbienceLoop() { + const duration = 8; + const now = audioContext.currentTime; + + // Wind sound - brown noise filtered + const bufferSize = audioContext.sampleRate * duration; + const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); + const data = buffer.getChannelData(0); + + let lastOut = 0; + for (let i = 0; i < bufferSize; i++) { + const white = Math.random() * 2 - 1; + data[i] = (lastOut + (0.02 * white)) / 1.02; + lastOut = data[i]; + data[i] *= 0.5; + } + + const noise = audioContext.createBufferSource(); + noise.buffer = buffer; + noise.loop = true; + + const filter = audioContext.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.value = 300; + filter.Q.value = 1; + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(AUDIO_CONFIG.ambienceVolume, now + 2); + + noise.connect(filter); + filter.connect(gainNode); + gainNode.connect(audioContext.destination); + + noise.start(now); + ambienceLoop = { source: noise, gain: gainNode }; + + // Add occasional whispers + scheduleWhisper(); +} + +function scheduleWhisper() { + if (!audioContext) return; + + const delay = 5000 + Math.random() * 10000; + + setTimeout(() => { + playWhisper(); + scheduleWhisper(); + }, delay); +} + +function playWhisper() { + if (!audioContext) return; + + const now = audioContext.currentTime; + const duration = 2; + + // Create eerie whisper-like sound + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(200 + Math.random() * 100, now); + oscillator.frequency.linearRampToValueAtTime(250 + Math.random() * 100, now + duration); + + const lfo = audioContext.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.value = 5 + Math.random() * 3; + + const lfoGain = audioContext.createGain(); + lfoGain.gain.value = 50; + + lfo.connect(lfoGain); + lfoGain.connect(oscillator.frequency); + + const gainNode = audioContext.createGain(); + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(AUDIO_CONFIG.ambienceVolume * 0.3, now + 0.5); + gainNode.gain.linearRampToValueAtTime(AUDIO_CONFIG.ambienceVolume * 0.2, now + duration * 0.7); + gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); + + const filter = audioContext.createBiquadFilter(); + filter.type = 'bandpass'; + filter.frequency.value = 1000; + filter.Q.value = 5; + + oscillator.connect(filter); + filter.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.start(now); + lfo.start(now); + oscillator.stop(now + duration); + lfo.stop(now + duration); +} + +function stopAmbience() { + if (ambienceLoop) { + const now = audioContext.currentTime; + ambienceLoop.gain.gain.exponentialRampToValueAtTime(0.01, now + 1); + setTimeout(() => { + if (ambienceLoop) { + ambienceLoop.source.stop(); + ambienceLoop = null; + } + }, 1000); + } +} + +function playSound(soundName) { + if (!isAudioInitialized) { + initAudio(); + } + + if (sounds[soundName]) { + sounds[soundName](); + } +} + +function setMasterVolume(volume) { + AUDIO_CONFIG.masterVolume = Math.max(0, Math.min(1, volume)); +} + +function setAmbienceVolume(volume) { + AUDIO_CONFIG.ambienceVolume = Math.max(0, Math.min(1, volume)); + if (ambienceLoop) { + ambienceLoop.gain.gain.value = AUDIO_CONFIG.ambienceVolume; + } +} + +function setSFXVolume(volume) { + AUDIO_CONFIG.sfxVolume = Math.max(0, Math.min(1, volume)); +} + +// Initialize on first user interaction +document.addEventListener('click', () => { + if (!isAudioInitialized) { + initAudio(); + } +}, { once: true }); + +// Export API +window.audioAPI = { + init: initAudio, + playSound: playSound, + startAmbience: startAmbience, + stopAmbience: stopAmbience, + setMasterVolume: setMasterVolume, + setAmbienceVolume: setAmbienceVolume, + setSFXVolume: setSFXVolume +}; diff --git a/public/index.html b/public/index.html index 58a91355d..541ea8b5e 100644 --- a/public/index.html +++ b/public/index.html @@ -1,33 +1,195 @@ - - - - - Cyclic: Hello World - - - - - -
-
-
-
-
- Hello World! -
- Sign in to Cyclic + + + + + + + Mortéz World | Dark Luxury Fashion + + + + + + + + + + + + + + + +
+ + +
+
+

MORTÉZ WORLD

+

Where darkness drips in luxury

+
-
- +
Press SPACE to skip
+
+ + +
+ +
+ + +
+ + +
+ + +
+
+
🏰
+
+

The Mansion

+

Explore the gothic estate

+
+
+
+
🗼
+
+

The Towers

+

Click to rotate and discover

+
+
+
+
🚪
+
+

The Door

+

Enter the world within

+
+
+
+ + +
+

Controls

+
    +
  • Drag - Rotate camera
  • +
  • Scroll - Zoom in/out
  • +
  • Click - Interact with objects
  • +
  • Double Click - Special actions
  • +
+
+ + +
+ +
+ + + +
+ + +
+
+
+

Loading Mortéz World...

-
- - - - - - - - + + + + + + + + + + + + + diff --git a/public/interactions.js b/public/interactions.js new file mode 100644 index 000000000..2f856eed8 --- /dev/null +++ b/public/interactions.js @@ -0,0 +1,395 @@ +// Mortéz World - Interaction System +let raycaster, mouse, camera, scene; +let hoveredObject = null; +let isDragging = false; +let previousMousePosition = { x: 0, y: 0 }; +let cameraRotation = { x: 0, y: 0 }; +let interactiveObjects = []; + +const INTERACTION_CONFIG = { + rotationSpeed: 0.003, + zoomSpeed: 0.05, + minDistance: 20, + maxDistance: 120, + hoverColor: 0xffb347, + normalEmissiveIntensity: 0.5, + hoverEmissiveIntensity: 1.2 +}; + +function initInteractions(sceneAPI) { + camera = sceneAPI.getCamera(); + scene = sceneAPI.getScene(); + + raycaster = new THREE.Raycaster(); + mouse = new THREE.Vector2(); + + // Collect all interactive objects + collectInteractiveObjects(); + + // Event listeners + window.addEventListener('mousemove', onMouseMove, false); + window.addEventListener('mousedown', onMouseDown, false); + window.addEventListener('mouseup', onMouseUp, false); + window.addEventListener('wheel', onMouseWheel, false); + window.addEventListener('click', onClick, false); + window.addEventListener('dblclick', onDoubleClick, false); + + // Touch support for mobile + window.addEventListener('touchstart', onTouchStart, false); + window.addEventListener('touchmove', onTouchMove, false); + window.addEventListener('touchend', onTouchEnd, false); +} + +function collectInteractiveObjects() { + scene.traverse((object) => { + if (object.userData.interactive) { + interactiveObjects.push(object); + } + }); +} + +function onMouseMove(event) { + // Update mouse position for raycasting + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + // Camera rotation when dragging + if (isDragging) { + const deltaX = event.clientX - previousMousePosition.x; + const deltaY = event.clientY - previousMousePosition.y; + + cameraRotation.y += deltaX * INTERACTION_CONFIG.rotationSpeed; + cameraRotation.x += deltaY * INTERACTION_CONFIG.rotationSpeed; + + // Clamp vertical rotation + cameraRotation.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, cameraRotation.x)); + + updateCameraPosition(); + + previousMousePosition = { + x: event.clientX, + y: event.clientY + }; + } + + // Check for hover interactions + checkHover(); + + // Parallax effect on non-interactive elements + updateParallax(mouse.x, mouse.y); +} + +function onMouseDown(event) { + isDragging = true; + previousMousePosition = { + x: event.clientX, + y: event.clientY + }; + document.body.style.cursor = 'grabbing'; +} + +function onMouseUp(event) { + isDragging = false; + document.body.style.cursor = 'default'; +} + +function onMouseWheel(event) { + event.preventDefault(); + + const delta = event.deltaY * INTERACTION_CONFIG.zoomSpeed; + const distance = camera.position.length(); + const newDistance = Math.max( + INTERACTION_CONFIG.minDistance, + Math.min(INTERACTION_CONFIG.maxDistance, distance + delta) + ); + + const ratio = newDistance / distance; + camera.position.multiplyScalar(ratio); +} + +function onClick(event) { + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(interactiveObjects, true); + + if (intersects.length > 0) { + const object = intersects[0].object; + handleObjectClick(object); + } +} + +function onDoubleClick(event) { + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(interactiveObjects, true); + + if (intersects.length > 0) { + const object = intersects[0].object; + handleObjectDoubleClick(object); + } +} + +function handleObjectClick(object) { + // Find the parent with userData + let target = object; + while (target && !target.userData.type) { + target = target.parent; + } + + if (!target || !target.userData.type) return; + + const type = target.userData.type; + + switch (type) { + case 'door': + triggerDoorAnimation(target); + showMessage("Welcome to Mortéz World..."); + break; + case 'leftTower': + focusOnObject(target, 30); + showMessage("The Tower of Shadows"); + break; + case 'rightTower': + focusOnObject(target, 25); + showMessage("The Tower of Whispers"); + break; + } + + // Play click sound if audio is available + if (window.audioAPI) { + window.audioAPI.playSound('click'); + } +} + +function handleObjectDoubleClick(object) { + let target = object; + while (target && !target.userData.type) { + target = target.parent; + } + + if (!target || !target.userData.type) return; + + const type = target.userData.type; + + if (type === 'leftTower' || type === 'rightTower') { + rotateTower(target); + } +} + +function checkHover() { + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(interactiveObjects, true); + + // Reset previous hover + if (hoveredObject && (intersects.length === 0 || intersects[0].object !== hoveredObject)) { + resetObjectHover(hoveredObject); + hoveredObject = null; + document.body.style.cursor = isDragging ? 'grabbing' : 'default'; + } + + // Set new hover + if (intersects.length > 0) { + const object = intersects[0].object; + if (object !== hoveredObject) { + hoveredObject = object; + highlightObject(object); + document.body.style.cursor = 'pointer'; + + // Play hover sound + if (window.audioAPI) { + window.audioAPI.playSound('hover'); + } + } + } +} + +function highlightObject(object) { + if (object.material) { + if (object.material.emissive) { + object.userData.originalEmissive = object.material.emissive.getHex(); + object.userData.originalEmissiveIntensity = object.material.emissiveIntensity; + object.material.emissive.setHex(INTERACTION_CONFIG.hoverColor); + object.material.emissiveIntensity = INTERACTION_CONFIG.hoverEmissiveIntensity; + } + } + + // Add glow effect + createGlowEffect(object); +} + +function resetObjectHover(object) { + if (object.material && object.userData.originalEmissive !== undefined) { + object.material.emissive.setHex(object.userData.originalEmissive); + object.material.emissiveIntensity = object.userData.originalEmissiveIntensity; + } + + // Remove glow effect + removeGlowEffect(object); +} + +function createGlowEffect(object) { + if (object.userData.glowLight) return; + + const glowLight = new THREE.PointLight(INTERACTION_CONFIG.hoverColor, 1, 10); + glowLight.position.copy(object.position); + object.parent.add(glowLight); + object.userData.glowLight = glowLight; +} + +function removeGlowEffect(object) { + if (object.userData.glowLight) { + object.parent.remove(object.userData.glowLight); + delete object.userData.glowLight; + } +} + +function updateCameraPosition() { + const distance = camera.position.length(); + const phi = cameraRotation.x; + const theta = cameraRotation.y; + + camera.position.x = distance * Math.sin(theta) * Math.cos(phi); + camera.position.y = distance * Math.sin(phi) + 15; + camera.position.z = distance * Math.cos(theta) * Math.cos(phi); + + camera.lookAt(0, 10, 0); +} + +function updateParallax(mouseX, mouseY) { + // Subtle camera movement based on mouse position + if (!isDragging && window.animationAPI && !window.animationAPI.isIntroPlaying()) { + const targetX = mouseX * 2; + const targetY = mouseY * 2; + + camera.position.x += (targetX - camera.position.x) * 0.01; + camera.position.y += (targetY - camera.position.y) * 0.01; + + camera.lookAt(0, 10, 0); + } +} + +function focusOnObject(object, distance = 30) { + if (!window.animationAPI) return; + + const targetPosition = object.position.clone(); + const cameraTarget = new THREE.Vector3( + targetPosition.x, + targetPosition.y + 10, + targetPosition.z + distance + ); + + window.animationAPI.animateCameraTo(cameraTarget, targetPosition, 2000); +} + +function rotateTower(tower) { + const duration = 2000; + const startRotation = tower.rotation.y; + const endRotation = startRotation + Math.PI * 2; + const startTime = Date.now(); + + function animate() { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = easeInOutCubic(progress); + + tower.rotation.y = startRotation + (endRotation - startRotation) * eased; + + if (progress < 1) { + requestAnimationFrame(animate); + } + } + + animate(); + + if (window.audioAPI) { + window.audioAPI.playSound('rotate'); + } +} + +function triggerDoorAnimation(door) { + const duration = 1500; + const startZ = door.position.z; + const endZ = startZ - 3; + const startTime = Date.now(); + + function animate() { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = easeInOutCubic(progress); + + door.position.z = startZ + (endZ - startZ) * eased; + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + // Door opened, trigger next scene or action + setTimeout(() => { + showMessage("Enter Mortéz World — where darkness drips in luxury."); + }, 500); + } + } + + animate(); + + if (window.audioAPI) { + window.audioAPI.playSound('door'); + } +} + +function showMessage(text) { + const messageEl = document.getElementById('interaction-message'); + if (messageEl) { + messageEl.textContent = text; + messageEl.classList.add('visible'); + + setTimeout(() => { + messageEl.classList.remove('visible'); + }, 3000); + } +} + +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +// Touch support +let touchStartPos = { x: 0, y: 0 }; + +function onTouchStart(event) { + if (event.touches.length === 1) { + touchStartPos = { + x: event.touches[0].clientX, + y: event.touches[0].clientY + }; + previousMousePosition = touchStartPos; + isDragging = true; + } +} + +function onTouchMove(event) { + if (event.touches.length === 1 && isDragging) { + const touch = event.touches[0]; + const deltaX = touch.clientX - previousMousePosition.x; + const deltaY = touch.clientY - previousMousePosition.y; + + cameraRotation.y += deltaX * INTERACTION_CONFIG.rotationSpeed; + cameraRotation.x += deltaY * INTERACTION_CONFIG.rotationSpeed; + + cameraRotation.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, cameraRotation.x)); + + updateCameraPosition(); + + previousMousePosition = { + x: touch.clientX, + y: touch.clientY + }; + } +} + +function onTouchEnd(event) { + isDragging = false; +} + +// Export API +window.interactionAPI = { + init: initInteractions, + showMessage: showMessage +}; diff --git a/public/main.css b/public/main.css index 6c11c4431..8e7f45987 100644 --- a/public/main.css +++ b/public/main.css @@ -1,37 +1,636 @@ -@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600&display=swap"); +/* Mortéz World - Dark Luxury Styling */ + +/* CSS Reset & Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* Color Palette */ + --color-midnight: #0a0e27; + --color-deep-blue: #1a1f3a; + --color-jet-black: #0d0d0d; + --color-dark-gray: #2a2a2a; + --color-medium-gray: #4a4a4a; + --color-light-gray: #6a6a6a; + --color-amber: #ff9500; + --color-amber-light: #ffb347; + --color-red: #8b0000; + --color-red-light: #ff4444; + + /* Typography */ + --font-display: 'Playfair Display', serif; + --font-body: 'Inter', sans-serif; + + /* Spacing */ + --spacing-xs: 0.5rem; + --spacing-sm: 1rem; + --spacing-md: 1.5rem; + --spacing-lg: 2rem; + --spacing-xl: 3rem; + + /* Transitions */ + --transition-fast: 0.2s ease; + --transition-medium: 0.4s ease; + --transition-slow: 0.8s ease; +} body { - background-color: #0f2c41; - background-image: url("https://i.imgur.com/PFnfpkq.png"); - text-align: center; - font-weight: 700; + font-family: var(--font-body); + background-color: var(--color-midnight); + color: #ffffff; + overflow: hidden; + cursor: default; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* 3D Canvas */ +#canvas3d { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + display: block; +} + +/* Lightning Flash Effect */ +#lightning-flash { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #ffffff; + opacity: 0; + pointer-events: none; + z-index: 5; + transition: opacity 0.1s ease; +} + +/* Loading Screen */ +#loading-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--color-jet-black); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + transition: opacity 0.5s ease; +} + +.loading-content { + text-align: center; +} + +.loading-spinner { + width: 60px; + height: 60px; + border: 3px solid var(--color-dark-gray); + border-top-color: var(--color-amber); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto var(--spacing-md); +} + +.loading-text { + font-family: var(--font-display); + font-size: 1.2rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--color-light-gray); +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Intro Overlay */ +#intro-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(10, 14, 39, 0.95); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + transition: opacity 1s ease; +} + +.intro-content { + text-align: center; + z-index: 2; +} + +#intro-title { + font-family: var(--font-display); + font-size: clamp(3rem, 10vw, 8rem); + font-weight: 900; + letter-spacing: 0.15em; + text-transform: uppercase; + background: linear-gradient(135deg, #ffffff 0%, var(--color-amber-light) 50%, var(--color-red-light) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: var(--spacing-md); + opacity: 0; + text-shadow: 0 0 40px rgba(255, 149, 0, 0.3); + animation: titleGlow 3s ease-in-out infinite; +} + +#intro-subtitle { + font-family: var(--font-body); + font-size: clamp(1rem, 2vw, 1.5rem); + font-weight: 300; + letter-spacing: 0.3em; + text-transform: lowercase; + font-style: italic; + color: var(--color-light-gray); + margin-bottom: var(--spacing-xl); + opacity: 0; +} + +@keyframes titleGlow { + 0%, 100% { text-shadow: 0 0 40px rgba(255, 149, 0, 0.3); } + 50% { text-shadow: 0 0 60px rgba(255, 149, 0, 0.5), 0 0 80px rgba(255, 68, 68, 0.3); } +} + +#enter-button { + position: relative; + padding: var(--spacing-md) var(--spacing-xl); + font-family: var(--font-body); + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.2em; + text-transform: uppercase; + color: #ffffff; + background: transparent; + border: 2px solid var(--color-amber); + cursor: pointer; + overflow: hidden; + opacity: 0; + transition: all var(--transition-medium); } -#hello-wrapper { - color: #fff; - font-family: 'Source Sans Pro', sans-serif; +#enter-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, var(--color-amber), transparent); + transition: left 0.5s ease; +} + +#enter-button:hover { + border-color: var(--color-amber-light); + box-shadow: 0 0 30px rgba(255, 149, 0, 0.5); + transform: scale(1.05); } -.typewriter { - border-right: solid 5px transparent; - white-space: nowrap; - overflow: hidden; - font-family: 'Source Sans Pro', sans-serif; - font-size: 50px; - animation: - animated-text 1.2s steps(17, end) 1s 1 normal both +#enter-button:hover::before { + left: 100%; } -/* text animation */ -@keyframes animated-text { - from { +.button-glow { + position: absolute; + top: 50%; + left: 50%; width: 0; - } - to { + height: 0; + background: radial-gradient(circle, var(--color-amber) 0%, transparent 70%); + opacity: 0; + transform: translate(-50%, -50%); + pointer-events: none; +} + +#enter-button:hover .button-glow { + animation: buttonGlowPulse 1.5s ease-in-out infinite; +} + +@keyframes buttonGlowPulse { + 0%, 100% { width: 0; height: 0; opacity: 0; } + 50% { width: 200px; height: 200px; opacity: 0.3; } +} + +.button-ripple { + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 149, 0, 0.5); + transform: translate(-50%, -50%); + animation: ripple 0.6s ease-out; +} + +@keyframes ripple { + to { + width: 300px; + height: 300px; + opacity: 0; + } +} + +.skip-hint { + position: absolute; + bottom: var(--spacing-lg); + left: 50%; + transform: translateX(-50%); + font-size: 0.875rem; + color: var(--color-light-gray); + opacity: 0.6; + letter-spacing: 0.1em; +} + +/* Main UI Container */ +#ui-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + pointer-events: none; + opacity: 0; + transition: opacity 1s ease; +} + +#ui-container > * { + pointer-events: auto; +} + +/* Header */ +.main-header { + position: absolute; + top: 0; + left: 0; width: 100%; - } + padding: var(--spacing-lg) var(--spacing-xl); + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(180deg, rgba(10, 14, 39, 0.9) 0%, transparent 100%); + backdrop-filter: blur(10px); +} + +.logo h1 { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 900; + letter-spacing: 0.2em; + background: linear-gradient(135deg, #ffffff 0%, var(--color-amber) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin: 0; +} + +.logo-subtitle { + display: block; + font-family: var(--font-body); + font-size: 0.75rem; + font-weight: 300; + letter-spacing: 0.3em; + color: var(--color-light-gray); + margin-top: -0.25rem; +} + +.main-nav { + display: flex; + gap: var(--spacing-lg); +} + +.nav-link { + font-family: var(--font-body); + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.15em; + text-transform: uppercase; + color: #ffffff; + text-decoration: none; + position: relative; + transition: color var(--transition-fast); +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -5px; + left: 0; + width: 0; + height: 2px; + background: var(--color-amber); + transition: width var(--transition-medium); +} + +.nav-link:hover { + color: var(--color-amber); +} + +.nav-link:hover::after { + width: 100%; +} + +/* Interaction Message */ +.interaction-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: var(--spacing-md) var(--spacing-lg); + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 600; + letter-spacing: 0.1em; + text-align: center; + color: #ffffff; + background: rgba(10, 14, 39, 0.9); + border: 2px solid var(--color-amber); + box-shadow: 0 0 40px rgba(255, 149, 0, 0.3); + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-medium); + z-index: 50; +} + +.interaction-message.visible { + opacity: 1; +} + +/* Info Panel */ +.info-panel { + position: absolute; + top: 50%; + right: var(--spacing-lg); + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: var(--spacing-md); + max-width: 280px; +} + +.info-item { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md); + background: rgba(10, 14, 39, 0.8); + border: 1px solid var(--color-dark-gray); + border-left: 3px solid var(--color-amber); + backdrop-filter: blur(10px); + transition: all var(--transition-medium); +} + +.info-item:hover { + background: rgba(26, 31, 58, 0.9); + border-left-color: var(--color-amber-light); + transform: translateX(-5px); + box-shadow: 0 5px 20px rgba(255, 149, 0, 0.2); +} + +.info-icon { + font-size: 2rem; + filter: grayscale(0.3); +} + +.info-text h3 { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.25rem; + color: var(--color-amber-light); +} + +.info-text p { + font-size: 0.875rem; + color: var(--color-light-gray); + line-height: 1.4; +} + +/* Controls Info */ +.controls-info { + position: absolute; + bottom: var(--spacing-lg); + left: var(--spacing-lg); + padding: var(--spacing-md); + background: rgba(10, 14, 39, 0.8); + border: 1px solid var(--color-dark-gray); + backdrop-filter: blur(10px); + max-width: 250px; +} + +.controls-info h4 { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + margin-bottom: var(--spacing-sm); + color: var(--color-amber); + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.controls-info ul { + list-style: none; +} + +.controls-info li { + font-size: 0.875rem; + color: var(--color-light-gray); + margin-bottom: var(--spacing-xs); + line-height: 1.6; +} + +.controls-info strong { + color: #ffffff; + font-weight: 600; +} + +/* Audio Controls */ +.audio-controls { + position: absolute; + top: var(--spacing-lg); + right: var(--spacing-lg); +} + +.audio-btn { + width: 50px; + height: 50px; + background: rgba(10, 14, 39, 0.8); + border: 2px solid var(--color-dark-gray); + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-medium); + backdrop-filter: blur(10px); +} + +.audio-btn:hover { + border-color: var(--color-amber); + background: rgba(26, 31, 58, 0.9); + transform: scale(1.1); + box-shadow: 0 0 20px rgba(255, 149, 0, 0.3); +} + +.audio-icon { + font-size: 1.5rem; +} + +/* Footer */ +.main-footer { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: var(--spacing-lg) var(--spacing-xl); + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(0deg, rgba(10, 14, 39, 0.9) 0%, transparent 100%); + backdrop-filter: blur(10px); +} + +.tagline { + font-family: var(--font-display); + font-size: 1rem; + font-style: italic; + color: var(--color-light-gray); + letter-spacing: 0.05em; +} + +.social-links { + display: flex; + gap: var(--spacing-md); +} + +.social-link { + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-light-gray); + text-decoration: none; + transition: color var(--transition-fast); +} + +.social-link:hover { + color: var(--color-amber); +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .info-panel { + right: var(--spacing-sm); + max-width: 220px; + } + + .main-nav { + gap: var(--spacing-md); + } + + .nav-link { + font-size: 0.75rem; + } +} + +@media (max-width: 768px) { + .main-header { + flex-direction: column; + gap: var(--spacing-sm); + padding: var(--spacing-md); + } + + .main-nav { + gap: var(--spacing-sm); + } + + .info-panel { + display: none; + } + + .controls-info { + bottom: var(--spacing-sm); + left: var(--spacing-sm); + max-width: 200px; + font-size: 0.75rem; + } + + .main-footer { + flex-direction: column; + gap: var(--spacing-sm); + padding: var(--spacing-md); + text-align: center; + } + + #intro-title { + font-size: 3rem; + } + + #intro-subtitle { + font-size: 1rem; + } +} + +@media (max-width: 480px) { + .controls-info { + display: none; + } + + .audio-controls { + top: auto; + bottom: var(--spacing-sm); + right: var(--spacing-sm); + } + + #intro-title { + font-size: 2rem; + } +} + +/* Cursor Styles */ +body { + cursor: default; +} + +a, button { + cursor: pointer; +} + +/* Selection */ +::selection { + background: var(--color-amber); + color: var(--color-jet-black); +} + +/* Scrollbar (for any overflow) */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-jet-black); +} + +::-webkit-scrollbar-thumb { + background: var(--color-dark-gray); + border-radius: 4px; } -#particles-js { - height: 100vh; +::-webkit-scrollbar-thumb:hover { + background: var(--color-amber); } diff --git a/public/scene.js b/public/scene.js new file mode 100644 index 000000000..41ae36315 --- /dev/null +++ b/public/scene.js @@ -0,0 +1,479 @@ +// Mortéz World - Main 3D Scene +let scene, camera, renderer, composer; +let mansion, leftTower, rightTower; +let particles = []; +let lights = {}; +let clock = new THREE.Clock(); + +// Scene configuration +const SCENE_CONFIG = { + fogColor: 0x0a0e27, + fogNear: 10, + fogFar: 150, + ambientLightColor: 0x1a1f3a, + ambientLightIntensity: 0.3, + moonLightColor: 0x4a5f8f, + moonLightIntensity: 0.5 +}; + +function initScene() { + // Scene setup + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(SCENE_CONFIG.fogColor, SCENE_CONFIG.fogNear, SCENE_CONFIG.fogFar); + scene.background = new THREE.Color(0x0a0e27); + + // Camera setup - cinematic starting position + camera = new THREE.PerspectiveCamera( + 60, + window.innerWidth / window.innerHeight, + 0.1, + 1000 + ); + camera.position.set(0, 15, 80); + camera.lookAt(0, 10, 0); + + // Renderer setup + renderer = new THREE.WebGLRenderer({ + canvas: document.getElementById('canvas3d'), + antialias: true, + alpha: false + }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.8; + + // Lighting setup + setupLighting(); + + // Build the mansion and environment + buildMansion(); + buildTowers(); + buildEnvironment(); + createMistParticles(); + createGround(); + + // Window resize handler + window.addEventListener('resize', onWindowResize, false); +} + +function setupLighting() { + // Ambient light - dark and moody + const ambientLight = new THREE.AmbientLight( + SCENE_CONFIG.ambientLightColor, + SCENE_CONFIG.ambientLightIntensity + ); + scene.add(ambientLight); + + // Moonlight - main directional light + const moonLight = new THREE.DirectionalLight( + SCENE_CONFIG.moonLightColor, + SCENE_CONFIG.moonLightIntensity + ); + moonLight.position.set(-30, 50, 30); + moonLight.castShadow = true; + moonLight.shadow.mapSize.width = 2048; + moonLight.shadow.mapSize.height = 2048; + moonLight.shadow.camera.near = 0.5; + moonLight.shadow.camera.far = 200; + moonLight.shadow.camera.left = -50; + moonLight.shadow.camera.right = 50; + moonLight.shadow.camera.top = 50; + moonLight.shadow.camera.bottom = -50; + scene.add(moonLight); + lights.moon = moonLight; + + // Window lights - amber glow + createWindowLights(); +} + +function createWindowLights() { + const windowPositions = [ + { x: -12, y: 8, z: -2 }, + { x: -8, y: 8, z: -2 }, + { x: 8, y: 8, z: -2 }, + { x: 12, y: 8, z: -2 }, + { x: -15, y: 15, z: -2 }, + { x: 15, y: 15, z: -2 } + ]; + + lights.windows = []; + windowPositions.forEach((pos, index) => { + const light = new THREE.PointLight(0xff9500, 0.8, 15); + light.position.set(pos.x, pos.y, pos.z); + light.castShadow = false; + scene.add(light); + lights.windows.push({ + light: light, + baseIntensity: 0.8, + flickerSpeed: 0.5 + Math.random() * 0.5 + }); + + // Add glowing sphere for window + const glowGeometry = new THREE.SphereGeometry(0.3, 16, 16); + const glowMaterial = new THREE.MeshBasicMaterial({ + color: 0xff9500, + transparent: true, + opacity: 0.9 + }); + const glowSphere = new THREE.Mesh(glowGeometry, glowMaterial); + glowSphere.position.copy(light.position); + scene.add(glowSphere); + }); +} + +function buildMansion() { + const mansionGroup = new THREE.Group(); + + // Main mansion body - gothic architecture + const bodyGeometry = new THREE.BoxGeometry(30, 20, 15); + const bodyMaterial = new THREE.MeshStandardMaterial({ + color: 0x1a1a1a, + roughness: 0.9, + metalness: 0.1 + }); + const body = new THREE.Mesh(bodyGeometry, bodyMaterial); + body.position.y = 10; + body.castShadow = true; + body.receiveShadow = true; + mansionGroup.add(body); + + // Roof - steep gothic style + const roofGeometry = new THREE.ConeGeometry(18, 8, 4); + const roofMaterial = new THREE.MeshStandardMaterial({ + color: 0x0d0d0d, + roughness: 0.8, + metalness: 0.2 + }); + const roof = new THREE.Mesh(roofGeometry, roofMaterial); + roof.position.y = 24; + roof.rotation.y = Math.PI / 4; + roof.castShadow = true; + mansionGroup.add(roof); + + // Windows - dark with subtle glow + const windowGeometry = new THREE.BoxGeometry(2, 3, 0.5); + const windowMaterial = new THREE.MeshStandardMaterial({ + color: 0xff9500, + emissive: 0xff9500, + emissiveIntensity: 0.5, + roughness: 0.3, + metalness: 0.7 + }); + + const windowPositions = [ + { x: -12, y: 8 }, { x: -8, y: 8 }, { x: 8, y: 8 }, { x: 12, y: 8 }, + { x: -12, y: 14 }, { x: -8, y: 14 }, { x: 8, y: 14 }, { x: 12, y: 14 } + ]; + + windowPositions.forEach(pos => { + const window = new THREE.Mesh(windowGeometry, windowMaterial); + window.position.set(pos.x, pos.y, 7.6); + mansionGroup.add(window); + }); + + // Door - large gothic entrance + const doorGeometry = new THREE.BoxGeometry(4, 8, 0.5); + const doorMaterial = new THREE.MeshStandardMaterial({ + color: 0x2a1810, + roughness: 0.9, + metalness: 0.1 + }); + const door = new THREE.Mesh(doorGeometry, doorMaterial); + door.position.set(0, 4, 7.6); + door.userData = { interactive: true, type: 'door' }; + mansionGroup.add(door); + + // Door frame accent + const frameGeometry = new THREE.BoxGeometry(5, 9, 0.3); + const frameMaterial = new THREE.MeshStandardMaterial({ + color: 0x4a4a4a, + roughness: 0.6, + metalness: 0.4 + }); + const frame = new THREE.Mesh(frameGeometry, frameMaterial); + frame.position.set(0, 4.5, 7.7); + mansionGroup.add(frame); + + mansion = mansionGroup; + scene.add(mansion); +} + +function buildTowers() { + // Left tower - larger + const leftTowerGroup = new THREE.Group(); + + const leftBodyGeometry = new THREE.CylinderGeometry(4, 5, 35, 8); + const towerMaterial = new THREE.MeshStandardMaterial({ + color: 0x1a1a1a, + roughness: 0.9, + metalness: 0.1 + }); + const leftBody = new THREE.Mesh(leftBodyGeometry, towerMaterial); + leftBody.position.y = 17.5; + leftBody.castShadow = true; + leftBody.receiveShadow = true; + leftTowerGroup.add(leftBody); + + // Left tower roof + const leftRoofGeometry = new THREE.ConeGeometry(5, 8, 8); + const leftRoof = new THREE.Mesh(leftRoofGeometry, towerMaterial); + leftRoof.position.y = 39; + leftRoof.castShadow = true; + leftTowerGroup.add(leftRoof); + + // Tower windows + for (let i = 0; i < 3; i++) { + const windowGeometry = new THREE.BoxGeometry(1.5, 2, 0.3); + const windowMaterial = new THREE.MeshStandardMaterial({ + color: 0xff4444, + emissive: 0xff4444, + emissiveIntensity: 0.3 + }); + const window = new THREE.Mesh(windowGeometry, windowMaterial); + window.position.set(4.5, 10 + i * 8, 0); + leftTowerGroup.add(window); + + // Add point light for window + const light = new THREE.PointLight(0xff4444, 0.5, 10); + light.position.set(5, 10 + i * 8, 0); + leftTowerGroup.add(light); + } + + leftTowerGroup.position.set(-20, 0, -5); + leftTowerGroup.userData = { interactive: true, type: 'leftTower' }; + leftTower = leftTowerGroup; + scene.add(leftTower); + + // Right tower - smaller + const rightTowerGroup = new THREE.Group(); + + const rightBodyGeometry = new THREE.CylinderGeometry(3, 4, 25, 8); + const rightBody = new THREE.Mesh(rightBodyGeometry, towerMaterial); + rightBody.position.y = 12.5; + rightBody.castShadow = true; + rightBody.receiveShadow = true; + rightTowerGroup.add(rightBody); + + // Right tower roof + const rightRoofGeometry = new THREE.ConeGeometry(4, 6, 8); + const rightRoof = new THREE.Mesh(rightRoofGeometry, towerMaterial); + rightRoof.position.y = 28; + rightRoof.castShadow = true; + rightTowerGroup.add(rightRoof); + + // Tower windows + for (let i = 0; i < 2; i++) { + const windowGeometry = new THREE.BoxGeometry(1.2, 1.8, 0.3); + const windowMaterial = new THREE.MeshStandardMaterial({ + color: 0xff9500, + emissive: 0xff9500, + emissiveIntensity: 0.3 + }); + const window = new THREE.Mesh(windowGeometry, windowMaterial); + window.position.set(3.5, 8 + i * 8, 0); + rightTowerGroup.add(window); + + // Add point light for window + const light = new THREE.PointLight(0xff9500, 0.4, 8); + light.position.set(4, 8 + i * 8, 0); + rightTowerGroup.add(light); + } + + rightTowerGroup.position.set(20, 0, -3); + rightTowerGroup.userData = { interactive: true, type: 'rightTower' }; + rightTower = rightTowerGroup; + scene.add(rightTower); +} + +function buildEnvironment() { + // Dead trees + for (let i = 0; i < 8; i++) { + const tree = createDeadTree(); + const angle = (i / 8) * Math.PI * 2; + const radius = 40 + Math.random() * 20; + tree.position.set( + Math.cos(angle) * radius, + 0, + Math.sin(angle) * radius + ); + scene.add(tree); + } + + // Rocks and debris + for (let i = 0; i < 15; i++) { + const rock = createRock(); + rock.position.set( + (Math.random() - 0.5) * 100, + 0, + (Math.random() - 0.5) * 100 + ); + scene.add(rock); + } +} + +function createDeadTree() { + const treeGroup = new THREE.Group(); + + const trunkGeometry = new THREE.CylinderGeometry(0.3, 0.5, 8, 6); + const trunkMaterial = new THREE.MeshStandardMaterial({ + color: 0x2a2520, + roughness: 1 + }); + const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); + trunk.position.y = 4; + trunk.castShadow = true; + treeGroup.add(trunk); + + // Twisted branches + for (let i = 0; i < 3; i++) { + const branchGeometry = new THREE.CylinderGeometry(0.1, 0.2, 4, 4); + const branch = new THREE.Mesh(branchGeometry, trunkMaterial); + branch.position.set( + (Math.random() - 0.5) * 2, + 6 + Math.random() * 2, + (Math.random() - 0.5) * 2 + ); + branch.rotation.z = (Math.random() - 0.5) * Math.PI / 2; + branch.castShadow = true; + treeGroup.add(branch); + } + + return treeGroup; +} + +function createRock() { + const geometry = new THREE.DodecahedronGeometry(1 + Math.random() * 2, 0); + const material = new THREE.MeshStandardMaterial({ + color: 0x3a3a3a, + roughness: 0.9, + metalness: 0.1 + }); + const rock = new THREE.Mesh(geometry, material); + rock.castShadow = true; + rock.receiveShadow = true; + rock.rotation.set( + Math.random() * Math.PI, + Math.random() * Math.PI, + Math.random() * Math.PI + ); + return rock; +} + +function createGround() { + const groundGeometry = new THREE.PlaneGeometry(200, 200, 50, 50); + const groundMaterial = new THREE.MeshStandardMaterial({ + color: 0x1a1f1a, + roughness: 0.95, + metalness: 0.05 + }); + + const ground = new THREE.Mesh(groundGeometry, groundMaterial); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + + // Add some terrain variation + const positions = groundGeometry.attributes.position; + for (let i = 0; i < positions.count; i++) { + const y = Math.random() * 0.5; + positions.setY(i, y); + } + positions.needsUpdate = true; + groundGeometry.computeVertexNormals(); + + scene.add(ground); +} + +function createMistParticles() { + const particleCount = 1000; + const geometry = new THREE.BufferGeometry(); + const positions = new Float32Array(particleCount * 3); + const velocities = new Float32Array(particleCount * 3); + + for (let i = 0; i < particleCount; i++) { + positions[i * 3] = (Math.random() - 0.5) * 150; + positions[i * 3 + 1] = Math.random() * 30; + positions[i * 3 + 2] = (Math.random() - 0.5) * 150; + + velocities[i * 3] = (Math.random() - 0.5) * 0.1; + velocities[i * 3 + 1] = Math.random() * 0.05; + velocities[i * 3 + 2] = (Math.random() - 0.5) * 0.1; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3)); + + const material = new THREE.PointsMaterial({ + color: 0x6a7a9a, + size: 0.8, + transparent: true, + opacity: 0.3, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + + const particleSystem = new THREE.Points(geometry, material); + particleSystem.userData.velocities = velocities; + scene.add(particleSystem); + particles.push(particleSystem); +} + +function updateParticles() { + particles.forEach(system => { + const positions = system.geometry.attributes.position.array; + const velocities = system.userData.velocities; + + for (let i = 0; i < positions.length; i += 3) { + positions[i] += velocities[i]; + positions[i + 1] += velocities[i + 1]; + positions[i + 2] += velocities[i + 2]; + + // Reset particles that drift too far + if (positions[i + 1] > 30) positions[i + 1] = 0; + if (Math.abs(positions[i]) > 75) positions[i] = (Math.random() - 0.5) * 150; + if (Math.abs(positions[i + 2]) > 75) positions[i + 2] = (Math.random() - 0.5) * 150; + } + + system.geometry.attributes.position.needsUpdate = true; + }); +} + +function updateLighting() { + const time = clock.getElapsedTime(); + + // Flicker window lights + if (lights.windows) { + lights.windows.forEach((windowLight, index) => { + const flicker = Math.sin(time * windowLight.flickerSpeed + index) * 0.2 + 0.8; + windowLight.light.intensity = windowLight.baseIntensity * flicker; + }); + } +} + +function animate() { + requestAnimationFrame(animate); + + updateParticles(); + updateLighting(); + + renderer.render(scene, camera); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// Export for use in other modules +window.sceneAPI = { + init: initScene, + animate: animate, + getCamera: () => camera, + getScene: () => scene, + getMansion: () => mansion, + getLeftTower: () => leftTower, + getRightTower: () => rightTower +};