Compare commits

..

No commits in common. "main" and "stage-1-stability" have entirely different histories.

50 changed files with 941 additions and 6478 deletions

128
AGENTS.md
View file

@ -14,12 +14,11 @@
```
EBLAN_ENGINE/
│
├── main.asm # Entry point, Win32 window, input/tick/render loop
├── main.asm # Entry point, Win32 window, message loop, 60 FPS frame ticker
├── build.bat # 1-click build script (supports MASM32 and MSVC BuildTools)
├── engine.exe # Compiled game executable
│
├── include/
│ ├── engine_simulation.inc # Fixed 60 Hz QPC scheduler, states and tick commands
│ ├── engine_defs.inc # Screen resolution, colors, key codes, constants, structs
│ ├── engine_data.inc # Framebuffer, depthbuffer, player state, map array, textures
│ ├── engine_math.inc # FPU Sin/Cos table generator, RNG, fixed-point math
@ -53,12 +52,12 @@ build.bat
## Core Engine Architecture
### 1. Frame Lifecycle
Each message-loop pass polls input, pumps fixed simulation, then draws independently:
1. `PollInput`: samples held movement and latches pending command edges until consumed by one tick. Focus regain resamples physical latches without emitting actions.
2. `PumpSimulationClock` / `Engine_Tick`: QPC accumulator at exactly 60 Hz, up to eight catch-up ticks. Stalls >250 ms and pause/focus/modal/restart boundaries discard timing debt. Playing updates player, entities, combat, gameplay hook, lights and toast; other states freeze or update only their own systems. `UpdatePlayer` uses signed 16.16 runtime remainders, normalized diagonals and full-body unit-step wall-sliding collision. Portals remain walkable; occupied doors cannot close.
Each tick (~60 times per second), `main.asm` executes:
1. `PollInput`: queries keyboard state (`keyForward`, `keyBackward`, `keyStrafeLeft`, `keyStrafeRight`, `keyTurnLeft`, `keyTurnRight`, `keyAction`).
2. `UpdatePlayer`: applies camera turning, calculates forward/strafe movement vectors, runs wall-sliding collision against `gameMap`, checks door interaction.
3. `Render3DScene`:
- Clears ceiling (`COLOR_CEILING`) and floor (`COLOR_FLOOR`).
- Selects `viewX/Y/Angle` from player or separate `csCameraX/Y/Angle`, then casts 320 rays across FOV (~67.5°).
- Casts 320 rays across FOV (~67.5°).
- Traverses grid using DDA until a solid cell is hit.
- Computes perpendicular wall distance (fish-eye corrected).
- Samples 128x128 texture stripe, applies side shading (Y-walls darkened for 3D depth) and distance fog.
@ -67,11 +66,6 @@ Each message-loop pass polls input, pumps fixed simulation, then draws independe
4. `Engine_RenderHUD`: draws 2D overlay (title, FPS counter, minimap radar, controls).
5. `StretchDIBits`: copies `screenBuffer` (320x240 BGRA) to window DC (1280x960).
Drawing never advances gameplay timers, closes dialogs or switches rooms.
`UpdateScreenFade` advances transitions on ticks; `ApplyScreenFade` only darkens
pixels. State precedence: Unfocused, Paused, Dialog, Transition, Dead, Cutscene,
Playing. Detailed rules and regression coverage: `docs/STABILIZATION.md`.
---
## How to Extend This Engine (Guidelines for AI Agents)
@ -143,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): `5` (readers also accept v1 fixed sprite coordinates and v2/v3/v4 raw-coordinate maps, including early v2 maps with transitions)
- `version` (uint16): `4` (readers also accept v1 fixed sprite coordinates and v2/v3 raw-coordinate maps, including early v2 maps with transitions)
- `mapWidth` (uint16): `32`
- `mapHeight` (uint16): `32`
- `playerStartX` (uint16): cell X coordinate (e.g. 2)
@ -153,17 +147,16 @@ 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+)
- `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)
- `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
- **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 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).
- 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).
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,20 +195,14 @@ Run `python tools\test_regressions.py` on Windows with the build toolchains inst
MOVE and PAN interpolate linearly using signed integer math. Camera endpoints must be walkable; movement segments ignore collisions. Each tick advances existing tweens, expires text and waits, then dispatches due commands in stored order. Same-timestamp commands can start independent movement, pan and fade together. WAIT delays overdue commands without shifting their timestamps. A replacement tween starts from the current pose.
Autoplay begins after a room transition finishes. C starts/replays the current map's track; Tab skips it. Movement/actions are blocked during playback. The camera owns `csCameraX/Y/Angle`, leaving the gameplay body stationary; lighting and flashlight follow the selected view. End/skip/level changes stop cinematic sound and clear subtitles/fade. END/Tab transfer the camera pose only if the entire player collision body is walkable; otherwise retain the previous safe player pose. Letterbox is 2.35:1, subtitles use up to 3 centered rows of 38 glyphs over a half-brightness panel. WAV names are ASCII basenames of up to 63 characters in `assets/sounds`; playback uses one asynchronous PlaySound channel.
`RestartCurrentRoom` is shared by death R and pause item 4. It reloads the current
room with the actual entry spawn, then clears gameplay transients and timing debt
only after success. Failed reload preserves the room, death/pause and statistics.
Genre, weapon/HUD configuration and selected weapon are preserved; authored room
lighting is reloaded. EBLN coordinates and IPC layouts are unchanged.
Autoplay begins after a room transition finishes. C starts/replays the current map's track; Tab skips it. Movement/actions are blocked during playback. End/skip/level changes stop cinematic sound, clear subtitles and fade, and return control at the current camera pose. Letterbox is 2.35:1, subtitles use up to 3 centered rows of 38 glyphs over a half-brightness panel. WAV names are ASCII basenames of up to 63 characters in `assets/sounds`; playback uses one asynchronous PlaySound channel.
F6 opens EblanStudio's timeline. Author commands with buttons, drag diamonds to change timestamps, select overlapping events through the command list, and place MOVE points by clicking the map. The inspector edits parameters and strings. Autoplay, scene data and strings participate in save/load and undo/redo. Preview shows the actual game perspective in the embedded viewport with the camera path on the map; F5 runs the full game, validates referenced WAV files and stages them with linked maps. Editor preview stops at a level-change command. F5/F6 are handled before ImGui keyboard-capture checks.
`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,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 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 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.
@ -231,95 +218,22 @@ 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 & 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.
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.
- Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`.
- 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.
- Comprehensive combat HUD: 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`, `Ctrl+B`, or the main menu.
- Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7` 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.).
- **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).
- Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Custom HUD).
- Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines.
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.
4. **Project Settings & Standalone Game Exporter (`editor/src/project_manager.h`)**:
- 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)
Stage-3 authored history and interaction rules are documented in
`docs/STABILIZATION.md`. Level Undo uses nested `LevelEdit` scopes around UI;
do not add a second snapshot inside a scope. Sprite switch flags opt into a door
binding only when `(flags & 0xFEFF) == 0xA805`: byte 1 is A8 (switch-only) or A9
(key-unlockable), bytes 2/3 are door X/Y. One switch per door; validate both C++ and
ASM loaders. No EBLN/IPC size change. Runtime unlocks reset in `InitEntities`.
F focuses selection, G fills; Ctrl+Space opens Content Drawer. External asset and
project-file writes are outside level Undo. Dialog text remains code-authored.
After the v4 cutscene strings, v5 appends `LightingSettings` (32 bytes), `lightCount * 40` bytes of `LightSource`, and 1024 DWORD baked RGB cells (`00RRGGBB`, 4096 bytes). Settings are eight DWORDs: mode (0..2), ambient (0..255), flashlight (0/1), flashlight radius (32..2048), intensity (0..255), half cone angle (1..96 angle units), baked-valid (1), reserved (0). Source layouts match `EditorLightSource` and MASM `LightSource`. Counts/settings/coordinates/radii/intensities/flags/colors and exact file size are validated before committing a room. v1..v4 load no authored lights and mode NONE; the old reserved header bytes are ignored.
Sources and baked data are map-local and travel with save/load, undo/redo, F5 and standalone exports. Static lighting is baked in the editor with BakeLightingRGB and in ASM with BakeMapLighting; all 1024 RGB cells are regression-compared. Realtime point/pulse/flicker lights use only the live source array on top of the persisted static map. Shadow flags are respected. Slot 0 belongs to the player flashlight, 1..13 to authored sources, 14 to muzzle flare, 15 to explosions; combat never overwrites authored lights. Both wall stripes and sprites receive RGB coefficients. Flat ceiling/floor colors use a camera-cell light sample, not per-pixel floor projection.
Editor light settings are in the property inspector and toolbar. A stale-bake indicator tracks static sources, geometry and ambient, excluding realtime movement. Saving or F5 automatically rebuilds a stale static bake; runtime and preview consume persisted RGB without baking each frame. The IPC v2 header includes flashlight parameters and a deterministic light tick; sources begin at offset 1,357,792 and baked RGB at 1,358,312. Update the C++, ASM and Python layouts together.
Lighting verification (24.10.2026): 117 editor checks and 131 ASM checks passed; all 1024 RGB bake cells match, and both lit and classic viewport framebuffers match direct ASM rendering. Project assets were verified unchanged.

View file

@ -112,7 +112,7 @@ build_editor.bat
## Переходы и проверка первых трёх этапов
Карты новых сохранений используют EBLN v5; существующие v1/v2/v3/v4 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
Карты новых сохранений используют EBLN v4; существующие v1/v2/v3 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
Порталы имеют отдельную текстуру и не занимают слот фонаря. Первый импорт изображения создаёт недостающие папки и пакет со стандартными текстурами в остальных слотах. Повреждённый пакет не перезаписывается импортом; ошибка показывается в строке состояния. Неудачный переход сохраняет текущую комнату и показывает сообщение в игре.
@ -134,16 +134,4 @@ python tools\test_regressions.py
Предпросмотр останавливается на команде перехода в другую комнату; цепочка комнат и игровой ввод проверяются через F5. F5/F6 работают при фокусе интерфейса и текстовых полей. Ошибка запуска показывается отдельным сообщением. При старте редактор открывает карту проекта относительно своего файла, поэтому запуск из другой рабочей папки не ломает пути.
Формат 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. Старые карты сохраняют прежний вид без освещения, пока вы не добавите источники и не включите нужный режим. Свет каждой комнаты сохраняется независимо. Перед экспортом игры сохраните карты.
Формат v4 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`.

View file

@ -24,55 +24,6 @@
---
### Симуляция и движение (этап 2)
`include/engine_simulation.inc` отделяет опрос ввода, фиксированные тики 60 Гц и
рисование. QPC-аккумулятор хранит `elapsedCounts * 60`; на тик вычитается частота
QPC без округления периода. За проход допускается до восьми тиков. Задержка более
250 мс, обратный отсчёт QPC, смена фокуса, пауза/возобновление, открытие/закрытие
диалога и успешный рестарт сбрасывают накопленный долг. Рендер не интерполируется
и не меняет скорость игрового времени.
`PollInput` сохраняет удерживаемые оси и накапливает фронты команд до следующего
тика; `ConsumeInputEdges` потребляет их один раз, включая заблокированные тики.
Возврат фокуса синхронизирует физические защёлки без новых команд: удерживаемое
движение возобновляется, действие требует нового нажатия.
`playerX/Y` остаются целыми мировыми координатами. `playerFractionX/Y` хранят
знаковые остатки 16.16 только в runtime; деление округляется к нулю симметрично
для всех направлений. Противоположные оси сокращаются; диагональ нормализуется
множителем 46341/65536. Скорость ходьбы/бега — 3/6 единиц за тик. Столкновения
проверяют все клетки квадратного тела `position ± playerRadius`, по единичным
подшагам каждой оси с проскальзыванием (до 64 подшагов за тик). Заблокированная
ось теряет дробный остаток. Порталы проходимы; дверь не закрывается поверх тела.
Приоритет состояний: Unfocused → Paused → Dialog → Transition → Dead → Cutscene →
Playing. Playing обновляет игрока, сущности, бой, `Game_OnTick`, свет и toast;
между подсистемами повторно проверяется общий запрет gameplay. Cutscene обновляет
трек/камеру и свет; Transition — только затемнение/загрузку. Пауза, диалог, смерть
и потеря фокуса замораживают мировые таймеры. `Engine_Frame` только рисует и
представляет кадр; `UpdateScreenFade` обновляет переход, `ApplyScreenFade` меняет
только пиксели. Скриншот и счётчик FPS относятся к представлению.
### Авторские транзакции и взаимодействия (этап 3)
`EditorApp::LevelEdit` снимает полный snapshot вокруг UI-редактирования. Сравнение
авторских данных исключает пустые действия; ActiveId виджета/жест холста объединяет
непрерывный ввод, включая запекание на отпускании мыши. Undo/Redo ограничены 64
снимками; успешная загрузка очищает историю. Snapshot включает свет, baked RGB,
кат-сцены, переходы, спавн, сущности и палитру, но не внешние файлы ассетов/проекта.
Настоящие ImGui-поля проверяются в `tests/studio_editor_checks.inl`.
Опциональная связь рычага с дверью помещается в существующий `SpriteStruct.flags`:
тип 5 в младшем байте, `0xA8`/`0xA9` во втором байте, X/Y двери в старших байтах.
`0xA9` разрешает также открыть дверь ключом: первый ключ расходуется и сохраняет
runtime-флаг отпирания до переинициализации комнаты. Без связи обычные двери
бесплатны; старые рычаги вызывают прежний hook/toggle. Оба загрузчика валидируют
целевую дверь, границы и уникальность связи. `ToggleDoorCell` не закрывает дверь
поверх тела игрока ни при прямом E, ни через рычаг. Прямое взаимодействие с дверью
потребляет E до обработки ближайших рычагов. Бинарные размеры EBLN v5 и IPC v2
не изменились. Подробный контракт и ограничения: `STABILIZATION.md`, этап 3.
## 2. Конвейер лучевого рендеринга (2.5D Raycasting)
Каждый кадр для экрана шириной 320 пикселей выпускается ровно **320 лучей** (по 1 лучу на каждый вертикальный столбец экрана):
@ -144,7 +95,7 @@ runtime-флаг отпирания до переинициализации ко
## 5. Загрузка карт и ресурсов
Новые карты сохраняются как EBLN v5: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3/v4 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
Новые карты сохраняются как EBLN v4: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
Файлы связанных подуровней лежат рядом друг с другом. В `targetMap[32]` хранится только ASCII-имя `.dat` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия.
@ -162,25 +113,14 @@ F5 запускает копию графа карт и ресурсов во в
`UpdatePlayer` обрабатывает автозапуск после завершения межкомнатного затемнения и ручной запуск клавишей C. При активной сцене управление движением, дверями и обычными порталами заблокировано. UpdateCutscene сначала обновляет уже начатые интерполяции и время показа текста, затем обрабатывает паузу и наступившие команды. Несколько команд с одним временем выполняются в порядке хранения: камера, поворот, текст, звук и затемнение могут работать одновременно. WAIT задерживает обработку следующих команд, но время и уже начатые интерполяции продолжаются.
Координаты камеры — сырые мировые единицы (64 на клетку), отдельные `csCameraX/Y/Angle`, не позиция игрока. Рендер выбирает `viewX/Y/Angle`; фонарик и выборки света следуют камере, как в предпросмотре. MOVE_CAM_TO использует линейную интерполяцию по длительности из `dataId`; PAN_CAM_TO — кратчайшую дугу угла 0..511 и длительность из `paramB`. Целочисленное умножение с последующим знаковым делением гарантирует точную конечную точку. Нулевая длительность означает мгновенную установку. Новая команда того же типа заменяет незавершённую интерполяцию, начиная с текущего состояния. Конечная клетка должна быть проходимой; сами отрезки не проверяют столкновения со стенами.
Координаты камеры — сырые мировые единицы (64 на клетку). MOVE_CAM_TO использует линейную интерполяцию по длительности из `dataId`; PAN_CAM_TO — кратчайшую дугу угла 0..511 и длительность из `paramB`. Целочисленное умножение с последующим знаковым делением гарантирует точную конечную точку. Нулевая длительность означает мгновенную установку. Новая команда того же типа заменяет незавершённую интерполяцию, начиная с текущего состояния. Конечная клетка должна быть проходимой; сами отрезки не проверяют столкновения со стенами.
FADE_SCREEN интерполирует темноту от 0 до 16 и масштабирует каждый RGB-канал. Во время сцены HUD заменён чёрными полосами по 52 пикселя (видимая область 320×136, примерно 2.35:1). Субтитры центрируются на полупрозрачной плашке: до 3 строк по 38 глифов 8×8, перенос по ширине или LF. Полный текст ограничен 95 символами CP1251; лишние экранные строки не выводятся. SHOW_SUBTITLE с нулевой длительностью остаётся до HIDE_SUBTITLE или завершения сцены.
PLAY_SOUND использует `PlaySoundA` из уже подключённой WinMM, асинхронно и без системного звука при ошибке. Строка содержит только ASCII-имя WAV длиной до 63 символов, файл лежит в `assets/sounds`. Бит 0 `param8` включает повтор, индекс -1 останавливает звук. Канал один; новый WAV заменяет предыдущий. END, Tab и переход прекращают звук и снимают наложения. При END/Tab камера передаёт позицию и угол игроку только если всё collision body свободно; иначе сохраняется прежняя безопасная поза игрока. Дробные остатки сбрасываются; защита портала предотвращает немедленный обратный переход из текущей клетки.
PLAY_SOUND использует `PlaySoundA` из уже подключённой WinMM, асинхронно и без системного звука при ошибке. Строка содержит только ASCII-имя WAV длиной до 63 символов, файл лежит в `assets/sounds`. Бит 0 `param8` включает повтор, индекс -1 останавливает звук. Канал один; новый WAV заменяет предыдущий. END, Tab и переход прекращают звук и снимают наложения. Положение камеры сохраняется при возврате управления; защита портала предотвращает немедленный обратный переход из текущей клетки.
CHANGE_SUBLEVEL ссылается на существующий переход по индексу и использует его имя карты, спавн, угол и настройку затемнения. Старый трек останавливается до загрузки новой комнаты. Уже достигнутая темнота сохраняется при начале затемнения перехода, чтобы избежать светлой вспышки. Новая комната может запустить собственную сцену после появления.
### Рестарт текущей комнаты
`RestartCurrentRoom` используется смертью + R и пунктом 4 паузы. Запоминаются
фактический путь комнаты и entry spawn, включая override перехода. Сначала
выполняется транзакционная загрузка; лишь после успеха сбрасываются сущности,
HP/патроны, ключи/очки, боевые таймеры/вспышки, диалог/toast, затемнение, ожидающий
переход и дробные остатки. Данные комнаты и авторский свет восстанавливаются из
файла; жанр, настройка оружия/прицела, HUD и выбранное оружие сохраняются. Ошибка
не заменяет комнату процедурной и не оживляет игрока; пауза остаётся. Процедурный
стартовый fallback перезапускается процедурно.
## 7. Таймлайн EblanStudio
F6 или кнопка панели открывает таймлайн (`editor/src/editor_cutscene.inl`). Дорожки камеры, текста, звука и сцены показывают метки времени и перетаскиваемые ромбы. Команды с одинаковым временем можно выбрать через список «Команда». Инспектор редактирует время, длительность, координаты, углы, строки, WAV, повтор и целевой портал. Для камеры кнопка «Указать на карте» позволяет выбрать свободную клетку, не меняя сетку. END добавляется автоматически и переносится за последнее действие. Удаление портала удаляет или перенумеровывает ссылки сцены.
@ -202,17 +142,3 @@ 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.

View file

