Compare commits
5 commits
c1da810bd5
...
1efa1195cf
| Author | SHA1 | Date | |
|---|---|---|---|
| 1efa1195cf | |||
| c14ae884aa | |||
| 341966debe | |||
| 25bd6377a7 | |||
| 5c568b8cba |
39 changed files with 4020 additions and 366 deletions
95
AGENTS.md
95
AGENTS.md
|
|
@ -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:
|
||||
- **Header (`MapHeader`, 38 bytes, packed)**:
|
||||
- `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`
|
||||
- `mapHeight` (uint16): `32`
|
||||
- `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
|
||||
- `floorColor` (uint32): 00RRGGBBh
|
||||
- `transitionCount` (uint16): 0..16 (v1 ignores this formerly reserved field)
|
||||
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4 only)
|
||||
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4 only)
|
||||
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4 only)
|
||||
- `reserved[4]` (bytes): reserved padding
|
||||
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4+)
|
||||
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4+)
|
||||
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4+)
|
||||
- `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).
|
||||
- **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.
|
||||
- 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.
|
||||
- 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).
|
||||
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`.
|
||||
|
||||
### 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.
|
||||
|
||||
|
|
@ -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).
|
||||
- `4: Key`: key card for unlocking locked doors.
|
||||
- `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`).
|
||||
|
||||
2. **Player Combat & HUD (`include/engine_combat.inc`)**:
|
||||
- Weapon attack via `Space`, `Ctrl`, or Left Mouse Button (`VK_LBUTTON`).
|
||||
- Center crosshair and hitscan raycasting against active enemy guards.
|
||||
- Animated retro pistol with muzzle flash, recoil, and cooldowns.
|
||||
2. **Player Combat & Multi-Weapon Arsenal (`include/engine_combat.inc`)**:
|
||||
- Weapon switching via keys `[1]`, `[2]`, `[3]`:
|
||||
- **[1] Боевой нож (Combat Knife)**: fast melee attack (cooldown 8, 30 damage, no ammo required).
|
||||
- **[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]`.
|
||||
- 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")**:
|
||||
- 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).
|
||||
- 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.
|
||||
|
||||
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.
|
||||
- "Экспорт автономной игры (.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.
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -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 работают при фокусе интерфейса и текстовых полей. Ошибка запуска показывается отдельным сообщением. При старте редактор открывает карту проекта относительно своего файла, поэтому запуск из другой рабочей папки не ломает пути.
|
||||
|
||||
Формат 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. Старые карты сохраняют прежний вид без освещения, пока вы не добавите источники и не включите нужный режим. Свет каждой комнаты сохраняется независимо. Перед экспортом игры сохраните карты.
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
|
||||
## 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` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия.
|
||||
|
||||
|
|
@ -142,3 +142,17 @@ WASD, стрелки и правая кнопка мыши позволяют о
|
|||
F5/F6 обрабатываются до признака захвата клавиатуры ImGui — иначе фокус внутри редактора блокировал запуск. Ошибки подготовки и запуска F5 выводятся отдельным диалогом. Начальный путь карты определяется относительно расположения редактора и загружается при старте, независимо от рабочей папки процесса.
|
||||
|
||||
Проверки выполняют полный путь x86 ASM → раздел памяти → x64 редактор → текстура DX11 на устройстве WARP. Пиксели встроенного вида сравниваются с прямым вызовом обычного ASM-рендерера. Отдельный сценарий `python tools/test_preview_ipc.py` проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок.
|
||||
|
||||
## 9. Запекание и динамический RGB-свет (24.10.2026)
|
||||
|
||||
Запечённый свет отделён от динамических источников. `lighting_format.h` выполняет запекание в редакторе; `engine_lighting_core.inc` содержит эквивалентный ASM-вариант и расчёт света при рендеринге. Постоянные источники (STATIC=0) входят в 1024 RGB-ячейки `lightGridRGB`. Точечные (POINT=1), пульсирующие (PULSE=4), мерцание, фонарик и временные вспышки прибавляются только при динамическом режиме. Это исключает повторное прибавление света. Источник цвета 0 излучает чёрный свет; белый задаётся явно как 00FFFFFFh.
|
||||
|
||||
Оба запекателя используют расстояние `max(abs(dx),abs(dy)) + 3*min(abs(dx),abs(dy))/8`, затухание `(radius-distance)*intensity/radius`, RGB-цвет и насыщение каналов. `CastLightRay` проверяет точные пересечения границ клеток; при пересечении угла проверяются обе соседние клетки. Порталы и открытые двери пропускают свет. Флаг SHADOWS отключает проверку препятствий для выбранного источника. Сглаживание выполняется в отдельном буфере с весом 2 для центра и 1 для доступных соседей, без усреднения сквозь стену.
|
||||
|
||||
В v5 после строк кат-сцены добавлены 32 байта настроек, lightCount записей по 40 байт и 4096 байт RGB-карты. Поля lightCount/reserved занимают смещения 34/36 прежнего 38-байтового заголовка. Настройки: режим, фон, включение фонаря, его радиус, интенсивность, полуугол, baked-valid=1, reserved=0 (все DWORD). Загрузка транзакционная, включая освещение: повреждённая комната не заменяет действующую. Старые v1..v4 сбрасывают источники и сохраняют классическое освещение. После перехода загружается свет новой комнаты; старые источники и вспышки очищаются.
|
||||
|
||||
Игровой вид использует протокол EBPV v2 размером 1 362 408 байт: прежние смещения пикселей и текстур сохранены, после кадра добавлены 13 источников и RGB-карта. Источники и фонарик синхронизируются при каждом запросе; время мерцания/пульсации задаётся 60-Гц счётчиком. Перезапекания на каждом кадре нет. В игре обновление использует время симуляции, а запечённая карта читается непосредственно из файла. У стен освещение берётся у видимой грани, у спрайтов — в их позиции; фонарик проверяет и конус, и заслонение. Плоские потолок/пол используют один образец в точке камеры, без проекции света по каждому пикселю пола.
|
||||
|
||||
Слоты 1..13 принадлежат источникам карты; 0 зарезервирован фонарю, 14 вспышке выстрела, 15 взрыву. Вспышки не могут перезаписать авторские источники. Режимы света, источники, RGB-данные и состояние запекания входят в Undo/Redo. Изменение постоянного света/геометрии/фона помечает результат устаревшим; динамическое движение не требует запекания. Сохранение и F5 автоматически обновляют устаревший результат.
|
||||
|
||||
Регрессии проверяют цвет, заслонение стенами/дверями/углами, флаги теней, отсутствие двойного освещения, пульсацию и затухание вспышек, сохранение v5, отказ от повреждённых данных, старые карты, Undo/Redo и реальное обновление игрового вида. Все 1024 RGB-ячейки сравниваются между C++ и ASM. Кадры сцены с авторским светом из встроенного вида сравниваются с прямым рендерингом той же карты в ASM.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,25 +1,40 @@
|
|||
[Window][Workspace]
|
||||
Pos=0,60
|
||||
Size=1536,708
|
||||
LastUsed=20261022
|
||||
Size=2560,1292
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
LastUsed=20261022
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Toolbar]
|
||||
Pos=0,20
|
||||
Size=1536,38
|
||||
LastUsed=20261022
|
||||
Size=2560,38
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][StatusBar]
|
||||
Pos=0,768
|
||||
Size=1536,32
|
||||
LastUsed=20261022
|
||||
Pos=0,1352
|
||||
Size=2560,32
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Не удалось запустить игру]
|
||||
Pos=649,356
|
||||
Size=237,80
|
||||
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=20261023
|
||||
|
||||
|
|
|
|||
11
editor/project.eblanproj
Normal file
11
editor/project.eblanproj
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@
|
|||
#include "level_format.h"
|
||||
#include "cutscene_preview.h"
|
||||
#include "preview_protocol.h"
|
||||
#include "lighting_format.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -33,6 +34,10 @@ struct LevelSnapshot {
|
|||
std::vector<SpriteEntry> sprites;
|
||||
std::vector<LevelTransition> transitions;
|
||||
CutsceneTrack cutscene;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
LightingSettings lightingSettings;
|
||||
std::array<uint32_t,1024> bakedRGB;
|
||||
uint64_t bakedSignature=0;
|
||||
};
|
||||
|
||||
class EditorApp {
|
||||
|
|
@ -67,10 +72,18 @@ public:
|
|||
void StopAudioSound();
|
||||
|
||||
void RenderAssetManagerModal();
|
||||
void RenderAssetManagerContents();
|
||||
void RenderCodeEditorWindow();
|
||||
void RenderCodeEditorContents(const std::string& root);
|
||||
void RenderProjectSettingsModal();
|
||||
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 showCutsceneTimeline = false;
|
||||
bool showCodeEditorModal = false;
|
||||
|
|
@ -96,12 +109,34 @@ public:
|
|||
// Editor state
|
||||
ToolMode currentTool = TOOL_BRUSH;
|
||||
CellType selectedPaletteCell = CELL_BRICK;
|
||||
uint8_t pendingSpawnArchetype = ENTITY_ARCHETYPE_PROP;
|
||||
int selectedSpriteIdx = -1;
|
||||
int selectedTransitionIdx = -1;
|
||||
int selectedLightIdx = -1;
|
||||
int draggingLightIdx = -1;
|
||||
bool isEngineRunning = false;
|
||||
std::string statusMessage = "Готов к работе. EblanStudio v1.2";
|
||||
std::string currentFilePath = "..\\assets\\map.dat";
|
||||
|
||||
// Lighting Subsystem State & Light Sources
|
||||
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
|
||||
int ambientLightLevel = 40;
|
||||
bool isFlashlightOn = true;
|
||||
int flashlightRadius = 384;
|
||||
int flashlightIntensity = 240;
|
||||
int flashlightCone = 45;
|
||||
bool showLightHeatmap = false;
|
||||
uint8_t bakedLightmap[MAP_TOTAL_CELLS] = { 0 };
|
||||
std::array<uint32_t,1024> bakedLightRGB{};
|
||||
uint64_t bakedLightingSignature = 0;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
|
||||
void BakeLighting();
|
||||
void InitDefaultLights();
|
||||
bool CastLightRay(int x1, int y1, int x2, int y2) const;
|
||||
bool LightingNeedsBake() const;
|
||||
LightingSettings GetLightingSettings() const;
|
||||
|
||||
// Viewport pan & zoom
|
||||
float canvasZoom = 20.0f; // pixels per tile
|
||||
float canvasPanX = 20.0f;
|
||||
|
|
@ -112,6 +147,8 @@ public:
|
|||
struct ID3D11ShaderResourceView* textureSRVs[16] = { nullptr };
|
||||
|
||||
private:
|
||||
void RenderLightingInspector();
|
||||
void RenderLightingSettings();
|
||||
bool EnsureEnginePreview();
|
||||
void StopEnginePreview();
|
||||
void UpdateEnginePreview();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <windows.h>
|
||||
#include "../imgui/imgui.h"
|
||||
|
||||
|
|
@ -17,14 +18,19 @@ struct CodeFileEntry {
|
|||
};
|
||||
|
||||
static const CodeFileEntry g_ProjectCodeFiles[] = {
|
||||
{ "engine_entity.inc (Entity AI & Pickups)", "include/engine_entity.inc" },
|
||||
{ "engine_combat.inc (Player Weapons & Combat)", "include/engine_combat.inc" },
|
||||
{ "main.asm (Engine Entry & Main Loop)", "main.asm" },
|
||||
{ "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" },
|
||||
{ "engine_light.inc (Light Sources & Bursts)", "include/engine_light.inc" },
|
||||
{ "engine_lighting_core.inc (Static RGB Bake & Dynamic Lighting)", "include/engine_lighting_core.inc" },
|
||||
{ "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" },
|
||||
{ "engine_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_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_input.inc (Keyboard Input)", "include/engine_input.inc" },
|
||||
{ "engine_physics.inc (Player Physics)", "include/engine_physics.inc" }
|
||||
{ "engine_raycaster.inc (3D DDA Raycaster)", "include/engine_raycaster.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]);
|
||||
|
||||
|
|
@ -34,6 +40,39 @@ struct ScriptTemplate {
|
|||
};
|
||||
|
||||
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)",
|
||||
"; ==============================================================================\n"
|
||||
|
|
@ -78,6 +117,15 @@ static const ScriptTemplate g_ScriptTemplates[] = {
|
|||
" invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR\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)",
|
||||
"; ==============================================================================\n"
|
||||
|
|
@ -90,6 +138,42 @@ static const ScriptTemplate g_ScriptTemplates[] = {
|
|||
};
|
||||
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 {
|
||||
int currentFile = 0;
|
||||
std::string fileContent;
|
||||
|
|
@ -105,6 +189,10 @@ struct CodeEditorState {
|
|||
int buildExitCode = 0;
|
||||
bool hasBuildResult = false;
|
||||
|
||||
// Search bar state
|
||||
char searchBuf[64] = "";
|
||||
int searchMatchCount = 0;
|
||||
|
||||
struct ParsedError {
|
||||
std::string file;
|
||||
int line = 0;
|
||||
|
|
@ -178,8 +266,9 @@ struct CodeEditorState {
|
|||
buildLog.clear();
|
||||
buildErrors.clear();
|
||||
hasBuildResult = false;
|
||||
buildExitCode = 0;
|
||||
|
||||
fs::path root(projectRoot);
|
||||
fs::path root = fs::path(projectRoot);
|
||||
fs::path buildBat = root / "build.bat";
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildBat = root.parent_path() / "build.bat";
|
||||
|
|
@ -187,7 +276,7 @@ struct CodeEditorState {
|
|||
}
|
||||
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n";
|
||||
buildLog = "[ERROR] Не найден скрипт сборки build.bat в: " + buildBat.string() + "\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
return;
|
||||
|
|
@ -299,14 +388,13 @@ struct CodeEditorState {
|
|||
if (!std::isdigit(static_cast<unsigned char>(c))) { isNum = false; break; }
|
||||
}
|
||||
if (isNum) {
|
||||
ParsedError err{};
|
||||
ParsedError err;
|
||||
err.file = line.substr(0, openParen);
|
||||
err.line = std::atoi(lineStr.c_str());
|
||||
err.message = line.substr(colon + 3);
|
||||
buildErrors.push_back(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
|
|
@ -320,25 +408,23 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
|
||||
g_CodeEditor.PollBuild();
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
ImGui::End();
|
||||
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::BeginMenu("Файл")) {
|
||||
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);
|
||||
}
|
||||
if (ImGui::MenuItem("Собрать движок (F7)", "F7")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
if (ImGui::MenuItem("Собрать проект (F7 / Ctrl+B)", "Ctrl+B")) {
|
||||
g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
|
@ -351,7 +437,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
if (ImGui::BeginMenu("Шаблоны кода")) {
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::MenuItem(g_ScriptTemplates[i].title)) {
|
||||
// Append template code to text buffer
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
|
||||
|
|
@ -366,12 +451,15 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
// Toolbar Buttons
|
||||
ImGui::BeginGroup();
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
RenderCodeEditorContents(root);
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorApp::RenderCodeEditorContents(const std::string& root) {
|
||||
g_CodeEditor.PollBuild();
|
||||
|
||||
// Top Toolbar
|
||||
ImGui::BeginGroup();
|
||||
if (ImGui::Button("Сохранить (Ctrl+S)")) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
|
|
@ -381,54 +469,113 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::Button("Компиляция...");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Собрать движок (F7)")) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.55f, 0.22f, 1.0f));
|
||||
if (ImGui::Button("Собрать проект (F7)")) {
|
||||
g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
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::TextDisabled("| %s", g_CodeEditor.statusText.c_str());
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Split Left (File list) and Right (Editor + Output)
|
||||
// Split Left (Navigator & API Catalog) and Right (Editor + Output)
|
||||
ImGui::Columns(2, "CodeEditorColumns", true);
|
||||
static bool initCol = true;
|
||||
if (initCol) {
|
||||
ImGui::SetColumnWidth(0, 240);
|
||||
ImGui::SetColumnWidth(0, 270);
|
||||
initCol = false;
|
||||
// Load initial file
|
||||
g_CodeEditor.LoadFile(0, root);
|
||||
}
|
||||
|
||||
// Left Column: Source File List
|
||||
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)) {
|
||||
if (g_CodeEditor.isModified) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
// Left Column: Tabbed Navigation (Files / API Catalog / Templates)
|
||||
ImGui::BeginTabBar("NavTabs");
|
||||
|
||||
if (ImGui::BeginTabItem("Файлы")) {
|
||||
ImGui::Spacing();
|
||||
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)) {
|
||||
if (g_CodeEditor.isModified) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
g_CodeEditor.LoadFile(i, root);
|
||||
}
|
||||
g_CodeEditor.LoadFile(i, root);
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Быстрые шаблоны:");
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::SmallButton(g_ScriptTemplates[i].title)) {
|
||||
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;
|
||||
if (ImGui::BeginTabItem("Справочник API")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.85f, 1.0f, 1.0f), "Функции и процедуры движка:");
|
||||
ImGui::BeginChild("ApiCatalogChild", ImVec2(0, 0), false);
|
||||
for (int i = 0; i < g_EngineApiCount; ++i) {
|
||||
ImGui::PushID(i);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%s", g_EngineApiCatalog[i].name);
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 65);
|
||||
if (ImGui::SmallButton("Вставить")) {
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
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();
|
||||
|
||||
// Right Column: Code Editor & Compiler Console
|
||||
|
|
@ -436,7 +583,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
float editorHeight = availY * 0.65f;
|
||||
float outputHeight = availY - editorHeight - 20;
|
||||
|
||||
// Multiline Text Editor with monospace font
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Файл: %s %s",
|
||||
g_ProjectCodeFiles[g_CodeEditor.currentFile].relativePath.c_str(),
|
||||
g_CodeEditor.isModified ? "*" : "");
|
||||
|
|
@ -453,10 +599,10 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Hotkey: Ctrl+S to save
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S)) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
// Hotkey: Ctrl+S to save, Ctrl+B to build
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::GetIO().KeyCtrl) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_S)) g_CodeEditor.SaveFile(root);
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_B)) g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
|
@ -466,9 +612,8 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
if (ImGui::BeginTabItem("Вывод компилятора (ml.exe)")) {
|
||||
ImGui::BeginChild("CompilerOutputChild", ImVec2(0, outputHeight), true);
|
||||
if (g_CodeEditor.buildLog.empty() && !g_CodeEditor.isBuilding) {
|
||||
ImGui::TextDisabled("Нажмите 'Собрать движок (F7)' для компиляции ассемблерного кода...");
|
||||
ImGui::TextDisabled("Нажмите 'Собрать проект (F7)' для компиляции ассемблерного кода...");
|
||||
} else {
|
||||
// Display log line by line with color tags
|
||||
size_t logStart = 0;
|
||||
while (logStart < g_CodeEditor.buildLog.size()) {
|
||||
size_t logEnd = g_CodeEditor.buildLog.find('\n', logStart);
|
||||
|
|
@ -508,7 +653,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
snprintf(lineNumBuf, sizeof(lineNumBuf), "%d", err.line);
|
||||
std::string label = err.file + ":" + lineNumBuf + " - " + err.message;
|
||||
if (ImGui::Selectable(label.c_str())) {
|
||||
// Switch to corresponding file if matched
|
||||
for (int f = 0; f < g_CodeFileCount; ++f) {
|
||||
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
|
||||
g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) {
|
||||
|
|
@ -526,5 +670,4 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::EndTabBar();
|
||||
|
||||
ImGui::Columns(1);
|
||||
ImGui::End();
|
||||
}
|
||||
|
|
|
|||
72
editor/src/editor_lighting.inl
Normal file
72
editor/src/editor_lighting.inl
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
void EditorApp::RenderLightingSettings() {
|
||||
if(!ImGui::CollapsingHeader("Освещение карты",ImGuiTreeNodeFlags_DefaultOpen)) return;
|
||||
auto edited=GetLightingSettings(); bool changed=false,activated=false;
|
||||
auto undoItem=[&](){activated|=ImGui::IsItemActivated();};
|
||||
const char* modes[]={"Без освещения","Только запечённый","Запечённый + динамический"};
|
||||
int mode=edited.mode;
|
||||
changed|=ImGui::Combo("Режим света",&mode,modes,3);undoItem();edited.mode=mode;
|
||||
int ambient=edited.ambient;
|
||||
changed|=ImGui::SliderInt("Фоновый свет",&ambient,0,255);undoItem();edited.ambient=ambient;
|
||||
bool flash=edited.flashlight!=0;
|
||||
changed|=ImGui::Checkbox("Фонарик игрока",&flash);undoItem();edited.flashlight=flash;
|
||||
if(flash) {
|
||||
int radius=edited.flashlightRadius,intensity=edited.flashlightIntensity,cone=edited.flashlightCone;
|
||||
changed|=ImGui::SliderInt("Дальность фонаря",&radius,32,2048);undoItem();
|
||||
changed|=ImGui::SliderInt("Яркость фонаря",&intensity,0,255);undoItem();
|
||||
changed|=ImGui::SliderInt("Полуугол фонаря",&cone,1,96);undoItem();
|
||||
edited.flashlightRadius=radius;edited.flashlightIntensity=intensity;edited.flashlightCone=cone;
|
||||
}
|
||||
if(activated)PushUndo();
|
||||
if(changed) {
|
||||
lightingMode=edited.mode;ambientLightLevel=edited.ambient;isFlashlightOn=edited.flashlight!=0;
|
||||
flashlightRadius=edited.flashlightRadius;flashlightIntensity=edited.flashlightIntensity;flashlightCone=edited.flashlightCone;
|
||||
}
|
||||
if(ImGui::Button("Запечь постоянный свет",ImVec2(-1,0))) {PushUndo();BakeLighting();}
|
||||
if(LightingNeedsBake()) ImGui::TextWrapped("Геометрия или постоянный свет изменены: нужно запечь. При сохранении и F5 запекание выполняется автоматически.");
|
||||
else ImGui::TextDisabled("Постоянный свет запечён");
|
||||
ImGui::Checkbox("Карта запечённого света",&showLightHeatmap);
|
||||
ImGui::TextWrapped("Постоянные источники входят в запекание. Точечные, мерцающие и пульсирующие работают в динамическом режиме.");
|
||||
}
|
||||
|
||||
void EditorApp::RenderLightingInspector() {
|
||||
auto& light=lightSources[selectedLightIdx];
|
||||
ImGui::Text("Источник света #%d / %d",selectedLightIdx+1,MAX_EDITOR_LIGHTS);
|
||||
if(ImGui::Button("Удалить источник")) {
|
||||
PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=-1;return;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if(ImGui::Button("Копировать")) {
|
||||
if(lightSources.size()<MAX_EDITOR_LIGHTS) {
|
||||
auto copy=light;copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32);
|
||||
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(lightSources.size())-1;
|
||||
} else statusMessage="Лимит: 13 источников карты; фонарик и вспышки имеют отдельные места.";
|
||||
return;
|
||||
}
|
||||
EditorLightSource edited=light;bool changed=false,activated=false;
|
||||
auto undoItem=[&](){activated|=ImGui::IsItemActivated();};
|
||||
bool active=(edited.flags&LIGHT_FLAG_ACTIVE)!=0;
|
||||
changed|=ImGui::Checkbox("Включён",&active);undoItem();
|
||||
edited.flags=(edited.flags&~LIGHT_FLAG_ACTIVE)|(active?LIGHT_FLAG_ACTIVE:0);
|
||||
float x=edited.x/64.0f,y=edited.y/64.0f;
|
||||
if(ImGui::SliderFloat("Позиция X",&x,0,31.98f,"%.2f")){edited.x=static_cast<int>(x*64);changed=true;}undoItem();
|
||||
if(ImGui::SliderFloat("Позиция Y",&y,0,31.98f,"%.2f")){edited.y=static_cast<int>(y*64);changed=true;}undoItem();
|
||||
changed|=ImGui::SliderInt("Радиус",&edited.radius,32,2048);undoItem();
|
||||
changed|=ImGui::SliderInt("Интенсивность",&edited.intensity,0,255);undoItem();edited.curIntensity=edited.intensity;
|
||||
float color[]={((edited.color>>16)&255)/255.f,((edited.color>>8)&255)/255.f,(edited.color&255)/255.f};
|
||||
if(ImGui::ColorEdit3("Цвет света",color)) {
|
||||
edited.color=(uint32_t(color[0]*255)<<16)|(uint32_t(color[1]*255)<<8)|uint32_t(color[2]*255);changed=true;
|
||||
}undoItem();
|
||||
const char* types[]={"Постоянный (запекание)","Точечный (динамический)","Пульсирующий (динамический)"};
|
||||
int type=edited.lightType==LIGHT_TYPE_STATIC?0:edited.lightType==LIGHT_TYPE_PULSE?2:1;
|
||||
if(ImGui::Combo("Тип света",&type,types,3)){edited.lightType=type==0?LIGHT_TYPE_STATIC:type==1?LIGHT_TYPE_POINT:LIGHT_TYPE_PULSE;changed=true;}undoItem();
|
||||
bool shadows=(edited.flags&LIGHT_FLAG_SHADOWS)!=0,flicker=(edited.flags&LIGHT_FLAG_FLICKER)!=0;
|
||||
changed|=ImGui::Checkbox("Тени от стен и дверей",&shadows);undoItem();
|
||||
edited.flags=(edited.flags&~LIGHT_FLAG_SHADOWS)|(shadows?LIGHT_FLAG_SHADOWS:0);
|
||||
if(edited.lightType!=LIGHT_TYPE_STATIC) {
|
||||
changed|=ImGui::Checkbox("Мерцание факела",&flicker);undoItem();
|
||||
edited.flags=(edited.flags&~LIGHT_FLAG_FLICKER)|(flicker?LIGHT_FLAG_FLICKER:0);
|
||||
}
|
||||
if(activated)PushUndo();
|
||||
if(changed)light=edited;
|
||||
RenderLightingSettings();
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ bool EditorApp::EnsureEnginePreview() {
|
|||
if (!previewPacket) return fail("Не удалось открыть буфер игрового вида.");
|
||||
// Initialize in place: no large packet on the thread's stack.
|
||||
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 + "\"";
|
||||
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,
|
||||
|
|
@ -105,6 +105,14 @@ void EditorApp::UpdateEnginePreview() {
|
|||
if (!sprites.empty()) memcpy(previewPacket->sprites,sprites.data(),previewPacket->spriteCount*sizeof(SpriteEntry));
|
||||
previewPacket->cinematic = scene && cutscenePreview.active;
|
||||
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);
|
||||
if (scene && cutscenePreview.subtitle >= 0 && cutscenePreview.subtitle < static_cast<int>(cutscene.strings.size())) {
|
||||
std::string encoded;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// ==============================================================================
|
||||
|
||||
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_H = 32;
|
||||
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_KEY = 4, // Door Key pickup
|
||||
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)
|
||||
|
|
@ -65,7 +66,7 @@ struct LevelTransition {
|
|||
|
||||
struct MapHeader {
|
||||
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 mapHeight; // 32
|
||||
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 cutsceneStringCount;
|
||||
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 {
|
||||
|
|
@ -104,6 +106,7 @@ struct SpriteEntry {
|
|||
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
|
||||
uint8_t t = static_cast<uint8_t>(s.flags & 0xFF);
|
||||
if (t == 0) {
|
||||
if (s.textureId == 11) return ENTITY_ARCHETYPE_BARREL;
|
||||
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
|
||||
if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT;
|
||||
if (s.textureId == 15) return ENTITY_ARCHETYPE_AMMO;
|
||||
|
|
@ -133,3 +136,53 @@ static_assert(sizeof(MapHeader) == 38, "EBLN header layout changed");
|
|||
static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed");
|
||||
static_assert(sizeof(LevelTransition) == 42, "EBLN transition 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");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "level_format.h"
|
||||
#include "cutscene_format.h"
|
||||
#include "lighting_format.h"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
|
@ -14,6 +15,7 @@ struct BinaryLevel {
|
|||
std::vector<SpriteEntry> sprites;
|
||||
std::vector<LevelTransition> transitions;
|
||||
CutsceneTrack cutscene;
|
||||
LevelLighting lighting;
|
||||
};
|
||||
|
||||
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)
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -85,9 +89,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
|
|||
if (eventCount > MAX_CUTSCENE_EVENTS || stringCount > MAX_CUTSCENE_STRINGS) {
|
||||
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 +
|
||||
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)) {
|
||||
error = "Размер файла не соответствует заголовку карты"; return false;
|
||||
}
|
||||
|
|
@ -108,6 +115,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
|
|||
if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; }
|
||||
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 (h.version == 1) for (auto& sprite : candidate.sprites) { sprite.x >>= 16; sprite.y >>= 16; }
|
||||
if (!ValidateLevel(candidate, error)) return false;
|
||||
|
|
|
|||
97
editor/src/lighting_format.h
Normal file
97
editor/src/lighting_format.h
Normal 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;
|
||||
}
|
||||
|
|
@ -30,46 +30,51 @@ void SetupGodotStyle() {
|
|||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImVec4* colors = style.Colors;
|
||||
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.ScrollbarRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowRounding = 8.0f;
|
||||
style.ChildRounding = 6.0f;
|
||||
style.FrameRounding = 5.0f;
|
||||
style.PopupRounding = 8.0f;
|
||||
style.ScrollbarRounding = 7.0f;
|
||||
style.GrabRounding = 5.0f;
|
||||
style.TabRounding = 6.0f;
|
||||
style.FrameBorderSize = 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)
|
||||
colors[ImGuiCol_Text] = ImVec4(0.92f, 0.92f, 0.95f, 1.00f);
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.52f, 0.55f, 1.00f);
|
||||
colors[ImGuiCol_WindowBg] = ImVec4(0.14f, 0.15f, 0.17f, 1.00f);
|
||||
colors[ImGuiCol_ChildBg] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f);
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_Border] = ImVec4(0.24f, 0.25f, 0.28f, 1.00f);
|
||||
// Modern Game Studio Dark Palette (Unreal / Unity / Godot Pro)
|
||||
colors[ImGuiCol_Text] = ImVec4(0.96f, 0.97f, 0.99f, 1.00f);
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.54f, 0.60f, 1.00f);
|
||||
colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
|
||||
colors[ImGuiCol_ChildBg] = ImVec4(0.14f, 0.15f, 0.18f, 1.00f);
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(0.14f, 0.16f, 0.20f, 0.98f);
|
||||
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_FrameBg] = ImVec4(0.20f, 0.21f, 0.24f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.30f, 0.34f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgActive] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_TitleBg] = ImVec4(0.11f, 0.12f, 0.13f, 1.00f);
|
||||
colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_MenuBarBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.11f, 0.12f, 0.60f);
|
||||
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.27f, 0.30f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.35f, 0.38f, 0.42f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.45f, 0.48f, 0.52f, 1.00f);
|
||||
colors[ImGuiCol_CheckMark] = ImVec4(0.30f, 0.70f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrab] = ImVec4(0.30f, 0.65f, 0.95f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.40f, 0.75f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_Button] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f);
|
||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.33f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_ButtonActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f);
|
||||
colors[ImGuiCol_Header] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f);
|
||||
colors[ImGuiCol_HeaderHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_HeaderActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f);
|
||||
colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f);
|
||||
colors[ImGuiCol_Tab] = ImVec4(0.16f, 0.18f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_TabHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_TabActive] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f);
|
||||
colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.20f, 0.25f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.25f, 0.29f, 0.36f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgActive] = ImVec4(0.30f, 0.36f, 0.45f, 1.00f);
|
||||
colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
|
||||
colors[ImGuiCol_TitleBgActive] = ImVec4(0.13f, 0.15f, 0.19f, 1.00f);
|
||||
colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.80f);
|
||||
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.28f, 0.35f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.34f, 0.39f, 0.48f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.42f, 0.48f, 0.60f, 1.00f);
|
||||
colors[ImGuiCol_CheckMark] = ImVec4(0.05f, 0.65f, 0.95f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrab] = ImVec4(0.05f, 0.60f, 0.92f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.20f, 0.75f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_Button] = ImVec4(0.20f, 0.23f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.34f, 0.43f, 1.00f);
|
||||
colors[ImGuiCol_ButtonActive] = ImVec4(0.35f, 0.43f, 0.55f, 1.00f);
|
||||
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.32f, 1.00f);
|
||||
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.35f, 0.45f, 1.00f);
|
||||
colors[ImGuiCol_HeaderActive] = ImVec4(0.35f, 0.44f, 0.56f, 1.00f);
|
||||
colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.16f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.34f, 0.42f, 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) {
|
||||
|
|
|
|||
|
|
@ -8,21 +8,33 @@
|
|||
// Never modify payload while state == 1. All fields have fixed wire widths.
|
||||
struct EnginePreviewPacket {
|
||||
uint32_t magic = 0x56504245; // EBPV
|
||||
uint32_t version = 1;
|
||||
uint32_t version = 2;
|
||||
volatile long state = 0;
|
||||
uint32_t parentPid = 0;
|
||||
int32_t x = 160, y = 160, angle = 0;
|
||||
uint32_t ceiling = 0, floor = 0, spriteCount = 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]{};
|
||||
SpriteEntry sprites[32]{};
|
||||
char subtitle[96]{};
|
||||
uint32_t textures[16*128*128]{};
|
||||
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(offsetof(EnginePreviewPacket, cells) == 128, "Preview header mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket, textures) == 2016, "Preview texture 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");
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ struct EblanProject {
|
|||
std::string windowTitle = "EBLAN ENGINE 3D - Standalone Retro Game";
|
||||
int defaultWidth = 1280;
|
||||
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 {
|
||||
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";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", defaultHeight);
|
||||
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;
|
||||
WriteFile(hFile, content.data(), static_cast<DWORD>(content.size()), &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
|
|
@ -65,6 +80,12 @@ struct EblanProject {
|
|||
else if (key == "WindowTitle") windowTitle = val;
|
||||
else if (key == "DefaultWidth") defaultWidth = 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Player Combat, Weapon Shooting & Combat HUD
|
||||
; EBLAN ENGINE - Player Combat, Multi-Weapon Arsenal & Combat HUD
|
||||
; ==============================================================================
|
||||
|
||||
.code
|
||||
|
|
@ -9,6 +9,8 @@ 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
|
||||
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||
Game_OnEntityKilled PROTO :DWORD, :DWORD
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdateCombat: Process weapon firing, hitscan raycasting, cooldowns & damage
|
||||
|
|
@ -19,6 +21,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
LOCAL sprX:DWORD, sprY:DWORD
|
||||
LOCAL transX:DWORD, transY:DWORD
|
||||
LOCAL sprScreenX:DWORD, wallDepth:DWORD
|
||||
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitTolerance:DWORD
|
||||
|
||||
; 1. Decrement cooldowns
|
||||
cmp playerAttackCooldown, 0
|
||||
|
|
@ -34,9 +37,17 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
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
|
||||
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
|
||||
cmp keyAttack, 0
|
||||
|
|
@ -44,16 +55,53 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
cmp playerAttackCooldown, 0
|
||||
jne combat_done
|
||||
|
||||
; Check ammo
|
||||
cmp playerAmmo, 0
|
||||
jle combat_dry_fire
|
||||
; Set weapon parameters based on playerWeapon
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitTolerance, 22
|
||||
|
||||
; 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 hitTolerance, 32
|
||||
mov playerAttackCooldown, 8
|
||||
mov playerFlashTimer, 3
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_shotgun:
|
||||
; Shotgun: needs 2 ammo, high damage (70), wide spread (hitTolerance 40), long cooldown
|
||||
cmp playerAmmo, 2
|
||||
jl combat_dry_fire
|
||||
sub playerAmmo, 2
|
||||
mov weaponDamage, 70
|
||||
mov maxHitDist, 999999
|
||||
mov hitTolerance, 40
|
||||
mov playerAttackCooldown, 22
|
||||
mov playerFlashTimer, 7
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_pistol:
|
||||
; Pistol: needs 1 ammo, accurate (hitTolerance 22), medium cooldown (12)
|
||||
cmp playerAmmo, 1
|
||||
jl combat_dry_fire
|
||||
dec playerAmmo
|
||||
mov playerAttackCooldown, 10
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitTolerance, 22
|
||||
mov playerAttackCooldown, 12
|
||||
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
|
||||
mov eax, ANGLE_512
|
||||
sub eax, playerAngle
|
||||
|
|
@ -78,9 +126,24 @@ hitscan_loop:
|
|||
|
||||
mov ecx, i
|
||||
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
|
||||
|
||||
; Also check if prop has barrel texture (11)
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
mov eax, [ebx].SpriteStruct.textureId
|
||||
cmp eax, 11
|
||||
je hitscan_target_valid
|
||||
|
||||
jmp next_hitscan_target
|
||||
|
||||
hitscan_target_valid:
|
||||
mov eax, [entityState + ecx*4]
|
||||
cmp eax, ENTITY_STATE_DEAD
|
||||
je next_hitscan_target
|
||||
|
|
@ -117,6 +180,11 @@ hitscan_loop:
|
|||
cmp transY, 16
|
||||
jle next_hitscan_target
|
||||
|
||||
; Must be closer than maximum weapon hit distance
|
||||
mov eax, transY
|
||||
cmp eax, maxHitDist
|
||||
jg next_hitscan_target
|
||||
|
||||
; Must be closer than the wall at center screen
|
||||
mov eax, transY
|
||||
cmp eax, wallDepth
|
||||
|
|
@ -141,12 +209,12 @@ hitscan_loop:
|
|||
add eax, SCREEN_HALF_W
|
||||
mov sprScreenX, eax
|
||||
|
||||
; Crosshair hitbox: within 22 pixels of center (SCREEN_HALF_W = 160)
|
||||
; Crosshair hitbox check
|
||||
sub eax, SCREEN_HALF_W
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx ; abs(sprScreenX - 160)
|
||||
cmp eax, 22
|
||||
cmp eax, hitTolerance
|
||||
jg next_hitscan_target
|
||||
|
||||
; Check if closer than previous target
|
||||
|
|
@ -165,10 +233,21 @@ hitscan_eval:
|
|||
cmp closestIdx, -1
|
||||
je combat_done
|
||||
|
||||
; Apply damage to hit guard
|
||||
mov ecx, closestIdx
|
||||
mov eax, [entityType + ecx*4]
|
||||
|
||||
; Check if hit an explosive barrel!
|
||||
cmp eax, ENTITY_TYPE_BARREL
|
||||
je explode_barrel
|
||||
mov ebx, closestIdx
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
cmp [ebx].SpriteStruct.textureId, 11
|
||||
je explode_barrel
|
||||
|
||||
; Hit an enemy guard!
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, 35
|
||||
sub eax, weaponDamage
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
|
|
@ -180,8 +259,9 @@ hitscan_eval:
|
|||
mov [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
mov DWORD PTR [entityTimer + ecx*4], 0
|
||||
add playerScore, 100
|
||||
invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_GUARD
|
||||
|
||||
; Drop 10 ammo reward
|
||||
; Drop ammo reward
|
||||
mov eax, playerAmmo
|
||||
add eax, 10
|
||||
cmp eax, playerMaxAmmo
|
||||
|
|
@ -191,6 +271,114 @@ hitscan_eval:
|
|||
mov playerAmmo, eax
|
||||
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
|
||||
|
||||
mov edx, i
|
||||
imul edx, sizeof SpriteStruct
|
||||
add edx, OFFSET spriteList
|
||||
|
||||
mov eax, [edx].SpriteStruct.x
|
||||
sub eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 128
|
||||
jg next_aoe
|
||||
|
||||
mov eax, [edx].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:
|
||||
mov playerAttackCooldown, 12
|
||||
|
||||
|
|
@ -204,24 +392,91 @@ UpdateCombat ENDP
|
|||
RenderCombatHUD PROC uses ebx ecx edx esi edi
|
||||
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)
|
||||
cmp showCrosshair, 0
|
||||
je skip_crosshair
|
||||
invoke DrawRect, 156, 120, 9, 1, 00FFFFFFh
|
||||
invoke DrawRect, 160, 116, 1, 9, 00FFFFFFh
|
||||
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
|
||||
cmp playerFlashTimer, 0
|
||||
je draw_gun_barrel
|
||||
; Recoil offset
|
||||
add gunY, 3
|
||||
je draw_pistol_barrel
|
||||
add gunY, 3 ; recoil offset
|
||||
|
||||
; Muzzle Flash Burst (Bright yellow & white star)
|
||||
invoke DrawRect, 157, 165, 7, 18, 00FFFF00h
|
||||
invoke DrawRect, 152, 170, 17, 8, 00FFAA00h
|
||||
invoke DrawRect, 158, 168, 5, 12, 00FFFFFFh
|
||||
|
||||
draw_gun_barrel:
|
||||
draw_pistol_barrel:
|
||||
; Weapon barrel & slide
|
||||
invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah
|
||||
invoke DrawRect, 156, gunY, 9, 28, 00555555h
|
||||
|
|
@ -234,15 +489,16 @@ draw_gun_barrel:
|
|||
add eax, 26
|
||||
invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates
|
||||
|
||||
; 3. Health Bar
|
||||
invoke DrawString8x8, 8, 42, OFFSET strHpLabel, COLOR_GREEN
|
||||
invoke DrawNumber8x8, 34, 42, playerHealth, COLOR_WHITE
|
||||
hud_stats_draw:
|
||||
; 3. Health Bar at top-left: X=8, Y=8
|
||||
invoke DrawString8x8, 8, 8, OFFSET strHpLabel, COLOR_GREEN
|
||||
invoke DrawNumber8x8, 34, 8, playerHealth, COLOR_WHITE
|
||||
|
||||
invoke DrawRect, 68, 42, 60, 8, 00441010h
|
||||
invoke DrawRectOutline, 68, 42, 60, 8, COLOR_WHITE
|
||||
invoke DrawRect, 66, 8, 50, 8, 00441010h
|
||||
invoke DrawRectOutline, 66, 8, 50, 8, COLOR_WHITE
|
||||
|
||||
mov eax, playerHealth
|
||||
imul eax, 58
|
||||
imul eax, 48
|
||||
xor edx, edx
|
||||
mov ecx, playerMaxHealth
|
||||
test ecx, ecx
|
||||
|
|
@ -253,34 +509,66 @@ draw_gun_barrel:
|
|||
mov barW, eax
|
||||
cmp barW, 0
|
||||
jle hud_ammo_draw
|
||||
cmp barW, 58
|
||||
cmp barW, 48
|
||||
jle @F
|
||||
mov barW, 58
|
||||
mov barW, 48
|
||||
@@:
|
||||
mov edx, 0000CC33h ; Green
|
||||
cmp playerHealth, 30
|
||||
jg @F
|
||||
mov edx, 00FF2222h ; Red warning
|
||||
@@:
|
||||
invoke DrawRect, 69, 43, barW, 6, edx
|
||||
invoke DrawRect, 67, 9, barW, 6, edx
|
||||
|
||||
hud_ammo_draw:
|
||||
; 4. Ammo Display
|
||||
invoke DrawString8x8, 140, 42, OFFSET strAmmoLabel, COLOR_YELLOW
|
||||
invoke DrawNumber8x8, 182, 42, playerAmmo, COLOR_YELLOW
|
||||
; 4. Ammo Display: X=126, Y=8
|
||||
invoke DrawString8x8, 126, 8, OFFSET strAmmoLabel, COLOR_YELLOW
|
||||
invoke DrawNumber8x8, 168, 8, playerAmmo, COLOR_YELLOW
|
||||
|
||||
; 5. Score Display
|
||||
invoke DrawString8x8, 222, 42, OFFSET strScoreLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 272, 42, playerScore, COLOR_CYAN
|
||||
|
||||
; 6. Keys (if collected)
|
||||
; 5. Keys: X=204, Y=8 (if collected)
|
||||
cmp playerKeys, 0
|
||||
jle hud_damage_flash
|
||||
invoke DrawString8x8, 222, 10, OFFSET strKeysLabel, COLOR_MAGENTA
|
||||
invoke DrawNumber8x8, 258, 10, playerKeys, COLOR_MAGENTA
|
||||
jle hud_score_draw
|
||||
invoke DrawString8x8, 204, 8, OFFSET strKeysLabel, 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:
|
||||
; 7. Damage Screen Flash
|
||||
; 8. Damage Screen Flash
|
||||
cmp playerHitFlash, 0
|
||||
jle hud_death_check
|
||||
; Red border vignette
|
||||
|
|
@ -290,7 +578,7 @@ hud_damage_flash:
|
|||
invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h
|
||||
|
||||
hud_death_check:
|
||||
; 8. Death Overlay Screen
|
||||
; 9. Death Overlay Screen
|
||||
cmp isPlayerDead, 0
|
||||
je combat_hud_done
|
||||
|
||||
|
|
@ -301,3 +589,29 @@ hud_death_check:
|
|||
combat_hud_done:
|
||||
ret
|
||||
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
|
||||
|
|
|
|||
|
|
@ -63,8 +63,36 @@ keyAttack db 0
|
|||
keyAttackHeld db 0
|
||||
keyRestart 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
|
||||
playerWeapon dd 1 ; Default: WEAPON_PISTOL
|
||||
playerHealth dd 100
|
||||
playerMaxHealth dd 100
|
||||
playerAmmo dd 50
|
||||
|
|
@ -117,7 +145,17 @@ strHpLabel db "HP:",0
|
|||
strAmmoLabel db "AMMO:",0
|
||||
strScoreLabel db "SCORE:",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
|
||||
cutsceneStringCount dd 0
|
||||
cutsceneFlags dd 0
|
||||
|
|
@ -223,3 +261,70 @@ entityState dd MAX_SPRITES dup(?)
|
|||
entityTimer dd MAX_SPRITES dup(?)
|
||||
entitySpeed 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(?)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,49 @@ VK_KEY_TAB equ 9
|
|||
VK_KEY_R equ 52h
|
||||
VK_KEY_CONTROL equ 11h
|
||||
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_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_KEY equ 4 ; Door Key pickup
|
||||
ENTITY_TYPE_SWITCH equ 5 ; Interactive switch trigger
|
||||
ENTITY_TYPE_BARREL equ 6 ; Explosive barrel (AOE damage)
|
||||
|
||||
ENTITY_STATE_IDLE equ 0
|
||||
ENTITY_STATE_CHASE equ 1
|
||||
|
|
@ -113,7 +157,7 @@ CELL_COUNT equ 11
|
|||
; Sector Transition Portals
|
||||
MAX_TRANSITIONS equ 16
|
||||
MAX_PATH_BYTES equ 260
|
||||
MAP_FORMAT_VERSION equ 4
|
||||
MAP_FORMAT_VERSION equ 5
|
||||
|
||||
LevelTransition STRUCT ; sizeof = 42 bytes (packed)
|
||||
triggerCellX db 0 ; 1 byte
|
||||
|
|
@ -138,6 +182,20 @@ SpriteStruct STRUCT
|
|||
flags dd 0
|
||||
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):
|
||||
; dd magic 'EBTX', dw version (1), dw slotCount, then slotCount * 65536 bytes
|
||||
TEXPACK_MAGIC equ 58544245h ; 'EBTX'
|
||||
|
|
@ -154,7 +212,7 @@ MAP_MAGIC equ 4E4C4245h ; 'EBLN'
|
|||
|
||||
MapHeader STRUCT ; sizeof = 38 bytes (packed)
|
||||
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
|
||||
mapHeight dw 0 ; 32
|
||||
playerStartX dw 0 ; cell X
|
||||
|
|
@ -167,7 +225,8 @@ MapHeader STRUCT ; sizeof = 38 bytes (packed)
|
|||
cutsceneCount dw 0
|
||||
cutsceneStringCount dw 0
|
||||
cutsceneFlags dw 0
|
||||
reserved db 4 dup(0)
|
||||
lightCount dw 0
|
||||
lightingReserved dw 0
|
||||
MapHeader ENDS
|
||||
|
||||
MAX_CUTSCENE_EVENTS equ 128
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
.code
|
||||
|
||||
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||
Game_OnPlayerInteract PROTO :DWORD, :DWORD
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; InitEntities: Initialize entity runtime state from spriteList flags/defaults
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -214,6 +217,7 @@ entity_loop:
|
|||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 25
|
||||
invoke ShowGameToast, OFFSET szMedkitPickedToast, 75, COLOR_GREEN
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -237,6 +241,7 @@ check_ammo_pickup:
|
|||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 10
|
||||
invoke ShowGameToast, OFFSET szAmmoPickedToast, 75, COLOR_YELLOW
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -244,7 +249,7 @@ check_ammo_pickup:
|
|||
; --------------------------------------------------------------------------
|
||||
check_key_pickup:
|
||||
cmp edx, ENTITY_TYPE_KEY
|
||||
jne check_guard_entity
|
||||
jne check_switch_entity
|
||||
cmp absDX, 32
|
||||
jge next_entity
|
||||
cmp absDY, 32
|
||||
|
|
@ -254,6 +259,39 @@ check_key_pickup:
|
|||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
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
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
|
|||
343
include/engine_gameplay.inc
Normal file
343
include/engine_gameplay.inc
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
; ==============================================================================
|
||||
; 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
|
||||
|
||||
|
|
@ -181,6 +181,204 @@ restart_released:
|
|||
mov keyRestartHeld, 0
|
||||
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
|
||||
je normal_keys_enabled
|
||||
mov keyForward, 0
|
||||
|
|
@ -193,12 +391,6 @@ restart_sampled:
|
|||
mov keyAction, 0
|
||||
mov keyAttack, 0
|
||||
normal_keys_enabled:
|
||||
; 11. Escape - Exit application
|
||||
invoke GetAsyncKeyState, VK_KEY_ESCAPE
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
mov isRunning, 0
|
||||
@@:
|
||||
|
||||
ret
|
||||
PollInput ENDP
|
||||
|
|
|
|||
103
include/engine_light.inc
Normal file
103
include/engine_light.inc
Normal 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
|
||||
817
include/engine_lighting_core.inc
Normal file
817
include/engine_lighting_core.inc
Normal 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
|
||||
|
|
@ -13,6 +13,11 @@ ValidateMapFilename PROTO :DWORD
|
|||
ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
CheckPortalTriggers 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
|
||||
|
|
@ -100,6 +105,7 @@ loop_wall2:
|
|||
mov portalLatchCell, -1
|
||||
mov mapCeilingColor, COLOR_CEILING
|
||||
mov mapFloorColor, COLOR_FLOOR
|
||||
call InitLightingSystem
|
||||
ret
|
||||
InitMap ENDP
|
||||
|
||||
|
|
@ -203,6 +209,7 @@ InteractDoor PROC uses ebx ecx edx esi edi
|
|||
|
||||
; Open the door!
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN
|
||||
invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN
|
||||
ret
|
||||
|
||||
check_door_close:
|
||||
|
|
@ -211,6 +218,7 @@ check_door_close:
|
|||
|
||||
; Close the door!
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR
|
||||
invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW
|
||||
ret
|
||||
|
||||
check_portal_interact:
|
||||
|
|
@ -523,7 +531,7 @@ ApplyScreenFade ENDP
|
|||
LoadSublevelBinary PROC uses ebx ecx edx esi edi, pszPath:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD
|
||||
LOCAL hFile:DWORD, bytesRead:DWORD, header:MapHeader
|
||||
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 stagedMap[MAP_SIZE]:BYTE
|
||||
LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE
|
||||
|
|
@ -644,6 +652,16 @@ old_map_no_transitions:
|
|||
movzx eax, header.cutsceneFlags
|
||||
mov sceneFlags, eax
|
||||
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
|
||||
imul eax, sizeof LevelTransition
|
||||
mov ebx, eax
|
||||
|
|
@ -657,6 +675,13 @@ old_map_no_cutscene:
|
|||
mov eax, numStrings
|
||||
imul eax, CUTSCENE_STRING_BYTES
|
||||
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
|
||||
cmp eax, ebx
|
||||
jne close_load_failed
|
||||
|
|
@ -697,6 +722,44 @@ old_map_no_cutscene:
|
|||
jz close_load_failed
|
||||
cmp bytesRead, ebx
|
||||
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
|
||||
test eax, eax
|
||||
jz close_load_failed
|
||||
|
|
@ -900,6 +963,7 @@ commit_level:
|
|||
mov eax, newAngle
|
||||
mov playerAngle, eax
|
||||
mov mapErrorFrames, 0
|
||||
invoke CommitLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights,OFFSET stagedLightRGB
|
||||
mov eax, 1
|
||||
ret
|
||||
close_load_failed:
|
||||
|
|
@ -968,6 +1032,8 @@ load_startup_map:
|
|||
jnz startup_map_ready
|
||||
call InitMap
|
||||
startup_map_ready:
|
||||
push eax
|
||||
call InitEntities
|
||||
pop eax
|
||||
ret
|
||||
LoadMapBinary ENDP
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
; Windowless renderer used by EblanStudio. Same Render3DScene and overlays as game.
|
||||
; Fixed wire layout is mirrored by editor/src/preview_protocol.h.
|
||||
.data
|
||||
previewPrefix db "--preview:",0
|
||||
previewHandle dd 0
|
||||
previewMemory dd 0
|
||||
previewParent dd 0
|
||||
|
|
@ -17,14 +16,14 @@ TryEnginePreview PROC uses ebx ecx edx esi edi
|
|||
test eax, eax
|
||||
jz preview_exit_error
|
||||
mov previewHandle, eax
|
||||
invoke MapViewOfFile, eax, 6, 0, 0, 1357792
|
||||
invoke MapViewOfFile, eax, 6, 0, 0, 1362408
|
||||
test eax, eax
|
||||
jz preview_exit_error
|
||||
mov previewMemory, eax
|
||||
mov ebx, eax
|
||||
cmp DWORD PTR [ebx], 56504245h
|
||||
jne preview_exit_error
|
||||
cmp DWORD PTR [ebx+4], 1
|
||||
cmp DWORD PTR [ebx+4], 2
|
||||
jne preview_exit_error
|
||||
invoke OpenProcess, 00100000h, 0, DWORD PTR [ebx+12] ; SYNCHRONIZE
|
||||
test eax, eax
|
||||
|
|
@ -107,6 +106,39 @@ preview_textures:
|
|||
mov ecx, MAX_TEXTURES*TEX_SIZE_BYTES/4
|
||||
rep movsd
|
||||
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
|
||||
mov ebx, previewMemory
|
||||
cmp DWORD PTR [ebx+40], 0
|
||||
|
|
|
|||
|
|
@ -36,16 +36,34 @@ Render3DScene PROC uses ebx ecx edx esi edi
|
|||
LOCAL glassLineHeight:DWORD
|
||||
LOCAL glassDrawStart:DWORD, glassDrawEnd:DWORD
|
||||
LOCAL glassTexX:DWORD, glassSideHit:DWORD
|
||||
LOCAL colLightLevel:DWORD
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; 1. Fast Background Clear: Ceiling (Top Half) and Floor (Bottom Half)
|
||||
; --------------------------------------------------------------------------
|
||||
invoke SamplePointLighting,playerX,playerY,0,0
|
||||
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)
|
||||
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)
|
||||
rep stosd
|
||||
|
||||
|
|
@ -465,6 +483,11 @@ wall_texture_ready:
|
|||
imul eax, texStep
|
||||
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
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -493,6 +516,9 @@ wall_pixel_loop:
|
|||
add eax, texPtr
|
||||
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
|
||||
cmp sideHit, 1
|
||||
jne apply_distance_fog
|
||||
|
|
@ -513,6 +539,10 @@ apply_distance_fog:
|
|||
|
||||
make_pitch_black:
|
||||
mov edx, 00111111h
|
||||
jmp write_wall_pixel
|
||||
|
||||
apply_col_light:
|
||||
call ShadeLightPixel
|
||||
|
||||
write_wall_pixel:
|
||||
mov eax, pScreenPixel
|
||||
|
|
@ -591,6 +621,7 @@ RenderSprites PROC uses ebx ecx edx esi edi passFilter:DWORD
|
|||
LOCAL texPtr:DWORD, pPixel:DWORD
|
||||
LOCAL camCos:DWORD, camSin:DWORD
|
||||
LOCAL keyIdx:DWORD, keyDist:DWORD
|
||||
LOCAL sprLightLevel:DWORD
|
||||
|
||||
mov eax, spriteCount
|
||||
test eax, eax
|
||||
|
|
@ -836,6 +867,9 @@ sprite_loop:
|
|||
mov texStepY, eax
|
||||
|
||||
; Render vertical stripes
|
||||
invoke ComputeSpriteLight, spriteX, spriteY, transY
|
||||
mov sprLightLevel, eax
|
||||
|
||||
mov eax, drawStartX
|
||||
mov stripe, eax
|
||||
|
||||
|
|
@ -916,6 +950,9 @@ sprite_pixel_y:
|
|||
cmp edx, COLOR_CHROMA_KEY
|
||||
je skip_sprite_pixel
|
||||
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
jne apply_spr_lighting
|
||||
|
||||
; Distance fog (same thresholds as walls)
|
||||
mov eax, transY
|
||||
cmp eax, 350
|
||||
|
|
@ -927,6 +964,10 @@ sprite_pixel_y:
|
|||
jmp write_sprite_pixel
|
||||
sprite_pixel_dark:
|
||||
mov edx, 00111111h
|
||||
jmp write_sprite_pixel
|
||||
|
||||
apply_spr_lighting:
|
||||
call ShadeLightPixel
|
||||
|
||||
write_sprite_pixel:
|
||||
mov eax, pPixel
|
||||
|
|
|
|||
111
main.asm
111
main.asm
|
|
@ -112,9 +112,11 @@ include include/engine_textures.inc
|
|||
include include/engine_input.inc
|
||||
include include/engine_physics.inc
|
||||
include include/engine_entity.inc
|
||||
include include/engine_light.inc
|
||||
include include/engine_raycaster.inc
|
||||
include include/engine_render2d.inc
|
||||
include include/engine_combat.inc
|
||||
include include/engine_gameplay.inc
|
||||
include include/engine_cutscene.inc
|
||||
include include/engine_preview.inc
|
||||
|
||||
|
|
@ -133,14 +135,7 @@ WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
|
|||
ret
|
||||
|
||||
handle_keydown:
|
||||
cmp wParam, VK_KEY_ESCAPE
|
||||
jne check_f12
|
||||
invoke PostQuitMessage, 0
|
||||
mov isRunning, 0
|
||||
xor eax, eax
|
||||
ret
|
||||
check_f12:
|
||||
; Movement and action edges are handled only by PollInput.
|
||||
; Key edges and ESC pause menu are handled via PollInput
|
||||
invoke DefWindowProcA, hWnd, uMsg, wParam, lParam
|
||||
ret
|
||||
|
||||
|
|
@ -164,7 +159,8 @@ Engine_Init PROC uses ebx esi edi
|
|||
call InitMathTables
|
||||
call GenerateDefaultTextures ; Procedural baseline (always valid)
|
||||
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
|
||||
invoke GetModuleHandleA, 0
|
||||
|
|
@ -226,19 +222,30 @@ Engine_RenderHUD PROC uses ebx ecx edx
|
|||
call RenderCutsceneOverlay
|
||||
ret
|
||||
normal_engine_hud:
|
||||
cmp hudStyle, HUD_STYLE_DEBUG
|
||||
jne clean_hud_mode
|
||||
|
||||
; --- DEBUG / DEVELOPER HUD MODE ---
|
||||
; 1. Top HUD Header
|
||||
invoke DrawString8x8, 10, 10, OFFSET strEngineName, COLOR_YELLOW
|
||||
invoke DrawString8x8, 8, 8, OFFSET strEngineName, COLOR_YELLOW
|
||||
|
||||
; 2. FPS Counter
|
||||
invoke DrawString8x8, 10, 24, OFFSET szFpsLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 42, 24, currentFps, COLOR_CYAN
|
||||
; 2. FPS Counter (drawn at top right X=250 so it never overlaps left elements)
|
||||
invoke DrawString8x8, 250, 8, OFFSET szFpsLabel, 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 DrawString8x8, 10, 218, OFFSET strControls1, COLOR_WHITE
|
||||
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
|
||||
cmp eax, 1
|
||||
je show_door_hint
|
||||
|
|
@ -247,11 +254,11 @@ normal_engine_hud:
|
|||
jmp check_minimap
|
||||
|
||||
show_door_hint:
|
||||
invoke DrawString8x8, 128, 150, OFFSET strDoorHint, COLOR_YELLOW
|
||||
invoke DrawString8x8, 116, 140, OFFSET strDoorHint, COLOR_YELLOW
|
||||
jmp check_minimap
|
||||
|
||||
show_portal_hint:
|
||||
invoke DrawString8x8, 110, 150, OFFSET strPortalHint, COLOR_CYAN
|
||||
invoke DrawString8x8, 110, 140, OFFSET strPortalHint, COLOR_CYAN
|
||||
|
||||
check_minimap:
|
||||
cmp mapErrorFrames, 0
|
||||
|
|
@ -261,6 +268,9 @@ hud_no_map_error:
|
|||
; 5. Minimap Radar (Toggle with 'M')
|
||||
call DrawMinimap
|
||||
|
||||
; 6. Gameplay HUD Overlays (Floating Toasts, Dialog Modals, Custom HUD)
|
||||
call RenderGameHUDOverlays
|
||||
|
||||
ret
|
||||
Engine_RenderHUD ENDP
|
||||
|
||||
|
|
@ -274,9 +284,17 @@ Engine_Frame PROC uses ebx esi edi
|
|||
dec mapErrorFrames
|
||||
no_error_tick:
|
||||
call PollInput
|
||||
cmp isGamePaused, 0
|
||||
jne @F
|
||||
cmp gameDialogActive, 0
|
||||
jne skip_movement_in_dialog
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
call UpdateDynamicLights
|
||||
skip_movement_in_dialog:
|
||||
call Game_OnTick
|
||||
@@:
|
||||
|
||||
; 2. 3D Raycasting & 2D HUD Render
|
||||
call Render3DScene
|
||||
|
|
@ -286,6 +304,7 @@ no_error_tick:
|
|||
call RenderCombatHUD
|
||||
@@:
|
||||
call ApplyCutsceneFade
|
||||
call RenderPauseMenu
|
||||
|
||||
; 2b. Screenshot request (F12): dump fresh framebuffer to BMP
|
||||
cmp keyScreenshot, 0
|
||||
|
|
@ -364,14 +383,14 @@ ParseStartupMap PROC uses ebx ecx edx esi edi
|
|||
skip_executable_quote:
|
||||
lodsb
|
||||
test al, al
|
||||
jz startup_arg_done
|
||||
jz check_cmdline_flags
|
||||
cmp al, 34
|
||||
jne skip_executable_quote
|
||||
jmp skip_argument_spaces
|
||||
skip_executable_word:
|
||||
lodsb
|
||||
test al, al
|
||||
jz startup_arg_done
|
||||
jz check_cmdline_flags
|
||||
cmp al, 32
|
||||
jne skip_executable_word
|
||||
skip_argument_spaces:
|
||||
|
|
@ -408,10 +427,59 @@ write_startup_char:
|
|||
dec ecx
|
||||
jnz startup_argument_loop
|
||||
mov startupMapPath, 0
|
||||
ret
|
||||
jmp check_cmdline_flags
|
||||
terminate_startup_arg:
|
||||
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
|
||||
ParseStartupMap ENDP
|
||||
|
||||
|
|
@ -422,6 +490,7 @@ main PROC
|
|||
LOCAL msg:MSG
|
||||
LOCAL curTick:DWORD, elapsed:DWORD
|
||||
|
||||
call LoadProjectSettings
|
||||
call ParseStartupMap
|
||||
call TryEnginePreview
|
||||
call Engine_Init
|
||||
|
|
|
|||
BIN
main.obj
BIN
main.obj
Binary file not shown.
|
|
@ -11,7 +11,7 @@ static void RunCutsceneChecks(EditorApp& app) {
|
|||
};
|
||||
Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference");
|
||||
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,
|
||||
"v4 track round trip retains UTF8 and autoplay");
|
||||
std::string encoded;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ static std::vector<uint8_t> Bytes(const fs::path& path) {
|
|||
}
|
||||
|
||||
#include "cutscene_editor_checks.inl"
|
||||
#include "lighting_editor_checks.inl"
|
||||
#include "viewport_editor_checks.inl"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
|
@ -138,6 +139,7 @@ int main(int argc, char** argv) {
|
|||
app.LaunchPlaytest();
|
||||
Check(!app.isEngineRunning && app.playtestDirectory.empty() && Bytes("../assets/map.dat") == originalMain,
|
||||
"missing linked map aborts F5 without data loss");
|
||||
RunLightingChecks(app);
|
||||
RunCutsceneChecks(app);
|
||||
if (argc > 1) {
|
||||
Check(ReadLevelFile(std::string(argv[1]) + "/map.dat", preview, error), "existing project main map remains compatible");
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ include include/engine_textures.inc
|
|||
include include/engine_input.inc
|
||||
include include/engine_physics.inc
|
||||
include include/engine_entity.inc
|
||||
include include/engine_light.inc
|
||||
include include/engine_raycaster.inc
|
||||
include include/engine_render2d.inc
|
||||
include include/engine_combat.inc
|
||||
include include/engine_gameplay.inc
|
||||
include include/engine_cutscene.inc
|
||||
include startup_parser.inc
|
||||
include project_paths.inc
|
||||
|
|
@ -126,6 +128,7 @@ state_changed:
|
|||
StateUnchanged ENDP
|
||||
|
||||
include tests/cutscene_engine_checks.inc
|
||||
include tests/lighting_engine_checks.inc
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -362,6 +365,7 @@ flat_depth_ok:
|
|||
call Render3DScene
|
||||
invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE
|
||||
|
||||
call RunLightingChecks
|
||||
call RunCutsceneChecks
|
||||
mov eax, resultCount
|
||||
inc eax
|
||||
|
|
|
|||
65
tests/lighting_editor_checks.inl
Normal file
65
tests/lighting_editor_checks.inl
Normal 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");
|
||||
}
|
||||
103
tests/lighting_engine_checks.inc
Normal file
103
tests/lighting_engine_checks.inc
Normal 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
|
||||
|
|
@ -49,6 +49,13 @@ static void RunViewportChecks(EditorApp& app, const char* actualEngine) {
|
|||
Check(equal,"DX11 texture pixels match the ASM framebuffer with opaque alpha");
|
||||
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();
|
||||
Check(!app.previewProcess.hProcess && !app.previewPacket && !app.previewMapping,"closing viewport releases renderer process and mapping");
|
||||
app.ShutdownD3D();device->Release();app.showCutsceneTimeline=false;
|
||||
|
|
|
|||
|
|
@ -9,16 +9,18 @@ import subprocess
|
|||
import time
|
||||
|
||||
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()}"
|
||||
with mmap.mmap(-1, SIZE, tagname=name) as packet:
|
||||
base = C.addressof(C.c_char.from_buffer(packet))
|
||||
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)
|
||||
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
|
||||
# Flat red wall, other slots black; proves texture upload rather than fallback.
|
||||
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"
|
||||
# Compare the live IPC output with a direct Render3DScene regression dump.
|
||||
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)
|
||||
version=h[1]; count=h[7]
|
||||
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)
|
||||
struct.pack_into("<ii",sprites,i*24,x>>16,y>>16)
|
||||
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()
|
||||
packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:]
|
||||
struct.pack_into("<I",packet,48,2)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ def main():
|
|||
f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"),
|
||||
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work)
|
||||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text()
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text() + (ROOT / "tests/lighting_engine_checks.inc").read_text()
|
||||
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||
raw = (work / "engine_results.bin").read_bytes()
|
||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||
|
|
@ -174,10 +174,14 @@ def main():
|
|||
if (work / "cutscene_trace.bin").read_bytes() != reference:
|
||||
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")
|
||||
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_tests = importlib.util.module_from_spec(viewport_spec)
|
||||
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 / "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())
|
||||
print("PASS original project assets remain byte-for-byte unchanged")
|
||||
succeeded = True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue