/**
* Сценарий 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) часы (scene:give) кладутся в сумку и видны в снапшоте;
* 6) сумка открыта — время заморожено (локация не тикается), закрыта — идёт;
* 7) инварианты чисты (time-bounded и прочие).
* Запуск: node tools/agent.mjs run tools/checks/time.mjs
*/
import { withChecks } from '../lib.mjs';
/** Ambient лугов и его ночной микс на середине заката (19:00). */
const AMB = { meadows: 0xd8dade, mix: 0xa1a6b3, night: 0x6a7288 };
export default async function ({ pretty }) {
return withChecks(
'time',
async (t) => {
const { c } = t;
await c.run('новая игра: старт 8:00, день', async () => {
await t.boot();
const s = await t.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 t.ctx.agent.command('scene:setTime', { hours: 8 }); // пин: точка отсчёта 480
await t.ctx.agent.step(600); // 600 тиков × (1/60 с) × 1 мин/с = 10 мин
const s = await t.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 t.ctx.agent.command('scene:setTime', { hours: 19 });
const w = await t.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 t.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 t.ctx.agent.command('scene:setTime', { hours: 21 });
const w = await t.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('часы в сумке: scene:give кладёт предмет', async () => {
await t.ctx.agent.command('scene:give', { id: 'clock' });
const s = await t.ctx.agent.snapshot();
c.expect(s.inventory.some((i) => i.id === 'clock' && i.count === 1), 'часов нет в снапшоте сумки', s.inventory);
return null;
});
await c.run('сумка: время заморожено, после закрытия — идёт', async () => {
await t.ctx.agent.command('scene:setTime', { hours: 10 }); // пин перед открытием
await t.ctx.agent.press('inventory'); // открыть сумку (атомарно)
await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 120 });
await t.ctx.agent.press('advance'); // применить часы (первый пункт) — строка в панели
await t.ctx.agent.step(120); // 2 с: в сумке локация не тикается
const s = await t.ctx.agent.snapshot();
c.expect(s.time.minutes === 600, `в сумке время уехало (${s.time.label})`, s.time);
await t.ctx.agent.press('inventory'); // закрыть сумку
const w = await t.ctx.agent.waitFor('s.time.minutes > 600', { timeoutTicks: 300 });
c.expect(w.ok, 'после закрытия сумки время не пошло', w.snapshot?.time);
return null;
});
await c.run('инварианты чисты при идущем времени', async () => {
await t.expectInvariantsClean('при идущем времени');
return null;
});
},
{ pretty }
);
}