feat: add static lightmap baking, dynamic lighting, player flashlight, and v5 format support

This commit is contained in:
Artvasil09 2026-10-24 19:10:49 +03:00
parent c14ae884aa
commit 1efa1195cf
35 changed files with 2003 additions and 54 deletions

View file

@ -137,7 +137,7 @@ invoke DrawNumber8x8, 120, 42, playerHealth, COLOR_WHITE
The engine supports both procedural default generation and binary level loading. When `assets/map.dat` exists, `LoadMapBinary` in `include/engine_map.inc` automatically loads it: The engine supports both procedural default generation and binary level loading. When `assets/map.dat` exists, `LoadMapBinary` in `include/engine_map.inc` automatically loads it:
- **Header (`MapHeader`, 38 bytes, packed)**: - **Header (`MapHeader`, 38 bytes, packed)**:
- `magic` (uint32): `0x4E4C4245` ("EBLN") - `magic` (uint32): `0x4E4C4245` ("EBLN")
- `version` (uint16): `4` (readers also accept v1 fixed sprite coordinates and v2/v3 raw-coordinate maps, including early v2 maps with transitions) - `version` (uint16): `5` (readers also accept v1 fixed sprite coordinates and v2/v3/v4 raw-coordinate maps, including early v2 maps with transitions)
- `mapWidth` (uint16): `32` - `mapWidth` (uint16): `32`
- `mapHeight` (uint16): `32` - `mapHeight` (uint16): `32`
- `playerStartX` (uint16): cell X coordinate (e.g. 2) - `playerStartX` (uint16): cell X coordinate (e.g. 2)
@ -147,16 +147,17 @@ The engine supports both procedural default generation and binary level loading.
- `ceilingColor` (uint32): 00RRGGBBh - `ceilingColor` (uint32): 00RRGGBBh
- `floorColor` (uint32): 00RRGGBBh - `floorColor` (uint32): 00RRGGBBh
- `transitionCount` (uint16): 0..16 (v1 ignores this formerly reserved field) - `transitionCount` (uint16): 0..16 (v1 ignores this formerly reserved field)
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4 only) - `cutsceneCount` (uint16): 0..128 commands (offset 28, v4+)
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4 only) - `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4+)
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4 only) - `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4+)
- `reserved[4]` (bytes): reserved padding - `lightCount` (uint16): 0..13 authored sources (offset 34, v5 only)
- `lightingReserved` (uint16): zero (offset 36, v5 only)
- **Grid Data**: `32 * 32 = 1024` bytes (row-major order). Cell value = texture slot (1 brick, 2 wood, 3 stone, 4 metal, 5 door, 7 pillar, 8 glass, 9 marble). - **Grid Data**: `32 * 32 = 1024` bytes (row-major order). Cell value = texture slot (1 brick, 2 wood, 3 stone, 4 metal, 5 door, 7 pillar, 8 glass, 9 marble).
- **Sprite Array**: `spriteCount * 24` bytes (`SpriteStruct`: `x`, `y` in raw world units, `textureId`, `scale` 16.16, `dist`, `flags`). - **Sprite Array**: `spriteCount * 24` bytes (`SpriteStruct`: `x`, `y` in raw world units, `textureId`, `scale` 16.16, `dist`, `flags`).
- **Transition Array**: `transitionCount * 42` bytes (`LevelTransition` in both `engine_defs.inc` and `editor/src/level_format.h`). Target names are ASCII basenames, 5..31 bytes including `.dat`, with a terminating NUL inside `targetMap[32]`. Put linked maps in the same assets directory. Triggers must be unique and occupy empty, open-door or portal cells; every portal cell needs a transition. - **Transition Array**: `transitionCount * 42` bytes (`LevelTransition` in both `engine_defs.inc` and `editor/src/level_format.h`). Target names are ASCII basenames, 5..31 bytes including `.dat`, with a terminating NUL inside `targetMap[32]`. Put linked maps in the same assets directory. Triggers must be unique and occupy empty, open-door or portal cells; every portal cell needs a transition.
- Files are validated completely before replacing the active room. Counts above capacity, truncated/extra payload, invalid coordinates/scales, blocked spawns and invalid transition names are rejected. Black ceiling/floor colors are valid. A failed runtime transition keeps the current room; a failed startup load creates the procedural map. - Files are validated completely before replacing the active room. Counts above capacity, truncated/extra payload, invalid coordinates/scales, blocked spawns and invalid transition names are rejected. Black ceiling/floor colors are valid. A failed runtime transition keeps the current room; a failed startup load creates the procedural map.
- **Cutscene Array (v4)**: after transitions, `cutsceneCount * 16` bytes; followed by `cutsceneStringCount * 96` bytes of NUL-terminated CP1251 strings. No serialized pointers. Earlier versions ignore the new header fields and load an empty track. - **Cutscene Array (v4)**: after transitions, `cutsceneCount * 16` bytes; followed by `cutsceneStringCount * 96` bytes of NUL-terminated CP1251 strings. No serialized pointers. Earlier versions ignore the new header fields and load an empty track.
- New saves use v4 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior). - New saves use v5 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior).
Human users can design levels visually using `run_editor.bat` (`editor/editor.exe`, written in C++17 + ImGui + DirectX 11), with 1-click Playtest (F5). Human users can design levels visually using `run_editor.bat` (`editor/editor.exe`, written in C++17 + ImGui + DirectX 11), with 1-click Playtest (F5).
AI agents can directly read or write `assets/map.dat` following this specification. AI agents can directly read or write `assets/map.dat` following this specification.
@ -202,7 +203,7 @@ F6 opens EblanStudio's timeline. Author commands with buttons, drag diamonds to
`examples/cutscene_demo/run_demo.bat` runs an isolated two-room example with camera waypoints, Russian subtitles, WAV, fades and a scripted transition. Regenerate it with `python tools/make_cutscene_demo.py`. `examples/cutscene_demo/run_demo.bat` runs an isolated two-room example with camera waypoints, Russian subtitles, WAV, fades and a scripted transition. Regenerate it with `python tools/make_cutscene_demo.py`.
### I. Embedded Game View ### I. Embedded Game View
The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-...` process to execute the actual `Render3DScene`, cinematic overlay and fade procedures. No duplicate raycaster. `editor/src/preview_protocol.h` and `include/engine_preview.inc` define a versioned 1,357,792-byte shared-memory mailbox: 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots and a 320x240 DIB frame. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout. The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-...` process to execute the actual `Render3DScene`, cinematic overlay and fade procedures. No duplicate raycaster. `editor/src/preview_protocol.h` and `include/engine_preview.inc` define a versioned 1,362,408-byte shared-memory mailbox (protocol v2): 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots, a 320x240 DIB frame, 13 authored light records and 1024 baked RGB cells. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout.
The renderer validates camera and sprite bounds, uploads textures only when their revision changes, and exits if its parent process ends. The editor has process-exit/timeout checks and a retry button, converts DIB padding to opaque alpha, uploads BGRA to DX11 and displays with point sampling at 4:3. Map/inspector changes appear without saving. Timeline playback/scrubbing drives the viewport camera, subtitles and fade. Free-camera inspection affects no authored data. The viewport shows the scene and cinematic overlays; normal game HUD, door interactions and room switching are tested in F5. The renderer validates camera and sprite bounds, uploads textures only when their revision changes, and exits if its parent process ends. The editor has process-exit/timeout checks and a retry button, converts DIB padding to opaque alpha, uploads BGRA to DX11 and displays with point sampling at 4:3. Map/inspector changes appear without saving. Timeline playback/scrubbing drives the viewport camera, subtitles and fade. Free-camera inspection affects no authored data. The viewport shows the scene and cinematic overlays; normal game HUD, door interactions and room switching are tested in F5.
@ -272,3 +273,32 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi
- Editor toolbar 1-click genre switcher: `☮ КВЕСТ / БЕЗ ОРУЖИЯ` vs `⚔ ЭКШЕН / С ОРУЖИЕМ`. - Editor toolbar 1-click genre switcher: `☮ КВЕСТ / БЕЗ ОРУЖИЯ` vs `⚔ ЭКШЕН / С ОРУЖИЕМ`.
- Project settings (`project.eblanproj`) persist `GameMode`, `ShowWeapons`, and `ShowCrosshair`. - Project settings (`project.eblanproj`) persist `GameMode`, `ShowWeapons`, and `ShowCrosshair`.
7. **Static Lightmap Baking & Realtime Dynamic Lighting (`include/engine_light.inc`)**:
- `BakeMapLighting`: precomputes 1024 RGB cells in `lightGridRGB` from active STATIC sources only, with exact supercover cell-boundary occlusion, approximate distance attenuation and an out-of-place, wall-aware cross filter. `lightGrid` is a derived grayscale view. Realtime sources are never baked or counted twice.
- Dynamic point lights (`LIGHT_TYPE_POINT`) with customizable color, radius, intensity, and organic torch flame flicker (`LIGHT_FLAG_FLICKER`).
- Dynamic burst lights (`LIGHT_TYPE_BURST`): weapon muzzle flare on pistol/shotgun shots and high-intensity fiery explosion blast on explosive barrel detonation.
- Player directional spotlight flashlight `[F]`: conical falloff (`flashlightConeAngle`), distance attenuation, and wall occlusion physics.
- Dynamic lighting mode cycle `[L]`:
- `LIGHTING_MODE_NONE (0)`: classic flat Wolf3D retro fullbright.
- `LIGHTING_MODE_BAKED (1)`: static baked lightmap shadows and ambient shading.
- `LIGHTING_MODE_DYNAMIC (2)`: full dynamic physics (point lights + player flashlight + muzzle flares + barrel blasts + lightmap).
- EblanStudio Studio & Editor Lighting Tools:
- 1-Click Toolbar Button: `💡 ЗАПЕЧЬ СВЕТ` (Bake Lightmap) for instant baking.
- Toolbar Lighting Mode Switcher: `[☀ Свет: ВЫКЛ]`, `[🕯 Свет: ЗАПЕЧЁН]`, `[⚡ Свет: ДИНАМИКА]`.
- Toolbar Flashlight Toggle: `[🔦 Фонарик: ВКЛ/ВЫКЛ]`.
- 2D Canvas Lightmap Heatmap Overlay: `[🗺 Карта: ВКЛ/ВЫКЛ]` visualizing light and shadow gradients across the level grid.
- Light Source Entities on 2D Canvas: interactive light markers with RGB tint, glow halo, radius circle, click-to-select, and real-time mouse dragging.
- Quick Spawner Palette: `+ Источник света` button.
- Scene Hierarchy: dedicated `💡 Источники света (Lights)` tree with selection and "+ Добавить свет".
- Property Inspector: position X/Y sliders, radius slider, intensity slider, RGB color picker, light type dropdown (Static, Point, Torch flicker, Pulse), wall shadow-casting toggle, and torch flicker toggle.
- Real-Time 3D Viewport synchronization via `EnginePreviewPacket` shared-memory protocol.
### K. Authored Lighting Format (EBLN v5)
After the v4 cutscene strings, v5 appends `LightingSettings` (32 bytes), `lightCount * 40` bytes of `LightSource`, and 1024 DWORD baked RGB cells (`00RRGGBB`, 4096 bytes). Settings are eight DWORDs: mode (0..2), ambient (0..255), flashlight (0/1), flashlight radius (32..2048), intensity (0..255), half cone angle (1..96 angle units), baked-valid (1), reserved (0). Source layouts match `EditorLightSource` and MASM `LightSource`. Counts/settings/coordinates/radii/intensities/flags/colors and exact file size are validated before committing a room. v1..v4 load no authored lights and mode NONE; the old reserved header bytes are ignored.
Sources and baked data are map-local and travel with save/load, undo/redo, F5 and standalone exports. Static lighting is baked in the editor with BakeLightingRGB and in ASM with BakeMapLighting; all 1024 RGB cells are regression-compared. Realtime point/pulse/flicker lights use only the live source array on top of the persisted static map. Shadow flags are respected. Slot 0 belongs to the player flashlight, 1..13 to authored sources, 14 to muzzle flare, 15 to explosions; combat never overwrites authored lights. Both wall stripes and sprites receive RGB coefficients. Flat ceiling/floor colors use a camera-cell light sample, not per-pixel floor projection.
Editor light settings are in the property inspector and toolbar. A stale-bake indicator tracks static sources, geometry and ambient, excluding realtime movement. Saving or F5 automatically rebuilds a stale static bake; runtime and preview consume persisted RGB without baking each frame. The IPC v2 header includes flashlight parameters and a deterministic light tick; sources begin at offset 1,357,792 and baked RGB at 1,358,312. Update the C++, ASM and Python layouts together.
Lighting verification (24.10.2026): 117 editor checks and 131 ASM checks passed; all 1024 RGB bake cells match, and both lit and classic viewport framebuffers match direct ASM rendering. Project assets were verified unchanged.

View file

@ -112,7 +112,7 @@ build_editor.bat
## Переходы и проверка первых трёх этапов ## Переходы и проверка первых трёх этапов
Карты новых сохранений используют EBLN v4; существующие v1/v2/v3 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются. Карты новых сохранений используют EBLN v5; существующие v1/v2/v3/v4 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
Порталы имеют отдельную текстуру и не занимают слот фонаря. Первый импорт изображения создаёт недостающие папки и пакет со стандартными текстурами в остальных слотах. Повреждённый пакет не перезаписывается импортом; ошибка показывается в строке состояния. Неудачный переход сохраняет текущую комнату и показывает сообщение в игре. Порталы имеют отдельную текстуру и не занимают слот фонаря. Первый импорт изображения создаёт недостающие папки и пакет со стандартными текстурами в остальных слотах. Повреждённый пакет не перезаписывается импортом; ошибка показывается в строке состояния. Неудачный переход сохраняет текущую комнату и показывает сообщение в игре.
@ -134,4 +134,16 @@ python tools\test_regressions.py
Предпросмотр останавливается на команде перехода в другую комнату; цепочка комнат и игровой ввод проверяются через F5. F5/F6 работают при фокусе интерфейса и текстовых полей. Ошибка запуска показывается отдельным сообщением. При старте редактор открывает карту проекта относительно своего файла, поэтому запуск из другой рабочей папки не ломает пути. Предпросмотр останавливается на команде перехода в другую комнату; цепочка комнат и игровой ввод проверяются через F5. F5/F6 работают при фокусе интерфейса и текстовых полей. Ошибка запуска показывается отдельным сообщением. При старте редактор открывает карту проекта относительно своего файла, поэтому запуск из другой рабочей папки не ломает пути.
Формат v4 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`. Формат v5 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`.
## Постоянный и динамический свет
В палитре выберите «+ Источник света» и поставьте его на карте. Источники доступны также в дереве сцены. В инспекторе задайте тип, цвет, радиус, яркость, тени и мерцание; работают перемещение, копирование, удаление и Undo/Redo.
- **Постоянный**: выберите тип «Постоянный (запекание)», затем нажмите «Запечь постоянный свет». RGB-освещение и тени сохраняются в карте. После изменения геометрии, фонового света или постоянного источника инспектор покажет, что нужно запечь заново. При сохранении и F5 это происходит автоматически.
- **Динамический**: выберите точечный или пульсирующий источник и режим «Запечённый + динамический». Перемещение, цвет, яркость и мерцание сразу влияют на игровой вид. Перезапекать постоянный свет при этом не нужно.
- **Режимы**: «Без освещения», «Только запечённый», «Запечённый + динамический». Фоновый свет и параметры фонарика настраиваются в разделе «Освещение карты» инспектора. В игре F переключает фонарик, L — режим света.
До 13 источников карты; фонарик, вспышки оружия и взрывов используют отдельные места. Закрытые двери и стены перекрывают динамический свет. Статические тени остаются результатом последнего запекания. Цвета стен и спрайтов освещаются по RGB; освещение плоских потолка/пола берётся в точке камеры. Запечённая карта имеет разрешение 32×32 ячейки.
Новые сохранения используют EBLN v5. Старые карты сохраняют прежний вид без освещения, пока вы не добавите источники и не включите нужный режим. Свет каждой комнаты сохраняется независимо. Перед экспортом игры сохраните карты.

View file

@ -95,7 +95,7 @@
## 5. Загрузка карт и ресурсов ## 5. Загрузка карт и ресурсов
Новые карты сохраняются как EBLN v4: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет. Новые карты сохраняются как EBLN v5: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3/v4 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
Файлы связанных подуровней лежат рядом друг с другом. В `targetMap[32]` хранится только ASCII-имя `.dat` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия. Файлы связанных подуровней лежат рядом друг с другом. В `targetMap[32]` хранится только ASCII-имя `.dat` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия.
@ -142,3 +142,17 @@ WASD, стрелки и правая кнопка мыши позволяют о
F5/F6 обрабатываются до признака захвата клавиатуры ImGui — иначе фокус внутри редактора блокировал запуск. Ошибки подготовки и запуска F5 выводятся отдельным диалогом. Начальный путь карты определяется относительно расположения редактора и загружается при старте, независимо от рабочей папки процесса. F5/F6 обрабатываются до признака захвата клавиатуры ImGui — иначе фокус внутри редактора блокировал запуск. Ошибки подготовки и запуска F5 выводятся отдельным диалогом. Начальный путь карты определяется относительно расположения редактора и загружается при старте, независимо от рабочей папки процесса.
Проверки выполняют полный путь x86 ASM → раздел памяти → x64 редактор → текстура DX11 на устройстве WARP. Пиксели встроенного вида сравниваются с прямым вызовом обычного ASM-рендерера. Отдельный сценарий `python tools/test_preview_ipc.py` проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок. Проверки выполняют полный путь x86 ASM → раздел памяти → x64 редактор → текстура DX11 на устройстве WARP. Пиксели встроенного вида сравниваются с прямым вызовом обычного ASM-рендерера. Отдельный сценарий `python tools/test_preview_ipc.py` проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок.
## 9. Запекание и динамический RGB-свет (24.10.2026)
Запечённый свет отделён от динамических источников. `lighting_format.h` выполняет запекание в редакторе; `engine_lighting_core.inc` содержит эквивалентный ASM-вариант и расчёт света при рендеринге. Постоянные источники (STATIC=0) входят в 1024 RGB-ячейки `lightGridRGB`. Точечные (POINT=1), пульсирующие (PULSE=4), мерцание, фонарик и временные вспышки прибавляются только при динамическом режиме. Это исключает повторное прибавление света. Источник цвета 0 излучает чёрный свет; белый задаётся явно как 00FFFFFFh.
Оба запекателя используют расстояние `max(abs(dx),abs(dy)) + 3*min(abs(dx),abs(dy))/8`, затухание `(radius-distance)*intensity/radius`, RGB-цвет и насыщение каналов. `CastLightRay` проверяет точные пересечения границ клеток; при пересечении угла проверяются обе соседние клетки. Порталы и открытые двери пропускают свет. Флаг SHADOWS отключает проверку препятствий для выбранного источника. Сглаживание выполняется в отдельном буфере с весом 2 для центра и 1 для доступных соседей, без усреднения сквозь стену.
В v5 после строк кат-сцены добавлены 32 байта настроек, lightCount записей по 40 байт и 4096 байт RGB-карты. Поля lightCount/reserved занимают смещения 34/36 прежнего 38-байтового заголовка. Настройки: режим, фон, включение фонаря, его радиус, интенсивность, полуугол, baked-valid=1, reserved=0 (все DWORD). Загрузка транзакционная, включая освещение: повреждённая комната не заменяет действующую. Старые v1..v4 сбрасывают источники и сохраняют классическое освещение. После перехода загружается свет новой комнаты; старые источники и вспышки очищаются.
Игровой вид использует протокол EBPV v2 размером 1 362 408 байт: прежние смещения пикселей и текстур сохранены, после кадра добавлены 13 источников и RGB-карта. Источники и фонарик синхронизируются при каждом запросе; время мерцания/пульсации задаётся 60-Гц счётчиком. Перезапекания на каждом кадре нет. В игре обновление использует время симуляции, а запечённая карта читается непосредственно из файла. У стен освещение берётся у видимой грани, у спрайтов — в их позиции; фонарик проверяет и конус, и заслонение. Плоские потолок/пол используют один образец в точке камеры, без проекции света по каждому пикселю пола.
Слоты 1..13 принадлежат источникам карты; 0 зарезервирован фонарю, 14 вспышке выстрела, 15 взрыву. Вспышки не могут перезаписать авторские источники. Режимы света, источники, RGB-данные и состояние запекания входят в Undo/Redo. Изменение постоянного света/геометрии/фона помечает результат устаревшим; динамическое движение не требует запекания. Сохранение и F5 автоматически обновляют устаревший результат.
Регрессии проверяют цвет, заслонение стенами/дверями/углами, флаги теней, отсутствие двойного освещения, пульсацию и затухание вспышек, сохранение v5, отказ от повреждённых данных, старые карты, Undo/Redo и реальное обновление игрового вида. Все 1024 RGB-ячейки сравниваются между C++ и ASM. Кадры сцены с авторским светом из встроенного вида сравниваются с прямым рендерингом той же карты в ASM.

