221 lines
7.3 KiB
JavaScript
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 };
|