diff --git a/app.js b/app.js index 051fe82..ddfbbd7 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,282 @@ -// ... existing code ... - -// === WEBSOCKET CLIENT === +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { wsClient } from './ws-client.js'; -// Initialize WebSocket client +// === COLOR PALETTE === +const NEXUS = { + colors: { + primary: 0x00ffcc, + secondary: 0x8800ff, + accent: 0xff8800, + bg: 0x000408, + nebula1: 0x0a0025, + nebula2: 0x00050f, + rayGold: 0xffaa44, + rayBlue: 0x88aaff, + rayCyan: 0x44ddff, + star: 0xffffff, + } +}; + +// === RENDERER === +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +renderer.toneMapping = THREE.ACESFilmicToneMapping; +renderer.toneMappingExposure = 0.9; +document.body.appendChild(renderer.domElement); + +// === SCENE === +const scene = new THREE.Scene(); +scene.background = new THREE.Color(NEXUS.colors.bg); +scene.fog = new THREE.FogExp2(NEXUS.colors.bg, 0.006); + +// === CAMERA === +const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 800); +camera.position.set(0, 8, 28); + +// === CONTROLS === +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; +controls.dampingFactor = 0.05; +controls.target.set(0, 4, 0); +controls.maxDistance = 120; +controls.minDistance = 2; + +// === LIGHTING === +const ambient = new THREE.AmbientLight(0x112233, 0.6); +scene.add(ambient); + +const coreLight = new THREE.PointLight(NEXUS.colors.primary, 3, 60); +coreLight.position.set(0, 8, 0); +scene.add(coreLight); + +// === STAR FIELD === +function createStarField() { + const count = 4000; + const positions = new Float32Array(count * 3); + const sizes = new Float32Array(count); + for (let i = 0; i < count; i++) { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + const r = 200 + Math.random() * 200; + positions[i * 3] = r * Math.sin(phi) * Math.cos(theta); + positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); + positions[i * 3 + 2] = r * Math.cos(phi); + sizes[i] = 0.3 + Math.random() * 0.7; + } + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); + const mat = new THREE.PointsMaterial({ + color: NEXUS.colors.star, + size: 0.4, + sizeAttenuation: true, + transparent: true, + opacity: 0.85, + }); + return new THREE.Points(geo, mat); +} +scene.add(createStarField()); + +// === VOLUMETRIC LIGHT RAYS === +// Cone-shaped shafts with additive blending simulating god rays from the skybox + +const LIGHT_RAY_VERT = /* glsl */` + varying vec2 vUv; + varying float vLocalY; + + void main() { + vUv = uv; + // Normalize local Y: 0.0 = bottom (wide base), 1.0 = top (tip/source) + vLocalY = clamp((position.y + 15.0) / 30.0, 0.0, 1.0); + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const LIGHT_RAY_FRAG = /* glsl */` + uniform float time; + uniform vec3 rayColor; + uniform float rayOpacity; + uniform float rayPhase; + + varying vec2 vUv; + varying float vLocalY; + + void main() { + // Soft radial fade using U coordinate around the circumference + // sin() creates a smooth bright-center / dark-edge per face + float radialFade = sin(vUv.x * 3.14159); + radialFade = pow(max(radialFade, 0.0), 0.6); + + // Length fade: bright at source (top/tip, vLocalY=1), fades at base + float lengthFade = pow(vLocalY, 0.35) * (1.0 - pow(vLocalY, 6.0) * 0.05); + + // Animated shimmer β€” ripples travel down the ray + float shimmer = 0.72 + 0.28 * sin(time * 1.6 + vLocalY * 7.0 + rayPhase); + + float alpha = radialFade * lengthFade * shimmer * rayOpacity; + + // Brighten core color slightly with shimmer + vec3 finalColor = rayColor * (1.0 + shimmer * 0.4); + + gl_FragColor = vec4(finalColor, alpha); + } +`; + +const lightRays = []; + +function createLightRay(pos, rotX, rotZ, color, opacity, phase) { + // Cone: tip (radiusTopβ‰ˆ0) at top (y=+15), base (radiusBottom=2.8) at bottom (y=-15) + const geo = new THREE.CylinderGeometry(0.05, 2.8, 30, 32, 8, true); + const mat = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 0.0 }, + rayColor: { value: new THREE.Color(color) }, + rayOpacity: { value: opacity }, + rayPhase: { value: phase }, + }, + vertexShader: LIGHT_RAY_VERT, + fragmentShader: LIGHT_RAY_FRAG, + transparent: true, + blending: THREE.AdditiveBlending, + depthWrite: false, + side: THREE.DoubleSide, + }); + + const mesh = new THREE.Mesh(geo, mat); + mesh.position.copy(pos); + mesh.rotation.x = rotX; + mesh.rotation.z = rotZ; + scene.add(mesh); + lightRays.push({ mesh, mat }); +} + +// Rays emanating from the upper skybox at varied angles and colors +const rayDefs = [ + // pos (x, y, z), rotX, rotZ, color, opacity, phase + { p: [-10, 26, -4], rx: 0.18, rz: 0.10, c: NEXUS.colors.rayGold, o: 0.20, ph: 0.0 }, + { p: [ 4, 30, -9], rx: -0.08, rz: -0.22, c: NEXUS.colors.rayGold, o: 0.16, ph: 1.1 }, + { p: [ 14, 24, 2], rx: 0.22, rz: 0.14, c: NEXUS.colors.rayBlue, o: 0.17, ph: 2.3 }, + { p: [-18, 28, 5], rx: -0.06, rz: 0.28, c: NEXUS.colors.rayGold, o: 0.13, ph: 0.7 }, + { p: [ 1, 32, -14], rx: 0.10, rz: -0.08, c: NEXUS.colors.rayCyan, o: 0.18, ph: 1.8 }, + { p: [ -6, 25, 12], rx: -0.20, rz: -0.10, c: NEXUS.colors.rayBlue, o: 0.15, ph: 3.1 }, + { p: [ 20, 29, -7], rx: 0.14, rz: 0.24, c: NEXUS.colors.rayGold, o: 0.12, ph: 0.4 }, + { p: [-12, 27, -9], rx: 0.05, rz: -0.16, c: NEXUS.colors.rayCyan, o: 0.14, ph: 2.6 }, + { p: [ 8, 23, 9], rx: -0.12, rz: 0.06, c: NEXUS.colors.rayGold, o: 0.11, ph: 1.5 }, +]; + +rayDefs.forEach(({ p, rx, rz, c, o, ph }) => { + createLightRay(new THREE.Vector3(...p), rx, rz, c, o, ph); +}); + +// === NEXUS CORE (floating icosahedron) === +const coreGeo = new THREE.IcosahedronGeometry(2.2, 1); +const coreMat = new THREE.MeshPhongMaterial({ + color: NEXUS.colors.primary, + emissive: NEXUS.colors.primary, + emissiveIntensity: 0.6, + wireframe: true, + transparent: true, + opacity: 0.85, +}); +const core = new THREE.Mesh(coreGeo, coreMat); +core.position.set(0, 5, 0); +scene.add(core); + +// Glow halo around core +const haloGeo = new THREE.SphereGeometry(3.2, 16, 16); +const haloMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.primary, + transparent: true, + opacity: 0.04, + side: THREE.BackSide, + blending: THREE.AdditiveBlending, + depthWrite: false, +}); +scene.add(new THREE.Mesh(haloGeo, haloMat)).position.set(0, 5, 0); + +// === GROUND PLANE === +const groundGeo = new THREE.CircleGeometry(40, 64); +const groundMat = new THREE.MeshPhongMaterial({ + color: 0x060618, + emissive: 0x010110, + shininess: 10, +}); +const ground = new THREE.Mesh(groundGeo, groundMat); +ground.rotation.x = -Math.PI / 2; +scene.add(ground); + +// === POST-PROCESSING (Unreal Bloom) === +const composer = new EffectComposer(renderer); +composer.addPass(new RenderPass(scene, camera)); +const bloom = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 0.9, // strength + 0.5, // radius + 0.82 // threshold +); +composer.addPass(bloom); + +// === RESIZE HANDLER === +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); + bloom.setSize(window.innerWidth, window.innerHeight); +}); + +// === LOADING SCREEN === +const loadingScreen = document.getElementById('loading-screen'); +const loadingProgress = document.getElementById('loading-progress'); + +function simulateLoad() { + let pct = 0; + const interval = setInterval(() => { + pct += Math.random() * 18; + if (pct >= 100) { + pct = 100; + clearInterval(interval); + setTimeout(() => loadingScreen.classList.add('hidden'), 300); + } + loadingProgress.style.width = pct + '%'; + }, 80); +} +simulateLoad(); + +// === ANIMATION LOOP === +const clock = new THREE.Clock(); + +function animate() { + requestAnimationFrame(animate); + + const t = clock.getElapsedTime(); + + // Update volumetric ray shaders + lightRays.forEach(({ mat }) => { + mat.uniforms.time.value = t; + }); + + // Rotate Nexus Core + core.rotation.y = t * 0.28; + core.rotation.x = t * 0.11; + + // Pulse the core point light + coreLight.intensity = 2.5 + 0.8 * Math.sin(t * 1.8); + + controls.update(); + composer.render(); +} + +animate(); + +// === WEBSOCKET INTEGRATION === wsClient.connect(); -// Handle WebSocket events window.addEventListener('player-joined', (event) => { console.log('Player joined:', event.detail); }); @@ -19,9 +289,23 @@ window.addEventListener('chat-message', (event) => { console.log('Chat message:', event.detail); }); -// Clean up on page unload window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); -// ... existing code ... +// === AUDIO TOGGLE === +const audioToggle = document.getElementById('audio-toggle'); +const ambientSound = document.getElementById('ambient-sound'); +if (audioToggle && ambientSound) { + audioToggle.addEventListener('click', () => { + if (ambientSound.paused) { + ambientSound.play().catch(() => {}); + audioToggle.textContent = 'πŸ”Š'; + audioToggle.classList.remove('muted'); + } else { + ambientSound.pause(); + audioToggle.textContent = 'πŸ”‡'; + audioToggle.classList.add('muted'); + } + }); +} diff --git a/index.html b/index.html index 34af931..6d1e80f 100644 --- a/index.html +++ b/index.html @@ -14,18 +14,44 @@ + + - + +
+
β—ˆ THE NEXUS
+
Timmy's Sovereign Space
+
-
- +
+
- + +
+
+
+ + +
+
+ + +
+
β—ˆ INITIALIZING NEXUS
+
+
+ + diff --git a/style.css b/style.css index a2aab15..3cdbd36 100644 --- a/style.css +++ b/style.css @@ -1,18 +1,186 @@ +/* === NEXUS DESIGN SYSTEM === */ +:root { + --color-bg: #000408; + --color-primary: #00ffcc; + --color-secondary: #8800ff; + --color-accent: #ff8800; + --color-text: #c0e8ff; + --color-text-muted: #446688; + --color-panel-bg: rgba(0, 10, 30, 0.75); + --color-panel-border: rgba(0, 200, 255, 0.25); + --font-body: 'Courier New', monospace; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-body); + overflow: hidden; + width: 100vw; + height: 100vh; +} + +canvas { + display: block; + position: fixed; + top: 0; + left: 0; +} + +/* === HUD === */ +#hud { + position: fixed; + top: 16px; + left: 16px; + z-index: 10; + pointer-events: none; +} + +#hud-title { + font-size: 18px; + font-weight: bold; + color: var(--color-primary); + text-shadow: 0 0 12px var(--color-primary); + letter-spacing: 3px; +} + +#hud-subtitle { + font-size: 11px; + color: var(--color-text-muted); + letter-spacing: 2px; + margin-top: 2px; +} + /* === AUDIO TOGGLE === */ +#audio-control { + position: fixed; + top: 8px; + right: 8px; + z-index: 10; +} + #audio-toggle { font-size: 14px; - background-color: var(--color-primary-primary); - color: var(--color-bg); + background-color: var(--color-panel-bg); + color: var(--color-primary); + border: 1px solid var(--color-panel-border); padding: 4px 8px; border-radius: 4px; font-family: var(--font-body); + cursor: pointer; transition: background-color 0.3s ease; } #audio-toggle:hover { - background-color: var(--color-secondary); + background-color: rgba(0, 255, 204, 0.15); } #audio-toggle.muted { - background-color: var(--color-text-muted); + color: var(--color-text-muted); +} + +/* === CHAT PANEL === */ +#chat-panel { + position: fixed; + bottom: 16px; + right: 16px; + width: 300px; + background: var(--color-panel-bg); + border: 1px solid var(--color-panel-border); + border-radius: 6px; + z-index: 10; + backdrop-filter: blur(8px); +} + +#chat-messages { + height: 150px; + overflow-y: auto; + padding: 8px; + font-size: 11px; + color: var(--color-text); + line-height: 1.5; +} + +#chat-input-row { + display: flex; + border-top: 1px solid var(--color-panel-border); +} + +#chat-input { + flex: 1; + background: transparent; + border: none; + color: var(--color-text); + font-family: var(--font-body); + font-size: 11px; + padding: 6px 8px; + outline: none; +} + +#chat-input::placeholder { + color: var(--color-text-muted); +} + +#chat-send { + background: transparent; + border: none; + border-left: 1px solid var(--color-panel-border); + color: var(--color-primary); + font-family: var(--font-body); + font-size: 11px; + padding: 6px 10px; + cursor: pointer; + transition: background 0.2s; +} + +#chat-send:hover { + background: rgba(0, 255, 204, 0.1); +} + +/* === LOADING SCREEN === */ +#loading-screen { + position: fixed; + inset: 0; + background: var(--color-bg); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 100; + transition: opacity 0.8s ease; +} + +#loading-screen.hidden { + opacity: 0; + pointer-events: none; +} + +#loading-text { + font-size: 20px; + letter-spacing: 6px; + color: var(--color-primary); + text-shadow: 0 0 20px var(--color-primary); + margin-bottom: 24px; +} + +#loading-bar { + width: 240px; + height: 2px; + background: rgba(0, 255, 204, 0.15); + border-radius: 1px; +} + +#loading-progress { + height: 100%; + background: var(--color-primary); + box-shadow: 0 0 8px var(--color-primary); + border-radius: 1px; + width: 0%; + transition: width 0.3s ease; }