chore: integrate main before stage 2
Preserve stage-1 stability fixes alongside v5 lighting, IPC v2, weapons and studio updates. Fix barrel AOE pointer corruption and extend modal gameplay gating. Verify 137 editor checks, 191 ASM checks, lighting bake and preview parity. Document stage-2 scope without implementing it.
This commit is contained in:
commit
527d92411d
44 changed files with 4694 additions and 416 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.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,63 @@ assets. New tests exercise production gameplay and IDE procedures, including
|
|||
failed atomic replacement under a Windows sharing lock and hidden compiler output
|
||||
exceeding pipe capacity. Interactive UI playtesting remains separate.
|
||||
|
||||
## Preparation for stage 2 — integration with current main
|
||||
|
||||
Integrated server history through `2bac213` with stage-1 commit `f42679f` in an
|
||||
isolated worktree, preserving both histories and the original working directory.
|
||||
Retained map v5, preview IPC v2, authored lighting, three weapons, explosive
|
||||
barrels, modal dialogues, peaceful mode and the updated studio UI.
|
||||
|
||||
Compatibility adjustments:
|
||||
|
||||
- Pause and modal dialogue join the shared gameplay gate, including direct calls
|
||||
to gameplay procedures. Blocked player updates cannot activate portal triggers.
|
||||
- Knife/pistol/shotgun keep their range, damage, ammunition and reserved light
|
||||
behavior. World-space half-widths are respectively 24/16/32 times sprite scale;
|
||||
the shotgun remains a single-target wider hit test, not three separate rays.
|
||||
- Barrel texture fallback is enabled only with zero flags in both editor and ASM.
|
||||
Explicit Prop overrides barrel artwork and cannot explode.
|
||||
- A new explosion regression reproduced access violation `0xC0000005`: the AOE
|
||||
loop kept a sprite pointer in EDX and destroyed it with CDQ. The pointer now
|
||||
stays in EBX. A nearby guard receives blast damage without crashing.
|
||||
- IDE tab navigation, API catalog and docked view retain stage-1 safe writes.
|
||||
|
||||
Verification: both production builds succeeded; editor **137/137**, ASM
|
||||
**191/191**. All 1024 baked RGB cells match between C++ and ASM; classic and lit
|
||||
preview frames match direct ASM rendering, including the x86/x64/DX11 path.
|
||||
Project assets remain byte-for-byte unchanged. The original working directory
|
||||
still has only its pre-existing `editor/imgui.ini` modification. No separate
|
||||
interactive UI playtest was performed during this preparation.
|
||||
|
||||
### Stage 2 implementation contract (not implemented here)
|
||||
|
||||
1. Separate message/input polling, fixed simulation ticks and drawing/presentation.
|
||||
Use QPC and a 60 Hz accumulator with bounded catch-up. Consume edge-triggered
|
||||
commands once, not once per catch-up tick. Reset timing debt after pause/focus
|
||||
stalls; advance world timers/lights only on the appropriate simulation tick.
|
||||
2. Keep movement fractions in runtime state without changing serialized raw-unit
|
||||
coordinates. Cancel opposing inputs, normalize combined forward/strafe input,
|
||||
and use symmetric signed accumulation. Clear fractions on load/teleport/restart.
|
||||
3. Check the full collision body on each axis and preserve wall sliding; prevent
|
||||
corner clipping and occupied-door closure. Keep portals intentionally walkable.
|
||||
4. Centralize Playing/Cutscene/Transition/Dead/Paused/Dialog update rules, including
|
||||
`Game_OnTick`, dynamic lights, fade, toasts and modal dismissal. Render routines
|
||||
must not advance those timers or perform input-driven state changes.
|
||||
5. Separate cinematic camera pose from gameplay/player position. Define END/skip
|
||||
handoff explicitly and validate the restored gameplay collision body, including
|
||||
paths crossing walls. Keep C++ preview and ASM cinematic traces aligned.
|
||||
6. Track the current room path and its loaded entry spawn separately from startup
|
||||
map. Restart restores that room and entry pose, resets gameplay transients and
|
||||
fractional movement, and preserves project mode/settings. Failed reload must
|
||||
not partially reset the current room. Make death-R and pause restart share it.
|
||||
|
||||
Acceptance checks: identical simulation after 60 ticks under different render
|
||||
schedules; single consumption of edge inputs during catch-up; symmetric movement
|
||||
for all quadrants and equal cardinal/diagonal speed; collision corner/slide cases;
|
||||
no simulation advancement during pause/dialog; no pickups/AI along camera paths;
|
||||
safe cinematic handoff; restart after a room transition; failed restart rollback;
|
||||
unchanged v1..v5 loading, lighting bake parity and IPC v2 framebuffer parity.
|
||||
|
||||
## Remaining stages
|
||||
|
||||
2. Predictable simulation: fixed 60 Hz ticks, fractional movement, normalized
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,25 +1,40 @@
|
|||
[Window][Workspace]
|
||||
Pos=0,60
|
||||
Size=1536,708
|
||||
LastUsed=20261022
|
||||
Pos=0,64
|
||||
Size=2560,1288
|
||||
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,42
|
||||
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=20261024
|
||||
|
||||
|
|
|
|||
14
editor/project.eblanproj
Normal file
14
editor/project.eblanproj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Project]
|
||||
Title=EBLAN_GAME
|
||||
Author=Developer
|
||||
Version=1.0.0
|
||||
StartupMap=assets/map.dat
|
||||
WindowTitle=EBLAN ENGINE 3D - Standalone Retro Game
|
||||
DefaultWidth=1280
|
||||
DefaultHeight=960
|
||||
GameMode=1
|
||||
ShowWeapons=1
|
||||
ShowCrosshair=1
|
||||
LightingMode=2
|
||||
AmbientLight=40
|
||||
Flashlight=1
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
@ -22,7 +23,18 @@ enum ToolMode {
|
|||
TOOL_ERASER,
|
||||
TOOL_FILL,
|
||||
TOOL_PLAYER_SPAWN,
|
||||
TOOL_SPRITE_SPAWN
|
||||
TOOL_SPRITE_SPAWN,
|
||||
TOOL_LIGHT_SPAWN
|
||||
};
|
||||
|
||||
enum LightPreset {
|
||||
LIGHT_PRESET_TORCH = 0,
|
||||
LIGHT_PRESET_LAMP,
|
||||
LIGHT_PRESET_ALARM,
|
||||
LIGHT_PRESET_CANDLE,
|
||||
LIGHT_PRESET_NEON,
|
||||
LIGHT_PRESET_TOXIC,
|
||||
LIGHT_PRESET_SPOTLIGHT
|
||||
};
|
||||
|
||||
// Full level snapshot for undo/redo (grid + spawn + palette + sprites + transitions)
|
||||
|
|
@ -33,6 +45,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 +83,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 +120,43 @@ 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;
|
||||
bool autoBakeLighting = true;
|
||||
int draggingLightRadiusIdx = -1;
|
||||
uint8_t bakedLightmap[MAP_TOTAL_CELLS] = { 0 };
|
||||
std::array<uint32_t,1024> bakedLightRGB{};
|
||||
uint64_t bakedLightingSignature = 0;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
|
||||
void BakeLighting();
|
||||
void InitDefaultLights();
|
||||
void ApplyLightPreset(int lightIdx, int presetId);
|
||||
void SnapLightToGrid(int lightIdx);
|
||||
bool CastLightRay(int x1, int y1, int x2, int y2) const;
|
||||
bool LightingNeedsBake() const;
|
||||
LightingSettings GetLightingSettings() const;
|
||||
|
||||
// Panel Dimensions & Splitters (Interactive Resizing like Unreal / Unity)
|
||||
float leftPanelWidth = 260.0f;
|
||||
float rightPanelWidth = 350.0f;
|
||||
float centerSplitRatio = 0.5f;
|
||||
|
||||
// Viewport pan & zoom
|
||||
float canvasZoom = 20.0f; // pixels per tile
|
||||
float canvasPanX = 20.0f;
|
||||
|
|
@ -112,6 +167,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;
|
||||
|
|
@ -106,6 +190,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;
|
||||
|
|
@ -193,8 +281,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";
|
||||
|
|
@ -202,7 +291,7 @@ struct CodeEditorState {
|
|||
}
|
||||
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n";
|
||||
buildLog = "[ERROR] Не найден скрипт сборки build.bat в: " + buildBat.string() + "\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
return;
|
||||
|
|
@ -319,14 +408,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;
|
||||
}
|
||||
|
|
@ -340,25 +428,23 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
g_CodeEditor.PollBuild();
|
||||
if (!showCodeEditorModal) return;
|
||||
|
||||
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();
|
||||
|
|
@ -371,7 +457,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);
|
||||
|
|
@ -386,12 +471,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);
|
||||
}
|
||||
|
|
@ -401,51 +489,110 @@ 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)) {
|
||||
g_CodeEditor.LoadFile(i, 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)) {
|
||||
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
|
||||
|
|
@ -453,7 +600,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 ? "*" : "");
|
||||
|
|
@ -470,10 +616,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();
|
||||
|
|
@ -483,9 +629,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);
|
||||
|
|
@ -525,7 +670,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) {
|
||||
|
|
@ -542,5 +686,4 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::EndTabBar();
|
||||
|
||||
ImGui::Columns(1);
|
||||
ImGui::End();
|
||||
}
|
||||
|
|
|
|||
268
editor/src/editor_lighting.inl
Normal file
268
editor/src/editor_lighting.inl
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
void EditorApp::ApplyLightPreset(int lightIdx, int presetId) {
|
||||
if (lightIdx < 0 || lightIdx >= static_cast<int>(lightSources.size())) return;
|
||||
PushUndo();
|
||||
auto& l = lightSources[lightIdx];
|
||||
l.flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS;
|
||||
switch (presetId) {
|
||||
case LIGHT_PRESET_TORCH: // 🕯 Факел
|
||||
l.color = 0x00FFB040;
|
||||
l.radius = 240;
|
||||
l.intensity = 220;
|
||||
l.curIntensity = 220;
|
||||
l.lightType = LIGHT_TYPE_POINT;
|
||||
l.flags |= LIGHT_FLAG_FLICKER;
|
||||
break;
|
||||
case LIGHT_PRESET_LAMP: // 💡 Лампа
|
||||
l.color = 0x00E0E8FF;
|
||||
l.radius = 280;
|
||||
l.intensity = 240;
|
||||
l.curIntensity = 240;
|
||||
l.lightType = LIGHT_TYPE_STATIC;
|
||||
break;
|
||||
case LIGHT_PRESET_ALARM: // 🚨 Тревожная сирена
|
||||
l.color = 0x00FF2020;
|
||||
l.radius = 300;
|
||||
l.intensity = 250;
|
||||
l.curIntensity = 250;
|
||||
l.lightType = LIGHT_TYPE_PULSE;
|
||||
break;
|
||||
case LIGHT_PRESET_CANDLE: // 🕯 Мягкая свеча
|
||||
l.color = 0x00FF9520;
|
||||
l.radius = 140;
|
||||
l.intensity = 180;
|
||||
l.curIntensity = 180;
|
||||
l.lightType = LIGHT_TYPE_POINT;
|
||||
l.flags |= LIGHT_FLAG_FLICKER;
|
||||
break;
|
||||
case LIGHT_PRESET_NEON: // 🔮 Киберпанк неон
|
||||
l.color = 0x0000E5FF;
|
||||
l.radius = 260;
|
||||
l.intensity = 230;
|
||||
l.curIntensity = 230;
|
||||
l.lightType = LIGHT_TYPE_POINT;
|
||||
break;
|
||||
case LIGHT_PRESET_TOXIC: // ☢ Токсик
|
||||
l.color = 0x0039FF14;
|
||||
l.radius = 250;
|
||||
l.intensity = 220;
|
||||
l.curIntensity = 220;
|
||||
l.lightType = LIGHT_TYPE_PULSE;
|
||||
break;
|
||||
case LIGHT_PRESET_SPOTLIGHT: // 🔦 Прожектор
|
||||
l.color = 0x00FFFFFF;
|
||||
l.radius = 380;
|
||||
l.intensity = 255;
|
||||
l.curIntensity = 255;
|
||||
l.lightType = LIGHT_TYPE_STATIC;
|
||||
break;
|
||||
}
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
statusMessage = "Применен пресет света.";
|
||||
}
|
||||
|
||||
void EditorApp::SnapLightToGrid(int lightIdx) {
|
||||
if (lightIdx < 0 || lightIdx >= static_cast<int>(lightSources.size())) return;
|
||||
PushUndo();
|
||||
auto& l = lightSources[lightIdx];
|
||||
int cellX = std::clamp(l.x / 64, 0, MAP_GRID_W - 1);
|
||||
int cellY = std::clamp(l.y / 64, 0, MAP_GRID_H - 1);
|
||||
l.x = cellX * 64 + 32;
|
||||
l.y = cellY * 64 + 32;
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
statusMessage = "Источник света выровнен по центру клетки (" + std::to_string(cellX) + ", " + std::to_string(cellY) + ").";
|
||||
}
|
||||
|
||||
void EditorApp::RenderLightingSettings() {
|
||||
auto edited = GetLightingSettings();
|
||||
bool changed = false, activated = false;
|
||||
auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); };
|
||||
|
||||
const char* modes[] = { "Без освещения (Fullbright)", "Только запечённый свет (Baked)", "Запечённый + Динамический (Full Physics)" };
|
||||
int mode = edited.mode;
|
||||
changed |= ImGui::Combo("Режим света", &mode, modes, 3); undoItem(); edited.mode = mode;
|
||||
|
||||
int ambient = edited.ambient;
|
||||
changed |= ImGui::SliderInt("Фоновый свет (Ambient)", &ambient, 0, 255); undoItem(); edited.ambient = ambient;
|
||||
|
||||
bool flash = (edited.flashlight != 0);
|
||||
changed |= ImGui::Checkbox("Фонарик игрока [F]", &flash); undoItem(); edited.flashlight = flash;
|
||||
if (flash) {
|
||||
int radius = edited.flashlightRadius, intensity = edited.flashlightIntensity, cone = edited.flashlightCone;
|
||||
changed |= ImGui::SliderInt("Дальность фонаря", &radius, 32, 2048); undoItem();
|
||||
changed |= ImGui::SliderInt("Яркость фонаря", &intensity, 0, 255); undoItem();
|
||||
changed |= ImGui::SliderInt("Полуугол луча", &cone, 1, 96); undoItem();
|
||||
edited.flashlightRadius = radius; edited.flashlightIntensity = intensity; edited.flashlightCone = cone;
|
||||
}
|
||||
|
||||
if (activated) PushUndo();
|
||||
if (changed) {
|
||||
lightingMode = edited.mode;
|
||||
ambientLightLevel = edited.ambient;
|
||||
isFlashlightOn = (edited.flashlight != 0);
|
||||
flashlightRadius = edited.flashlightRadius;
|
||||
flashlightIntensity = edited.flashlightIntensity;
|
||||
flashlightCone = edited.flashlightCone;
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Checkbox("Авто-запекание (Live Realtime Update)", &autoBakeLighting);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Мгновенно обновляет карту теней и 3D вид при любых изменениях источников и стен.");
|
||||
|
||||
ImGui::Checkbox("Карта запечённого света (Хитмап на 2D сетке)", &showLightHeatmap);
|
||||
|
||||
if (ImGui::Button("💡 Запечь постоянный свет сейчас", ImVec2(-1, 26))) {
|
||||
PushUndo();
|
||||
BakeLighting();
|
||||
}
|
||||
|
||||
if (LightingNeedsBake()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.2f, 1.0f), "⚠ Геометрия или источники изменены: требуется запекание");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "✔ Постоянный свет запечён (1024 ячейки)");
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::RenderLightingInspector() {
|
||||
if (selectedLightIdx < 0 || selectedLightIdx >= static_cast<int>(lightSources.size())) return;
|
||||
auto& light = lightSources[selectedLightIdx];
|
||||
|
||||
const char* typeDesc = (light.lightType == LIGHT_TYPE_STATIC) ? "Постоянный (Запекаемый)" :
|
||||
(light.lightType == LIGHT_TYPE_PULSE) ? "Пульсирующий (Сирена)" :
|
||||
(light.flags & LIGHT_FLAG_FLICKER) ? "Мерцающий факел" : "Точечный (Динамический)";
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.25f, 1.0f), "Источник света #%d из %d: %s",
|
||||
selectedLightIdx + 1, static_cast<int>(lightSources.size()), typeDesc);
|
||||
ImGui::Separator();
|
||||
|
||||
// Quick Action Bar (2x2 Grid)
|
||||
float colW2 = (ImGui::GetContentRegionAvail().x - 6.0f) * 0.5f;
|
||||
if (ImGui::Button("Фокус (F)", ImVec2(colW2, 24))) { FocusSelectedEntity(); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Дублировать (Ctrl+D)", ImVec2(colW2, 24))) { DuplicateSelectedEntity(); }
|
||||
|
||||
if (ImGui::Button("К сетке (Snap)", ImVec2(colW2, 24))) { SnapLightToGrid(selectedLightIdx); }
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.15f, 0.15f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.20f, 0.20f, 1.0f));
|
||||
if (ImGui::Button("Удалить (Del)", ImVec2(colW2, 24))) {
|
||||
ImGui::PopStyleColor(2);
|
||||
DeleteSelectedEntity();
|
||||
return;
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f), "Быстрые пресеты источников:");
|
||||
|
||||
// Preset Buttons: Clean 3-Column Grid (all 7 presets visible!)
|
||||
float colW3 = (ImGui::GetContentRegionAvail().x - 12.0f) / 3.0f;
|
||||
if (ImGui::Button("Факел", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TORCH);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Факел: Тёплый огонь (оранжевый, 240px, мерцание, тени)");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Лампа", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_LAMP);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Лампа: Запекаемый дневной свет (280px, статика)");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Сирена", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_ALARM);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Сирена: Аварийный маяк (красный, 300px, пульсация)");
|
||||
|
||||
if (ImGui::Button("Свеча", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_CANDLE);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Свеча: Мягкий тёплый огонёк (140px, мерцание)");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Неон", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_NEON);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Неон: Циановый неоновый свет (260px, динамика)");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Токсик", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TOXIC);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Токсик: Зелёное радиационное свечение (250px, пульс)");
|
||||
|
||||
if (ImGui::Button("Прожектор (Направленный)", ImVec2(-1, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_SPOTLIGHT);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Прожектор: Мощный белый свет (380px, статика)");
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
EditorLightSource edited = light;
|
||||
bool changed = false, activated = false;
|
||||
auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); };
|
||||
|
||||
// Toggle Active
|
||||
bool active = (edited.flags & LIGHT_FLAG_ACTIVE) != 0;
|
||||
changed |= ImGui::Checkbox("Включён на карте", &active); undoItem();
|
||||
edited.flags = (edited.flags & ~LIGHT_FLAG_ACTIVE) | (active ? LIGHT_FLAG_ACTIVE : 0);
|
||||
|
||||
// Coordinates with Cell info
|
||||
int curCellX = edited.x / 64;
|
||||
int curCellY = edited.y / 64;
|
||||
ImGui::Text("Клетка: [%d, %d] | Мировые координаты: (%d, %d)", curCellX, curCellY, edited.x, edited.y);
|
||||
float xCells = edited.x / 64.0f;
|
||||
float yCells = edited.y / 64.0f;
|
||||
if (ImGui::SliderFloat("Позиция X (клетки)", &xCells, 0.5f, 31.5f, "%.2f")) {
|
||||
edited.x = std::clamp(static_cast<int>(xCells * 64.0f), 32, (MAP_GRID_W - 1) * 64);
|
||||
changed = true;
|
||||
} undoItem();
|
||||
if (ImGui::SliderFloat("Позиция Y (клетки)", &yCells, 0.5f, 31.5f, "%.2f")) {
|
||||
edited.y = std::clamp(static_cast<int>(yCells * 64.0f), 32, (MAP_GRID_H - 1) * 64);
|
||||
changed = true;
|
||||
} undoItem();
|
||||
|
||||
// Radius & Intensity
|
||||
changed |= ImGui::SliderInt("Радиус (px)", &edited.radius, 32, 2048, "%d px"); undoItem();
|
||||
changed |= ImGui::SliderInt("Яркость", &edited.intensity, 0, 255, "%d / 255"); undoItem();
|
||||
edited.curIntensity = edited.intensity;
|
||||
|
||||
// Color Picker & Quick Swatches
|
||||
float color[] = { ((edited.color >> 16) & 255) / 255.f, ((edited.color >> 8) & 255) / 255.f, (edited.color & 255) / 255.f };
|
||||
if (ImGui::ColorEdit3("Цвет света", color)) {
|
||||
edited.color = (uint32_t(color[0] * 255) << 16) | (uint32_t(color[1] * 255) << 8) | uint32_t(color[2] * 255);
|
||||
changed = true;
|
||||
} undoItem();
|
||||
|
||||
// Quick Color Swatches
|
||||
ImGui::TextDisabled("Оттенки:");
|
||||
ImGui::SameLine();
|
||||
auto ColorSwatch = [&](const char* label, uint32_t hexColor) {
|
||||
float c[3] = { ((hexColor >> 16) & 255) / 255.f, ((hexColor >> 8) & 255) / 255.f, (hexColor & 255) / 255.f };
|
||||
if (ImGui::ColorButton(label, ImVec4(c[0], c[1], c[2], 1.0f), 0, ImVec2(18, 18))) {
|
||||
edited.color = hexColor;
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
};
|
||||
ColorSwatch("##warm_fire", 0x00FFB040);
|
||||
ColorSwatch("##cool_white", 0x00E0E8FF);
|
||||
ColorSwatch("##neon_cyan", 0x0000E5FF);
|
||||
ColorSwatch("##ruby_red", 0x00FF2020);
|
||||
ColorSwatch("##toxic_green", 0x0039FF14);
|
||||
ColorSwatch("##pure_white", 0x00FFFFFF);
|
||||
ImGui::NewLine();
|
||||
|
||||
// Light Type
|
||||
const char* types[] = { "Постоянный (запекание в lightmap)", "Точечный (динамический реалтайм)", "Пульсирующий (динамическая сирена)" };
|
||||
int type = (edited.lightType == LIGHT_TYPE_STATIC) ? 0 : (edited.lightType == LIGHT_TYPE_PULSE) ? 2 : 1;
|
||||
if (ImGui::Combo("Тип источника", &type, types, 3)) {
|
||||
edited.lightType = (type == 0) ? LIGHT_TYPE_STATIC : (type == 1) ? LIGHT_TYPE_POINT : LIGHT_TYPE_PULSE;
|
||||
changed = true;
|
||||
} undoItem();
|
||||
|
||||
// Behavior flags
|
||||
bool shadows = (edited.flags & LIGHT_FLAG_SHADOWS) != 0;
|
||||
changed |= ImGui::Checkbox("Тени от стен и дверей", &shadows); undoItem();
|
||||
edited.flags = (edited.flags & ~LIGHT_FLAG_SHADOWS) | (shadows ? LIGHT_FLAG_SHADOWS : 0);
|
||||
|
||||
if (edited.lightType != LIGHT_TYPE_STATIC) {
|
||||
bool flicker = (edited.flags & LIGHT_FLAG_FLICKER) != 0;
|
||||
changed |= ImGui::Checkbox("Мерцание огня / факела (Flicker)", &flicker); undoItem();
|
||||
edited.flags = (edited.flags & ~LIGHT_FLAG_FLICKER) | (flicker ? LIGHT_FLAG_FLICKER : 0);
|
||||
}
|
||||
|
||||
if (activated) PushUndo();
|
||||
if (changed) {
|
||||
light = edited;
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
if (ImGui::CollapsingHeader("Глобальное освещение карты")) {
|
||||
RenderLightingSettings();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (s.flags == 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;
|
||||
|
|
@ -136,3 +139,53 @@ static_assert(sizeof(MapHeader) == 38, "EBLN header layout changed");
|
|||
static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed");
|
||||
static_assert(sizeof(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) {
|
||||
|
|
@ -90,7 +95,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||
return 1;
|
||||
}
|
||||
|
||||
ShowWindow(hWnd, SW_SHOWDEFAULT);
|
||||
ShowWindow(hWnd, SW_MAXIMIZE);
|
||||
UpdateWindow(hWnd);
|
||||
DragAcceptFiles(hWnd, TRUE);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
; Trace the current world along the player's forward direction. EAX = first
|
||||
; blocking boundary in 24.8 world units. Glass blocks shots (unlike rendering).
|
||||
|
|
@ -143,6 +145,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
LOCAL sprX:DWORD, sprY:DWORD
|
||||
LOCAL transX:DWORD, transY:DWORD
|
||||
LOCAL targetHalfWidth:DWORD, wallDepth:DWORD
|
||||
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitHalfWidth:DWORD
|
||||
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
|
|
@ -162,9 +165,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
|
||||
|
|
@ -172,16 +183,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 hitHalfWidth, 16
|
||||
|
||||
; Fire weapon!
|
||||
cmp playerWeapon, WEAPON_KNIFE
|
||||
je setup_knife
|
||||
cmp playerWeapon, WEAPON_SHOTGUN
|
||||
je setup_shotgun
|
||||
jmp setup_pistol
|
||||
|
||||
setup_knife:
|
||||
; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown
|
||||
mov weaponDamage, 30
|
||||
mov maxHitDist, 64
|
||||
mov hitHalfWidth, 24
|
||||
mov playerAttackCooldown, 8
|
||||
mov playerFlashTimer, 3
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_shotgun:
|
||||
; Shotgun: needs 2 ammo, high damage (70), wider world hitbox, long cooldown
|
||||
cmp playerAmmo, 2
|
||||
jl combat_dry_fire
|
||||
sub playerAmmo, 2
|
||||
mov weaponDamage, 70
|
||||
mov maxHitDist, 999999
|
||||
mov hitHalfWidth, 32
|
||||
mov playerAttackCooldown, 22
|
||||
mov playerFlashTimer, 7
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_pistol:
|
||||
; Pistol: needs 1 ammo, accurate world hitbox, medium cooldown (12)
|
||||
cmp playerAmmo, 1
|
||||
jl combat_dry_fire
|
||||
dec playerAmmo
|
||||
mov playerAttackCooldown, 10
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitHalfWidth, 16
|
||||
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
|
||||
|
|
@ -206,9 +254,16 @@ 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
|
||||
|
||||
jmp next_hitscan_target
|
||||
|
||||
hitscan_target_valid:
|
||||
mov eax, [entityState + ecx*4]
|
||||
cmp eax, ENTITY_STATE_DEAD
|
||||
je next_hitscan_target
|
||||
|
|
@ -245,6 +300,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
|
||||
|
||||
; Compare in 24.8 so a target on a wall boundary is not hit through it.
|
||||
mov eax, transY
|
||||
shl eax, 8
|
||||
|
|
@ -262,14 +322,14 @@ hitscan_loop:
|
|||
add ecx, eax
|
||||
mov transX, ecx
|
||||
|
||||
; World-space billboard hitbox: 32 units wide at scale 1.0, scaled with
|
||||
; World-space billboard hitbox: weapon-specific width, scaled with
|
||||
; the authored sprite, never with screen distance. Zero means legacy 1.0.
|
||||
mov eax, [ebx].SpriteStruct.scale
|
||||
test eax, eax
|
||||
jnz @F
|
||||
mov eax, FIXED_ONE
|
||||
@@:
|
||||
imul eax, 16
|
||||
imul eax, hitHalfWidth
|
||||
sar eax, 16
|
||||
mov targetHalfWidth, eax
|
||||
mov eax, transX
|
||||
|
|
@ -295,10 +355,15 @@ 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
|
||||
; Hit an enemy guard!
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, 35
|
||||
sub eax, weaponDamage
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
|
|
@ -310,8 +375,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
|
||||
|
|
@ -321,6 +387,115 @@ 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
|
||||
|
||||
; EDX is volatile scratch for CDQ below, not a safe sprite pointer.
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
sub eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 128
|
||||
jg next_aoe
|
||||
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
sub eax, edi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 128
|
||||
jg next_aoe
|
||||
|
||||
; Within blast: deal 60 blast damage!
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, 60
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
mov [entityHealth + ecx*4], eax
|
||||
test eax, eax
|
||||
jnz next_aoe
|
||||
|
||||
mov [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
add playerScore, 150
|
||||
|
||||
next_aoe:
|
||||
inc i
|
||||
jmp aoe_loop
|
||||
|
||||
aoe_check_player:
|
||||
; If player was standing too close to the barrel (< 96 units), player takes blast damage!
|
||||
mov eax, playerX
|
||||
sub eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 96
|
||||
jg aoe_done
|
||||
|
||||
mov eax, playerY
|
||||
sub eax, edi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 96
|
||||
jg aoe_done
|
||||
|
||||
; Player blast damage
|
||||
mov eax, playerHealth
|
||||
sub eax, 25
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
mov playerHealth, eax
|
||||
test eax, eax
|
||||
jnz aoe_done
|
||||
mov isPlayerDead, 1
|
||||
|
||||
aoe_done:
|
||||
pop edi
|
||||
pop esi
|
||||
jmp combat_done
|
||||
|
||||
combat_dry_fire:
|
||||
mov playerAttackCooldown, 12
|
||||
|
||||
|
|
@ -334,24 +509,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
|
||||
|
|
@ -364,15 +606,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
|
||||
|
|
@ -383,34 +626,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
|
||||
|
|
@ -420,7 +695,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
|
||||
|
||||
|
|
@ -431,3 +706,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
|
||||
|
|
|
|||
|
|
@ -66,8 +66,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
|
||||
|
|
@ -120,7 +148,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
|
||||
|
|
@ -226,3 +264,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
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -86,8 +89,13 @@ init_spr_loop:
|
|||
|
||||
fallback_archetype:
|
||||
; If flags == 0, infer archetype from textureId:
|
||||
; Slot 13 = Enemy Guard, 14 = Medkit, 15 = Ammo
|
||||
; Slot 11 = Explosive Barrel, 13 = Guard, 14 = Medkit, 15 = Ammo
|
||||
mov edx, [ebx].SpriteStruct.textureId
|
||||
cmp edx, 11
|
||||
jne check_guard_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_BARREL
|
||||
jmp next_spr_slot
|
||||
check_guard_slot:
|
||||
cmp edx, 13
|
||||
jne check_medkit_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_GUARD
|
||||
|
|
@ -217,6 +225,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
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -240,6 +249,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
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -247,7 +257,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
|
||||
|
|
@ -257,6 +267,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
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
|
|||
342
include/engine_gameplay.inc
Normal file
342
include/engine_gameplay.inc
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Gameplay Events, Scripting Hooks, HUD Toasts & Dialog Modals
|
||||
; ==============================================================================
|
||||
|
||||
.code
|
||||
|
||||
DrawPixel PROTO :DWORD, :DWORD, :DWORD
|
||||
DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
PlaySoundA PROTO :DWORD, :DWORD, :DWORD
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; ShowGameToast: Displays a floating toast notification banner on screen
|
||||
; ------------------------------------------------------------------------------
|
||||
ShowGameToast PROC uses esi edi ecx eax, pszText:DWORD, duration:DWORD, textColor:DWORD
|
||||
mov esi, pszText
|
||||
test esi, esi
|
||||
jz toast_skip_copy
|
||||
|
||||
mov edi, OFFSET gameToastText
|
||||
mov ecx, 63
|
||||
toast_copy_loop:
|
||||
lodsb
|
||||
stosb
|
||||
test al, al
|
||||
jz toast_copy_done
|
||||
dec ecx
|
||||
jnz toast_copy_loop
|
||||
xor al, al
|
||||
stosb
|
||||
toast_copy_done:
|
||||
|
||||
toast_skip_copy:
|
||||
mov eax, duration
|
||||
mov gameToastTimer, eax
|
||||
mov eax, textColor
|
||||
mov gameToastColor, eax
|
||||
ret
|
||||
ShowGameToast ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; RenderGameToast: Draws active toast notification at top center of screen
|
||||
; ------------------------------------------------------------------------------
|
||||
RenderGameToast PROC uses ebx ecx edx esi edi
|
||||
cmp gameToastTimer, 0
|
||||
jle rt_done
|
||||
dec gameToastTimer
|
||||
|
||||
; 1. Calculate text character length
|
||||
mov esi, OFFSET gameToastText
|
||||
xor ecx, ecx
|
||||
count_len:
|
||||
mov al, [esi+ecx]
|
||||
test al, al
|
||||
jz count_done
|
||||
inc ecx
|
||||
cmp ecx, 63
|
||||
jb count_len
|
||||
count_done:
|
||||
test ecx, ecx
|
||||
jz rt_done
|
||||
|
||||
; Width in pixels = length * 8
|
||||
mov eax, ecx
|
||||
shl eax, 3
|
||||
mov edx, eax
|
||||
|
||||
; Center X = (SCREEN_W - width) / 2
|
||||
mov ebx, SCREEN_W
|
||||
sub ebx, edx
|
||||
sar ebx, 1
|
||||
|
||||
; Box coordinates: X = ebx - 6, Y = 16, W = edx + 12, H = 14
|
||||
mov eax, ebx
|
||||
sub eax, 6
|
||||
mov esi, edx
|
||||
add esi, 12
|
||||
|
||||
invoke DrawRect, eax, 16, esi, 14, 00101010h
|
||||
invoke DrawRectOutline, eax, 16, esi, 14, gameToastColor
|
||||
invoke DrawString8x8, ebx, 19, OFFSET gameToastText, gameToastColor
|
||||
|
||||
rt_done:
|
||||
ret
|
||||
RenderGameToast ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; ShowGameDialog: Opens an interactive retro dialogue modal with speaker and text
|
||||
; ------------------------------------------------------------------------------
|
||||
ShowGameDialog PROC uses esi edi ecx eax, pszSpeaker:DWORD, pszText:DWORD
|
||||
mov esi, pszSpeaker
|
||||
mov edi, OFFSET gameDialogSpeaker
|
||||
mov ecx, 31
|
||||
test esi, esi
|
||||
jz no_speaker
|
||||
copy_spk:
|
||||
lodsb
|
||||
stosb
|
||||
test al, al
|
||||
jz spk_done
|
||||
dec ecx
|
||||
jnz copy_spk
|
||||
xor al, al
|
||||
stosb
|
||||
spk_done:
|
||||
jmp setup_dlg_text
|
||||
no_speaker:
|
||||
mov BYTE PTR [edi], 0
|
||||
|
||||
setup_dlg_text:
|
||||
mov esi, pszText
|
||||
mov edi, OFFSET gameDialogText
|
||||
mov ecx, 127
|
||||
test esi, esi
|
||||
jz no_text
|
||||
copy_txt:
|
||||
lodsb
|
||||
stosb
|
||||
test al, al
|
||||
jz txt_done
|
||||
dec ecx
|
||||
jnz copy_txt
|
||||
xor al, al
|
||||
stosb
|
||||
txt_done:
|
||||
jmp activate_dlg
|
||||
no_text:
|
||||
mov BYTE PTR [edi], 0
|
||||
|
||||
activate_dlg:
|
||||
mov gameDialogActive, 1
|
||||
ret
|
||||
ShowGameDialog ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; CloseGameDialog: Closes active dialogue box
|
||||
; ------------------------------------------------------------------------------
|
||||
CloseGameDialog PROC
|
||||
mov gameDialogActive, 0
|
||||
ret
|
||||
CloseGameDialog ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; RenderGameDialog: Draws active dialogue box at bottom of screen
|
||||
; ------------------------------------------------------------------------------
|
||||
RenderGameDialog PROC uses ebx ecx edx esi edi
|
||||
cmp gameDialogActive, 0
|
||||
je rd_done
|
||||
|
||||
; Dialog Box background: X = 16, Y = 170, W = 288, H = 42
|
||||
invoke DrawRect, 16, 170, 288, 42, 00121212h
|
||||
invoke DrawRectOutline, 16, 170, 288, 42, COLOR_CYAN
|
||||
|
||||
; Speaker name (if not empty)
|
||||
cmp BYTE PTR [gameDialogSpeaker], 0
|
||||
je draw_dlg_body
|
||||
invoke DrawString8x8, 24, 174, OFFSET gameDialogSpeaker, COLOR_YELLOW
|
||||
|
||||
draw_dlg_body:
|
||||
; Text
|
||||
invoke DrawString8x8, 24, 186, OFFSET gameDialogText, COLOR_WHITE
|
||||
|
||||
; Close hint in bottom-right
|
||||
invoke DrawString8x8, 140, 200, OFFSET szDialogCloseHint, COLOR_GRAY
|
||||
|
||||
; Check if player pressed Action key or Attack to dismiss
|
||||
cmp keyAction, 0
|
||||
jne do_dismiss
|
||||
cmp keyAttack, 0
|
||||
jne do_dismiss
|
||||
jmp rd_done
|
||||
|
||||
do_dismiss:
|
||||
mov keyAction, 0
|
||||
mov keyAttack, 0
|
||||
mov gameDialogActive, 0
|
||||
|
||||
rd_done:
|
||||
ret
|
||||
RenderGameDialog ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; PlayGameSFX: Asynchronously plays a sound effect via WinMM PlaySoundA
|
||||
; ------------------------------------------------------------------------------
|
||||
PlayGameSFX PROC uses eax, pszRelativePath:DWORD
|
||||
cmp isCutsceneActive, 0
|
||||
jne sfx_done
|
||||
mov eax, pszRelativePath
|
||||
test eax, eax
|
||||
jz sfx_done
|
||||
cmp BYTE PTR [eax], 0
|
||||
jz sfx_done
|
||||
invoke PlaySoundA, eax, 0, 9 ; SND_FILENAME (1) | SND_ASYNC (8) = 9
|
||||
sfx_done:
|
||||
ret
|
||||
PlayGameSFX ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Game_OnTick: Hook called every 60 Hz frame during active gameplay
|
||||
; ------------------------------------------------------------------------------
|
||||
Game_OnTick PROC uses eax ebx ecx edx
|
||||
; Extensible hook for custom game logic (quests, timers, zones)
|
||||
ret
|
||||
Game_OnTick ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Game_OnPlayerInteract: Hook called when player presses [E] in range of an entity
|
||||
; Returns: EAX = 1 if handled by custom script, 0 if default engine behavior
|
||||
; ------------------------------------------------------------------------------
|
||||
Game_OnPlayerInteract PROC uses ebx ecx edx esi edi, entityIdx:DWORD, entityTypeVal:DWORD
|
||||
; Extensible hook for custom interactions
|
||||
xor eax, eax
|
||||
ret
|
||||
Game_OnPlayerInteract ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Game_OnEntityKilled: Hook called when enemy guard or barrel is destroyed
|
||||
; ------------------------------------------------------------------------------
|
||||
Game_OnEntityKilled PROC uses eax ebx ecx edx, entityIdx:DWORD, entityTypeVal:DWORD
|
||||
; Extensible hook for boss fights, doors opening on kill, score multipliers
|
||||
ret
|
||||
Game_OnEntityKilled ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Game_OnCustomHUD: Hook called during HUD rendering
|
||||
; ------------------------------------------------------------------------------
|
||||
Game_OnCustomHUD PROC uses eax ebx ecx edx
|
||||
; Extensible hook for custom UI bars, quest markers, mini-dialogues
|
||||
ret
|
||||
Game_OnCustomHUD ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; RenderGameHUDOverlays: Renders active toast banner, dialogue modal, and custom HUD
|
||||
; ------------------------------------------------------------------------------
|
||||
RenderGameHUDOverlays PROC
|
||||
call RenderGameToast
|
||||
call RenderGameDialog
|
||||
call Game_OnCustomHUD
|
||||
ret
|
||||
RenderGameHUDOverlays ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; CheckCmdFlag: Case-insensitive substring search (EAX = 1 if found, 0 if not)
|
||||
; ------------------------------------------------------------------------------
|
||||
CheckCmdFlag PROC uses esi edi ecx, pszCmd:DWORD, pszFlag:DWORD
|
||||
mov esi, pszCmd
|
||||
test esi, esi
|
||||
jz flag_not_found
|
||||
check_next_char:
|
||||
cmp byte ptr [esi], 0
|
||||
je flag_not_found
|
||||
mov edi, pszFlag
|
||||
mov ecx, esi
|
||||
compare_loop:
|
||||
mov al, byte ptr [edi]
|
||||
test al, al
|
||||
jz flag_found
|
||||
mov ah, byte ptr [ecx]
|
||||
cmp al, 'A'
|
||||
jl @F
|
||||
cmp al, 'Z'
|
||||
jg @F
|
||||
add al, 32
|
||||
@@:
|
||||
cmp ah, 'A'
|
||||
jl @F
|
||||
cmp ah, 'Z'
|
||||
jg @F
|
||||
add ah, 32
|
||||
@@:
|
||||
cmp al, ah
|
||||
jne mismatch
|
||||
inc edi
|
||||
inc ecx
|
||||
jmp compare_loop
|
||||
mismatch:
|
||||
inc esi
|
||||
jmp check_next_char
|
||||
flag_found:
|
||||
mov eax, 1
|
||||
ret
|
||||
flag_not_found:
|
||||
xor eax, eax
|
||||
ret
|
||||
CheckCmdFlag ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; LoadProjectSettings: Reads project.eblanproj if present in working directory
|
||||
; ------------------------------------------------------------------------------
|
||||
LoadProjectSettings PROC uses ebx ecx edx esi edi
|
||||
LOCAL hFile:DWORD, bytesRead:DWORD
|
||||
LOCAL projBuf[1024]:BYTE
|
||||
|
||||
invoke CreateFileA, OFFSET szProjectFile, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0
|
||||
cmp eax, INVALID_HANDLE_VALUE
|
||||
je proj_done
|
||||
mov hFile, eax
|
||||
|
||||
lea edx, projBuf
|
||||
invoke ReadFile, hFile, edx, 1023, ADDR bytesRead, 0
|
||||
invoke CloseHandle, hFile
|
||||
|
||||
cmp bytesRead, 0
|
||||
jle proj_done
|
||||
|
||||
mov eax, bytesRead
|
||||
mov byte ptr [projBuf + eax], 0
|
||||
|
||||
lea edx, projBuf
|
||||
invoke CheckCmdFlag, edx, OFFSET szProjGameMode0
|
||||
test eax, eax
|
||||
jnz set_adv_mode
|
||||
|
||||
lea edx, projBuf
|
||||
invoke CheckCmdFlag, edx, OFFSET szProjShowWeapons0
|
||||
test eax, eax
|
||||
jnz set_adv_mode
|
||||
|
||||
lea edx, projBuf
|
||||
invoke CheckCmdFlag, edx, OFFSET szProjGameMode1
|
||||
test eax, eax
|
||||
jnz set_act_mode
|
||||
jmp proj_done
|
||||
|
||||
set_adv_mode:
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
mov enableWeapons, 0
|
||||
mov playerWeapon, WEAPON_NONE
|
||||
mov showCrosshair, 0
|
||||
jmp proj_done
|
||||
|
||||
set_act_mode:
|
||||
mov gameMode, GAME_MODE_ACTION
|
||||
mov enableWeapons, 1
|
||||
mov playerWeapon, WEAPON_PISTOL
|
||||
mov showCrosshair, 1
|
||||
|
||||
proj_done:
|
||||
ret
|
||||
LoadProjectSettings ENDP
|
||||
|
|
@ -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:
|
||||
|
|
@ -526,7 +534,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
|
||||
|
|
@ -647,6 +655,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
|
||||
|
|
@ -660,6 +678,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
|
||||
|
|
@ -700,6 +725,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
|
||||
|
|
@ -903,6 +966,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:
|
||||
|
|
@ -971,6 +1035,8 @@ load_startup_map:
|
|||
jnz startup_map_ready
|
||||
call InitMap
|
||||
startup_map_ready:
|
||||
push eax
|
||||
call InitEntities
|
||||
pop eax
|
||||
ret
|
||||
LoadMapBinary ENDP
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ IsGameplayBlocked PROC
|
|||
or eax, isCutsceneActive
|
||||
or eax, cutscenePending
|
||||
or eax, fadeState
|
||||
or eax, isGamePaused
|
||||
or eax, gameDialogActive
|
||||
ret
|
||||
IsGameplayBlocked ENDP
|
||||
|
||||
|
|
@ -25,6 +27,10 @@ UpdatePlayer PROC uses ebx ecx edx esi edi
|
|||
LOCAL checkGridX:DWORD, checkGridY:DWORD
|
||||
|
||||
mov gameplayTickBlocked, 1
|
||||
cmp isGamePaused, 0
|
||||
jne update_player_done
|
||||
cmp gameDialogActive, 0
|
||||
jne update_player_done
|
||||
cmp isPlayerDead, 0
|
||||
jne update_player_done
|
||||
; Freeze player movement and inputs during level transitions
|
||||
|
|
@ -237,8 +243,9 @@ allow_portal_y:
|
|||
mov playerY, eax
|
||||
|
||||
update_player_done:
|
||||
cmp fadeState, 0
|
||||
jne @F
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
jnz @F
|
||||
call CheckPortalTriggers
|
||||
@@:
|
||||
ret
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
#include "stability_editor_checks.inl"
|
||||
|
||||
|
|
@ -139,6 +140,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,7 +128,9 @@ state_changed:
|
|||
StateUnchanged ENDP
|
||||
|
||||
include tests/cutscene_engine_checks.inc
|
||||
include tests/lighting_engine_checks.inc
|
||||
include tests/stability_engine_checks.inc
|
||||
include tests/integration_engine_checks.inc
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -363,6 +367,7 @@ flat_depth_ok:
|
|||
call Render3DScene
|
||||
invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE
|
||||
|
||||
call RunLightingChecks
|
||||
call RunCutsceneChecks
|
||||
call RunStabilityChecks
|
||||
mov eax, resultCount
|
||||
|
|
|
|||
85
tests/integration_engine_checks.inc
Normal file
85
tests/integration_engine_checks.inc
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
; Compatibility of stage-1 fixes with newer weapon/modal/lighting systems.
|
||||
.code
|
||||
RunIntegrationChecks PROC
|
||||
call ResetCombatFixture
|
||||
mov isGamePaused, 1
|
||||
mov keyForward, 1
|
||||
mov playerAttackCooldown, 4
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK playerX, 160 ; pause freezes player movement through production procedure
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; pause freezes guard AI through production procedure
|
||||
CHECK playerAttackCooldown, 4 ; pause freezes weapon cooldowns
|
||||
CHECK playerAmmo, 50 ; pause preserves ammunition
|
||||
mov isGamePaused, 0
|
||||
mov keyForward, 0
|
||||
call UpdatePlayer
|
||||
mov gameDialogActive, 1
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; modal dialogue freezes guards
|
||||
CHECK playerAmmo, 50 ; modal dialogue cannot fire
|
||||
mov gameDialogActive, 0
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; peaceful adventure mode cannot damage targets
|
||||
call ResetCombatFixture
|
||||
mov isPlayerDead, 1
|
||||
mov transitionCount, 1
|
||||
mov [transitionList].LevelTransition.triggerCellX, 2
|
||||
mov [transitionList].LevelTransition.triggerCellY, 2
|
||||
mov [transitionList].LevelTransition.fadeEffect, 1
|
||||
mov portalLatchCell, -1
|
||||
call UpdatePlayer
|
||||
CHECK fadeState, 0 ; dead player cannot activate a room transition
|
||||
|
||||
call ResetCombatFixture
|
||||
mov playerWeapon, WEAPON_KNIFE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; knife cannot hit beyond its world range
|
||||
CHECK playerAmmo, 50 ; knife consumes no ammunition
|
||||
mov playerAttackCooldown, 0
|
||||
mov [spriteList].SpriteStruct.x, 208
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 70 ; knife retains its close-range damage after merge
|
||||
call ResetCombatFixture
|
||||
mov playerWeapon, WEAPON_SHOTGUN
|
||||
mov [spriteList].SpriteStruct.y, 184
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 30 ; shotgun retains wider world-space hitbox and damage
|
||||
CHECK playerAmmo, 48 ; shotgun consumes two rounds
|
||||
mov eax, [lightSources+14*sizeof LightSource].LightSource.flags
|
||||
and eax, LIGHT_FLAG_ACTIVE
|
||||
CHECK eax, 1 ; shotgun spawns muzzle light in reserved slot
|
||||
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.textureId, 11
|
||||
mov [spriteList].SpriteStruct.flags, 06026400h
|
||||
call InitEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.textureId, 11 ; explicit Prop with barrel texture cannot explode
|
||||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_BARREL ; legacy barrel archetype matches editor fallback
|
||||
|
||||
; Real explosion with a nearby guard: CDQ must not destroy sprite pointer.
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.textureId, 11
|
||||
mov [spriteList].SpriteStruct.flags, ENTITY_TYPE_BARREL
|
||||
mov spriteCount, 2
|
||||
mov esi, OFFSET spriteList+sizeof SpriteStruct
|
||||
mov [esi].SpriteStruct.x, 352
|
||||
mov [esi].SpriteStruct.y, 160
|
||||
mov [esi].SpriteStruct.textureId, 13
|
||||
mov [esi].SpriteStruct.scale, FIXED_ONE
|
||||
mov [esi].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.textureId, 0 ; explicit barrel still explodes on hitscan
|
||||
CHECK entityHealth[4], 40 ; blast damages nearby guard without corrupting pointer
|
||||
mov eax, [lightSources+15*sizeof LightSource].LightSource.flags
|
||||
and eax, LIGHT_FLAG_ACTIVE
|
||||
CHECK eax, 1 ; explosion spawns light in reserved slot
|
||||
ret
|
||||
RunIntegrationChecks ENDP
|
||||
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
|
||||
|
|
@ -4,6 +4,11 @@ static void RunStabilityEditorChecks() {
|
|||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_GUARD, "zero flags retain legacy guard fallback");
|
||||
sprite.flags = 0x06026400;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop matches ASM despite guard texture");
|
||||
sprite.textureId = 11;
|
||||
sprite.flags = 0;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_BARREL, "zero flags retain new legacy barrel fallback");
|
||||
sprite.flags = 0x06026400;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop overrides barrel artwork");
|
||||
for (int health : {1, 100, 255, 256, 300, 500}) {
|
||||
SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_GUARD, health, 2, 6);
|
||||
Check(GetSpriteHealth(sprite) == (health > 255 ? 255 : health), "health setter clamps before byte packing");
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ ResetCombatFixture PROC uses eax ecx edi
|
|||
mov isCutsceneActive, 0
|
||||
mov cutscenePending, 0
|
||||
mov isPlayerDead, 0
|
||||
mov isGamePaused, 0
|
||||
mov gameDialogActive, 0
|
||||
mov gameMode, GAME_MODE_ACTION
|
||||
mov enableWeapons, 1
|
||||
mov playerWeapon, WEAPON_PISTOL
|
||||
mov playerHealth, 100
|
||||
mov playerAmmo, 50
|
||||
mov playerScore, 0
|
||||
|
|
@ -198,5 +203,6 @@ RunStabilityChecks PROC
|
|||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_GUARD ; zero flags keep legacy texture fallback
|
||||
call RunIntegrationChecks
|
||||
ret
|
||||
RunStabilityChecks 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)
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def main():
|
|||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + "".join(
|
||||
(ROOT / "tests" / name).read_text() for name in
|
||||
("cutscene_engine_checks.inc", "stability_engine_checks.inc"))
|
||||
("cutscene_engine_checks.inc", "lighting_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc"))
|
||||
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||
raw = (work / "engine_results.bin").read_bytes()
|
||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||
|
|
@ -176,10 +176,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