// ==================== ПОГОДА (biome-aware) ==================== import { state } from '../core/state.js'; import { isNight } from '../entities/mob-ai.js'; import { surfaceGyAt } from './generation.js'; import { getCachedBiome } from './generation.js'; import { SEA_GY } from '../core/constants.js'; // Возможные погоды по биому const BIOME_WEATHER = { snow: ['clear', 'snow', 'blizzard'], tundra: ['clear', 'snow', 'blizzard'], forest: ['clear', 'rain', 'storm'], plains: ['clear', 'rain'], desert: ['clear', 'clear', 'rain'], swamp: ['clear', 'rain', 'storm'], mountains: ['clear', 'rain', 'snow'] }; // Помощник интерполяции цветов function lerpColor(a, b, t) { const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16); const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16); const r = Math.round(ar + (br - ar) * t); const g = Math.round(ag + (bg - ag) * t); const bl = Math.round(ab + (bb - ab) * t); return '#' + ((1<<24)|(r<<16)|(g<<8)|bl).toString(16).slice(1); } // Цвета неба по биому, погоде и времени суток export function skyColor(night, biome, weatherType, worldTime) { function weatherSky() { switch (weatherType) { case 'blizzard': return '#8a9bae'; case 'storm': return '#3d4f5f'; case 'rain': return '#6B7B8D'; case 'snow': return '#b8c6d4'; case 'clear': return null; default: return null; } } const dayColor = (biome === 'desert') ? '#f4d9a0' : (biome === 'tundra' || biome === 'snow') ? '#c4d4e0' : (biome === 'swamp') ? '#4a6741' : '#87CEEB'; const dawnColor = '#FF9940'; const duskMidColor = '#FF6B35'; const duskEndColor = '#1a0533'; const nightColor = '#070816'; let skyColorResult; if (worldTime < 0.05) { const t = worldTime / 0.05; skyColorResult = lerpColor(dawnColor, dayColor, t); } else if (worldTime < 0.45) { skyColorResult = dayColor; } else if (worldTime < 0.55) { const t = (worldTime - 0.45) / 0.1; if (t < 0.5) { skyColorResult = lerpColor(dayColor, duskMidColor, t * 2); } else { skyColorResult = lerpColor(duskMidColor, duskEndColor, (t - 0.5) * 2); } } else { skyColorResult = nightColor; if (weatherType === 'blizzard') return '#1a1a2e'; if (weatherType === 'storm') return '#0a0a15'; return nightColor; } const wc = weatherSky(); if (wc) { const blendT = 0.6; skyColorResult = lerpColor(skyColorResult, wc, blendT); } return skyColorResult; } export function updateWeather(dt) { state.weatherTimer += dt; if (state.weatherTimer >= state.weatherChangeInterval) { state.weatherTimer = 0; state.weatherChangeInterval = 60 + Math.random() * 120; // Определяем биом по позиции игрока const playerGx = Math.floor(state.player.x / state.TILE); const biome = getCachedBiome(playerGx); const weathers = BIOME_WEATHER[biome] || ['clear']; const night = isNight(); const weights = weathers.map(w => { if (w === 'clear') return night ? 2 : 4; if (w === 'rain') return night ? 3 : 2; if (w === 'snow') return night ? 2 : 3; if (w === 'storm') return night ? 3 : 1; if (w === 'blizzard') return night ? 3 : 1; return 1; }); const total = weights.reduce((a, b) => a + b, 0); let r = Math.random() * total; state.weatherType = weathers[0]; for (let i = 0; i < weathers.length; i++) { r -= weights[i]; if (r <= 0) { state.weatherType = weathers[i]; break; } } state.isRaining = (state.weatherType === 'rain' || state.weatherType === 'storm'); } // Плавная интерполяция интенсивности дождя const rainTarget = (state.weatherType === 'rain' || state.weatherType === 'storm') ? 0.6 : 0; state.rainIntensity += (rainTarget - state.rainIntensity) * dt * 0.5; if (state.rainIntensity < 0.01) state.rainIntensity = 0; // Молния (только при storm) if (state.weatherType === 'storm') { state.lightningTimer -= dt; if (state.lightningTimer <= 0) { state.lightningFlash = 0.25; state.lightningTimer = 4 + Math.random() * 12; } } if (state.lightningFlash > 0) { state.lightningFlash -= dt; if (state.lightningFlash < 0) state.lightningFlash = 0; } // Снежинки (при snow / blizzard) if (state.weatherType === 'snow' || state.weatherType === 'blizzard') { const spawnCount = state.weatherType === 'blizzard' ? Math.floor(80 * dt) : Math.floor(30 * dt); const maxFlakes = state.weatherType === 'blizzard' ? 600 : 250; for (let i = 0; i < spawnCount && state.snowflakes.length < maxFlakes; i++) { state.snowflakes.push({ x: state.camX + Math.random() * state.W, y: state.camY - 20, vy: 40 + Math.random() * 60, vx: (state.weatherType === 'blizzard' ? -60 - Math.random() * 80 : -10 - Math.random() * 20), size: 2 + Math.random() * 3, wobble: Math.random() * Math.PI * 2 }); } } // Обновление снежинок for (let i = state.snowflakes.length - 1; i >= 0; i--) { const s = state.snowflakes[i]; s.y += s.vy * dt; s.x += s.vx * dt; s.wobble += dt * 3; s.x += Math.sin(s.wobble) * 15 * dt; if (s.y > state.camY + state.H + 20 || s.x < state.camX - 50 || s.x > state.camX + state.W + 50) { state.snowflakes.splice(i, 1); } } // Убираем снег при смене погоды if (state.weatherType !== 'snow' && state.weatherType !== 'blizzard' && !state.isRaining) { if (state.snowflakes.length > 0) { state.snowflakes.splice(0, Math.ceil(state.snowflakes.length * dt * 2)); } } } export function updateRain(dt) { if (!state.isRaining || state.rainIntensity < 0.01) { if (state.raindrops.length > 0) { state.raindrops.splice(0, Math.ceil(state.raindrops.length * dt * 3)); } return; } const spawnRate = Math.floor(state.rainIntensity * 60 * dt); for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) { state.raindrops.push({ x: state.camX + Math.random() * state.W, y: state.camY - 20, vy: 400 + Math.random() * 200, len: 8 + Math.random() * 12 }); } for (let i = state.raindrops.length - 1; i >= 0; i--) { const d = state.raindrops[i]; d.y += d.vy * dt; d.x -= 30 * dt; if (d.y > state.camY + state.H + 20) { state.raindrops.splice(i, 1); } } } export function drawWeather(ctx) { if (state.raindrops.length > 0) { ctx.save(); ctx.strokeStyle = 'rgba(174,194,224,0.5)'; ctx.lineWidth = 1.5; ctx.beginPath(); for (const d of state.raindrops) { ctx.moveTo(d.x, d.y); ctx.lineTo(d.x - 3, d.y + d.len); } ctx.stroke(); ctx.restore(); } if (state.snowflakes.length > 0) { ctx.save(); ctx.fillStyle = 'rgba(255,255,255,0.85)'; for (const s of state.snowflakes) { ctx.beginPath(); ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } if (state.lightningFlash > 0) { ctx.save(); ctx.fillStyle = `rgba(255,255,255,${Math.min(1, state.lightningFlash * 4)})`; ctx.fillRect(0, 0, state.W, state.H); ctx.restore(); } } export { skyColor };