@ -49,188 +49,19 @@ 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
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.
## Stage 2 — predictable simulation
Implemented without changing EBLN v1..v5, sprite coordinates or preview IPC v2:
- `include/engine_simulation.inc` owns QPC scheduling and `Engine_Tick`. The
accumulator stores elapsed counts multiplied by 60, avoiding rounded tick
periods. Each pump runs at most eight ticks; stalls over 250 ms, backwards
counter samples, pause/resume, focus changes, modal boundaries and successful
restarts discard timing debt. Drawing runs independently, without interpolation.
Under overload the engine drops debt rather than fast-forwarding.
- `PollInput` records held movement and pending command edges. An edge survives
render-only frames and is consumed after one simulation tick, including blocked
ticks. On focus regain physical latches are resampled and new edges discarded;
movement may resume while held, but actions require release and a fresh press.
F12 remains a presentation-only screenshot request.
- Movement uses signed 16.16 runtime remainders with truncation toward zero.
Opposing axes cancel before rounding; combined forward/strafe uses 46341/65536
normalization. Collision checks the axis-aligned body against every overlapped
cell, stepping one world unit per axis with wall sliding (maximum 64 substeps
per tick). Blocked axes discard their remainder. Portals remain walkable; open
doors cannot close over the body. Fractions reset on room load and camera handoff.
- State precedence is Unfocused, Paused, Dialog, Transition, Dead, Cutscene,
Playing. Playing updates player, entities, combat, `Game_OnTick`, lights and
toast lifetime in that order, rechecking the shared gameplay gate between
subsystems and entities. Cutscene updates camera/track and lights only;
transition advances only its fade/load sequencer. Death accepts R; pause accepts
menu commands; dialog accepts E/Space, Enter or attack dismissal. Other world
timers freeze in those states. END/skip and transition completion never resume
gameplay partway through the same tick.
- `Engine_Frame` and HUD/overlay procedures do not update gameplay timers, dismiss
dialogs or load rooms. `UpdateScreenFade` is the tick sequencer;
`ApplyScreenFade` is only a framebuffer operation.
- Cinematics own `csCameraX/Y/Angle`; rendering selects `viewX/Y/Angle` without
moving the player's body. Wall samples and camera-attached flashlight use the
cinematic view, matching the editor preview. Paths may cross walls. END/Tab
transfers position and angle only if the entire body is walkable at the camera
pose; otherwise the previous safe player pose remains. No path search or
collision correction is attempted. Existing sound/overlay cleanup remains.
- `RestartCurrentRoom` reloads the remembered room path with its actual entry
spawn (including transition overrides), then resets entities, health/ammo,
keys/score, combat flashes/cooldown, dialog/toast, fades, pending transition and
movement fractions. Authored lighting and original room data are reloaded;
game genre, weapon/HUD configuration and selected weapon remain unchanged.
Procedural startup fallback restarts procedurally. Failed file reload leaves
the active room and gameplay intact; it does not revive a dead player or resume
a paused game. Death R and pause-menu item 4 use this same procedure.
Verification commands are the same as stage 1. New production ASM checks cover
different render schedules, catch-up limits and 64-bit QPC arithmetic, pending
edges and focus regain, pause/modal timing, all 512 movement directions, diagonal
normalization, sprint, body corners/sliding/doors, high-speed wall collision,
wall-crossing cutscene skip/END, lit camera framebuffer parity, both restart entry
points and failed-reload rollback. The runner's lighting/cutscene labels now follow
the actual ASM call order rather than mislabelling failures. Interactive keyboard
and UI playtesting remains separate; no UI redesign is included in this stage.
Verification (2026-10-31): production engine and editor builds succeeded; editor
**137/137**, ASM **288/288**. All 320 projection angles, frame-by-frame C++/ASM
cinematic traces and 1024 baked RGB cells agree. Classic/lit IPC v2 frames match
direct ASM rendering through the x86/x64/DX11 path. Project assets remain
byte-for-byte unchanged. No manual interactive playtest was performed.
## Stage 3 — authored history, interactions and studio UI
Implemented:
- Nested `LevelEdit` scopes compare complete level snapshots around production UI
edits. Inspector transforms, sprite texture/type/AI parameters, spawn/angle,
palette/presets, portal filenames/destinations/fade, light settings/sources and
cinematic events/strings participate in Undo/Redo. Existing explicit `PushUndo`
calls are suppressed inside these scopes, avoiding duplicate entries. A stable
active widget or canvas stroke groups continuous edits; release-time baking and
timeline normalization belong to the same gesture. Selection/view changes and
clicks without authored changes create no history. New edits clear Redo; history
is limited to 64 snapshots. Successful loads clear history, failed loads preserve
it; creating a default level is undoable. Undo/Redo clear active widgets, drag
state and selection, restore RGB bake/signature and rescrub cinematic preview.
- Sprite drags clamp to map bounds. Inspector duplication returns immediately,
avoiding references invalidated by vector growth. Releasing a light drag outside
the canvas clears its radius/position drag state.
- A switch can opt into one authored door target without changing EBLN v5 or IPC
v2 layouts. Flags low byte is 5; byte 1 is `0xA8` (switch-only door) or `0xA9`
(also unlockable with a key), bytes 2/3 are door cell X/Y. Other switch flags
retain legacy hook/toggle behavior. Each door has at most one bound switch;
C++ and ASM loaders reject non-door targets, out-of-bounds targets and duplicate
bindings before replacing the room. Painting away a bound door leaves a visible
inspector error and blocks save/F5 until the target is repaired or unbound.
- Ordinary doors remain free. Bound doors require their switch, or one collected
key if explicitly enabled. First direct unlock consumes one key and persists
until room reinitialization; subsequent opens/closes are free. Switch use itself
never consumes a key. `ToggleDoorCell` shares full-player-body closure protection
between direct and remote interactions. An occupied/invalid target does not
change switch state. `E` prioritizes the facing door/portal, then the first nearby
switch in authored order, consuming the action once. Custom switch hooks run
before default door behavior and may handle the action themselves. This stage
does not add generic proximity dialogs or switch line-of-sight tests.
- Removed fake per-entity dialogue fields and the nonfunctional insertion promise.
UI explicitly identifies dialogue as an `engine_gameplay.inc` code hook, not map
data. Texture/audio import and project settings remain external file operations,
not part of level Undo; IDE text history is separate.
- UE5-inspired studio uses charcoal surfaces, compact square panels, Outliner and
Details headers, a two-row horizontally scrollable toolbar, resizable views and
a bottom Content Drawer (`Ctrl+Space`). Minimum desktop size is 1000x680; narrow
mode shortens viewport tabs. No docking framework or alternative renderer was
introduced. Workspace geometry ignores stale ImGui layout entries; no existing
`imgui.ini` is rewritten by the implementation. F5/F6/F7 remain; F focuses, G
fills, resolving the previous F conflict. Runtime pause menu uses the same
restrained charcoal/blue visual language with five clearly separated rows.
Verification (2026-10-31): production builds succeed; editor **162/162**, ASM
**307/307**. New checks exercise actual ImGui spawn/sprite/portal inputs and palette
preset buttons, history grouping/branching/capacity, workspace/content drawer at
1000/1360/1920 widths, real C++/ASM binding validation, key consumption, legacy
compatibility, single-action priority and remotely occupied doors. All 320 ray
angles, cinematic traces, 1024 RGB bake cells and classic/lit IPC frames still
agree. Project assets remain byte-for-byte unchanged. No manual visual/keyboard
playtest was performed; automated UI checks do not replace it.
## Remaining stages
2. Predictable simulation: fixed 60 Hz ticks, fractional movement, normalized
input, complete collision shape, current-room restart/reset policy. Extend
gameplay gating into explicit states and separate cinematic camera ownership.
3. Honest editor/gameplay: complete undo, keys/switches and supported interactions;
UE5-inspired editor/runtime UI redesign.
4. Project lifecycle and validated, safe standalone export.
5. Measurements and targeted renderer/editor optimizations.
6. Finished multi-room demo game and release verification.
7. Data-driven extensions and subsequent rendering features.
Stages 1–3 do not claim that all audit findings are fixed: AI visibility/melee
through walls and exporter safety remain open. Existing map and texture assets
are not migrated or rewritten.
Stage 1 intentionally does not claim that all audit findings are fixed: restart
semantics, fixed timing, collision/AI visibility, full undo and exporter safety
remain assigned to subsequent stages. Existing map and texture assets are not
migrated or rewritten.

Binary file not shown.

View file

