diff --git a/app.js b/app.js index 051fe82..d28a8b6 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,193 @@ -// ... existing code ... - -// === WEBSOCKET CLIENT === +import * as THREE from 'three'; import { wsClient } from './ws-client.js'; -// Initialize WebSocket client +// === COLOR PALETTE === +const NEXUS = { + colors: { + bg: 0x0a0a1a, + primary: 0x00ffcc, + secondary: 0x7b2fff, + accent: 0xff6b35, + gridLine: 0x1a1a3a, + } +}; + +// === SCENE SETUP === +const scene = new THREE.Scene(); +scene.background = new THREE.Color(NEXUS.colors.bg); +scene.fog = new THREE.FogExp2(NEXUS.colors.bg, 0.025); + +const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); +camera.position.set(0, 5, 15); +camera.lookAt(0, 0, 0); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setPixelRatio(window.devicePixelRatio); +document.body.appendChild(renderer.domElement); + +// Lighting +const ambientLight = new THREE.AmbientLight(0x404060, 0.6); +scene.add(ambientLight); + +const dirLight = new THREE.DirectionalLight(0x00ffcc, 0.8); +dirLight.position.set(5, 10, 5); +scene.add(dirLight); + +// Grid floor +const gridHelper = new THREE.GridHelper(100, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine); +scene.add(gridHelper); + +// === WEATHER SYSTEM === +const WEATHER_MODES = ['none', 'rain', 'snow']; +let weatherModeIndex = 0; +let weatherPoints = null; +let weatherVelocities = null; +let currentWeatherMode = 'none'; + +function buildRainGeometry() { + const count = 3000; + const positions = new Float32Array(count * 3); + const velocities = new Float32Array(count); + + for (let i = 0; i < count; i++) { + positions[i * 3] = (Math.random() - 0.5) * 80; + positions[i * 3 + 1] = Math.random() * 50; + positions[i * 3 + 2] = (Math.random() - 0.5) * 80; + velocities[i] = 0.4 + Math.random() * 0.4; + } + + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + + const mat = new THREE.PointsMaterial({ + color: 0x99ccff, + size: 0.08, + transparent: true, + opacity: 0.6, + sizeAttenuation: true, + }); + + return { points: new THREE.Points(geo, mat), velocities }; +} + +function buildSnowGeometry() { + const count = 1500; + const positions = new Float32Array(count * 3); + // velocities: [xDrift, yFall] per particle, stored flat + const velocities = new Float32Array(count * 2); + + for (let i = 0; i < count; i++) { + positions[i * 3] = (Math.random() - 0.5) * 80; + positions[i * 3 + 1] = Math.random() * 50; + positions[i * 3 + 2] = (Math.random() - 0.5) * 80; + velocities[i * 2] = (Math.random() - 0.5) * 0.025; + velocities[i * 2 + 1] = 0.03 + Math.random() * 0.05; + } + + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + + const mat = new THREE.PointsMaterial({ + color: 0xffffff, + size: 0.18, + transparent: true, + opacity: 0.85, + sizeAttenuation: true, + }); + + return { points: new THREE.Points(geo, mat), velocities }; +} + +function setWeather(mode) { + if (weatherPoints) { + scene.remove(weatherPoints); + weatherPoints.geometry.dispose(); + weatherPoints.material.dispose(); + weatherPoints = null; + weatherVelocities = null; + } + + currentWeatherMode = mode; + + if (mode === 'rain') { + const { points, velocities } = buildRainGeometry(); + weatherPoints = points; + weatherVelocities = velocities; + scene.add(weatherPoints); + } else if (mode === 'snow') { + const { points, velocities } = buildSnowGeometry(); + weatherPoints = points; + weatherVelocities = velocities; + scene.add(weatherPoints); + } + + const btn = document.getElementById('weather-toggle'); + if (btn) { + const labels = { none: '⛅ Weather', rain: '🌧 Rain', snow: '❄ Snow' }; + btn.textContent = labels[mode]; + btn.dataset.mode = mode; + } +} + +function tickWeather() { + if (!weatherPoints || !weatherVelocities) return; + const pos = weatherPoints.geometry.attributes.position.array; + const count = pos.length / 3; + + if (currentWeatherMode === 'rain') { + for (let i = 0; i < count; i++) { + pos[i * 3 + 1] -= weatherVelocities[i]; + if (pos[i * 3 + 1] < 0) { + pos[i * 3 + 1] = 50; + pos[i * 3] = (Math.random() - 0.5) * 80; + pos[i * 3 + 2] = (Math.random() - 0.5) * 80; + } + } + } else if (currentWeatherMode === 'snow') { + for (let i = 0; i < count; i++) { + pos[i * 3] += weatherVelocities[i * 2]; + pos[i * 3 + 1] -= weatherVelocities[i * 2 + 1]; + if (pos[i * 3 + 1] < 0) { + pos[i * 3 + 1] = 50; + pos[i * 3] = (Math.random() - 0.5) * 80; + pos[i * 3 + 2] = (Math.random() - 0.5) * 80; + } + } + } + + weatherPoints.geometry.attributes.position.needsUpdate = true; +} + +// Weather toggle button +const weatherBtn = document.getElementById('weather-toggle'); +if (weatherBtn) { + weatherBtn.addEventListener('click', () => { + weatherModeIndex = (weatherModeIndex + 1) % WEATHER_MODES.length; + setWeather(WEATHER_MODES[weatherModeIndex]); + }); +} + +setWeather('none'); + +// === ANIMATION LOOP === +function animate() { + requestAnimationFrame(animate); + tickWeather(); + renderer.render(scene, camera); +} + +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); + +animate(); + +// === WEBSOCKET CLIENT === wsClient.connect(); -// Handle WebSocket events window.addEventListener('player-joined', (event) => { console.log('Player joined:', event.detail); }); @@ -19,9 +200,6 @@ window.addEventListener('chat-message', (event) => { console.log('Chat message:', event.detail); }); -// Clean up on page unload window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); - -// ... existing code ... diff --git a/index.html b/index.html index 34af931..4898656 100644 --- a/index.html +++ b/index.html @@ -14,18 +14,35 @@ + + - + +
+ +
+ + +
- -
- - + +
+ +
- + diff --git a/style.css b/style.css index a2aab15..605c6f6 100644 --- a/style.css +++ b/style.css @@ -1,18 +1,90 @@ +/* === DESIGN SYSTEM === */ +:root { + --color-bg: #0a0a1a; + --color-primary: #00ffcc; + --color-secondary: #7b2fff; + --color-accent: #ff6b35; + --color-text: #e0e0e0; + --color-text-muted: #666680; + --font-body: 'Courier New', monospace; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-body); + overflow: hidden; + width: 100vw; + height: 100vh; +} + +/* === HUD LAYOUT === */ +#hud { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; +} + +.hud-controls { + position: absolute; + top: 8px; + right: 8px; + pointer-events: all; +} + +.hud-controls--left { + right: auto; + left: 8px; +} + +/* === HUD BUTTON === */ +.hud-btn { + background-color: rgba(0, 255, 204, 0.15); + color: var(--color-primary); + border: 1px solid var(--color-primary); + padding: 5px 10px; + border-radius: 4px; + font-size: 12px; + font-family: var(--font-body); + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; + white-space: nowrap; +} + +.hud-btn:hover { + background-color: rgba(0, 255, 204, 0.3); +} + /* === AUDIO TOGGLE === */ #audio-toggle { font-size: 14px; - background-color: var(--color-primary-primary); - color: var(--color-bg); - padding: 4px 8px; - border-radius: 4px; - font-family: var(--font-body); - transition: background-color 0.3s ease; -} - -#audio-toggle:hover { - background-color: var(--color-secondary); } #audio-toggle.muted { - background-color: var(--color-text-muted); + background-color: rgba(102, 102, 128, 0.15); + color: var(--color-text-muted); + border-color: var(--color-text-muted); +} + +/* === WEATHER TOGGLE === */ +#weather-toggle[data-mode="rain"] { + background-color: rgba(153, 204, 255, 0.2); + color: #99ccff; + border-color: #99ccff; +} + +#weather-toggle[data-mode="snow"] { + background-color: rgba(255, 255, 255, 0.15); + color: #ffffff; + border-color: #ffffff; }