diff --git a/docs/plan.md b/docs/plan.md index 55aed6d..1c2557c 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -748,11 +748,19 @@ `step >= 1`. Полноценную событийную систему (триггеры onEnter/onFlag → скрипты) не заводить до третьего кейса, который не ложится на квест-стадии. -- **Трек 21 — сборка**: сейв полного состояния, меню (подтверждено - фидбеком: пользователь заметил, что меню нет — игра стартует сразу в - мир), полный прогон, сравнительные скриншоты с v1 как референсом. - **Меню и сейв закрыты раньше срока** — раундом полировки (ниже); из - трека 21 осталось: полный прогон и сравнительные скриншоты с v1. +- **Трек 21 — сборка** (ЗАКРЫТ 2026-09-10): сейв полного состояния и меню + закрыты раньше срока раундом полировки (ниже). **Полный прогон**: + 11 гейтов демки + 6 гейтов игры — все зелёные на текущем коде, 295 + юнит-тестов. **Сравнительные скриншоты с v1** (зеркальные тулзы + `v1/apps/game/tools/compare-shot.mjs` и `v2/apps/game/tools/compare-shot.mjs`): + 5 общих участков (поляна лугов, пруды, Звенец у колодца, дом Ирвина, + роща у резонатора) — герой в одном тайле, день 10:00, листы попарно + v1|v2 (`/tmp/cmp_sheet_*.png`). Итог сверки: раскладка мира, объекты, + переходы и подсказки «E» совпадают 1:1 (карты конвертированы без + искажений); допустимые расхождения референса — v2-камера крупнее + (~2×, UNIT=3), тон тёмных областей светлее (пруды/роща/дом без + сумеречного затемнения v1), вода бледнее, интерьер дома беднее + деталями (полки/камин v1 — пропы строго в тайл 3×3). - **Раунд полировки** (2026-09-10, «улучшим игру технически и визуально»): недоделки визуального слоя трека 18 + техполировка. Меню (Esc) + автосейв/«Продолжить» (движковый SaveManager, localStorage `rpg2:`; diff --git a/v1/apps/game/tools/compare-shot.mjs b/v1/apps/game/tools/compare-shot.mjs new file mode 100644 index 0000000..bf7008b --- /dev/null +++ b/v1/apps/game/tools/compare-shot.mjs @@ -0,0 +1,77 @@ +/** + * Сравнительный просмотр с v2 (трек 21): те же участки мира, что снимает + * v2/apps/game/tools/compare-shot.mjs — герой в одном и том же тайле, день + * 10:00, снимок в файл. Тайлы участков общие (карты v2 конвертированы 1:1). + * + * node v1/apps/game/tools/compare-shot.mjs --dir /tmp + * → /tmp/cmp_v1_meadows.png, cmp_v1_ponds.png, ... (5 сегментов) + */ +import { fileURLToPath } from 'node:url'; +import { startDevServer, openGame } from './lib.mjs'; + +const dir = process.argv[process.argv.indexOf('--dir') + 1] ?? '/tmp'; + +const server = await startDevServer({ port: 5311 }); +let ctx = null; +try { + // Новая игра, врагов спим (мир для снимков), день 10:00. + ctx = await openGame({ url: server.url, newGame: true }); + const a = ctx.agent; + await a.command('scene:sleepAll', {}); + await a.command('scene:setTime', { hours: 10 }); + + const shot = async (name) => { + await a.waitFor('!s.transitioning', { timeoutTicks: 600 }); + await new Promise((r) => setTimeout(r, 400)); // дозреть фейду/камере + const path = `${dir}/cmp_v1_${name}.png`; + await a.screenshot(path); + console.log(`снимок: ${path}`); + }; + const area = (id) => a.waitFor(`s.area === "${id}"`, { timeoutTicks: 1200 }); + const goto = async (id) => { + const w = await area(id); + if (!w.ok) throw new Error(`не перешли в «${id}»`); + await a.waitFor('!s.transitioning', { timeoutTicks: 600 }); + }; + + // 1) Луга — стартовая поляна (спавн 16,28). + await shot('meadows'); + + // 2) Пруды: телепорт к тайлу-переходу (2,2), шаг на него. + await a.command('scene:teleport', { x: 3, y: 2 }); + if (!(await a.walkTo(2, 2, { timeoutTicks: 600 }))) throw new Error('не дошёл до перехода в пруды'); + await goto('ponds'); + await shot('ponds'); + + // 3) Звенец: шагом на тропу (54,28), внутри — к колодцу (16,14). + if (!(await a.walkTo(2, 2, { timeoutTicks: 600 }))) throw new Error('не дошёл до обратного перехода'); + await goto('meadows'); + await a.command('scene:teleport', { x: 53, y: 28 }); + if (!(await a.walkTo(54, 28, { timeoutTicks: 600 }))) throw new Error('не дошёл до тропы в Звенец'); + await goto('zvenets'); + await a.command('scene:teleport', { x: 16, y: 14 }); + await shot('zvenets'); + + // 4) Дом Ирвина: подойти к двери (13,12) и нажать действие. + const near = await a.approach(13, 12, { timeoutTicks: 600 }); + if (near === null) throw new Error('не подошёл к двери дома'); + a.press('interact'); + await goto('house_elder'); + await shot('house_elder'); + + // 5) Роща: выход из дома (5,8), тропа (54,20), внутри — к резонатору (24,16). + if (!(await a.walkTo(5, 8, { timeoutTicks: 600 }))) throw new Error('не вышел из дома'); + await goto('zvenets'); + await a.command('scene:teleport', { x: 53, y: 20 }); + if (!(await a.walkTo(54, 20, { timeoutTicks: 600 }))) throw new Error('не дошёл до выхода в рощу'); + await goto('rust_grove'); + await a.command('scene:teleport', { x: 24, y: 16 }); + await shot('rust_grove'); +} catch (e) { + console.error(e); + process.exitCode = 1; +} finally { + if (ctx) await ctx.browser.close(); + server.stop(); +} +process.exit(process.exitCode ?? 0); \ No newline at end of file diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index deee692..6899896 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -55,6 +55,7 @@ node apps/game/tools/checks/atmosphere.mjs # гейт времени суток/света/музыки игры (/tmp/v2_atmosphere_*.png) node apps/game/tools/checks/act2.mjs # гейт акта 2: резонатор/Машина/олени (/tmp/v2_act2_*.png) node apps/game/tools/checks/cutscenes.mjs # гейт катсцен: dry-run сцен, камера к герою (/tmp/v2_cutscene_*.png) +node apps/game/tools/compare-shot.mjs --dir /tmp # сравнительные снимки с v1 (5 участков, пары к v1/apps/game/tools/compare-shot.mjs) node apps/game/tools/shot.mjs --area rust_grove --at 24,16 --flags act2_hook --out /tmp/s.png # оперативный скриншот участка карты агентом npx vite --port 5299 # демка (в apps/demo), http://localhost:5299 @@ -731,6 +732,25 @@ пересоздания, z-fighting копланарных кубов над world-мешем, зомби-vite, `encodePng` Buffer, резолв `@rpg/engine` вне v2. +## Трек 21 (2026-09-10): полный прогон и сравнительные скриншоты с v1 + +- **Полный прогон** (сборка трека): 11 гейтов демки, затем 6 гейтов игры, + затем `npm run check:fast` (295 тестов). Гейты гонять по одному с паузой + и чисткой зомби-vite между прогонами (`ss -tlnp` → kill по PID). + Отчёты гейтов демки — многострочный JSON (`"ok": true`, с пробелом), + игры — однострочный (`"ok":true`); надёжный критерий — exit code. +- **Сравнительные скриншоты v1↔v2**: зеркальные тулзы + `v1/apps/game/tools/compare-shot.mjs` и `v2/apps/game/tools/compare-shot.mjs` + снимают 5 общих участков (поляна, пруды, Звенец, дом Ирвина, роща) — + герой в одном и том же тайле (карты v2 конвертированы 1:1, тайлы общие), + день 10:00, `/tmp/cmp_v1_*.png` / `/tmp/cmp_v2_*.png`. Листы попарно + склеиваются скриптом с `png.mjs`-кодеком движка (v1 слева, v2 справа). + Итог: раскладка/объекты/переходы/подсказки совпадают 1:1; допустимые + расхождения — камера v2 крупнее (~2×), тёмные области v2 светлее + (нет сумеречного затемнения v1), вода бледнее, интерьер беднее деталями. + v1-тулза умеет переходы пешком/телепортом к тайлу рядом с триггером + (телепорт точно на step-триггер не срабатывает — как в v2). + ## Рабочие привычки - Для задач из 3+ шагов веди список задач и иди автономно. diff --git a/v2/apps/game/tools/compare-shot.mjs b/v2/apps/game/tools/compare-shot.mjs new file mode 100644 index 0000000..9fc4ee6 --- /dev/null +++ b/v2/apps/game/tools/compare-shot.mjs @@ -0,0 +1,58 @@ +/** + * Сравнительный просмотр с v1 (трек 21): те же участки мира, что снимает + * v1/apps/game/tools/compare-shot.mjs — герой в одном и том же тайле, день + * 10:00, снимок в файл. Тайлы участков общие (карты v2 конвертированы 1:1). + * + * node v2/apps/game/tools/compare-shot.mjs --dir /tmp + * → /tmp/cmp_v2_meadows.png, cmp_v2_ponds.png, ... (5 сегментов) + */ +import { fileURLToPath } from 'node:url'; +import { AgentClient, launchBrowser, startDevServer } from '@rpg/engine/tools/agent-lib.mjs'; + +const dir = process.argv[process.argv.indexOf('--dir') + 1] ?? '/tmp'; +const port = 5312; + +const server = startDevServer({ port, cwd: fileURLToPath(new URL('../', import.meta.url)) }); +try { + await server.waitReady; + const browser = await launchBrowser(); + try { + const page = await browser.newPage(); + const pageErrors = []; + page.on('pageerror', (e) => pageErrors.push(String(e))); + await page.goto(`http://localhost:${port}/`, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForFunction('window.__beacon === true', { timeout: 60000 }); + await new Promise((r) => setTimeout(r, 1200)); + const client = new AgentClient(page); + // Страница стартует в меню; дальше — ручной режим, день 10:00, без пепла. + await client.call('startGame', false); + await client.call('setManual', true); + await client.call('setAsh', false); + await client.call('setFauna', false); + await client.call('setClock', 600); + + const shot = async (name, area, tile) => { + await client.call('gotoArea', area, tile); + await client.call('stepTicks', 10); // ручной режим: кадр рендерится только тут + const path = `${dir}/cmp_v2_${name}.png`; + await client.screenshot(path); + console.log(`снимок: ${path}`); + }; + + // Тайлы те же, что в v1-сценарии: спавн лугов, entry прудов, колодец + // Звенца, дом Ирвина, резонатор рощи. + await shot('meadows', 'meadows', { x: 16, y: 28 }); + await shot('ponds', 'ponds', { x: 4, y: 4 }); + await shot('zvenets', 'zvenets', { x: 16, y: 14 }); + await shot('house_elder', 'house_elder', { x: 5, y: 7 }); + await shot('rust_grove', 'rust_grove', { x: 24, y: 16 }); + const errs = pageErrors; + if (errs.length) console.error('pageerror:', errs.slice(0, 3)); + } finally { + await browser.close(); + } +} catch (e) { + console.error(e); + process.exit(1); +} +process.exit(0); // chromium-хэндлы держат event loop — выходим явно \ No newline at end of file