chore: integrate main before stage 2

Preserve stage-1 stability fixes alongside v5 lighting, IPC v2, weapons and studio updates. Fix barrel AOE pointer corruption and extend modal gameplay gating.

Verify 137 editor checks, 191 ASM checks, lighting bake and preview parity. Document stage-2 scope without implementing it.
This commit is contained in:
Игорь Федосеев 2026-10-31 22:10:07 +03:00
commit 527d92411d
44 changed files with 4694 additions and 416 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.
@ -218,22 +219,86 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi
- `3: Ammo Box`: collectible ammunition pickup (+20 rounds up to 100). - `3: Ammo Box`: collectible ammunition pickup (+20 rounds up to 100).
- `4: Key`: key card for unlocking locked doors. - `4: Key`: key card for unlocking locked doors.
- `5: Switch`: interactive trigger/lever toggled by proximity action ([E]). - `5: Switch`: interactive trigger/lever toggled by proximity action ([E]).
- `6: Explosive Barrel`: destructible barrel that explodes on weapon hit with 60 AOE damage to surrounding entities and fiery screen flash.
- Packed into `SpriteStruct.flags` without altering binary map compatibility (`sizeof(SpriteEntry) == 24`). - Packed into `SpriteStruct.flags` without altering binary map compatibility (`sizeof(SpriteEntry) == 24`).
2. **Player Combat & HUD (`include/engine_combat.inc`)**: 2. **Player Combat & Multi-Weapon Arsenal (`include/engine_combat.inc`)**:
- Weapon attack via `Space`, `Ctrl`, or Left Mouse Button (`VK_LBUTTON`). - Weapon switching via keys `[1]`, `[2]`, `[3]`:
- Center crosshair and hitscan raycasting against active enemy guards. - **[1] Боевой нож (Combat Knife)**: fast melee attack (cooldown 8, 30 damage, no ammo required).
- Animated retro pistol with muzzle flash, recoil, and cooldowns. - **[2] Пистолет (Pistol)**: standard handgun (cooldown 12, 35 damage, 1 ammo).
- **[3] Дробовик (Pump Shotgun)**: heavy scatter weapon (cooldown 22, 70 damage, 2 ammo, wide spread).
- Dynamic Muzzle Flash Room Illumination: shooting dynamically brightens ceiling and floor shading across the whole 3D environment for realistic muzzle flare lighting.
- Center crosshair and hitscan raycasting against active enemy guards and barrels.
- Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`. - Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`.
- Comprehensive combat HUD: Health bar, Ammo counter, Score counter, and Key indicator. - In-Game Retro Pause Menu via `[ESC]`: sleek semi-transparent pause menu with `[1] ПРОДОЛЖИТЬ`, `[2] ЗАНОВО`, `[3] МИНИ-КАРТА`, `[4] ВЫХОД В WINDOWS`.
- Comprehensive combat HUD: Active weapon badge with slot indicators `[1] [2] [3]`, Health bar, Ammo counter, Score counter, and Key indicator.
3. **In-Editor MASM32 IDE (`F7` / "Код / IDE")**: 3. **In-Editor MASM32 IDE (`F7` / "Код / IDE")**:
- Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7` or the main menu. - Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7`, `Ctrl+B`, or the main menu.
- MASM syntax highlighting (instructions, registers, directives, comments, strings, numbers). - MASM syntax highlighting (instructions, registers, directives, comments, strings, numbers).
- Project file navigator (`engine_entity.inc`, `engine_combat.inc`, `main.asm`, etc.). - Project file navigator (`engine_entity.inc`, `engine_combat.inc`, `main.asm`, etc.).
- Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Custom HUD). - **Engine API Reference Catalog**: built-in searchable catalog of all engine procedures with 1-click **"Вставить"** snippet insertion.
- Real-time search bar (`Поиск в коде...`) with instant match counter.
- Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Explosive Barrel, Custom HUD).
- Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines. - Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines.
4. **Project Settings & Standalone Game Exporter (`editor/src/project_manager.h`)**: 4. **Editor UI, 2D Canvas & Project Exporter (`editor/src/editor_app.cpp`)**:
- Modern Game Studio dark theme with Electric Cyan and Neon Amber accents.
- **2D Canvas Guard Aggro Circles**: visualizes guard detection and chase radius on the 2D map.
- **Quick Entity Spawner Palette**: 1-click spawn buttons (`+ Враг`, `+ Аптечка`, `+ Патроны`, `+ Ключ`, `+ Рычаг`, `+ Бочка`, `+ Проп`).
- Canvas Zoom buttons (`[-]`, `[100%]`, `[+]`) and pan controls.
- Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map. - Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map.
- "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme. - "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme.
5. **Gameplay Scripting Subsystem & Interactive Modals (`include/engine_gameplay.inc`)**:
- `ShowGameToast(pszText, durationTicks, color)`: floating framed notification banners (auto-centered, custom CP1251 string, colored outline, auto-dismiss).
- `ShowGameDialog(pszSpeaker, pszText)`: modal dialogue & lore note reader freezing player movement until dismissed with `[E]` / `[ENTER]`.
- Extensible engine lifecycle hooks:
- `Game_OnTick`: 60 Hz frame tick hook for custom game logic, timers, and puzzle triggers.
- `Game_OnPlayerInteract(entityIdx, entityType)`: intercepts `[E]` interactions before default behavior.
- `Game_OnEntityKilled(entityIdx, entityType)`: triggers on guard death or barrel explosion for boss encounters.
- `Game_OnCustomHUD`: custom 2D canvas drawing (health bars, gauges, quest markers).
- `PlayGameSFX(pszRelativePath)`: safe asynchronous WinMM audio player.
- Built-in automatic gameplay toasts for keys, medkits, ammo, switches, doors, and explosions.
6. **Game Genres, Peaceful Adventure Mode & Immersive Clean HUD**:
- `GAME_MODE_ADVENTURE (0)`: pure exploration / quest / horror / puzzle mode without weapons, ammo, crosshair, or combat overlays.
- `GAME_MODE_ACTION (1)`: full combat shooter mode with weapon models, crosshair, health bar, and ammo counter.
- `HUD_STYLE_CLEAN (0)`: 100% immersive full-screen view (no debug banners, no text collisions, no bottom controls clutter).
- `HUD_STYLE_DEBUG (1)`: developer mode displaying FPS counter at top-right (X=250), engine title, score, and controls hints.
- Instant in-game switching:
- `[ESC] -> [2]`: toggle weapons / peaceful mode on the fly with toast notifications.
- `[ESC] -> [3]`: toggle clean vs debug HUD.
- `[F1]` or `[H]`: quick hotkey to toggle clean HUD vs debug HUD.
- Editor toolbar 1-click genre switcher: `☮ КВЕСТ / БЕЗ ОРУЖИЯ` vs `⚔ ЭКШЕН / С ОРУЖИЕМ`.
- 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.

View file

@ -49,6 +49,63 @@ assets. New tests exercise production gameplay and IDE procedures, including
failed atomic replacement under a Windows sharing lock and hidden compiler output failed atomic replacement under a Windows sharing lock and hidden compiler output
exceeding pipe capacity. Interactive UI playtesting remains separate. exceeding pipe capacity. Interactive UI playtesting remains separate.
## Preparation for stage 2 — integration with current main
Integrated server history through `2bac213` with stage-1 commit `f42679f` in an
isolated worktree, preserving both histories and the original working directory.
Retained map v5, preview IPC v2, authored lighting, three weapons, explosive
barrels, modal dialogues, peaceful mode and the updated studio UI.
Compatibility adjustments:
- Pause and modal dialogue join the shared gameplay gate, including direct calls
to gameplay procedures. Blocked player updates cannot activate portal triggers.
- Knife/pistol/shotgun keep their range, damage, ammunition and reserved light
behavior. World-space half-widths are respectively 24/16/32 times sprite scale;
the shotgun remains a single-target wider hit test, not three separate rays.
- Barrel texture fallback is enabled only with zero flags in both editor and ASM.
Explicit Prop overrides barrel artwork and cannot explode.
- A new explosion regression reproduced access violation `0xC0000005`: the AOE
loop kept a sprite pointer in EDX and destroyed it with CDQ. The pointer now
stays in EBX. A nearby guard receives blast damage without crashing.
- IDE tab navigation, API catalog and docked view retain stage-1 safe writes.
Verification: both production builds succeeded; editor **137/137**, ASM
**191/191**. All 1024 baked RGB cells match between C++ and ASM; classic and lit
preview frames match direct ASM rendering, including the x86/x64/DX11 path.
Project assets remain byte-for-byte unchanged. The original working directory
still has only its pre-existing `editor/imgui.ini` modification. No separate
interactive UI playtest was performed during this preparation.
### Stage 2 implementation contract (not implemented here)
1. Separate message/input polling, fixed simulation ticks and drawing/presentation.
Use QPC and a 60 Hz accumulator with bounded catch-up. Consume edge-triggered
commands once, not once per catch-up tick. Reset timing debt after pause/focus
stalls; advance world timers/lights only on the appropriate simulation tick.
2. Keep movement fractions in runtime state without changing serialized raw-unit
coordinates. Cancel opposing inputs, normalize combined forward/strafe input,
and use symmetric signed accumulation. Clear fractions on load/teleport/restart.
3. Check the full collision body on each axis and preserve wall sliding; prevent
corner clipping and occupied-door closure. Keep portals intentionally walkable.
4. Centralize Playing/Cutscene/Transition/Dead/Paused/Dialog update rules, including
`Game_OnTick`, dynamic lights, fade, toasts and modal dismissal. Render routines
must not advance those timers or perform input-driven state changes.
5. Separate cinematic camera pose from gameplay/player position. Define END/skip
handoff explicitly and validate the restored gameplay collision body, including
paths crossing walls. Keep C++ preview and ASM cinematic traces aligned.
6. Track the current room path and its loaded entry spawn separately from startup
map. Restart restores that room and entry pose, resets gameplay transients and
fractional movement, and preserves project mode/settings. Failed reload must
not partially reset the current room. Make death-R and pause restart share it.
Acceptance checks: identical simulation after 60 ticks under different render
schedules; single consumption of edge inputs during catch-up; symmetric movement
for all quadrants and equal cardinal/diagonal speed; collision corner/slide cases;
no simulation advancement during pause/dialog; no pickups/AI along camera paths;
safe cinematic handoff; restart after a room transition; failed restart rollback;
unchanged v1..v5 loading, lighting bake parity and IPC v2 framebuffer parity.
## Remaining stages ## Remaining stages
2. Predictable simulation: fixed 60 Hz ticks, fractional movement, normalized 2. Predictable simulation: fixed 60 Hz ticks, fractional movement, normalized

Binary file not shown.

View file

