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 @@ + +
- + +