@ -1,40 +1,25 @@
[Window][Workspace]
Pos=0,64
Size=2560,1288
LastUsed=20261024
Pos=0,60
Size=1536,708
LastUsed=20261022
[Window][Debug##Default]
Pos=60,60
Size=400,400
LastUsed=20261024
LastUsed=20261022
[Window][Toolbar]
Pos=0,20
Size=2560,42
LastUsed=20261024
Size=1536,38
LastUsed=20261022
[Window][StatusBar]
Pos=0,1352
Size=2560,32
LastUsed=20261024
Pos=0,768
Size=1536,32
LastUsed=20261022
[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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,6 @@
#include "level_format.h"
#include "cutscene_preview.h"
#include "preview_protocol.h"
#include "lighting_format.h"
#include <memory>
#include <string>
#include <vector>
@ -23,18 +22,7 @@ enum ToolMode {
TOOL_ERASER,
TOOL_FILL,
TOOL_PLAYER_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
TOOL_SPRITE_SPAWN
};
// Full level snapshot for undo/redo (grid + spawn + palette + sprites + transitions)
@ -45,10 +33,6 @@ 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 {
@ -83,24 +67,15 @@ 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;
bool showProjectSettingsModal = false;
bool showExportGameModal = false;
bool contentDrawerOpen = false;
std::unique_ptr<EblanProject> project;
std::string exportStatusMessage;
@ -121,43 +96,12 @@ 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;
@ -168,20 +112,6 @@ public:
struct ID3D11ShaderResourceView* textureSRVs[16] = { nullptr };
private:
// Nested UI scopes capture authored data, never selection/view preferences.
struct LevelEdit {
EditorApp& app;
explicit LevelEdit(EditorApp& owner) : app(owner) { app.BeginLevelEdit(); }
~LevelEdit() { app.EndLevelEdit(); }
};
void BeginLevelEdit();
void EndLevelEdit();
void RecordLevelEdit(const LevelSnapshot& before, unsigned gesture);
static bool SameLevel(const LevelSnapshot& a, const LevelSnapshot& b);
unsigned editDepth = 0, editEpoch = 0, scopeEpoch = 0, undoGesture = 0;
LevelSnapshot editBefore;
void RenderLightingInspector();
void RenderLightingSettings();
bool EnsureEnginePreview();
void StopEnginePreview();
void UpdateEnginePreview();

View file

@ -6,7 +6,6 @@
#include <string>
#include <filesystem>
#include <cctype>
#include <cstring>
#include <windows.h>
#include "../imgui/imgui.h"
@ -18,19 +17,14 @@ struct CodeFileEntry {
};
static const CodeFileEntry g_ProjectCodeFiles[] = {
{ "engine_light.inc (Light Sources & Bursts)", "include/engine_light.inc" },
{ "engine_lighting_core.inc (Static RGB Bake & Dynamic Lighting)", "include/engine_lighting_core.inc" },
{ "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" },
{ "engine_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_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_defs.inc (Constants & Structs)", "include/engine_defs.inc" },
{ "engine_render2d.inc (2D Canvas & HUD)", "include/engine_render2d.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" }
{ "engine_input.inc (Keyboard Input)", "include/engine_input.inc" },
{ "engine_physics.inc (Player Physics)", "include/engine_physics.inc" }
};
static const int g_CodeFileCount = sizeof(g_ProjectCodeFiles) / sizeof(g_ProjectCodeFiles[0]);
@ -40,39 +34,6 @@ 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"
@ -117,15 +78,6 @@ 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"
@ -138,42 +90,6 @@ 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;
@ -190,10 +106,6 @@ 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;
@ -281,9 +193,8 @@ struct CodeEditorState {
buildLog.clear();
buildErrors.clear();
hasBuildResult = false;
buildExitCode = 0;
fs::path root = fs::path(projectRoot);
fs::path root(projectRoot);
fs::path buildBat = root / "build.bat";
if (!fs::exists(buildBat)) {
buildBat = root.parent_path() / "build.bat";
@ -291,7 +202,7 @@ struct CodeEditorState {
}
if (!fs::exists(buildBat)) {
buildLog = "[ERROR] Не найден скрипт сборки build.bat в: " + buildBat.string() + "\n";
buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n";
hasBuildResult = true;
buildExitCode = 1;
return;
@ -408,13 +319,14 @@ 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;
}
@ -428,23 +340,25 @@ void EditorApp::RenderCodeEditorWindow() {
g_CodeEditor.PollBuild();
if (!showCodeEditorModal) return;
ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
ImGui::End();
return;
}
char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
// Menu Bar
// Top 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 / Ctrl+B)", "Ctrl+B")) {
if (ImGui::MenuItem("Собрать движок (F7)", "F7")) {
char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
g_CodeEditor.StartBuild(root);
}
ImGui::Separator();
@ -457,6 +371,7 @@ 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);
@ -471,15 +386,12 @@ void EditorApp::RenderCodeEditorWindow() {
ImGui::EndMenuBar();
}
RenderCodeEditorContents(root);
ImGui::End();
}
void EditorApp::RenderCodeEditorContents(const std::string& root) {
g_CodeEditor.PollBuild();
// Top Toolbar
// Toolbar Buttons
ImGui::BeginGroup();
char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
std::string root = fs::path(module).parent_path().string();
if (ImGui::Button("Сохранить (Ctrl+S)")) {
g_CodeEditor.SaveFile(root);
}
@ -489,110 +401,51 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
ImGui::Button("Компиляция...");
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.55f, 0.22f, 1.0f));
if (ImGui::Button("Собрать проект (F7)")) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.2f, 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 (Navigator & API Catalog) and Right (Editor + Output)
// Split Left (File list) and Right (Editor + Output)
ImGui::Columns(2, "CodeEditorColumns", true);
static bool initCol = true;
if (initCol) {
ImGui::SetColumnWidth(0, 270);
ImGui::SetColumnWidth(0, 240);
initCol = false;
// Load initial file
g_CodeEditor.LoadFile(0, 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);
}
// 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);
}
ImGui::EndTabItem();
}
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::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;
}
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
@ -600,6 +453,7 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
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 ? "*" : "");
@ -616,10 +470,10 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
}
ImGui::PopStyleColor();
// 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);
// Hotkey: Ctrl+S to save
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S)) {
g_CodeEditor.SaveFile(root);
}
ImGui::Separator();
@ -629,8 +483,9 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
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);
@ -670,6 +525,7 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
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) {
@ -686,4 +542,5 @@ void EditorApp::RenderCodeEditorContents(const std::string& root) {
ImGui::EndTabBar();
ImGui::Columns(1);
ImGui::End();
}

View file

@ -1,268 +0,0 @@
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(); return; }
if (ImGui::Button("К сетке (Snap)", ImVec2(colW2, 24))) { SnapLightToGrid(selectedLightIdx); }
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.15f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.20f, 0.20f, 1.0f));
if (ImGui::Button("Удалить (Del)", ImVec2(colW2, 24))) {
ImGui::PopStyleColor(2);
DeleteSelectedEntity();
return;
}
ImGui::PopStyleColor(2);
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f), "Быстрые пресеты источников:");
// Preset Buttons: Clean 3-Column Grid (all 7 presets visible!)
float colW3 = (ImGui::GetContentRegionAvail().x - 12.0f) / 3.0f;
if (ImGui::Button("Факел", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TORCH);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Факел: Тёплый огонь (оранжевый, 240px, мерцание, тени)");
ImGui::SameLine();
if (ImGui::Button("Лампа", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_LAMP);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Лампа: Запекаемый дневной свет (280px, статика)");
ImGui::SameLine();
if (ImGui::Button("Сирена", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_ALARM);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Сирена: Аварийный маяк (красный, 300px, пульсация)");
if (ImGui::Button("Свеча", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_CANDLE);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Свеча: Мягкий тёплый огонёк (140px, мерцание)");
ImGui::SameLine();
if (ImGui::Button("Неон", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_NEON);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Неон: Циановый неоновый свет (260px, динамика)");
ImGui::SameLine();
if (ImGui::Button("Токсик", ImVec2(colW3, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TOXIC);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Токсик: Зелёное радиационное свечение (250px, пульс)");
if (ImGui::Button("Прожектор (Направленный)", ImVec2(-1, 24))) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_SPOTLIGHT);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Прожектор: Мощный белый свет (380px, статика)");
ImGui::Separator();
EditorLightSource edited = light;
bool changed = false, activated = false;
auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); };
// Toggle Active
bool active = (edited.flags & LIGHT_FLAG_ACTIVE) != 0;
changed |= ImGui::Checkbox("Включён на карте", &active); undoItem();
edited.flags = (edited.flags & ~LIGHT_FLAG_ACTIVE) | (active ? LIGHT_FLAG_ACTIVE : 0);
// Coordinates with Cell info
int curCellX = edited.x / 64;
int curCellY = edited.y / 64;
ImGui::Text("Клетка: [%d, %d] | Мировые координаты: (%d, %d)", curCellX, curCellY, edited.x, edited.y);
float xCells = edited.x / 64.0f;
float yCells = edited.y / 64.0f;
if (ImGui::SliderFloat("Позиция X (клетки)", &xCells, 0.5f, 31.5f, "%.2f")) {
edited.x = std::clamp(static_cast<int>(xCells * 64.0f), 32, (MAP_GRID_W - 1) * 64);
changed = true;
} undoItem();
if (ImGui::SliderFloat("Позиция Y (клетки)", &yCells, 0.5f, 31.5f, "%.2f")) {
edited.y = std::clamp(static_cast<int>(yCells * 64.0f), 32, (MAP_GRID_H - 1) * 64);
changed = true;
} undoItem();
// Radius & Intensity
changed |= ImGui::SliderInt("Радиус (px)", &edited.radius, 32, 2048, "%d px"); undoItem();
changed |= ImGui::SliderInt("Яркость", &edited.intensity, 0, 255, "%d / 255"); undoItem();
edited.curIntensity = edited.intensity;
// Color Picker & Quick Swatches
float color[] = { ((edited.color >> 16) & 255) / 255.f, ((edited.color >> 8) & 255) / 255.f, (edited.color & 255) / 255.f };
if (ImGui::ColorEdit3("Цвет света", color)) {
edited.color = (uint32_t(color[0] * 255) << 16) | (uint32_t(color[1] * 255) << 8) | uint32_t(color[2] * 255);
changed = true;
} undoItem();
// Quick Color Swatches
ImGui::TextDisabled("Оттенки:");
ImGui::SameLine();
auto ColorSwatch = [&](const char* label, uint32_t hexColor) {
float c[3] = { ((hexColor >> 16) & 255) / 255.f, ((hexColor >> 8) & 255) / 255.f, (hexColor & 255) / 255.f };
if (ImGui::ColorButton(label, ImVec4(c[0], c[1], c[2], 1.0f), 0, ImVec2(18, 18))) {
edited.color = hexColor;
changed = true;
}
ImGui::SameLine();
};
ColorSwatch("##warm_fire", 0x00FFB040);
ColorSwatch("##cool_white", 0x00E0E8FF);
ColorSwatch("##neon_cyan", 0x0000E5FF);
ColorSwatch("##ruby_red", 0x00FF2020);
ColorSwatch("##toxic_green", 0x0039FF14);
ColorSwatch("##pure_white", 0x00FFFFFF);
ImGui::NewLine();
// Light Type
const char* types[] = { "Постоянный (запекание в lightmap)", "Точечный (динамический реалтайм)", "Пульсирующий (динамическая сирена)" };
int type = (edited.lightType == LIGHT_TYPE_STATIC) ? 0 : (edited.lightType == LIGHT_TYPE_PULSE) ? 2 : 1;
if (ImGui::Combo("Тип источника", &type, types, 3)) {
edited.lightType = (type == 0) ? LIGHT_TYPE_STATIC : (type == 1) ? LIGHT_TYPE_POINT : LIGHT_TYPE_PULSE;
changed = true;
} undoItem();
// Behavior flags
bool shadows = (edited.flags & LIGHT_FLAG_SHADOWS) != 0;
changed |= ImGui::Checkbox("Тени от стен и дверей", &shadows); undoItem();
edited.flags = (edited.flags & ~LIGHT_FLAG_SHADOWS) | (shadows ? LIGHT_FLAG_SHADOWS : 0);
if (edited.lightType != LIGHT_TYPE_STATIC) {
bool flicker = (edited.flags & LIGHT_FLAG_FLICKER) != 0;
changed |= ImGui::Checkbox("Мерцание огня / факела (Flicker)", &flicker); undoItem();
edited.flags = (edited.flags & ~LIGHT_FLAG_FLICKER) | (flicker ? LIGHT_FLAG_FLICKER : 0);
}
if (activated) PushUndo();
if (changed) {
light = edited;
if (autoBakeLighting) BakeLighting();
}
ImGui::Spacing();
ImGui::Separator();
if (ImGui::CollapsingHeader("Глобальное освещение карты")) {
RenderLightingSettings();
}
}

View file

@ -41,7 +41,7 @@ bool EditorApp::EnsureEnginePreview() {
if (!previewPacket) return fail("Не удалось открыть буфер игрового вида.");
// Initialize in place: no large packet on the thread's stack.
memset(previewPacket, 0, sizeof(*previewPacket));
previewPacket->magic = 0x56504245; previewPacket->version = 2; previewPacket->parentPid = GetCurrentProcessId();
previewPacket->magic = 0x56504245; previewPacket->version = 1; 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,14 +105,6 @@ 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;

View file

@ -6,7 +6,7 @@
// ==============================================================================
constexpr uint32_t MAP_MAGIC = 0x4E4C4245; // 'EBLN'
constexpr uint16_t MAP_FORMAT_VERSION = 5; // v5 adds authored lighting and baked RGB cells
constexpr uint16_t MAP_FORMAT_VERSION = 4; // v4 adds a cutscene track and CP1251 string pool
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,8 +48,7 @@ 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_BARREL = 6, // Explosive Barrel (AOE Blast)
ENTITY_ARCHETYPE_COUNT = 7
ENTITY_ARCHETYPE_COUNT = 6
};
#pragma pack(push, 1)
@ -66,7 +65,7 @@ struct LevelTransition {
struct MapHeader {
uint32_t magic; // 'EBLN'
uint16_t version; // Current writer: 5; reader also accepts v1/v2/v3/v4
uint16_t version; // Current writer: 4; reader also accepts v1/v2/v3
uint16_t mapWidth; // 32
uint16_t mapHeight; // 32
uint16_t playerStartX; // Cell coordinate (0..31)
@ -79,8 +78,7 @@ 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
uint16_t lightCount; // v5 only, authored lights (slots 1..13 in engine)
uint16_t lightingReserved; // must be zero in v5; header remains 38 bytes
uint8_t reserved[4]; // Total header remains 38 bytes
};
struct CutsceneEvent {
@ -103,21 +101,9 @@ struct SpriteEntry {
};
#pragma pack(pop)
// Opt-in switch binding. Existing switch flags keep legacy hook-only behavior.
constexpr uint32_t SWITCH_DOOR_BOUND = 0x0000A800;
constexpr uint32_t SWITCH_DOOR_KEY = 0x00000100;
inline bool HasSwitchDoor(const SpriteEntry& s) {
return (s.flags & 0x0000FEFF) == (SWITCH_DOOR_BOUND | ENTITY_ARCHETYPE_SWITCH);
}
inline void SetSwitchDoor(SpriteEntry& s, int x, int y, bool key) {
s.flags = ENTITY_ARCHETYPE_SWITCH | SWITCH_DOOR_BOUND | (key ? SWITCH_DOOR_KEY : 0) |
(uint32_t(x & 31) << 16) | (uint32_t(y & 31) << 24);
}
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;
@ -150,53 +136,3 @@ 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");

View file

@ -1,7 +1,6 @@
#pragma once
#include "level_format.h"
#include "cutscene_format.h"
#include "lighting_format.h"
#include <array>
#include <cstring>
#include <fstream>
@ -15,7 +14,6 @@ struct BinaryLevel {
std::vector<SpriteEntry> sprites;
std::vector<LevelTransition> transitions;
CutsceneTrack cutscene;
LevelLighting lighting;
};
inline bool IsMapFilename(const char* name) {
@ -51,14 +49,6 @@ inline bool ValidateLevel(const BinaryLevel& level, std::string& error) {
if (sprite.x < 0 || sprite.x >= MAP_GRID_W * 64 || sprite.y < 0 || sprite.y >= MAP_GRID_H * 64 ||
sprite.textureId >= 16 || sprite.scale < 0 || sprite.scale > 16 * 65536)
return fail("Неверные координаты, текстура или масштаб спрайта");
if (HasSwitchDoor(sprite)) {
unsigned x=(sprite.flags>>16)&255, y=(sprite.flags>>24)&255;
if (x>=32 || y>=32 || (level.cells[y*32+x]!=CELL_DOOR && level.cells[y*32+x]!=CELL_DOOR_OPEN))
return fail("Рычаг должен ссылаться на клетку двери");
for (const auto& other : level.sprites)
if (&other!=&sprite && HasSwitchDoor(other) && (other.flags>>16)==(sprite.flags>>16))
return fail("У двери может быть только один связанный рычаг");
}
}
std::array<bool, MAP_TOTAL_CELLS> triggers{};
for (const auto& transition : level.transitions) {
@ -73,8 +63,6 @@ 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);
}
@ -97,12 +85,9 @@ 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 +
(h.version>=5?sizeof(LightingSettings)+lightCount*sizeof(EditorLightSource)+4096:0);
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES;
if (fileSize != static_cast<std::streamoff>(expected)) {
error = "Размер файла не соответствует заголовку карты"; return false;
}
@ -123,12 +108,6 @@ 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;

View file

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

View file

@ -26,71 +26,50 @@ void CreateRenderTarget();
void CleanupRenderTarget();
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
void SetupUnrealStyle() {
void SetupGodotStyle() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
style.WindowRounding = 0.0f;
style.ChildRounding = 0.0f;
style.FrameRounding = 2.0f;
style.PopupRounding = 2.0f;
style.ScrollbarRounding = 2.0f;
style.GrabRounding = 2.0f;
style.TabRounding = 2.0f;
style.FrameBorderSize = 0.0f;
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.FrameBorderSize = 1.0f;
style.WindowBorderSize = 1.0f;
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(7.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 5.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
// 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);
// 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);
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.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);
// UE5-inspired neutral surfaces. Blue denotes selection, not every panel.
for (int i=0;i<ImGuiCol_COUNT;++i) {
auto& c=colors[i];
float gray=(c.x+c.y+c.z)/3.0f;
c.x=c.y=c.z=gray;
}
colors[ImGuiCol_Text]=ImVec4(0.82f,0.82f,0.82f,1);
colors[ImGuiCol_WindowBg]=ImVec4(0.075f,0.075f,0.075f,1);
colors[ImGuiCol_ChildBg]=ImVec4(0.12f,0.12f,0.12f,1);
colors[ImGuiCol_FrameBg]=ImVec4(0.075f,0.075f,0.075f,1);
colors[ImGuiCol_PopupBg]=ImVec4(0.13f,0.13f,0.13f,1);
colors[ImGuiCol_Header]=ImVec4(0.18f,0.18f,0.18f,1);
colors[ImGuiCol_HeaderActive]=ImVec4(0.12f,0.32f,0.49f,1);
colors[ImGuiCol_CheckMark]=ImVec4(0.16f,0.56f,0.86f,1);
colors[ImGuiCol_SliderGrab]=colors[ImGuiCol_CheckMark];
colors[ImGuiCol_TabActive]=ImVec4(0.20f,0.20f,0.20f,1);
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);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
@ -111,7 +90,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
return 1;
}
ShowWindow(hWnd, SW_MAXIMIZE);
ShowWindow(hWnd, SW_SHOWDEFAULT);
UpdateWindow(hWnd);
DragAcceptFiles(hWnd, TRUE);
@ -156,7 +135,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
io.Fonts->AddFontDefault();
}
SetupUnrealStyle();
SetupGodotStyle();
ImGui_ImplWin32_Init(hWnd);
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
@ -260,12 +239,6 @@ LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
return true;
switch (msg) {
case WM_GETMINMAXINFO: {
auto* limits=reinterpret_cast<MINMAXINFO*>(lParam);
limits->ptMinTrackSize.x=1000;
limits->ptMinTrackSize.y=680;
return 0;
}
case WM_DROPFILES: {
HDROP hDrop = reinterpret_cast<HDROP>(wParam);
char filePath[MAX_PATH];

View file

@ -8,33 +8,21 @@
// Never modify payload while state == 1. All fields have fixed wire widths.
struct EnginePreviewPacket {
uint32_t magic = 0x56504245; // EBPV
uint32_t version = 2;
uint32_t version = 1;
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 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]{};
uint32_t reserved[19]{};
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(offsetof(EnginePreviewPacket,lights)==1357792,"Light payload offset changed");
static_assert(sizeof(EnginePreviewPacket) == 1362408, "Preview mapping size mismatch");
static_assert(sizeof(EnginePreviewPacket) == 1357792, "Preview mapping size mismatch");

View file

@ -16,12 +16,6 @@ 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);
@ -37,15 +31,6 @@ 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);
@ -80,12 +65,6 @@ 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");
}
}

Binary file not shown.

View file

@ -1,5 +1,5 @@
; ==============================================================================
; EBLAN ENGINE - Player Combat, Multi-Weapon Arsenal & Combat HUD
; EBLAN ENGINE - Player Combat, Weapon Shooting & Combat HUD
; ==============================================================================
.code
@ -9,8 +9,6 @@ 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).
@ -145,7 +143,6 @@ 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
@ -165,17 +162,9 @@ UpdateCombat PROC uses ebx ecx edx esi edi
dec playerHitFlash
@@:
; If player is dead, game is paused, or in peaceful adventure mode, cannot attack
; If player is dead, 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
@ -183,53 +172,16 @@ UpdateCombat PROC uses ebx ecx edx esi edi
cmp playerAttackCooldown, 0
jne combat_done
; Set weapon parameters based on playerWeapon
mov weaponDamage, 35
mov maxHitDist, 999999
mov hitHalfWidth, 16
; Check ammo
cmp playerAmmo, 0
jle combat_dry_fire
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
; Fire weapon!
dec playerAmmo
mov weaponDamage, 35
mov maxHitDist, 999999
mov hitHalfWidth, 16
mov playerAttackCooldown, 12
mov playerAttackCooldown, 10
mov playerFlashTimer, 5
invoke SpawnMuzzleFlashLight, playerX, playerY
fire_hitscan:
; 3. Hitscan check against enemies & destructible barrels in crosshair
; 3. Hitscan check against enemies in center crosshair
; Camera basis for -playerAngle
mov eax, ANGLE_512
sub eax, playerAngle
@ -254,16 +206,9 @@ hitscan_loop:
mov ecx, i
mov eax, [entityType + ecx*4]
; Check if Guard or Barrel
cmp eax, ENTITY_TYPE_GUARD
je hitscan_target_valid
cmp eax, ENTITY_TYPE_BARREL
je hitscan_target_valid
jne next_hitscan_target
jmp next_hitscan_target
hitscan_target_valid:
mov eax, [entityState + ecx*4]
cmp eax, ENTITY_STATE_DEAD
je next_hitscan_target
@ -300,11 +245,6 @@ hitscan_target_valid:
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
@ -322,14 +262,14 @@ hitscan_target_valid:
add ecx, eax
mov transX, ecx
; World-space billboard hitbox: weapon-specific width, scaled with
; World-space billboard hitbox: 32 units wide at scale 1.0, 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, hitHalfWidth
imul eax, 16
sar eax, 16
mov targetHalfWidth, eax
mov eax, transX
@ -355,15 +295,10 @@ 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, weaponDamage
sub eax, 35
jns @F
xor eax, eax
@@:
@ -375,9 +310,8 @@ 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 ammo reward
; Drop 10 ammo reward
mov eax, playerAmmo
add eax, 10
cmp eax, playerMaxAmmo
@ -387,115 +321,6 @@ 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
@ -509,91 +334,24 @@ 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 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:
; 2. Render Retro Weapon at bottom center
mov gunY, 192
cmp playerFlashTimer, 0
je draw_pistol_barrel
add gunY, 3 ; recoil offset
je draw_gun_barrel
; Recoil offset
add gunY, 3
; 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_pistol_barrel:
draw_gun_barrel:
; Weapon barrel & slide
invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah
invoke DrawRect, 156, gunY, 9, 28, 00555555h
@ -606,16 +364,15 @@ draw_pistol_barrel:
add eax, 26
invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates
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
; 3. Health Bar
invoke DrawString8x8, 8, 42, OFFSET strHpLabel, COLOR_GREEN
invoke DrawNumber8x8, 34, 42, playerHealth, COLOR_WHITE
invoke DrawRect, 66, 8, 50, 8, 00441010h
invoke DrawRectOutline, 66, 8, 50, 8, COLOR_WHITE
invoke DrawRect, 68, 42, 60, 8, 00441010h
invoke DrawRectOutline, 68, 42, 60, 8, COLOR_WHITE
mov eax, playerHealth
imul eax, 48
imul eax, 58
xor edx, edx
mov ecx, playerMaxHealth
test ecx, ecx
@ -626,66 +383,34 @@ hud_stats_draw:
mov barW, eax
cmp barW, 0
jle hud_ammo_draw
cmp barW, 48
cmp barW, 58
jle @F
mov barW, 48
mov barW, 58
@@:
mov edx, 0000CC33h ; Green
cmp playerHealth, 30
jg @F
mov edx, 00FF2222h ; Red warning
@@:
invoke DrawRect, 67, 9, barW, 6, edx
invoke DrawRect, 69, 43, barW, 6, edx
hud_ammo_draw:
; 4. Ammo Display: X=126, Y=8
invoke DrawString8x8, 126, 8, OFFSET strAmmoLabel, COLOR_YELLOW
invoke DrawNumber8x8, 168, 8, playerAmmo, COLOR_YELLOW
; 4. Ammo Display
invoke DrawString8x8, 140, 42, OFFSET strAmmoLabel, COLOR_YELLOW
invoke DrawNumber8x8, 182, 42, playerAmmo, COLOR_YELLOW
; 5. Keys: X=204, Y=8 (if collected)
; 5. Score Display
invoke DrawString8x8, 222, 42, OFFSET strScoreLabel, COLOR_CYAN
invoke DrawNumber8x8, 272, 42, playerScore, COLOR_CYAN
; 6. Keys (if collected)
cmp playerKeys, 0
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
jle hud_damage_flash
invoke DrawString8x8, 222, 10, OFFSET strKeysLabel, COLOR_MAGENTA
invoke DrawNumber8x8, 258, 10, playerKeys, COLOR_MAGENTA
hud_damage_flash:
; 8. Damage Screen Flash
; 7. Damage Screen Flash
cmp playerHitFlash, 0
jle hud_death_check
; Red border vignette
@ -695,7 +420,7 @@ hud_damage_flash:
invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h
hud_death_check:
; 9. Death Overlay Screen
; 8. Death Overlay Screen
cmp isPlayerDead, 0
je combat_hud_done
@ -706,34 +431,3 @@ 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
; Compact charcoal surfaces, restrained blue accent, consistent option rows.
invoke DrawRect, 46, 52, 228, 144, 000B0B0Bh
invoke DrawRect, 44, 50, 228, 144, 00202020h
invoke DrawRectOutline, 44, 50, 228, 144, 003C3C3Ch
invoke DrawRect, 44, 50, 228, 2, 002DA5E0h
invoke DrawRect, 45, 53, 226, 24, 00292929h
invoke DrawRect, 52, 84, 212, 18, 00353535h
; Header
invoke DrawString8x8, 112, 61, OFFSET strPauseTitle, 00DEDEDEh
; Menu Options
invoke DrawRect, 52, 84, 2, 18, 002DA5E0h
invoke DrawString8x8, 60, 89, OFFSET strPauseOpt1, 00FFFFFFh
invoke DrawString8x8, 60, 109, OFFSET strPauseOpt2, 00CDCDCDh
invoke DrawString8x8, 60, 129, OFFSET strPauseOpt3, 00CDCDCDh
invoke DrawString8x8, 60, 149, OFFSET strPauseOpt4, 00CDCDCDh
invoke DrawRect, 52, 166, 212, 1, 003C3C3Ch
invoke DrawString8x8, 60, 177, OFFSET strPauseOpt5, 00D99898h
pause_menu_done:
ret
RenderPauseMenu ENDP

View file

@ -231,19 +231,6 @@ csv_invalid:
ValidateStagedCutscene ENDP
StopCutscene PROC uses eax ebx
cmp isCutsceneActive, 0
je cs_release_done
invoke CanOccupyPlayer, csCameraX, csCameraY
test eax, eax
jz cs_release_done
mov eax, csCameraX
mov playerX, eax
mov eax, csCameraY
mov playerY, eax
mov eax, csCameraAngle
mov playerAngle, eax
cs_release_done:
call ResetMovementFractions
mov isCutsceneActive, 0
mov cutscenePending, 0
mov cutsceneSubtitle, -1
@ -269,12 +256,6 @@ StartCutscene PROC
cmp cutsceneCount, 0
je cs_start_empty
call StopCutscene
mov eax, playerX
mov csCameraX, eax
mov eax, playerY
mov csCameraY, eax
mov eax, playerAngle
mov csCameraAngle, eax
mov cutsceneTick, 0
mov cutsceneIndex, 0
mov isCutsceneActive, 1
@ -297,13 +278,13 @@ UpdateCutscene PROC uses ebx ecx edx esi edi
imul csMoveElapsed
idiv csMoveDuration
add eax, csMoveFromX
mov csCameraX, eax
mov playerX, eax
mov eax, csMoveToY
sub eax, csMoveFromY
imul csMoveElapsed
idiv csMoveDuration
add eax, csMoveFromY
mov csCameraY, eax
mov playerY, eax
mov eax, csMoveElapsed
cmp eax, csMoveDuration
jb cs_step_angle
@ -317,7 +298,7 @@ cs_step_angle:
idiv csAngleDuration
add eax, csAngleFrom
and eax, ANGLE_MASK
mov csCameraAngle, eax
mov playerAngle, eax
mov eax, csAngleElapsed
cmp eax, csAngleDuration
jb cs_step_fade
@ -387,9 +368,9 @@ cs_wait:
jnz cs_advance_tick
jmp cs_dispatch
cs_move:
mov eax, csCameraX
mov eax, playerX
mov csMoveFromX, eax
mov eax, csCameraY
mov eax, playerY
mov csMoveFromY, eax
mov eax, [esi].CutsceneEvent.paramA
mov csMoveToX, eax
@ -401,12 +382,12 @@ cs_move:
test eax, eax
jnz cs_dispatch
mov eax, csMoveToX
mov csCameraX, eax
mov playerX, eax
mov eax, csMoveToY
mov csCameraY, eax
mov playerY, eax
jmp cs_dispatch
cs_pan:
mov eax, csCameraAngle
mov eax, playerAngle
mov csAngleFrom, eax
mov ebx, [esi].CutsceneEvent.paramA
sub ebx, eax
@ -420,7 +401,7 @@ cs_pan:
test eax, eax
jnz cs_dispatch
mov eax, [esi].CutsceneEvent.paramA
mov csCameraAngle, eax
mov playerAngle, eax
jmp cs_dispatch
cs_text:
mov eax, [esi].CutsceneEvent.paramA

View file

