/**
* Сценарий time — внутриигровое время суток через мост: старт, ход по тикам,
* кривая суток (закат), ночные источники/лампа, инварианты.
* Темп: 1 реальная секунда = 1 игровая минута, тик фиксированный 1/60 с —
* 600 шагов моста = ровно 10 игровых минут (детерминированно, без ожидания).
* 1) Новая игра: старт 8:00, фактор ночи 0;
* 2) перемотка 8:00 + 600 шагов → +10 минут (±1, float-хвост-floor);
* 3) 19:00 — середина заката: night ≈ 0.5, ambient — микс по фактическому
* фактору (время идёт и после пина, точное равенство неустойчиво);
* 4) 21:00 — ночь: night 1, лампа героя зажглась на светлой днём улице;
* 5) инварианты чисты (time-bounded и прочие).
* Запуск: node tools/agent.mjs run tools/checks/time.mjs
*/
import { startDevServer, openGame, Checks } from '../lib.mjs';
/** Ambient лугов и его ночной микс на середине заката (19:00). */
const AMB = { meadows: 0xd8dade, mix: 0xa1a6b3, night: 0x6a7288 };
export default async function ({ pretty }) {
const c = new Checks('time');
const server = await startDevServer();
let ctx;
try {
await c.run('новая игра: старт 8:00, день', async () => {
ctx = await openGame({ url: server.url, newGame: true });
await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
const s = await ctx.agent.snapshot();
c.expect(s.time.minutes >= 480 && s.time.minutes < 485, `старт не 8:00 (${s.time.label})`, s.time);
c.expect(s.time.night === 0, 'утром фактор ночи не нулевой', s.time);
c.expect(/^\d{1,2}:\d{2}$/.test(s.time.label), `label не часы (${s.time.label})`, s.time);
return null;
});
await c.run('ход времени: 600 шагов = +10 минут (±1)', async () => {
await ctx.agent.command('scene:setTime', { hours: 8 }); // пин: точка отсчёта 480
await ctx.agent.step(600); // 600 тиков × (1/60 с) × 1 мин/с = 10 мин
const s = await ctx.agent.snapshot();
const dt = s.time.minutes - 480;
c.expect(dt >= 9 && dt <= 11, `после 600 шагов прошло ${dt} мин (не ~10)`, s.time);
return null;
});
await c.run('закат 19:00: night ≈ 0.5, ambient — микс по фактору', async () => {
await ctx.agent.command('scene:setTime', { hours: 19 });
const w = await ctx.agent.waitFor('Math.abs(s.time.night - 0.5) < 0.02', { timeoutTicks: 300 });
c.expect(w.ok, 'на середине заката фактор ночи не ~0.5', w.snapshot?.time);
// Время продолжает идти (1 тик ≈ +1/60 мин) — сверяем ambient с миксом
// по фактическому фактору из того же снапшота (округление по каналам).
const s = await ctx.agent.snapshot();
const mix = (a, b, k) =>
[16, 8, 0].map((sh) => {
const ca = (a >> sh) & 255, cb = (b >> sh) & 255;
return Math.round(ca * (1 - k) + cb * k) << sh;
}).reduce((x, y) => x | y);
const expected = mix(AMB.meadows, AMB.night, s.time.night);
c.expect(s.lighting.ambient === expected, `ambient ${s.lighting.ambient.toString(16)} не микс ${expected.toString(16)}`, { ambient: s.lighting.ambient, night: s.time.night });
return null;
});
await c.run('ночь 21:00: night 1, ночная цель и лампа героя', async () => {
await ctx.agent.command('scene:setTime', { hours: 21 });
const w = await ctx.agent.waitFor(
`s.time.night === 1 && s.lighting.ambient === ${AMB.night} && s.lighting.sources.some((l) => l.id === "lamp")`,
{ timeoutTicks: 300 }
);
c.expect(w.ok, 'в 21:00 луга не ночные или лампа не зажглась', w.snapshot?.lighting);
return null;
});
await c.run('инварианты чисты при идущем времени', async () => {
const inv = await ctx.agent.invariants();
const bad = inv.filter((i) => i.severity === 'error');
c.expect(bad.length === 0, 'ошибки инвариантов', bad);
return null;
});
} finally {
await ctx?.browser?.close();
server.stop();
}
return c.finish({ pretty }).ok ? 0 : 1;
}