import { describe, expect, it } from 'vitest';
import {
DEFAULT_DAY_NIGHT,
GLOW_BASE_PX,
dayNightFactor,
dimColor,
flickerFactor,
lightFrame,
lerpAmbient,
pulseEnvelope,
pulseTotal
} from '../lightSim';
describe('flickerFactor', () => {
it('детерминирован: одинаковые (time, seed) — одинаковый результат', () => {
const a = flickerFactor(1.234, 0.37, 0.3, 0.9);
const b = flickerFactor(1.234, 0.37, 0.3, 0.9);
expect(a).toBe(b);
});
it('разные seed дают разные значения', () => {
const a = flickerFactor(0.5, 0.1, 0.3, 0.9);
const b = flickerFactor(0.5, 0.9, 0.3, 0.9);
expect(a).not.toBe(b);
});
it('в диапазоне [1-amount, 1] на сетке времени', () => {
for (let i = 0; i < 180; i++) {
const f = flickerFactor(i / 60, 0.25, 0.35, 0.9);
expect(f).toBeGreaterThanOrEqual(1 - 0.35 - 1e-9);
expect(f).toBeLessThanOrEqual(1 + 1e-9);
}
});
it('amount = 0 — ровный свет', () => {
for (let i = 0; i < 60; i++) {
expect(flickerFactor(i / 60, 0.7, 0, 0.9)).toBe(1);
}
});
});
describe('lightFrame', () => {
it('дефолты: intensity 1, radius 2*GLOW_BASE_PX', () => {
const f = lightFrame({ id: 'l', x: 10, y: 20, color: 0xf2b45a }, 0);
expect(f.tint).toBe(0xf2b45a);
expect(f.scale).toBe(2);
expect(f.alpha).toBeGreaterThan(0);
expect(f.alpha).toBeLessThanOrEqual(1);
expect(f.x).toBe(10);
expect(f.y).toBe(20);
});
it('alpha = intensity * flickerFactor, scale = radius / GLOW_BASE_PX', () => {
const def = { id: 'l', x: 0, y: 0, color: 0xffaa00, intensity: 1.5, radius: 64, flicker: 0, seed: 0 };
const f = lightFrame(def, 1);
expect(f.alpha).toBeCloseTo(1.5, 10);
expect(f.scale).toBe(64 / GLOW_BASE_PX);
// alpha клампится в адаптере, но чистый кадр может превышать 1
expect(f.alpha).toBe(1.5);
});
it('выключенный источник — alpha 0', () => {
const f = lightFrame({ id: 'l', x: 0, y: 0, color: 0xffffff, enabled: false }, 0);
expect(f.alpha).toBe(0);
});
it('мерцание меняет alpha между шагами', () => {
const def = { id: 'l', x: 0, y: 0, color: 0xffffff, intensity: 1, flicker: 0.5 };
const a1 = lightFrame(def, 0).alpha;
const a2 = lightFrame(def, 0.37).alpha;
expect(a1).not.toBe(a2);
});
});
describe('pulseEnvelope / pulseTotal', () => {
it('трапеция: рост, плато, спад, нули за пределами', () => {
const spec = { attack: 0.1, hold: 0.2, decay: 0.3 };
expect(pulseEnvelope(0, spec)).toBe(0);
expect(pulseEnvelope(0.05, spec)).toBeCloseTo(0.5, 10);
expect(pulseEnvelope(0.15, spec)).toBe(1);
expect(pulseEnvelope(0.25, spec)).toBe(1);
expect(pulseEnvelope(0.45, spec)).toBeCloseTo(0.5, 10);
expect(pulseEnvelope(0.7, spec)).toBe(0);
expect(pulseTotal(spec)).toBeCloseTo(0.6, 10);
});
it('без hold — пик сразу после attack', () => {
const spec = { attack: 0.1, decay: 0.2 };
expect(pulseEnvelope(0.1, spec)).toBe(1);
expect(pulseEnvelope(0.2, spec)).toBeCloseTo(0.5, 10);
expect(pulseTotal(spec)).toBeCloseTo(0.3, 10);
});
it('attack = 0 — мгновенный пик', () => {
const spec = { attack: 0, decay: 0.2 };
expect(pulseEnvelope(0, spec)).toBe(0);
expect(pulseEnvelope(0.1, spec)).toBeCloseTo(0.5, 10);
expect(pulseEnvelope(0.2, spec)).toBe(0);
expect(pulseTotal(spec)).toBeCloseTo(0.2, 10);
});
it('отрицательное время и значения вне диапазона — 0', () => {
const spec = { attack: 0.1, decay: 0.2 };
expect(pulseEnvelope(-0.5, spec)).toBe(0);
expect(pulseEnvelope(10, spec)).toBe(0);
});
});
describe('lerpAmbient', () => {
it('краи и середина по каналам', () => {
expect(lerpAmbient(0x000000, 0xffffff, 0)).toBe(0x000000);
expect(lerpAmbient(0x000000, 0xffffff, 1)).toBe(0xffffff);
expect(lerpAmbient(0xff0000, 0x00ff00, 0.5)).toBe(0x808000); // round(127.5) = 128
});
});
describe('dimColor', () => {
it('краи: полная яркость и ноль', () => {
expect(dimColor(0x54586a, 1)).toBe(0x54586a);
expect(dimColor(0x54586a, 0)).toBe(0x000000);
});
it('монотонность по яркости', () => {
let prev = 0;
for (let k = 0; k <= 1.001; k += 0.1) {
const c = dimColor(0x808080, k);
expect(c).toBeGreaterThanOrEqual(prev);
prev = c;
}
});
});
describe('dayNightFactor', () => {
it('день — 0', () => {
expect(dayNightFactor(8)).toBe(0);
expect(dayNightFactor(12)).toBe(0);
expect(dayNightFactor(17.9)).toBe(0);
});
it('закат 18–20 — линейный рост', () => {
expect(dayNightFactor(18)).toBe(0);
expect(dayNightFactor(19)).toBeCloseTo(0.5, 10);
expect(dayNightFactor(20)).toBe(1);
});
it('ночь — 1', () => {
expect(dayNightFactor(23)).toBe(1);
expect(dayNightFactor(0)).toBe(1);
expect(dayNightFactor(5.9)).toBe(1);
});
it('рассвет 6–8 — линейный спад', () => {
expect(dayNightFactor(6)).toBe(1);
expect(dayNightFactor(7)).toBeCloseTo(0.5, 10);
expect(dayNightFactor(8)).toBe(0);
});
it('стык 24/0 непрерывен, отрицательные и большие часы ок', () => {
expect(dayNightFactor(24)).toBe(1);
expect(dayNightFactor(25)).toBe(dayNightFactor(1));
expect(dayNightFactor(-4)).toBe(1);
expect(dayNightFactor(-16)).toBe(0); // -16 ≡ 8
});
it('монотонность на переходах', () => {
for (let t = 18; t < 20; t += 0.1) {
expect(dayNightFactor(t + 0.1)).toBeGreaterThanOrEqual(dayNightFactor(t));
}
for (let t = 6; t < 8; t += 0.1) {
expect(dayNightFactor(t + 0.1)).toBeLessThanOrEqual(dayNightFactor(t));
}
});
it('кастомный spec', () => {
const spec = { dawn: 0, day: 1, dusk: 10, night: 12 };
expect(dayNightFactor(5, spec)).toBe(0);
expect(dayNightFactor(11, spec)).toBeCloseTo(0.5, 10);
expect(dayNightFactor(12, spec)).toBe(1);
expect(DEFAULT_DAY_NIGHT.dusk).toBe(18); // дефолт не тронут
});
});