@ -20,26 +20,8 @@ isRunning dd 1
; Latched by UpdatePlayer: scene/end/skip/instant-transition ticks never run AI
; or combat, even if they return control partway through the tick.
gameplayTickBlocked dd 0
qpcFrequency dq 0
qpcPrevious dq 0
qpcNow dq 0
tickAccumulator dq 0 ; QPC counts multiplied by 60
clockReset dd 0
simulationTicks dd 0
simulationState dd 0
inputFocused dd 1
playerFractionX dd 0 ; signed 16.16 remainder, truncated toward zero
playerFractionY dd 0
viewX dd 160
viewY dd 160
viewAngle dd 0
csCameraX dd 160
csCameraY dd 160
csCameraAngle dd 0
currentMapPath db MAX_PATH_BYTES dup(0)
roomEntryX dd 2
roomEntryY dd 2
roomEntryAngle dd 0
targetFrameTimeMs dd 16 ; ~60 FPS (16.6 ms)
lastTickCount dd 0
fpsCounter dd 0
fpsTimer dd 0
currentFps dd 60
@ -57,9 +39,9 @@ rngSeed dd 12345678h
playerX dd 160
playerY dd 160
playerAngle dd 0 ; 0..511 (0 = East, 128 = South, 256 = West, 384 = North)
playerMoveSpeed dd 3 ; raw units per 60 Hz tick (walk)
playerRunSpeed dd 6 ; raw units per 60 Hz tick (sprint)
playerTurnSpeed dd 4 ; angle units per 60 Hz tick
playerMoveSpeed dd 3 ; raw units per frame (walk)
playerRunSpeed dd 6 ; raw units per frame (sprint)
playerTurnSpeed dd 4 ; angle units per frame
playerRadius dd 16 ; collision radius (raw units)
; Input State Flags
@ -82,56 +64,10 @@ keySceneHeld db 0
keySkipHeld db 0
keyAttack db 0
keyAttackHeld db 0
keySpaceAttackHeld 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
keyMap db 0
keyHud db 0
keyWeaponHeld1 db 0
keyWeaponHeld2 db 0
keyWeaponHeld3 db 0
keyMenu1 db 0
keyMenu2 db 0
keyMenu3 db 0
keyMenu4 db 0
keyMenu5 db 0
keyMenuHeld1 db 0
keyMenuHeld2 db 0
keyMenuHeld3 db 0
keyMenuHeld4 db 0
keyMenuHeld5 db 0
keyDismiss db 0
keyDismissHeld 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
@ -184,17 +120,7 @@ strHpLabel db "HP:",0
strAmmoLabel db "AMMO:",0
strScoreLabel db "SCORE:",0
strKeysLabel db "KEY:",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
strYouDied db "YOU DIED! PRESS [R] TO RESTART",0
cutsceneCount dd 0
cutsceneStringCount dd 0
cutsceneFlags dd 0
@ -297,75 +223,6 @@ ALIGN 4
entityType dd MAX_SPRITES dup(?)
entityHealth dd MAX_SPRITES dup(?)
entityState dd MAX_SPRITES dup(?)
doorUnlocked db MAP_SIZE 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
szDoorLockedToast db "ДВЕРЬ ЗАПЕРТА", 0
szBarrelExplodedToast db "ВЗРЫВ БОЧКИ!", 0
szWeaponsEnabledToast db "РЕЖИМ: ОРУЖИЕ ВКЛЮЧЕНО", 0
szWeaponsDisabledToast db "РЕЖИМ: БЕЗ ОРУЖИЯ (КВЕСТ)", 0
szHudCleanToast db "ИНТЕРФЕЙС: ЧИСТЫЙ ВИД", 0
szHudDebugToast db "ИНТЕРФЕЙС: РЕЖИМ ОТЛАДКИ", 0
szFlashlightOn db 0D4h,0CEh,0CDh,0C0h,0D0h,0C8h,0CAh,03Ah,020h,0C2h,0CAh,0CBh,020h,05Bh,046h,05Dh,0 ; ФОНАРИК: ВКЛ [F] (CP1251)
szFlashlightOff db 0D4h,0CEh,0CDh,0C0h,0D0h,0C8h,0CAh,03Ah,020h,0C2h,0DBh,0CAh,0CBh,020h,05Bh,046h,05Dh,0 ; ФОНАРИК: ВЫКЛ [F] (CP1251)
szLightMode0 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C2h,0DBh,0CAh,0CBh,0DEh,0D7h,0C5h,0CDh,020h,028h,057h,06Fh,06Ch,066h,033h,044h,029h,0 ; СВЕТ: ВЫКЛЮЧЕН (Wolf3D) (CP1251)
szLightMode1 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C7h,0C0h,0CFh,0C5h,0D7h,0A8h,0CDh,0CDh,0DBh,0C9h,020h,028h,04Ch,069h,067h,068h,074h,06Dh,061h,070h,029h,0 ; СВЕТ: ЗАПЕЧЁННЫЙ (Lightmap) (CP1251)
szLightMode2 db 0D1h,0C2h,0C5h,0D2h,03Ah,020h,0C4h,0C8h,0CDh,0C0h,0CCh,0C8h,0D7h,0C5h,0D1h,0CAh,0C8h,0C9h,020h,0D1h,020h,0D4h,0C8h,0C7h,0C8h,0CAh,0CEh,0C9h,0 ; СВЕТ: ДИНАМИЧЕСКИЙ С ФИЗИКОЙ (CP1251)
szBakeDoneToast db 0D1h,0C2h,0C5h,0D2h,020h,0C7h,0C0h,0CFh,0C5h,0D7h,0A8h,0CDh,03Ah,020h,031h,030h,032h,034h,020h,0DFh,0D7h,0C5h,0C9h,0CAh,0C8h,0 ; СВЕТ ЗАПЕЧЁН: 1024 ЯЧЕЙКИ (CP1251)
szProjectFile db "project.eblanproj", 0
szProjGameMode0 db "GameMode=0", 0
szProjGameMode1 db "GameMode=1", 0
szProjShowWeapons0 db "ShowWeapons=0", 0
szFlagNoWeapons db "noweapons", 0
szFlagAdventure db "adventure", 0
szFlagAction db "action", 0
szFlagDebugHud db "debug-hud", 0
szFlagCleanHud db "clean-hud", 0
previewPrefix db "--preview:", 0
; Persistent and staged lighting buffers (declared before the map loader).
.data
lightingTick dd 0
authoredLightCount dd 0
legacyLightingSettings dd 0,40,1,384,240,45,1,0
lightRed dd 256
lightGreen dd 256
lightBlue dd 256
lightAccumR dd 0
lightAccumG dd 0
lightAccumB dd 0
lightWeight dd 0
.data?
ALIGN 4
lightGridRGB dd MAP_SIZE dup(?)
lightSmoothRGB dd MAP_SIZE dup(?)
stagedLightSettings dd 8 dup(?)
stagedLightSources db 13*40 dup(?)
stagedLightRGB dd MAP_SIZE dup(?)

View file

@ -67,57 +67,6 @@ 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, wider hitbox, 70 damage)
WEAPON_COUNT equ 3
SIM_PLAYING equ 0
SIM_CUTSCENE equ 1
SIM_TRANSITION equ 2
SIM_DEAD equ 3
SIM_PAUSED equ 4
SIM_DIALOG equ 5
SIM_UNFOCUSED equ 6
; Entity Archetypes & States
ENTITY_TYPE_PROP equ 0
@ -126,7 +75,6 @@ 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
@ -165,7 +113,7 @@ CELL_COUNT equ 11
; Sector Transition Portals
MAX_TRANSITIONS equ 16
MAX_PATH_BYTES equ 260
MAP_FORMAT_VERSION equ 5
MAP_FORMAT_VERSION equ 4
LevelTransition STRUCT ; sizeof = 42 bytes (packed)
triggerCellX db 0 ; 1 byte
@ -190,20 +138,6 @@ 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'
@ -220,7 +154,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, 5 = lighting
version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes
mapWidth dw 0 ; 32
mapHeight dw 0 ; 32
playerStartX dw 0 ; cell X
@ -233,8 +167,7 @@ MapHeader STRUCT ; sizeof = 38 bytes (packed)
cutsceneCount dw 0
cutsceneStringCount dw 0
cutsceneFlags dw 0
lightCount dw 0
lightingReserved dw 0
reserved db 4 dup(0)
MapHeader ENDS
MAX_CUTSCENE_EVENTS equ 128

View file

@ -4,18 +4,10 @@
.code
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
Game_OnPlayerInteract PROTO :DWORD, :DWORD
RestartCurrentRoom PROTO
; ------------------------------------------------------------------------------
; InitEntities: Initialize entity runtime state from spriteList flags/defaults
; ------------------------------------------------------------------------------
InitEntities PROC uses eax ebx ecx edx esi edi
xor eax, eax
mov ecx, MAP_SIZE
mov edi, OFFSET doorUnlocked
rep stosb
; 1. Clear runtime arrays
xor eax, eax
mov ecx, MAX_SPRITES
@ -94,13 +86,8 @@ init_spr_loop:
fallback_archetype:
; If flags == 0, infer archetype from textureId:
; Slot 11 = Explosive Barrel, 13 = Guard, 14 = Medkit, 15 = Ammo
; Slot 13 = Enemy 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
@ -145,7 +132,13 @@ UpdateEntities PROC uses ebx ecx edx esi edi
je entities_active
cmp keyRestart, 0
je entities_done
call RestartCurrentRoom
; Respawn player
mov playerHealth, 100
mov playerAmmo, 50
mov isPlayerDead, 0
mov playerHitFlash, 0
call LoadMapBinary
call InitEntities
ret
entities_active:
@ -155,9 +148,6 @@ entities_active:
mov i, 0
entity_loop:
call IsGameplayBlocked
test eax, eax
jnz entities_done
mov eax, i
cmp eax, spriteCount
jge entities_done
@ -227,7 +217,6 @@ 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
; --------------------------------------------------------------------------
@ -251,7 +240,6 @@ 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
; --------------------------------------------------------------------------
@ -259,7 +247,7 @@ check_ammo_pickup:
; --------------------------------------------------------------------------
check_key_pickup:
cmp edx, ENTITY_TYPE_KEY
jne check_switch_entity
jne check_guard_entity
cmp absDX, 32
jge next_entity
cmp absDY, 32
@ -269,61 +257,6 @@ 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
; Opt-in authored door binding: byte 1 = A8/A9, X/Y in bytes 2/3.
mov eax, [ebx].SpriteStruct.flags
mov edx, eax
and edx, 0FEFFh
cmp edx, 0A805h
jne legacy_switch_toggle
mov edx, eax
shr edx, 24
shr eax, 16
and eax, 0FFh
invoke ToggleDoorCell, eax, edx
test eax, eax
jz next_entity
; ToggleDoorCell returns the resulting cell, or zero if blocked/invalid.
cmp eax, CELL_DOOR_OPEN
sete al
movzx eax, al
mov ecx, i
mov [entityState + ecx*4], eax
jmp switch_show_state
legacy_switch_toggle:
; Legacy switches still toggle state and run the public gameplay hook.
mov ecx, i
mov eax, [entityState + ecx*4]
xor eax, 1
mov [entityState + ecx*4], eax
switch_show_state:
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
; --------------------------------------------------------------------------

View file

@ -1,331 +0,0 @@
; ==============================================================================
; 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
; 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
mov clockReset, 1
ret
ShowGameDialog ENDP
; ------------------------------------------------------------------------------
; CloseGameDialog: Closes active dialogue box
; ------------------------------------------------------------------------------
CloseGameDialog PROC
mov gameDialogActive, 0
mov clockReset, 1
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
rd_done:
ret
RenderGameDialog ENDP
; ------------------------------------------------------------------------------
; PlayGameSFX: Asynchronously plays a sound effect via WinMM PlaySoundA
; ------------------------------------------------------------------------------
PlayGameSFX PROC uses eax, pszRelativePath:DWORD
cmp isCutsceneActive, 0
jne sfx_done
mov eax, pszRelativePath
test eax, eax
jz sfx_done
cmp BYTE PTR [eax], 0
jz sfx_done
invoke PlaySoundA, eax, 0, 9 ; SND_FILENAME (1) | SND_ASYNC (8) = 9
sfx_done:
ret
PlayGameSFX ENDP
; ------------------------------------------------------------------------------
; Game_OnTick: Hook called every 60 Hz frame during active gameplay
; ------------------------------------------------------------------------------
Game_OnTick PROC uses eax ebx ecx edx
; Extensible hook for custom game logic (quests, timers, zones)
ret
Game_OnTick ENDP
; ------------------------------------------------------------------------------
; Game_OnPlayerInteract: Hook called when player presses [E] in range of an entity
; Returns: EAX = 1 if handled by custom script, 0 if default engine behavior
; ------------------------------------------------------------------------------
Game_OnPlayerInteract PROC uses ebx ecx edx esi edi, entityIdx:DWORD, entityTypeVal:DWORD
; Extensible hook for custom interactions
xor eax, eax
ret
Game_OnPlayerInteract ENDP
; ------------------------------------------------------------------------------
; Game_OnEntityKilled: Hook called when enemy guard or barrel is destroyed
; ------------------------------------------------------------------------------
Game_OnEntityKilled PROC uses eax ebx ecx edx, entityIdx:DWORD, entityTypeVal:DWORD
; Extensible hook for boss fights, doors opening on kill, score multipliers
ret
Game_OnEntityKilled ENDP
; ------------------------------------------------------------------------------
; Game_OnCustomHUD: Hook called during HUD rendering
; ------------------------------------------------------------------------------
Game_OnCustomHUD PROC uses eax ebx ecx edx
; Extensible hook for custom UI bars, quest markers, mini-dialogues
ret
Game_OnCustomHUD ENDP
; ------------------------------------------------------------------------------
; RenderGameHUDOverlays: Renders active toast banner, dialogue modal, and custom HUD
; ------------------------------------------------------------------------------
RenderGameHUDOverlays PROC
call RenderGameToast
call RenderGameDialog
call Game_OnCustomHUD
ret
RenderGameHUDOverlays ENDP
; ------------------------------------------------------------------------------
; CheckCmdFlag: Case-insensitive substring search (EAX = 1 if found, 0 if not)
; ------------------------------------------------------------------------------
CheckCmdFlag PROC uses esi edi ecx, pszCmd:DWORD, pszFlag:DWORD
mov esi, pszCmd
test esi, esi
jz flag_not_found
check_next_char:
cmp byte ptr [esi], 0
je flag_not_found
mov edi, pszFlag
mov ecx, esi
compare_loop:
mov al, byte ptr [edi]
test al, al
jz flag_found
mov ah, byte ptr [ecx]
cmp al, 'A'
jl @F
cmp al, 'Z'
jg @F
add al, 32
@@:
cmp ah, 'A'
jl @F
cmp ah, 'Z'
jg @F
add ah, 32
@@:
cmp al, ah
jne mismatch
inc edi
inc ecx
jmp compare_loop
mismatch:
inc esi
jmp check_next_char
flag_found:
mov eax, 1
ret
flag_not_found:
xor eax, eax
ret
CheckCmdFlag ENDP
; ------------------------------------------------------------------------------
; LoadProjectSettings: Reads project.eblanproj if present in working directory
; ------------------------------------------------------------------------------
LoadProjectSettings PROC uses ebx ecx edx esi edi
LOCAL hFile:DWORD, bytesRead:DWORD
LOCAL projBuf[1024]:BYTE
invoke CreateFileA, OFFSET szProjectFile, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0
cmp eax, INVALID_HANDLE_VALUE
je proj_done
mov hFile, eax
lea edx, projBuf
invoke ReadFile, hFile, edx, 1023, ADDR bytesRead, 0
invoke CloseHandle, hFile
cmp bytesRead, 0
jle proj_done
mov eax, bytesRead
mov byte ptr [projBuf + eax], 0
lea edx, projBuf
invoke CheckCmdFlag, edx, OFFSET szProjGameMode0
test eax, eax
jnz set_adv_mode
lea edx, projBuf
invoke CheckCmdFlag, edx, OFFSET szProjShowWeapons0
test eax, eax
jnz set_adv_mode
lea edx, projBuf
invoke CheckCmdFlag, edx, OFFSET szProjGameMode1
test eax, eax
jnz set_act_mode
jmp proj_done
set_adv_mode:
mov gameMode, GAME_MODE_ADVENTURE
mov enableWeapons, 0
mov playerWeapon, WEAPON_NONE
mov showCrosshair, 0
jmp proj_done
set_act_mode:
mov gameMode, GAME_MODE_ACTION
mov enableWeapons, 1
mov playerWeapon, WEAPON_PISTOL
mov showCrosshair, 1
proj_done:
ret
LoadProjectSettings ENDP

View file

@ -1,70 +1,13 @@
; Keyboard sampling never mutates simulation. Pending edges survive render-only
; frames and are cleared only by ConsumeInputEdges after one simulation tick.
; ==============================================================================
; EBLAN ENGINE - Input System (Keyboard via GetAsyncKeyState)
; ==============================================================================
.code
SampleHeld PROC uses ebx, first:DWORD, second:DWORD
invoke GetAsyncKeyState, first
mov ebx, eax
cmp second, 0
je @F
invoke GetAsyncKeyState, second
or eax, ebx
@@:
shr eax, 15
and eax, 1
ret
SampleHeld ENDP
SampleEdge PROC uses ebx esi edi, first:DWORD, second:DWORD, held:DWORD, pending:DWORD
invoke SampleHeld, first, second
mov esi, held
mov edi, pending
test eax, eax
jz edge_released
cmp BYTE PTR [esi], 0
jne edge_done
mov BYTE PTR [edi], 1
edge_released:
mov [esi], al
edge_done:
ret
SampleEdge ENDP
ConsumeInputEdges PROC
mov keyAction, 0
mov keyCutscene, 0
mov keySkipScene, 0
mov keyAttack, 0
mov keyRestart, 0
mov keyPause, 0
mov keyMap, 0
mov keyHud, 0
mov keyFlashlight, 0
mov keyLightMode, 0
mov keyWeapon1, 0
mov keyWeapon2, 0
mov keyWeapon3, 0
mov keyMenu1, 0
mov keyMenu2, 0
mov keyMenu3, 0
mov keyMenu4, 0
mov keyMenu5, 0
mov keyDismiss, 0
ret
ConsumeInputEdges ENDP
; ------------------------------------------------------------------------------
; PollInput: Reads keyboard state and updates movement & action flags
; ------------------------------------------------------------------------------
PollInput PROC uses eax ebx
LOCAL regained:DWORD
mov regained, 0
invoke GetForegroundWindow
cmp eax, hEngineWnd
je input_has_focus
cmp inputFocused, 0
je @F
mov clockReset, 1
@@:
mov inputFocused, 0
call ConsumeInputEdges
mov keyScreenshot, 0
mov keyForward, 0
mov keyBackward, 0
mov keyStrafeLeft, 0
@ -72,55 +15,190 @@ PollInput PROC uses eax ebx
mov keyTurnLeft, 0
mov keyTurnRight, 0
mov keyRun, 0
; On regain, sample every physical latch but discard the resulting edges.
mov keyAction, 0
mov keyScreenshot, 0
mov keyCutscene, 0
mov keySkipScene, 0
mov keyAttack, 0
mov keyRestart, 0
invoke GetForegroundWindow
cmp eax, hEngineWnd
je input_has_focus
mov keyActionHeld, 0
mov keyMapHeld, 0
mov keyShotHeld, 0
mov keySceneHeld, 0
mov keySkipHeld, 0
mov keyAttackHeld, 0
mov keyRestartHeld, 0
ret
input_has_focus:
cmp inputFocused, 0
jne @F
mov clockReset, 1
mov regained, 1
; 1. Forward (W / Up Arrow)
invoke GetAsyncKeyState, VK_KEY_W
mov bx, ax
invoke GetAsyncKeyState, VK_KEY_UP
or bx, ax
test bx, 8000h
jz @F
mov keyForward, 1
@@:
mov inputFocused, 1
invoke SampleHeld, VK_KEY_W, VK_KEY_UP
mov keyForward, al
invoke SampleHeld, VK_KEY_S, VK_KEY_DOWN
mov keyBackward, al
invoke SampleHeld, VK_KEY_A, 0
mov keyStrafeLeft, al
invoke SampleHeld, VK_KEY_D, 0
mov keyStrafeRight, al
invoke SampleHeld, VK_KEY_LEFT, 0
mov keyTurnLeft, al
invoke SampleHeld, VK_KEY_RIGHT, 0
mov keyTurnRight, al
invoke SampleHeld, VK_KEY_SHIFT, 0
mov keyRun, al
invoke SampleEdge, VK_KEY_E, VK_KEY_SPACE, OFFSET keyActionHeld, OFFSET keyAction
invoke SampleEdge, VK_KEY_CONTROL, VK_LBUTTON, OFFSET keyAttackHeld, OFFSET keyAttack
; Separate Space latch prevents a second poll from re-emitting a pending edge.
invoke SampleEdge, VK_KEY_SPACE, 0, OFFSET keySpaceAttackHeld, OFFSET keyAttack
invoke SampleEdge, VK_KEY_R, 0, OFFSET keyRestartHeld, OFFSET keyRestart
invoke SampleEdge, VK_KEY_C, 0, OFFSET keySceneHeld, OFFSET keyCutscene
invoke SampleEdge, VK_KEY_TAB, 0, OFFSET keySkipHeld, OFFSET keySkipScene
invoke SampleEdge, VK_KEY_M, 0, OFFSET keyMapHeld, OFFSET keyMap
invoke SampleEdge, VK_KEY_F12, 0, OFFSET keyShotHeld, OFFSET keyScreenshot
invoke SampleEdge, VK_KEY_F1, VK_KEY_H, OFFSET keyHudStyleHeld, OFFSET keyHud
invoke SampleEdge, VK_KEY_F, 0, OFFSET keyFlashlightHeld, OFFSET keyFlashlight
invoke SampleEdge, VK_KEY_L, 0, OFFSET keyLightModeHeld, OFFSET keyLightMode
invoke SampleEdge, VK_KEY_ESCAPE, 0, OFFSET keyPauseHeld, OFFSET keyPause
invoke SampleEdge, 13, 0, OFFSET keyDismissHeld, OFFSET keyDismiss
invoke SampleEdge, VK_KEY_1, 0, OFFSET keyWeaponHeld1, OFFSET keyWeapon1
invoke SampleEdge, VK_KEY_2, 0, OFFSET keyWeaponHeld2, OFFSET keyWeapon2
invoke SampleEdge, VK_KEY_3, 0, OFFSET keyWeaponHeld3, OFFSET keyWeapon3
invoke SampleEdge, VK_KEY_1, 0, OFFSET keyMenuHeld1, OFFSET keyMenu1
invoke SampleEdge, VK_KEY_2, 0, OFFSET keyMenuHeld2, OFFSET keyMenu2
invoke SampleEdge, VK_KEY_3, 0, OFFSET keyMenuHeld3, OFFSET keyMenu3
invoke SampleEdge, VK_KEY_4, 0, OFFSET keyMenuHeld4, OFFSET keyMenu4
invoke SampleEdge, VK_KEY_5, 0, OFFSET keyMenuHeld5, OFFSET keyMenu5
cmp regained, 0
je input_sample_done
call ConsumeInputEdges
mov keyScreenshot, 0
input_sample_done:
; 2. Backward (S / Down Arrow)
invoke GetAsyncKeyState, VK_KEY_S
mov bx, ax
invoke GetAsyncKeyState, VK_KEY_DOWN
or bx, ax
test bx, 8000h
jz @F
mov keyBackward, 1
@@:
; 3. Strafe Left (A)
invoke GetAsyncKeyState, VK_KEY_A
test ax, 8000h
jz @F
mov keyStrafeLeft, 1
@@:
; 4. Strafe Right (D)
invoke GetAsyncKeyState, VK_KEY_D
test ax, 8000h
jz @F
mov keyStrafeRight, 1
@@:
; 5. Turn Left (Left Arrow)
invoke GetAsyncKeyState, VK_KEY_LEFT
test ax, 8000h
jz @F
mov keyTurnLeft, 1
@@:
; 6. Turn Right (Right Arrow)
invoke GetAsyncKeyState, VK_KEY_RIGHT
test ax, 8000h
jz @F
mov keyTurnRight, 1
@@:
; Actions are emitted once per physical press (also for E + Space).
invoke GetAsyncKeyState, VK_KEY_E
mov bx, ax
invoke GetAsyncKeyState, VK_KEY_SPACE
or bx, ax
test bx, 8000h
jz action_released
cmp keyActionHeld, 0
jne action_sampled
mov keyAction, 1
mov keyActionHeld, 1
jmp action_sampled
action_released:
mov keyActionHeld, 0
action_sampled:
; 8. Run / Sprint (Shift)
invoke GetAsyncKeyState, VK_KEY_SHIFT
test ax, 8000h
jz @F
mov keyRun, 1
@@:
invoke GetAsyncKeyState, VK_KEY_M
test ax, 8000h
jz map_key_released
cmp keyMapHeld, 0
jne map_key_sampled
xor keyShowMap, 1
mov keyMapHeld, 1
jmp map_key_sampled
map_key_released:
mov keyMapHeld, 0
map_key_sampled:
invoke GetAsyncKeyState, VK_KEY_F12
test ax, 8000h
jz shot_key_released
cmp keyShotHeld, 0
jne shot_key_sampled
mov keyScreenshot, 1
mov keyShotHeld, 1
jmp shot_key_sampled
shot_key_released:
mov keyShotHeld, 0
shot_key_sampled:
invoke GetAsyncKeyState, VK_KEY_C
test ax, 8000h
jz scene_key_released
cmp keySceneHeld, 0
jne scene_key_sampled
mov keyCutscene, 1
mov keySceneHeld, 1
jmp scene_key_sampled
scene_key_released:
mov keySceneHeld, 0
scene_key_sampled:
invoke GetAsyncKeyState, VK_KEY_TAB
test ax, 8000h
jz skip_key_released
cmp keySkipHeld, 0
jne skip_key_sampled
mov keySkipScene, 1
mov keySkipHeld, 1
jmp skip_key_sampled
skip_key_released:
mov keySkipHeld, 0
skip_key_sampled:
; Attack Key: Space or Ctrl or Left Mouse Button
invoke GetAsyncKeyState, VK_KEY_SPACE
mov bx, ax
invoke GetAsyncKeyState, VK_KEY_CONTROL
or bx, ax
invoke GetAsyncKeyState, VK_LBUTTON
or bx, ax
test bx, 8000h
jz attack_released
cmp keyAttackHeld, 0
jne attack_sampled
mov keyAttack, 1
mov keyAttackHeld, 1
jmp attack_sampled
attack_released:
mov keyAttackHeld, 0
attack_sampled:
; Restart Key: R
invoke GetAsyncKeyState, VK_KEY_R
test ax, 8000h
jz restart_released
cmp keyRestartHeld, 0
jne restart_sampled
mov keyRestart, 1
mov keyRestartHeld, 1
jmp restart_sampled
restart_released:
mov keyRestartHeld, 0
restart_sampled:
cmp isCutsceneActive, 0
je normal_keys_enabled
mov keyForward, 0
mov keyBackward, 0
mov keyStrafeLeft, 0
mov keyStrafeRight, 0
mov keyTurnLeft, 0
mov keyTurnRight, 0
mov keyRun, 0
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

