diff --git a/app.js b/app.js index 23cc7d3..9532c9f 100644 --- a/app.js +++ b/app.js @@ -575,6 +575,97 @@ async function loadSovereigntyStatus() { loadSovereigntyStatus(); +// === WEATHER SYSTEM === +// Cycles: none → rain → snow → none (W key or HUD button) +let weatherState = 'none'; // 'none' | 'rain' | 'snow' + +const RAIN_COUNT = 2000; +const SNOW_COUNT = 600; +const WX = 30, WY_TOP = 16, WY_BOT = -6, WZ = 30; // particle volume + +// --- Rain particles --- +const rainPositions = new Float32Array(RAIN_COUNT * 3); +for (let i = 0; i < RAIN_COUNT; i++) { + rainPositions[i * 3] = (Math.random() - 0.5) * WX; + rainPositions[i * 3 + 1] = WY_BOT + Math.random() * (WY_TOP - WY_BOT); + rainPositions[i * 3 + 2] = (Math.random() - 0.5) * WZ; +} +const rainGeo = new THREE.BufferGeometry(); +rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3)); +const rainMat = new THREE.PointsMaterial({ + color: 0x8ab4f8, + size: 0.07, + sizeAttenuation: true, + transparent: true, + opacity: 0.65, +}); +const rainPoints = new THREE.Points(rainGeo, rainMat); +rainPoints.visible = false; +scene.add(rainPoints); + +// --- Snow particles --- +const snowPositions = new Float32Array(SNOW_COUNT * 3); +const snowDrift = new Float32Array(SNOW_COUNT); // per-flake drift phase +for (let i = 0; i < SNOW_COUNT; i++) { + snowPositions[i * 3] = (Math.random() - 0.5) * WX; + snowPositions[i * 3 + 1] = WY_BOT + Math.random() * (WY_TOP - WY_BOT); + snowPositions[i * 3 + 2] = (Math.random() - 0.5) * WZ; + snowDrift[i] = Math.random() * Math.PI * 2; +} +const snowGeo = new THREE.BufferGeometry(); +snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPositions, 3)); +const snowMat = new THREE.PointsMaterial({ + color: 0xeef4ff, + size: 0.2, + sizeAttenuation: true, + transparent: true, + opacity: 0.88, +}); +const snowPoints = new THREE.Points(snowGeo, snowMat); +snowPoints.visible = false; +scene.add(snowPoints); + +const weatherIndicator = document.getElementById('weather-indicator'); +const weatherLabel = document.getElementById('weather-label'); +const weatherToggleBtn = document.getElementById('weather-toggle'); + +function updateWeatherHUD() { + const labels = { none: '', rain: 'RAIN', snow: 'SNOW' }; + const icons = { none: '☁️', rain: '🌧️', snow: '❄️' }; + if (weatherIndicator) { + weatherIndicator.classList.toggle('visible', weatherState !== 'none'); + } + if (weatherLabel) weatherLabel.textContent = labels[weatherState]; + if (weatherToggleBtn) weatherToggleBtn.textContent = icons[weatherState]; +} + +function cycleWeather() { + if (weatherState === 'none') { + weatherState = 'rain'; + rainPoints.visible = true; + snowPoints.visible = false; + } else if (weatherState === 'rain') { + weatherState = 'snow'; + rainPoints.visible = false; + snowPoints.visible = true; + } else { + weatherState = 'none'; + rainPoints.visible = false; + snowPoints.visible = false; + } + updateWeatherHUD(); +} + +if (weatherToggleBtn) { + weatherToggleBtn.addEventListener('click', cycleWeather); +} + +document.addEventListener('keydown', (e) => { + if ((e.key === 'w' || e.key === 'W') && !e.metaKey && !e.ctrlKey) { + cycleWeather(); + } +}); + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -656,6 +747,32 @@ function animate() { sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; } + // Animate weather particles + if (weatherState === 'rain') { + const pos = rainGeo.attributes.position.array; + for (let i = 0; i < RAIN_COUNT; i++) { + pos[i * 3 + 1] -= 0.45; + if (pos[i * 3 + 1] < WY_BOT) { + pos[i * 3 + 1] = WY_TOP; + pos[i * 3] = (Math.random() - 0.5) * WX; + pos[i * 3 + 2] = (Math.random() - 0.5) * WZ; + } + } + rainGeo.attributes.position.needsUpdate = true; + } else if (weatherState === 'snow') { + const pos = snowGeo.attributes.position.array; + for (let i = 0; i < SNOW_COUNT; i++) { + pos[i * 3 + 1] -= 0.03; + pos[i * 3] += Math.sin(elapsed * 0.4 + snowDrift[i]) * 0.007; + if (pos[i * 3 + 1] < WY_BOT) { + pos[i * 3 + 1] = WY_TOP; + pos[i * 3] = (Math.random() - 0.5) * WX; + pos[i * 3 + 2] = (Math.random() - 0.5) * WZ; + } + } + snowGeo.attributes.position.needsUpdate = true; + } + composer.render(); } diff --git a/index.html b/index.html index 69d6b65..57b648a 100644 --- a/index.html +++ b/index.html @@ -33,9 +33,17 @@ + +
+ RAIN + [W] to change +
+
MAP VIEW [Tab] to exit diff --git a/style.css b/style.css index 8ccbc2d..0606bdb 100644 --- a/style.css +++ b/style.css @@ -184,6 +184,37 @@ body.photo-mode #overview-indicator { 100% { opacity: 0; transform: translate(-50%, -50%) scale(1); } } +/* === WEATHER INDICATOR === */ +#weather-indicator { + display: none; + position: fixed; + top: 8px; + left: 50%; + transform: translateX(-50%); + color: var(--color-primary); + font-family: var(--font-body); + font-size: 11px; + letter-spacing: 0.2em; + text-transform: uppercase; + pointer-events: none; + z-index: 20; + border: 1px solid var(--color-primary); + padding: 4px 10px; + background: rgba(0, 0, 8, 0.6); + white-space: nowrap; + animation: overview-pulse 2s ease-in-out infinite; +} + +#weather-indicator.visible { + display: block; +} + +.weather-hint { + margin-left: 12px; + color: var(--color-text-muted); + font-size: 10px; +} + /* === CRT / CYBERPUNK OVERLAY === */ .crt-overlay { position: fixed;