grechka-game/src3d/world/weather.js

221 lines
7.3 KiB
JavaScript

// ==================== ПОГОДА (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 };