View file

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

View file

@ -1,835 +0,0 @@
; 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
LOCAL originX:DWORD,originY:DWORD,originAngle:DWORD
; Camera-attached flashlight. Cinematics never move the gameplay body, but
; use the same lighting origin as the editor's camera-only preview packet.
mov eax,playerX
mov originX,eax
mov eax,playerY
mov originY,eax
mov eax,playerAngle
mov originAngle,eax
cmp isCutsceneActive,0
je spl_origin_ready
mov eax,csCameraX
mov originX,eax
mov eax,csCameraY
mov originY,eax
mov eax,csCameraAngle
mov originAngle,eax
spl_origin_ready:
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,originX
mov ebx,y
sub ebx,originY
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,originAngle
and ecx,511
mov eax,x
sub eax,originX
imul [tableCos+ecx*4]
shrd eax,edx,16
mov forward,eax
mov eax,y
sub eax,originY
imul [tableSin+ecx*4]
shrd eax,edx,16
add forward,eax
cmp forward,0
jle spl_fog
mov eax,y
sub eax,originY
imul [tableCos+ecx*4]
shrd eax,edx,16
mov lateral,eax
mov eax,x
sub eax,originX
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,originX,originY,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,viewX
mov x,eax
mov eax,rawDist
imul rayDirY
shrd eax,edx,24
add eax,viewY
mov y,eax
cmp sideHit,0
jne cwl_y
mov eax,mapX
shl eax,6
cmp stepX,0
jg @F
add eax,64
@@:
sub eax,stepX
mov x,eax
jmp cwl_ready
cwl_y:
mov eax,mapY
shl eax,6
cmp stepY,0
jg @F
add eax,64
@@:
sub eax,stepY
mov y,eax
cwl_ready:
invoke SamplePointLighting,x,y,perpWallDist,sideHit
ret
ComputeWallLightLevel ENDP
ComputeSpriteLight PROC sprX:DWORD,sprY:DWORD,sprDist:DWORD
invoke SamplePointLighting,sprX,sprY,sprDist,0
ret
ComputeSpriteLight ENDP
ShadeLightPixel PROC uses eax ebx
mov eax,edx
and eax,255
imul eax,lightBlue
shr eax,8
mov ebx,eax
mov eax,edx
shr eax,8
and eax,255
imul eax,lightGreen
shr eax,8
shl eax,8
or ebx,eax
mov eax,edx
shr eax,16
and eax,255
imul eax,lightRed
shr eax,8
shl eax,16
or eax,ebx
mov edx,eax
ret
ShadeLightPixel ENDP

View file

@ -13,19 +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
ResetMovementFractions PROTO
CanOccupyPlayer PROTO :DWORD,:DWORD
; ------------------------------------------------------------------------------
; InitMap: Creates a rich test level with rooms, hallways, pillars & doors
; ------------------------------------------------------------------------------
InitMap PROC uses eax ecx edi
call StopCutscene
; Clear map to empty
mov edi, OFFSET gameMap
mov ecx, MAP_SIZE
@ -98,11 +90,7 @@ loop_wall2:
mov playerX, 160
mov playerY, 160
mov playerAngle, 0
mov currentMapPath, 0
mov roomEntryX, 2
mov roomEntryY, 2
mov roomEntryAngle, 0
call ResetMovementFractions
call StopCutscene
mov cutsceneCount, 0
mov cutsceneStringCount, 0
mov cutsceneFlags, 0
@ -112,7 +100,6 @@ loop_wall2:
mov portalLatchCell, -1
mov mapCeilingColor, COLOR_CEILING
mov mapFloorColor, COLOR_FLOOR
call InitLightingSystem
ret
InitMap ENDP
@ -187,46 +174,6 @@ IsSolidCell ENDP
; ------------------------------------------------------------------------------
; InteractDoor: Checks if player is facing a door or portal and handles interaction
; ------------------------------------------------------------------------------
; Returns resulting cell or zero. Shared by direct interaction and switches.
ToggleDoorCell PROC uses ebx ecx edx, cellX:DWORD, cellY:DWORD
invoke GetMapCell, cellX, cellY
cmp eax, CELL_DOOR
jne toggle_check_close
invoke SetMapCell, cellX, cellY, CELL_DOOR_OPEN
mov eax, CELL_DOOR_OPEN
ret
toggle_check_close:
cmp eax, CELL_DOOR_OPEN
jne toggle_blocked
mov eax, playerX
sub eax, playerRadius
sar eax, CELL_SHIFT
cmp eax, cellX
jg toggle_close
mov eax, playerX
add eax, playerRadius
sar eax, CELL_SHIFT
cmp eax, cellX
jl toggle_close
mov eax, playerY
sub eax, playerRadius
sar eax, CELL_SHIFT
cmp eax, cellY
jg toggle_close
mov eax, playerY
add eax, playerRadius
sar eax, CELL_SHIFT
cmp eax, cellY
jge toggle_blocked
toggle_close:
invoke SetMapCell, cellX, cellY, CELL_DOOR
mov eax, CELL_DOOR
ret
toggle_blocked:
xor eax, eax
ret
ToggleDoorCell ENDP
InteractDoor PROC uses ebx ecx edx esi edi
LOCAL targetX:DWORD, targetY:DWORD
@ -252,62 +199,18 @@ InteractDoor PROC uses ebx ecx edx esi edi
invoke GetMapCell, targetX, targetY
cmp eax, CELL_DOOR
je interact_door_found
jne check_door_close
; Open the door!
invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN
ret
check_door_close:
cmp eax, CELL_DOOR_OPEN
jne check_portal_interact
interact_door_found:
mov keyAction, 0
; A bound door is switch-only, or key-unlockable. Ordinary doors stay free.
mov edi, targetY
shl edi, CELL_SHIFT-1 ; MAP_W is 32
add edi, targetX
cmp byte ptr [doorUnlocked+edi], 0
jne interact_toggle
mov esi, OFFSET spriteList
mov ecx, spriteCount
interact_binding_loop:
test ecx, ecx
jz interact_toggle
mov eax, [esi].SpriteStruct.flags
cmp al, ENTITY_TYPE_SWITCH
jne interact_binding_next
mov edx, eax
and edx, 0FEFFh
cmp edx, 0A805h
jne interact_binding_next
mov edx, eax
shr edx, 16
and edx, 0FFh
cmp edx, targetX
jne interact_binding_next
mov edx, eax
shr edx, 24
cmp edx, targetY
jne interact_binding_next
test eax, 100h
jz interact_locked
cmp playerKeys, 0
je interact_locked
dec playerKeys
mov byte ptr [doorUnlocked+edi], 1
jmp interact_toggle
interact_binding_next:
add esi, sizeof SpriteStruct
dec ecx
jmp interact_binding_loop
interact_locked:
invoke ShowGameToast, OFFSET szDoorLockedToast, 50, COLOR_YELLOW
ret
interact_toggle:
invoke ToggleDoorCell, targetX, targetY
cmp eax, CELL_DOOR_OPEN
je interact_opened
cmp eax, CELL_DOOR
jne no_door
invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW
ret
interact_opened:
invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN
; Close the door!
invoke SetMapCell, targetX, targetY, CELL_DOOR
ret
check_portal_interact:
@ -328,7 +231,6 @@ portal_find_loop:
jne portal_find_next
; Found transition!
mov keyAction, 0
movzx eax, [esi].LevelTransition.targetSpawnX
movzx ebx, [esi].LevelTransition.targetSpawnY
movzx ecx, [esi].LevelTransition.targetAngle
@ -500,7 +402,7 @@ next_map_copied:
cmp fadeEffect, 0
je switch_instant
; Initiate smooth Fade Out (up to 16 fixed ticks, ~0.26s).
; Initiate smooth Fade Out (takes 16 frames ~ 0.26s)
mov fadeState, 1
mov fadeLevel, 1
ret
@ -527,11 +429,11 @@ transition_loaded:
DoSublevelTransition ENDP
; ------------------------------------------------------------------------------
; UpdateScreenFade: fixed-tick transition sequencer, never called from drawing.
; ApplyScreenFade: Screen darkening post-processing and transition sequencer
; ------------------------------------------------------------------------------
UpdateScreenFade PROC
ApplyScreenFade PROC uses ebx ecx edx esi edi
cmp fadeState, 0
je fade_tick_done
je fade_done
cmp fadeState, 1
je do_fade_out
@ -539,35 +441,31 @@ UpdateScreenFade PROC
je do_switch_state
cmp fadeState, 3
je do_fade_in
jmp fade_tick_done
jmp fade_done
do_fade_out:
inc fadeLevel
cmp fadeLevel, 16
jl fade_tick_done
jl apply_darkening
mov fadeLevel, 16
mov fadeState, 2
call DoSublevelTransition
mov fadeState, 3
jmp fade_tick_done
jmp apply_darkening
do_switch_state:
call DoSublevelTransition
mov fadeState, 3
jmp fade_tick_done
jmp apply_darkening
do_fade_in:
dec fadeLevel
cmp fadeLevel, 0
jg fade_tick_done
jg apply_darkening
mov fadeLevel, 0
mov fadeState, 0
fade_tick_done:
ret
UpdateScreenFade ENDP
jmp fade_done
; Pure framebuffer post-process. Repeated drawing never switches rooms.
ApplyScreenFade PROC uses ebx ecx edx esi edi
apply_darkening:
cmp fadeLevel, 0
jle fade_done
@ -628,7 +526,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, numLights:DWORD
LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD
LOCAL resolvedPath[MAX_PATH_BYTES]:BYTE
LOCAL stagedMap[MAP_SIZE]:BYTE
LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE
@ -749,16 +647,6 @@ 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
@ -772,13 +660,6 @@ old_map_no_lighting:
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
@ -819,44 +700,6 @@ map_lighting_size_ready:
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
@ -893,49 +736,6 @@ sprite_coords_raw:
jl close_load_failed
cmp [esi].SpriteStruct.scale, 16 * FIXED_ONE
ja close_load_failed
mov eax, [esi].SpriteStruct.flags
cmp al, ENTITY_TYPE_SWITCH
jne validated_sprite_binding
mov edx, eax
and edx, 0FEFFh
cmp edx, 0A805h
jne validated_sprite_binding
mov edx, eax
shr edx, 24
cmp edx, MAP_H
jae close_load_failed
shr eax, 16
and eax, 0FFh
cmp eax, MAP_W
jae close_load_failed
shl edx, 5
add edx, eax
mov al, stagedMap[edx]
cmp al, CELL_DOOR
je binding_cell_ok
cmp al, CELL_DOOR_OPEN
jne close_load_failed
binding_cell_ok:
lea edi, stagedSprites
binding_unique_loop:
cmp edi, esi
jae validated_sprite_binding
mov eax, [edi].SpriteStruct.flags
cmp al, ENTITY_TYPE_SWITCH
jne binding_unique_next
mov edx, eax
and edx, 0FEFFh
cmp edx, 0A805h
jne binding_unique_next
shr eax, 16
mov edx, [esi].SpriteStruct.flags
shr edx, 16
cmp eax, edx
je close_load_failed
binding_unique_next:
add edi, sizeof SpriteStruct
jmp binding_unique_loop
validated_sprite_binding:
add esi, sizeof SpriteStruct
dec ecx
jmp validate_sprite
@ -1102,22 +902,7 @@ commit_level:
mov playerY, eax
mov eax, newAngle
mov playerAngle, eax
mov eax, newX
mov roomEntryX, eax
mov eax, newY
mov roomEntryY, eax
mov eax, newAngle
mov roomEntryAngle, eax
lea esi, resolvedPath
mov edi, OFFSET currentMapPath
remember_room_path:
lodsb
stosb
test al, al
jnz remember_room_path
call ResetMovementFractions
mov mapErrorFrames, 0
invoke CommitLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights,OFFSET stagedLightRGB
mov eax, 1
ret
close_load_failed:
@ -1186,49 +971,6 @@ load_startup_map:
jnz startup_map_ready
call InitMap
startup_map_ready:
push eax
call InitEntities
pop eax
ret
LoadMapBinary ENDP
; Reload the current room and the actual entry spawn, not the startup map.
; LoadSublevelBinary commits only on success, so failure preserves all gameplay.
RestartCurrentRoom PROC
cmp currentMapPath, 0
je restart_procedural
invoke LoadSublevelBinary, OFFSET currentMapPath, roomEntryX, roomEntryY, roomEntryAngle
test eax, eax
jz restart_failed
jmp restart_committed
restart_procedural:
call InitMap
restart_committed:
call InitEntities
call ResetMovementFractions
mov playerHealth, 100
mov playerAmmo, 50
mov playerScore, 0
mov playerKeys, 0
mov isPlayerDead, 0
mov isGamePaused, 0
mov gameDialogActive, 0
mov gameToastTimer, 0
mov playerAttackCooldown, 0
mov playerFlashTimer, 0
mov playerHitFlash, 0
mov fadeState, 0
mov fadeLevel, 0
mov mapErrorFrames, 0
mov pendingMapPath, 0
mov pendingSpawnX, 0
mov pendingSpawnY, 0
mov pendingSpawnAngle, 0
mov gameplayTickBlocked, 1
mov clockReset, 1
mov eax, 1
ret
restart_failed:
xor eax, eax
ret
RestartCurrentRoom ENDP

View file