Binary file not shown.

View file

@ -1,22 +1,22 @@
[Window][Workspace] [Window][Workspace]
Pos=0,60 Pos=0,60
Size=2560,1292 Size=2560,1292
LastUsed=20261023 LastUsed=20261024
[Window][Debug##Default] [Window][Debug##Default]
Pos=60,60 Pos=60,60
Size=400,400 Size=400,400
LastUsed=20261023 LastUsed=20261024
[Window][Toolbar] [Window][Toolbar]
Pos=0,20 Pos=0,20
Size=2560,38 Size=2560,38
LastUsed=20261023 LastUsed=20261024
[Window][StatusBar] [Window][StatusBar]
Pos=0,1352 Pos=0,1352
Size=2560,32 Size=2560,32
LastUsed=20261023 LastUsed=20261024
[Window][Не удалось запустить игру] [Window][Не удалось запустить игру]
Pos=649,356 Pos=649,356

View file

@ -145,6 +145,7 @@ void EditorApp::InitDefaultLevel() {
selectedCutsceneEvent = -1; selectedCutsceneEvent = -1;
StopCutscenePreview(); StopCutscenePreview();
cutsceneCursor = 0; cutsceneCursor = 0;
InitDefaultLights();
previewX = playerStartX*64+32; previewY = playerStartY*64+32; previewAngle = playerStartAngle; previewX = playerStartX*64+32; previewY = playerStartY*64+32; previewAngle = playerStartAngle;
previewSceneCamera = true; previewSceneCamera = true;
ScrubCutscene(0); ScrubCutscene(0);
@ -171,6 +172,10 @@ bool EditorApp::SaveLevelToFile(const std::string& filepath, bool updateCurrentP
h.cutsceneCount = static_cast<uint16_t>(cutscene.events.size()); h.cutsceneCount = static_cast<uint16_t>(cutscene.events.size());
h.cutsceneStringCount = static_cast<uint16_t>(cutscene.strings.size()); h.cutsceneStringCount = static_cast<uint16_t>(cutscene.strings.size());
h.cutsceneFlags = cutscene.flags; h.cutsceneFlags = cutscene.flags;
if(!ValidateLighting(GetLightingSettings(),lightSources.data(),lightSources.size(),error)) {statusMessage=error;return false;}
if(LightingNeedsBake()) BakeLighting();
h.lightCount=static_cast<uint16_t>(lightSources.size());
level.lighting.settings=GetLightingSettings();level.lighting.lights=lightSources;level.lighting.rgb=bakedLightRGB;
bool success = AtomicWrite(filepath, [&](std::ofstream& out) { bool success = AtomicWrite(filepath, [&](std::ofstream& out) {
out.write(reinterpret_cast<const char*>(&h), sizeof(h)); out.write(reinterpret_cast<const char*>(&h), sizeof(h));
out.write(reinterpret_cast<const char*>(level.cells.data()), level.cells.size()); out.write(reinterpret_cast<const char*>(level.cells.data()), level.cells.size());
@ -183,6 +188,9 @@ bool EditorApp::SaveLevelToFile(const std::string& filepath, bool updateCurrentP
std::memcpy(bytes, encoded.data(), encoded.size()); std::memcpy(bytes, encoded.data(), encoded.size());
out.write(bytes, sizeof(bytes)); out.write(bytes, sizeof(bytes));
} }
out.write(reinterpret_cast<const char*>(&level.lighting.settings),sizeof(LightingSettings));
if(!lightSources.empty()) out.write(reinterpret_cast<const char*>(lightSources.data()),lightSources.size()*sizeof(EditorLightSource));
out.write(reinterpret_cast<const char*>(bakedLightRGB.data()),4096);
}); });
if (!success) { statusMessage = "Не удалось сохранить карту: " + filepath; return false; } if (!success) { statusMessage = "Не удалось сохранить карту: " + filepath; return false; }
if (updateCurrentPath) currentFilePath = filepath; if (updateCurrentPath) currentFilePath = filepath;
@ -201,6 +209,14 @@ bool EditorApp::LoadLevel(const std::string& filepath) {
ceilingColor = h.ceilingColor & 0xFFFFFF; floorColor = h.floorColor & 0xFFFFFF; ceilingColor = h.ceilingColor & 0xFFFFFF; floorColor = h.floorColor & 0xFFFFFF;
sprites = std::move(candidate.sprites); transitions = std::move(candidate.transitions); sprites = std::move(candidate.sprites); transitions = std::move(candidate.transitions);
cutscene = std::move(candidate.cutscene); cutscene = std::move(candidate.cutscene);
lightSources=std::move(candidate.lighting.lights);
const auto& ls=candidate.lighting.settings;
lightingMode=ls.mode;ambientLightLevel=ls.ambient;isFlashlightOn=ls.flashlight!=0;
flashlightRadius=ls.flashlightRadius;flashlightIntensity=ls.flashlightIntensity;flashlightCone=ls.flashlightCone;
bakedLightRGB=candidate.lighting.rgb;
for(int i=0;i<1024;++i) {auto c=bakedLightRGB[i];bakedLightmap[i]=(((c>>16)&255)+((c>>8)&255)+(c&255))/3;}
bakedLightingSignature=LightingBakeSignature(&map[0][0],lightSources,ambientLightLevel);
selectedLightIdx=draggingLightIdx=-1;
selectedCutsceneEvent = -1; cutsceneCursor = 0; placeCameraPoint = false; selectedCutsceneEvent = -1; cutsceneCursor = 0; placeCameraPoint = false;
ScrubCutscene(0); ScrubCutscene(0);
currentFilePath = filepath; currentFilePath = filepath;
@ -346,6 +362,42 @@ void EditorApp::CheckPlaytestProcess() {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Undo / Redo // Undo / Redo
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Lighting Subsystem: Static Lightmap Baking & Raytraced Shadowing
// -----------------------------------------------------------------------------
bool EditorApp::CastLightRay(int x1,int y1,int x2,int y2) const {
return LightingVisible(&map[0][0],x1,y1,x2,y2);
}
LightingSettings EditorApp::GetLightingSettings() const {
LightingSettings s;
s.mode=lightingMode;s.ambient=ambientLightLevel;s.flashlight=isFlashlightOn;
s.flashlightRadius=flashlightRadius;s.flashlightIntensity=flashlightIntensity;s.flashlightCone=flashlightCone;
return s;
}
bool EditorApp::LightingNeedsBake() const {
return bakedLightingSignature!=LightingBakeSignature(&map[0][0],lightSources,ambientLightLevel);
}
void EditorApp::BakeLighting() {
bakedLightRGB=BakeLightingRGB(&map[0][0],lightSources,ambientLightLevel);
for(int i=0;i<1024;++i) {
uint32_t c=bakedLightRGB[i];
bakedLightmap[i]=static_cast<uint8_t>((((c>>16)&255)+((c>>8)&255)+(c&255))/3);
}
bakedLightingSignature=LightingBakeSignature(&map[0][0],lightSources,ambientLightLevel);
statusMessage="Постоянный свет запечён. Динамические источники рассчитываются отдельно.";
}
void EditorApp::InitDefaultLights() {
lightSources.clear();
lightingMode=LIGHTING_MODE_DYNAMIC;ambientLightLevel=40;isFlashlightOn=true;
flashlightRadius=384;flashlightIntensity=240;flashlightCone=45;
selectedLightIdx=draggingLightIdx=-1;
BakeLighting();
}
LevelSnapshot EditorApp::CaptureSnapshot() const { LevelSnapshot EditorApp::CaptureSnapshot() const {
LevelSnapshot s; LevelSnapshot s;
std::memcpy(s.map, map, sizeof(map)); std::memcpy(s.map, map, sizeof(map));
@ -357,6 +409,8 @@ LevelSnapshot EditorApp::CaptureSnapshot() const {
s.sprites = sprites; s.sprites = sprites;
s.transitions = transitions; s.transitions = transitions;
s.cutscene = cutscene; s.cutscene = cutscene;
s.lightSources = lightSources;
s.lightingSettings=GetLightingSettings();s.bakedRGB=bakedLightRGB;s.bakedSignature=bakedLightingSignature;
return s; return s;
} }
@ -371,6 +425,13 @@ void EditorApp::RestoreSnapshot(const LevelSnapshot& s) {
StopCutscenePreview(); StopCutscenePreview();
transitions = s.transitions; transitions = s.transitions;
cutscene = s.cutscene; cutscene = s.cutscene;
lightSources = s.lightSources;
lightingMode=s.lightingSettings.mode;ambientLightLevel=s.lightingSettings.ambient;isFlashlightOn=s.lightingSettings.flashlight!=0;
flashlightRadius=s.lightingSettings.flashlightRadius;flashlightIntensity=s.lightingSettings.flashlightIntensity;flashlightCone=s.lightingSettings.flashlightCone;
bakedLightRGB=s.bakedRGB;bakedLightingSignature=s.bakedSignature;
for(int i=0;i<1024;++i) {auto c=bakedLightRGB[i];bakedLightmap[i]=(((c>>16)&255)+((c>>8)&255)+(c&255))/3;}
selectedLightIdx = -1;
draggingLightIdx = -1;
selectedCutsceneEvent = -1; placeCameraPoint = false; selectedCutsceneEvent = -1; placeCameraPoint = false;
ScrubCutscene(cutsceneCursor); ScrubCutscene(cutsceneCursor);
selectedSpriteIdx = -1; selectedSpriteIdx = -1;
@ -1276,6 +1337,21 @@ void EditorApp::ApplyTool(int gx, int gy) {
if (SetCell(gx, gy, CELL_EMPTY)) { playerStartX = gx; playerStartY = gy; } if (SetCell(gx, gy, CELL_EMPTY)) { playerStartX = gx; playerStartY = gy; }
break; break;
case TOOL_SPRITE_SPAWN: case TOOL_SPRITE_SPAWN:
if (pendingSpawnArchetype == 99) {
if (lightSources.size() < MAX_EDITOR_LIGHTS) {
if(lightingMode==0)lightingMode=2;
EditorLightSource nl;
nl.x = gx * 64 + 32; nl.y = gy * 64 + 32;
lightSources.push_back(nl);
selectedLightIdx = static_cast<int>(lightSources.size()) - 1;
selectedSpriteIdx = -1; selectedTransitionIdx = -1;
BakeLighting();
statusMessage = "Источник света создан на карте.";
} else {
statusMessage = "Лимит источников света карты: 13.";
}
break;
}
if (sprites.size() < MAX_LEVEL_SPRITES) { if (sprites.size() < MAX_LEVEL_SPRITES) {
SpriteEntry sprite{}; SpriteEntry sprite{};
sprite.x = gx * 64 + 32; sprite.y = gy * 64 + 32; sprite.x = gx * 64 + 32; sprite.y = gy * 64 + 32;
@ -1303,7 +1379,7 @@ void EditorApp::ApplyTool(int gx, int gy) {
SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_PROP, 100, 0, 0); SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_PROP, 100, 0, 0);
} }
sprites.push_back(sprite); sprites.push_back(sprite);
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1; selectedSpriteIdx = static_cast<int>(sprites.size()) - 1; selectedLightIdx=-1;
selectedTransitionIdx = -1; selectedTransitionIdx = -1;
} }
break; break;
@ -1339,6 +1415,11 @@ void EditorApp::FloodFill(int startX, int startY, uint8_t targetType, uint8_t fi
} }
void EditorApp::DuplicateSelectedEntity() { void EditorApp::DuplicateSelectedEntity() {
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {
if(lightSources.size()>=MAX_EDITOR_LIGHTS){statusMessage="Лимит: 13 источников карты.";return;}
auto copy=lightSources[selectedLightIdx];copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32);
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(lightSources.size())-1;return;
}
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) { if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
if (sprites.size() >= MAX_LEVEL_SPRITES) { if (sprites.size() >= MAX_LEVEL_SPRITES) {
statusMessage = "Лимит спрайтов на карте достигнут (макс. 32)."; statusMessage = "Лимит спрайтов на карте достигнут (макс. 32).";
@ -1355,6 +1436,9 @@ void EditorApp::DuplicateSelectedEntity() {
} }
void EditorApp::DeleteSelectedEntity() { void EditorApp::DeleteSelectedEntity() {
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {
PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=draggingLightIdx=-1;return;
}
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) { if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
PushUndo(); PushUndo();
sprites.erase(sprites.begin() + selectedSpriteIdx); sprites.erase(sprites.begin() + selectedSpriteIdx);
@ -1370,6 +1454,10 @@ void EditorApp::DeleteSelectedEntity() {
} }
void EditorApp::FocusSelectedEntity() { void EditorApp::FocusSelectedEntity() {
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {
canvasPanX=150.0f-lightSources[selectedLightIdx].x/64.0f*canvasZoom;
canvasPanY=150.0f-lightSources[selectedLightIdx].y/64.0f*canvasZoom;return;
}
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) { if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
int gx = sprites[selectedSpriteIdx].x / 64; int gx = sprites[selectedSpriteIdx].x / 64;
int gy = sprites[selectedSpriteIdx].y / 64; int gy = sprites[selectedSpriteIdx].y / 64;
@ -1768,6 +1856,51 @@ void EditorApp::RenderToolbar() {
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
ImGui::SameLine(); ImGui::SameLine();
// Lighting Controls in Toolbar
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.80f, 0.50f, 0.05f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.95f, 0.65f, 0.10f, 1.0f));
if (ImGui::Button(" 💡 ЗАПЕЧЬ СВЕТ ")) {
PushUndo(); BakeLighting();
}
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Запечь постоянное освещение (Lightmap) для всей карты с учетом стен и препятствий");
ImGui::SameLine();
const char* lModeBtn = (lightingMode == 0) ? " ☀ Свет: ВЫКЛ " :
(lightingMode == 1) ? " 🕯 Свет: ЗАПЕЧЁН " :
" ⚡ Свет: ДИНАМИКА ";
if (ImGui::Button(lModeBtn)) {
PushUndo(); lightingMode = (lightingMode + 1) % 3;
statusMessage = (lightingMode == 0) ? "Режим света: ВЫКЛ (Wolf3D flat)" :
(lightingMode == 1) ? "Режим света: ЗАПЕЧЁННЫЙ (Lightmap)" :
"Режим света: ДИНАМИЧЕСКИЙ (Point lights + Flashlight)";
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Переключение режима освещения (Без света / Запечённый / Динамический)");
ImGui::SameLine();
if (isFlashlightOn) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.55f, 0.70f, 1.0f));
if (ImGui::Button(" 🔦 Фонарик: ВКЛ ")) {PushUndo();isFlashlightOn = false;}
ImGui::PopStyleColor();
} else {
if (ImGui::Button(" 🔦 Фонарик: ВЫКЛ ")) {PushUndo();isFlashlightOn = true;}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Переключить фонарик игрока [F]");
ImGui::SameLine();
if (showLightHeatmap) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.70f, 0.40f, 0.10f, 1.0f));
if (ImGui::Button(" 🗺 Карта: ВКЛ ")) showLightHeatmap = false;
ImGui::PopStyleColor();
} else {
if (ImGui::Button(" 🗺 Карта: ВЫКЛ ")) showLightHeatmap = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Отображать световую карту (Heatmap) на 2D сетке");
ImGui::SameLine();
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
ImGui::SameLine();
// Tools // Tools
auto ToolButton = [&](const char* label, ToolMode mode) { auto ToolButton = [&](const char* label, ToolMode mode) {
bool active = (currentTool == mode); bool active = (currentTool == mode);
@ -1889,6 +2022,16 @@ void EditorApp::RenderGridCanvas() {
ImU32 col = GetCellColor(map[y][x]); ImU32 col = GetCellColor(map[y][x]);
drawList->AddRectFilled(tileP0, tileP1, col); drawList->AddRectFilled(tileP0, tileP1, col);
if (showLightHeatmap) {
uint8_t l = bakedLightmap[y * MAP_GRID_W + x];
ImU32 lightTint = (l < 50) ? IM_COL32(10, 15, 30, 160) :
(l < 100) ? IM_COL32(30, 30, 20, 100) :
(l < 180) ? IM_COL32(200, 160, 30, static_cast<int>((l - 100) * 0.7f)) :
IM_COL32(255, 220, 80, static_cast<int>(60 + (l - 180) * 0.6f));
drawList->AddRectFilled(tileP0, tileP1, lightTint);
}
drawList->AddRect(tileP0, tileP1, IM_COL32(50, 50, 55, 120), 0.0f, 0, 1.0f); drawList->AddRect(tileP0, tileP1, IM_COL32(50, 50, 55, 120), 0.0f, 0, 1.0f);
if (map[y][x] == CELL_PORTAL) { if (map[y][x] == CELL_PORTAL) {
@ -1963,6 +2106,41 @@ void EditorApp::RenderGridCanvas() {
); );
} }
// 2b. Draw Light Sources on Map Canvas
int hoveredLight = -1;
for (size_t i = 0; i < lightSources.size(); ++i) {
float lx = lightSources[i].x / 64.0f;
float ly = lightSources[i].y / 64.0f;
ImVec2 lcPos = ImVec2(gridOrigin.x + lx * tileSize, gridOrigin.y + ly * tileSize);
float dx = mousePos.x - lcPos.x;
float dy = mousePos.y - lcPos.y;
float r = tileSize * 0.45f;
if (std::sqrt(dx * dx + dy * dy) < r) {
hoveredLight = static_cast<int>(i);
}
if (static_cast<int>(i) == selectedLightIdx || static_cast<int>(i) == hoveredLight) {
float radPix = (lightSources[i].radius / 64.0f) * tileSize;
drawList->AddCircle(lcPos, radPix, IM_COL32(255, 210, 40, 160), 48, 1.5f);
drawList->AddCircleFilled(lcPos, radPix, IM_COL32(255, 210, 40, 18));
}
uint32_t c = lightSources[i].color;
ImU32 lightCol = IM_COL32((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF, 255);
ImU32 haloCol = IM_COL32((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF, 70);
drawList->AddCircleFilled(lcPos, tileSize * 0.38f, haloCol);
drawList->AddCircleFilled(lcPos, tileSize * 0.22f, lightCol);
drawList->AddCircleFilled(lcPos, tileSize * 0.08f, IM_COL32(255, 255, 255, 255));
if (static_cast<int>(i) == selectedLightIdx) {
drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 255, 0, 255), 24, 2.0f);
} else if (static_cast<int>(i) == hoveredLight) {
drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 255, 200, 200), 24, 1.5f);
}
}
// 3. Draw Player Spawn Point & Direction Cone // 3. Draw Player Spawn Point & Direction Cone
{ {
ImVec2 pPos = ImVec2(gridOrigin.x + (playerStartX + 0.5f) * tileSize, ImVec2 pPos = ImVec2(gridOrigin.x + (playerStartX + 0.5f) * tileSize,
@ -2010,12 +2188,18 @@ void EditorApp::RenderGridCanvas() {
drawList->PopClipRect(); return; drawList->PopClipRect(); return;
} }
if (placeCameraPoint) { drawList->PopClipRect(); return; } if (placeCameraPoint) { drawList->PopClipRect(); return; }
// Left Click: start action (drag sprite OR paint) // Left Click: start action (drag light OR drag sprite OR paint)
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (currentTool == TOOL_SPRITE_SPAWN && hoveredSprite >= 0) { if (hoveredLight >= 0) {
draggingLightIdx = hoveredLight;
selectedLightIdx = hoveredLight;
selectedSpriteIdx = -1;
selectedTransitionIdx = -1;
PushUndo();
} else if (currentTool == TOOL_SPRITE_SPAWN && hoveredSprite >= 0) {
// Start dragging existing sprite // Start dragging existing sprite
draggingSpriteIdx = hoveredSprite; draggingSpriteIdx = hoveredSprite;
selectedSpriteIdx = hoveredSprite; selectedSpriteIdx = hoveredSprite; selectedLightIdx=-1;
selectedTransitionIdx = -1; selectedTransitionIdx = -1;
PushUndo(); PushUndo();
} else if (inGrid) { } else if (inGrid) {
@ -2023,7 +2207,7 @@ void EditorApp::RenderGridCanvas() {
if (map[hoverGy][hoverGx] == CELL_PORTAL) { if (map[hoverGy][hoverGx] == CELL_PORTAL) {
for (size_t i = 0; i < transitions.size(); ++i) { for (size_t i = 0; i < transitions.size(); ++i) {
if (transitions[i].triggerCellX == hoverGx && transitions[i].triggerCellY == hoverGy) { if (transitions[i].triggerCellX == hoverGx && transitions[i].triggerCellY == hoverGy) {
selectedTransitionIdx = static_cast<int>(i); selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
selectedSpriteIdx = -1; selectedSpriteIdx = -1;
break; break;
} }
@ -2035,9 +2219,14 @@ void EditorApp::RenderGridCanvas() {
} }
} }
// Left Drag: continue painting OR move sprite // Left Drag: continue painting OR move light OR move sprite
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (draggingSpriteIdx >= 0) { if (draggingLightIdx >= 0) {
float wx = (mousePos.x - gridOrigin.x) / tileSize;
float wy = (mousePos.y - gridOrigin.y) / tileSize;
lightSources[draggingLightIdx].x = std::clamp(static_cast<int32_t>(wx * 64.0f), 32, (MAP_GRID_W - 1) * 64);
lightSources[draggingLightIdx].y = std::clamp(static_cast<int32_t>(wy * 64.0f), 32, (MAP_GRID_H - 1) * 64);
} else if (draggingSpriteIdx >= 0) {
float wx = (mousePos.x - gridOrigin.x) / tileSize; float wx = (mousePos.x - gridOrigin.x) / tileSize;
float wy = (mousePos.y - gridOrigin.y) / tileSize; float wy = (mousePos.y - gridOrigin.y) / tileSize;
sprites[draggingSpriteIdx].x = static_cast<int32_t>(wx * 64.0f); sprites[draggingSpriteIdx].x = static_cast<int32_t>(wx * 64.0f);
@ -2049,6 +2238,10 @@ void EditorApp::RenderGridCanvas() {
// Left Release: end drag / stroke // Left Release: end drag / stroke
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
if (draggingLightIdx >= 0) {
draggingLightIdx = -1;
BakeLighting();
}
draggingSpriteIdx = -1; draggingSpriteIdx = -1;
strokeSnapshotTaken = false; strokeSnapshotTaken = false;
} }
@ -2164,6 +2357,7 @@ void EditorApp::RenderTilePalette() {
SpawnerBtn("+ Переключатель [E]", ENTITY_ARCHETYPE_SWITCH, ImVec4(0.70f, 0.40f, 0.10f, 1.0f)); SpawnerBtn("+ Переключатель [E]", ENTITY_ARCHETYPE_SWITCH, ImVec4(0.70f, 0.40f, 0.10f, 1.0f));
SpawnerBtn("+ Бочка (Взрывная)", ENTITY_ARCHETYPE_BARREL, ImVec4(0.70f, 0.25f, 0.10f, 1.0f)); SpawnerBtn("+ Бочка (Взрывная)", ENTITY_ARCHETYPE_BARREL, ImVec4(0.70f, 0.25f, 0.10f, 1.0f));
SpawnerBtn("+ Декорация (Проп)", ENTITY_ARCHETYPE_PROP, ImVec4(0.35f, 0.35f, 0.40f, 1.0f)); SpawnerBtn("+ Декорация (Проп)", ENTITY_ARCHETYPE_PROP, ImVec4(0.35f, 0.35f, 0.40f, 1.0f));
SpawnerBtn("+ Источник света", 99, ImVec4(0.85f, 0.60f, 0.10f, 1.0f));
} }
void EditorApp::RenderHierarchy() { void EditorApp::RenderHierarchy() {
@ -2195,7 +2389,7 @@ void EditorApp::RenderHierarchy() {
spr.textureId = defaultTex; spr.textureId = defaultTex;
SetSpriteEntityData(spr, archetype, (archetype == ENTITY_ARCHETYPE_GUARD ? 60 : 100), (archetype == ENTITY_ARCHETYPE_GUARD ? 2 : 0), (archetype == ENTITY_ARCHETYPE_GUARD ? 6 : 0)); SetSpriteEntityData(spr, archetype, (archetype == ENTITY_ARCHETYPE_GUARD ? 60 : 100), (archetype == ENTITY_ARCHETYPE_GUARD ? 2 : 0), (archetype == ENTITY_ARCHETYPE_GUARD ? 6 : 0));
sprites.push_back(spr); sprites.push_back(spr);
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1; selectedSpriteIdx = static_cast<int>(sprites.size()) - 1; selectedLightIdx=-1;
selectedTransitionIdx = -1; selectedTransitionIdx = -1;
statusMessage = "Сущность создана на сцене."; statusMessage = "Сущность создана на сцене.";
} else { } else {
@ -2210,6 +2404,19 @@ void EditorApp::RenderHierarchy() {
AddEntityItem("🕹 Рычаг / Переключатель", ENTITY_ARCHETYPE_SWITCH); AddEntityItem("🕹 Рычаг / Переключатель", ENTITY_ARCHETYPE_SWITCH);
AddEntityItem("💥 Взрывная бочка", ENTITY_ARCHETYPE_BARREL); AddEntityItem("💥 Взрывная бочка", ENTITY_ARCHETYPE_BARREL);
AddEntityItem("🏺 Декорация / Проп", ENTITY_ARCHETYPE_PROP); AddEntityItem("🏺 Декорация / Проп", ENTITY_ARCHETYPE_PROP);
if (ImGui::MenuItem("💡 Источник света")) {
if (lightSources.size() < MAX_EDITOR_LIGHTS) {
PushUndo();
if(lightingMode==0)lightingMode=2;
EditorLightSource nl;
nl.x = (playerStartX + 1) * 64 + 32; nl.y = (playerStartY + 1) * 64 + 32;
lightSources.push_back(nl);
selectedLightIdx = static_cast<int>(lightSources.size()) - 1;
selectedSpriteIdx = -1; selectedTransitionIdx = -1;
BakeLighting();
statusMessage = "Источник света создан на сцене.";
}
}
ImGui::EndPopup(); ImGui::EndPopup();
} }
@ -2228,7 +2435,7 @@ void EditorApp::RenderHierarchy() {
char playerLabel[96]; char playerLabel[96];
snprintf(playerLabel, sizeof(playerLabel), "👤 Игрок [Spawn: (%d, %d)]", playerStartX, playerStartY); snprintf(playerLabel, sizeof(playerLabel), "👤 Игрок [Spawn: (%d, %d)]", playerStartX, playerStartY);
if (MatchesSearch(playerLabel) || MatchesSearch("player") || MatchesSearch("игрок")) { if (MatchesSearch(playerLabel) || MatchesSearch("player") || MatchesSearch("игрок")) {
bool isPlayerSelected = (selectedSpriteIdx == -1 && selectedTransitionIdx == -1); bool isPlayerSelected = (selectedSpriteIdx == -1 && selectedTransitionIdx == -1 && selectedLightIdx == -1);
if (ImGui::Selectable(playerLabel, isPlayerSelected)) { if (ImGui::Selectable(playerLabel, isPlayerSelected)) {
selectedSpriteIdx = -1; selectedSpriteIdx = -1;
selectedTransitionIdx = -1; selectedTransitionIdx = -1;
@ -2269,25 +2476,25 @@ void EditorApp::RenderHierarchy() {
bool isSel = (selectedSpriteIdx == static_cast<int>(i)); bool isSel = (selectedSpriteIdx == static_cast<int>(i));
if (ImGui::Selectable(label, isSel)) { if (ImGui::Selectable(label, isSel)) {
selectedSpriteIdx = static_cast<int>(i); selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
selectedTransitionIdx = -1; selectedTransitionIdx = -1;
} }
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
selectedSpriteIdx = static_cast<int>(i); selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
FocusSelectedEntity(); FocusSelectedEntity();
} }
if (ImGui::BeginPopupContextItem()) { if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("📋 Дублировать", "Ctrl+D")) { if (ImGui::MenuItem("📋 Дублировать", "Ctrl+D")) {
selectedSpriteIdx = static_cast<int>(i); selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
DuplicateSelectedEntity(); DuplicateSelectedEntity();
} }
if (ImGui::MenuItem("🎯 Сфокусировать", "F")) { if (ImGui::MenuItem("🎯 Сфокусировать", "F")) {
selectedSpriteIdx = static_cast<int>(i); selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
FocusSelectedEntity(); FocusSelectedEntity();
} }
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("🗑 Удалить", "Del")) { if (ImGui::MenuItem("🗑 Удалить", "Del")) {
selectedSpriteIdx = static_cast<int>(i); selectedSpriteIdx = static_cast<int>(i); selectedLightIdx=-1;
DeleteSelectedEntity(); DeleteSelectedEntity();
} }
ImGui::EndPopup(); ImGui::EndPopup();
@ -2326,6 +2533,15 @@ void EditorApp::RenderHierarchy() {
ImGui::TreePop(); ImGui::TreePop();
} }
if(ImGui::TreeNodeEx("Источники света",ImGuiTreeNodeFlags_DefaultOpen)) {
for(int i=0;i<static_cast<int>(lightSources.size());++i) {
char label[100];snprintf(label,sizeof(label),"Свет #%d: %s##light_%d",i+1,
lightSources[i].lightType==LIGHT_TYPE_STATIC?"постоянный":"динамический",i);
if(ImGui::Selectable(label,selectedLightIdx==i)){selectedLightIdx=i;selectedSpriteIdx=selectedTransitionIdx=-1;}
}
if(ImGui::SmallButton("+ Добавить свет")) {pendingSpawnArchetype=99;currentTool=TOOL_SPRITE_SPAWN;statusMessage="Выберите место источника на карте.";}
ImGui::TreePop();
}
// 3. Sublevel Transitions Category // 3. Sublevel Transitions Category
char transHdr[64]; char transHdr[64];
snprintf(transHdr, sizeof(transHdr), "🌀 Порталы перехода (%zu)", transitions.size()); snprintf(transHdr, sizeof(transHdr), "🌀 Порталы перехода (%zu)", transitions.size());
@ -2340,18 +2556,18 @@ void EditorApp::RenderHierarchy() {
bool isSel = (selectedTransitionIdx == static_cast<int>(i)); bool isSel = (selectedTransitionIdx == static_cast<int>(i));
if (ImGui::Selectable(label, isSel)) { if (ImGui::Selectable(label, isSel)) {
selectedTransitionIdx = static_cast<int>(i); selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
selectedSpriteIdx = -1; selectedSpriteIdx = -1;
} }
if (ImGui::BeginPopupContextItem()) { if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("🎯 Сфокусировать", "F")) { if (ImGui::MenuItem("🎯 Сфокусировать", "F")) {
selectedTransitionIdx = static_cast<int>(i); selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
canvasPanX = 150.0f - (transitions[i].triggerCellX * canvasZoom); canvasPanX = 150.0f - (transitions[i].triggerCellX * canvasZoom);
canvasPanY = 150.0f - (transitions[i].triggerCellY * canvasZoom); canvasPanY = 150.0f - (transitions[i].triggerCellY * canvasZoom);
} }
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("🗑 Удалить портал", "Del")) { if (ImGui::MenuItem("🗑 Удалить портал", "Del")) {
selectedTransitionIdx = static_cast<int>(i); selectedTransitionIdx = static_cast<int>(i); selectedLightIdx=-1;
DeleteSelectedEntity(); DeleteSelectedEntity();
} }
ImGui::EndPopup(); ImGui::EndPopup();
@ -2370,6 +2586,10 @@ void EditorApp::RenderInspector() {
ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "ИНСПЕКТОР СВОЙСТВ (INSPECTOR)"); ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "ИНСПЕКТОР СВОЙСТВ (INSPECTOR)");
ImGui::Separator(); ImGui::Separator();
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(lightSources.size())) {RenderLightingInspector();return;}
RenderLightingSettings();
ImGui::Separator();
// Case 1: Sprite Entity Selected // Case 1: Sprite Entity Selected
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) { if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
auto& spr = sprites[selectedSpriteIdx]; auto& spr = sprites[selectedSpriteIdx];
@ -2807,3 +3027,5 @@ void EditorApp::RenderExportGameModal() {
} }
} }
#include "editor_lighting.inl"

View file

@ -9,6 +9,7 @@
#include "level_format.h" #include "level_format.h"
#include "cutscene_preview.h" #include "cutscene_preview.h"
#include "preview_protocol.h" #include "preview_protocol.h"
#include "lighting_format.h"
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@ -33,6 +34,10 @@ struct LevelSnapshot {
std::vector<SpriteEntry> sprites; std::vector<SpriteEntry> sprites;
std::vector<LevelTransition> transitions; std::vector<LevelTransition> transitions;
CutsceneTrack cutscene; CutsceneTrack cutscene;
std::vector<EditorLightSource> lightSources;
LightingSettings lightingSettings;
std::array<uint32_t,1024> bakedRGB;
uint64_t bakedSignature=0;
}; };
class EditorApp { class EditorApp {
@ -107,10 +112,31 @@ public:
uint8_t pendingSpawnArchetype = ENTITY_ARCHETYPE_PROP; uint8_t pendingSpawnArchetype = ENTITY_ARCHETYPE_PROP;
int selectedSpriteIdx = -1; int selectedSpriteIdx = -1;
int selectedTransitionIdx = -1; int selectedTransitionIdx = -1;
int selectedLightIdx = -1;
int draggingLightIdx = -1;
bool isEngineRunning = false; bool isEngineRunning = false;
std::string statusMessage = "Готов к работе. EblanStudio v1.2"; std::string statusMessage = "Готов к работе. EblanStudio v1.2";
std::string currentFilePath = "..\\assets\\map.dat"; std::string currentFilePath = "..\\assets\\map.dat";
// Lighting Subsystem State & Light Sources
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
int ambientLightLevel = 40;
bool isFlashlightOn = true;
int flashlightRadius = 384;
int flashlightIntensity = 240;
int flashlightCone = 45;
bool showLightHeatmap = false;
uint8_t bakedLightmap[MAP_TOTAL_CELLS] = { 0 };
std::array<uint32_t,1024> bakedLightRGB{};
uint64_t bakedLightingSignature = 0;
std::vector<EditorLightSource> lightSources;
void BakeLighting();
void InitDefaultLights();
bool CastLightRay(int x1, int y1, int x2, int y2) const;
bool LightingNeedsBake() const;
LightingSettings GetLightingSettings() const;
// Viewport pan & zoom // Viewport pan & zoom
float canvasZoom = 20.0f; // pixels per tile float canvasZoom = 20.0f; // pixels per tile
float canvasPanX = 20.0f; float canvasPanX = 20.0f;
@ -121,6 +147,8 @@ public:
struct ID3D11ShaderResourceView* textureSRVs[16] = { nullptr }; struct ID3D11ShaderResourceView* textureSRVs[16] = { nullptr };
private: private:
void RenderLightingInspector();
void RenderLightingSettings();
bool EnsureEnginePreview(); bool EnsureEnginePreview();
void StopEnginePreview(); void StopEnginePreview();
void UpdateEnginePreview(); void UpdateEnginePreview();

View file

@ -18,6 +18,8 @@ struct CodeFileEntry {
}; };
static const CodeFileEntry g_ProjectCodeFiles[] = { static const CodeFileEntry g_ProjectCodeFiles[] = {
{ "engine_light.inc (Light Sources & Bursts)", "include/engine_light.inc" },
{ "engine_lighting_core.inc (Static RGB Bake & Dynamic Lighting)", "include/engine_lighting_core.inc" },
{ "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" }, { "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" },
{ "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" }, { "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" },
{ "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" }, { "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" },

View file

@ -0,0 +1,72 @@
void EditorApp::RenderLightingSettings() {
if(!ImGui::CollapsingHeader("Освещение карты",ImGuiTreeNodeFlags_DefaultOpen)) return;
auto edited=GetLightingSettings(); bool changed=false,activated=false;
auto undoItem=[&](){activated|=ImGui::IsItemActivated();};
const char* modes[]={"Без освещения","Только запечённый","Запечённый + динамический"};
int mode=edited.mode;
changed|=ImGui::Combo("Режим света",&mode,modes,3);undoItem();edited.mode=mode;
int ambient=edited.ambient;
changed|=ImGui::SliderInt("Фоновый свет",&ambient,0,255);undoItem();edited.ambient=ambient;
bool flash=edited.flashlight!=0;
changed|=ImGui::Checkbox("Фонарик игрока",&flash);undoItem();edited.flashlight=flash;
if(flash) {
int radius=edited.flashlightRadius,intensity=edited.flashlightIntensity,cone=edited.flashlightCone;
changed|=ImGui::SliderInt("Дальность фонаря",&radius,32,2048);undoItem();
changed|=ImGui::SliderInt("Яркость фонаря",&intensity,0,255);undoItem();
changed|=ImGui::SliderInt("Полуугол фонаря",&cone,1,96);undoItem();
edited.flashlightRadius=radius;edited.flashlightIntensity=intensity;edited.flashlightCone=cone;
}
if(activated)PushUndo();
if(changed) {
lightingMode=edited.mode;ambientLightLevel=edited.ambient;isFlashlightOn=edited.flashlight!=0;
flashlightRadius=edited.flashlightRadius;flashlightIntensity=edited.flashlightIntensity;flashlightCone=edited.flashlightCone;
}
if(ImGui::Button("Запечь постоянный свет",ImVec2(-1,0))) {PushUndo();BakeLighting();}
if(LightingNeedsBake()) ImGui::TextWrapped("Геометрия или постоянный свет изменены: нужно запечь. При сохранении и F5 запекание выполняется автоматически.");
else ImGui::TextDisabled("Постоянный свет запечён");
ImGui::Checkbox("Карта запечённого света",&showLightHeatmap);
ImGui::TextWrapped("Постоянные источники входят в запекание. Точечные, мерцающие и пульсирующие работают в динамическом режиме.");
}
void EditorApp::RenderLightingInspector() {
auto& light=lightSources[selectedLightIdx];
ImGui::Text("Источник света #%d / %d",selectedLightIdx+1,MAX_EDITOR_LIGHTS);
if(ImGui::Button("Удалить источник")) {
PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=-1;return;
}
ImGui::SameLine();
if(ImGui::Button("Копировать")) {
if(lightSources.size()<MAX_EDITOR_LIGHTS) {
auto copy=light;copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32);
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(lightSources.size())-1;
} else statusMessage="Лимит: 13 источников карты; фонарик и вспышки имеют отдельные места.";
return;
}
EditorLightSource edited=light;bool changed=false,activated=false;
auto undoItem=[&](){activated|=ImGui::IsItemActivated();};
bool active=(edited.flags&LIGHT_FLAG_ACTIVE)!=0;
changed|=ImGui::Checkbox("Включён",&active);undoItem();
edited.flags=(edited.flags&~LIGHT_FLAG_ACTIVE)|(active?LIGHT_FLAG_ACTIVE:0);
float x=edited.x/64.0f,y=edited.y/64.0f;
if(ImGui::SliderFloat("Позиция X",&x,0,31.98f,"%.2f")){edited.x=static_cast<int>(x*64);changed=true;}undoItem();
if(ImGui::SliderFloat("Позиция Y",&y,0,31.98f,"%.2f")){edited.y=static_cast<int>(y*64);changed=true;}undoItem();
changed|=ImGui::SliderInt("Радиус",&edited.radius,32,2048);undoItem();
changed|=ImGui::SliderInt("Интенсивность",&edited.intensity,0,255);undoItem();edited.curIntensity=edited.intensity;
float color[]={((edited.color>>16)&255)/255.f,((edited.color>>8)&255)/255.f,(edited.color&255)/255.f};
if(ImGui::ColorEdit3("Цвет света",color)) {
edited.color=(uint32_t(color[0]*255)<<16)|(uint32_t(color[1]*255)<<8)|uint32_t(color[2]*255);changed=true;
}undoItem();
const char* types[]={"Постоянный (запекание)","Точечный (динамический)","Пульсирующий (динамический)"};
int type=edited.lightType==LIGHT_TYPE_STATIC?0:edited.lightType==LIGHT_TYPE_PULSE?2:1;
if(ImGui::Combo("Тип света",&type,types,3)){edited.lightType=type==0?LIGHT_TYPE_STATIC:type==1?LIGHT_TYPE_POINT:LIGHT_TYPE_PULSE;changed=true;}undoItem();
bool shadows=(edited.flags&LIGHT_FLAG_SHADOWS)!=0,flicker=(edited.flags&LIGHT_FLAG_FLICKER)!=0;
changed|=ImGui::Checkbox("Тени от стен и дверей",&shadows);undoItem();
edited.flags=(edited.flags&~LIGHT_FLAG_SHADOWS)|(shadows?LIGHT_FLAG_SHADOWS:0);
if(edited.lightType!=LIGHT_TYPE_STATIC) {
changed|=ImGui::Checkbox("Мерцание факела",&flicker);undoItem();
edited.flags=(edited.flags&~LIGHT_FLAG_FLICKER)|(flicker?LIGHT_FLAG_FLICKER:0);
}
if(activated)PushUndo();
if(changed)light=edited;
RenderLightingSettings();
}

View file

@ -41,7 +41,7 @@ bool EditorApp::EnsureEnginePreview() {
if (!previewPacket) return fail("Не удалось открыть буфер игрового вида."); if (!previewPacket) return fail("Не удалось открыть буфер игрового вида.");
// Initialize in place: no large packet on the thread's stack. // Initialize in place: no large packet on the thread's stack.
memset(previewPacket, 0, sizeof(*previewPacket)); memset(previewPacket, 0, sizeof(*previewPacket));
previewPacket->magic = 0x56504245; previewPacket->version = 1; previewPacket->parentPid = GetCurrentProcessId(); previewPacket->magic = 0x56504245; previewPacket->version = 2; previewPacket->parentPid = GetCurrentProcessId();
std::string command = "\"" + engine + "\" \"--preview:" + name + "\""; std::string command = "\"" + engine + "\" \"--preview:" + name + "\"";
STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE;
if (!CreateProcessA(engine.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, if (!CreateProcessA(engine.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
@ -105,6 +105,14 @@ void EditorApp::UpdateEnginePreview() {
if (!sprites.empty()) memcpy(previewPacket->sprites,sprites.data(),previewPacket->spriteCount*sizeof(SpriteEntry)); if (!sprites.empty()) memcpy(previewPacket->sprites,sprites.data(),previewPacket->spriteCount*sizeof(SpriteEntry));
previewPacket->cinematic = scene && cutscenePreview.active; previewPacket->cinematic = scene && cutscenePreview.active;
previewPacket->fade = scene ? cutscenePreview.fade : 0; previewPacket->fade = scene ? cutscenePreview.fade : 0;
previewPacket->lightingMode = static_cast<uint32_t>(lightingMode);
previewPacket->ambientLight = static_cast<uint32_t>(ambientLightLevel);
previewPacket->flashlight = isFlashlightOn ? 1 : 0;
previewPacket->lightCount = static_cast<uint32_t>((std::min)(lightSources.size(), size_t(MAX_EDITOR_LIGHTS)));
previewPacket->flashlightRadius=flashlightRadius;previewPacket->flashlightIntensity=flashlightIntensity;previewPacket->flashlightCone=flashlightCone;
if(!lightSources.empty()) memcpy(previewPacket->lights,lightSources.data(),previewPacket->lightCount*sizeof(EditorLightSource));
memcpy(previewPacket->bakedRGB,bakedLightRGB.data(),4096);
previewPacket->lightTick=static_cast<uint32_t>(GetTickCount64()*60/1000);
memset(previewPacket->subtitle,0,96); memset(previewPacket->subtitle,0,96);
if (scene && cutscenePreview.subtitle >= 0 && cutscenePreview.subtitle < static_cast<int>(cutscene.strings.size())) { if (scene && cutscenePreview.subtitle >= 0 && cutscenePreview.subtitle < static_cast<int>(cutscene.strings.size())) {
std::string encoded; std::string encoded;

View file

@ -6,7 +6,7 @@
// ============================================================================== // ==============================================================================
constexpr uint32_t MAP_MAGIC = 0x4E4C4245; // 'EBLN' constexpr uint32_t MAP_MAGIC = 0x4E4C4245; // 'EBLN'
constexpr uint16_t MAP_FORMAT_VERSION = 4; // v4 adds a cutscene track and CP1251 string pool constexpr uint16_t MAP_FORMAT_VERSION = 5; // v5 adds authored lighting and baked RGB cells
constexpr int MAP_GRID_W = 32; constexpr int MAP_GRID_W = 32;
constexpr int MAP_GRID_H = 32; constexpr int MAP_GRID_H = 32;
constexpr int MAP_TOTAL_CELLS = MAP_GRID_W * MAP_GRID_H; // 1024 bytes constexpr int MAP_TOTAL_CELLS = MAP_GRID_W * MAP_GRID_H; // 1024 bytes
@ -66,7 +66,7 @@ struct LevelTransition {
struct MapHeader { struct MapHeader {
uint32_t magic; // 'EBLN' uint32_t magic; // 'EBLN'
uint16_t version; // Current writer: 4; reader also accepts v1/v2/v3 uint16_t version; // Current writer: 5; reader also accepts v1/v2/v3/v4
uint16_t mapWidth; // 32 uint16_t mapWidth; // 32
uint16_t mapHeight; // 32 uint16_t mapHeight; // 32
uint16_t playerStartX; // Cell coordinate (0..31) uint16_t playerStartX; // Cell coordinate (0..31)
@ -79,7 +79,8 @@ struct MapHeader {
uint16_t cutsceneCount; // v4 only; old versions ignore the remaining header fields uint16_t cutsceneCount; // v4 only; old versions ignore the remaining header fields
uint16_t cutsceneStringCount; uint16_t cutsceneStringCount;
uint16_t cutsceneFlags; // bit 0 = start automatically after entering this map uint16_t cutsceneFlags; // bit 0 = start automatically after entering this map
uint8_t reserved[4]; // Total header remains 38 bytes uint16_t lightCount; // v5 only, authored lights (slots 1..13 in engine)
uint16_t lightingReserved; // must be zero in v5; header remains 38 bytes
}; };
struct CutsceneEvent { struct CutsceneEvent {
@ -135,3 +136,53 @@ static_assert(sizeof(MapHeader) == 38, "EBLN header layout changed");
static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed"); static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed");
static_assert(sizeof(LevelTransition) == 42, "EBLN transition layout changed"); static_assert(sizeof(LevelTransition) == 42, "EBLN transition layout changed");
static_assert(sizeof(CutsceneEvent) == 16, "EBLN cutscene event layout changed"); static_assert(sizeof(CutsceneEvent) == 16, "EBLN cutscene event layout changed");
// Lighting Modes & Types (mirrors include/engine_defs.inc)
enum LightingMode : uint32_t {
LIGHTING_MODE_NONE = 0,
LIGHTING_MODE_BAKED = 1,
LIGHTING_MODE_DYNAMIC = 2
};
enum LightType : uint32_t {
LIGHT_TYPE_STATIC = 0,
LIGHT_TYPE_POINT = 1,
LIGHT_TYPE_FLASHLIGHT = 2,
LIGHT_TYPE_BURST = 3,
LIGHT_TYPE_PULSE = 4
};
constexpr uint32_t LIGHT_FLAG_INACTIVE = 0;
constexpr uint32_t LIGHT_FLAG_ACTIVE = 1;
constexpr uint32_t LIGHT_FLAG_SHADOWS = 2;
constexpr uint32_t LIGHT_FLAG_FLICKER = 4;
constexpr int MAX_EDITOR_LIGHTS = 13; // engine slots 0 / 14 / 15 are flashlight / muzzle / explosion
#pragma pack(push, 1)
struct EditorLightSource {
int32_t x = 0; // raw world units (cell * 64 + 32)
int32_t y = 0;
int32_t radius = 240; // radius in raw world units
int32_t intensity = 220; // 0..255 base intensity
int32_t curIntensity = 220;// 0..255 current intensity
int32_t lightType = LIGHT_TYPE_POINT;
uint32_t flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS | LIGHT_FLAG_FLICKER;
uint32_t color = 0x00FFB040;// 00RRGGBBh tint (warm torch)
int32_t decaySpeed = 0;
int32_t flickerPhase = 0;
};
#pragma pack(pop)
static_assert(sizeof(EditorLightSource) == 40, "EditorLightSource size mismatch with MASM LightSource");
struct LightingSettings {
uint32_t mode = LIGHTING_MODE_NONE;
uint32_t ambient = 40;
uint32_t flashlight = 1;
uint32_t flashlightRadius = 384;
uint32_t flashlightIntensity = 240;
uint32_t flashlightCone = 45;
uint32_t baked = 1;
uint32_t reserved = 0;
};
static_assert(sizeof(LightingSettings) == 32, "Lighting settings wire layout changed");

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "level_format.h" #include "level_format.h"
#include "cutscene_format.h" #include "cutscene_format.h"
#include "lighting_format.h"
#include <array> #include <array>
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
@ -14,6 +15,7 @@ struct BinaryLevel {
std::vector<SpriteEntry> sprites; std::vector<SpriteEntry> sprites;
std::vector<LevelTransition> transitions; std::vector<LevelTransition> transitions;
CutsceneTrack cutscene; CutsceneTrack cutscene;
LevelLighting lighting;
}; };
inline bool IsMapFilename(const char* name) { inline bool IsMapFilename(const char* name) {
@ -63,6 +65,8 @@ inline bool ValidateLevel(const BinaryLevel& level, std::string& error) {
} }
for (size_t i = 0; i < level.cells.size(); ++i) for (size_t i = 0; i < level.cells.size(); ++i)
if (level.cells[i] == CELL_PORTAL && !triggers[i]) return fail("Портал не имеет настроенного перехода"); if (level.cells[i] == CELL_PORTAL && !triggers[i]) return fail("Портал не имеет настроенного перехода");
if(!ValidateLighting(level.lighting.settings,level.lighting.lights.data(),level.lighting.lights.size(),error)) return false;
for(auto c:level.lighting.rgb) if(c&0xFF000000) {error="Неверный цвет запечённого света";return false;}
return ValidateCutscene(level.cutscene, level.cells.data(), level.transitions.size(), error); return ValidateCutscene(level.cutscene, level.cells.data(), level.transitions.size(), error);
} }
@ -85,9 +89,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
if (eventCount > MAX_CUTSCENE_EVENTS || stringCount > MAX_CUTSCENE_STRINGS) { if (eventCount > MAX_CUTSCENE_EVENTS || stringCount > MAX_CUTSCENE_STRINGS) {
error = "Превышен лимит событий или строк кат-сцены"; return false; error = "Превышен лимит событий или строк кат-сцены"; return false;
} }
const size_t lightCount=h.version>=5?h.lightCount:0;
if(lightCount>MAX_EDITOR_LIGHTS || (h.version>=5&&h.lightingReserved)) {error="Неверный заголовок освещения";return false;}
const size_t expected = sizeof(MapHeader) + MAP_TOTAL_CELLS + const size_t expected = sizeof(MapHeader) + MAP_TOTAL_CELLS +
h.spriteCount * sizeof(SpriteEntry) + transitionCount * sizeof(LevelTransition) + h.spriteCount * sizeof(SpriteEntry) + transitionCount * sizeof(LevelTransition) +
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES; eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES +
(h.version>=5?sizeof(LightingSettings)+lightCount*sizeof(EditorLightSource)+4096:0);
if (fileSize != static_cast<std::streamoff>(expected)) { if (fileSize != static_cast<std::streamoff>(expected)) {
error = "Размер файла не соответствует заголовку карты"; return false; error = "Размер файла не соответствует заголовку карты"; return false;
} }
@ -108,6 +115,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; } if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; }
candidate.cutscene.strings.push_back(CutsceneDecode(bytes, static_cast<int>(end - bytes))); candidate.cutscene.strings.push_back(CutsceneDecode(bytes, static_cast<int>(end - bytes)));
} }
if(h.version>=5) {
in.read(reinterpret_cast<char*>(&candidate.lighting.settings),sizeof(LightingSettings));
candidate.lighting.lights.resize(lightCount);
if(lightCount)in.read(reinterpret_cast<char*>(candidate.lighting.lights.data()),lightCount*sizeof(EditorLightSource));
in.read(reinterpret_cast<char*>(candidate.lighting.rgb.data()),4096);
}
if (!in) { error = "Не удалось прочитать карту целиком"; return false; } if (!in) { error = "Не удалось прочитать карту целиком"; return false; }
if (h.version == 1) for (auto& sprite : candidate.sprites) { sprite.x >>= 16; sprite.y >>= 16; } if (h.version == 1) for (auto& sprite : candidate.sprites) { sprite.x >>= 16; sprite.y >>= 16; }
if (!ValidateLevel(candidate, error)) return false; if (!ValidateLevel(candidate, error)) return false;

View file

@ -0,0 +1,97 @@
#pragma once
#include "level_format.h"
#include <array>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
struct LevelLighting {
LightingSettings settings;
std::vector<EditorLightSource> lights;
std::array<uint32_t,1024> rgb{}; // 00RRGGBB, baked static contribution + ambient
LevelLighting() { rgb.fill(0x282828); }
};
inline bool ValidateLighting(const LightingSettings& s, const EditorLightSource* lights, size_t count, std::string& error) {
if (s.mode>2 || s.ambient>255 || s.flashlight>1 || s.flashlightRadius<32 || s.flashlightRadius>2048 ||
s.flashlightIntensity>255 || s.flashlightCone<1 || s.flashlightCone>96 || s.baked!=1 || s.reserved || count>MAX_EDITOR_LIGHTS) {
error="Неверные параметры освещения"; return false;
}
for(size_t i=0;i<count;++i) {
const auto& l=lights[i];
if(l.x<0 || l.x>=2048 || l.y<0 || l.y>=2048 || l.radius<1 || l.radius>2048 || l.intensity<0 || l.intensity>255 ||
l.curIntensity<0 || l.curIntensity>255 || (l.lightType!=LIGHT_TYPE_STATIC && l.lightType!=LIGHT_TYPE_POINT && l.lightType!=LIGHT_TYPE_PULSE) || (l.flags&~7u) || (l.color&0xFF000000)) {
error="Неверные координаты, цвет или параметры источника света"; return false;
}
}
return true;
}
inline bool LightingWalkable(uint8_t cell) { return cell==0 || cell==6 || cell==10; }
// Exact supercover grid traversal; corner crossings cannot leak diagonally through walls.
inline bool LightingVisible(const uint8_t* cells,int x1,int y1,int x2,int y2) {
if(x1<0||x1>=2048||y1<0||y1>=2048||x2<0||x2>=2048||y2<0||y2>=2048) return false;
int cx=x1/64,cy=y1/64,ex=x2/64,ey=y2/64;
const int dx=std::abs(x2-x1),dy=std::abs(y2-y1),sx=x2>=x1?1:-1,sy=y2>=y1?1:-1;
int nx=sx>0?(cx+1)*64-x1:x1-cx*64,ny=sy>0?(cy+1)*64-y1:y1-cy*64;
auto free=[&](int x,int y){return x>=0&&x<32&&y>=0&&y<32&&LightingWalkable(cells[y*32+x]);};
// A wall-mounted source may start in a wall; every subsequent cell is tested.
for(int steps=0;steps<65;++steps) {
if(cx==ex&&cy==ey) return free(cx,cy);
const int a=dx?nx*dy:0x3FFFFFFF,b=dy?ny*dx:0x3FFFFFFF;
if(a==b) {
if(!free(cx+sx,cy)||!free(cx,cy+sy)) return false;
cx+=sx;cy+=sy;nx+=64;ny+=64;
} else if(a<b) {cx+=sx;nx+=64;} else {cy+=sy;ny+=64;}
if(!free(cx,cy)) return false;
}
return false;
}
inline int LightDistance(int dx,int dy) {
dx=std::abs(dx);dy=std::abs(dy);
return (std::max)(dx,dy)+(3*(std::min)(dx,dy))/8;
}
inline std::array<uint32_t,1024> BakeLightingRGB(const uint8_t* cells,const std::vector<EditorLightSource>& lights,int ambient) {
std::array<uint32_t,1024> rgb;
const uint32_t base=ambient*0x010101u;rgb.fill(base);
for(int i=0;i<1024;++i) {
if(!LightingWalkable(cells[i])) continue;
int x=(i%32)*64+32,y=(i/32)*64+32,r=ambient,g=ambient,b=ambient;
for(const auto& l:lights) {
if(!(l.flags&LIGHT_FLAG_ACTIVE)||l.lightType!=LIGHT_TYPE_STATIC||l.radius<=0) continue;
int d=LightDistance(x-l.x,y-l.y);if(d>=l.radius) continue;
if((l.flags&LIGHT_FLAG_SHADOWS)&&!LightingVisible(cells,l.x,l.y,x,y)) continue;
int a=(l.radius-d)*l.intensity/l.radius;
r=(std::min)(255,r+a*int((l.color>>16)&255)/255);
g=(std::min)(255,g+a*int((l.color>>8)&255)/255);
b=(std::min)(255,b+a*int(l.color&255)/255);
}
rgb[i]=(r<<16)|(g<<8)|b;
}
auto smooth=rgb;
for(int y=0;y<32;++y)for(int x=0;x<32;++x) {
int i=y*32+x;if(!LightingWalkable(cells[i])) continue;
int r=((rgb[i]>>16)&255)*2,g=((rgb[i]>>8)&255)*2,b=(rgb[i]&255)*2,n=2;
for(auto delta: {std::pair<int,int>{-1,0},{1,0},{0,-1},{0,1}}) {
int xx=x+delta.first,yy=y+delta.second;
if(xx<0||xx>=32||yy<0||yy>=32||!LightingWalkable(cells[yy*32+xx])) continue;
uint32_t c=rgb[yy*32+xx];r+=(c>>16)&255;g+=(c>>8)&255;b+=c&255;++n;
}
smooth[i]=((r/n)<<16)|((g/n)<<8)|(b/n);
}
return smooth;
}
inline uint64_t LightingBakeSignature(const uint8_t* cells,const std::vector<EditorLightSource>& lights,int ambient) {
uint64_t hash=1469598103934665603ull;
auto add=[&](uint32_t v){hash^=v;hash*=1099511628211ull;};
for(int i=0;i<1024;++i)add(cells[i]);add(ambient);
for(const auto& l:lights)if(l.lightType==LIGHT_TYPE_STATIC) {
add(l.x);add(l.y);add(l.radius);add(l.intensity);add(l.flags&3);add(l.color);
}
return hash;
}

View file

@ -8,21 +8,33 @@
// Never modify payload while state == 1. All fields have fixed wire widths. // Never modify payload while state == 1. All fields have fixed wire widths.
struct EnginePreviewPacket { struct EnginePreviewPacket {
uint32_t magic = 0x56504245; // EBPV uint32_t magic = 0x56504245; // EBPV
uint32_t version = 1; uint32_t version = 2;
volatile long state = 0; volatile long state = 0;
uint32_t parentPid = 0; uint32_t parentPid = 0;
int32_t x = 160, y = 160, angle = 0; int32_t x = 160, y = 160, angle = 0;
uint32_t ceiling = 0, floor = 0, spriteCount = 0; uint32_t ceiling = 0, floor = 0, spriteCount = 0;
uint32_t cinematic = 0, fade = 0, textureRevision = 0; uint32_t cinematic = 0, fade = 0, textureRevision = 0;
uint32_t reserved[19]{}; uint32_t lightingMode = 2; // offset 52: 0=None, 1=Baked, 2=Dynamic
uint32_t ambientLight = 40; // offset 56: base shadow level
uint32_t flashlight = 1; // offset 60: 1=flashlight active
uint32_t lightCount = 0; // offset 64: count of active lights
uint32_t flashlightRadius = 384; // 68
uint32_t flashlightIntensity = 240; // 72
uint32_t flashlightCone = 45; // 76
uint32_t lightingRevision = 0; // 80
uint32_t lightTick = 0; // 84, deterministic 60 Hz time
uint32_t reserved[10]{};
uint8_t cells[1024]{}; uint8_t cells[1024]{};
SpriteEntry sprites[32]{}; SpriteEntry sprites[32]{};
char subtitle[96]{}; char subtitle[96]{};
uint32_t textures[16*128*128]{}; uint32_t textures[16*128*128]{};
uint32_t pixels[320*240]{}; uint32_t pixels[320*240]{};
EditorLightSource lights[MAX_EDITOR_LIGHTS]{};
uint32_t bakedRGB[1024]{};
}; };
static_assert(sizeof(long) == 4, "Preview protocol requires Windows LONG"); static_assert(sizeof(long) == 4, "Preview protocol requires Windows LONG");
static_assert(offsetof(EnginePreviewPacket, cells) == 128, "Preview header mismatch"); static_assert(offsetof(EnginePreviewPacket, cells) == 128, "Preview header mismatch");
static_assert(offsetof(EnginePreviewPacket, textures) == 2016, "Preview texture offset mismatch"); static_assert(offsetof(EnginePreviewPacket, textures) == 2016, "Preview texture offset mismatch");
static_assert(offsetof(EnginePreviewPacket, pixels) == 1050592, "Preview framebuffer offset mismatch"); static_assert(offsetof(EnginePreviewPacket, pixels) == 1050592, "Preview framebuffer offset mismatch");
static_assert(sizeof(EnginePreviewPacket) == 1357792, "Preview mapping size mismatch"); static_assert(offsetof(EnginePreviewPacket,lights)==1357792,"Light payload offset changed");
static_assert(sizeof(EnginePreviewPacket) == 1362408, "Preview mapping size mismatch");

View file

@ -19,6 +19,9 @@ struct EblanProject {
int gameMode = 1; // 0 = Adventure (No weapons / Peaceful), 1 = Action (Weapons) int gameMode = 1; // 0 = Adventure (No weapons / Peaceful), 1 = Action (Weapons)
bool showWeapons = true; bool showWeapons = true;
bool showCrosshair = true; bool showCrosshair = true;
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
int ambientLight = 40;
bool flashlight = true;
bool Save(const std::string& path) const { bool Save(const std::string& path) const {
HANDLE hFile = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); HANDLE hFile = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
@ -38,6 +41,11 @@ struct EblanProject {
content += std::string("GameMode=") + numBuf + "\r\n"; content += std::string("GameMode=") + numBuf + "\r\n";
content += std::string("ShowWeapons=") + (showWeapons ? "1" : "0") + "\r\n"; content += std::string("ShowWeapons=") + (showWeapons ? "1" : "0") + "\r\n";
content += std::string("ShowCrosshair=") + (showCrosshair ? "1" : "0") + "\r\n"; content += std::string("ShowCrosshair=") + (showCrosshair ? "1" : "0") + "\r\n";
snprintf(numBuf, sizeof(numBuf), "%d", lightingMode);
content += std::string("LightingMode=") + numBuf + "\r\n";
snprintf(numBuf, sizeof(numBuf), "%d", ambientLight);
content += std::string("AmbientLight=") + numBuf + "\r\n";
content += std::string("Flashlight=") + (flashlight ? "1" : "0") + "\r\n";
DWORD written = 0; DWORD written = 0;
WriteFile(hFile, content.data(), static_cast<DWORD>(content.size()), &written, nullptr); WriteFile(hFile, content.data(), static_cast<DWORD>(content.size()), &written, nullptr);
CloseHandle(hFile); CloseHandle(hFile);
@ -75,6 +83,9 @@ struct EblanProject {
else if (key == "GameMode") gameMode = std::atoi(val.c_str()); else if (key == "GameMode") gameMode = std::atoi(val.c_str());
else if (key == "ShowWeapons") showWeapons = (val == "1" || val == "true"); else if (key == "ShowWeapons") showWeapons = (val == "1" || val == "true");
else if (key == "ShowCrosshair") showCrosshair = (val == "1" || val == "true"); else if (key == "ShowCrosshair") showCrosshair = (val == "1" || val == "true");
else if (key == "LightingMode") lightingMode = std::atoi(val.c_str());
else if (key == "AmbientLight") ambientLight = std::atoi(val.c_str());
else if (key == "Flashlight") flashlight = (val == "1" || val == "true");
} }
} }

Binary file not shown.

View file

@ -85,6 +85,7 @@ setup_shotgun:
mov hitTolerance, 40 mov hitTolerance, 40
mov playerAttackCooldown, 22 mov playerAttackCooldown, 22
mov playerFlashTimer, 7 mov playerFlashTimer, 7
invoke SpawnMuzzleFlashLight, playerX, playerY
jmp fire_hitscan jmp fire_hitscan
setup_pistol: setup_pistol:
@ -97,6 +98,7 @@ setup_pistol:
mov hitTolerance, 22 mov hitTolerance, 22
mov playerAttackCooldown, 12 mov playerAttackCooldown, 12
mov playerFlashTimer, 5 mov playerFlashTimer, 5
invoke SpawnMuzzleFlashLight, playerX, playerY
fire_hitscan: fire_hitscan:
; 3. Hitscan check against enemies & destructible barrels in crosshair ; 3. Hitscan check against enemies & destructible barrels in crosshair
@ -288,6 +290,7 @@ explode_barrel:
push edi push edi
mov esi, [ebx].SpriteStruct.x mov esi, [ebx].SpriteStruct.x
mov edi, [ebx].SpriteStruct.y mov edi, [ebx].SpriteStruct.y
invoke SpawnExplosionLight, esi, edi
mov i, 0 mov i, 0
aoe_loop: aoe_loop:

View file

@ -71,8 +71,20 @@ keyPause db 0
keyPauseHeld db 0 keyPauseHeld db 0
keyHudStyleHeld db 0 keyHudStyleHeld db 0
keyMenuHeld db 0 keyMenuHeld db 0
keyFlashlight db 0
keyFlashlightHeld db 0
keyLightMode db 0
keyLightModeHeld db 0
isGamePaused dd 0 isGamePaused dd 0
; Lighting Configuration & Dynamic State
lightingMode dd LIGHTING_MODE_NONE ; 0 = None, 1 = Baked, 2 = Dynamic+Baked (InitLightingSystem enables DYNAMIC)
ambientLightLevel dd 40 ; Base shadow level (40/256 = ~15% darkness)
isFlashlightOn dd 1 ; 1 = Player flashlight active
flashlightRadius dd 384 ; 6 cells distance
flashlightIntensity dd 240 ; Bright focused beam
flashlightConeAngle dd 45 ; Half-angle of beam (~32 degrees in 512-angle circle)
; Game Mode & HUD Configuration ; Game Mode & HUD Configuration
gameMode dd GAME_MODE_ACTION ; 0 = Adventure (No weapons), 1 = Action / Combat gameMode dd GAME_MODE_ACTION ; 0 = Adventure (No weapons), 1 = Action / Combat
hudStyle dd HUD_STYLE_CLEAN ; 0 = Clean / Immersive, 1 = Debug / Classic hudStyle dd HUD_STYLE_CLEAN ; 0 = Clean / Immersive, 1 = Debug / Classic
@ -250,6 +262,11 @@ entityTimer dd MAX_SPRITES dup(?)
entitySpeed dd MAX_SPRITES dup(?) entitySpeed dd MAX_SPRITES dup(?)
entityAggro dd MAX_SPRITES dup(?) entityAggro dd MAX_SPRITES dup(?)
ALIGN 4
lightGrid db MAP_SIZE dup(?) ; 1024 bytes: baked light level (0..255) per cell
ALIGN 4
lightSources db MAX_LIGHTS * (sizeof LightSource) dup(?)
; Gameplay Variables & Built-in Toast Strings (CP1251) ; Gameplay Variables & Built-in Toast Strings (CP1251)
.data .data
ALIGN 4 ALIGN 4
@ -275,6 +292,12 @@ szWeaponsEnabledToast db "РЕЖИМ: ОРУЖИЕ ВКЛЮЧЕНО", 0
szWeaponsDisabledToast db "РЕЖИМ: БЕЗ ОРУЖИЯ (КВЕСТ)", 0 szWeaponsDisabledToast db "РЕЖИМ: БЕЗ ОРУЖИЯ (КВЕСТ)", 0
szHudCleanToast db "ИНТЕРФЕЙС: ЧИСТЫЙ ВИД", 0 szHudCleanToast db "ИНТЕРФЕЙС: ЧИСТЫЙ ВИД", 0
szHudDebugToast db "ИНТЕРФЕЙС: РЕЖИМ ОТЛАДКИ", 0 szHudDebugToast db "ИНТЕРФЕЙС: РЕЖИМ ОТЛАДКИ", 0
szFlashlightOn db 0D4h,0CEh,0CDh,0C0h,0D0h,0C8h,0CAh,03Ah,020h,0C2h,0CAh,0CBh,020h,05Bh,046h,05Dh,0 ; ФОНАРИК: ВКЛ [F] (CP1251)
szFlashlightOff db 0D4h,0CEh,0CDh,0C0h,0D0h,0C8h,0CAh,03Ah,020h,0C2h,0DBh,0CAh,0CBh,020h,05Bh,046h,05Dh,0 ; ФОНАРИК: ВЫКЛ [F] (CP1251)
szLightMode0 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C2h,0DBh,0CAh,0CBh,0DEh,0D7h,0C5h,0CDh,020h,028h,057h,06Fh,06Ch,066h,033h,044h,029h,0 ; СВЕТ: ВЫКЛЮЧЕН (Wolf3D) (CP1251)
szLightMode1 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C7h,0C0h,0CFh,0C5h,0D7h,0A8h,0CDh,0CDh,0DBh,0C9h,020h,028h,04Ch,069h,067h,068h,074h,06Dh,061h,070h,029h,0 ; СВЕТ: ЗАПЕЧЁННЫЙ (Lightmap) (CP1251)
szLightMode2 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C4h,0C8h,0CDh,0C0h,0CCh,0C8h,0D7h,0C5h,0D1h,0CAh,0C8h,0C9h,020h,0D1h,020h,0D4h,0C8h,0C7h,0C8h,0CAh,0CEh,0C9h,0 ; СВЕТ: ДИНАМИЧЕСКИЙ С ФИЗИКОЙ (CP1251)
szBakeDoneToast db 0D1h,0C2h,0C5h,0D2h,020h,0C7h,0C0h,0CFh,0C5h,0D7h,0A8h,0CDh,03Ah,020h,031h,030h,032h,034h,020h,0DFh,0D7h,0C5h,0C9h,0CAh,0C8h,0 ; СВЕТ ЗАПЕЧЁН: 1024 ЯЧЕЙКИ (CP1251)
szProjectFile db "project.eblanproj", 0 szProjectFile db "project.eblanproj", 0
szProjGameMode0 db "GameMode=0", 0 szProjGameMode0 db "GameMode=0", 0
szProjGameMode1 db "GameMode=1", 0 szProjGameMode1 db "GameMode=1", 0
@ -285,3 +308,23 @@ szFlagAction db "action", 0
szFlagDebugHud db "debug-hud", 0 szFlagDebugHud db "debug-hud", 0
szFlagCleanHud db "clean-hud", 0 szFlagCleanHud db "clean-hud", 0
previewPrefix db "--preview:", 0 previewPrefix db "--preview:", 0
; Persistent and staged lighting buffers (declared before the map loader).
.data
lightingTick dd 0
authoredLightCount dd 0
legacyLightingSettings dd 0,40,1,384,240,45,1,0
lightRed dd 256
lightGreen dd 256
lightBlue dd 256
lightAccumR dd 0
lightAccumG dd 0
lightAccumB dd 0
lightWeight dd 0
.data?
ALIGN 4
lightGridRGB dd MAP_SIZE dup(?)
lightSmoothRGB dd MAP_SIZE dup(?)
stagedLightSettings dd 8 dup(?)
stagedLightSources db 13*40 dup(?)
stagedLightRGB dd MAP_SIZE dup(?)

View file

@ -74,6 +74,27 @@ VK_KEY_4 equ 34h
VK_KEY_5 equ 35h VK_KEY_5 equ 35h
VK_KEY_F1 equ 70h VK_KEY_F1 equ 70h
VK_KEY_H equ 48h VK_KEY_H equ 48h
VK_KEY_F equ 46h ; Flashlight toggle
VK_KEY_L equ 4Ch ; Lighting mode cycle
; Lighting Modes
LIGHTING_MODE_NONE equ 0 ; Fullbright (classic retro Wolf3D flat lighting)
LIGHTING_MODE_BAKED equ 1 ; Static Baked Lightmap (raytraced shadow-casting)
LIGHTING_MODE_DYNAMIC equ 2 ; Dynamic Point Lights + Realtime Physics + Flashlight
; Light Source Archetypes & Flags
LIGHT_TYPE_STATIC equ 0 ; Static baked light
LIGHT_TYPE_POINT equ 1 ; Realtime dynamic point light (torch, lamp)
LIGHT_TYPE_FLASHLIGHT equ 2 ; Player spotlight cone
LIGHT_TYPE_BURST equ 3 ; Decaying light flare (muzzle flash, explosion)
LIGHT_TYPE_PULSE equ 4 ; Flickering / pulsing light
LIGHT_FLAG_INACTIVE equ 0
LIGHT_FLAG_ACTIVE equ 1
LIGHT_FLAG_SHADOWS equ 2 ; Raycast wall occlusion (shadow casting)
LIGHT_FLAG_FLICKER equ 4 ; Organic torch flicker
MAX_LIGHTS equ 16
; Game Modes ; Game Modes
GAME_MODE_ADVENTURE equ 0 ; Exploration / Quest / Horror / Puzzle (No weapons, clean view) GAME_MODE_ADVENTURE equ 0 ; Exploration / Quest / Horror / Puzzle (No weapons, clean view)
@ -136,7 +157,7 @@ CELL_COUNT equ 11
; Sector Transition Portals ; Sector Transition Portals
MAX_TRANSITIONS equ 16 MAX_TRANSITIONS equ 16
MAX_PATH_BYTES equ 260 MAX_PATH_BYTES equ 260
MAP_FORMAT_VERSION equ 4 MAP_FORMAT_VERSION equ 5
LevelTransition STRUCT ; sizeof = 42 bytes (packed) LevelTransition STRUCT ; sizeof = 42 bytes (packed)
triggerCellX db 0 ; 1 byte triggerCellX db 0 ; 1 byte
@ -161,6 +182,20 @@ SpriteStruct STRUCT
flags dd 0 flags dd 0
SpriteStruct ENDS SpriteStruct ENDS
; Light Source Struct (sizeof = 40 bytes)
LightSource STRUCT
x dd 0 ; raw world units (0..2047)
y dd 0 ; raw world units (0..2047)
radius dd 0 ; radius in raw world units
intensity dd 0 ; 0..255 base intensity
curIntensity dd 0 ; 0..255 current intensity (after flicker/decay)
lightType dd 0 ; LIGHT_TYPE_*
flags dd 0 ; LIGHT_FLAG_*
color dd 0 ; 00RRGGBBh tint (0 = neutral white/ambient)
decaySpeed dd 0 ; decay per tick for BURST lights
flickerPhase dd 0 ; flicker oscillation phase
LightSource ENDS
; Texture Pack (assets/textures/texturepack.bin): ; Texture Pack (assets/textures/texturepack.bin):
; dd magic 'EBTX', dw version (1), dw slotCount, then slotCount * 65536 bytes ; dd magic 'EBTX', dw version (1), dw slotCount, then slotCount * 65536 bytes
TEXPACK_MAGIC equ 58544245h ; 'EBTX' TEXPACK_MAGIC equ 58544245h ; 'EBTX'
@ -177,7 +212,7 @@ MAP_MAGIC equ 4E4C4245h ; 'EBLN'
MapHeader STRUCT ; sizeof = 38 bytes (packed) MapHeader STRUCT ; sizeof = 38 bytes (packed)
magic dd 0 ; 'EBLN' magic dd 0 ; 'EBLN'
version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes, 5 = lighting
mapWidth dw 0 ; 32 mapWidth dw 0 ; 32
mapHeight dw 0 ; 32 mapHeight dw 0 ; 32
playerStartX dw 0 ; cell X playerStartX dw 0 ; cell X
@ -190,7 +225,8 @@ MapHeader STRUCT ; sizeof = 38 bytes (packed)
cutsceneCount dw 0 cutsceneCount dw 0
cutsceneStringCount dw 0 cutsceneStringCount dw 0
cutsceneFlags dw 0 cutsceneFlags dw 0
reserved db 4 dup(0) lightCount dw 0
lightingReserved dw 0
MapHeader ENDS MapHeader ENDS
MAX_CUTSCENE_EVENTS equ 128 MAX_CUTSCENE_EVENTS equ 128

View file

@ -203,6 +203,53 @@ hud_key_released:
mov keyHudStyleHeld, 0 mov keyHudStyleHeld, 0
hud_key_sampled: hud_key_sampled:
; F: Toggle Flashlight
invoke GetAsyncKeyState, VK_KEY_F
test ax, 8000h
jz flash_key_released
cmp keyFlashlightHeld, 0
jne flash_key_sampled
xor isFlashlightOn, 1
mov keyFlashlightHeld, 1
cmp isFlashlightOn, 1
je toast_flash_on
invoke ShowGameToast, OFFSET szFlashlightOff, 60, 00808080h
jmp flash_key_sampled
toast_flash_on:
invoke ShowGameToast, OFFSET szFlashlightOn, 60, COLOR_CYAN
jmp flash_key_sampled
flash_key_released:
mov keyFlashlightHeld, 0
flash_key_sampled:
; L: Cycle Lighting Mode (0: Off -> 1: Baked -> 2: Dynamic)
invoke GetAsyncKeyState, VK_KEY_L
test ax, 8000h
jz light_key_released
cmp keyLightModeHeld, 0
jne light_key_sampled
inc lightingMode
cmp lightingMode, 3
jl @F
mov lightingMode, 0
@@:
mov keyLightModeHeld, 1
cmp lightingMode, LIGHTING_MODE_NONE
je toast_light_none
cmp lightingMode, LIGHTING_MODE_BAKED
je toast_light_baked
invoke ShowGameToast, OFFSET szLightMode2, 60, COLOR_YELLOW
jmp light_key_sampled
toast_light_none:
invoke ShowGameToast, OFFSET szLightMode0, 60, 00808080h
jmp light_key_sampled
toast_light_baked:
invoke ShowGameToast, OFFSET szLightMode1, 60, COLOR_CYAN
jmp light_key_sampled
light_key_released:
mov keyLightModeHeld, 0
light_key_sampled:
; Weapon Selection Keys: 1, 2, 3 (only in Action mode with weapons enabled) ; Weapon Selection Keys: 1, 2, 3 (only in Action mode with weapons enabled)
cmp gameMode, GAME_MODE_ACTION cmp gameMode, GAME_MODE_ACTION
jne skip_weapon_select jne skip_weapon_select

103
include/engine_light.inc Normal file
View file

@ -0,0 +1,103 @@
include include/engine_lighting_core.inc
AddLightSource PROC uses ebx ecx edx x:DWORD, y:DWORD, radius:DWORD, intensity:DWORD, lightType:DWORD, flags:DWORD, color:DWORD
cmp x,2048
jae no_slot_avail
cmp y,2048
jae no_slot_avail
cmp radius,1
jb no_slot_avail
cmp radius,2048
ja no_slot_avail
cmp intensity,255
ja no_slot_avail
cmp lightType,4
ja no_slot_avail
mov eax,flags
and eax,0FFFFFFF8h
jnz no_slot_avail
test color,0FF000000h
jnz no_slot_avail
mov ecx, 1 ; Slot 0 is reserved for player flashlight
find_slot:
cmp ecx, 14
jge no_slot_avail
mov ebx, ecx
imul ebx, sizeof LightSource
add ebx, OFFSET lightSources
test [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE
jz slot_found
inc ecx
jmp find_slot
slot_found:
mov eax, x
mov [ebx].LightSource.x, eax
mov eax, y
mov [ebx].LightSource.y, eax
mov eax, radius
mov [ebx].LightSource.radius, eax
mov eax, intensity
mov [ebx].LightSource.intensity, eax
mov [ebx].LightSource.curIntensity, eax
mov eax, lightType
mov [ebx].LightSource.lightType, eax
mov eax, flags
or eax, LIGHT_FLAG_ACTIVE
mov [ebx].LightSource.flags, eax
mov eax, color
mov [ebx].LightSource.color, eax
mov [ebx].LightSource.decaySpeed, 0
mov [ebx].LightSource.flickerPhase, 0
mov eax, ecx
ret
no_slot_avail:
mov eax, -1
ret
AddLightSource ENDP
; ------------------------------------------------------------------------------
; SpawnExplosionLight: Triggers high-intensity fiery explosion light burst
; ------------------------------------------------------------------------------
SpawnExplosionLight PROC uses ebx x:DWORD, y:DWORD
mov ebx, 15 * (sizeof LightSource)
add ebx, OFFSET lightSources
mov eax, x
mov [ebx].LightSource.x, eax
mov eax, y
mov [ebx].LightSource.y, eax
mov [ebx].LightSource.radius, 450
mov [ebx].LightSource.intensity, 255
mov [ebx].LightSource.curIntensity, 255
mov [ebx].LightSource.lightType, LIGHT_TYPE_BURST
mov [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE or LIGHT_FLAG_SHADOWS
mov [ebx].LightSource.decaySpeed, 12
mov [ebx].LightSource.color, 00FFAA33h ; Warm fire tint
ret
SpawnExplosionLight ENDP
; ------------------------------------------------------------------------------
; SpawnMuzzleFlashLight: Triggers momentary bright gunshot flash
; ------------------------------------------------------------------------------
SpawnMuzzleFlashLight PROC uses ebx x:DWORD, y:DWORD
mov ebx, 14 * (sizeof LightSource)
add ebx, OFFSET lightSources
mov eax, x
mov [ebx].LightSource.x, eax
mov eax, y
mov [ebx].LightSource.y, eax
mov [ebx].LightSource.radius, 350
mov [ebx].LightSource.intensity, 240
mov [ebx].LightSource.curIntensity, 240
mov [ebx].LightSource.lightType, LIGHT_TYPE_BURST
mov [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE or LIGHT_FLAG_SHADOWS
mov [ebx].LightSource.decaySpeed, 60
mov [ebx].LightSource.color, 00FFFFEEh ; White/yellow flare
ret
SpawnMuzzleFlashLight ENDP

View file

@ -0,0 +1,817 @@
; Versioned authored lighting, RGB shading and exact occlusion traversal.
.code
InitLightingSystem PROTO
BakeMapLighting PROTO
UpdateDynamicLights PROTO
AddLightSource PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
SpawnExplosionLight PROTO :DWORD,:DWORD
SpawnMuzzleFlashLight PROTO :DWORD,:DWORD
CastLightRay PROTO :DWORD,:DWORD,:DWORD,:DWORD
ComputeWallLightLevel PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
ComputeSpriteLight PROTO :DWORD,:DWORD,:DWORD
SamplePointLighting PROTO :DWORD,:DWORD,:DWORD,:DWORD
LightCellFree PROTO :DWORD,:DWORD
RefreshLightGrid PROTO
ValidateLightingData PROC uses ebx ecx edx esi, settings:DWORD,sources:DWORD,count:DWORD
mov esi,settings
cmp DWORD PTR [esi],2
ja vl_invalid
cmp DWORD PTR [esi+4],255
ja vl_invalid
cmp DWORD PTR [esi+8],1
ja vl_invalid
cmp DWORD PTR [esi+12],32
jb vl_invalid
cmp DWORD PTR [esi+12],2048
ja vl_invalid
cmp DWORD PTR [esi+16],255
ja vl_invalid
cmp DWORD PTR [esi+20],1
jb vl_invalid
cmp DWORD PTR [esi+20],96
ja vl_invalid
cmp DWORD PTR [esi+24],1
jne vl_invalid
cmp DWORD PTR [esi+28],0
jne vl_invalid
cmp count,13
ja vl_invalid
mov ecx,count
mov esi,sources
vl_source:
test ecx,ecx
jz vl_valid
cmp [esi].LightSource.x,2048
jae vl_invalid
cmp [esi].LightSource.y,2048
jae vl_invalid
cmp [esi].LightSource.radius,1
jb vl_invalid
cmp [esi].LightSource.radius,2048
ja vl_invalid
cmp [esi].LightSource.intensity,255
ja vl_invalid
cmp [esi].LightSource.curIntensity,255
ja vl_invalid
cmp [esi].LightSource.lightType,1
jbe vl_type_ok
cmp [esi].LightSource.lightType,4
jne vl_invalid
vl_type_ok:
mov eax,[esi].LightSource.flags
and eax,0FFFFFFF8h
jnz vl_invalid
test [esi].LightSource.color,0FF000000h
jnz vl_invalid
add esi,40
dec ecx
jmp vl_source
vl_valid:
mov eax,1
ret
vl_invalid:
xor eax,eax
ret
ValidateLightingData ENDP
InitLightingSystem PROC uses eax ecx edi
mov lightingMode,0
mov ambientLightLevel,40
mov isFlashlightOn,1
mov flashlightRadius,384
mov flashlightIntensity,240
mov flashlightConeAngle,45
mov authoredLightCount,0
mov lightingTick,0
mov edi,OFFSET lightSources
mov ecx,MAX_LIGHTS*10
xor eax,eax
rep stosd
mov edi,OFFSET lightGridRGB
mov ecx,MAP_SIZE
mov eax,00282828h
rep stosd
call RefreshLightGrid
ret
InitLightingSystem ENDP
CommitLightingData PROC uses eax ecx esi edi,settings:DWORD,sources:DWORD,count:DWORD,rgb:DWORD
call InitLightingSystem
mov esi,settings
lodsd
mov lightingMode,eax
lodsd
mov ambientLightLevel,eax
lodsd
mov isFlashlightOn,eax
lodsd
mov flashlightRadius,eax
lodsd
mov flashlightIntensity,eax
lodsd
mov flashlightConeAngle,eax
mov esi,sources
mov edi,OFFSET lightSources+40
mov ecx,count
mov authoredLightCount,ecx
imul ecx,10
rep movsd
mov esi,rgb
mov edi,OFFSET lightGridRGB
mov ecx,MAP_SIZE
rep movsd
call RefreshLightGrid
ret
CommitLightingData ENDP
LightCellFree PROC uses edx,x:DWORD,y:DWORD
cmp x,32
jae lcf_no
cmp y,32
jae lcf_no
mov eax,y
shl eax,5
add eax,x
movzx eax,BYTE PTR [gameMap+eax]
cmp eax,0
je lcf_yes
cmp eax,6
je lcf_yes
cmp eax,10
je lcf_yes
lcf_no:
xor eax,eax
ret
lcf_yes:
mov eax,1
ret
LightCellFree ENDP
; Supercover traversal: test both sides when crossing an exact grid corner.
CastLightRay PROC uses ebx ecx edx esi edi,x1:DWORD,y1:DWORD,x2:DWORD,y2:DWORD
LOCAL cellX:DWORD,cellY:DWORD,ex:DWORD,ey:DWORD,deltaX:DWORD,deltaY:DWORD,sx:DWORD,sy:DWORD,nx:DWORD,ny:DWORD,axv:DWORD
mov eax,x1
or eax,y1
or eax,x2
or eax,y2
test eax,0FFFFF800h
jnz lr_blocked
mov eax,x1
shr eax,6
mov cellX,eax
mov eax,y1
shr eax,6
mov cellY,eax
mov eax,x2
shr eax,6
mov ex,eax
mov eax,y2
shr eax,6
mov ey,eax
mov sx,1
mov eax,x2
sub eax,x1
jns lr_dx
neg eax
mov sx,-1
lr_dx:
mov deltaX,eax
mov sy,1
mov eax,y2
sub eax,y1
jns lr_dy
neg eax
mov sy,-1
lr_dy:
mov deltaY,eax
mov eax,cellX
shl eax,6
cmp sx,1
jne lr_nx_neg
add eax,64
sub eax,x1
jmp lr_nx
lr_nx_neg:
neg eax
add eax,x1
lr_nx:
mov nx,eax
mov eax,cellY
shl eax,6
cmp sy,1
jne lr_ny_neg
add eax,64
sub eax,y1
jmp lr_ny
lr_ny_neg:
neg eax
add eax,y1
lr_ny:
mov ny,eax
mov edi,65
lr_step:
mov eax,cellX
cmp eax,ex
jne lr_cross
mov eax,cellY
cmp eax,ey
je lr_end
lr_cross:
mov eax,3FFFFFFFh
cmp deltaX,0
je lr_a
mov eax,nx
imul eax,deltaY
lr_a:
mov axv,eax
mov eax,3FFFFFFFh
cmp deltaY,0
je lr_b
mov eax,ny
imul eax,deltaX
lr_b:
cmp axv,eax
jb lr_x
ja lr_y
mov eax,cellX
add eax,sx
invoke LightCellFree,eax,cellY
test eax,eax
jz lr_blocked
mov eax,cellY
add eax,sy
invoke LightCellFree,cellX,eax
test eax,eax
jz lr_blocked
mov eax,sx
add cellX,eax
add nx,64
lr_y:
mov eax,sy
add cellY,eax
add ny,64
jmp lr_test
lr_x:
mov eax,sx
add cellX,eax
add nx,64
lr_test:
invoke LightCellFree,cellX,cellY
test eax,eax
jz lr_blocked
dec edi
jnz lr_step
lr_blocked:
xor eax,eax
ret
lr_end:
invoke LightCellFree,cellX,cellY
ret
CastLightRay ENDP
LightDistance PROC uses ebx edx,deltaX:DWORD,deltaY:DWORD
mov eax,deltaX
cdq
xor eax,edx
sub eax,edx
mov ebx,eax
mov eax,deltaY
cdq
xor eax,edx
sub eax,edx
cmp eax,ebx
jae ld_max
xchg eax,ebx
ld_max:
lea ebx,[ebx+ebx*2]
shr ebx,3
add eax,ebx
ret
LightDistance ENDP
AddLightColor PROC uses eax ebx ecx edx,color:DWORD,amount:DWORD
mov ebx,255
mov eax,color
and eax,255
imul eax,amount
xor edx,edx
div ebx
add lightAccumB,eax
mov eax,color
shr eax,8
and eax,255
imul eax,amount
xor edx,edx
div ebx
add lightAccumG,eax
mov eax,color
shr eax,16
and eax,255
imul eax,amount
xor edx,edx
div ebx
add lightAccumR,eax
ret
AddLightColor ENDP
PointLightAmount PROC uses ebx ecx edx esi,source:DWORD,x:DWORD,y:DWORD
LOCAL distance:DWORD
mov esi,source
mov eax,x
sub eax,[esi].LightSource.x
mov ecx,y
sub ecx,[esi].LightSource.y
invoke LightDistance,eax,ecx
mov distance,eax
cmp eax,[esi].LightSource.radius
jae pla_zero
test [esi].LightSource.flags,LIGHT_FLAG_SHADOWS
jz pla_visible
invoke CastLightRay,[esi].LightSource.x,[esi].LightSource.y,x,y
test eax,eax
jz pla_zero
pla_visible:
mov eax,[esi].LightSource.radius
sub eax,distance
imul eax,[esi].LightSource.curIntensity
xor edx,edx
div [esi].LightSource.radius
ret
pla_zero:
xor eax,eax
ret
PointLightAmount ENDP
RefreshLightGrid PROC uses eax ebx ecx edx esi edi
mov esi,OFFSET lightGridRGB
mov edi,OFFSET lightGrid
mov ecx,MAP_SIZE
rlg_loop:
lodsd
mov edx,eax
and eax,255
mov ebx,edx
shr ebx,8
and ebx,255
add eax,ebx
shr edx,16
and edx,255
add eax,edx
xor edx,edx
mov ebx,3
div ebx
stosb
loop rlg_loop
ret
RefreshLightGrid ENDP
PackBakedRGB PROC uses ebx edx
mov eax,lightAccumR
cmp eax,255
jbe @F
mov eax,255
@@:
shl eax,16
mov ebx,eax
mov eax,lightAccumG
cmp eax,255
jbe @F
mov eax,255
@@:
shl eax,8
or ebx,eax
mov eax,lightAccumB
cmp eax,255
jbe @F
mov eax,255
@@:
or eax,ebx
ret
PackBakedRGB ENDP
AccumulateBakedCell PROC uses eax ebx ecx edx,x:DWORD,y:DWORD,weight:DWORD
invoke LightCellFree,x,y
test eax,eax
jz abc_done
mov eax,y
shl eax,5
add eax,x
mov edx,[lightGridRGB+eax*4]
mov eax,edx
and eax,255
imul eax,weight
add lightAccumB,eax
mov eax,edx
shr eax,8
and eax,255
imul eax,weight
add lightAccumG,eax
shr edx,16
imul edx,weight
add lightAccumR,edx
mov eax,weight
add lightWeight,eax
abc_done:
ret
AccumulateBakedCell ENDP
BakeMapLighting PROC uses ebx ecx edx esi edi
LOCAL index:DWORD,x:DWORD,y:DWORD
mov index,0
bl_cell:
mov eax,index
and eax,31
mov x,eax
mov eax,index
shr eax,5
mov y,eax
mov eax,ambientLightLevel
mov lightAccumR,eax
mov lightAccumG,eax
mov lightAccumB,eax
invoke LightCellFree,x,y
test eax,eax
jz bl_store
mov eax,x
shl eax,6
add eax,32
mov x,eax
mov eax,y
shl eax,6
add eax,32
mov y,eax
mov esi,OFFSET lightSources+40
mov edi,13
bl_source:
test [esi].LightSource.flags,LIGHT_FLAG_ACTIVE
jz bl_next_source
cmp [esi].LightSource.lightType,LIGHT_TYPE_STATIC
jne bl_next_source
mov eax,[esi].LightSource.intensity
push [esi].LightSource.curIntensity
mov [esi].LightSource.curIntensity,eax
invoke PointLightAmount,esi,x,y
pop [esi].LightSource.curIntensity
invoke AddLightColor,[esi].LightSource.color,eax
bl_next_source:
add esi,40
dec edi
jnz bl_source
bl_store:
call PackBakedRGB
mov ebx,index
mov [lightGridRGB+ebx*4],eax
inc index
cmp index,MAP_SIZE
jb bl_cell
; Smooth out of place, without averaging through wall cells.
mov index,0
bl_smooth:
mov ebx,index
mov eax,[lightGridRGB+ebx*4]
mov [lightSmoothRGB+ebx*4],eax
mov eax,ebx
and eax,31
mov x,eax
shr ebx,5
mov y,ebx
invoke LightCellFree,x,y
test eax,eax
jz bl_next_smooth
mov lightAccumR,0
mov lightAccumG,0
mov lightAccumB,0
mov lightWeight,0
invoke AccumulateBakedCell,x,y,2
mov eax,x
dec eax
invoke AccumulateBakedCell,eax,y,1
mov eax,x
inc eax
invoke AccumulateBakedCell,eax,y,1
mov eax,y
dec eax
invoke AccumulateBakedCell,x,eax,1
mov eax,y
inc eax
invoke AccumulateBakedCell,x,eax,1
mov eax,lightAccumR
xor edx,edx
div lightWeight
mov lightAccumR,eax
mov eax,lightAccumG
xor edx,edx
div lightWeight
mov lightAccumG,eax
mov eax,lightAccumB
xor edx,edx
div lightWeight
mov lightAccumB,eax
call PackBakedRGB
mov ebx,index
mov [lightSmoothRGB+ebx*4],eax
bl_next_smooth:
inc index
cmp index,MAP_SIZE
jb bl_smooth
mov esi,OFFSET lightSmoothRGB
mov edi,OFFSET lightGridRGB
mov ecx,MAP_SIZE
rep movsd
call RefreshLightGrid
ret
BakeMapLighting ENDP
UpdateDynamicLights PROC uses eax ebx ecx edx esi
inc lightingTick
mov esi,OFFSET lightSources+40
mov ecx,1
udl_next:
test [esi].LightSource.flags,LIGHT_FLAG_ACTIVE
jz udl_skip
cmp [esi].LightSource.lightType,LIGHT_TYPE_BURST
je udl_burst
mov eax,[esi].LightSource.intensity
cmp [esi].LightSource.lightType,LIGHT_TYPE_STATIC
je udl_store
cmp [esi].LightSource.lightType,LIGHT_TYPE_PULSE
jne udl_flicker
mov edx,lightingTick
shl edx,2
add edx,ecx
and edx,511
mov ebx,[tableSin+edx*4]
sar ebx,1
add ebx,98304
imul eax,ebx
sar eax,17
udl_flicker:
test [esi].LightSource.flags,LIGHT_FLAG_FLICKER
jz udl_store
mov edx,lightingTick
shr edx,2
imul edx,1103515245
imul ebx,ecx,12345
add edx,ebx
shr edx,16
and edx,15
sub edx,7
add eax,edx
cmp eax,0
jge @F
xor eax,eax
@@:
cmp eax,255
jle udl_store
mov eax,255
jmp udl_store
udl_burst:
mov eax,[esi].LightSource.curIntensity
sub eax,[esi].LightSource.decaySpeed
jg udl_store
xor eax,eax
mov [esi].LightSource.flags,0
udl_store:
mov [esi].LightSource.curIntensity,eax
udl_skip:
add esi,40
inc ecx
cmp ecx,16
jb udl_next
ret
UpdateDynamicLights ENDP
SamplePointLighting PROC uses ebx ecx edx esi edi,x:DWORD,y:DWORD,distance:DWORD,side:DWORD
LOCAL forward:DWORD,lateral:DWORD,beam:DWORD,atten:DWORD,fog:DWORD
mov eax,256
mov lightRed,eax
mov lightGreen,eax
mov lightBlue,eax
cmp lightingMode,0
je spl_done
mov eax,x
sar eax,6
and eax,31
mov edx,y
sar edx,6
and edx,31
shl edx,5
add eax,edx
mov eax,[lightGridRGB+eax*4]
mov edx,eax
and eax,255
mov lightAccumB,eax
mov eax,edx
shr eax,8
and eax,255
mov lightAccumG,eax
shr edx,16
mov lightAccumR,edx
cmp lightingMode,2
jne spl_fog
mov esi,OFFSET lightSources+40
mov edi,15
spl_source:
test [esi].LightSource.flags,LIGHT_FLAG_ACTIVE
jz spl_next
cmp [esi].LightSource.lightType,LIGHT_TYPE_STATIC
je spl_next
cmp [esi].LightSource.radius,0
jle spl_next
invoke PointLightAmount,esi,x,y
invoke AddLightColor,[esi].LightSource.color,eax
spl_next:
add esi,40
dec edi
jnz spl_source
cmp isFlashlightOn,0
je spl_fog
mov eax,x
sub eax,playerX
mov ebx,y
sub ebx,playerY
invoke LightDistance,eax,ebx
cmp eax,flashlightRadius
jae spl_fog
mov edx,flashlightRadius
sub edx,eax
mov eax,edx
imul eax,flashlightIntensity
xor edx,edx
div flashlightRadius
mov atten,eax
mov ecx,playerAngle
and ecx,511
mov eax,x
sub eax,playerX
imul [tableCos+ecx*4]
shrd eax,edx,16
mov forward,eax
mov eax,y
sub eax,playerY
imul [tableSin+ecx*4]
shrd eax,edx,16
add forward,eax
cmp forward,0
jle spl_fog
mov eax,y
sub eax,playerY
imul [tableCos+ecx*4]
shrd eax,edx,16
mov lateral,eax
mov eax,x
sub eax,playerX
imul [tableSin+ecx*4]
shrd eax,edx,16
sub lateral,eax
mov ecx,flashlightConeAngle
mov eax,forward
imul [tableSin+ecx*4]
idiv DWORD PTR [tableCos+ecx*4]
mov beam,eax
test eax,eax
jle spl_fog
mov eax,lateral
cdq
xor eax,edx
sub eax,edx
cmp eax,beam
jae spl_fog
mov ebx,beam
sub ebx,eax
mov eax,ebx
imul eax,atten
xor edx,edx
div beam
mov atten,eax
invoke CastLightRay,playerX,playerY,x,y
test eax,eax
jz spl_fog
invoke AddLightColor,00FFFFFFh,atten
spl_fog:
mov fog,256
mov eax,distance
cmp eax,280
jle spl_side
cmp eax,720
jb spl_partial
mov fog,32
jmp spl_side
spl_partial:
sub eax,280
imul eax,224
xor edx,edx
mov ebx,440
div ebx
sub fog,eax
spl_side:
cmp side,1
jne spl_coeff
mov eax,fog
imul eax,192
shr eax,8
mov fog,eax
spl_coeff:
mov eax,lightAccumR
cmp eax,256
jbe @F
mov eax,256
@@:
imul eax,fog
shr eax,8
mov lightRed,eax
mov eax,lightAccumG
cmp eax,256
jbe @F
mov eax,256
@@:
imul eax,fog
shr eax,8
mov lightGreen,eax
mov eax,lightAccumB
cmp eax,256
jbe @F
mov eax,256
@@:
imul eax,fog
shr eax,8
mov lightBlue,eax
spl_done:
mov eax,lightRed
cmp eax,lightGreen
jae @F
mov eax,lightGreen
@@:
cmp eax,lightBlue
jae @F
mov eax,lightBlue
@@:
ret
SamplePointLighting ENDP
ComputeWallLightLevel PROC uses ebx ecx edx,screenX:DWORD,mapX:DWORD,mapY:DWORD,sideHit:DWORD,stepX:DWORD,stepY:DWORD,rawDist:DWORD,perpWallDist:DWORD,rayAngle:DWORD,rayDirX:DWORD,rayDirY:DWORD
LOCAL x:DWORD,y:DWORD
mov eax,rawDist
imul rayDirX
shrd eax,edx,24
add eax,playerX
mov x,eax
mov eax,rawDist
imul rayDirY
shrd eax,edx,24
add eax,playerY
mov y,eax
cmp sideHit,0
jne cwl_y
mov eax,mapX
shl eax,6
cmp stepX,0
jg @F
add eax,64
@@:
sub eax,stepX
mov x,eax
jmp cwl_ready
cwl_y:
mov eax,mapY
shl eax,6
cmp stepY,0
jg @F
add eax,64
@@:
sub eax,stepY
mov y,eax
cwl_ready:
invoke SamplePointLighting,x,y,perpWallDist,sideHit
ret
ComputeWallLightLevel ENDP
ComputeSpriteLight PROC sprX:DWORD,sprY:DWORD,sprDist:DWORD
invoke SamplePointLighting,sprX,sprY,sprDist,0
ret
ComputeSpriteLight ENDP
ShadeLightPixel PROC uses eax ebx
mov eax,edx
and eax,255
imul eax,lightBlue
shr eax,8
mov ebx,eax
mov eax,edx
shr eax,8
and eax,255
imul eax,lightGreen
shr eax,8
shl eax,8
or ebx,eax
mov eax,edx
shr eax,16
and eax,255
imul eax,lightRed
shr eax,8
shl eax,16
or eax,ebx
mov edx,eax
ret
ShadeLightPixel ENDP

View file

@ -14,6 +14,10 @@ ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
CheckPortalTriggers PROTO CheckPortalTriggers PROTO
InitEntities PROTO InitEntities PROTO
ShowGameToast PROTO :DWORD, :DWORD, :DWORD ShowGameToast PROTO :DWORD, :DWORD, :DWORD
BakeMapLighting PROTO
InitLightingSystem PROTO
ValidateLightingData PROTO :DWORD,:DWORD,:DWORD
CommitLightingData PROTO :DWORD,:DWORD,:DWORD,:DWORD
; ------------------------------------------------------------------------------ ; ------------------------------------------------------------------------------
; InitMap: Creates a rich test level with rooms, hallways, pillars & doors ; InitMap: Creates a rich test level with rooms, hallways, pillars & doors
@ -101,6 +105,7 @@ loop_wall2:
mov portalLatchCell, -1 mov portalLatchCell, -1
mov mapCeilingColor, COLOR_CEILING mov mapCeilingColor, COLOR_CEILING
mov mapFloorColor, COLOR_FLOOR mov mapFloorColor, COLOR_FLOOR
call InitLightingSystem
ret ret
InitMap ENDP InitMap ENDP
@ -526,7 +531,7 @@ ApplyScreenFade ENDP
LoadSublevelBinary PROC uses ebx ecx edx esi edi, pszPath:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD LoadSublevelBinary PROC uses ebx ecx edx esi edi, pszPath:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD
LOCAL hFile:DWORD, bytesRead:DWORD, header:MapHeader LOCAL hFile:DWORD, bytesRead:DWORD, header:MapHeader
LOCAL numSprites:DWORD, numTransitions:DWORD, index:DWORD LOCAL numSprites:DWORD, numTransitions:DWORD, index:DWORD
LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD, numLights:DWORD
LOCAL resolvedPath[MAX_PATH_BYTES]:BYTE LOCAL resolvedPath[MAX_PATH_BYTES]:BYTE
LOCAL stagedMap[MAP_SIZE]:BYTE LOCAL stagedMap[MAP_SIZE]:BYTE
LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE
@ -647,6 +652,16 @@ old_map_no_transitions:
movzx eax, header.cutsceneFlags movzx eax, header.cutsceneFlags
mov sceneFlags, eax mov sceneFlags, eax
old_map_no_cutscene: old_map_no_cutscene:
mov numLights,0
cmp header.version,5
jb old_map_no_lighting
cmp header.lightingReserved,0
jne close_load_failed
movzx eax,header.lightCount
cmp eax,13
ja close_load_failed
mov numLights,eax
old_map_no_lighting:
mov eax, numTransitions mov eax, numTransitions
imul eax, sizeof LevelTransition imul eax, sizeof LevelTransition
mov ebx, eax mov ebx, eax
@ -660,6 +675,13 @@ old_map_no_cutscene:
mov eax, numStrings mov eax, numStrings
imul eax, CUTSCENE_STRING_BYTES imul eax, CUTSCENE_STRING_BYTES
add ebx, eax add ebx, eax
cmp header.version,5
jb map_lighting_size_ready
mov eax,numLights
imul eax,40
add ebx,eax
add ebx,32+4096
map_lighting_size_ready:
invoke GetFileSize, hFile, 0 invoke GetFileSize, hFile, 0
cmp eax, ebx cmp eax, ebx
jne close_load_failed jne close_load_failed
@ -700,6 +722,44 @@ old_map_no_cutscene:
jz close_load_failed jz close_load_failed
cmp bytesRead, ebx cmp bytesRead, ebx
jne close_load_failed jne close_load_failed
mov esi,OFFSET legacyLightingSettings
mov edi,OFFSET stagedLightSettings
mov ecx,8
rep movsd
mov edi,OFFSET stagedLightRGB
mov ecx,MAP_SIZE
mov eax,00282828h
rep stosd
cmp header.version,5
jb staged_lighting_ready
invoke ReadFile,hFile,OFFSET stagedLightSettings,32,ADDR bytesRead,0
test eax,eax
jz close_load_failed
cmp bytesRead,32
jne close_load_failed
mov ebx,numLights
imul ebx,40
invoke ReadFile,hFile,OFFSET stagedLightSources,ebx,ADDR bytesRead,0
test eax,eax
jz close_load_failed
cmp bytesRead,ebx
jne close_load_failed
invoke ReadFile,hFile,OFFSET stagedLightRGB,4096,ADDR bytesRead,0
test eax,eax
jz close_load_failed
cmp bytesRead,4096
jne close_load_failed
invoke ValidateLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights
test eax,eax
jz close_load_failed
mov esi,OFFSET stagedLightRGB
mov ecx,MAP_SIZE
map_check_light_rgb:
lodsd
test eax,0FF000000h
jnz close_load_failed
loop map_check_light_rgb
staged_lighting_ready:
invoke ValidateStagedCutscene, numEvents, numStrings, sceneFlags, ADDR stagedMap, numTransitions invoke ValidateStagedCutscene, numEvents, numStrings, sceneFlags, ADDR stagedMap, numTransitions
test eax, eax test eax, eax
jz close_load_failed jz close_load_failed
@ -903,6 +963,7 @@ commit_level:
mov eax, newAngle mov eax, newAngle
mov playerAngle, eax mov playerAngle, eax
mov mapErrorFrames, 0 mov mapErrorFrames, 0
invoke CommitLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights,OFFSET stagedLightRGB
mov eax, 1 mov eax, 1
ret ret
close_load_failed: close_load_failed:
@ -971,6 +1032,8 @@ load_startup_map:
jnz startup_map_ready jnz startup_map_ready
call InitMap call InitMap
startup_map_ready: startup_map_ready:
push eax
call InitEntities call InitEntities
pop eax
ret ret
LoadMapBinary ENDP LoadMapBinary ENDP

View file

@ -16,14 +16,14 @@ TryEnginePreview PROC uses ebx ecx edx esi edi
test eax, eax test eax, eax
jz preview_exit_error jz preview_exit_error
mov previewHandle, eax mov previewHandle, eax
invoke MapViewOfFile, eax, 6, 0, 0, 1357792 invoke MapViewOfFile, eax, 6, 0, 0, 1362408
test eax, eax test eax, eax
jz preview_exit_error jz preview_exit_error
mov previewMemory, eax mov previewMemory, eax
mov ebx, eax mov ebx, eax
cmp DWORD PTR [ebx], 56504245h cmp DWORD PTR [ebx], 56504245h
jne preview_exit_error jne preview_exit_error
cmp DWORD PTR [ebx+4], 1 cmp DWORD PTR [ebx+4], 2
jne preview_exit_error jne preview_exit_error
invoke OpenProcess, 00100000h, 0, DWORD PTR [ebx+12] ; SYNCHRONIZE invoke OpenProcess, 00100000h, 0, DWORD PTR [ebx+12] ; SYNCHRONIZE
test eax, eax test eax, eax
@ -106,6 +106,39 @@ preview_textures:
mov ecx, MAX_TEXTURES*TEX_SIZE_BYTES/4 mov ecx, MAX_TEXTURES*TEX_SIZE_BYTES/4
rep movsd rep movsd
preview_scene: preview_scene:
mov eax,[ebx+52]
mov stagedLightSettings,eax
mov eax,[ebx+56]
mov stagedLightSettings[4],eax
mov eax,[ebx+60]
mov stagedLightSettings[8],eax
mov eax,[ebx+68]
mov stagedLightSettings[12],eax
mov eax,[ebx+72]
mov stagedLightSettings[16],eax
mov eax,[ebx+76]
mov stagedLightSettings[20],eax
mov stagedLightSettings[24],1
mov stagedLightSettings[28],0
lea esi,[ebx+1357792]
invoke ValidateLightingData,OFFSET stagedLightSettings,esi,DWORD PTR [ebx+64]
test eax,eax
jz preview_exit_error
lea esi,[ebx+1358312]
mov edi,MAP_SIZE
preview_check_light_rgb:
lodsd
test eax,0FF000000h
jnz preview_exit_error
dec edi
jnz preview_check_light_rgb
lea esi,[ebx+1357792]
lea edi,[ebx+1358312]
invoke CommitLightingData,OFFSET stagedLightSettings,esi,DWORD PTR [ebx+64],edi
mov eax,[ebx+84]
dec eax
mov lightingTick,eax
call UpdateDynamicLights
call Render3DScene call Render3DScene
mov ebx, previewMemory mov ebx, previewMemory
cmp DWORD PTR [ebx+40], 0 cmp DWORD PTR [ebx+40], 0

View file

@ -36,12 +36,18 @@ Render3DScene PROC uses ebx ecx edx esi edi
LOCAL glassLineHeight:DWORD LOCAL glassLineHeight:DWORD
LOCAL glassDrawStart:DWORD, glassDrawEnd:DWORD LOCAL glassDrawStart:DWORD, glassDrawEnd:DWORD
LOCAL glassTexX:DWORD, glassSideHit:DWORD LOCAL glassTexX:DWORD, glassSideHit:DWORD
LOCAL colLightLevel:DWORD
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
; 1. Fast Background Clear: Ceiling (Top Half) and Floor (Bottom Half) ; 1. Fast Background Clear: Ceiling (Top Half) and Floor (Bottom Half)
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
invoke SamplePointLighting,playerX,playerY,0,0
mov edi, OFFSET screenBuffer mov edi, OFFSET screenBuffer
mov eax, mapCeilingColor mov edx,mapCeilingColor
call ShadeLightPixel
mov eax,edx
cmp lightingMode,0
jne @F
cmp playerFlashTimer, 0 cmp playerFlashTimer, 0
je @F je @F
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
@ -49,7 +55,11 @@ Render3DScene PROC uses ebx ecx edx esi edi
mov ecx, (SCREEN_W * SCREEN_HALF_H) mov ecx, (SCREEN_W * SCREEN_HALF_H)
rep stosd rep stosd
mov eax, mapFloorColor mov edx,mapFloorColor
call ShadeLightPixel
mov eax,edx
cmp lightingMode,0
jne @F
cmp playerFlashTimer, 0 cmp playerFlashTimer, 0
je @F je @F
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
@ -473,6 +483,11 @@ wall_texture_ready:
imul eax, texStep imul eax, texStep
mov texPos, eax mov texPos, eax
; Compute wall lighting (baked lightmap + dynamic point lights + flashlight)
invoke ComputeWallLightLevel, screenX, mapX, mapY, sideHit, stepX, stepY,\
rawDist, perpWallDist, rayAngle, rayDirX, rayDirY
mov colLightLevel, eax
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
; Draw Vertical Wall Stripe ; Draw Vertical Wall Stripe
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
@ -501,6 +516,9 @@ wall_pixel_loop:
add eax, texPtr add eax, texPtr
mov edx, [eax] ; Sampled Color (BGRA) mov edx, [eax] ; Sampled Color (BGRA)
cmp lightingMode, LIGHTING_MODE_NONE
jne apply_col_light
; Wall shading: Y-facing walls darkened 50% for 3D depth perception ; Wall shading: Y-facing walls darkened 50% for 3D depth perception
cmp sideHit, 1 cmp sideHit, 1
jne apply_distance_fog jne apply_distance_fog
@ -521,6 +539,10 @@ apply_distance_fog:
make_pitch_black: make_pitch_black:
mov edx, 00111111h mov edx, 00111111h
jmp write_wall_pixel
apply_col_light:
call ShadeLightPixel
write_wall_pixel: write_wall_pixel:
mov eax, pScreenPixel mov eax, pScreenPixel
@ -599,6 +621,7 @@ RenderSprites PROC uses ebx ecx edx esi edi passFilter:DWORD
LOCAL texPtr:DWORD, pPixel:DWORD LOCAL texPtr:DWORD, pPixel:DWORD
LOCAL camCos:DWORD, camSin:DWORD LOCAL camCos:DWORD, camSin:DWORD
LOCAL keyIdx:DWORD, keyDist:DWORD LOCAL keyIdx:DWORD, keyDist:DWORD
LOCAL sprLightLevel:DWORD
mov eax, spriteCount mov eax, spriteCount
test eax, eax test eax, eax
@ -844,6 +867,9 @@ sprite_loop:
mov texStepY, eax mov texStepY, eax
; Render vertical stripes ; Render vertical stripes
invoke ComputeSpriteLight, spriteX, spriteY, transY
mov sprLightLevel, eax
mov eax, drawStartX mov eax, drawStartX
mov stripe, eax mov stripe, eax
@ -924,6 +950,9 @@ sprite_pixel_y:
cmp edx, COLOR_CHROMA_KEY cmp edx, COLOR_CHROMA_KEY
je skip_sprite_pixel je skip_sprite_pixel
cmp lightingMode, LIGHTING_MODE_NONE
jne apply_spr_lighting
; Distance fog (same thresholds as walls) ; Distance fog (same thresholds as walls)
mov eax, transY mov eax, transY
cmp eax, 350 cmp eax, 350
@ -935,6 +964,10 @@ sprite_pixel_y:
jmp write_sprite_pixel jmp write_sprite_pixel
sprite_pixel_dark: sprite_pixel_dark:
mov edx, 00111111h mov edx, 00111111h
jmp write_sprite_pixel
apply_spr_lighting:
call ShadeLightPixel
write_sprite_pixel: write_sprite_pixel:
mov eax, pPixel mov eax, pPixel

View file

@ -112,6 +112,7 @@ include include/engine_textures.inc
include include/engine_input.inc include include/engine_input.inc
include include/engine_physics.inc include include/engine_physics.inc
include include/engine_entity.inc include include/engine_entity.inc
include include/engine_light.inc
include include/engine_raycaster.inc include include/engine_raycaster.inc
include include/engine_render2d.inc include include/engine_render2d.inc
include include/engine_combat.inc include include/engine_combat.inc
@ -158,7 +159,8 @@ Engine_Init PROC uses ebx esi edi
call InitMathTables call InitMathTables
call GenerateDefaultTextures ; Procedural baseline (always valid) call GenerateDefaultTextures ; Procedural baseline (always valid)
call LoadTexturePack ; Optional: override from assets\textures\texturepack.bin call LoadTexturePack ; Optional: override from assets\textures\texturepack.bin
call LoadMapBinary ; Loads assets/map.dat (falls back to InitMap if missing) call InitLightingSystem
call LoadMapBinary ; Each room commits its own validated lighting
; 2. Register Window Class ; 2. Register Window Class
invoke GetModuleHandleA, 0 invoke GetModuleHandleA, 0
@ -289,6 +291,7 @@ no_error_tick:
call UpdatePlayer call UpdatePlayer
call UpdateEntities call UpdateEntities
call UpdateCombat call UpdateCombat
call UpdateDynamicLights
skip_movement_in_dialog: skip_movement_in_dialog:
call Game_OnTick call Game_OnTick
@@: @@:

BIN
main.obj

Binary file not shown.

View file

@ -11,7 +11,7 @@ static void RunCutsceneChecks(EditorApp& app) {
}; };
Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference"); Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference");
BinaryLevel level; std::string error; BinaryLevel level; std::string error;
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == 4 && level.cutscene.events.size() == 9 && Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == MAP_FORMAT_VERSION && level.cutscene.events.size() == 9 &&
level.cutscene.strings[0] == "Привет, мир!" && level.cutscene.flags == CUTSCENE_AUTOPLAY, level.cutscene.strings[0] == "Привет, мир!" && level.cutscene.flags == CUTSCENE_AUTOPLAY,
"v4 track round trip retains UTF8 and autoplay"); "v4 track round trip retains UTF8 and autoplay");
std::string encoded; std::string encoded;

View file

@ -22,6 +22,7 @@ static std::vector<uint8_t> Bytes(const fs::path& path) {
} }
#include "cutscene_editor_checks.inl" #include "cutscene_editor_checks.inl"
#include "lighting_editor_checks.inl"
#include "viewport_editor_checks.inl" #include "viewport_editor_checks.inl"
int main(int argc, char** argv) { int main(int argc, char** argv) {
@ -138,6 +139,7 @@ int main(int argc, char** argv) {
app.LaunchPlaytest(); app.LaunchPlaytest();
Check(!app.isEngineRunning && app.playtestDirectory.empty() && Bytes("../assets/map.dat") == originalMain, Check(!app.isEngineRunning && app.playtestDirectory.empty() && Bytes("../assets/map.dat") == originalMain,
"missing linked map aborts F5 without data loss"); "missing linked map aborts F5 without data loss");
RunLightingChecks(app);
RunCutsceneChecks(app); RunCutsceneChecks(app);
if (argc > 1) { if (argc > 1) {
Check(ReadLevelFile(std::string(argv[1]) + "/map.dat", preview, error), "existing project main map remains compatible"); Check(ReadLevelFile(std::string(argv[1]) + "/map.dat", preview, error), "existing project main map remains compatible");

View file

@ -17,6 +17,7 @@ include include/engine_textures.inc
include include/engine_input.inc include include/engine_input.inc
include include/engine_physics.inc include include/engine_physics.inc
include include/engine_entity.inc include include/engine_entity.inc
include include/engine_light.inc
include include/engine_raycaster.inc include include/engine_raycaster.inc
include include/engine_render2d.inc include include/engine_render2d.inc
include include/engine_combat.inc include include/engine_combat.inc
@ -127,6 +128,7 @@ state_changed:
StateUnchanged ENDP StateUnchanged ENDP
include tests/cutscene_engine_checks.inc include tests/cutscene_engine_checks.inc
include tests/lighting_engine_checks.inc
AuditMain PROC AuditMain PROC
call InitMathTables call InitMathTables
@ -363,6 +365,7 @@ flat_depth_ok:
call Render3DScene call Render3DScene
invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE
call RunLightingChecks
call RunCutsceneChecks call RunCutsceneChecks
mov eax, resultCount mov eax, resultCount
inc eax inc eax

View file

@ -0,0 +1,65 @@
static void RunLightingChecks(EditorApp& app) {
app.InitDefaultLevel();app.sprites.clear();app.transitions.clear();app.lightSources.clear();
memset(app.map,0,sizeof(app.map));
for(int i=0;i<32;++i){app.map[0][i]=app.map[31][i]=app.map[i][0]=app.map[i][31]=1;app.map[i][10]=1;}
app.playerStartX=2;app.playerStartY=5;app.playerStartAngle=0;
app.lightingMode=2;app.ambientLightLevel=10;app.isFlashlightOn=false;
EditorLightSource fixed;fixed.x=352;fixed.y=352;fixed.radius=900;fixed.intensity=220;fixed.curIntensity=220;
fixed.lightType=LIGHT_TYPE_STATIC;fixed.flags=LIGHT_FLAG_ACTIVE|LIGHT_FLAG_SHADOWS;fixed.color=0xFF0000;
app.lightSources.push_back(fixed);app.BakeLighting();
auto red=app.bakedLightRGB[5*32+5];
Check(((red>>16)&255)>100 && (red&0xFFFF)==0x0A0A,"static bake preserves RGB light color");
Check(app.bakedLightRGB[5*32+14]==0x0A0A0A,"baked light cannot cross a wall or smooth through it");
auto before=app.bakedLightRGB;
EditorLightSource moving=fixed;moving.x=480;moving.lightType=LIGHT_TYPE_POINT;moving.color=0x0000FF;
app.lightSources.push_back(moving);app.BakeLighting();
Check(app.bakedLightRGB==before,"dynamic lights never enter the static bake");
app.map[5][7]=CELL_DOOR;
Check(!app.CastLightRay(352,352,608,352),"closed door blocks editor light rays");
app.map[5][7]=CELL_DOOR_OPEN;
Check(app.CastLightRay(352,352,608,352),"open door transmits editor light rays");
app.map[5][7]=0;app.map[5][6]=1;
Check(!app.CastLightRay(352,352,416,416),"diagonal light cannot leak through a blocked corner");
app.map[5][6]=0;
app.lightSources[0].flags=LIGHT_FLAG_ACTIVE;app.BakeLighting();
Check(((app.bakedLightRGB[5*32+14]>>16)&255)>10,"disabling shadows permits light across walls");
app.lightSources[0].flags|=LIGHT_FLAG_SHADOWS;app.BakeLighting();
app.PushUndo();app.lightSources[0].color=0x00FF00;app.ambientLightLevel=25;app.lightingMode=1;app.BakeLighting();
app.Undo();
Check(app.lightSources[0].color==0xFF0000 && app.ambientLightLevel==10 && app.lightingMode==2 && app.bakedLightRGB==before,
"undo restores sources, settings and the exact baked RGB data");
app.Redo();Check(app.lightSources[0].color==0x00FF00 && app.ambientLightLevel==25,"redo restores authored lighting");
app.Undo();
app.lightSources[0].x+=1;
Check(app.LightingNeedsBake(),"editing static lights marks the bake out of date");
app.lightSources[0].x-=1;app.lightSources[1].x+=64;
Check(!app.LightingNeedsBake(),"moving a realtime source does not invalidate static lighting");
app.lightSources[1].x-=64;
Check(app.SaveLevel("../assets/lit.dat"),"save v5 authored lights and persistent RGB bake");
std::ofstream reference("../assets/lighting_reference.bin",std::ios::binary);
reference.write(reinterpret_cast<const char*>(app.bakedLightRGB.data()),4096);reference.close();
auto valid=Bytes("../assets/lit.dat");
BinaryLevel loaded;std::string error;
Check(ReadLevelFile("../assets/lit.dat",loaded,error) && loaded.header.version==5 && loaded.lighting.lights.size()==2 &&
loaded.lighting.settings.flashlight==0 && loaded.lighting.rgb==app.bakedLightRGB,"v5 lighting round trip is lossless");
app.lightSources[0].radius=0;
Check(!app.SaveLevel("../assets/lit.dat") && Bytes("../assets/lit.dat")==valid,"invalid light cannot overwrite an existing level");
app.lightSources[0].radius=900;
app.lightSources[0].lightType=LIGHT_TYPE_BURST;
Check(!app.SaveLevel("../assets/lit.dat") && Bytes("../assets/lit.dat")==valid,"internal burst slots cannot be serialized as authored lights");
app.lightSources[0].lightType=LIGHT_TYPE_STATIC;
auto bad=valid;bad[34]=14;
{std::ofstream out("../assets/lit_badcount.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
bad=valid;uint32_t invalidRadius=0; // settings start after header and grid; no sprites, transitions or script
memcpy(bad.data()+38+1024+32+8,&invalidRadius,4);
{std::ofstream out("../assets/lit_badradius.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
bad=valid;bad.back()=255;
{std::ofstream out("../assets/lit_badrgb.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
Check(!app.LoadLevel("../assets/lit_badcount.dat") && app.lightSources.size()==2,"malformed lighting count preserves the editor scene");
Check(!app.LoadLevel("../assets/lit_badradius.dat"),"malformed light radius rejected before scene replacement");
Check(!app.LoadLevel("../assets/lit_badrgb.dat"),"invalid baked RGB padding rejected before scene replacement");
std::string start,engine;
Check(app.PreparePlaytest(start,engine),"F5 packages v5 lighting with the current unsaved map");app.CleanupPlaytest();
app.LoadLevel("../assets/map.dat");
Check(app.lightSources.empty() && app.lightingMode==0,"legacy maps clear old lights and retain classic rendering");
}

View file

@ -0,0 +1,103 @@
.data
lightFile db 'lit.dat',0
lightBadCount db 'lit_badcount.dat',0
lightBadRadius db 'lit_badradius.dat',0
lightBadRGB db 'lit_badrgb.dat',0
lightBakeDump db 'lighting_bake.bin',0
lightFrameDump db 'lighting_frame.bin',0
lightSavedRed dd 0
lightSavedBlue dd 0
lightSavedPulse dd 0
.code
RunLightingChecks PROC uses ebx ecx edx esi edi
invoke LoadSublevelBinary,OFFSET lightFile,-1,-1,0
CHECK eax,1 ; v5 authored lighting loads in ASM
CHECK authoredLightCount,2 ; all authored lights reach runtime slots
CHECK lightingMode,2 ; map lighting mode loads without a startup override
CHECK isFlashlightOn,0 ; saved flashlight toggle reaches the game
CHECK DWORD PTR [lightSources+40+LightSource.color],00FF0000h ; static light color retained on load
call Render3DScene
invoke Dump,OFFSET lightFrameDump,OFFSET screenBuffer,FRAME_BUFFER_SIZE
call BakeMapLighting
invoke Dump,OFFSET lightBakeDump,OFFSET lightGridRGB,4096
CHECK DWORD PTR [lightGridRGB+(5*32+14)*4],000A0A0Ah ; static light cannot leak through a wall
mov lightingMode,1
invoke SamplePointLighting,480,352,0,0
mov eax,lightRed
mov lightSavedRed,eax
mov eax,lightBlue
mov lightSavedBlue,eax
mov lightingMode,2
call UpdateDynamicLights
invoke SamplePointLighting,480,352,0,0
CHECK lightRed,lightSavedRed ; dynamic mode does not count static light twice
xor eax,eax
mov ebx,lightBlue
cmp ebx,lightSavedBlue
seta al
CHECK eax,1 ; realtime blue light contributes beyond the baked map
mov DWORD PTR [lightSources+80+LightSource.x],928
invoke SamplePointLighting,480,352,0,0
CHECK lightBlue,10 ; dynamic source is occluded by a separating wall
mov DWORD PTR [lightSources+80+LightSource.flags],LIGHT_FLAG_ACTIVE
invoke SamplePointLighting,480,352,0,0
xor eax,eax
cmp lightBlue,10
seta al
CHECK eax,1 ; disabling dynamic shadows allows unoccluded contribution
mov DWORD PTR [lightSources+80+LightSource.lightType],LIGHT_TYPE_PULSE
mov lightingTick,0
call UpdateDynamicLights
mov eax,DWORD PTR [lightSources+80+LightSource.curIntensity]
mov lightSavedPulse,eax
mov lightingTick,31
call UpdateDynamicLights
mov eax,DWORD PTR [lightSources+80+LightSource.curIntensity]
cmp eax,lightSavedPulse
setne al
movzx eax,al
CHECK eax,1 ; pulse light changes intensity with the simulation clock
mov DWORD PTR [lightSources+13*40+LightSource.intensity],77
invoke SpawnMuzzleFlashLight,160,160
invoke SpawnExplosionLight,160,160
CHECK DWORD PTR [lightSources+13*40+LightSource.intensity],77 ; combat bursts cannot overwrite authored light slots
call UpdateDynamicLights
CHECK DWORD PTR [lightSources+14*40+LightSource.curIntensity],180 ; muzzle light decays in its reserved slot
call InitLightingSystem
mov lightingMode,2
mov edi,OFFSET lightGridRGB
mov ecx,1024
mov eax,000A0A0Ah
rep stosd
mov playerX,160
mov playerY,160
mov playerAngle,0
invoke SamplePointLighting,288,160,0,0
xor eax,eax
cmp lightRed,10
seta al
CHECK eax,1 ; flashlight illuminates points inside its forward cone
invoke SamplePointLighting,160,288,0,0
CHECK lightRed,10 ; flashlight cannot illuminate sprites outside its cone
mov BYTE PTR [gameMap+2*32+3],CELL_DOOR
invoke SamplePointLighting,288,160,0,0
CHECK lightRed,10 ; a closed door blocks the player flashlight
mov BYTE PTR [gameMap+2*32+3],CELL_DOOR_OPEN
invoke CastLightRay,160,160,288,160
CHECK eax,1 ; an open door transmits dynamic light
mov BYTE PTR [gameMap+5*32+6],CELL_BRICK
invoke CastLightRay,352,352,416,416
CHECK eax,0 ; ASM light rays reject diagonal corner leaks
invoke LoadSublevelBinary,OFFSET lightFile,-1,-1,0
invoke LoadSublevelBinary,OFFSET lightBadCount,-1,-1,0
CHECK eax,0 ; excessive lighting count rejected before committing a room
CHECK authoredLightCount,2 ; malformed lighting preserves current authored lights
invoke LoadSublevelBinary,OFFSET lightBadRadius,-1,-1,0
CHECK eax,0 ; zero radius rejected without a divide by zero
invoke LoadSublevelBinary,OFFSET lightBadRGB,-1,-1,0
CHECK eax,0 ; malformed baked RGB cannot replace current lighting
invoke LoadSublevelBinary,OFFSET mainPath,-1,-1,0
CHECK authoredLightCount,0 ; loading a legacy room clears stale dynamic sources
CHECK lightingMode,0 ; legacy room restores classic lighting mode
ret
RunLightingChecks ENDP

View file

@ -49,6 +49,13 @@ static void RunViewportChecks(EditorApp& app, const char* actualEngine) {
Check(equal,"DX11 texture pixels match the ASM framebuffer with opaque alpha"); Check(equal,"DX11 texture pixels match the ASM framebuffer with opaque alpha");
context->Release(); context->Release();
} }
app.LoadLevel("../assets/lit.dat");app.showCutsceneTimeline=false;app.lightingMode=1;
Check(pump(),"saved static lighting arrives in the live viewport");
auto bakedFrame=app.previewPixels;app.lightingMode=2;
Check(pump() && app.previewPixels!=bakedFrame && app.previewPacket->lightCount==2,
"authored dynamic lights visibly affect the embedded game view");
auto dynamicFrame=app.previewPixels;app.lightSources[1].x+=256;
Check(pump() && app.previewPixels!=dynamicFrame,"moving a dynamic light updates the image without rebaking");
app.StopEnginePreview(); app.StopEnginePreview();
Check(!app.previewProcess.hProcess && !app.previewPacket && !app.previewMapping,"closing viewport releases renderer process and mapping"); Check(!app.previewProcess.hProcess && !app.previewPacket && !app.previewMapping,"closing viewport releases renderer process and mapping");
app.ShutdownD3D();device->Release();app.showCutsceneTimeline=false; app.ShutdownD3D();device->Release();app.showCutsceneTimeline=false;

View file

@ -9,16 +9,18 @@ import subprocess
import time import time
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SIZE, TEXTURES, PIXELS = 1357792, 2016, 1050592 SIZE, TEXTURES, PIXELS = 1362408, 2016, 1050592
def check_preview(engine, expected=None, output=None): def check_preview(engine, expected=None, output=None, level_path=None):
name = f"Local\\EblanPreview-test-{os.getpid()}-{time.monotonic_ns()}" name = f"Local\\EblanPreview-test-{os.getpid()}-{time.monotonic_ns()}"
with mmap.mmap(-1, SIZE, tagname=name) as packet: with mmap.mmap(-1, SIZE, tagname=name) as packet:
base = C.addressof(C.c_char.from_buffer(packet)) base = C.addressof(C.c_char.from_buffer(packet))
state = C.cast(base+8, C.POINTER(C.c_long)) state = C.cast(base+8, C.POINTER(C.c_long))
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 1, 0, os.getpid(), 160, 1024, 0, struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 2, 0, os.getpid(), 160, 1024, 0,
0x383838, 0x222222, 0, 0, 0, 1) 0x383838, 0x222222, 0, 0, 0, 1)
struct.pack_into("<8I",packet,52,0,40,0,0,384,240,45,0)
packet[1358312:1358312+4096]=struct.pack("<I",0x282828)*1024
for y in range(32): packet[128+y*32+10] = 1 for y in range(32): packet[128+y*32+10] = 1
# Flat red wall, other slots black; proves texture upload rather than fallback. # Flat red wall, other slots black; proves texture upload rather than fallback.
packet[TEXTURES+65536:TEXTURES+2*65536] = struct.pack("<I",0xE02010)*16384 packet[TEXTURES+65536:TEXTURES+2*65536] = struct.pack("<I",0xE02010)*16384
@ -53,7 +55,7 @@ def check_preview(engine, expected=None, output=None):
assert subtitle!=frame, "Subtitle overlay missing" assert subtitle!=frame, "Subtitle overlay missing"
# Compare the live IPC output with a direct Render3DScene regression dump. # Compare the live IPC output with a direct Render3DScene regression dump.
if expected: if expected:
data=(ROOT/"assets/map.dat").read_bytes() data=Path(level_path or ROOT/"assets/map.dat").read_bytes()
h=struct.unpack_from("<I7HII",data) h=struct.unpack_from("<I7HII",data)
version=h[1]; count=h[7] version=h[1]; count=h[7]
struct.pack_into("<3i",packet,16,h[4]*64+32,h[5]*64+32,h[6]) struct.pack_into("<3i",packet,16,h[4]*64+32,h[5]*64+32,h[6])
@ -65,6 +67,16 @@ def check_preview(engine, expected=None, output=None):
x,y=struct.unpack_from("<ii",sprites,i*24) x,y=struct.unpack_from("<ii",sprites,i*24)
struct.pack_into("<ii",sprites,i*24,x>>16,y>>16) struct.pack_into("<ii",sprites,i*24,x>>16,y>>16)
packet[1152:1152+len(sprites)]=sprites packet[1152:1152+len(sprites)]=sprites
if version>=5:
transitions,events,strings=struct.unpack_from("<3H",data,26)
lights=struct.unpack_from("<H",data,34)[0]
offset=1062+count*24+transitions*42+events*16+strings*96
mode,ambient,flash,radius,intensity,cone,_,_=struct.unpack_from("<8I",data,offset)
struct.pack_into("<7I",packet,52,mode,ambient,flash,lights,radius,intensity,cone)
offset+=32
packet[1357792:1357792+lights*40]=data[offset:offset+lights*40]
offset+=lights*40
packet[1358312:1358312+4096]=data[offset:offset+4096]
pack=(ROOT/"assets/textures/texturepack.bin").read_bytes() pack=(ROOT/"assets/textures/texturepack.bin").read_bytes()
packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:] packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:]
struct.pack_into("<I",packet,48,2) struct.pack_into("<I",packet,48,2)

View file

@ -148,7 +148,7 @@ def main():
f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"), f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"),
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work) "kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work)
run([str(work / "engine_tests.exe"), "pace map.dat"], work) run([str(work / "engine_tests.exe"), "pace map.dat"], work)
source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text() source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text() + (ROOT / "tests/lighting_engine_checks.inc").read_text()
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M) names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
raw = (work / "engine_results.bin").read_bytes() raw = (work / "engine_results.bin").read_bytes()
values = struct.unpack(f"<{len(raw)//4}I", raw) values = struct.unpack(f"<{len(raw)//4}I", raw)
@ -174,10 +174,14 @@ def main():
if (work / "cutscene_trace.bin").read_bytes() != reference: if (work / "cutscene_trace.bin").read_bytes() != reference:
raise RuntimeError("ASM and editor cinematic players disagree on frame-by-frame state") raise RuntimeError("ASM and editor cinematic players disagree on frame-by-frame state")
print("PASS editor and ASM cutscene state agrees on every tested frame") print("PASS editor and ASM cutscene state agrees on every tested frame")
if (work / "lighting_bake.bin").read_bytes() != (work / "assets/lighting_reference.bin").read_bytes():
raise RuntimeError("Editor and ASM static RGB bakes differ")
print("PASS all 1024 baked RGB cells match between the editor and ASM")
viewport_spec = importlib.util.spec_from_file_location("preview_ipc", ROOT / "tools/test_preview_ipc.py") viewport_spec = importlib.util.spec_from_file_location("preview_ipc", ROOT / "tools/test_preview_ipc.py")
viewport_tests = importlib.util.module_from_spec(viewport_spec) viewport_tests = importlib.util.module_from_spec(viewport_spec)
viewport_spec.loader.exec_module(viewport_tests) viewport_spec.loader.exec_module(viewport_tests)
viewport_tests.check_preview(ROOT / "engine.exe",work / "project_frame.bgra",work / "live_viewport.bmp") viewport_tests.check_preview(ROOT / "engine.exe",work / "project_frame.bgra",work / "live_viewport.bmp")
viewport_tests.check_preview(ROOT / "engine.exe",work / "lighting_frame.bin",work / "lighting_preview.bmp",work / "assets/lit.dat")
(work / "cutscene_preview.bmp").write_bytes(bitmap_header + (work / "cutscene_frame.bgra").read_bytes()) (work / "cutscene_preview.bmp").write_bytes(bitmap_header + (work / "cutscene_frame.bgra").read_bytes())
print("PASS original project assets remain byte-for-byte unchanged") print("PASS original project assets remain byte-for-byte unchanged")
succeeded = True succeeded = True