@ -1,25 +1,40 @@
[Window][Workspace] [Window][Workspace]
Pos=0,60 Pos=0,64
Size=1536,708 Size=2560,1288
LastUsed=20261022 LastUsed=20261024
[Window][Debug##Default] [Window][Debug##Default]
Pos=60,60 Pos=60,60
Size=400,400 Size=400,400
LastUsed=20261022 LastUsed=20261024
[Window][Toolbar] [Window][Toolbar]
Pos=0,20 Pos=0,20
Size=1536,38 Size=2560,42
LastUsed=20261022 LastUsed=20261024
[Window][StatusBar] [Window][StatusBar]
Pos=0,768 Pos=0,1352
Size=1536,32 Size=2560,32
LastUsed=20261022 LastUsed=20261024
[Window][Не удалось запустить игру] [Window][Не удалось запустить игру]
Pos=649,356 Pos=649,356
Size=237,80 Size=237,80
LastUsed=20261022 LastUsed=20261022
[Window][Редактор кода и скриптов MASM32 - EblanStudio IDE]
Pos=60,60
Size=900,650
LastUsed=20261022
[Window][Менеджер ассетов (Texture & Audio Manager)]
Pos=61,60
Size=820,580
LastUsed=20261023
[Window][Редактор кода MASM32 & Игровых скриптов (F7)]
Pos=60,60
Size=1020,680
LastUsed=20261024

14
editor/project.eblanproj Normal file
View file

@ -0,0 +1,14 @@
[Project]
Title=EBLAN_GAME
Author=Developer
Version=1.0.0
StartupMap=assets/map.dat
WindowTitle=EBLAN ENGINE 3D - Standalone Retro Game
DefaultWidth=1280
DefaultHeight=960
GameMode=1
ShowWeapons=1
ShowCrosshair=1
LightingMode=2
AmbientLight=40
Flashlight=1

File diff suppressed because it is too large Load diff

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>
@ -22,7 +23,18 @@ enum ToolMode {
TOOL_ERASER, TOOL_ERASER,
TOOL_FILL, TOOL_FILL,
TOOL_PLAYER_SPAWN, TOOL_PLAYER_SPAWN,
TOOL_SPRITE_SPAWN TOOL_SPRITE_SPAWN,
TOOL_LIGHT_SPAWN
};
enum LightPreset {
LIGHT_PRESET_TORCH = 0,
LIGHT_PRESET_LAMP,
LIGHT_PRESET_ALARM,
LIGHT_PRESET_CANDLE,
LIGHT_PRESET_NEON,
LIGHT_PRESET_TOXIC,
LIGHT_PRESET_SPOTLIGHT
}; };
// Full level snapshot for undo/redo (grid + spawn + palette + sprites + transitions) // Full level snapshot for undo/redo (grid + spawn + palette + sprites + transitions)
@ -33,6 +45,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 {
@ -67,10 +83,18 @@ public:
void StopAudioSound(); void StopAudioSound();
void RenderAssetManagerModal(); void RenderAssetManagerModal();
void RenderAssetManagerContents();
void RenderCodeEditorWindow(); void RenderCodeEditorWindow();
void RenderCodeEditorContents(const std::string& root);
void RenderProjectSettingsModal(); void RenderProjectSettingsModal();
void RenderExportGameModal(); void RenderExportGameModal();
void DuplicateSelectedEntity();
void DeleteSelectedEntity();
void FocusSelectedEntity();
char searchHierarchyBuf[64] = "";
int leftPanelTab = 0; // 0 = Hierarchy, 1 = Tile Palette, 2 = Project Assets
bool showAssetManagerModal = false; bool showAssetManagerModal = false;
bool showCutsceneTimeline = false; bool showCutsceneTimeline = false;
bool showCodeEditorModal = false; bool showCodeEditorModal = false;
@ -96,12 +120,43 @@ public:
// Editor state // Editor state
ToolMode currentTool = TOOL_BRUSH; ToolMode currentTool = TOOL_BRUSH;
CellType selectedPaletteCell = CELL_BRICK; CellType selectedPaletteCell = CELL_BRICK;
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;
bool autoBakeLighting = true;
int draggingLightRadiusIdx = -1;
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();
void ApplyLightPreset(int lightIdx, int presetId);
void SnapLightToGrid(int lightIdx);
bool CastLightRay(int x1, int y1, int x2, int y2) const;
bool LightingNeedsBake() const;
LightingSettings GetLightingSettings() const;
// Panel Dimensions & Splitters (Interactive Resizing like Unreal / Unity)
float leftPanelWidth = 260.0f;
float rightPanelWidth = 350.0f;
float centerSplitRatio = 0.5f;
// 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;
@ -112,6 +167,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

@ -6,6 +6,7 @@
#include <string> #include <string>
#include <filesystem> #include <filesystem>
#include <cctype> #include <cctype>
#include <cstring>
#include <windows.h> #include <windows.h>
#include "../imgui/imgui.h" #include "../imgui/imgui.h"
@ -17,14 +18,19 @@ struct CodeFileEntry {
}; };
static const CodeFileEntry g_ProjectCodeFiles[] = { static const CodeFileEntry g_ProjectCodeFiles[] = {
{ "engine_entity.inc (Entity AI & Pickups)", "include/engine_entity.inc" }, { "engine_light.inc (Light Sources & Bursts)", "include/engine_light.inc" },
{ "engine_combat.inc (Player Weapons & Combat)", "include/engine_combat.inc" }, { "engine_lighting_core.inc (Static RGB Bake & Dynamic Lighting)", "include/engine_lighting_core.inc" },
{ "main.asm (Engine Entry & Main Loop)", "main.asm" }, { "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" },
{ "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" }, { "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" },
{ "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" },
{ "main.asm (Engine Entry & Frame Loop)", "main.asm" },
{ "engine_defs.inc (Constants & Structs)", "include/engine_defs.inc" }, { "engine_defs.inc (Constants & Structs)", "include/engine_defs.inc" },
{ "engine_data.inc (Memory & Game State)", "include/engine_data.inc" },
{ "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" },
{ "engine_render2d.inc (2D Canvas & HUD)", "include/engine_render2d.inc" }, { "engine_render2d.inc (2D Canvas & HUD)", "include/engine_render2d.inc" },
{ "engine_input.inc (Keyboard Input)", "include/engine_input.inc" }, { "engine_raycaster.inc (3D DDA Raycaster)", "include/engine_raycaster.inc" },
{ "engine_physics.inc (Player Physics)", "include/engine_physics.inc" } { "engine_input.inc (Keyboard & Mouse)", "include/engine_input.inc" },
{ "engine_physics.inc (Player Movement)", "include/engine_physics.inc" }
}; };
static const int g_CodeFileCount = sizeof(g_ProjectCodeFiles) / sizeof(g_ProjectCodeFiles[0]); static const int g_CodeFileCount = sizeof(g_ProjectCodeFiles) / sizeof(g_ProjectCodeFiles[0]);
@ -34,6 +40,39 @@ struct ScriptTemplate {
}; };
static const ScriptTemplate g_ScriptTemplates[] = { static const ScriptTemplate g_ScriptTemplates[] = {
{
"NPC Диалог и квест (ShowGameDialog)",
"; ==============================================================================\n"
"; NPC Quest Dialogue: Displays conversation modal and gives objective\n"
"; ==============================================================================\n"
".data\n"
"szNpcSpeaker db \"СТАРИК\", 0\n"
"szNpcText db \"ВОЗЬМИ СИНИЙ КЛЮЧ И ОТКРОЙ ЗАПЕРТУЮ ДВЕРЬ НА СЕВЕРЕ!\", 0\n"
".code\n"
" invoke ShowGameDialog, OFFSET szNpcSpeaker, OFFSET szNpcText\n"
" invoke ShowGameToast, OFFSET szKeyPickedToast, 90, COLOR_CYAN\n"
},
{
"Записка / Терминал с лором (Lore Note)",
"; ==============================================================================\n"
"; Readable Note / Terminal: Player reads lore note on [E]\n"
"; ==============================================================================\n"
".data\n"
"szNoteTitle db \"ДНЕВНИК УЧЕНОГО\", 0\n"
"szNoteBody db \"ЭКСПЕРИМЕНТ ВЫШЕЛ ИЗ-ПОД КОНТРОЛЯ. КОД ОТ ЛИФТА: 4 9 2 1.\", 0\n"
".code\n"
" invoke ShowGameDialog, OFFSET szNoteTitle, OFFSET szNoteBody\n"
},
{
"Всплывающее уведомление (Game Toast)",
"; ==============================================================================\n"
"; Floating Toast Banner (auto-centered with colored outline and fade)\n"
"; ==============================================================================\n"
".data\n"
"szSecretToast db \"СЕКРЕТНАЯ КОМНАТА ОБНАРУЖЕНА!\", 0\n"
".code\n"
" invoke ShowGameToast, OFFSET szSecretToast, 120, COLOR_YELLOW\n"
},
{ {
"Патрулирующий охранник (Patrol Guard AI)", "Патрулирующий охранник (Patrol Guard AI)",
"; ==============================================================================\n" "; ==============================================================================\n"
@ -78,6 +117,15 @@ static const ScriptTemplate g_ScriptTemplates[] = {
" invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR\n" " invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR\n"
"switch_done:\n" "switch_done:\n"
}, },
{
"Взрывная бочка (AOE Explosion Trigger)",
"; ==============================================================================\n"
"; Explosive Barrel: Deals 60 AOE damage and triggers fiery screen flash\n"
"; ==============================================================================\n"
" mov [entityState + ecx*4], ENTITY_STATE_DEAD\n"
" mov playerHitFlash, 8\n"
" add playerScore, 200\n"
},
{ {
"Кастомный элемент HUD (Custom Gauge Widget)", "Кастомный элемент HUD (Custom Gauge Widget)",
"; ==============================================================================\n" "; ==============================================================================\n"
@ -90,6 +138,42 @@ static const ScriptTemplate g_ScriptTemplates[] = {
}; };
static const int g_ScriptTemplateCount = sizeof(g_ScriptTemplates) / sizeof(g_ScriptTemplates[0]); static const int g_ScriptTemplateCount = sizeof(g_ScriptTemplates) / sizeof(g_ScriptTemplates[0]);
struct EngineApiItem {
const char* category;
const char* name;
const char* description;
const char* snippet;
};
static const EngineApiItem g_EngineApiCatalog[] = {
{ "Геймплей и Скрипты", "ShowGameToast", "Всплывающий баннер с цветной рамкой и затуханием", "invoke ShowGameToast, OFFSET szMyMessage, 90, COLOR_YELLOW\n" },
{ "Геймплей и Скрипты", "ShowGameDialog", "Диалоговое окно NPC или чтение записки", "invoke ShowGameDialog, OFFSET szSpeaker, OFFSET szText\n" },
{ "Геймплей и Скрипты", "CloseGameDialog", "Принудительно закрыть активное диалоговое окно", "call CloseGameDialog\n" },
{ "Геймплей и Скрипты", "PlayGameSFX", "Асинхронное воспроизведение звука WAV", "invoke PlayGameSFX, OFFSET szMySoundWav\n" },
{ "Геймплей и Скрипты", "Game_OnPlayerInteract", "Хук взаимодействия [E] (1=обработано, 0=дефолт)", "Game_OnPlayerInteract PROC uses ebx ecx, entityIdx:DWORD, entityTypeVal:DWORD\n ; Ваша логика по нажатию [E]\n xor eax, eax\n ret\nGame_OnPlayerInteract ENDP\n" },
{ "Геймплей и Скрипты", "Game_OnEntityKilled", "Хук уничтожения врага/бочки (триггеры боссов)", "Game_OnEntityKilled PROC uses ebx ecx, entityIdx:DWORD, entityTypeVal:DWORD\n ; Смерть сущности\n ret\nGame_OnEntityKilled ENDP\n" },
{ "Геймплей и Скрипты", "Game_OnTick", "Покадровый хук 60 Hz (таймеры, зоны, квесты)", "Game_OnTick PROC uses ebx ecx\n ; Ваша покадровая логика\n ret\nGame_OnTick ENDP\n" },
{ "Бой и оружие", "UpdateCombat", "Обновляет прицел, стрельбу и расчет урона", "call UpdateCombat\n" },
{ "Бой и оружие", "RenderCombatHUD", "Рисует оружие, прицел и статус игрока", "call RenderCombatHUD\n" },
{ "Бой и оружие", "Смена оружия", "Устанавливает активное оружие игрока", "mov playerWeapon, WEAPON_SHOTGUN ; 0=Knife, 1=Pistol, 2=Shotgun\n" },
{ "Бой и оружие", "Нанесение урона", "Уменьшает здоровье врага по индексу", "mov ecx, targetIdx\nmov eax, [entityHealth + ecx*4]\nsub eax, 35\nmov [entityHealth + ecx*4], eax\n" },
{ "Сущности и AI", "InitEntities", "Сбрасывает и спавнит сущности уровня", "call InitEntities\n" },
{ "Сущности и AI", "UpdateEntities", "AI преследования, патруля и подбор предметов", "call UpdateEntities\n" },
{ "Сущности и AI", "Патруль врага", "Инкремент таймера контрольной точки патруля", "mov ecx, [entityTimer + edx*4]\ninc ecx\nmov [entityTimer + edx*4], ecx\n" },
{ "2D Рендеринг", "DrawRect", "Рисует закрашенный прямоугольник (X, Y, W, H, Color)", "invoke DrawRect, 10, 20, 80, 12, 00336699h\n" },
{ "2D Рендеринг", "DrawRectOutline", "Рисует рамку прямоугольника", "invoke DrawRectOutline, 10, 20, 80, 12, COLOR_WHITE\n" },
{ "2D Рендеринг", "DrawString8x8", "Выводит строку шрифтом 8x8 (CP1251)", "invoke DrawString8x8, 10, 22, OFFSET myString, COLOR_YELLOW\n" },
{ "2D Рендеринг", "DrawNumber8x8", "Выводит целое число на экран", "invoke DrawNumber8x8, 60, 22, myNumberVar, COLOR_WHITE\n" },
{ "Рэйкастинг и Мир", "tableCos / tableSin", "Чтение тригонометрии 16.16 (угол 0..511)", "mov eax, playerAngle\nand eax, ANGLE_MASK\nmov ebx, [tableCos + eax*4]\nmov edx, [tableSin + eax*4]\n" },
{ "Рэйкастинг и Мир", "depthBuffer", "Глубина стен для спрайтового z-теста", "mov eax, [depthBuffer + screenX*4]\n" },
{ "Рэйкастинг и Мир", "CheckWallCollision", "Проверка коллизии с ячейкой карты", "invoke CheckWallCollision, targetX, targetY\n" }
};
static const int g_EngineApiCount = sizeof(g_EngineApiCatalog) / sizeof(g_EngineApiCatalog[0]);
struct CodeEditorState { struct CodeEditorState {
int currentFile = 0; int currentFile = 0;
std::string fileContent; std::string fileContent;
@ -106,6 +190,10 @@ struct CodeEditorState {
int buildExitCode = 0; int buildExitCode = 0;
bool hasBuildResult = false; bool hasBuildResult = false;
// Search bar state
char searchBuf[64] = "";
int searchMatchCount = 0;
struct ParsedError { struct ParsedError {
std::string file; std::string file;
int line = 0; int line = 0;
@ -193,8 +281,9 @@ struct CodeEditorState {
buildLog.clear(); buildLog.clear();
buildErrors.clear(); buildErrors.clear();
hasBuildResult = false; hasBuildResult = false;
buildExitCode = 0;
fs::path root(projectRoot); fs::path root = fs::path(projectRoot);
fs::path buildBat = root / "build.bat"; fs::path buildBat = root / "build.bat";
if (!fs::exists(buildBat)) { if (!fs::exists(buildBat)) {
buildBat = root.parent_path() / "build.bat"; buildBat = root.parent_path() / "build.bat";
@ -202,7 +291,7 @@ struct CodeEditorState {
} }
if (!fs::exists(buildBat)) { if (!fs::exists(buildBat)) {
buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n"; buildLog = "[ERROR] Не найден скрипт сборки build.bat в: " + buildBat.string() + "\n";
hasBuildResult = true; hasBuildResult = true;
buildExitCode = 1; buildExitCode = 1;
return; return;
@ -319,14 +408,13 @@ struct CodeEditorState {
if (!std::isdigit(static_cast<unsigned char>(c))) { isNum = false; break; } if (!std::isdigit(static_cast<unsigned char>(c))) { isNum = false; break; }
} }
if (isNum) { if (isNum) {
ParsedError err{}; ParsedError err;
err.file = line.substr(0, openParen); err.file = line.substr(0, openParen);
err.line = std::atoi(lineStr.c_str()); err.line = std::atoi(lineStr.c_str());
err.message = line.substr(colon + 3); err.message = line.substr(colon + 3);
buildErrors.push_back(err); buildErrors.push_back(err);
} }
} }
if (end == std::string::npos) break; if (end == std::string::npos) break;
start = end + 1; start = end + 1;
} }
@ -340,25 +428,23 @@ void EditorApp::RenderCodeEditorWindow() {
g_CodeEditor.PollBuild(); g_CodeEditor.PollBuild();
if (!showCodeEditorModal) return; if (!showCodeEditorModal) return;
ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) { if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
ImGui::End(); ImGui::End();
return; return;
} }
// Top Menu Bar char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
// Menu Bar
if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenuBar()) {
if (ImGui::BeginMenu("Файл")) { if (ImGui::BeginMenu("Файл")) {
if (ImGui::MenuItem("Сохранить (Ctrl+S)", "Ctrl+S")) { if (ImGui::MenuItem("Сохранить (Ctrl+S)", "Ctrl+S")) {
char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
g_CodeEditor.SaveFile(root); g_CodeEditor.SaveFile(root);
} }
if (ImGui::MenuItem("Собрать движок (F7)", "F7")) { if (ImGui::MenuItem("Собрать проект (F7 / Ctrl+B)", "Ctrl+B")) {
char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
g_CodeEditor.StartBuild(root); g_CodeEditor.StartBuild(root);
} }
ImGui::Separator(); ImGui::Separator();
@ -371,7 +457,6 @@ void EditorApp::RenderCodeEditorWindow() {
if (ImGui::BeginMenu("Шаблоны кода")) { if (ImGui::BeginMenu("Шаблоны кода")) {
for (int i = 0; i < g_ScriptTemplateCount; ++i) { for (int i = 0; i < g_ScriptTemplateCount; ++i) {
if (ImGui::MenuItem(g_ScriptTemplates[i].title)) { if (ImGui::MenuItem(g_ScriptTemplates[i].title)) {
// Append template code to text buffer
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data()); size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
size_t tLen = std::strlen(g_ScriptTemplates[i].code); size_t tLen = std::strlen(g_ScriptTemplates[i].code);
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024); g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
@ -386,12 +471,15 @@ void EditorApp::RenderCodeEditorWindow() {
ImGui::EndMenuBar(); ImGui::EndMenuBar();
} }
// Toolbar Buttons RenderCodeEditorContents(root);
ImGui::BeginGroup(); ImGui::End();
char module[MAX_PATH]{}; }
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
void EditorApp::RenderCodeEditorContents(const std::string& root) {
g_CodeEditor.PollBuild();
// Top Toolbar
ImGui::BeginGroup();
if (ImGui::Button("Сохранить (Ctrl+S)")) { if (ImGui::Button("Сохранить (Ctrl+S)")) {
g_CodeEditor.SaveFile(root); g_CodeEditor.SaveFile(root);
} }
@ -401,51 +489,110 @@ void EditorApp::RenderCodeEditorWindow() {
ImGui::Button("Компиляция..."); ImGui::Button("Компиляция...");
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} else { } else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.55f, 0.22f, 1.0f));
if (ImGui::Button("Собрать движок (F7)")) { if (ImGui::Button("Собрать проект (F7)")) {
g_CodeEditor.StartBuild(root); g_CodeEditor.StartBuild(root);
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
// Search bar
ImGui::SetNextItemWidth(200);
if (ImGui::InputTextWithHint("##SearchCode", "Поиск в коде...", g_CodeEditor.searchBuf, sizeof(g_CodeEditor.searchBuf))) {
if (g_CodeEditor.searchBuf[0]) {
int count = 0;
const char* p = g_CodeEditor.textBuffer.data();
while ((p = std::strstr(p, g_CodeEditor.searchBuf)) != nullptr) {
count++;
p += std::strlen(g_CodeEditor.searchBuf);
}
g_CodeEditor.searchMatchCount = count;
} else {
g_CodeEditor.searchMatchCount = 0;
}
}
if (g_CodeEditor.searchBuf[0]) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "[%d совп.]", g_CodeEditor.searchMatchCount);
}
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("| %s", g_CodeEditor.statusText.c_str()); ImGui::TextDisabled("| %s", g_CodeEditor.statusText.c_str());
ImGui::EndGroup(); ImGui::EndGroup();
ImGui::Separator(); ImGui::Separator();
// Split Left (File list) and Right (Editor + Output) // Split Left (Navigator & API Catalog) and Right (Editor + Output)
ImGui::Columns(2, "CodeEditorColumns", true); ImGui::Columns(2, "CodeEditorColumns", true);
static bool initCol = true; static bool initCol = true;
if (initCol) { if (initCol) {
ImGui::SetColumnWidth(0, 240); ImGui::SetColumnWidth(0, 270);
initCol = false; initCol = false;
// Load initial file
g_CodeEditor.LoadFile(0, root); g_CodeEditor.LoadFile(0, root);
} }
// Left Column: Source File List // Left Column: Tabbed Navigation (Files / API Catalog / Templates)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:"); ImGui::BeginTabBar("NavTabs");
for (int i = 0; i < g_CodeFileCount; ++i) {
bool isSelected = (g_CodeEditor.currentFile == i); if (ImGui::BeginTabItem("Файлы")) {
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) { ImGui::Spacing();
g_CodeEditor.LoadFile(i, root); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:");
for (int i = 0; i < g_CodeFileCount; ++i) {
bool isSelected = (g_CodeEditor.currentFile == i);
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
g_CodeEditor.LoadFile(i, root);
}
} }
ImGui::EndTabItem();
} }
ImGui::Spacing(); if (ImGui::BeginTabItem("Справочник API")) {
ImGui::Separator(); ImGui::Spacing();
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Быстрые шаблоны:"); ImGui::TextColored(ImVec4(0.2f, 0.85f, 1.0f, 1.0f), "Функции и процедуры движка:");
for (int i = 0; i < g_ScriptTemplateCount; ++i) { ImGui::BeginChild("ApiCatalogChild", ImVec2(0, 0), false);
if (ImGui::SmallButton(g_ScriptTemplates[i].title)) { for (int i = 0; i < g_EngineApiCount; ++i) {
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data()); ImGui::PushID(i);
size_t tLen = std::strlen(g_ScriptTemplates[i].code); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%s", g_EngineApiCatalog[i].name);
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024); ImGui::SameLine(ImGui::GetContentRegionAvail().x - 65);
std::strcat(g_CodeEditor.textBuffer.data(), "\n"); if (ImGui::SmallButton("Вставить")) {
std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code); size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
g_CodeEditor.isModified = true; size_t sLen = std::strlen(g_EngineApiCatalog[i].snippet);
g_CodeEditor.EnsureBufferSize(curLen + sLen + 1024);
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
std::strcat(g_CodeEditor.textBuffer.data(), g_EngineApiCatalog[i].snippet);
g_CodeEditor.isModified = true;
g_CodeEditor.statusText = std::string("Вставлен код: ") + g_EngineApiCatalog[i].name;
}
ImGui::TextDisabled(" %s", g_EngineApiCatalog[i].description);
ImGui::Separator();
ImGui::PopID();
} }
ImGui::EndChild();
ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem("Шаблоны")) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.5f, 1.0f), "Быстрые шаблоны скриптов:");
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
if (ImGui::Button(g_ScriptTemplates[i].title, ImVec2(-1, 26))) {
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code);
g_CodeEditor.isModified = true;
g_CodeEditor.statusText = std::string("Вставлен шаблон: ") + g_ScriptTemplates[i].title;
}
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
ImGui::NextColumn(); ImGui::NextColumn();
// Right Column: Code Editor & Compiler Console // Right Column: Code Editor & Compiler Console
@ -453,7 +600,6 @@ void EditorApp::RenderCodeEditorWindow() {
float editorHeight = availY * 0.65f; float editorHeight = availY * 0.65f;
float outputHeight = availY - editorHeight - 20; float outputHeight = availY - editorHeight - 20;
// Multiline Text Editor with monospace font
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Файл: %s %s", ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Файл: %s %s",
g_ProjectCodeFiles[g_CodeEditor.currentFile].relativePath.c_str(), g_ProjectCodeFiles[g_CodeEditor.currentFile].relativePath.c_str(),
g_CodeEditor.isModified ? "*" : ""); g_CodeEditor.isModified ? "*" : "");
@ -470,10 +616,10 @@ void EditorApp::RenderCodeEditorWindow() {
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
// Hotkey: Ctrl+S to save // Hotkey: Ctrl+S to save, Ctrl+B to build
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::GetIO().KeyCtrl) {
ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S)) { if (ImGui::IsKeyPressed(ImGuiKey_S)) g_CodeEditor.SaveFile(root);
g_CodeEditor.SaveFile(root); if (ImGui::IsKeyPressed(ImGuiKey_B)) g_CodeEditor.StartBuild(root);
} }
ImGui::Separator(); ImGui::Separator();
@ -483,9 +629,8 @@ void EditorApp::RenderCodeEditorWindow() {
if (ImGui::BeginTabItem("Вывод компилятора (ml.exe)")) { if (ImGui::BeginTabItem("Вывод компилятора (ml.exe)")) {
ImGui::BeginChild("CompilerOutputChild", ImVec2(0, outputHeight), true); ImGui::BeginChild("CompilerOutputChild", ImVec2(0, outputHeight), true);
if (g_CodeEditor.buildLog.empty() && !g_CodeEditor.isBuilding) { if (g_CodeEditor.buildLog.empty() && !g_CodeEditor.isBuilding) {
ImGui::TextDisabled("Нажмите 'Собрать движок (F7)' для компиляции ассемблерного кода..."); ImGui::TextDisabled("Нажмите 'Собрать проект (F7)' для компиляции ассемблерного кода...");
} else { } else {
// Display log line by line with color tags
size_t logStart = 0; size_t logStart = 0;
while (logStart < g_CodeEditor.buildLog.size()) { while (logStart < g_CodeEditor.buildLog.size()) {
size_t logEnd = g_CodeEditor.buildLog.find('\n', logStart); size_t logEnd = g_CodeEditor.buildLog.find('\n', logStart);
@ -525,7 +670,6 @@ void EditorApp::RenderCodeEditorWindow() {
snprintf(lineNumBuf, sizeof(lineNumBuf), "%d", err.line); snprintf(lineNumBuf, sizeof(lineNumBuf), "%d", err.line);
std::string label = err.file + ":" + lineNumBuf + " - " + err.message; std::string label = err.file + ":" + lineNumBuf + " - " + err.message;
if (ImGui::Selectable(label.c_str())) { if (ImGui::Selectable(label.c_str())) {
// Switch to corresponding file if matched
for (int f = 0; f < g_CodeFileCount; ++f) { for (int f = 0; f < g_CodeFileCount; ++f) {
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos || if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) { g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) {
@ -542,5 +686,4 @@ void EditorApp::RenderCodeEditorWindow() {
ImGui::EndTabBar(); ImGui::EndTabBar();
ImGui::Columns(1); ImGui::Columns(1);
ImGui::End();
} }

View file

@ -0,0 +1,268 @@
void EditorApp::ApplyLightPreset(int lightIdx, int presetId) {
if (lightIdx < 0 || lightIdx >= static_cast<int>(lightSources.size())) return;
PushUndo();
auto& l = lightSources[lightIdx];
l.flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS;
switch (presetId) {
case LIGHT_PRESET_TORCH: // 🕯 Факел
l.color = 0x00FFB040;
l.radius = 240;
l.intensity = 220;
l.curIntensity = 220;
l.lightType = LIGHT_TYPE_POINT;
l.flags |= LIGHT_FLAG_FLICKER;
break;
case LIGHT_PRESET_LAMP: // 💡 Лампа
l.color = 0x00E0E8FF;
l.radius = 280;
l.intensity = 240;
l.curIntensity = 240;
l.lightType = LIGHT_TYPE_STATIC;
break;
case LIGHT_PRESET_ALARM: // 🚨 Тревожная сирена
l.color = 0x00FF2020;
l.radius = 300;
l.intensity = 250;
l.curIntensity = 250;
l.lightType = LIGHT_TYPE_PULSE;
break;
case LIGHT_PRESET_CANDLE: // 🕯 Мягкая свеча
l.color = 0x00FF9520;
l.radius = 140;
l.intensity = 180;
l.curIntensity = 180;
l.lightType = LIGHT_TYPE_POINT;
l.flags |= LIGHT_FLAG_FLICKER;
break;
case LIGHT_PRESET_NEON: // 🔮 Киберпанк неон
l.color = 0x0000E5FF;
l.radius = 260;
l.intensity = 230;
l.curIntensity = 230;
l.lightType = LIGHT_TYPE_POINT;
break;
case LIGHT_PRESET_TOXIC: // ☢ Токсик
l.color = 0x0039FF14;
l.radius = 250;
l.intensity = 220;
l.curIntensity = 220;
l.lightType = LIGHT_TYPE_PULSE;
break;
case LIGHT_PRESET_SPOTLIGHT: // 🔦 Прожектор
l.color = 0x00FFFFFF;
l.radius = 380;
l.intensity = 255;
l.curIntensity = 255;
l.lightType = LIGHT_TYPE_STATIC;
break;
}
if (autoBakeLighting) BakeLighting();
statusMessage = "Применен пресет света.";
}
void EditorApp::SnapLightToGrid(int lightIdx) {
if (lightIdx < 0 || lightIdx >= static_cast<int>(lightSources.size())) return;
PushUndo();
auto& l = lightSources[lightIdx];
int cellX = std::clamp(l.x / 64, 0, MAP_GRID_W - 1);
int cellY = std::clamp(l.y / 64, 0, MAP_GRID_H - 1);
l.x = cellX * 64 + 32;
l.y = cellY * 64 + 32;
if (autoBakeLighting) BakeLighting();
statusMessage = "Источник света выровнен по центру клетки (" + std::to_string(cellX) + ", " + std::to_string(cellY) + ").";
}
void EditorApp::RenderLightingSettings() {
auto edited = GetLightingSettings();
bool changed = false, activated = false;
auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); };
const char* modes[] = { "Без освещения (Fullbright)", "Только запечённый свет (Baked)", "Запечённый + Динамический (Full Physics)" };
int mode = edited.mode;
changed |= ImGui::Combo("Режим света", &mode, modes, 3); undoItem(); edited.mode = mode;
int ambient = edited.ambient;
changed |= ImGui::SliderInt("Фоновый свет (Ambient)", &ambient, 0, 255); undoItem(); edited.ambient = ambient;
bool flash = (edited.flashlight != 0);
changed |= ImGui::Checkbox("Фонарик игрока [F]", &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 (autoBakeLighting) BakeLighting();
}
ImGui::Spacing();
ImGui::Checkbox("Авто-запекание (Live Realtime Update)", &autoBakeLighting);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Мгновенно обновляет карту теней и 3D вид при любых изменениях источников и стен.");
ImGui::Checkbox("Карта запечённого света (Хитмап на 2D сетке)", &showLightHeatmap);
if (ImGui::Button("💡 Запечь постоянный свет сейчас", ImVec2(-1, 26))) {
PushUndo();
BakeLighting();
}
if (LightingNeedsBake()) {
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.2f, 1.0f), "⚠ Геометрия или источники изменены: требуется запекание");
} else {
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "✔ Постоянный свет запечён (1024 ячейки)");
}
}
void EditorApp::RenderLightingInspector() {
if (selectedLightIdx < 0 || selectedLightIdx >= static_cast<int>(lightSources.size())) return;
auto& light = lightSources[selectedLightIdx];
const char* typeDesc = (light.lightType == LIGHT_TYPE_STATIC) ? "Постоянный (Запекаемый)" :
(light.lightType == LIGHT_TYPE_PULSE) ? "Пульсирующий (Сирена)" :
(light.flags & LIGHT_FLAG_FLICKER) ? "Мерцающий факел" : "Точечный (Динамический)";
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.25f, 1.0f), "Источник света #%d из %d: %s",
selectedLightIdx + 1, static_cast<int>(lightSources.size()), typeDesc);
ImGui::Separator();
// Quick Action Bar (2x2 Grid)
float colW2 = (ImGui::GetContentRegionAvail().x - 6.0f) * 0.5f;
if (ImGui::Button("Фокус (F)", ImVec2(colW2, 24))) { FocusSelectedEntity(); }
ImGui::SameLine();
if (ImGui::Button("Дублировать (Ctrl+D)", ImVec2(colW2, 24))) { DuplicateSelectedEntity(); }
if (ImGui::Button("К сетке (Snap)", ImVec2(colW2, 24))) { SnapLightToGrid(selectedLightIdx); }
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.15f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.20f, 0.20f, 1.0f));
if (ImGui::Button("Удалить (Del)", ImVec2(colW2, 24))) {
ImGui::PopStyleColor(2);
DeleteSelectedEntity();
return;
}
ImGui::PopStyleColor(2);
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f), "Быстрые пресеты источников:");
// Preset Buttons: Clean 3-Column Grid (all 7 presets visible!)
float colW3 = (ImGui::GetContentRegionAvail().x - 12.0f) / 3.0f;
if (ImGui::Button("Факел", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TORCH);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Факел: Тёплый огонь (оранжевый, 240px, мерцание, тени)");
ImGui::SameLine();
if (ImGui::Button("Лампа", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_LAMP);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Лампа: Запекаемый дневной свет (280px, статика)");
ImGui::SameLine();
if (ImGui::Button("Сирена", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_ALARM);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Сирена: Аварийный маяк (красный, 300px, пульсация)");
if (ImGui::Button("Свеча", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_CANDLE);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Свеча: Мягкий тёплый огонёк (140px, мерцание)");
ImGui::SameLine();
if (ImGui::Button("Неон", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_NEON);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Неон: Циановый неоновый свет (260px, динамика)");
ImGui::SameLine();
if (ImGui::Button("Токсик", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TOXIC);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Токсик: Зелёное радиационное свечение (250px, пульс)");
if (ImGui::Button("Прожектор (Направленный)", ImVec2(-1, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_SPOTLIGHT);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Прожектор: Мощный белый свет (380px, статика)");
ImGui::Separator();
EditorLightSource edited = light;
bool changed = false, activated = false;
auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); };
// Toggle Active
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);
// Coordinates with Cell info
int curCellX = edited.x / 64;
int curCellY = edited.y / 64;
ImGui::Text("Клетка: [%d, %d] | Мировые координаты: (%d, %d)", curCellX, curCellY, edited.x, edited.y);
float xCells = edited.x / 64.0f;
float yCells = edited.y / 64.0f;
if (ImGui::SliderFloat("Позиция X (клетки)", &xCells, 0.5f, 31.5f, "%.2f")) {
edited.x = std::clamp(static_cast<int>(xCells * 64.0f), 32, (MAP_GRID_W - 1) * 64);
changed = true;
} undoItem();
if (ImGui::SliderFloat("Позиция Y (клетки)", &yCells, 0.5f, 31.5f, "%.2f")) {
edited.y = std::clamp(static_cast<int>(yCells * 64.0f), 32, (MAP_GRID_H - 1) * 64);
changed = true;
} undoItem();
// Radius & Intensity
changed |= ImGui::SliderInt("Радиус (px)", &edited.radius, 32, 2048, "%d px"); undoItem();
changed |= ImGui::SliderInt("Яркость", &edited.intensity, 0, 255, "%d / 255"); undoItem();
edited.curIntensity = edited.intensity;
// Color Picker & Quick Swatches
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();
// Quick Color Swatches
ImGui::TextDisabled("Оттенки:");
ImGui::SameLine();
auto ColorSwatch = [&](const char* label, uint32_t hexColor) {
float c[3] = { ((hexColor >> 16) & 255) / 255.f, ((hexColor >> 8) & 255) / 255.f, (hexColor & 255) / 255.f };
if (ImGui::ColorButton(label, ImVec4(c[0], c[1], c[2], 1.0f), 0, ImVec2(18, 18))) {
edited.color = hexColor;
changed = true;
}
ImGui::SameLine();
};
ColorSwatch("##warm_fire", 0x00FFB040);
ColorSwatch("##cool_white", 0x00E0E8FF);
ColorSwatch("##neon_cyan", 0x0000E5FF);
ColorSwatch("##ruby_red", 0x00FF2020);
ColorSwatch("##toxic_green", 0x0039FF14);
ColorSwatch("##pure_white", 0x00FFFFFF);
ImGui::NewLine();
// Light Type
const char* types[] = { "Постоянный (запекание в lightmap)", "Точечный (динамический реалтайм)", "Пульсирующий (динамическая сирена)" };
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();
// Behavior flags
bool shadows = (edited.flags & LIGHT_FLAG_SHADOWS) != 0;
changed |= ImGui::Checkbox("Тени от стен и дверей", &shadows); undoItem();
edited.flags = (edited.flags & ~LIGHT_FLAG_SHADOWS) | (shadows ? LIGHT_FLAG_SHADOWS : 0);
if (edited.lightType != LIGHT_TYPE_STATIC) {
bool flicker = (edited.flags & LIGHT_FLAG_FLICKER) != 0;
changed |= ImGui::Checkbox("Мерцание огня / факела (Flicker)", &flicker); undoItem();
edited.flags = (edited.flags & ~LIGHT_FLAG_FLICKER) | (flicker ? LIGHT_FLAG_FLICKER : 0);
}
if (activated) PushUndo();
if (changed) {
light = edited;
if (autoBakeLighting) BakeLighting();
}
ImGui::Spacing();
ImGui::Separator();
if (ImGui::CollapsingHeader("Глобальное освещение карты")) {
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
@ -48,7 +48,8 @@ enum EntityArchetype : uint8_t {
ENTITY_ARCHETYPE_AMMO = 3, // Ammo pickup (+20 Ammo) ENTITY_ARCHETYPE_AMMO = 3, // Ammo pickup (+20 Ammo)
ENTITY_ARCHETYPE_KEY = 4, // Door Key pickup ENTITY_ARCHETYPE_KEY = 4, // Door Key pickup
ENTITY_ARCHETYPE_SWITCH = 5, // Interactive Switch / Trigger ENTITY_ARCHETYPE_SWITCH = 5, // Interactive Switch / Trigger
ENTITY_ARCHETYPE_COUNT = 6 ENTITY_ARCHETYPE_BARREL = 6, // Explosive Barrel (AOE Blast)
ENTITY_ARCHETYPE_COUNT = 7
}; };
#pragma pack(push, 1) #pragma pack(push, 1)
@ -65,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)
@ -78,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 {
@ -104,6 +106,7 @@ struct SpriteEntry {
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) { inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
uint8_t t = static_cast<uint8_t>(s.flags & 0xFF); uint8_t t = static_cast<uint8_t>(s.flags & 0xFF);
if (s.flags == 0) { if (s.flags == 0) {
if (s.textureId == 11) return ENTITY_ARCHETYPE_BARREL;
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD; if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT; if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT;
if (s.textureId == 15) return ENTITY_ARCHETYPE_AMMO; if (s.textureId == 15) return ENTITY_ARCHETYPE_AMMO;
@ -136,3 +139,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

@ -30,46 +30,51 @@ void SetupGodotStyle() {
ImGuiStyle& style = ImGui::GetStyle(); ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors; ImVec4* colors = style.Colors;
style.WindowRounding = 4.0f; style.WindowRounding = 8.0f;
style.FrameRounding = 3.0f; style.ChildRounding = 6.0f;
style.PopupRounding = 4.0f; style.FrameRounding = 5.0f;
style.ScrollbarRounding = 4.0f; style.PopupRounding = 8.0f;
style.GrabRounding = 3.0f; style.ScrollbarRounding = 7.0f;
style.TabRounding = 4.0f; style.GrabRounding = 5.0f;
style.TabRounding = 6.0f;
style.FrameBorderSize = 1.0f; style.FrameBorderSize = 1.0f;
style.WindowBorderSize = 1.0f; style.WindowBorderSize = 1.0f;
style.WindowPadding = ImVec2(10.0f, 10.0f);
style.FramePadding = ImVec2(8.0f, 5.0f);
style.ItemSpacing = ImVec2(8.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
// Dark graphite palette (Godot 4 inspired) // Modern Game Studio Dark Palette (Unreal / Unity / Godot Pro)
colors[ImGuiCol_Text] = ImVec4(0.92f, 0.92f, 0.95f, 1.00f); colors[ImGuiCol_Text] = ImVec4(0.96f, 0.97f, 0.99f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.52f, 0.55f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.54f, 0.60f, 1.00f);
colors[ImGuiCol_WindowBg] = ImVec4(0.14f, 0.15f, 0.17f, 1.00f); colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
colors[ImGuiCol_ChildBg] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f); colors[ImGuiCol_ChildBg] = ImVec4(0.14f, 0.15f, 0.18f, 1.00f);
colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.14f, 0.16f, 0.20f, 0.98f);
colors[ImGuiCol_Border] = ImVec4(0.24f, 0.25f, 0.28f, 1.00f); colors[ImGuiCol_Border] = ImVec4(0.24f, 0.26f, 0.31f, 1.00f);
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.21f, 0.24f, 1.00f); colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.20f, 0.25f, 1.00f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.30f, 0.34f, 1.00f); colors[ImGuiCol_FrameBgHovered] = ImVec4(0.25f, 0.29f, 0.36f, 1.00f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f); colors[ImGuiCol_FrameBgActive] = ImVec4(0.30f, 0.36f, 0.45f, 1.00f);
colors[ImGuiCol_TitleBg] = ImVec4(0.11f, 0.12f, 0.13f, 1.00f); colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); colors[ImGuiCol_TitleBgActive] = ImVec4(0.13f, 0.15f, 0.19f, 1.00f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f); colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.11f, 0.12f, 0.60f); colors[ImGuiCol_ScrollbarBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.80f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.27f, 0.30f, 1.00f); colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.28f, 0.35f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.35f, 0.38f, 0.42f, 1.00f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.34f, 0.39f, 0.48f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.45f, 0.48f, 0.52f, 1.00f); colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.42f, 0.48f, 0.60f, 1.00f);
colors[ImGuiCol_CheckMark] = ImVec4(0.30f, 0.70f, 1.00f, 1.00f); colors[ImGuiCol_CheckMark] = ImVec4(0.05f, 0.65f, 0.95f, 1.00f);
colors[ImGuiCol_SliderGrab] = ImVec4(0.30f, 0.65f, 0.95f, 1.00f); colors[ImGuiCol_SliderGrab] = ImVec4(0.05f, 0.60f, 0.92f, 1.00f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.40f, 0.75f, 1.00f, 1.00f); colors[ImGuiCol_SliderGrabActive] = ImVec4(0.20f, 0.75f, 1.00f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f); colors[ImGuiCol_Button] = ImVec4(0.20f, 0.23f, 0.29f, 1.00f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.33f, 0.38f, 1.00f); colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.34f, 0.43f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f); colors[ImGuiCol_ButtonActive] = ImVec4(0.35f, 0.43f, 0.55f, 1.00f);
colors[ImGuiCol_Header] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.32f, 1.00f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f); colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.35f, 0.45f, 1.00f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f); colors[ImGuiCol_HeaderActive] = ImVec4(0.35f, 0.44f, 0.56f, 1.00f);
colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f); colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.29f, 1.00f);
colors[ImGuiCol_Tab] = ImVec4(0.16f, 0.18f, 0.20f, 1.00f); colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.16f, 0.20f, 1.00f);
colors[ImGuiCol_TabHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f); colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.34f, 0.42f, 1.00f);
colors[ImGuiCol_TabActive] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); colors[ImGuiCol_TabActive] = ImVec4(0.22f, 0.27f, 0.35f, 1.00f);
} }
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
@ -90,7 +95,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
return 1; return 1;
} }
ShowWindow(hWnd, SW_SHOWDEFAULT); ShowWindow(hWnd, SW_MAXIMIZE);
UpdateWindow(hWnd); UpdateWindow(hWnd);
DragAcceptFiles(hWnd, TRUE); DragAcceptFiles(hWnd, TRUE);

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