@ -1,221 +1,244 @@
; Fixed-tick player simulation. Wire coordinates remain raw world units.
.code
StartCutscene PROTO
StopCutscene PROTO
UpdateCutscene PROTO
; ==============================================================================
; EBLAN ENGINE - Player Movement, Physics & Collision Handling
; ==============================================================================
.code
; Shared gameplay gate. Returns EAX != 0 when simulation must be frozen.
; UpdatePlayer owns the per-tick latch; renderer never changes this decision.
IsGameplayBlocked PROC
mov eax, gameplayTickBlocked
or eax, isPlayerDead
or eax, isCutsceneActive
or eax, cutscenePending
or eax, fadeState
or eax, isGamePaused
or eax, gameDialogActive
ret
IsGameplayBlocked ENDP
ResetMovementFractions PROC
mov playerFractionX, 0
mov playerFractionY, 0
ret
ResetMovementFractions ENDP
; Test every overlapped cell, not only the leading edge's center. Portal cells
; block rays but deliberately remain walkable for players/camera handoff.
CanOccupyPlayer PROC uses ebx ecx edx esi edi, x:DWORD, y:DWORD
LOCAL minX:DWORD, maxX:DWORD, maxY:DWORD, row:DWORD
mov eax, x
sub eax, playerRadius
sar eax, CELL_SHIFT
mov minX, eax
mov eax, x
add eax, playerRadius
sar eax, CELL_SHIFT
mov maxX, eax
mov eax, y
sub eax, playerRadius
sar eax, CELL_SHIFT
mov row, eax
mov eax, y
add eax, playerRadius
sar eax, CELL_SHIFT
mov maxY, eax
body_row:
mov ebx, minX
body_cell:
invoke GetMapCell, ebx, row
cmp eax, CELL_PORTAL
je body_next
invoke IsSolidCell, eax
test eax, eax
jnz body_blocked
body_next:
inc ebx
cmp ebx, maxX
jle body_cell
inc row
mov eax, row
cmp eax, maxY
jle body_row
mov eax, 1
ret
body_blocked:
xor eax, eax
ret
CanOccupyPlayer ENDP
; ------------------------------------------------------------------------------
; UpdatePlayer: Updates rotation, computes movement vector & handles collisions
; ------------------------------------------------------------------------------
UpdatePlayer PROC uses ebx ecx edx esi edi
LOCAL forward:SDWORD, strafe:SDWORD, speed:DWORD
LOCAL moveX:SDWORD, moveY:SDWORD, steps:DWORD
LOCAL moveX:DWORD, moveY:DWORD
LOCAL speed:DWORD
LOCAL nextX:DWORD, nextY:DWORD
LOCAL checkGridX:DWORD, checkGridY:DWORD
mov gameplayTickBlocked, 1
cmp isGamePaused, 0
jne player_done
cmp gameDialogActive, 0
jne player_done
cmp isPlayerDead, 0
jne player_done
jne update_player_done
; Freeze player movement and inputs during level transitions
cmp fadeState, 0
jne player_done
jne update_player_done
cmp cutscenePending, 0
je @F
je scene_manual_start
call StartCutscene
@@:
scene_manual_start:
cmp keyCutscene, 0
je @F
je scene_check_active
cmp isCutsceneActive, 0
jne @F
jne scene_check_active
call StartCutscene
@@:
scene_check_active:
cmp isCutsceneActive, 0
je player_active
je normal_player_update
cmp keySkipScene, 0
je player_scene
je scene_update_tick
call StopCutscene
ret
player_scene:
scene_update_tick:
call UpdateCutscene
ret
player_active:
normal_player_update:
mov gameplayTickBlocked, 0
movzx eax, keyTurnRight
movzx ebx, keyTurnLeft
sub eax, ebx
imul eax, playerTurnSpeed
add eax, playerAngle
mov moveX, 0
mov moveY, 0
; 1. Handle Camera Rotation
cmp keyTurnLeft, 0
je check_turn_right
mov eax, playerAngle
sub eax, playerTurnSpeed
and eax, ANGLE_MASK
mov playerAngle, eax
check_turn_right:
cmp keyTurnRight, 0
je check_action
mov eax, playerAngle
add eax, playerTurnSpeed
and eax, ANGLE_MASK
mov playerAngle, eax
check_action:
cmp keyAction, 0
je @F
je check_movement
call InteractDoor
call IsGameplayBlocked
test eax, eax
jnz player_done
@@:
movzx eax, keyForward
movzx ebx, keyBackward
sub eax, ebx
mov forward, eax
movzx eax, keyStrafeRight
movzx ebx, keyStrafeLeft
sub eax, ebx
mov strafe, eax
or eax, forward
jz player_done
mov eax, playerMoveSpeed
cmp fadeState, 0
jne update_player_done
cmp gameplayTickBlocked, 0
jne update_player_done
; 2. Determine Movement Speed
check_movement:
mov eax, playerMoveSpeed ; Base speed: raw units/frame
cmp keyRun, 0
je @F
mov eax, playerRunSpeed
mov eax, playerRunSpeed ; Sprint speed
@@:
shl eax, 16
cmp forward, 0
je speed_ready
cmp strafe, 0
je speed_ready
; 1/sqrt(2), 16.16. Product may exceed 32 bits.
mov ebx, 46341
imul ebx
shrd eax, edx, 16
speed_ready:
mov speed, eax
mov ecx, playerAngle
mov eax, [tableCos+ecx*4]
imul eax, forward
mov ebx, [tableSin+ecx*4]
imul ebx, strafe
sub eax, ebx
imul speed
mov ebx, FIXED_ONE
idiv ebx
add eax, playerFractionX
cdq
mov ebx, FIXED_ONE
idiv ebx
mov moveX, eax
mov playerFractionX, edx
mov eax, [tableSin+ecx*4]
imul eax, forward
mov ebx, [tableCos+ecx*4]
imul ebx, strafe
add eax, ebx
imul speed
mov ebx, FIXED_ONE
idiv ebx
add eax, playerFractionY
cdq
mov ebx, FIXED_ONE
idiv ebx
mov moveY, eax
mov playerFractionY, edx
; Unit substeps prevent tunneling even when scripts increase speed.
mov steps, 0
player_step:
; 3. Forward / Backward Vector
cmp keyForward, 0
je check_backward
mov eax, playerAngle
and eax, ANGLE_MASK
; dx += cos(angle) * speed
mov ebx, [tableCos + eax*4]
imul ebx, speed
sar ebx, 16
add moveX, ebx
; dy += sin(angle) * speed
mov ecx, [tableSin + eax*4]
imul ecx, speed
sar ecx, 16
add moveY, ecx
check_backward:
cmp keyBackward, 0
je check_strafe_left
mov eax, playerAngle
and eax, ANGLE_MASK
mov ebx, [tableCos + eax*4]
imul ebx, speed
sar ebx, 16
sub moveX, ebx
mov ecx, [tableSin + eax*4]
imul ecx, speed
sar ecx, 16
sub moveY, ecx
; 4. Strafe Left / Right Vector (Angle +/- 90 degrees)
check_strafe_left:
cmp keyStrafeLeft, 0
je check_strafe_right
; Left vector is (angle - 128)
mov eax, playerAngle
sub eax, ANGLE_90
and eax, ANGLE_MASK
mov ebx, [tableCos + eax*4]
imul ebx, speed
sar ebx, 16
add moveX, ebx
mov ecx, [tableSin + eax*4]
imul ecx, speed
sar ecx, 16
add moveY, ecx
check_strafe_right:
cmp keyStrafeRight, 0
je apply_physics
; Right vector is (angle + 128)
mov eax, playerAngle
add eax, ANGLE_90
and eax, ANGLE_MASK
mov ebx, [tableCos + eax*4]
imul ebx, speed
sar ebx, 16
add moveX, ebx
mov ecx, [tableSin + eax*4]
imul ecx, speed
sar ecx, 16
add moveY, ecx
; 5. Apply Movement with Wall-Sliding Collision Detection
apply_physics:
mov eax, moveX
or eax, moveY
jz player_done
inc steps
cmp steps, 64
ja player_done
jz update_player_done ; No movement input
; --- Test X Movement ---
mov eax, playerX
add eax, moveX
mov nextX, eax
; Buffer check (collision body radius)
mov ebx, nextX
cmp moveX, 0
je step_y
mov esi, 1
jg @F
mov esi, -1
jge @F
mov eax, playerRadius
sub ebx, eax ; Moving left: check left edge
jmp check_x_col
@@:
mov ebx, playerX
add ebx, esi
invoke CanOccupyPlayer, ebx, playerY
mov eax, playerRadius
add ebx, eax ; Moving right: check right edge
check_x_col:
sar ebx, 6 ; divide by CELL_SIZE (64)
mov checkGridX, ebx
mov ecx, playerY
sar ecx, 6
mov checkGridY, ecx
invoke GetMapCell, checkGridX, checkGridY
cmp eax, CELL_PORTAL
je allow_portal_x
invoke IsSolidCell, eax
test eax, eax
jz blocked_x
mov playerX, ebx
sub moveX, esi
jmp step_y
blocked_x:
mov moveX, 0
mov playerFractionX, 0
step_y:
jnz skip_x_move ; Collision on X: slide along wall!
allow_portal_x:
mov eax, nextX
mov playerX, eax
skip_x_move:
; --- Test Y Movement ---
mov eax, playerY
add eax, moveY
mov nextY, eax
mov ecx, nextY
cmp moveY, 0
je player_step
mov esi, 1
jg @F
mov esi, -1
jge @F
mov eax, playerRadius
sub ecx, eax
jmp check_y_col
@@:
mov ebx, playerY
add ebx, esi
invoke CanOccupyPlayer, playerX, ebx
mov eax, playerRadius
add ecx, eax
check_y_col:
sar ecx, 6
mov checkGridY, ecx
mov ebx, playerX
sar ebx, 6
mov checkGridX, ebx
invoke GetMapCell, checkGridX, checkGridY
cmp eax, CELL_PORTAL
je allow_portal_y
invoke IsSolidCell, eax
test eax, eax
jz blocked_y
mov playerY, ebx
sub moveY, esi
jmp player_step
blocked_y:
mov moveY, 0
mov playerFractionY, 0
jmp player_step
player_done:
call IsGameplayBlocked
test eax, eax
jnz @F
jnz update_player_done ; Collision on Y
allow_portal_y:
mov eax, nextY
mov playerY, eax
update_player_done:
cmp fadeState, 0
jne @F
call CheckPortalTriggers
@@:
ret

View file

@ -1,6 +1,7 @@
; 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
@ -16,14 +17,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, 1362408
invoke MapViewOfFile, eax, 6, 0, 0, 1357792
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], 2
cmp DWORD PTR [ebx+4], 1
jne preview_exit_error
invoke OpenProcess, 00100000h, 0, DWORD PTR [ebx+12] ; SYNCHRONIZE
test eax, eax
@ -106,39 +107,6 @@ 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

View file

@ -7,25 +7,6 @@
RenderSprites PROTO :DWORD
RenderGlass PROTO
SelectRenderCamera PROC
mov eax, playerX
mov viewX, eax
mov eax, playerY
mov viewY, eax
mov eax, playerAngle
mov viewAngle, eax
cmp isCutsceneActive, 0
je render_camera_ready
mov eax, csCameraX
mov viewX, eax
mov eax, csCameraY
mov viewY, eax
mov eax, csCameraAngle
mov viewAngle, eax
render_camera_ready:
ret
SelectRenderCamera ENDP
; ------------------------------------------------------------------------------
; Render3DScene: Full frame 2.5D raycasting pass (Floor, Ceiling, Walls, Sprites)
; ------------------------------------------------------------------------------
@ -55,35 +36,16 @@ 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)
; --------------------------------------------------------------------------
call SelectRenderCamera
invoke SamplePointLighting,viewX,viewY,0,0
mov edi, OFFSET screenBuffer
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 eax, mapCeilingColor
mov ecx, (SCREEN_W * SCREEN_HALF_H)
rep stosd
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 eax, mapFloorColor
mov ecx, (SCREEN_W * SCREEN_HALF_H)
rep stosd
@ -98,7 +60,7 @@ ray_column_loop:
; Lookup Tangential Ray Angle Offset (flat plane perspective correction)
mov eax, screenX
mov edx, [tableRayAngleOffset + eax*4]
add edx, viewAngle
add edx, playerAngle
and edx, ANGLE_MASK
mov rayAngle, edx
mov eax, edx
@ -110,11 +72,11 @@ ray_column_loop:
mov rayDirY, edx
; Current Grid Cell
mov eax, viewX
mov eax, playerX
sar eax, 6
mov mapX, eax
mov eax, viewY
mov eax, playerY
sar eax, 6
mov mapY, eax
@ -165,7 +127,7 @@ nonzero_step_x:
; sideDistX = (playerX - (mapX * 64)) * deltaDistX / 64
mov eax, mapX
shl eax, 6
mov edx, viewX
mov edx, playerX
sub edx, eax
mov eax, edx
imul deltaDistX
@ -179,7 +141,7 @@ ray_dir_x_pos:
mov eax, mapX
inc eax
shl eax, 6
sub eax, viewX
sub eax, playerX
imul deltaDistX
shrd eax, edx, 6
mov sideDistX, eax
@ -196,7 +158,7 @@ nonzero_step_y:
mov stepY, -1
mov eax, mapY
shl eax, 6
mov edx, viewY
mov edx, playerY
sub edx, eax
mov eax, edx
imul deltaDistY
@ -209,7 +171,7 @@ ray_dir_y_pos:
mov eax, mapY
inc eax
shl eax, 6
sub eax, viewY
sub eax, playerY
imul deltaDistY
shrd eax, edx, 6
mov sideDistY, eax
@ -317,7 +279,7 @@ glass_apply_fisheye:
mov ebx, rayDirY
imul ebx
shrd eax, edx, 8
mov ebx, viewY
mov ebx, playerY
shl ebx, 16
add eax, ebx
jmp glass_compute_tex_x
@ -326,7 +288,7 @@ glass_tex_hit_y:
mov ebx, rayDirX
imul ebx
shrd eax, edx, 8
mov ebx, viewX
mov ebx, playerX
shl ebx, 16
add eax, ebx
glass_compute_tex_x:
@ -438,7 +400,7 @@ rounded_distance_ready:
mov ebx, rayDirY
imul ebx
shrd eax, edx, 8 ; (24.8 * 16.16) -> 16.16
mov ebx, viewY
mov ebx, playerY
shl ebx, 16
add eax, ebx
jmp compute_tex_x
@ -449,7 +411,7 @@ tex_hit_y:
mov ebx, rayDirX
imul ebx
shrd eax, edx, 8
mov ebx, viewX
mov ebx, playerX
shl ebx, 16
add eax, ebx
@ -503,11 +465,6 @@ 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
; --------------------------------------------------------------------------
@ -536,9 +493,6 @@ 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
@ -559,10 +513,6 @@ apply_distance_fog:
make_pitch_black:
mov edx, 00111111h
jmp write_wall_pixel
apply_col_light:
call ShadeLightPixel
write_wall_pixel:
mov eax, pScreenPixel
@ -641,7 +591,6 @@ 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
@ -649,7 +598,7 @@ RenderSprites PROC uses ebx ecx edx esi edi passFilter:DWORD
; Camera basis for -playerAngle (rotate world into camera space)
mov eax, ANGLE_512
sub eax, viewAngle
sub eax, playerAngle
and eax, ANGLE_MASK
mov ecx, [tableCos + eax*4]
mov camCos, ecx
@ -670,12 +619,12 @@ dist_loop:
add ebx, OFFSET spriteList
mov eax, [ebx].SpriteStruct.x
sub eax, viewX ; dx
sub eax, playerX ; dx
imul eax, eax
mov ecx, eax
mov eax, [ebx].SpriteStruct.y
sub eax, viewY ; dy
sub eax, playerY ; dy
imul eax, eax
add eax, ecx
mov [ebx].SpriteStruct.dist, eax
@ -759,11 +708,11 @@ sprite_loop:
; Sprite position relative to player (raw units)
mov eax, [ebx].SpriteStruct.x
sub eax, viewX
sub eax, playerX
mov spriteX, eax
mov eax, [ebx].SpriteStruct.y
sub eax, viewY
sub eax, playerY
mov spriteY, eax
; Transform to camera space:
@ -887,9 +836,6 @@ sprite_loop:
mov texStepY, eax
; Render vertical stripes
invoke ComputeSpriteLight, spriteX, spriteY, transY
mov sprLightLevel, eax
mov eax, drawStartX
mov stripe, eax
@ -970,9 +916,6 @@ 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
@ -984,10 +927,6 @@ 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

View file

@ -1,253 +0,0 @@
; Fixed 60 Hz simulation and QPC scheduler. No presentation or keyboard APIs in
; Engine_Tick. AdvanceSimulation accepts elapsed QPC counts for deterministic tests.
.code
QueryPerformanceCounter PROTO :DWORD
QueryPerformanceFrequency PROTO :DWORD
GetSimulationState PROC
mov eax, SIM_UNFOCUSED
cmp inputFocused, 0
je state_ready
mov eax, SIM_PAUSED
cmp isGamePaused, 0
jne state_ready
mov eax, SIM_DIALOG
cmp gameDialogActive, 0
jne state_ready
mov eax, SIM_TRANSITION
cmp fadeState, 0
jne state_ready
mov eax, SIM_DEAD
cmp isPlayerDead, 0
jne state_ready
mov eax, SIM_CUTSCENE
cmp isCutsceneActive, 0
jne state_ready
cmp cutscenePending, 0
jne state_ready
mov eax, SIM_PLAYING
state_ready:
ret
GetSimulationState ENDP
; UI edges execute on ticks, never during polling or drawing.
ApplyInputCommands PROC
cmp keyPause, 0
je command_menu
xor isGamePaused, 1
mov clockReset, 1
command_menu:
cmp isGamePaused, 0
je command_hud
cmp keyMenu1, 0
je @F
mov isGamePaused, 0
mov clockReset, 1
@@:
cmp keyMenu2, 0
je @F
xor enableWeapons, 1
mov eax, enableWeapons
mov gameMode, eax
mov showCrosshair, eax
mov playerWeapon, WEAPON_NONE
test eax, eax
jz @F
mov playerWeapon, WEAPON_PISTOL
@@:
cmp keyMenu3, 0
je @F
xor hudStyle, 1
@@:
cmp keyMenu4, 0
je @F
call RestartCurrentRoom
; A failed reload stays paused with all room/gameplay state intact.
@@:
cmp keyMenu5, 0
je command_done
mov isRunning, 0
ret
command_hud:
cmp keyMap, 0
je @F
xor keyShowMap, 1
@@:
cmp keyHud, 0
je @F
xor hudStyle, 1
@@:
call GetSimulationState
cmp eax, SIM_PLAYING
jne command_done
cmp keyFlashlight, 0
je @F
xor isFlashlightOn, 1
@@:
cmp keyLightMode, 0
je @F
inc lightingMode
cmp lightingMode, 3
jb @F
mov lightingMode, 0
@@:
cmp enableWeapons, 0
je command_done
cmp gameMode, GAME_MODE_ACTION
jne command_done
cmp keyWeapon1, 0
je @F
mov playerWeapon, WEAPON_KNIFE
@@:
cmp keyWeapon2, 0
je @F
mov playerWeapon, WEAPON_PISTOL
@@:
cmp keyWeapon3, 0
je command_done
mov playerWeapon, WEAPON_SHOTGUN
command_done:
ret
ApplyInputCommands ENDP
Engine_Tick PROC
inc simulationTicks
mov gameplayTickBlocked, 1
call GetSimulationState
mov simulationState, eax
cmp eax, SIM_UNFOCUSED
je tick_done
call ApplyInputCommands
cmp clockReset, 0
jne tick_done
call GetSimulationState
mov simulationState, eax
cmp eax, SIM_PAUSED
je tick_done
cmp eax, SIM_DIALOG
je tick_dialog
cmp eax, SIM_TRANSITION
je tick_transition
cmp eax, SIM_DEAD
je tick_dead
cmp mapErrorFrames, 0
je @F
dec mapErrorFrames
@@:
call UpdatePlayer
call IsGameplayBlocked
test eax, eax
jnz tick_cinematic
call UpdateEntities
call IsGameplayBlocked
test eax, eax
jnz tick_done
call UpdateCombat
call IsGameplayBlocked
test eax, eax
jnz tick_done
call Game_OnTick
call IsGameplayBlocked
test eax, eax
jnz tick_done
call UpdateDynamicLights
cmp gameToastTimer, 0
jle tick_done
dec gameToastTimer
jmp tick_done
tick_cinematic:
cmp isCutsceneActive, 0
je tick_done
call UpdateDynamicLights
jmp tick_done
tick_dialog:
mov al, keyAction
or al, keyDismiss
or al, keyAttack
jz tick_done
call CloseGameDialog
mov clockReset, 1
jmp tick_done
tick_transition:
call UpdateScreenFade
jmp tick_done
tick_dead:
cmp keyRestart, 0
je tick_done
call RestartCurrentRoom
tick_done:
call ConsumeInputEdges
ret
Engine_Tick ENDP
InitSimulationClock PROC
invoke QueryPerformanceFrequency, OFFSET qpcFrequency
invoke QueryPerformanceCounter, OFFSET qpcPrevious
mov DWORD PTR tickAccumulator, 0
mov DWORD PTR tickAccumulator+4, 0
mov clockReset, 0
ret
InitSimulationClock ENDP
AdvanceSimulation PROC uses ebx esi edi, deltaLow:DWORD, deltaHigh:DWORD
; Drop elapsed stalls over 250ms, backwards time, and explicit pause/focus
; debt. Bound catch-up to 8 ticks even on an overloaded machine.
cmp clockReset, 0
jne clock_discard
mov eax, DWORD PTR qpcFrequency
mov edx, DWORD PTR qpcFrequency+4
shrd eax, edx, 2
shr edx, 2
cmp deltaHigh, edx
ja clock_discard
jb clock_add
cmp deltaLow, eax
ja clock_discard
clock_add:
mov eax, deltaLow
mov ebx, 60
mul ebx
imul ebx, deltaHigh
add edx, ebx
add DWORD PTR tickAccumulator, eax
adc DWORD PTR tickAccumulator+4, edx
mov esi, 8
clock_tick:
mov edx, DWORD PTR tickAccumulator+4
cmp edx, DWORD PTR qpcFrequency+4
jb clock_done
ja clock_step
mov eax, DWORD PTR tickAccumulator
cmp eax, DWORD PTR qpcFrequency
jb clock_done
clock_step:
mov eax, DWORD PTR qpcFrequency
mov edx, DWORD PTR qpcFrequency+4
sub DWORD PTR tickAccumulator, eax
sbb DWORD PTR tickAccumulator+4, edx
call Engine_Tick
cmp clockReset, 0
jne clock_discard
dec esi
jnz clock_tick
clock_discard:
mov DWORD PTR tickAccumulator, 0
mov DWORD PTR tickAccumulator+4, 0
mov clockReset, 0
clock_done:
ret
AdvanceSimulation ENDP
PumpSimulationClock PROC uses ebx
invoke QueryPerformanceCounter, OFFSET qpcNow
mov eax, DWORD PTR qpcNow
mov edx, DWORD PTR qpcNow+4
sub eax, DWORD PTR qpcPrevious
sbb edx, DWORD PTR qpcPrevious+4
mov ebx, DWORD PTR qpcNow
mov DWORD PTR qpcPrevious, ebx
mov ebx, DWORD PTR qpcNow+4
mov DWORD PTR qpcPrevious+4, ebx
invoke AdvanceSimulation, eax, edx
ret
PumpSimulationClock ENDP

149
main.asm
View file

