/** Тесты чистой части погоды: кламп spec'а, вектор ветра, детерминизм и
* заворачивание в окно у слоёв дождя/снега, обесцвечивание облачности. */
import { describe, expect, it } from 'vitest';
import { cloudDim } from '../daynight';
import {
DEFAULT_WEATHER, WeatherSystem, clampWeather, rainPoints, snowPoints, windVector,
} from '../weather';
const WIN = { minX: 0, maxX: 48, minZ: 0, maxZ: 32 };
const RAIN_SPEED_TEST = 22; // базовая скорость капли (i=0 — без надбавки)
describe('clampWeather', () => {
it('частичный ввод дополняется дефолтом', () => {
const w = clampWeather({ rain: 0.3 });
expect(w.rain).toBe(0.3);
expect(w.snow).toBe(0);
expect(w.windStrength).toBe(0);
expect(w.windDir).toBe(DEFAULT_WEATHER.windDir);
expect(w.cloud).toBe(0);
});
it('выходит за 0..1 — клампится; NaN/не-число — дефолт', () => {
expect(clampWeather({ rain: 5 }).rain).toBe(1);
expect(clampWeather({ snow: -2 }).snow).toBe(0);
expect(clampWeather({ windStrength: NaN }).windStrength).toBe(0);
expect(clampWeather({ cloud: 'x' as unknown as number }).cloud).toBe(0);
});
});
describe('windVector', () => {
it('ноль силы — ноль дрейфа; сила 1 — 6 юнит/с по направлению', () => {
expect(windVector({ ...DEFAULT_WEATHER, windStrength: 0 })).toEqual([0, 0]);
const [x, z] = windVector({ ...DEFAULT_WEATHER, windStrength: 1, windDir: Math.PI / 2 });
expect(x).toBeCloseTo(6);
expect(z).toBeCloseTo(0);
const [x2, z2] = windVector({ ...DEFAULT_WEATHER, windStrength: 0.5, windDir: 0 });
expect(x2).toBeCloseTo(0);
expect(z2).toBeCloseTo(3);
});
});
describe('rain/snow points', () => {
// окно 48×32 = 1536 кв. юнитов; плотность дождя 0.016 → 25 капель,
// каждая — штрих из 3 точек (75); снег 0.007 → 11 хлопьев
it('число точек = плотность × площадь окна × интенсивность; ноль — пусто', () => {
const spec = { ...DEFAULT_WEATHER, rain: 1, snow: 0 };
expect(rainPoints(spec, 0, WIN).length).toBe(75);
expect(snowPoints({ ...spec, snow: 1 }, 0, WIN).length).toBe(11);
expect(rainPoints({ ...spec, rain: 0 }, 0, WIN).length).toBe(0);
expect(snowPoints({ ...spec, snow: 0 }, 0, WIN).length).toBe(0);
});
it('детерминизм: тот же t — те же позы', () => {
const spec = { ...DEFAULT_WEATHER, rain: 0.4, snow: 0.6, windStrength: 0.3 };
const a = rainPoints(spec, 3.7, WIN);
const b = rainPoints(spec, 3.7, WIN);
expect(a).toEqual(b);
expect(snowPoints(spec, 1.2, WIN)).toEqual(snowPoints(spec, 1.2, WIN));
});
it('время идёт — капли падают (y убывает, потом цикл)', () => {
const spec = { ...DEFAULT_WEATHER, rain: 1 };
const t0 = rainPoints(spec, 0, WIN)[0]!.pos[1];
const t1 = rainPoints(spec, 0.5, WIN)[0]!.pos[1];
expect(t1).toBeLessThan(t0);
});
it('позы внутри окна (wrap), альфа в 0..1', () => {
const spec = { ...DEFAULT_WEATHER, snow: 1, windStrength: 1 };
for (const p of snowPoints(spec, 17, WIN)) {
expect(p.pos[0]).toBeGreaterThanOrEqual(WIN.minX);
expect(p.pos[0]).toBeLessThanOrEqual(WIN.maxX);
expect(p.pos[2]).toBeGreaterThanOrEqual(WIN.minZ);
expect(p.pos[2]).toBeLessThanOrEqual(WIN.maxZ);
expect(p.alpha).toBeGreaterThanOrEqual(0);
expect(p.alpha).toBeLessThanOrEqual(1);
}
});
it('высота слоя: капли и снег не ниже земли цикла (0..TOP)', () => {
const spec = { ...DEFAULT_WEATHER, rain: 1, snow: 1 };
for (const p of rainPoints(spec, 13, WIN)) expect(p.pos[1]).toBeGreaterThanOrEqual(0);
for (const p of snowPoints(spec, 13, WIN)) expect(p.pos[1]).toBeGreaterThanOrEqual(0);
});
it('респавн меняет колонку: траектория не повторяется по циклам', () => {
// без ветра капля i=0 на цикле 0 и цикле+1 — на том же y (начало
// падения), но на другой колонке (sin-хэш от номера цикла)
const spec = { ...DEFAULT_WEATHER, rain: 1 };
const a = rainPoints(spec, 0, WIN)[0]!.pos;
const b = rainPoints(spec, 40 / RAIN_SPEED_TEST, WIN)[0]!.pos; // ровно один цикл
expect(b[1]).toBeCloseTo(a[1]);
expect(b[0]).not.toBeCloseTo(a[0], 3);
});
});
describe('cloudDim (облачность)', () => {
it('k=0 — цвет без изменений, k=1 — затемнение на долю dim', () => {
expect(cloudDim(0xf7f4ea, 0, 0.7, 0.25)).toBe(0xf7f4ea);
const c = cloudDim(0xf7f4ea, 1, 0.7, 0.25);
expect(c).toBeLessThan(0xf7f4ea);
});
it('обесцвечивание: каналы сходятся к серому при полной облачности', () => {
// насыщенный красный при sat=1 и dim=0 становится серым той же яркости
const c = cloudDim(0xff0000, 1, 1, 0);
const r = (c >> 16) & 255, g = (c >> 8) & 255, b = c & 255;
expect(r).toBe(g);
expect(g).toBe(b);
});
});
describe('WeatherSystem (без WebGL — только спецификация)', () => {
it('set клампит и current отдаёт нормализованную погоду', () => {
const sys = new WeatherSystem({
addParticles: () => ({ update: () => {}, setTint: () => {}, dispose: () => {} }),
setCloud: () => {},
visibleWindow: () => ({ minX: 0, maxX: 0, minZ: 0, maxZ: 0 }),
} as never);
sys.set({ rain: 5, windStrength: -1, cloud: 0.5 });
expect(sys.current.rain).toBe(1);
expect(sys.current.windStrength).toBe(0);
expect(sys.current.cloud).toBe(0.5);
expect(sys.info()).toEqual({ rainDrops: 0, snowFlakes: 0 }); // счётчики — после update
});
it('update считает фактические капли/снежинки от окна', () => {
const sys = new WeatherSystem({
addParticles: () => ({ update: () => {}, setTint: () => {}, dispose: () => {} }),
setCloud: () => {},
visibleWindow: () => ({ minX: 0, maxX: 48, minZ: 0, maxZ: 32 }),
} as never);
sys.set({ rain: 1 });
sys.update(1);
// окно погоды — след кадра + маржа 16: (48+32)×(32+32) = 5120 кв. юнитов
expect(sys.info()).toEqual({ rainDrops: 82, snowFlakes: 0 });
sys.set({ rain: 0, snow: 0 }); // пустая погода прячет слои и счётчики
expect(sys.info()).toEqual({ rainDrops: 0, snowFlakes: 0 });
});
});