@ -16,6 +16,12 @@ struct EblanProject {
std::string windowTitle = "EBLAN ENGINE 3D - Standalone Retro Game"; std::string windowTitle = "EBLAN ENGINE 3D - Standalone Retro Game";
int defaultWidth = 1280; int defaultWidth = 1280;
int defaultHeight = 960; int defaultHeight = 960;
int gameMode = 1; // 0 = Adventure (No weapons / Peaceful), 1 = Action (Weapons)
bool showWeapons = 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);
@ -31,6 +37,15 @@ struct EblanProject {
content += std::string("DefaultWidth=") + numBuf + "\r\n"; content += std::string("DefaultWidth=") + numBuf + "\r\n";
snprintf(numBuf, sizeof(numBuf), "%d", defaultHeight); snprintf(numBuf, sizeof(numBuf), "%d", defaultHeight);
content += std::string("DefaultHeight=") + numBuf + "\r\n"; content += std::string("DefaultHeight=") + numBuf + "\r\n";
snprintf(numBuf, sizeof(numBuf), "%d", gameMode);
content += std::string("GameMode=") + numBuf + "\r\n";
content += std::string("ShowWeapons=") + (showWeapons ? "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);
@ -65,6 +80,12 @@ struct EblanProject {
else if (key == "WindowTitle") windowTitle = val; else if (key == "WindowTitle") windowTitle = val;
else if (key == "DefaultWidth") defaultWidth = std::atoi(val.c_str()); else if (key == "DefaultWidth") defaultWidth = std::atoi(val.c_str());
else if (key == "DefaultHeight") defaultHeight = std::atoi(val.c_str()); else if (key == "DefaultHeight") defaultHeight = 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 == "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

@ -1,5 +1,5 @@
; ============================================================================== ; ==============================================================================
; EBLAN ENGINE - Player Combat, Weapon Shooting & Combat HUD ; EBLAN ENGINE - Player Combat, Multi-Weapon Arsenal & Combat HUD
; ============================================================================== ; ==============================================================================
.code .code
@ -9,6 +9,8 @@ DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
Game_OnEntityKilled PROTO :DWORD, :DWORD
; Trace the current world along the player's forward direction. EAX = first ; Trace the current world along the player's forward direction. EAX = first
; blocking boundary in 24.8 world units. Glass blocks shots (unlike rendering). ; blocking boundary in 24.8 world units. Glass blocks shots (unlike rendering).
@ -143,6 +145,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi
LOCAL sprX:DWORD, sprY:DWORD LOCAL sprX:DWORD, sprY:DWORD
LOCAL transX:DWORD, transY:DWORD LOCAL transX:DWORD, transY:DWORD
LOCAL targetHalfWidth:DWORD, wallDepth:DWORD LOCAL targetHalfWidth:DWORD, wallDepth:DWORD
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitHalfWidth:DWORD
call IsGameplayBlocked call IsGameplayBlocked
test eax, eax test eax, eax
@ -162,9 +165,17 @@ UpdateCombat PROC uses ebx ecx edx esi edi
dec playerHitFlash dec playerHitFlash
@@: @@:
; If player is dead, cannot attack ; If player is dead, game is paused, or in peaceful adventure mode, cannot attack
cmp isPlayerDead, 0 cmp isPlayerDead, 0
jne combat_done jne combat_done
cmp isGamePaused, 0
jne combat_done
cmp gameMode, GAME_MODE_ADVENTURE
je combat_done
cmp enableWeapons, 0
je combat_done
cmp playerWeapon, WEAPON_NONE
je combat_done
; 2. Check attack input ; 2. Check attack input
cmp keyAttack, 0 cmp keyAttack, 0
@ -172,16 +183,53 @@ UpdateCombat PROC uses ebx ecx edx esi edi
cmp playerAttackCooldown, 0 cmp playerAttackCooldown, 0
jne combat_done jne combat_done
; Check ammo ; Set weapon parameters based on playerWeapon
cmp playerAmmo, 0 mov weaponDamage, 35
jle combat_dry_fire mov maxHitDist, 999999
mov hitHalfWidth, 16
; Fire weapon! cmp playerWeapon, WEAPON_KNIFE
je setup_knife
cmp playerWeapon, WEAPON_SHOTGUN
je setup_shotgun
jmp setup_pistol
setup_knife:
; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown
mov weaponDamage, 30
mov maxHitDist, 64
mov hitHalfWidth, 24
mov playerAttackCooldown, 8
mov playerFlashTimer, 3
jmp fire_hitscan
setup_shotgun:
; Shotgun: needs 2 ammo, high damage (70), wider world hitbox, long cooldown
cmp playerAmmo, 2
jl combat_dry_fire
sub playerAmmo, 2
mov weaponDamage, 70
mov maxHitDist, 999999
mov hitHalfWidth, 32
mov playerAttackCooldown, 22
mov playerFlashTimer, 7
invoke SpawnMuzzleFlashLight, playerX, playerY
jmp fire_hitscan
setup_pistol:
; Pistol: needs 1 ammo, accurate world hitbox, medium cooldown (12)
cmp playerAmmo, 1
jl combat_dry_fire
dec playerAmmo dec playerAmmo
mov playerAttackCooldown, 10 mov weaponDamage, 35
mov maxHitDist, 999999
mov hitHalfWidth, 16
mov playerAttackCooldown, 12
mov playerFlashTimer, 5 mov playerFlashTimer, 5
invoke SpawnMuzzleFlashLight, playerX, playerY
; 3. Hitscan check against enemies in center crosshair fire_hitscan:
; 3. Hitscan check against enemies & destructible barrels in crosshair
; Camera basis for -playerAngle ; Camera basis for -playerAngle
mov eax, ANGLE_512 mov eax, ANGLE_512
sub eax, playerAngle sub eax, playerAngle
@ -206,9 +254,16 @@ hitscan_loop:
mov ecx, i mov ecx, i
mov eax, [entityType + ecx*4] mov eax, [entityType + ecx*4]
cmp eax, ENTITY_TYPE_GUARD
jne next_hitscan_target
; Check if Guard or Barrel
cmp eax, ENTITY_TYPE_GUARD
je hitscan_target_valid
cmp eax, ENTITY_TYPE_BARREL
je hitscan_target_valid
jmp next_hitscan_target
hitscan_target_valid:
mov eax, [entityState + ecx*4] mov eax, [entityState + ecx*4]
cmp eax, ENTITY_STATE_DEAD cmp eax, ENTITY_STATE_DEAD
je next_hitscan_target je next_hitscan_target
@ -245,6 +300,11 @@ hitscan_loop:
cmp transY, 16 cmp transY, 16
jle next_hitscan_target jle next_hitscan_target
; Must be closer than maximum weapon hit distance
mov eax, transY
cmp eax, maxHitDist
jg next_hitscan_target
; Compare in 24.8 so a target on a wall boundary is not hit through it. ; Compare in 24.8 so a target on a wall boundary is not hit through it.
mov eax, transY mov eax, transY
shl eax, 8 shl eax, 8
@ -262,14 +322,14 @@ hitscan_loop:
add ecx, eax add ecx, eax
mov transX, ecx mov transX, ecx
; World-space billboard hitbox: 32 units wide at scale 1.0, scaled with ; World-space billboard hitbox: weapon-specific width, scaled with
; the authored sprite, never with screen distance. Zero means legacy 1.0. ; the authored sprite, never with screen distance. Zero means legacy 1.0.
mov eax, [ebx].SpriteStruct.scale mov eax, [ebx].SpriteStruct.scale
test eax, eax test eax, eax
jnz @F jnz @F
mov eax, FIXED_ONE mov eax, FIXED_ONE
@@: @@:
imul eax, 16 imul eax, hitHalfWidth
sar eax, 16 sar eax, 16
mov targetHalfWidth, eax mov targetHalfWidth, eax
mov eax, transX mov eax, transX
@ -295,10 +355,15 @@ hitscan_eval:
cmp closestIdx, -1 cmp closestIdx, -1
je combat_done je combat_done
; Apply damage to hit guard
mov ecx, closestIdx mov ecx, closestIdx
mov eax, [entityType + ecx*4]
; Check if hit an explosive barrel!
cmp eax, ENTITY_TYPE_BARREL
je explode_barrel
; Hit an enemy guard!
mov eax, [entityHealth + ecx*4] mov eax, [entityHealth + ecx*4]
sub eax, 35 sub eax, weaponDamage
jns @F jns @F
xor eax, eax xor eax, eax
@@: @@:
@ -310,8 +375,9 @@ hitscan_eval:
mov [entityState + ecx*4], ENTITY_STATE_DEAD mov [entityState + ecx*4], ENTITY_STATE_DEAD
mov DWORD PTR [entityTimer + ecx*4], 0 mov DWORD PTR [entityTimer + ecx*4], 0
add playerScore, 100 add playerScore, 100
invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_GUARD
; Drop 10 ammo reward ; Drop ammo reward
mov eax, playerAmmo mov eax, playerAmmo
add eax, 10 add eax, 10
cmp eax, playerMaxAmmo cmp eax, playerMaxAmmo
@ -321,6 +387,115 @@ hitscan_eval:
mov playerAmmo, eax mov playerAmmo, eax
jmp combat_done jmp combat_done
explode_barrel:
; Exploding Barrel: Destroy barrel and trigger AOE blast!
mov [entityState + ecx*4], ENTITY_STATE_DEAD
mov ebx, closestIdx
imul ebx, sizeof SpriteStruct
add ebx, OFFSET spriteList
mov [ebx].SpriteStruct.textureId, 0 ; disappear
; Fiery orange-red screen flash
mov playerHitFlash, 8
add playerScore, 200
invoke ShowGameToast, OFFSET szBarrelExplodedToast, 60, 00FFA020h
invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_BARREL
; AOE damage to all surrounding guards within 128 units (2 cells)
push esi
push edi
mov esi, [ebx].SpriteStruct.x
mov edi, [ebx].SpriteStruct.y
invoke SpawnExplosionLight, esi, edi
mov i, 0
aoe_loop:
mov eax, i
cmp eax, spriteCount
jge aoe_check_player
mov ecx, i
cmp ecx, closestIdx
je next_aoe
mov eax, [entityType + ecx*4]
cmp eax, ENTITY_TYPE_GUARD
jne next_aoe
cmp [entityState + ecx*4], ENTITY_STATE_DEAD
je next_aoe
; EDX is volatile scratch for CDQ below, not a safe sprite pointer.
mov ebx, i
imul ebx, sizeof SpriteStruct
add ebx, OFFSET spriteList
mov eax, [ebx].SpriteStruct.x
sub eax, esi
cdq
xor eax, edx
sub eax, edx
cmp eax, 128
jg next_aoe
mov eax, [ebx].SpriteStruct.y
sub eax, edi
cdq
xor eax, edx
sub eax, edx
cmp eax, 128
jg next_aoe
; Within blast: deal 60 blast damage!
mov eax, [entityHealth + ecx*4]
sub eax, 60
jns @F
xor eax, eax
@@:
mov [entityHealth + ecx*4], eax
test eax, eax
jnz next_aoe
mov [entityState + ecx*4], ENTITY_STATE_DEAD
add playerScore, 150
next_aoe:
inc i
jmp aoe_loop
aoe_check_player:
; If player was standing too close to the barrel (< 96 units), player takes blast damage!
mov eax, playerX
sub eax, esi
cdq
xor eax, edx
sub eax, edx
cmp eax, 96
jg aoe_done
mov eax, playerY
sub eax, edi
cdq
xor eax, edx
sub eax, edx
cmp eax, 96
jg aoe_done
; Player blast damage
mov eax, playerHealth
sub eax, 25
jns @F
xor eax, eax
@@:
mov playerHealth, eax
test eax, eax
jnz aoe_done
mov isPlayerDead, 1
aoe_done:
pop edi
pop esi
jmp combat_done
combat_dry_fire: combat_dry_fire:
mov playerAttackCooldown, 12 mov playerAttackCooldown, 12
@ -334,24 +509,91 @@ UpdateCombat ENDP
RenderCombatHUD PROC uses ebx ecx edx esi edi RenderCombatHUD PROC uses ebx ecx edx esi edi
LOCAL barW:DWORD, gunY:DWORD LOCAL barW:DWORD, gunY:DWORD
; If in adventure mode, unarmed, or weapons disabled, skip weapon & combat stats
cmp gameMode, GAME_MODE_ADVENTURE
je hud_damage_flash
cmp enableWeapons, 0
je hud_damage_flash
cmp playerWeapon, WEAPON_NONE
je hud_damage_flash
; 1. Center Crosshair (subtle cross at 160, 120) ; 1. Center Crosshair (subtle cross at 160, 120)
cmp showCrosshair, 0
je skip_crosshair
invoke DrawRect, 156, 120, 9, 1, 00FFFFFFh invoke DrawRect, 156, 120, 9, 1, 00FFFFFFh
invoke DrawRect, 160, 116, 1, 9, 00FFFFFFh invoke DrawRect, 160, 116, 1, 9, 00FFFFFFh
invoke DrawRect, 160, 120, 1, 1, 0000FF00h invoke DrawRect, 160, 120, 1, 1, 0000FF00h
skip_crosshair:
; 2. Render Retro Weapon at bottom center ; 2. Render Active Weapon at bottom center
cmp playerWeapon, WEAPON_KNIFE
je render_knife_weapon
cmp playerWeapon, WEAPON_SHOTGUN
je render_shotgun_weapon
jmp render_pistol_weapon
render_knife_weapon:
; Knife Combat Animation: thrusts forward when cooldown active
mov gunY, 190
cmp playerFlashTimer, 0
je draw_knife
sub gunY, 12 ; thrust forward!
draw_knife:
; Silver steel blade
invoke DrawRect, 166, gunY, 6, 26, 00D0D0D0h
invoke DrawRect, 168, gunY, 2, 26, 00FFFFFFh ; shine
invoke DrawRect, 167, gunY - 4, 4, 4, 00B0B0B0h ; blade tip
invoke DrawRect, 168, gunY - 7, 2, 3, 00E0E0E0h
; Brass crossguard
mov eax, gunY
add eax, 26
invoke DrawRect, 160, eax, 18, 5, 00D4AF37h
; Wooden grip
mov eax, gunY
add eax, 31
invoke DrawRect, 164, eax, 10, 22, 00502A10h
jmp hud_stats_draw
render_shotgun_weapon:
; Heavy Pump Shotgun
mov gunY, 188
cmp playerFlashTimer, 0
je draw_shotgun_barrels
add gunY, 4 ; heavy recoil!
; Massive dual blast flash!
invoke DrawRect, 146, 155, 28, 22, 00FFAA00h
invoke DrawRect, 150, 158, 20, 16, 00FFFF00h
invoke DrawRect, 154, 162, 12, 10, 00FFFFFFh
draw_shotgun_barrels:
; Twin heavy steel barrels
invoke DrawRect, 148, gunY, 10, 32, 002B2B2Bh
invoke DrawRect, 150, gunY, 6, 32, 00484848h
invoke DrawRect, 162, gunY, 10, 32, 002B2B2Bh
invoke DrawRect, 164, gunY, 6, 32, 00484848h
; Barrel rib connector
invoke DrawRect, 158, gunY, 4, 32, 00222222h
; Wooden pump forend
mov eax, gunY
add eax, 20
invoke DrawRect, 144, eax, 32, 18, 00603818h
invoke DrawRect, 146, eax, 28, 14, 007A4820h
jmp hud_stats_draw
render_pistol_weapon:
mov gunY, 192 mov gunY, 192
cmp playerFlashTimer, 0 cmp playerFlashTimer, 0
je draw_gun_barrel je draw_pistol_barrel
; Recoil offset add gunY, 3 ; recoil offset
add gunY, 3
; Muzzle Flash Burst (Bright yellow & white star) ; Muzzle Flash Burst (Bright yellow & white star)
invoke DrawRect, 157, 165, 7, 18, 00FFFF00h invoke DrawRect, 157, 165, 7, 18, 00FFFF00h
invoke DrawRect, 152, 170, 17, 8, 00FFAA00h invoke DrawRect, 152, 170, 17, 8, 00FFAA00h
invoke DrawRect, 158, 168, 5, 12, 00FFFFFFh invoke DrawRect, 158, 168, 5, 12, 00FFFFFFh
draw_gun_barrel: draw_pistol_barrel:
; Weapon barrel & slide ; Weapon barrel & slide
invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah
invoke DrawRect, 156, gunY, 9, 28, 00555555h invoke DrawRect, 156, gunY, 9, 28, 00555555h
@ -364,15 +606,16 @@ draw_gun_barrel:
add eax, 26 add eax, 26
invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates
; 3. Health Bar hud_stats_draw:
invoke DrawString8x8, 8, 42, OFFSET strHpLabel, COLOR_GREEN ; 3. Health Bar at top-left: X=8, Y=8
invoke DrawNumber8x8, 34, 42, playerHealth, COLOR_WHITE invoke DrawString8x8, 8, 8, OFFSET strHpLabel, COLOR_GREEN
invoke DrawNumber8x8, 34, 8, playerHealth, COLOR_WHITE
invoke DrawRect, 68, 42, 60, 8, 00441010h invoke DrawRect, 66, 8, 50, 8, 00441010h
invoke DrawRectOutline, 68, 42, 60, 8, COLOR_WHITE invoke DrawRectOutline, 66, 8, 50, 8, COLOR_WHITE
mov eax, playerHealth mov eax, playerHealth
imul eax, 58 imul eax, 48
xor edx, edx xor edx, edx
mov ecx, playerMaxHealth mov ecx, playerMaxHealth
test ecx, ecx test ecx, ecx
@ -383,34 +626,66 @@ draw_gun_barrel:
mov barW, eax mov barW, eax
cmp barW, 0 cmp barW, 0
jle hud_ammo_draw jle hud_ammo_draw
cmp barW, 58 cmp barW, 48
jle @F jle @F
mov barW, 58 mov barW, 48
@@: @@:
mov edx, 0000CC33h ; Green mov edx, 0000CC33h ; Green
cmp playerHealth, 30 cmp playerHealth, 30
jg @F jg @F
mov edx, 00FF2222h ; Red warning mov edx, 00FF2222h ; Red warning
@@: @@:
invoke DrawRect, 69, 43, barW, 6, edx invoke DrawRect, 67, 9, barW, 6, edx
hud_ammo_draw: hud_ammo_draw:
; 4. Ammo Display ; 4. Ammo Display: X=126, Y=8
invoke DrawString8x8, 140, 42, OFFSET strAmmoLabel, COLOR_YELLOW invoke DrawString8x8, 126, 8, OFFSET strAmmoLabel, COLOR_YELLOW
invoke DrawNumber8x8, 182, 42, playerAmmo, COLOR_YELLOW invoke DrawNumber8x8, 168, 8, playerAmmo, COLOR_YELLOW
; 5. Score Display ; 5. Keys: X=204, Y=8 (if collected)
invoke DrawString8x8, 222, 42, OFFSET strScoreLabel, COLOR_CYAN
invoke DrawNumber8x8, 272, 42, playerScore, COLOR_CYAN
; 6. Keys (if collected)
cmp playerKeys, 0 cmp playerKeys, 0
jle hud_damage_flash jle hud_score_draw
invoke DrawString8x8, 222, 10, OFFSET strKeysLabel, COLOR_MAGENTA invoke DrawString8x8, 204, 8, OFFSET strKeysLabel, COLOR_MAGENTA
invoke DrawNumber8x8, 258, 10, playerKeys, COLOR_MAGENTA invoke DrawNumber8x8, 238, 8, playerKeys, COLOR_MAGENTA
hud_score_draw:
; 6. Score Display: only in Debug HUD style
cmp hudStyle, HUD_STYLE_CLEAN
je draw_weapon_badge
invoke DrawString8x8, 8, 20, OFFSET strScoreLabel, COLOR_CYAN
invoke DrawNumber8x8, 58, 20, playerScore, COLOR_CYAN
draw_weapon_badge:
; 7. Active Weapon Badge at bottom-right corner: X=228, Y=222
invoke DrawRect, 228, 222, 88, 14, 00141414h
invoke DrawRectOutline, 228, 222, 88, 14, 00444444h
cmp playerWeapon, WEAPON_KNIFE
jne @F
invoke DrawString8x8, 233, 225, OFFSET strWeaponKnife, COLOR_CYAN
jmp draw_weapon_slots
@@:
cmp playerWeapon, WEAPON_SHOTGUN
jne @F
invoke DrawString8x8, 233, 225, OFFSET strWeaponShotgun, 00FFAA00h
jmp draw_weapon_slots
@@:
invoke DrawString8x8, 233, 225, OFFSET strWeaponPistol, COLOR_YELLOW
draw_weapon_slots:
; Draw small weapon slot indicator boxes: [1] [2] [3]
invoke DrawRectOutline, 287, 225, 6, 8, 00555555h
invoke DrawRectOutline, 295, 225, 6, 8, 00555555h
invoke DrawRectOutline, 303, 225, 6, 8, 00555555h
; Highlight active slot
mov eax, playerWeapon
imul eax, 8
add eax, 287
invoke DrawRectOutline, eax, 225, 6, 8, COLOR_YELLOW
hud_damage_flash: hud_damage_flash:
; 7. Damage Screen Flash ; 8. Damage Screen Flash
cmp playerHitFlash, 0 cmp playerHitFlash, 0
jle hud_death_check jle hud_death_check
; Red border vignette ; Red border vignette
@ -420,7 +695,7 @@ hud_damage_flash:
invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h
hud_death_check: hud_death_check:
; 8. Death Overlay Screen ; 9. Death Overlay Screen
cmp isPlayerDead, 0 cmp isPlayerDead, 0
je combat_hud_done je combat_hud_done
@ -431,3 +706,29 @@ hud_death_check:
combat_hud_done: combat_hud_done:
ret ret
RenderCombatHUD ENDP RenderCombatHUD ENDP
; ------------------------------------------------------------------------------
; RenderPauseMenu: Renders stylish retro pause overlay when isGamePaused != 0
; ------------------------------------------------------------------------------
RenderPauseMenu PROC uses ebx ecx edx
cmp isGamePaused, 0
je pause_menu_done
; Centered semi-transparent panel 220x112 at (50, 64)
invoke DrawRect, 50, 64, 220, 112, 00141416h
invoke DrawRectOutline, 50, 64, 220, 112, COLOR_CYAN
invoke DrawRectOutline, 52, 66, 216, 108, COLOR_WHITE
; Header
invoke DrawString8x8, 112, 72, OFFSET strPauseTitle, COLOR_YELLOW
; Menu Options
invoke DrawString8x8, 64, 88, OFFSET strPauseOpt1, COLOR_GREEN
invoke DrawString8x8, 64, 102, OFFSET strPauseOpt2, COLOR_WHITE
invoke DrawString8x8, 64, 116, OFFSET strPauseOpt3, COLOR_CYAN
invoke DrawString8x8, 64, 130, OFFSET strPauseOpt4, COLOR_YELLOW
invoke DrawString8x8, 64, 146, OFFSET strPauseOpt5, 00FF4444h
pause_menu_done:
ret
RenderPauseMenu ENDP

View file

@ -66,8 +66,36 @@ keyAttack db 0
keyAttackHeld db 0 keyAttackHeld db 0
keyRestart db 0 keyRestart db 0
keyRestartHeld db 0 keyRestartHeld db 0
keyWeapon1 db 0
keyWeapon2 db 0
keyWeapon3 db 0
keyWeapon4 db 0
keyPause db 0
keyPauseHeld db 0
keyHudStyleHeld db 0
keyMenuHeld db 0
keyFlashlight db 0
keyFlashlightHeld db 0
keyLightMode db 0
keyLightModeHeld db 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
gameMode dd GAME_MODE_ACTION ; 0 = Adventure (No weapons), 1 = Action / Combat
hudStyle dd HUD_STYLE_CLEAN ; 0 = Clean / Immersive, 1 = Debug / Classic
enableWeapons dd 1 ; 1 = Weapons active, 0 = Pure exploration
showCrosshair dd 1 ; 1 = Show crosshair in Action mode, 0 = Hidden
; Player Combat & Gameplay Stats ; Player Combat & Gameplay Stats
playerWeapon dd 1 ; Default: WEAPON_PISTOL
playerHealth dd 100 playerHealth dd 100
playerMaxHealth dd 100 playerMaxHealth dd 100
playerAmmo dd 50 playerAmmo dd 50
@ -120,7 +148,17 @@ strHpLabel db "HP:",0
strAmmoLabel db "AMMO:",0 strAmmoLabel db "AMMO:",0
strScoreLabel db "SCORE:",0 strScoreLabel db "SCORE:",0
strKeysLabel db "KEY:",0 strKeysLabel db "KEY:",0
strYouDied db "YOU DIED! PRESS [R] TO RESTART",0 strWeaponLabel db "WPN:",0
strWeaponKnife db "KNIFE",0
strWeaponPistol db "PISTOL",0
strWeaponShotgun db "SHOTGUN",0
strPauseTitle db "== ПАУЗА ==",0
strPauseOpt1 db "[1] ПРОДОЛЖИТЬ",0
strPauseOpt2 db "[2] ОРУЖИЕ: ВКЛ/ВЫКЛ",0
strPauseOpt3 db "[3] ИНТЕРФЕЙС: ЧИСТЫЙ/ИНФО",0
strPauseOpt4 db "[4] ПЕРЕЗАПУСК УРОВНЯ",0
strPauseOpt5 db "[5] ВЫХОД В WINDOWS",0
strYouDied db "ВЫ ПОГИБЛИ! НАЖМИТЕ [R]",0
cutsceneCount dd 0 cutsceneCount dd 0
cutsceneStringCount dd 0 cutsceneStringCount dd 0
cutsceneFlags dd 0 cutsceneFlags dd 0
@ -226,3 +264,70 @@ entityState dd MAX_SPRITES dup(?)
entityTimer dd MAX_SPRITES dup(?) 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)
.data
ALIGN 4
gameToastTimer dd 0
gameToastColor dd 0
gameToastText db 64 dup(0)
ALIGN 4
gameDialogActive dd 0
gameDialogSpeaker db 32 dup(0)
gameDialogText db 128 dup(0)
szDialogCloseHint db "[ENTER / E] - ЗАКРЫТЬ", 0
szKeyPickedToast db "ПОЛУЧЕН КЛЮЧ", 0
szMedkitPickedToast db "АПТЕЧКА +25 ЗДОРОВЬЯ", 0
szAmmoPickedToast db "БОЕПРИПАСЫ +20 ПАТРОНОВ", 0
szSwitchOnToast db "ПЕРЕКЛЮЧАТЕЛЬ: ВКЛ", 0
szSwitchOffToast db "ПЕРЕКЛЮЧАТЕЛЬ: ВЫКЛ", 0
szDoorOpenedToast db "ДВЕРЬ ОТКРЫТА", 0
szDoorClosedToast db "ДВЕРЬ ЗАКРЫТА", 0
szBarrelExplodedToast db "ВЗРЫВ БОЧКИ!", 0
szWeaponsEnabledToast db "РЕЖИМ: ОРУЖИЕ ВКЛЮЧЕНО", 0
szWeaponsDisabledToast db "РЕЖИМ: БЕЗ ОРУЖИЯ (КВЕСТ)", 0
szHudCleanToast 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
szProjGameMode0 db "GameMode=0", 0
szProjGameMode1 db "GameMode=1", 0
szProjShowWeapons0 db "ShowWeapons=0", 0
szFlagNoWeapons db "noweapons", 0
szFlagAdventure db "adventure", 0
szFlagAction db "action", 0
szFlagDebugHud db "debug-hud", 0
szFlagCleanHud db "clean-hud", 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

@ -67,6 +67,49 @@ VK_KEY_TAB equ 9
VK_KEY_R equ 52h VK_KEY_R equ 52h
VK_KEY_CONTROL equ 11h VK_KEY_CONTROL equ 11h
VK_LBUTTON equ 01h VK_LBUTTON equ 01h
VK_KEY_1 equ 31h
VK_KEY_2 equ 32h
VK_KEY_3 equ 33h
VK_KEY_4 equ 34h
VK_KEY_5 equ 35h
VK_KEY_F1 equ 70h
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_MODE_ADVENTURE equ 0 ; Exploration / Quest / Horror / Puzzle (No weapons, clean view)
GAME_MODE_ACTION equ 1 ; Combat Action (Weapons, Crosshair, Ammo, Health bar)
; HUD Styles
HUD_STYLE_CLEAN equ 0 ; Clean / Immersive (No debug watermark, no bottom bar)
HUD_STYLE_DEBUG equ 1 ; Debug Mode (FPS counter, Engine watermark, Minimap)
; Weapon Archetypes
WEAPON_NONE equ -1 ; No weapon (Unarmed / Peaceful / Quest)
WEAPON_KNIFE equ 0 ; Melee blade (no ammo, fast, 30 damage)
WEAPON_PISTOL equ 1 ; Handgun (1 ammo, accurate, 35 damage)
WEAPON_SHOTGUN equ 2 ; Pump Shotgun (2 ammo, 3-ray spread, 70 damage)
WEAPON_COUNT equ 3
; Entity Archetypes & States ; Entity Archetypes & States
ENTITY_TYPE_PROP equ 0 ENTITY_TYPE_PROP equ 0
@ -75,6 +118,7 @@ ENTITY_TYPE_MEDKIT equ 2 ; Health pickup (+25 HP)
ENTITY_TYPE_AMMO equ 3 ; Ammo pickup (+20 Ammo) ENTITY_TYPE_AMMO equ 3 ; Ammo pickup (+20 Ammo)
ENTITY_TYPE_KEY equ 4 ; Door Key pickup ENTITY_TYPE_KEY equ 4 ; Door Key pickup
ENTITY_TYPE_SWITCH equ 5 ; Interactive switch trigger ENTITY_TYPE_SWITCH equ 5 ; Interactive switch trigger
ENTITY_TYPE_BARREL equ 6 ; Explosive barrel (AOE damage)
ENTITY_STATE_IDLE equ 0 ENTITY_STATE_IDLE equ 0
ENTITY_STATE_CHASE equ 1 ENTITY_STATE_CHASE equ 1
@ -113,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
@ -138,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'
@ -154,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
@ -167,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

@ -4,6 +4,9 @@
.code .code
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
Game_OnPlayerInteract PROTO :DWORD, :DWORD
; ------------------------------------------------------------------------------ ; ------------------------------------------------------------------------------
; InitEntities: Initialize entity runtime state from spriteList flags/defaults ; InitEntities: Initialize entity runtime state from spriteList flags/defaults
; ------------------------------------------------------------------------------ ; ------------------------------------------------------------------------------
@ -86,8 +89,13 @@ init_spr_loop:
fallback_archetype: fallback_archetype:
; If flags == 0, infer archetype from textureId: ; If flags == 0, infer archetype from textureId:
; Slot 13 = Enemy Guard, 14 = Medkit, 15 = Ammo ; Slot 11 = Explosive Barrel, 13 = Guard, 14 = Medkit, 15 = Ammo
mov edx, [ebx].SpriteStruct.textureId mov edx, [ebx].SpriteStruct.textureId
cmp edx, 11
jne check_guard_slot
mov [entityType + ecx*4], ENTITY_TYPE_BARREL
jmp next_spr_slot
check_guard_slot:
cmp edx, 13 cmp edx, 13
jne check_medkit_slot jne check_medkit_slot
mov [entityType + ecx*4], ENTITY_TYPE_GUARD mov [entityType + ecx*4], ENTITY_TYPE_GUARD
@ -217,6 +225,7 @@ entity_loop:
mov ecx, i mov ecx, i
mov [entityType + ecx*4], ENTITY_TYPE_PROP mov [entityType + ecx*4], ENTITY_TYPE_PROP
add playerScore, 25 add playerScore, 25
invoke ShowGameToast, OFFSET szMedkitPickedToast, 75, COLOR_GREEN
jmp next_entity jmp next_entity
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
@ -240,6 +249,7 @@ check_ammo_pickup:
mov ecx, i mov ecx, i
mov [entityType + ecx*4], ENTITY_TYPE_PROP mov [entityType + ecx*4], ENTITY_TYPE_PROP
add playerScore, 10 add playerScore, 10
invoke ShowGameToast, OFFSET szAmmoPickedToast, 75, COLOR_YELLOW
jmp next_entity jmp next_entity
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
@ -247,7 +257,7 @@ check_ammo_pickup:
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
check_key_pickup: check_key_pickup:
cmp edx, ENTITY_TYPE_KEY cmp edx, ENTITY_TYPE_KEY
jne check_guard_entity jne check_switch_entity
cmp absDX, 32 cmp absDX, 32
jge next_entity jge next_entity
cmp absDY, 32 cmp absDY, 32
@ -257,6 +267,39 @@ check_key_pickup:
mov ecx, i mov ecx, i
mov [entityType + ecx*4], ENTITY_TYPE_PROP mov [entityType + ecx*4], ENTITY_TYPE_PROP
add playerScore, 50 add playerScore, 50
invoke ShowGameToast, OFFSET szKeyPickedToast, 90, COLOR_CYAN
jmp next_entity
; --------------------------------------------------------------------------
; Switch Trigger (Toggle on [E])
; --------------------------------------------------------------------------
check_switch_entity:
cmp edx, ENTITY_TYPE_SWITCH
jne check_guard_entity
cmp absDX, 48
jge next_entity
cmp absDY, 48
jge next_entity
cmp keyAction, 0
je next_entity
mov keyAction, 0
; Check if handled by user gameplay script hook
invoke Game_OnPlayerInteract, i, edx
test eax, eax
jnz next_entity
; Default switch behavior: toggle state
mov ecx, i
mov eax, [entityState + ecx*4]
xor eax, 1
mov [entityState + ecx*4], eax
test eax, eax
jz switch_off
invoke ShowGameToast, OFFSET szSwitchOnToast, 60, COLOR_GREEN
jmp next_entity
switch_off:
invoke ShowGameToast, OFFSET szSwitchOffToast, 60, COLOR_YELLOW
jmp next_entity jmp next_entity
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------

342
include/engine_gameplay.inc Normal file
View file

@ -0,0 +1,342 @@
; ==============================================================================
; EBLAN ENGINE - Gameplay Events, Scripting Hooks, HUD Toasts & Dialog Modals
; ==============================================================================
.code
DrawPixel PROTO :DWORD, :DWORD, :DWORD
DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
PlaySoundA PROTO :DWORD, :DWORD, :DWORD
; ------------------------------------------------------------------------------
; ShowGameToast: Displays a floating toast notification banner on screen
; ------------------------------------------------------------------------------
ShowGameToast PROC uses esi edi ecx eax, pszText:DWORD, duration:DWORD, textColor:DWORD
mov esi, pszText
test esi, esi
jz toast_skip_copy
mov edi, OFFSET gameToastText
mov ecx, 63
toast_copy_loop:
lodsb
stosb
test al, al
jz toast_copy_done
dec ecx
jnz toast_copy_loop
xor al, al
stosb
toast_copy_done:
toast_skip_copy:
mov eax, duration
mov gameToastTimer, eax
mov eax, textColor
mov gameToastColor, eax
ret
ShowGameToast ENDP
; ------------------------------------------------------------------------------
; RenderGameToast: Draws active toast notification at top center of screen
; ------------------------------------------------------------------------------
RenderGameToast PROC uses ebx ecx edx esi edi
cmp gameToastTimer, 0
jle rt_done
dec gameToastTimer
; 1. Calculate text character length
mov esi, OFFSET gameToastText
xor ecx, ecx
count_len:
mov al, [esi+ecx]
test al, al
jz count_done
inc ecx
cmp ecx, 63
jb count_len
count_done:
test ecx, ecx
jz rt_done
; Width in pixels = length * 8
mov eax, ecx
shl eax, 3
mov edx, eax
; Center X = (SCREEN_W - width) / 2
mov ebx, SCREEN_W
sub ebx, edx
sar ebx, 1
; Box coordinates: X = ebx - 6, Y = 16, W = edx + 12, H = 14
mov eax, ebx
sub eax, 6
mov esi, edx
add esi, 12
invoke DrawRect, eax, 16, esi, 14, 00101010h
invoke DrawRectOutline, eax, 16, esi, 14, gameToastColor
invoke DrawString8x8, ebx, 19, OFFSET gameToastText, gameToastColor
rt_done:
ret
RenderGameToast ENDP
; ------------------------------------------------------------------------------
; ShowGameDialog: Opens an interactive retro dialogue modal with speaker and text
; ------------------------------------------------------------------------------
ShowGameDialog PROC uses esi edi ecx eax, pszSpeaker:DWORD, pszText:DWORD
mov esi, pszSpeaker
mov edi, OFFSET gameDialogSpeaker
mov ecx, 31
test esi, esi
jz no_speaker
copy_spk:
lodsb
stosb
test al, al
jz spk_done
dec ecx
jnz copy_spk
xor al, al
stosb
spk_done:
jmp setup_dlg_text
no_speaker:
mov BYTE PTR [edi], 0
setup_dlg_text:
mov esi, pszText
mov edi, OFFSET gameDialogText
mov ecx, 127
test esi, esi
jz no_text
copy_txt:
lodsb
stosb
test al, al
jz txt_done
dec ecx
jnz copy_txt
xor al, al
stosb
txt_done:
jmp activate_dlg
no_text:
mov BYTE PTR [edi], 0
activate_dlg:
mov gameDialogActive, 1
ret
ShowGameDialog ENDP
; ------------------------------------------------------------------------------
; CloseGameDialog: Closes active dialogue box
; ------------------------------------------------------------------------------
CloseGameDialog PROC
mov gameDialogActive, 0
ret
CloseGameDialog ENDP
; ------------------------------------------------------------------------------
; RenderGameDialog: Draws active dialogue box at bottom of screen
; ------------------------------------------------------------------------------
RenderGameDialog PROC uses ebx ecx edx esi edi
cmp gameDialogActive, 0
je rd_done
; Dialog Box background: X = 16, Y = 170, W = 288, H = 42
invoke DrawRect, 16, 170, 288, 42, 00121212h
invoke DrawRectOutline, 16, 170, 288, 42, COLOR_CYAN
; Speaker name (if not empty)
cmp BYTE PTR [gameDialogSpeaker], 0
je draw_dlg_body
invoke DrawString8x8, 24, 174, OFFSET gameDialogSpeaker, COLOR_YELLOW
draw_dlg_body:
; Text
invoke DrawString8x8, 24, 186, OFFSET gameDialogText, COLOR_WHITE
; Close hint in bottom-right
invoke DrawString8x8, 140, 200, OFFSET szDialogCloseHint, COLOR_GRAY
; Check if player pressed Action key or Attack to dismiss
cmp keyAction, 0
jne do_dismiss
cmp keyAttack, 0
jne do_dismiss
jmp rd_done
do_dismiss:
mov keyAction, 0
mov keyAttack, 0
mov gameDialogActive, 0
rd_done:
ret
RenderGameDialog ENDP
; ------------------------------------------------------------------------------
; PlayGameSFX: Asynchronously plays a sound effect via WinMM PlaySoundA
; ------------------------------------------------------------------------------
PlayGameSFX PROC uses eax, pszRelativePath:DWORD
cmp isCutsceneActive, 0
jne sfx_done
mov eax, pszRelativePath
test eax, eax
jz sfx_done
cmp BYTE PTR [eax], 0
jz sfx_done
invoke PlaySoundA, eax, 0, 9 ; SND_FILENAME (1) | SND_ASYNC (8) = 9
sfx_done:
ret
PlayGameSFX ENDP
; ------------------------------------------------------------------------------
; Game_OnTick: Hook called every 60 Hz frame during active gameplay
; ------------------------------------------------------------------------------
Game_OnTick PROC uses eax ebx ecx edx
; Extensible hook for custom game logic (quests, timers, zones)
ret
Game_OnTick ENDP
; ------------------------------------------------------------------------------
; Game_OnPlayerInteract: Hook called when player presses [E] in range of an entity
; Returns: EAX = 1 if handled by custom script, 0 if default engine behavior
; ------------------------------------------------------------------------------
Game_OnPlayerInteract PROC uses ebx ecx edx esi edi, entityIdx:DWORD, entityTypeVal:DWORD
; Extensible hook for custom interactions
xor eax, eax
ret
Game_OnPlayerInteract ENDP
; ------------------------------------------------------------------------------
; Game_OnEntityKilled: Hook called when enemy guard or barrel is destroyed
; ------------------------------------------------------------------------------
Game_OnEntityKilled PROC uses eax ebx ecx edx, entityIdx:DWORD, entityTypeVal:DWORD
; Extensible hook for boss fights, doors opening on kill, score multipliers
ret
Game_OnEntityKilled ENDP
; ------------------------------------------------------------------------------
; Game_OnCustomHUD: Hook called during HUD rendering
; ------------------------------------------------------------------------------
Game_OnCustomHUD PROC uses eax ebx ecx edx
; Extensible hook for custom UI bars, quest markers, mini-dialogues
ret
Game_OnCustomHUD ENDP
; ------------------------------------------------------------------------------
; RenderGameHUDOverlays: Renders active toast banner, dialogue modal, and custom HUD
; ------------------------------------------------------------------------------
RenderGameHUDOverlays PROC
call RenderGameToast
call RenderGameDialog
call Game_OnCustomHUD
ret
RenderGameHUDOverlays ENDP
; ------------------------------------------------------------------------------
; CheckCmdFlag: Case-insensitive substring search (EAX = 1 if found, 0 if not)
; ------------------------------------------------------------------------------
CheckCmdFlag PROC uses esi edi ecx, pszCmd:DWORD, pszFlag:DWORD
mov esi, pszCmd
test esi, esi
jz flag_not_found
check_next_char:
cmp byte ptr [esi], 0
je flag_not_found
mov edi, pszFlag
mov ecx, esi
compare_loop:
mov al, byte ptr [edi]
test al, al
jz flag_found
mov ah, byte ptr [ecx]
cmp al, 'A'
jl @F
cmp al, 'Z'
jg @F
add al, 32
@@:
cmp ah, 'A'
jl @F
cmp ah, 'Z'
jg @F
add ah, 32
@@:
cmp al, ah
jne mismatch
inc edi
inc ecx
jmp compare_loop
mismatch:
inc esi
jmp check_next_char
flag_found:
mov eax, 1
ret
flag_not_found:
xor eax, eax
ret
CheckCmdFlag ENDP
; ------------------------------------------------------------------------------
; LoadProjectSettings: Reads project.eblanproj if present in working directory
; ------------------------------------------------------------------------------
LoadProjectSettings PROC uses ebx ecx edx esi edi
LOCAL hFile:DWORD, bytesRead:DWORD
LOCAL projBuf[1024]:BYTE
invoke CreateFileA, OFFSET szProjectFile, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0
cmp eax, INVALID_HANDLE_VALUE
je proj_done
mov hFile, eax
lea edx, projBuf
invoke ReadFile, hFile, edx, 1023, ADDR bytesRead, 0
invoke CloseHandle, hFile
cmp bytesRead, 0
jle proj_done
mov eax, bytesRead
mov byte ptr [projBuf + eax], 0
lea edx, projBuf
invoke CheckCmdFlag, edx, OFFSET szProjGameMode0
test eax, eax
jnz set_adv_mode
lea edx, projBuf
invoke CheckCmdFlag, edx, OFFSET szProjShowWeapons0
test eax, eax
jnz set_adv_mode
lea edx, projBuf
invoke CheckCmdFlag, edx, OFFSET szProjGameMode1
test eax, eax
jnz set_act_mode
jmp proj_done
set_adv_mode:
mov gameMode, GAME_MODE_ADVENTURE
mov enableWeapons, 0
mov playerWeapon, WEAPON_NONE
mov showCrosshair, 0
jmp proj_done
set_act_mode:
mov gameMode, GAME_MODE_ACTION
mov enableWeapons, 1
mov playerWeapon, WEAPON_PISTOL
mov showCrosshair, 1
proj_done:
ret
LoadProjectSettings ENDP

View file

@ -181,6 +181,204 @@ restart_released:
mov keyRestartHeld, 0 mov keyRestartHeld, 0
restart_sampled: restart_sampled:
; F1 / H: Toggle Clean HUD vs Debug HUD
invoke GetAsyncKeyState, VK_KEY_F1
mov bx, ax
invoke GetAsyncKeyState, VK_KEY_H
or bx, ax
test bx, 8000h
jz hud_key_released
cmp keyHudStyleHeld, 0
jne hud_key_sampled
xor hudStyle, 1
mov keyHudStyleHeld, 1
cmp hudStyle, HUD_STYLE_CLEAN
je toast_hud_clean
invoke ShowGameToast, OFFSET szHudDebugToast, 60, COLOR_CYAN
jmp hud_key_sampled
toast_hud_clean:
invoke ShowGameToast, OFFSET szHudCleanToast, 60, COLOR_GREEN
jmp hud_key_sampled
hud_key_released:
mov keyHudStyleHeld, 0
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)
cmp gameMode, GAME_MODE_ACTION
jne skip_weapon_select
cmp enableWeapons, 0
je skip_weapon_select
invoke GetAsyncKeyState, VK_KEY_1
test ax, 8000h
jz @F
mov playerWeapon, WEAPON_KNIFE
@@:
invoke GetAsyncKeyState, VK_KEY_2
test ax, 8000h
jz @F
mov playerWeapon, WEAPON_PISTOL
@@:
invoke GetAsyncKeyState, VK_KEY_3
test ax, 8000h
jz @F
mov playerWeapon, WEAPON_SHOTGUN
@@:
skip_weapon_select:
; Escape / Pause Key
invoke GetAsyncKeyState, VK_KEY_ESCAPE
test ax, 8000h
jz pause_key_released
cmp keyPauseHeld, 0
jne pause_key_sampled
xor isGamePaused, 1
mov keyPauseHeld, 1
jmp pause_key_sampled
pause_key_released:
mov keyPauseHeld, 0
pause_key_sampled:
; If paused, handle pause menu choices
cmp isGamePaused, 0
je not_in_pause_menu
; While paused, mask normal movement & attack
mov keyForward, 0
mov keyBackward, 0
mov keyStrafeLeft, 0
mov keyStrafeRight, 0
mov keyTurnLeft, 0
mov keyTurnRight, 0
mov keyAttack, 0
mov keyAction, 0
; Check if any menu key is pressed
invoke GetAsyncKeyState, VK_KEY_1
mov bx, ax
invoke GetAsyncKeyState, VK_KEY_2
or bx, ax
invoke GetAsyncKeyState, VK_KEY_3
or bx, ax
invoke GetAsyncKeyState, VK_KEY_4
or bx, ax
invoke GetAsyncKeyState, VK_KEY_5
or bx, ax
test bx, 8000h
jz menu_keys_released
; If a key is pressed, check if already held
cmp keyMenuHeld, 0
jne pause_menu_done
mov keyMenuHeld, 1
; Option 1: Resume
invoke GetAsyncKeyState, VK_KEY_1
test ax, 8000h
jz @F
mov isGamePaused, 0
jmp pause_menu_done
@@:
; Option 2: Toggle Weapons / Peaceful Mode
invoke GetAsyncKeyState, VK_KEY_2
test ax, 8000h
jz @F
xor enableWeapons, 1
cmp enableWeapons, 0
je set_peaceful_mode
mov gameMode, GAME_MODE_ACTION
mov playerWeapon, WEAPON_PISTOL
mov showCrosshair, 1
invoke ShowGameToast, OFFSET szWeaponsEnabledToast, 80, COLOR_YELLOW
jmp pause_menu_done
set_peaceful_mode:
mov gameMode, GAME_MODE_ADVENTURE
mov playerWeapon, WEAPON_NONE
mov showCrosshair, 0
invoke ShowGameToast, OFFSET szWeaponsDisabledToast, 80, COLOR_GREEN
jmp pause_menu_done
@@:
; Option 3: Toggle HUD (Clean vs Debug)
invoke GetAsyncKeyState, VK_KEY_3
test ax, 8000h
jz @F
xor hudStyle, 1
cmp hudStyle, HUD_STYLE_CLEAN
je toast_pause_clean
invoke ShowGameToast, OFFSET szHudDebugToast, 60, COLOR_CYAN
jmp pause_menu_done
toast_pause_clean:
invoke ShowGameToast, OFFSET szHudCleanToast, 60, COLOR_GREEN
jmp pause_menu_done
@@:
; Option 4: Restart level
invoke GetAsyncKeyState, VK_KEY_4
test ax, 8000h
jz @F
mov isGamePaused, 0
mov keyRestart, 1
jmp pause_menu_done
@@:
; Option 5: Quit to Windows
invoke GetAsyncKeyState, VK_KEY_5
test ax, 8000h
jz pause_menu_done
mov isRunning, 0
jmp pause_menu_done
menu_keys_released:
mov keyMenuHeld, 0
pause_menu_done:
ret
not_in_pause_menu:
cmp isCutsceneActive, 0 cmp isCutsceneActive, 0
je normal_keys_enabled je normal_keys_enabled
mov keyForward, 0 mov keyForward, 0
@ -193,12 +391,6 @@ restart_sampled:
mov keyAction, 0 mov keyAction, 0
mov keyAttack, 0 mov keyAttack, 0
normal_keys_enabled: normal_keys_enabled:
; 11. Escape - Exit application
invoke GetAsyncKeyState, VK_KEY_ESCAPE
test ax, 8000h
jz @F
mov isRunning, 0
@@:
ret ret
PollInput ENDP PollInput ENDP

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

@ -13,6 +13,11 @@ ValidateMapFilename PROTO :DWORD
ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
CheckPortalTriggers PROTO CheckPortalTriggers PROTO
InitEntities PROTO InitEntities PROTO
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
@ -100,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
@ -203,6 +209,7 @@ InteractDoor PROC uses ebx ecx edx esi edi
; Open the door! ; Open the door!
invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN
invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN
ret ret
check_door_close: check_door_close:
@ -211,6 +218,7 @@ check_door_close:
; Close the door! ; Close the door!
invoke SetMapCell, targetX, targetY, CELL_DOOR invoke SetMapCell, targetX, targetY, CELL_DOOR
invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW
ret ret
check_portal_interact: check_portal_interact:
@ -526,7 +534,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 +655,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 +678,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 +725,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 +966,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 +1035,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

@ -12,6 +12,8 @@ IsGameplayBlocked PROC
or eax, isCutsceneActive or eax, isCutsceneActive
or eax, cutscenePending or eax, cutscenePending
or eax, fadeState or eax, fadeState
or eax, isGamePaused
or eax, gameDialogActive
ret ret
IsGameplayBlocked ENDP IsGameplayBlocked ENDP
@ -25,6 +27,10 @@ UpdatePlayer PROC uses ebx ecx edx esi edi
LOCAL checkGridX:DWORD, checkGridY:DWORD LOCAL checkGridX:DWORD, checkGridY:DWORD
mov gameplayTickBlocked, 1 mov gameplayTickBlocked, 1
cmp isGamePaused, 0
jne update_player_done
cmp gameDialogActive, 0
jne update_player_done
cmp isPlayerDead, 0 cmp isPlayerDead, 0
jne update_player_done jne update_player_done
; Freeze player movement and inputs during level transitions ; Freeze player movement and inputs during level transitions
@ -237,8 +243,9 @@ allow_portal_y:
mov playerY, eax mov playerY, eax
update_player_done: update_player_done:
cmp fadeState, 0 call IsGameplayBlocked
jne @F test eax, eax
jnz @F
call CheckPortalTriggers call CheckPortalTriggers
@@: @@:
ret ret

View file

@ -1,7 +1,6 @@
; Windowless renderer used by EblanStudio. Same Render3DScene and overlays as game. ; Windowless renderer used by EblanStudio. Same Render3DScene and overlays as game.
; Fixed wire layout is mirrored by editor/src/preview_protocol.h. ; Fixed wire layout is mirrored by editor/src/preview_protocol.h.
.data .data
previewPrefix db "--preview:",0
previewHandle dd 0 previewHandle dd 0
previewMemory dd 0 previewMemory dd 0
previewParent dd 0 previewParent dd 0
@ -17,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
@ -107,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,16 +36,34 @@ 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
je @F
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
@@:
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
je @F
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
@@:
mov ecx, (SCREEN_W * SCREEN_HALF_H) mov ecx, (SCREEN_W * SCREEN_HALF_H)
rep stosd rep stosd
@ -465,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
; -------------------------------------------------------------------------- ; --------------------------------------------------------------------------
@ -493,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
@ -513,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
@ -591,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
@ -836,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
@ -916,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
@ -927,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

111
main.asm
View file

@ -112,9 +112,11 @@ 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
include include/engine_gameplay.inc
include include/engine_cutscene.inc include include/engine_cutscene.inc
include include/engine_preview.inc include include/engine_preview.inc
@ -133,14 +135,7 @@ WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
ret ret
handle_keydown: handle_keydown:
cmp wParam, VK_KEY_ESCAPE ; Key edges and ESC pause menu are handled via PollInput
jne check_f12
invoke PostQuitMessage, 0
mov isRunning, 0
xor eax, eax
ret
check_f12:
; Movement and action edges are handled only by PollInput.
invoke DefWindowProcA, hWnd, uMsg, wParam, lParam invoke DefWindowProcA, hWnd, uMsg, wParam, lParam
ret ret
@ -164,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
@ -226,19 +222,30 @@ Engine_RenderHUD PROC uses ebx ecx edx
call RenderCutsceneOverlay call RenderCutsceneOverlay
ret ret
normal_engine_hud: normal_engine_hud:
cmp hudStyle, HUD_STYLE_DEBUG
jne clean_hud_mode
; --- DEBUG / DEVELOPER HUD MODE ---
; 1. Top HUD Header ; 1. Top HUD Header
invoke DrawString8x8, 10, 10, OFFSET strEngineName, COLOR_YELLOW invoke DrawString8x8, 8, 8, OFFSET strEngineName, COLOR_YELLOW
; 2. FPS Counter ; 2. FPS Counter (drawn at top right X=250 so it never overlaps left elements)
invoke DrawString8x8, 10, 24, OFFSET szFpsLabel, COLOR_CYAN invoke DrawString8x8, 250, 8, OFFSET szFpsLabel, COLOR_CYAN
invoke DrawNumber8x8, 42, 24, currentFps, COLOR_CYAN invoke DrawNumber8x8, 282, 8, currentFps, COLOR_CYAN
; 3. Controls Bar at Bottom ; 3. Controls Bar at Bottom (Debug mode only)
invoke DrawRect, 0, 215, SCREEN_W, 25, 00101010h invoke DrawRect, 0, 215, SCREEN_W, 25, 00101010h
invoke DrawString8x8, 10, 218, OFFSET strControls1, COLOR_WHITE invoke DrawString8x8, 10, 218, OFFSET strControls1, COLOR_WHITE
invoke DrawString8x8, 10, 228, OFFSET strControls3, COLOR_GREEN invoke DrawString8x8, 10, 228, OFFSET strControls3, COLOR_GREEN
jmp check_hints
; 4. Door / Portal Interaction Hint (center screen) clean_hud_mode:
; --- CLEAN / IMMERSIVE HUD MODE ---
; In clean mode, no watermarks and no permanent bottom bar.
; Full 320x240 view is dedicated to the game world.
check_hints:
; 4. Door / Portal Interaction Hint (Cleanly centered above crosshair)
call CheckDoorHint call CheckDoorHint
cmp eax, 1 cmp eax, 1
je show_door_hint je show_door_hint
@ -247,11 +254,11 @@ normal_engine_hud:
jmp check_minimap jmp check_minimap
show_door_hint: show_door_hint:
invoke DrawString8x8, 128, 150, OFFSET strDoorHint, COLOR_YELLOW invoke DrawString8x8, 116, 140, OFFSET strDoorHint, COLOR_YELLOW
jmp check_minimap jmp check_minimap
show_portal_hint: show_portal_hint:
invoke DrawString8x8, 110, 150, OFFSET strPortalHint, COLOR_CYAN invoke DrawString8x8, 110, 140, OFFSET strPortalHint, COLOR_CYAN
check_minimap: check_minimap:
cmp mapErrorFrames, 0 cmp mapErrorFrames, 0
@ -261,6 +268,9 @@ hud_no_map_error:
; 5. Minimap Radar (Toggle with 'M') ; 5. Minimap Radar (Toggle with 'M')
call DrawMinimap call DrawMinimap
; 6. Gameplay HUD Overlays (Floating Toasts, Dialog Modals, Custom HUD)
call RenderGameHUDOverlays
ret ret
Engine_RenderHUD ENDP Engine_RenderHUD ENDP
@ -274,9 +284,17 @@ Engine_Frame PROC uses ebx esi edi
dec mapErrorFrames dec mapErrorFrames
no_error_tick: no_error_tick:
call PollInput call PollInput
cmp isGamePaused, 0
jne @F
cmp gameDialogActive, 0
jne skip_movement_in_dialog
call UpdatePlayer call UpdatePlayer
call UpdateEntities call UpdateEntities
call UpdateCombat call UpdateCombat
call UpdateDynamicLights
skip_movement_in_dialog:
call Game_OnTick
@@:
; 2. 3D Raycasting & 2D HUD Render ; 2. 3D Raycasting & 2D HUD Render
call Render3DScene call Render3DScene
@ -286,6 +304,7 @@ no_error_tick:
call RenderCombatHUD call RenderCombatHUD
@@: @@:
call ApplyCutsceneFade call ApplyCutsceneFade
call RenderPauseMenu
; 2b. Screenshot request (F12): dump fresh framebuffer to BMP ; 2b. Screenshot request (F12): dump fresh framebuffer to BMP
cmp keyScreenshot, 0 cmp keyScreenshot, 0
@ -364,14 +383,14 @@ ParseStartupMap PROC uses ebx ecx edx esi edi
skip_executable_quote: skip_executable_quote:
lodsb lodsb
test al, al test al, al
jz startup_arg_done jz check_cmdline_flags
cmp al, 34 cmp al, 34
jne skip_executable_quote jne skip_executable_quote
jmp skip_argument_spaces jmp skip_argument_spaces
skip_executable_word: skip_executable_word:
lodsb lodsb
test al, al test al, al
jz startup_arg_done jz check_cmdline_flags
cmp al, 32 cmp al, 32
jne skip_executable_word jne skip_executable_word
skip_argument_spaces: skip_argument_spaces:
@ -408,10 +427,59 @@ write_startup_char:
dec ecx dec ecx
jnz startup_argument_loop jnz startup_argument_loop
mov startupMapPath, 0 mov startupMapPath, 0
ret jmp check_cmdline_flags
terminate_startup_arg: terminate_startup_arg:
mov byte ptr [edi], 0 mov byte ptr [edi], 0
startup_arg_done:
; If startupMapPath starts with '-' and not '--preview:', clear it
mov al, byte ptr [startupMapPath]
cmp al, '-'
jne check_cmdline_flags
mov esi, OFFSET startupMapPath
mov edi, OFFSET previewPrefix
mov ecx, 10
repe cmpsb
je check_cmdline_flags
mov byte ptr [startupMapPath], 0
check_cmdline_flags:
invoke GetCommandLineA
mov ebx, eax
invoke CheckCmdFlag, ebx, OFFSET szFlagNoWeapons
test eax, eax
jz @F
mov gameMode, GAME_MODE_ADVENTURE
mov enableWeapons, 0
mov playerWeapon, WEAPON_NONE
mov showCrosshair, 0
@@:
invoke CheckCmdFlag, ebx, OFFSET szFlagAdventure
test eax, eax
jz @F
mov gameMode, GAME_MODE_ADVENTURE
mov enableWeapons, 0
mov playerWeapon, WEAPON_NONE
mov showCrosshair, 0
@@:
invoke CheckCmdFlag, ebx, OFFSET szFlagAction
test eax, eax
jz @F
mov gameMode, GAME_MODE_ACTION
mov enableWeapons, 1
mov playerWeapon, WEAPON_PISTOL
mov showCrosshair, 1
@@:
invoke CheckCmdFlag, ebx, OFFSET szFlagDebugHud
test eax, eax
jz @F
mov hudStyle, HUD_STYLE_DEBUG
@@:
invoke CheckCmdFlag, ebx, OFFSET szFlagCleanHud
test eax, eax
jz @F
mov hudStyle, HUD_STYLE_CLEAN
@@:
ret ret
ParseStartupMap ENDP ParseStartupMap ENDP
@ -422,6 +490,7 @@ main PROC
LOCAL msg:MSG LOCAL msg:MSG
LOCAL curTick:DWORD, elapsed:DWORD LOCAL curTick:DWORD, elapsed:DWORD
call LoadProjectSettings
call ParseStartupMap call ParseStartupMap
call TryEnginePreview call TryEnginePreview
call Engine_Init call Engine_Init

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"
#include "stability_editor_checks.inl" #include "stability_editor_checks.inl"
@ -139,6 +140,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,9 +17,11 @@ 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
include include/engine_gameplay.inc
include include/engine_cutscene.inc include include/engine_cutscene.inc
include startup_parser.inc include startup_parser.inc
include project_paths.inc include project_paths.inc
@ -126,7 +128,9 @@ state_changed:
StateUnchanged ENDP StateUnchanged ENDP
include tests/cutscene_engine_checks.inc include tests/cutscene_engine_checks.inc
include tests/lighting_engine_checks.inc
include tests/stability_engine_checks.inc include tests/stability_engine_checks.inc
include tests/integration_engine_checks.inc
AuditMain PROC AuditMain PROC
call InitMathTables call InitMathTables
@ -363,6 +367,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
call RunStabilityChecks call RunStabilityChecks
mov eax, resultCount mov eax, resultCount

View file

@ -0,0 +1,85 @@
; Compatibility of stage-1 fixes with newer weapon/modal/lighting systems.
.code
RunIntegrationChecks PROC
call ResetCombatFixture
mov isGamePaused, 1
mov keyForward, 1
mov playerAttackCooldown, 4
call UpdatePlayer
call UpdateEntities
call UpdateCombat
CHECK playerX, 160 ; pause freezes player movement through production procedure
CHECK [spriteList].SpriteStruct.x, 288 ; pause freezes guard AI through production procedure
CHECK playerAttackCooldown, 4 ; pause freezes weapon cooldowns
CHECK playerAmmo, 50 ; pause preserves ammunition
mov isGamePaused, 0
mov keyForward, 0
call UpdatePlayer
mov gameDialogActive, 1
call UpdateEntities
call UpdateCombat
CHECK [spriteList].SpriteStruct.x, 288 ; modal dialogue freezes guards
CHECK playerAmmo, 50 ; modal dialogue cannot fire
mov gameDialogActive, 0
mov gameMode, GAME_MODE_ADVENTURE
call UpdateCombat
CHECK entityHealth, 100 ; peaceful adventure mode cannot damage targets
call ResetCombatFixture
mov isPlayerDead, 1
mov transitionCount, 1
mov [transitionList].LevelTransition.triggerCellX, 2
mov [transitionList].LevelTransition.triggerCellY, 2
mov [transitionList].LevelTransition.fadeEffect, 1
mov portalLatchCell, -1
call UpdatePlayer
CHECK fadeState, 0 ; dead player cannot activate a room transition
call ResetCombatFixture
mov playerWeapon, WEAPON_KNIFE
call UpdateCombat
CHECK entityHealth, 100 ; knife cannot hit beyond its world range
CHECK playerAmmo, 50 ; knife consumes no ammunition
mov playerAttackCooldown, 0
mov [spriteList].SpriteStruct.x, 208
call UpdateCombat
CHECK entityHealth, 70 ; knife retains its close-range damage after merge
call ResetCombatFixture
mov playerWeapon, WEAPON_SHOTGUN
mov [spriteList].SpriteStruct.y, 184
call UpdateCombat
CHECK entityHealth, 30 ; shotgun retains wider world-space hitbox and damage
CHECK playerAmmo, 48 ; shotgun consumes two rounds
mov eax, [lightSources+14*sizeof LightSource].LightSource.flags
and eax, LIGHT_FLAG_ACTIVE
CHECK eax, 1 ; shotgun spawns muzzle light in reserved slot
call ResetCombatFixture
mov [spriteList].SpriteStruct.textureId, 11
mov [spriteList].SpriteStruct.flags, 06026400h
call InitEntities
call UpdateCombat
CHECK [spriteList].SpriteStruct.textureId, 11 ; explicit Prop with barrel texture cannot explode
mov [spriteList].SpriteStruct.flags, 0
call InitEntities
CHECK entityType, ENTITY_TYPE_BARREL ; legacy barrel archetype matches editor fallback
; Real explosion with a nearby guard: CDQ must not destroy sprite pointer.
call ResetCombatFixture
mov [spriteList].SpriteStruct.textureId, 11
mov [spriteList].SpriteStruct.flags, ENTITY_TYPE_BARREL
mov spriteCount, 2
mov esi, OFFSET spriteList+sizeof SpriteStruct
mov [esi].SpriteStruct.x, 352
mov [esi].SpriteStruct.y, 160
mov [esi].SpriteStruct.textureId, 13
mov [esi].SpriteStruct.scale, FIXED_ONE
mov [esi].SpriteStruct.flags, 0
call InitEntities
call UpdateCombat
CHECK [spriteList].SpriteStruct.textureId, 0 ; explicit barrel still explodes on hitscan
CHECK entityHealth[4], 40 ; blast damages nearby guard without corrupting pointer
mov eax, [lightSources+15*sizeof LightSource].LightSource.flags
and eax, LIGHT_FLAG_ACTIVE
CHECK eax, 1 ; explosion spawns light in reserved slot
ret
RunIntegrationChecks ENDP

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

@ -4,6 +4,11 @@ static void RunStabilityEditorChecks() {
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_GUARD, "zero flags retain legacy guard fallback"); Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_GUARD, "zero flags retain legacy guard fallback");
sprite.flags = 0x06026400; sprite.flags = 0x06026400;
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop matches ASM despite guard texture"); Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop matches ASM despite guard texture");
sprite.textureId = 11;
sprite.flags = 0;
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_BARREL, "zero flags retain new legacy barrel fallback");
sprite.flags = 0x06026400;
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop overrides barrel artwork");
for (int health : {1, 100, 255, 256, 300, 500}) { for (int health : {1, 100, 255, 256, 300, 500}) {
SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_GUARD, health, 2, 6); SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_GUARD, health, 2, 6);
Check(GetSpriteHealth(sprite) == (health > 255 ? 255 : health), "health setter clamps before byte packing"); Check(GetSpriteHealth(sprite) == (health > 255 ? 255 : health), "health setter clamps before byte packing");

