Newer
Older
rpg / v2 / packages / engine / src / render / __tests__ / weather.test.ts
/** Тесты чистой части погоды: кламп 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 };

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);
    });
});

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 });
    });
});