diff --git a/apps/game/src/data/locations.ts b/apps/game/src/data/locations.ts index b020236..f4bdd23 100644 --- a/apps/game/src/data/locations.ts +++ b/apps/game/src/data/locations.ts @@ -156,8 +156,23 @@ } ], atmosphere: 'ash', - // Светлый пепельный день: чуть холоднее белого. - lighting: { ambient: 0xd8dade }, + // Светлый пепельный день: чуть холоднее белого. Вечером (флаг evening, + // после сдачи квеста) — синие сумерки: лунные колокольчики светятся. + lighting: { + ambient: 0xd8dade, + nightAmbient: 0x6a7288, + sources: [ + { + id: 'bells', + tiles: [TILES.BELLFLOWER], + color: 0xf2cd68, + radius: 1.4, + intensity: 0.5, + flicker: 0.15, + whenFlag: FLAGS.evening + } + ] + }, interactables: INTERACTABLES.meadows, // Глобальный ветер: порывы свистят выше базового амбиента лугов. ambienceLayers: [{ key: 'ambience/wind', volume: 0.3 }], @@ -185,8 +200,8 @@ { tile: { x: 2, y: 2 }, target: { kind: 'area', area: 'meadows', entry: { x: 3, y: 3 } } } ], atmosphere: 'fog', - // Молочно-серый день у прудов. - lighting: { ambient: 0xc4cad2 }, + // Молочно-серый день у прудов; вечером — глухие синие сумерки. + lighting: { ambient: 0xc4cad2, nightAmbient: 0x767d92 }, // Вода прудов слышна у берега — тихий плеск поверх ветрового амбиента. // Глобальный ветер + вода у берегов (громкость — от дистанции). ambienceLayers: [ @@ -258,15 +273,17 @@ ], interactables: INTERACTABLES.zvenets, atmosphere: 'ash', - // Серые сумерки деревни: тёплые окна трёх домов; после звонка — - // холодный отсвет резонанса от башни (спек-синтез звука — её «жизнь»). + // Серые сумерки деревни; вечером — синие сумерки и тёплые окна трёх + // домов; после звонка — холодный отсвет резонанса от башни. lighting: { ambient: 0xdad6d0, + nightAmbient: 0x7b8298, sources: [ - // Окна — на теле спрайта дома (тело занимает тайл выше-левее базового). - { id: 'window_elder', at: { x: 6.5, y: 5.5 }, radius: 1.8, intensity: 0.45, flicker: 0.1 }, - { id: 'window_north', at: { x: 17.5, y: 4.5 }, radius: 1.8, intensity: 0.45, flicker: 0.1 }, - { id: 'window_shop', at: { x: 20.5, y: 8.5 }, radius: 1.8, intensity: 0.45, flicker: 0.1 }, + // Окна — на теле спрайта дома (тело занимает тайл выше-левее базового); + // днём не горят, зажигаются вечером. + { id: 'window_elder', at: { x: 6.5, y: 5.5 }, radius: 1.8, intensity: 0.45, flicker: 0.1, whenFlag: FLAGS.evening }, + { id: 'window_north', at: { x: 17.5, y: 4.5 }, radius: 1.8, intensity: 0.45, flicker: 0.1, whenFlag: FLAGS.evening }, + { id: 'window_shop', at: { x: 20.5, y: 8.5 }, radius: 1.8, intensity: 0.45, flicker: 0.1, whenFlag: FLAGS.evening }, { id: 'tower_resonance', at: { x: 14, y: 7 }, diff --git a/apps/game/tools/checks/lighting.mjs b/apps/game/tools/checks/lighting.mjs index 6d63d4c..570128d 100644 --- a/apps/game/tools/checks/lighting.mjs +++ b/apps/game/tools/checks/lighting.mjs @@ -1,20 +1,27 @@ /** - * Сценарий lighting — освещение сцены через мост: ambient областей, окна - * Звенца, очаг дома Ирвина (флаг квеста), лампа героя, мерцание. - * 1) Луга: светлый дневной ambient, источников нет (и лампы — улица светлая); - * 2) Звенец: серые сумерки, три окна домов, башня молчит (флага нет); - * 3) Дом Ирвина: тёмный интерьер, hearth_low (не hearth_high), лампа героя; - * мерцание очага — интенсивность гуляет по серии снапшотов; - * 4) телепорт внутри дома — лампа следует за героем (дистанция лампа↔очаг - * меняется; разность экранных координат камерой не компенсируется); - * 5) quest_bells_done: hearth_high вместо hearth_low и строго ярче; - * 6) инварианты чисты. + * Сценарий lighting — освещение сцены через мост: ambient областей, день/ночь + * (флаг evening), окна Звенца, очаг дома Ирвина (флаг квеста), лампа героя, + * мерцание, виньетка при низком hp. + * 1) Луга: светлый дневной ambient, источников нет; + * 2) evening: ночная цель ambient лугов, лампа героя на светлой днём улице; + * 3) Звенец: ночная цель деревни, три окна горят вечером, башня молчит; + * 4) Дом Ирвина: тёмный интерьер, hearth_low, лампа; мерцание очага гуляет; + * 5) телепорт внутри дома — лампа следует за героем (разность векторов); + * 6) quest_bells_done: hearth_high вместо hearth_low и строго ярче; + * 7) урон герою до hp 2 — виньетка темнеет (полный боевой путь); + * 8) инварианты чисты. * Запуск: node tools/agent.mjs run tools/checks/lighting.mjs */ import { startDevServer, openGame, Checks } from '../lib.mjs'; /** Ambient областей (те же константы, что в data/locations.ts). */ -const AMB = { meadows: 0xd8dade, zvenets: 0xdad6d0, house_elder: 0x54586a }; +const AMB = { + meadows: 0xd8dade, + meadowsNight: 0x6a7288, + zvenets: 0xdad6d0, + zvenetsNight: 0x7b8298, + house_elder: 0x54586a +}; /** Экранное смещение источника b относительно a (px). */ function lightVec(s, a, b) { @@ -34,19 +41,29 @@ const s = await ctx.agent.snapshot(); c.expect(s.lighting.ambient === AMB.meadows, 'ambient лугов не дневной', s.lighting); c.expect(s.lighting.sources.length === 0, 'на светлой улице не должно быть источников', s.lighting.sources); + c.expect(s.lighting.vignette === 0, 'виньетка не должна гореть на свежей игре', s.lighting); return null; }); - await c.run('Звенец: сумерки, три окна, башня молчит', async () => { + await c.run('вечер на лугах: ночная цель ambient, лампа героя', async () => { + await ctx.agent.command('scene:setFlag', { flag: 'evening' }); + const w = await ctx.agent.waitFor( + `s.lighting.ambient === ${AMB.meadowsNight} && s.lighting.sources.some((l) => l.id === "lamp")`, + { timeoutTicks: 300 } + ); + c.expect(w.ok, 'вечером луга не потемнели или лампа не зажглась', w.snapshot?.lighting); + return null; + }); + await c.run('Звенец вечером: ночная цель, три окна, башня молчит', async () => { await ctx.agent.walkTo(26, 14, { timeoutTicks: 3000 }); const w = await ctx.agent.waitFor('s.area === "zvenets"', { timeoutTicks: 600 }); c.expect(w.ok, 'не пришли в Звенец'); await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 }); const s = await ctx.agent.snapshot(); - c.expect(s.lighting.ambient === AMB.zvenets, 'ambient Звенца не сумеречный', s.lighting); + c.expect(s.lighting.ambient === AMB.zvenetsNight, 'ambient Звенца не ночной', s.lighting); const windows = s.lighting.sources.filter((l) => l.id.startsWith('window_')); - c.expect(windows.length === 3, 'окон домов должно быть три', s.lighting.sources); + c.expect(windows.length === 3, 'вечером должно гореть три окна домов', s.lighting.sources); c.expect(!s.lighting.sources.some((l) => l.id === 'tower_resonance'), 'башня светит без звонка', s.lighting.sources); - c.expect(!s.lighting.sources.some((l) => l.id === 'lamp'), 'на улице светлой деревни лампе героя не место', s.lighting.sources); + c.expect(s.lighting.sources.some((l) => l.id === 'lamp'), 'вечером на улице нет лампы героя', s.lighting.sources); return null; }); await c.run('дом Ирвина: тёмный интерьер, hearth_low, лампа героя', async () => { @@ -94,6 +111,12 @@ c.expect(high.intensity > 0.84 - 0.02, 'hearth_high не ярче тлеющего', high); return null; }); + await c.run('виньетка: урон до hp 2 — края экрана темнеют', async () => { + await ctx.agent.command('scene:damagePlayer', { value: 3 }); + const w = await ctx.agent.waitFor('s.lighting.vignette > 0.29', { timeoutTicks: 300 }); + c.expect(w.ok, 'виньетка не загорелась при hp 2', w.snapshot?.lighting); + return null; + }); await c.run('инварианты чисты при работающем свете', async () => { const inv = await ctx.agent.invariants(); const bad = inv.filter((i) => i.severity === 'error'); diff --git a/docs/art-style.md b/docs/art-style.md index 92b35e4..f1706d8 100644 --- a/docs/art-style.md +++ b/docs/art-style.md @@ -106,11 +106,15 @@ 5. **Механизмы**: ржавчина по нижним граням, сталь сверху. Пепел ложится на горизонтальные грани (P5 сверху рельсов). 6. **Анимация ходьбы**: 2 кадра (ноги в разнобой) + idle. Читается на 16×24 — крупность важнее плавности. 7. **Свет сцены — оверлей, не пиксели спрайта**: тёплые гало источников (`Lighting` - в `renderer.lightRoot`) рисуются поверх затемнения ступенчатыми кругами и в - палитру спрайтов не входят. Цвет света — из тёплой гаммы жизни (F1 `#f2b45a` - по умолчанию); холодный свет — только для неживого (резонанс башни — W2). - Интенсивность источника ≤ 1.1, радиус ≤ 5 юнитов; «золото — событие, а не - заливка» относится и к свету: гало — событие сцены, не фон. + в `renderer.lightRoot`) рисуются поверх затемнения ступенчатыми кругами + (4 ступени + дизер-кольцо 1px по внешнему радиусу — «мыло» не размывает + пиксельную сетку) и в палитру спрайтов не входят. Цвет света — из тёплой гаммы + жизни (F1 `#f2b45a` по умолчанию); холодный свет — только для неживого + (резонанс башни — W2). Ночной ambient областей — синие сумерки (`#6a7288` + и темнее), свет колокольчиков — B2 `#f2cd68`. Интенсивность источника ≤ 1.1, + радиус ≤ 5 юнитов; «золото — событие, а не заливка» относится и к свету: + гало — событие сцены, не фон. Виньетка (низкое hp, зона наката) и тёмные + ауры зон — multiply-затемнение краёв/пятна, не рисуются в спрайты. ## 4. Ассеты и чеклист diff --git a/docs/demo.md b/docs/demo.md index ccc16ce..639915a 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -26,6 +26,7 @@ | Tween + easing | хит-флэш, кольца резонанса, всплывающие тосты, кат-сцены | готово | | Particles (атмосфера + burst) | пепел лугов, туман прудов, удары/смерти сгустков | готово | | Lighting (ambient multiply + аддитивные источники) | сумерки Звенца и окна домов, тёмный интерьер с очагом, лампа героя в темноте, отсвет башни после звонка | готово | +| Lighting II (импульсы, день/ночь, виньетка, тёмные пятна) | вечер после сдачи квеста (ночной ambient + светящиеся колокольчики), окна гаснут днём, вспышки ударов/резонанса/урона, виньетка при низком hp и в накате, тёмные ауры зон наката | готово | | StateMachine | `EnemyBrain` — ИИ сгустков (dormant/rise/chase/…/sleep/dead) | готово | | ECS (World) | боевые сущности: враги и снаряды на `Entity` | готово | | Panel/Button/MenuList | меню, настройки, сейвы, сумка | готово | diff --git a/docs/plan.md b/docs/plan.md index c8c21b6..4be1985 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -27,6 +27,7 @@ | 8 | Синтез звука по запросу для агента (работа E, ниже) | ✅ E1–E4 сделаны (2026-09-07) | | 9 | Инфраструктура аудио + музыкальный слой (работы F/G, ниже) | ✅ сделаны (2026-09-07) | | 10 | Освещение: ambient multiply + аддитивные источники (движок L1, игра L2, контент L3) | ✅ сделано (2026-09-07); точки расширения: day/night через `setAmbient`-лерп, динамические источники `addLight/removeLight`, импульсы переосвещения — позже | +| 10b | Освещение II: импульсы (`pulseLight`/`pulseAmbient`), день/ночь по флагу `evening` (`nightAmbient` + лерп 6 c), виньетка (`setVignette`, низкий hp/накат), тёмные ауры зон (`upsertDarkSpot`), гало: 4 ступени + дизер-кольцо | ✅ сделано (2026-09-07): движок L1 + игра L2 (хуки боя, `scene:damagePlayer`) + контент L3 (ночь лугов/Звенца/прудов, свет колокольчиков, окна по `evening`) | Отложено: **IsoLayout как параметр игры** — `DEFAULT_ISO` в `math/iso.ts` остаётся единственной точкой входа; прокидывание конфига игры через ~15 мест движка не даёт