View file

@ -9,6 +9,11 @@ ResetCombatFixture PROC uses eax ecx edi
mov isCutsceneActive, 0 mov isCutsceneActive, 0
mov cutscenePending, 0 mov cutscenePending, 0
mov isPlayerDead, 0 mov isPlayerDead, 0
mov isGamePaused, 0
mov gameDialogActive, 0
mov gameMode, GAME_MODE_ACTION
mov enableWeapons, 1
mov playerWeapon, WEAPON_PISTOL
mov playerHealth, 100 mov playerHealth, 100
mov playerAmmo, 50 mov playerAmmo, 50
mov playerScore, 0 mov playerScore, 0
@ -198,5 +203,6 @@ RunStabilityChecks PROC
mov [spriteList].SpriteStruct.flags, 0 mov [spriteList].SpriteStruct.flags, 0
call InitEntities call InitEntities
CHECK entityType, ENTITY_TYPE_GUARD ; zero flags keep legacy texture fallback CHECK entityType, ENTITY_TYPE_GUARD ; zero flags keep legacy texture fallback
call RunIntegrationChecks
ret ret
RunStabilityChecks ENDP RunStabilityChecks 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

@ -150,7 +150,7 @@ def main():
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() + "".join( source = (ROOT / "tests/engine_regressions.asm").read_text() + "".join(
(ROOT / "tests" / name).read_text() for name in (ROOT / "tests" / name).read_text() for name in
("cutscene_engine_checks.inc", "stability_engine_checks.inc")) ("cutscene_engine_checks.inc", "lighting_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc"))
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)
@ -176,10 +176,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