diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md
index e64cf40..257caa2 100644
--- a/v2/CLAUDE.md
+++ b/v2/CLAUDE.md
@@ -984,15 +984,22 @@
(14: герой и 3 NPC манекенами со скинами, дерево ×3/куст ×2/валун,
дом/колодец/башня/Машина, олень), клик — вьювер-оверлей `#devview`
(канвас + статы + ⏸/⏵ клипа + чекбокс поворота + селект скинов —
- перекраска манекена другой палитрой «посмотреть скин»). Мир стоит,
- пока открыта панель/вьювер (как в диалоге). Мост: `devInfo`
- (каталог), `devOpen`/`devOpenEntry(id)`/`devClose`, `devStats`.
+ перекраска манекена другой палитрой «посмотреть скин»). Время суток
+ (трек 31): секция сверху панели — метка, слайдер 0..1439 и пресеты
+ (8:00/12:00/18:54/22:00); перемотка дёргает `clock.set` + `lighting.update`
+ (свет сразу, не дожидаясь тика); панель — мир на паузе, время течёт после
+ закрытия. Мир стоит, пока открыта панель/вьювер (как в диалоге). Мост:
+ `devInfo` (каталог + метка часов), `devOpen`/`devOpenEntry(id)`/`devClose`,
+ `devSetClock(m)`, `devStats`.
Олень — VoxelModel из `deerVoxels` покоя (`gridFromVoxels`: список
- [x,y,z,цвет] → сетка с палитрой по уникальным цветам).
-- Гейт `tools/checks/panels.mjs` (21 проверка — 17-й гейт игры):
+ [x,y,z,цвет] → сетка с палитрой по уникальным цветам). Грабля: список
+ объектов выталкивал секцию времени за край панели — панель flex-колонка,
+ время сверху, `#dev-list` скроллится (`flex:1; min-height:0`).
+- Гейт `tools/checks/panels.mjs` (24 проверки — 17-й гейт игры):
лоад-скрин, 4 слота + автосейв, saveTo/loadFrom/saveDelete,
восстановление позиции, сумка (мир стоит, «Пусто.»), каталог ≥14,
- статы манекена/оленечья, кадр вьювера не пустой, закрытие.
+ статы манекена/оленечья, кадр вьювера не пустой, закрытие,
+ время суток (метка 18:54, закат 0.45 сразу).
Все 6 старых гейтов игры + 11 демок зелёные, **303 теста**
(+2: орбита и статы ModelViewer).
- Эффекты в вьювере (частицы/свет у объекта) — не делались, открытый
diff --git a/v2/apps/game/index.html b/v2/apps/game/index.html
index 5f6959e..cd6ba7f 100644
--- a/v2/apps/game/index.html
+++ b/v2/apps/game/index.html
@@ -160,10 +160,11 @@
border-left: 1px solid #44454e;
z-index: 14;
font: 14px monospace;
- overflow-y: auto;
+ display: flex; /* время сверху всегда видно, объекты скроллятся */
+ flex-direction: column;
}
#dev h2 { margin: 0 0 8px; color: #d99a32; font-size: 15px; letter-spacing: 1px; }
- #dev-list { display: flex; flex-direction: column; gap: 4px; }
+ #dev-list { display: flex; flex-direction: column; gap: 4px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
#dev-list button {
text-align: left;
padding: 5px 8px;
@@ -175,6 +176,16 @@
cursor: pointer;
}
#dev-list button:hover { border-color: #d99a32; color: #fff; }
+ /* Время суток: метка + слайдер + пресеты */
+ #dev-time { display: flex; flex-direction: column; gap: 6px; }
+ #dev-clock { color: #c9c9d2; font: 14px monospace; }
+ #dev-clock-slider { width: 100%; accent-color: #d99a32; }
+ #dev-presets { display: flex; gap: 4px; flex-wrap: wrap; }
+ #dev-presets button {
+ background: none; border: 1px solid #44454e; color: #c9c9d2;
+ font: 12px monospace; padding: 2px 6px; cursor: pointer;
+ }
+ #dev-presets button:hover { border-color: #d99a32; color: #fff; }
#devview {
position: fixed;
inset: 0;
@@ -302,7 +313,18 @@
-
ОБЪЕКТЫ · F3
+
ВРЕМЯ СУТОК
+
+
8:00
+
+
+
+
+
+
+
+
+
ОБЪЕКТЫ · F3
diff --git a/v2/apps/game/src/devview.ts b/v2/apps/game/src/devview.ts
index 59c2a6b..870bafe 100644
--- a/v2/apps/game/src/devview.ts
+++ b/v2/apps/game/src/devview.ts
@@ -126,7 +126,9 @@
/**
* Панель: DOM-список (#dev) + вьювер-оверлей (#devview). onOpenChange —
- * игра ставит мир на паузу (как в диалоге).
+ * игра ставит мир на паузу (как в диалоге). getMinutes/setMinutes — часы
+ * суток (слайдер и пресеты секции «Время суток»; мир на паузе, время
+ * течёт только после закрытия панели).
*/
export class DevPanel {
private viewer: ModelViewer | null = null;
@@ -136,6 +138,8 @@
constructor(
private readonly onOpenChange: (open: boolean) => void,
+ private readonly getMinutes: () => number,
+ private readonly setMinutes: (m: number) => void,
) {
document.getElementById('dev-open')!.addEventListener('click', () => this.closeViewer());
const playBtn = document.getElementById('dev-play')!;
@@ -152,6 +156,18 @@
const i = Number((e.target as HTMLSelectElement).value);
this.applySkin(i);
});
+ // Время суток: слайдер тянет часы сразу, пресеты — быстрая перемотка
+ const slider = document.getElementById('dev-clock-slider') as HTMLInputElement;
+ slider.addEventListener('input', () => {
+ this.setMinutes(Number(slider.value));
+ this.refreshClock();
+ });
+ for (const btn of document.querySelectorAll('#dev-presets button')) {
+ btn.addEventListener('click', () => {
+ this.setMinutes(Number(btn.dataset.min));
+ this.refreshClock();
+ });
+ }
}
private playing = true;
@@ -164,7 +180,7 @@
return el !== null && !el.hidden;
}
- /** Показать список объектов. */
+ /** Показать список объектов (время суток — синхронизировать с часами). */
openPanel(): void {
document.getElementById('dev')!.hidden = false;
const list = document.getElementById('dev-list')!;
@@ -175,9 +191,19 @@
btn.addEventListener('click', () => this.openViewer(e.id));
list.appendChild(btn);
}
+ this.refreshClock();
this.onOpenChange(true);
}
+ /** Слайдер/метка из текущих часов игры (время течёт вне панели). */
+ refreshClock(): void {
+ const m = this.getMinutes();
+ (document.getElementById('dev-clock-slider') as HTMLInputElement).value = String(m);
+ const h = Math.floor(m / 60);
+ const mm = m % 60;
+ document.getElementById('dev-clock')!.textContent = `${h}:${mm < 10 ? '0' : ''}${mm}`;
+ }
+
/** Закрыть всё (панель и вьювер). */
close(): void {
this.closeViewer();
diff --git a/v2/apps/game/src/main.ts b/v2/apps/game/src/main.ts
index fbcb776..3f6a749 100644
--- a/v2/apps/game/src/main.ts
+++ b/v2/apps/game/src/main.ts
@@ -691,11 +691,19 @@
bagEl.hidden = false;
}
-// ---- Дев-панель (F3): список объектов + вьювер движка ----
-const dev = new DevPanel((open) => {
- if (open) showHint('');
- updateHud();
-});
+// ---- Дев-панель (F3): список объектов + вьювер движка + время суток ----
+const dev = new DevPanel(
+ (open) => {
+ if (open) showHint('');
+ updateHud();
+ },
+ () => clock.minutes,
+ (m) => {
+ clock.set(m);
+ lighting?.update(); // свет пересчитать сразу, не дожидаясь тика
+ updateHud();
+ },
+);
/** Тост: всплывашка эффекта, уходит сама (реальное время). */
function showToast(text: string): void {
@@ -1371,9 +1379,16 @@
if (bagOpen && menuOpen) closeMenu();
renderBag();
},
- /** Дев-панель: каталог объектов (гейт). */
+ /** Дев-панель: каталог объектов + метка времени (гейт). */
devInfo() {
- return { open: dev.open, entries: devEntries() };
+ return { open: dev.open, entries: devEntries(), clock: clock.label };
+ },
+ /** Время суток из дев-панели агентом: перемотка + свет сразу. */
+ devSetClock(minutes: number): void {
+ clock.set(minutes);
+ lighting?.update();
+ dev.refreshClock();
+ updateHud();
},
/** Дев-панель агентом: открыть список / вьювер / закрыть. */
devOpen(): void {
diff --git a/v2/apps/game/tools/checks/panels.mjs b/v2/apps/game/tools/checks/panels.mjs
index c9cf3c4..c9660f0 100644
--- a/v2/apps/game/tools/checks/panels.mjs
+++ b/v2/apps/game/tools/checks/panels.mjs
@@ -126,6 +126,20 @@
await client.call('devClose');
await client.call('devClose'); // панель тоже
+ // ---- Время суток в дев-панели (слайдер/пресеты) ----
+ await client.call('devOpen');
+ await client.call('devSetClock', 1134); // пресет «18:54» — золотой час
+ const di2 = await client.call('devInfo');
+ const ci2 = await client.call('clockInfo');
+ check('дев-панель: метка времени 18:54 после перемотки',
+ di2.clock === '18:54' && ci2.label === '18:54', { dev: di2.clock, clock: ci2.label });
+ check('дев-панель: свет пересчитан сразу (закат 0.45)',
+ Math.abs(ci2.nightFactor - 0.45) < 0.01, ci2);
+ const shotTime = await client.screenshot('/tmp/v2_panels_devtime.png');
+ check('скриншот панели со временем снят (визуально)', shotTime.length > 0, shotTime);
+ await client.call('devSetClock', 480); // вернуть утро (дальнейшие проверки не темнеют)
+ await client.call('devClose');
+
const errs = client.errors();
check('нет pageerror за прогон', errs.length === 0, errs.slice(0, 3));
} finally {