@ -112,13 +112,10 @@ 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_simulation.inc
include include/engine_preview.inc
.code
@ -136,7 +133,14 @@ WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
ret
handle_keydown:
; Key edges and ESC pause menu are handled via PollInput
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.
invoke DefWindowProcA, hWnd, uMsg, wParam, lParam
ret
@ -160,8 +164,7 @@ 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 InitLightingSystem
call LoadMapBinary ; Each room commits its own validated lighting
call LoadMapBinary ; Loads assets/map.dat (falls back to InitMap if missing)
; 2. Register Window Class
invoke GetModuleHandleA, 0
@ -208,8 +211,8 @@ Engine_Init PROC uses ebx esi edi
mov hEngineDC, eax
invoke GetTickCount
mov lastTickCount, eax
mov fpsTimer, eax
call InitSimulationClock
ret
Engine_Init ENDP
@ -223,30 +226,19 @@ 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, 8, 8, OFFSET strEngineName, COLOR_YELLOW
invoke DrawString8x8, 10, 10, OFFSET strEngineName, COLOR_YELLOW
; 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
; 2. FPS Counter
invoke DrawString8x8, 10, 24, OFFSET szFpsLabel, COLOR_CYAN
invoke DrawNumber8x8, 42, 24, currentFps, COLOR_CYAN
; 3. Controls Bar at Bottom (Debug mode only)
; 3. Controls Bar at Bottom
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
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)
; 4. Door / Portal Interaction Hint (center screen)
call CheckDoorHint
cmp eax, 1
je show_door_hint
@ -255,11 +247,11 @@ check_hints:
jmp check_minimap
show_door_hint:
invoke DrawString8x8, 116, 140, OFFSET strDoorHint, COLOR_YELLOW
invoke DrawString8x8, 128, 150, OFFSET strDoorHint, COLOR_YELLOW
jmp check_minimap
show_portal_hint:
invoke DrawString8x8, 110, 140, OFFSET strPortalHint, COLOR_CYAN
invoke DrawString8x8, 110, 150, OFFSET strPortalHint, COLOR_CYAN
check_minimap:
cmp mapErrorFrames, 0
@ -269,16 +261,23 @@ 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
; ------------------------------------------------------------------------------
; Engine_Frame: draws/presents only; simulation runs independently at 60 Hz.
; Engine_Frame: Updates single tick and presents frame buffer to window
; ------------------------------------------------------------------------------
Engine_Frame PROC uses ebx esi edi
; 1. Input & Physics Update
cmp mapErrorFrames, 0
je no_error_tick
dec mapErrorFrames
no_error_tick:
call PollInput
call UpdatePlayer
call UpdateEntities
call UpdateCombat
; 2. 3D Raycasting & 2D HUD Render
call Render3DScene
call Engine_RenderHUD
@ -287,7 +286,6 @@ Engine_Frame PROC uses ebx esi edi
call RenderCombatHUD
@@:
call ApplyCutsceneFade
call RenderPauseMenu
; 2b. Screenshot request (F12): dump fresh framebuffer to BMP
cmp keyScreenshot, 0
@ -322,6 +320,16 @@ Engine_Frame PROC uses ebx esi edi
mov fpsTimer, eax
frame_done:
; Reset one-frame input triggers
mov keyForward, 0
mov keyBackward, 0
mov keyStrafeLeft, 0
mov keyStrafeRight, 0
mov keyTurnLeft, 0
mov keyTurnRight, 0
mov keyAction, 0
mov keyAttack, 0
mov keyRestart, 0
ret
Engine_Frame ENDP
@ -356,14 +364,14 @@ ParseStartupMap PROC uses ebx ecx edx esi edi
skip_executable_quote:
lodsb
test al, al
jz check_cmdline_flags
jz startup_arg_done
cmp al, 34
jne skip_executable_quote
jmp skip_argument_spaces
skip_executable_word:
lodsb
test al, al
jz check_cmdline_flags
jz startup_arg_done
cmp al, 32
jne skip_executable_word
skip_argument_spaces:
@ -400,59 +408,10 @@ write_startup_char:
dec ecx
jnz startup_argument_loop
mov startupMapPath, 0
jmp check_cmdline_flags
ret
terminate_startup_arg:
mov byte ptr [edi], 0
; 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
@@:
startup_arg_done:
ret
ParseStartupMap ENDP
@ -461,8 +420,8 @@ ParseStartupMap ENDP
; ------------------------------------------------------------------------------
main PROC
LOCAL msg:MSG
LOCAL curTick:DWORD, elapsed:DWORD
call LoadProjectSettings
call ParseStartupMap
call TryEnginePreview
call Engine_Init
@ -485,12 +444,26 @@ main_loop:
jmp @B
process_game_tick:
call PollInput
call PumpSimulationClock
call Engine_Frame
; Frame Rate Limiter (~60 FPS)
invoke GetTickCount
mov curTick, eax
sub eax, lastTickCount
mov elapsed, eax
mov eax, targetFrameTimeMs
cmp elapsed, eax
jge do_tick
invoke Sleep, 1
jmp main_loop
do_tick:
mov eax, curTick
mov lastTickCount, eax
call Engine_Frame
jmp main_loop
exit_loop:
call StopCutscene
invoke ReleaseDC, hEngineWnd, hEngineDC

BIN
main.obj

Binary file not shown.

View file

@ -11,7 +11,7 @@ static void RunCutsceneChecks(EditorApp& app) {
};
Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference");
BinaryLevel level; std::string error;
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == MAP_FORMAT_VERSION && level.cutscene.events.size() == 9 &&
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == 4 && 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;

View file

@ -48,11 +48,11 @@ scene_trace_tick:
mov edi, esi
imul edi, 24
add edi, OFFSET sceneTrace
mov eax, csCameraX
mov eax, playerX
stosd
mov eax, csCameraY
mov eax, playerY
stosd
mov eax, csCameraAngle
mov eax, playerAngle
stosd
mov eax, cutsceneFade
stosd
@ -67,9 +67,9 @@ scene_trace_tick:
scene_trace_not_zero:
cmp esi, 2
jne scene_trace_not_two
CHECK csCameraX, 224 ; camera reaches exact interpolated X midpoint
CHECK csCameraY, 192 ; camera interpolates a negative Y delta
CHECK csCameraAngle, 0 ; pan wraps through zero along shortest arc
CHECK playerX, 224 ; camera reaches exact interpolated X midpoint
CHECK playerY, 192 ; camera interpolates a negative Y delta
CHECK playerAngle, 0 ; pan wraps through zero along shortest arc
CHECK cutsceneFade, 8 ; fade reaches midpoint
CHECK cutsceneSubtitle, 0 ; wait blocks a later hide command
scene_trace_not_two:
@ -93,7 +93,7 @@ scene_trace_not_two:
CHECK isCutsceneActive, 1 ; player update starts autoplay
CHECK playerX, 160 ; movement input does not move the player during the first scene frame
call UpdatePlayer
CHECK csCameraX, 192 ; next frame is driven only by cinematic interpolation
CHECK playerX, 192 ; next frame is driven only by cinematic interpolation
mov keySkipScene, 1
call UpdatePlayer
CHECK isCutsceneActive, 0 ; Tab skips and releases camera control
@ -105,14 +105,11 @@ scene_trace_not_two:
mov keyForward, 0
mov testFocus, 1
mov hEngineWnd, 1
; First regain focus with no scene command held; subsequent presses are real.
call PollInput
call ConsumeInputEdges
mov testKeys[VK_KEY_W*2], 8000h
mov testKeys[VK_KEY_TAB*2], 8000h
call PollInput
movzx eax, keyForward
CHECK eax, 1 ; polling records held movement independently of scene ownership
CHECK eax, 0 ; scene masks normal movement input
movzx eax, keySkipScene
CHECK eax, 1 ; skip input remains available in a scene
call StopCutscene
@ -205,7 +202,7 @@ scene_next_pixel:
CHECK fadeLevel, 16 ; transition preserves an existing cinematic fade without a bright flash
mov esi, 32
scene_transition_fade:
call UpdateScreenFade
call ApplyScreenFade
dec esi
jnz scene_transition_fade
CHECK spriteCount, 1 ; scripted transition loads the destination room

View file

@ -22,10 +22,8 @@ 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"
#include "studio_editor_checks.inl"
int main(int argc, char** argv) {
EditorApp app;
@ -141,7 +139,6 @@ 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");
@ -157,7 +154,6 @@ int main(int argc, char** argv) {
}
if(argc>2) RunViewportChecks(app,argv[2]);
RunStabilityEditorChecks();
RunStudioEditorChecks();
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
return failures ? 1 : 0;
}

View file

@ -17,19 +17,16 @@ 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_simulation.inc
include startup_parser.inc
include project_paths.inc
.data
resultCount dd 0
results dd 512 dup(0)
results dd 256 dup(0)
resultPath db 'engine_results.bin',0
anglePath db 'angles.bin',0
framePath db 'frame.bgra',0
@ -129,11 +126,7 @@ 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
include tests/simulation_engine_checks.inc
include tests/studio_engine_checks.inc
AuditMain PROC
call InitMathTables
@ -180,7 +173,7 @@ dirty_stack:
invoke SwitchSublevel, OFFSET mainPath, 2, 2, 0, 1
mov esi, 32
fade_frames:
call UpdateScreenFade
call ApplyScreenFade
dec esi
jnz fade_frames
CHECK fadeState, 0 ; fade finishes and releases player
@ -263,7 +256,6 @@ copy_missing_startup:
call PollInput
movzx eax, keyAction
CHECK eax, 1 ; action emitted on first key press
call ConsumeInputEdges
call PollInput
movzx eax, keyAction
CHECK eax, 0 ; held action does not repeat
@ -371,11 +363,8 @@ flat_depth_ok:
call Render3DScene
invoke Dump, OFFSET projectFramePath, OFFSET screenBuffer, FRAME_BUFFER_SIZE
call RunLightingChecks
call RunCutsceneChecks
call RunStabilityChecks
call RunSimulationChecks
call RunStudioChecks
mov eax, resultCount
inc eax
shl eax, 2

View file

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

View file

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

View file

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

View file

@ -1,471 +0,0 @@
.data
scheduleSnapshot dd 8 dup(0)
symmetryOK dd 1
speedOK dd 1
COMM cinematicReference:BYTE:FRAME_BUFFER_SIZE
.code
ResetSimulationFixture PROC
call ResetCombatFixture
mov spriteCount, 0
mov playerX, 800
mov playerY, 800
mov playerMoveSpeed, 3
mov playerRunSpeed, 6
mov playerRadius, 16
mov keyAttack, 0
mov keyForward, 1
mov simulationTicks, 0
mov lightingTick, 0
mov gameToastTimer, 100
mov mapErrorFrames, 0
mov DWORD PTR qpcFrequency, 60000
mov DWORD PTR qpcFrequency+4, 0
mov DWORD PTR tickAccumulator, 0
mov DWORD PTR tickAccumulator+4, 0
ret
ResetSimulationFixture ENDP
MoveSixtyTicks PROC uses esi
mov esi, 60
move_sixty_loop:
call UpdatePlayer
dec esi
jnz move_sixty_loop
ret
MoveSixtyTicks ENDP
RunSimulationChecks PROC uses ebx esi edi
call ResetSimulationFixture
mov playerAngle, 32
mov esi, 60
schedule_one:
invoke AdvanceSimulation, 1000, 0
call Render3DScene
dec esi
jnz schedule_one
mov eax, playerX
mov scheduleSnapshot, eax
mov eax, playerY
mov scheduleSnapshot[4], eax
mov eax, playerFractionX
mov scheduleSnapshot[8], eax
mov eax, playerFractionY
mov scheduleSnapshot[12], eax
CHECK simulationTicks, 60 ; QPC accumulator executes exactly sixty ticks per second
CHECK lightingTick, 60 ; lights advance once per simulation tick
CHECK gameToastTimer, 40 ; toast lifetime follows simulation rather than renders
call ResetSimulationFixture
mov playerAngle, 32
mov esi, 20
schedule_three:
invoke AdvanceSimulation, 3000, 0
call Render3DScene
call Render3DScene
dec esi
jnz schedule_three
mov ebx, scheduleSnapshot
CHECK playerX, ebx ; batched ticks preserve X under a different render schedule
mov ebx, scheduleSnapshot[4]
CHECK playerY, ebx ; batched ticks preserve Y under a different render schedule
mov ebx, scheduleSnapshot[8]
CHECK playerFractionX, ebx ; batched ticks preserve fractional X
mov ebx, scheduleSnapshot[12]
CHECK playerFractionY, ebx ; batched ticks preserve fractional Y
CHECK simulationTicks, 60 ; catch-up does not change total simulated time
CHECK gameToastTimer, 40 ; repeated drawing does not age toasts
invoke AdvanceSimulation, 60000, 0
CHECK simulationTicks, 60 ; long stalls discard timing debt instead of fast-forwarding
invoke AdvanceSimulation, 15000, 0
CHECK simulationTicks, 68 ; catch-up is bounded to eight ticks
CHECK DWORD PTR tickAccumulator, 0 ; overload discards excess catch-up debt
call ResetSimulationFixture
mov keyForward, 0
mov keyHud, 1
mov hudStyle, HUD_STYLE_CLEAN
invoke AdvanceSimulation, 100, 0
movzx eax, keyHud
CHECK eax, 1 ; pending edge survives a frame with no simulation tick
invoke AdvanceSimulation, 2900, 0
CHECK hudStyle, HUD_STYLE_DEBUG ; edge toggles exactly once across three catch-up ticks
movzx eax, keyHud
CHECK eax, 0 ; simulation consumes edge after its first tick
mov keyPause, 1
invoke AdvanceSimulation, 3000, 0
CHECK isGamePaused, 1 ; pause edge is applied inside the simulation
CHECK simulationTicks, 4 ; pause boundary discards remaining catch-up ticks
CHECK DWORD PTR tickAccumulator, 0 ; pause clears timing debt
mov ebx, lightingTick
mov keyAttack, 1
call Engine_Tick
CHECK lightingTick, ebx ; pause freezes dynamic lights
CHECK playerAmmo, 50 ; paused tick consumes attack without firing
mov keyMenu1, 1
call Engine_Tick
CHECK isGamePaused, 0 ; menu resume returns to gameplay without advancing world
mov clockReset, 0
mov gameDialogActive, 1
mov keyDismiss, 1
call Engine_Tick
CHECK gameDialogActive, 0 ; Enter dismisses dialogue on a simulation tick
CHECK lightingTick, ebx ; dialogue dismissal does not advance world timers
CHECK gameToastTimer, 97 ; pause and modal ticks preserve toast lifetime
call ResetSimulationFixture
call MoveSixtyTicks
CHECK playerX, 980 ; cardinal speed remains three world units per tick
CHECK playerY, 800 ; cardinal movement has no orthogonal drift
call ResetSimulationFixture
mov keyStrafeRight, 1
call MoveSixtyTicks
CHECK playerX, 927 ; diagonal input is normalized rather than sqrt-two faster
CHECK playerY, 927 ; diagonal normalization is symmetric on both axes
call ResetSimulationFixture
mov playerAngle, 32
call MoveSixtyTicks
mov ebx, playerX
sub ebx, 800
mov scheduleSnapshot, ebx
mov ebx, playerY
sub ebx, 800
mov scheduleSnapshot[4], ebx
call ResetSimulationFixture
mov playerAngle, 288
call MoveSixtyTicks
mov ebx, 800
sub ebx, scheduleSnapshot
CHECK playerX, ebx ; opposite angle has exactly symmetric accumulated X
mov ebx, 800
sub ebx, scheduleSnapshot[4]
CHECK playerY, ebx ; opposite angle has exactly symmetric accumulated Y
call ResetSimulationFixture
mov keyBackward, 1
mov keyStrafeLeft, 1
mov keyStrafeRight, 1
call MoveSixtyTicks
CHECK playerX, 800 ; opposing forward inputs cancel before rounding
CHECK playerY, 800 ; opposing strafe inputs cancel before rounding
call ResetSimulationFixture
mov playerX, 175
mov playerY, 184
mov byte ptr [gameMap+3*MAP_W+3], CELL_BRICK
call UpdatePlayer
CHECK playerX, 175 ; collision checks leading corners beyond the center cell
CHECK playerFractionX, 0 ; blocked axis discards its fractional remainder
mov keyStrafeLeft, 1
call UpdatePlayer
CHECK playerY, 182 ; blocked X still permits sliding on free Y
invoke CanOccupyPlayer, 8, 160
CHECK eax, 0 ; collision body cannot extend outside the map
mov byte ptr [gameMap+3*MAP_W+3], CELL_PORTAL
invoke CanOccupyPlayer, 184, 184
CHECK eax, 1 ; portal cells remain walkable for the complete collision body
mov byte ptr [gameMap+2*MAP_W+3], CELL_DOOR_OPEN
mov playerX, 180
mov playerY, 160
mov playerAngle, 0
call InteractDoor
movzx eax, byte ptr [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; door cannot close over an overlapping player body
mov playerX, 160
call InteractDoor
movzx eax, byte ptr [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR ; unoccupied door still closes normally
call ResetSimulationFixture
mov isCutsceneActive, 1
mov csCameraX, 184
mov csCameraY, 184
mov csCameraAngle, 128
mov byte ptr [gameMap+3*MAP_W+3], CELL_BRICK
call SelectRenderCamera
CHECK viewX, 184 ; renderer selects cinematic camera without replacing player pose
CHECK playerX, 800 ; camera ownership leaves gameplay position untouched
call StopCutscene
CHECK playerX, 800 ; unsafe cinematic handoff retains last safe gameplay position
CHECK playerAngle, 0 ; unsafe cinematic handoff also retains gameplay orientation
mov isCutsceneActive, 1
mov csCameraX, 864
mov csCameraY, 800
call StopCutscene
CHECK playerX, 864 ; walkable cinematic handoff transfers camera position
CHECK playerAngle, 128 ; walkable cinematic handoff transfers orientation
; Render routines must not dismiss, age or transition anything.
mov gameToastTimer, 12
mov gameDialogActive, 1
mov keyAction, 1
mov fadeState, 1
mov fadeLevel, 4
call RenderGameToast
call RenderGameDialog
call ApplyScreenFade
call ApplyScreenFade
CHECK gameToastTimer, 12 ; drawing toast never decrements its timer
CHECK gameDialogActive, 1 ; drawing modal never consumes input or closes it
CHECK fadeLevel, 4 ; drawing fade never advances transition level
CHECK fadeState, 1 ; drawing fade never switches room state
call ResetSimulationFixture
invoke LoadSublevelBinary, OFFSET subPath, 4, 6, 64
mov playerX, 600
mov playerHealth, 1
mov playerAmmo, 2
mov playerKeys, 3
mov playerScore, 700
mov isPlayerDead, 1
mov gameDialogActive, 1
mov playerFractionX, 12345
mov gameMode, GAME_MODE_ADVENTURE
mov enableWeapons, 0
call RestartCurrentRoom
CHECK eax, 1 ; current-room restart succeeds after a sublevel transition
CHECK playerX, 288 ; restart retains actual transition entry X rather than file spawn
CHECK playerY, 416 ; restart retains actual transition entry Y
CHECK playerAngle, 64 ; restart retains actual transition entry angle
CHECK spriteCount, 1 ; restart reloads current sublevel rather than startup room
CHECK playerHealth, 100 ; restart resets health after successful reload
CHECK playerAmmo, 50 ; restart resets ammunition after successful reload
CHECK playerKeys, 0 ; restart clears acquired keys
CHECK playerScore, 0 ; restart clears room score
CHECK playerFractionX, 0 ; restart clears fractional movement
CHECK gameDialogActive, 0 ; restart clears old modal dialogue
CHECK gameMode, GAME_MODE_ADVENTURE ; restart preserves configured game genre
CHECK enableWeapons, 0 ; restart preserves weapon configuration
; Replace only remembered path in this isolated fixture to force load failure.
mov DWORD PTR currentMapPath, 73696D61h
mov DWORD PTR currentMapPath+4, 676E6973h
mov DWORD PTR currentMapPath+8, 7461642Eh
mov BYTE PTR currentMapPath+12, 0
mov playerHealth, 17
mov playerAmmo, 9
mov playerFractionX, 222
mov isPlayerDead, 1
call RestartCurrentRoom
CHECK eax, 0 ; failed restart reports failure without procedural fallback
CHECK playerHealth, 17 ; failed restart preserves health transactionally
CHECK playerAmmo, 9 ; failed restart preserves ammunition transactionally
CHECK playerFractionX, 222 ; failed restart preserves movement remainder
CHECK isPlayerDead, 1 ; failed restart does not revive player in old room
CHECK spriteCount, 1 ; failed restart preserves active room entities
CHECK playerX, 288 ; failed restart preserves current camera pose
; Keys pressed while another application owns focus must not become commands.
call ResetSimulationFixture
mov hEngineWnd, 1
mov testFocus, 1
mov testKeys[VK_KEY_E*2], 0
mov testKeys[VK_KEY_CONTROL*2], 0
mov testKeys[VK_KEY_SPACE*2], 0
call PollInput
call ConsumeInputEdges
mov testFocus, 0
call PollInput
mov testKeys[VK_KEY_E*2], 8000h
mov testKeys[VK_KEY_CONTROL*2], 8000h
mov testFocus, 1
call PollInput
movzx eax, keyAction
CHECK eax, 0 ; focus regain suppresses action pressed while unfocused
movzx eax, keyAttack
CHECK eax, 0 ; focus regain suppresses attack pressed while unfocused
call ConsumeInputEdges
call PollInput
movzx eax, keyAttack
CHECK eax, 0 ; resynchronized held attack does not fire on the next poll
mov testKeys[VK_KEY_CONTROL*2], 0
call PollInput
mov testKeys[VK_KEY_CONTROL*2], 8000h
call PollInput
movzx eax, keyAttack
CHECK eax, 1 ; releasing and pressing again rearms attack after focus regain
call ResetSimulationFixture
invoke ShowGameDialog, 0, 0
CHECK clockReset, 1 ; opening a modal discards outstanding timing debt
mov clockReset, 0
call CloseGameDialog
CHECK clockReset, 1 ; closing a modal through the API discards timing debt
call ResetSimulationFixture
mov lightingMode, LIGHTING_MODE_DYNAMIC
mov isFlashlightOn, 1
mov isCutsceneActive, 1
mov csCameraX, 160
mov csCameraY, 160
mov csCameraAngle, 0
invoke SamplePointLighting, 224, 160, 0, 0
mov eax, lightRed
mov scheduleSnapshot, eax
mov isCutsceneActive, 0
mov playerX, 160
mov playerY, 160
invoke SamplePointLighting, 224, 160, 0, 0
mov ebx, scheduleSnapshot
CHECK lightRed, ebx ; cinematic flashlight matches preview camera rather than safe player pose
; Exhaust all half-turn pairs, including signed fractional remainders.
mov edi, 0
mov symmetryOK, 1
mov speedOK, 1
symmetry_angle:
call ResetSimulationFixture
mov playerAngle, edi
call MoveSixtyTicks
mov eax, playerX
sub eax, 800
mov scheduleSnapshot, eax
imul eax, eax
mov ebx, playerY
sub ebx, 800
mov scheduleSnapshot[4], ebx
imul ebx, ebx
add eax, ebx
cmp eax, 178*178
jb speed_mismatch
cmp eax, 181*181
jbe speed_valid
speed_mismatch:
mov speedOK, 0
speed_valid:
mov eax, playerFractionX
mov scheduleSnapshot[8], eax
mov eax, playerFractionY
mov scheduleSnapshot[12], eax
call ResetSimulationFixture
lea eax, [edi+256]
mov playerAngle, eax
call MoveSixtyTicks
mov eax, 800
sub eax, scheduleSnapshot
cmp playerX, eax
jne symmetry_mismatch
mov eax, 800
sub eax, scheduleSnapshot[4]
cmp playerY, eax
jne symmetry_mismatch
mov eax, scheduleSnapshot[8]
neg eax
cmp playerFractionX, eax
jne symmetry_mismatch
mov eax, scheduleSnapshot[12]
neg eax
cmp playerFractionY, eax
je symmetry_next
symmetry_mismatch:
mov symmetryOK, 0
symmetry_next:
inc edi
cmp edi, 256
jb symmetry_angle
CHECK symmetryOK, 1 ; all 512 directions retain opposite-position and fraction symmetry
CHECK speedOK, 1 ; all angular speeds remain within integer projection tolerance
call ResetSimulationFixture
mov playerX, 160
mov playerY, 160
mov byte ptr [gameMap+2*MAP_W+4], CELL_BRICK
mov cutsceneCount, 2
mov [cutsceneEvents].CutsceneEvent.tickTimestamp, 0
mov [cutsceneEvents].CutsceneEvent.opCode, OP_MOVE_CAM_TO
mov [cutsceneEvents].CutsceneEvent.paramA, 416
mov [cutsceneEvents].CutsceneEvent.paramB, 160
mov [cutsceneEvents].CutsceneEvent.dataId, 4
mov [cutsceneEvents+16].CutsceneEvent.tickTimestamp, 5
mov [cutsceneEvents+16].CutsceneEvent.opCode, OP_END
call StartCutscene
call UpdatePlayer
call UpdatePlayer
call UpdatePlayer
CHECK csCameraX, 288 ; cinematic path can cross a wall independently of the body
CHECK playerX, 160 ; wall-crossing camera path never teleports gameplay body
mov keySkipScene, 1
call UpdatePlayer
CHECK playerX, 160 ; skipping inside a wall retains safe player pose
mov keySkipScene, 0
call StartCutscene
mov esi, 6
wall_scene_tick:
call UpdatePlayer
dec esi
jnz wall_scene_tick
CHECK isCutsceneActive, 0 ; wall-crossing cinematic reaches END normally
CHECK playerX, 416 ; END hands off safe endpoint across the wall
call ResetSimulationFixture
mov playerMoveSpeed, 64
mov playerX, 175
mov playerY, 160
mov byte ptr [gameMap+2*MAP_W+3], CELL_BRICK
call UpdatePlayer
CHECK playerX, 175 ; high-speed movement cannot tunnel through a wall
call ResetSimulationFixture
mov keyRun, 1
call MoveSixtyTicks
CHECK playerX, 1160 ; sprint retains six world units per fixed tick
mov keyForward, 0
mov keyTurnLeft, 1
mov keyTurnRight, 1
call UpdatePlayer
CHECK playerAngle, 0 ; opposing turn inputs cancel
call ResetSimulationFixture
invoke LoadSublevelBinary, OFFSET subPath, 4, 6, 64
mov isPlayerDead, 1
mov keyRestart, 1
call Engine_Tick
CHECK isPlayerDead, 0 ; death R uses the shared current-room restart
CHECK playerX, 288 ; death restart restores transition entry pose
mov clockReset, 0
mov isGamePaused, 1
mov playerHealth, 7
mov keyMenu4, 1
call Engine_Tick
CHECK isGamePaused, 0 ; pause-menu restart resumes only after successful reload
CHECK playerHealth, 100 ; pause-menu restart uses the same transient reset
CHECK playerY, 416 ; pause-menu restart keeps the current room entry
call ResetSimulationFixture
; Frequency > 2^32 exercises carries in accumulator and elapsed multiplication.
mov DWORD PTR qpcFrequency, 8
mov DWORD PTR qpcFrequency+4, 1
invoke AdvanceSimulation, 71582788, 0
CHECK simulationTicks, 0 ; QPC scheduler keeps a fractional high-frequency tick
invoke AdvanceSimulation, 2, 0
CHECK simulationTicks, 1 ; QPC scheduler carries elapsed counts across DWORD boundaries
CHECK DWORD PTR tickAccumulator, 96 ; QPC scheduler retains exact high-frequency remainder
mov clockReset, 1
invoke AdvanceSimulation, 1000, 0
CHECK DWORD PTR tickAccumulator, 0 ; explicit clock boundary discards existing fractional debt
invoke AdvanceSimulation, -1, -1
CHECK simulationTicks, 1 ; backwards counter samples cannot fast-forward simulation
call ResetSimulationFixture
call InitLightingSystem
mov lightingMode, LIGHTING_MODE_DYNAMIC
mov isCutsceneActive, 1
mov csCameraX, 160
mov csCameraY, 160
mov csCameraAngle, 32
mov byte ptr [gameMap+3*MAP_W+6], CELL_BRICK
call Render3DScene
mov esi, OFFSET screenBuffer
mov edi, OFFSET cinematicReference
mov ecx, FRAME_BUFFER_SIZE/4
rep movsd
mov isCutsceneActive, 0
mov playerX, 160
mov playerY, 160
mov playerAngle, 32
call Render3DScene
mov esi, OFFSET screenBuffer
mov edi, OFFSET cinematicReference
mov ecx, FRAME_BUFFER_SIZE/4
repe cmpsd
sete al
movzx eax, al
CHECK eax, 1 ; cinematic lit framebuffer equals camera-only preview with separate gameplay body
ret
RunSimulationChecks ENDP

View file

@ -4,11 +4,6 @@ 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");

View file

@ -6,20 +6,9 @@ ResetCombatFixture PROC uses eax ecx edi
xor eax, eax
rep stosb
mov fadeState, 0
mov fadeLevel, 0
call ResetMovementFractions
mov isCutsceneActive, 0
mov cutscenePending, 0
mov isPlayerDead, 0
mov clockReset, 0
mov inputFocused, 1
call ConsumeInputEdges
mov keyRun, 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
@ -209,6 +198,5 @@ 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

View file

@ -1,106 +0,0 @@
static void RunStudioEditorChecks() {
EditorApp app;
app.LoadLevel("../assets/map.dat");
auto original=app.CaptureSnapshot();
auto before=original;
app.RecordLevelEdit(before,71);
Check(app.undoStack.empty(),"selection-only gesture creates no undo entry");
app.playerStartX=7;
app.RecordLevelEdit(before,71);
before=app.CaptureSnapshot(); app.playerStartX=8; app.RecordLevelEdit(before,71);
Check(app.undoStack.size()==1,"continuous inspector gesture creates exactly one snapshot");
app.RecordLevelEdit(app.CaptureSnapshot(),0);
before=app.CaptureSnapshot(); app.floorColor=0; app.RecordLevelEdit(before,72);
Check(app.undoStack.size()==2,"next property edit gets an independent snapshot");
app.Undo(); Check(app.playerStartX==8 && app.floorColor==original.floorColor,"undo palette preserves preceding transform");
app.Undo(); Check(EditorApp::SameLevel(app.CaptureSnapshot(),original),"undo continuous transform restores initial state");
app.Redo(); app.Redo(); Check(app.playerStartX==8 && app.floorColor==0,"redo restores both authored edits");
app.Undo(); before=app.CaptureSnapshot(); app.ceilingColor=123; app.RecordLevelEdit(before,0);
Check(app.redoStack.empty(),"new edit after undo invalidates redo branch");
before=app.CaptureSnapshot();
app.sprites[0].x+=10; app.sprites[0].scale=131072; app.sprites[0].textureId=13;
SetSpriteEntityData(app.sprites[0],ENTITY_ARCHETYPE_GUARD,55,3,8);
app.transitions[0].targetSpawnX=9; app.transitions[0].targetAngle=128;
strcpy_s(app.transitions[0].targetMap,"other.dat");
app.cutscene.flags=CUTSCENE_AUTOPLAY;
app.RecordLevelEdit(before,0);
app.draggingLightRadiusIdx=2; app.strokeSnapshotTaken=true; app.Undo();
Check(EditorApp::SameLevel(app.CaptureSnapshot(),before),"snapshot restores sprite parameters portal names and cutscene flags together");
Check(app.draggingLightRadiusIdx==-1 && !app.strokeSnapshotTaken,"undo cancels light-radius drag and paint stroke");
for(int i=0;i<80;++i) {before=app.CaptureSnapshot(); app.floorColor=i+1; app.RecordLevelEdit(before,0);}
Check(app.undoStack.size()==64,"history remains bounded to sixty-four snapshots");
BinaryLevel level; std::string error;
ReadLevelFile("../assets/map.dat",level,error);
level.cells[10*32+10]=CELL_DOOR;
SetSwitchDoor(level.sprites[0],10,10,true);
Check(ValidateLevel(level,error) && HasSwitchDoor(level.sprites[0]),"switch binding uses existing v5 sprite bytes");
level.cells[10*32+10]=CELL_EMPTY;
Check(!ValidateLevel(level,error),"switch binding cannot target a non-door cell");
level.cells[10*32+10]=CELL_DOOR;
level.sprites.push_back(level.sprites[0]);
Check(!ValidateLevel(level,error),"duplicate door bindings are rejected");
level.sprites.pop_back(); level.sprites[0].flags|=0x20000000;
Check(!ValidateLevel(level,error),"out-of-bounds switch target is rejected");
// Drive actual ImGui inspector controls, not a duplicate model of UI edits.
ImGui::CreateContext();
auto& io=ImGui::GetIO(); io.IniFilename=nullptr; io.DisplaySize=ImVec2(1400,1000); io.DeltaTime=1.0f/60;
unsigned char* pixels; int w,h; io.Fonts->GetTexDataAsRGBA32(&pixels,&w,&h);
app.LoadLevel("../assets/map.dat"); app.selectedSpriteIdx=-1; app.selectedTransitionIdx=-1;
auto frame=[&](const char* target, const char* text) {
if(text) io.AddInputCharactersUTF8(text);
ImGui::NewFrame();
ImGui::SetNextWindowPos(ImVec2(0,0)); ImGui::SetNextWindowSize(ImVec2(600,950));
ImGui::Begin("InspectorTest",nullptr,ImGuiWindowFlags_NoSavedSettings);
if(target) {
auto& g=*ImGui::GetCurrentContext();
g.NavActivateId=g.NavActivateDownId=ImGui::GetID(target); g.NavActivateFlags=ImGuiActivateFlags_PreferInput;
}
app.RenderInspector();
ImGui::End(); ImGui::Render();
};
frame(nullptr,nullptr);
frame("Spawn X",nullptr);
frame(nullptr,"7");
io.AddKeyEvent(ImGuiKey_Enter,true); frame(nullptr,nullptr);
io.AddKeyEvent(ImGuiKey_Enter,false); frame(nullptr,nullptr);
Check(app.playerStartX==7 && app.undoStack.size()==1,"real inspector spawn input records original authored state");
app.Undo(); Check(app.playerStartX==2,"real inspector spawn input can be undone");
app.Redo(); Check(app.playerStartX==7,"real inspector spawn input can be redone");
app.selectedSpriteIdx=0;
frame(nullptr,nullptr);
auto oldX=app.sprites[0].x; auto count=app.undoStack.size();
frame("Позиция X",nullptr); frame(nullptr,"6");
io.AddKeyEvent(ImGuiKey_Enter,true); frame(nullptr,nullptr);
io.AddKeyEvent(ImGuiKey_Enter,false); frame(nullptr,nullptr);
Check(app.sprites[0].x==384 && app.undoStack.size()==count+1,"real sprite inspector transform participates in history");
app.Undo(); Check(app.sprites[0].x==oldX,"real sprite inspector undo restores raw world coordinates");
app.selectedSpriteIdx=-1; app.selectedTransitionIdx=0;
auto oldSpawn=app.transitions[0].targetSpawnX; count=app.undoStack.size();
frame(nullptr,nullptr); frame("Целевой Spawn X",nullptr); frame(nullptr,"11");
io.AddKeyEvent(ImGuiKey_Enter,true); frame(nullptr,nullptr);
io.AddKeyEvent(ImGuiKey_Enter,false); frame(nullptr,nullptr);
Check(app.transitions[0].targetSpawnX==11 && app.undoStack.size()==count+1,"real portal destination input records a transaction");
app.Undo(); Check(app.transitions[0].targetSpawnX==oldSpawn,"real portal destination undo restores original spawn");
app.selectedTransitionIdx=-1;
auto oldPalette=app.CaptureSnapshot(); count=app.undoStack.size();
frame(nullptr,nullptr); frame("Хоррор",nullptr); frame(nullptr,nullptr);
Check(app.ceilingColor==0x003B0710 && app.undoStack.size()==count+1,"real environment preset records both colors once");
app.Undo(); Check(app.ceilingColor==oldPalette.ceilingColor && app.floorColor==oldPalette.floorColor,
"real environment preset undo restores ceiling and floor");
app.viewportMode=2; app.showCutsceneTimeline=false;
auto uiBefore=app.CaptureSnapshot(); count=app.undoStack.size();
for (float width : {1000.0f,1360.0f,1920.0f}) {
io.DisplaySize=ImVec2(width,840);
app.contentDrawerOpen=true;
ImGui::NewFrame(); app.RenderUI(); ImGui::Render();
ImGui::NewFrame(); app.RenderUI(); ImGui::Render();
}
Check(ImGui::GetDrawData()->TotalVtxCount>0 && app.editDepth==0,
"full workspace and content drawer render at supported window widths");
Check(EditorApp::SameLevel(uiBefore,app.CaptureSnapshot()) && app.undoStack.size()==count,
"workspace layout and content browsing do not dirty authored history");
ImGui::DestroyContext();
}

View file

@ -1,88 +0,0 @@
.data
bindingPath db 'binding.dat',0
badBindingPath db 'badbinding.dat',0
badBindingBoundsPath db 'badbindingbounds.dat',0
badBindingDuplicatePath db 'badbindingduplicate.dat',0
.code
RunStudioChecks PROC uses ebx esi edi
invoke LoadSublevelBinary, OFFSET bindingPath, -1, -1, 0
CHECK eax, 1 ; ASM loader accepts valid authored door binding
invoke LoadSublevelBinary, OFFSET badBindingPath, -1, -1, 0
CHECK eax, 0 ; ASM loader rejects switch binding to a non-door
invoke LoadSublevelBinary, OFFSET badBindingBoundsPath, -1, -1, 0
CHECK eax, 0 ; ASM loader rejects out-of-bounds switch target
invoke LoadSublevelBinary, OFFSET badBindingDuplicatePath, -1, -1, 0
CHECK eax, 0 ; ASM loader rejects duplicate switch target bindings
call ResetSimulationFixture
mov keyForward, 0
mov playerX, 160
mov playerY, 160
mov playerAngle, 0
mov spriteCount, 1
mov [spriteList].SpriteStruct.x, 160
mov [spriteList].SpriteStruct.y, 160
mov [spriteList].SpriteStruct.textureId, 12
mov [spriteList].SpriteStruct.scale, FIXED_ONE
; target (3,2), key-unlockable switch binding
mov [spriteList].SpriteStruct.flags, 0203A905h
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR
call InitEntities
mov playerKeys, 0
mov keyAction, 1
call InteractDoor
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR ; bound door stays locked without a key
movzx eax, keyAction
CHECK eax, 0 ; locked door consumes action before nearby switches
mov playerKeys, 2
call InteractDoor
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; key opens the explicitly authored locked door
CHECK playerKeys, 1 ; first unlock consumes exactly one key
call InteractDoor
call InteractDoor
CHECK playerKeys, 1 ; unlocked door never consumes a second key
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; unlocked door remains reusable
call InitEntities
movzx eax, BYTE PTR [doorUnlocked+2*MAP_W+3]
CHECK eax, 0 ; room entity initialization clears temporary unlocks
mov [spriteList].SpriteStruct.flags, 0203A805h
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR
mov keyAction, 1
call InteractDoor
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR ; switch-only door cannot be bypassed with keys
mov playerAngle, 256 ; face away from door so E reaches nearby switch
mov keyAction, 1
call UpdateEntities
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; authored switch opens its target door
CHECK entityState, 1 ; switch state reflects the resulting door state
mov keyAction, 1
call UpdateEntities
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR ; second switch action closes target door
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR_OPEN
mov playerX, 3*64-8 ; body overlaps target door
mov [spriteList].SpriteStruct.x, 3*64-8
mov keyAction, 1
call UpdateEntities
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; remote switch cannot close a door across player body
mov playerX, 160
mov playerAngle, 0
mov [spriteList].SpriteStruct.flags, 6405h ; legacy switch has no binding marker
mov BYTE PTR [gameMap+2*MAP_W+3], CELL_DOOR
call InitEntities
mov playerKeys, 0
mov keyAction, 1
call InteractDoor
call UpdateEntities
movzx eax, BYTE PTR [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; ordinary doors remain free on legacy maps
CHECK entityState, 0 ; one action cannot open a door and toggle a nearby legacy switch
invoke ToggleDoorCell, 32, 32
CHECK eax, 0 ; invalid switch targets are bounded at runtime
ret
RunStudioChecks ENDP

View file

@ -49,13 +49,6 @@ 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;

View file

@ -9,18 +9,16 @@ import subprocess
import time
ROOT = Path(__file__).resolve().parents[1]
SIZE, TEXTURES, PIXELS = 1362408, 2016, 1050592
SIZE, TEXTURES, PIXELS = 1357792, 2016, 1050592
def check_preview(engine, expected=None, output=None, level_path=None):
def check_preview(engine, expected=None, output=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, 2, 0, os.getpid(), 160, 1024, 0,
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 1, 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
@ -55,7 +53,7 @@ def check_preview(engine, expected=None, output=None, level_path=None):
assert subtitle!=frame, "Subtitle overlay missing"
# Compare the live IPC output with a direct Render3DScene regression dump.
if expected:
data=Path(level_path or ROOT/"assets/map.dat").read_bytes()
data=(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])
@ -67,16 +65,6 @@ def check_preview(engine, expected=None, output=None, level_path=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)

View file

@ -82,16 +82,6 @@ def fixtures(work):
orphan = bytearray(grid); orphan[2 * 32 + 4] = 10
level("orphan.dat", transitions=[], cells=orphan)
level("duplicate.dat", transitions=[(4, 2, b"sublevel_02.dat", 3, 2, 0, 1, 0)] * 2)
binding = bytearray(level("binding.dat",count=1,transitions=[]))
binding[38+2*32+3]=5
struct.pack_into("<I",binding,38+1024+20,0x0203A905)
(assets/"binding.dat").write_bytes(binding)
broken=bytearray(binding); broken[38+2*32+3]=0
(assets/"badbinding.dat").write_bytes(broken)
broken=bytearray(binding); struct.pack_into("<I",broken,38+1024+20,0x2003A905)
(assets/"badbindingbounds.dat").write_bytes(broken)
broken=bytearray(binding); struct.pack_into("<H",broken,16,2); broken+=broken[38+1024:]
(assets/"badbindingduplicate.dat").write_bytes(broken)
(assets / "badpack.bin").write_bytes(struct.pack("<IHH", 0x58544245, 1, 16) + bytes(12))
with wave.open(str(work / "editor/quiet.wav"), "wb") as sound:
sound.setnchannels(1); sound.setsampwidth(2); sound.setframerate(8000)
@ -160,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
("lighting_engine_checks.inc", "cutscene_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc", "simulation_engine_checks.inc", "studio_engine_checks.inc"))
("cutscene_engine_checks.inc", "stability_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)
@ -186,14 +176,10 @@ 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