Compare commits
11 commits
stage-1-st
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a56403d65 | |||
| aa64510def | |||
| 527d92411d | |||
| 2bac213ac5 | |||
| d3e4efc39b | |||
| 432fccfd60 | |||
| 1efa1195cf | |||
| c14ae884aa | |||
| 341966debe | |||
| 25bd6377a7 | |||
| 5c568b8cba |
50 changed files with 6468 additions and 931 deletions
128
AGENTS.md
128
AGENTS.md
|
|
@ -14,11 +14,12 @@
|
|||
```
|
||||
EBLAN_ENGINE/
|
||||
│
|
||||
├── main.asm # Entry point, Win32 window, message loop, 60 FPS frame ticker
|
||||
├── main.asm # Entry point, Win32 window, input/tick/render loop
|
||||
├── 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
|
||||
|
|
@ -52,12 +53,12 @@ build.bat
|
|||
## Core Engine Architecture
|
||||
|
||||
### 1. Frame Lifecycle
|
||||
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.
|
||||
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.
|
||||
3. `Render3DScene`:
|
||||
- Clears ceiling (`COLOR_CEILING`) and floor (`COLOR_FLOOR`).
|
||||
- Casts 320 rays across FOV (~67.5°).
|
||||
- Selects `viewX/Y/Angle` from player or separate `csCameraX/Y/Angle`, then 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.
|
||||
|
|
@ -66,6 +67,11 @@ Each tick (~60 times per second), `main.asm` executes:
|
|||
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)
|
||||
|
|
@ -137,7 +143,7 @@ invoke DrawNumber8x8, 120, 42, playerHealth, COLOR_WHITE
|
|||
The engine supports both procedural default generation and binary level loading. When `assets/map.dat` exists, `LoadMapBinary` in `include/engine_map.inc` automatically loads it:
|
||||
- **Header (`MapHeader`, 38 bytes, packed)**:
|
||||
- `magic` (uint32): `0x4E4C4245` ("EBLN")
|
||||
- `version` (uint16): `4` (readers also accept v1 fixed sprite coordinates and v2/v3 raw-coordinate maps, including early v2 maps with transitions)
|
||||
- `version` (uint16): `5` (readers also accept v1 fixed sprite coordinates and v2/v3/v4 raw-coordinate maps, including early v2 maps with transitions)
|
||||
- `mapWidth` (uint16): `32`
|
||||
- `mapHeight` (uint16): `32`
|
||||
- `playerStartX` (uint16): cell X coordinate (e.g. 2)
|
||||
|
|
@ -147,16 +153,17 @@ The engine supports both procedural default generation and binary level loading.
|
|||
- `ceilingColor` (uint32): 00RRGGBBh
|
||||
- `floorColor` (uint32): 00RRGGBBh
|
||||
- `transitionCount` (uint16): 0..16 (v1 ignores this formerly reserved field)
|
||||
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4 only)
|
||||
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4 only)
|
||||
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4 only)
|
||||
- `reserved[4]` (bytes): reserved padding
|
||||
- `cutsceneCount` (uint16): 0..128 commands (offset 28, v4+)
|
||||
- `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4+)
|
||||
- `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4+)
|
||||
- `lightCount` (uint16): 0..13 authored sources (offset 34, v5 only)
|
||||
- `lightingReserved` (uint16): zero (offset 36, v5 only)
|
||||
- **Grid Data**: `32 * 32 = 1024` bytes (row-major order). Cell value = texture slot (1 brick, 2 wood, 3 stone, 4 metal, 5 door, 7 pillar, 8 glass, 9 marble).
|
||||
- **Sprite Array**: `spriteCount * 24` bytes (`SpriteStruct`: `x`, `y` in raw world units, `textureId`, `scale` 16.16, `dist`, `flags`).
|
||||
- **Transition Array**: `transitionCount * 42` bytes (`LevelTransition` in both `engine_defs.inc` and `editor/src/level_format.h`). Target names are ASCII basenames, 5..31 bytes including `.dat`, with a terminating NUL inside `targetMap[32]`. Put linked maps in the same assets directory. Triggers must be unique and occupy empty, open-door or portal cells; every portal cell needs a transition.
|
||||
- Files are validated completely before replacing the active room. Counts above capacity, truncated/extra payload, invalid coordinates/scales, blocked spawns and invalid transition names are rejected. Black ceiling/floor colors are valid. A failed runtime transition keeps the current room; a failed startup load creates the procedural map.
|
||||
- **Cutscene Array (v4)**: after transitions, `cutsceneCount * 16` bytes; followed by `cutsceneStringCount * 96` bytes of NUL-terminated CP1251 strings. No serialized pointers. Earlier versions ignore the new header fields and load an empty track.
|
||||
- New saves use v4 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior).
|
||||
- New saves use v5 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior).
|
||||
|
||||
Human users can design levels visually using `run_editor.bat` (`editor/editor.exe`, written in C++17 + ImGui + DirectX 11), with 1-click Playtest (F5).
|
||||
AI agents can directly read or write `assets/map.dat` following this specification.
|
||||
|
|
@ -195,14 +202,20 @@ 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. 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.
|
||||
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.
|
||||
|
||||
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,357,792-byte shared-memory mailbox: 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots and a 320x240 DIB frame. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout.
|
||||
The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-...` process to execute the actual `Render3DScene`, cinematic overlay and fade procedures. No duplicate raycaster. `editor/src/preview_protocol.h` and `include/engine_preview.inc` define a versioned 1,362,408-byte shared-memory mailbox (protocol v2): 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots, a 320x240 DIB frame, 13 authored light records and 1024 baked RGB cells. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout.
|
||||
|
||||
The renderer validates camera and sprite bounds, uploads textures only when their revision changes, and exits if its parent process ends. The editor has process-exit/timeout checks and a retry button, converts DIB padding to opaque alpha, uploads BGRA to DX11 and displays with point sampling at 4:3. Map/inspector changes appear without saving. Timeline playback/scrubbing drives the viewport camera, subtitles and fade. Free-camera inspection affects no authored data. The viewport shows the scene and cinematic overlays; normal game HUD, door interactions and room switching are tested in F5.
|
||||
|
||||
|
|
@ -218,22 +231,95 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi
|
|||
- `3: Ammo Box`: collectible ammunition pickup (+20 rounds up to 100).
|
||||
- `4: Key`: key card for unlocking locked doors.
|
||||
- `5: Switch`: interactive trigger/lever toggled by proximity action ([E]).
|
||||
- `6: Explosive Barrel`: destructible barrel that explodes on weapon hit with 60 AOE damage to surrounding entities and fiery screen flash.
|
||||
- Packed into `SpriteStruct.flags` without altering binary map compatibility (`sizeof(SpriteEntry) == 24`).
|
||||
|
||||
2. **Player Combat & HUD (`include/engine_combat.inc`)**:
|
||||
- Weapon attack via `Space`, `Ctrl`, or Left Mouse Button (`VK_LBUTTON`).
|
||||
- Center crosshair and hitscan raycasting against active enemy guards.
|
||||
- Animated retro pistol with muzzle flash, recoil, and cooldowns.
|
||||
2. **Player Combat & Multi-Weapon Arsenal (`include/engine_combat.inc`)**:
|
||||
- Weapon switching via keys `[1]`, `[2]`, `[3]`:
|
||||
- **[1] Боевой нож (Combat Knife)**: fast melee attack (cooldown 8, 30 damage, no ammo required).
|
||||
- **[2] Пистолет (Pistol)**: standard handgun (cooldown 12, 35 damage, 1 ammo).
|
||||
- **[3] Дробовик (Pump Shotgun)**: heavy scatter weapon (cooldown 22, 70 damage, 2 ammo, wide spread).
|
||||
- Dynamic Muzzle Flash Room Illumination: shooting dynamically brightens ceiling and floor shading across the whole 3D environment for realistic muzzle flare lighting.
|
||||
- Center crosshair and hitscan raycasting against active enemy guards and barrels.
|
||||
- Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`.
|
||||
- Comprehensive combat HUD: Health bar, Ammo counter, Score counter, and Key indicator.
|
||||
- In-Game Retro Pause Menu via `[ESC]`: sleek semi-transparent pause menu with `[1] ПРОДОЛЖИТЬ`, `[2] ЗАНОВО`, `[3] МИНИ-КАРТА`, `[4] ВЫХОД В WINDOWS`.
|
||||
- Comprehensive combat HUD: Active weapon badge with slot indicators `[1] [2] [3]`, Health bar, Ammo counter, Score counter, and Key indicator.
|
||||
|
||||
3. **In-Editor MASM32 IDE (`F7` / "Код / IDE")**:
|
||||
- Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7` or the main menu.
|
||||
- Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7`, `Ctrl+B`, or the main menu.
|
||||
- MASM syntax highlighting (instructions, registers, directives, comments, strings, numbers).
|
||||
- Project file navigator (`engine_entity.inc`, `engine_combat.inc`, `main.asm`, etc.).
|
||||
- Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Custom HUD).
|
||||
- **Engine API Reference Catalog**: built-in searchable catalog of all engine procedures with 1-click **"Вставить"** snippet insertion.
|
||||
- Real-time search bar (`Поиск в коде...`) with instant match counter.
|
||||
- Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Explosive Barrel, Custom HUD).
|
||||
- Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines.
|
||||
|
||||
4. **Project Settings & Standalone Game Exporter (`editor/src/project_manager.h`)**:
|
||||
4. **Editor UI, 2D Canvas & Project Exporter (`editor/src/editor_app.cpp`)**:
|
||||
- Modern Game Studio dark theme with Electric Cyan and Neon Amber accents.
|
||||
- **2D Canvas Guard Aggro Circles**: visualizes guard detection and chase radius on the 2D map.
|
||||
- **Quick Entity Spawner Palette**: 1-click spawn buttons (`+ Враг`, `+ Аптечка`, `+ Патроны`, `+ Ключ`, `+ Рычаг`, `+ Бочка`, `+ Проп`).
|
||||
- Canvas Zoom buttons (`[-]`, `[100%]`, `[+]`) and pan controls.
|
||||
- Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map.
|
||||
- "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme.
|
||||
|
||||
5. **Gameplay Scripting Subsystem & Interactive Modals (`include/engine_gameplay.inc`)**:
|
||||
- `ShowGameToast(pszText, durationTicks, color)`: floating framed notification banners (auto-centered, custom CP1251 string, colored outline, auto-dismiss).
|
||||
- `ShowGameDialog(pszSpeaker, pszText)`: modal dialogue & lore note reader freezing player movement until dismissed with `[E]` / `[ENTER]`.
|
||||
- Extensible engine lifecycle hooks:
|
||||
- `Game_OnTick`: 60 Hz frame tick hook for custom game logic, timers, and puzzle triggers.
|
||||
- `Game_OnPlayerInteract(entityIdx, entityType)`: intercepts `[E]` interactions before default behavior.
|
||||
- `Game_OnEntityKilled(entityIdx, entityType)`: triggers on guard death or barrel explosion for boss encounters.
|
||||
- `Game_OnCustomHUD`: custom 2D canvas drawing (health bars, gauges, quest markers).
|
||||
- `PlayGameSFX(pszRelativePath)`: safe asynchronous WinMM audio player.
|
||||
- Built-in automatic gameplay toasts for keys, medkits, ammo, switches, doors, and explosions.
|
||||
|
||||
6. **Game Genres, Peaceful Adventure Mode & Immersive Clean HUD**:
|
||||
- `GAME_MODE_ADVENTURE (0)`: pure exploration / quest / horror / puzzle mode without weapons, ammo, crosshair, or combat overlays.
|
||||
- `GAME_MODE_ACTION (1)`: full combat shooter mode with weapon models, crosshair, health bar, and ammo counter.
|
||||
- `HUD_STYLE_CLEAN (0)`: 100% immersive full-screen view (no debug banners, no text collisions, no bottom controls clutter).
|
||||
- `HUD_STYLE_DEBUG (1)`: developer mode displaying FPS counter at top-right (X=250), engine title, score, and controls hints.
|
||||
- Instant in-game switching:
|
||||
- `[ESC] -> [2]`: toggle weapons / peaceful mode on the fly with toast notifications.
|
||||
- `[ESC] -> [3]`: toggle clean vs debug HUD.
|
||||
- `[F1]` or `[H]`: quick hotkey to toggle clean HUD vs debug HUD.
|
||||
- Editor toolbar 1-click genre switcher: `☮ КВЕСТ / БЕЗ ОРУЖИЯ` vs `⚔ ЭКШЕН / С ОРУЖИЕМ`.
|
||||
- Project settings (`project.eblanproj`) persist `GameMode`, `ShowWeapons`, and `ShowCrosshair`.
|
||||
|
||||
7. **Static Lightmap Baking & Realtime Dynamic Lighting (`include/engine_light.inc`)**:
|
||||
- `BakeMapLighting`: precomputes 1024 RGB cells in `lightGridRGB` from active STATIC sources only, with exact supercover cell-boundary occlusion, approximate distance attenuation and an out-of-place, wall-aware cross filter. `lightGrid` is a derived grayscale view. Realtime sources are never baked or counted twice.
|
||||
- Dynamic point lights (`LIGHT_TYPE_POINT`) with customizable color, radius, intensity, and organic torch flame flicker (`LIGHT_FLAG_FLICKER`).
|
||||
- Dynamic burst lights (`LIGHT_TYPE_BURST`): weapon muzzle flare on pistol/shotgun shots and high-intensity fiery explosion blast on explosive barrel detonation.
|
||||
- Player directional spotlight flashlight `[F]`: conical falloff (`flashlightConeAngle`), distance attenuation, and wall occlusion physics.
|
||||
- Dynamic lighting mode cycle `[L]`:
|
||||
- `LIGHTING_MODE_NONE (0)`: classic flat Wolf3D retro fullbright.
|
||||
- `LIGHTING_MODE_BAKED (1)`: static baked lightmap shadows and ambient shading.
|
||||
- `LIGHTING_MODE_DYNAMIC (2)`: full dynamic physics (point lights + player flashlight + muzzle flares + barrel blasts + lightmap).
|
||||
- EblanStudio Studio & Editor Lighting Tools:
|
||||
- 1-Click Toolbar Button: `💡 ЗАПЕЧЬ СВЕТ` (Bake Lightmap) for instant baking.
|
||||
- Toolbar Lighting Mode Switcher: `[☀ Свет: ВЫКЛ]`, `[🕯 Свет: ЗАПЕЧЁН]`, `[⚡ Свет: ДИНАМИКА]`.
|
||||
- Toolbar Flashlight Toggle: `[🔦 Фонарик: ВКЛ/ВЫКЛ]`.
|
||||
- 2D Canvas Lightmap Heatmap Overlay: `[🗺 Карта: ВКЛ/ВЫКЛ]` visualizing light and shadow gradients across the level grid.
|
||||
- Light Source Entities on 2D Canvas: interactive light markers with RGB tint, glow halo, radius circle, click-to-select, and real-time mouse dragging.
|
||||
- Quick Spawner Palette: `+ Источник света` button.
|
||||
- Scene Hierarchy: dedicated `💡 Источники света (Lights)` tree with selection and "+ Добавить свет".
|
||||
- Property Inspector: position X/Y sliders, radius slider, intensity slider, RGB color picker, light type dropdown (Static, Point, Torch flicker, Pulse), wall shadow-casting toggle, and torch flicker toggle.
|
||||
- Real-Time 3D Viewport synchronization via `EnginePreviewPacket` shared-memory protocol.
|
||||
|
||||
|
||||
### K. Authored Lighting Format (EBLN v5)
|
||||
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.
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -112,7 +112,7 @@ build_editor.bat
|
|||
|
||||
## Переходы и проверка первых трёх этапов
|
||||
|
||||
Карты новых сохранений используют EBLN v4; существующие v1/v2/v3 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
|
||||
Карты новых сохранений используют EBLN v5; существующие v1/v2/v3/v4 продолжают читаться. Связанные `.dat` лежат в одной папке. В портале задаётся ASCII-имя файла до 31 символа, целевой спавн, угол и затемнение. На карту доступно до 16 переходов. Заливка учитывает этот лимит целиком; удаление клетки удаляет её триггер. Чёрный пол и потолок поддерживаются.
|
||||
|
||||
Порталы имеют отдельную текстуру и не занимают слот фонаря. Первый импорт изображения создаёт недостающие папки и пакет со стандартными текстурами в остальных слотах. Повреждённый пакет не перезаписывается импортом; ошибка показывается в строке состояния. Неудачный переход сохраняет текущую комнату и показывает сообщение в игре.
|
||||
|
||||
|
|
@ -134,4 +134,16 @@ python tools\test_regressions.py
|
|||
|
||||
Предпросмотр останавливается на команде перехода в другую комнату; цепочка комнат и игровой ввод проверяются через F5. F5/F6 работают при фокусе интерфейса и текстовых полей. Ошибка запуска показывается отдельным сообщением. При старте редактор открывает карту проекта относительно своего файла, поэтому запуск из другой рабочей папки не ломает пути.
|
||||
|
||||
Формат v4 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`.
|
||||
Формат v5 и параметры команд описаны в [AGENTS.md](AGENTS.md) и [архитектуре движка](docs/ENGINE_ARCHITECTURE.md). Сценарии регрессии также проверяют сохранение/загрузку трека, повреждённые команды, Undo/Redo, упаковку WAV для F5, горячие клавиши при фокусе в интерфейсе, покадровое совпадение состояния C++ с ASM, передачу кадров в DX11 и точное совпадение пикселей игрового вида с движком. Перед запуском проверок соберите `engine.exe` через `build.bat`.
|
||||
|
||||
## Постоянный и динамический свет
|
||||
|
||||
В палитре выберите «+ Источник света» и поставьте его на карте. Источники доступны также в дереве сцены. В инспекторе задайте тип, цвет, радиус, яркость, тени и мерцание; работают перемещение, копирование, удаление и Undo/Redo.
|
||||
|
||||
- **Постоянный**: выберите тип «Постоянный (запекание)», затем нажмите «Запечь постоянный свет». RGB-освещение и тени сохраняются в карте. После изменения геометрии, фонового света или постоянного источника инспектор покажет, что нужно запечь заново. При сохранении и F5 это происходит автоматически.
|
||||
- **Динамический**: выберите точечный или пульсирующий источник и режим «Запечённый + динамический». Перемещение, цвет, яркость и мерцание сразу влияют на игровой вид. Перезапекать постоянный свет при этом не нужно.
|
||||
- **Режимы**: «Без освещения», «Только запечённый», «Запечённый + динамический». Фоновый свет и параметры фонарика настраиваются в разделе «Освещение карты» инспектора. В игре F переключает фонарик, L — режим света.
|
||||
|
||||
До 13 источников карты; фонарик, вспышки оружия и взрывов используют отдельные места. Закрытые двери и стены перекрывают динамический свет. Статические тени остаются результатом последнего запекания. Цвета стен и спрайтов освещаются по RGB; освещение плоских потолка/пола берётся в точке камеры. Запечённая карта имеет разрешение 32×32 ячейки.
|
||||
|
||||
Новые сохранения используют EBLN v5. Старые карты сохраняют прежний вид без освещения, пока вы не добавите источники и не включите нужный режим. Свет каждой комнаты сохраняется независимо. Перед экспортом игры сохраните карты.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,55 @@
|
|||
|
||||
---
|
||||
|
||||
### Симуляция и движение (этап 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 лучу на каждый вертикальный столбец экрана):
|
||||
|
|
@ -95,7 +144,7 @@
|
|||
|
||||
## 5. Загрузка карт и ресурсов
|
||||
|
||||
Новые карты сохраняются как EBLN v4: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
|
||||
Новые карты сохраняются как EBLN v5: заголовок 38 байт, сетка 1024 байта, спрайты по 24 байта, переходы по 42 байта, команды кат-сцены по 16 байт и строки по 96 байт. Чтение старых v1/v2/v3/v4 сохранено; ранние v2 с переходами поддерживаются. Загрузчики сначала проверяют весь файл во временных буферах, затем заменяют комнату. При ошибке перехода текущая комната сохраняется; при ошибке начальной загрузки создаётся процедурная карта. Значение цвета 0 — полноценный чёрный цвет.
|
||||
|
||||
Файлы связанных подуровней лежат рядом друг с другом. В `targetMap[32]` хранится только ASCII-имя `.dat` с завершающим нулём. Переход после появления в его ячейке повторно активируется только после выхода из неё. Двери и взаимодействие реагируют на отдельные нажатия.
|
||||
|
||||
|
|
@ -113,14 +162,25 @@ F5 запускает копию графа карт и ресурсов во в
|
|||
|
||||
`UpdatePlayer` обрабатывает автозапуск после завершения межкомнатного затемнения и ручной запуск клавишей C. При активной сцене управление движением, дверями и обычными порталами заблокировано. UpdateCutscene сначала обновляет уже начатые интерполяции и время показа текста, затем обрабатывает паузу и наступившие команды. Несколько команд с одним временем выполняются в порядке хранения: камера, поворот, текст, звук и затемнение могут работать одновременно. WAIT задерживает обработку следующих команд, но время и уже начатые интерполяции продолжаются.
|
||||
|
||||
Координаты камеры — сырые мировые единицы (64 на клетку). MOVE_CAM_TO использует линейную интерполяцию по длительности из `dataId`; PAN_CAM_TO — кратчайшую дугу угла 0..511 и длительность из `paramB`. Целочисленное умножение с последующим знаковым делением гарантирует точную конечную точку. Нулевая длительность означает мгновенную установку. Новая команда того же типа заменяет незавершённую интерполяцию, начиная с текущего состояния. Конечная клетка должна быть проходимой; сами отрезки не проверяют столкновения со стенами.
|
||||
Координаты камеры — сырые мировые единицы (64 на клетку), отдельные `csCameraX/Y/Angle`, не позиция игрока. Рендер выбирает `viewX/Y/Angle`; фонарик и выборки света следуют камере, как в предпросмотре. 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 и переход прекращают звук и снимают наложения. Положение камеры сохраняется при возврате управления; защита портала предотвращает немедленный обратный переход из текущей клетки.
|
||||
PLAY_SOUND использует `PlaySoundA` из уже подключённой WinMM, асинхронно и без системного звука при ошибке. Строка содержит только ASCII-имя WAV длиной до 63 символов, файл лежит в `assets/sounds`. Бит 0 `param8` включает повтор, индекс -1 останавливает звук. Канал один; новый WAV заменяет предыдущий. END, Tab и переход прекращают звук и снимают наложения. При END/Tab камера передаёт позицию и угол игроку только если всё collision body свободно; иначе сохраняется прежняя безопасная поза игрока. Дробные остатки сбрасываются; защита портала предотвращает немедленный обратный переход из текущей клетки.
|
||||
|
||||
CHANGE_SUBLEVEL ссылается на существующий переход по индексу и использует его имя карты, спавн, угол и настройку затемнения. Старый трек останавливается до загрузки новой комнаты. Уже достигнутая темнота сохраняется при начале затемнения перехода, чтобы избежать светлой вспышки. Новая комната может запустить собственную сцену после появления.
|
||||
|
||||
### Рестарт текущей комнаты
|
||||
|
||||
`RestartCurrentRoom` используется смертью + R и пунктом 4 паузы. Запоминаются
|
||||
фактический путь комнаты и entry spawn, включая override перехода. Сначала
|
||||
выполняется транзакционная загрузка; лишь после успеха сбрасываются сущности,
|
||||
HP/патроны, ключи/очки, боевые таймеры/вспышки, диалог/toast, затемнение, ожидающий
|
||||
переход и дробные остатки. Данные комнаты и авторский свет восстанавливаются из
|
||||
файла; жанр, настройка оружия/прицела, HUD и выбранное оружие сохраняются. Ошибка
|
||||
не заменяет комнату процедурной и не оживляет игрока; пауза остаётся. Процедурный
|
||||
стартовый fallback перезапускается процедурно.
|
||||
|
||||
## 7. Таймлайн EblanStudio
|
||||
|
||||
F6 или кнопка панели открывает таймлайн (`editor/src/editor_cutscene.inl`). Дорожки камеры, текста, звука и сцены показывают метки времени и перетаскиваемые ромбы. Команды с одинаковым временем можно выбрать через список «Команда». Инспектор редактирует время, длительность, координаты, углы, строки, WAV, повтор и целевой портал. Для камеры кнопка «Указать на карте» позволяет выбрать свободную клетку, не меняя сетку. END добавляется автоматически и переносится за последнее действие. Удаление портала удаляет или перенумеровывает ссылки сцены.
|
||||
|
|
@ -142,3 +202,17 @@ WASD, стрелки и правая кнопка мыши позволяют о
|
|||
F5/F6 обрабатываются до признака захвата клавиатуры ImGui — иначе фокус внутри редактора блокировал запуск. Ошибки подготовки и запуска F5 выводятся отдельным диалогом. Начальный путь карты определяется относительно расположения редактора и загружается при старте, независимо от рабочей папки процесса.
|
||||
|
||||
Проверки выполняют полный путь x86 ASM → раздел памяти → x64 редактор → текстура DX11 на устройстве WARP. Пиксели встроенного вида сравниваются с прямым вызовом обычного ASM-рендерера. Отдельный сценарий `python tools/test_preview_ipc.py` проверяет обновление стен и текстур, субтитры, затемнение и завершение без открытия окон. Для полного регрессионного набора предварительно соберите движок.
|
||||
|
||||
## 9. Запекание и динамический RGB-свет (24.10.2026)
|
||||
|
||||
Запечённый свет отделён от динамических источников. `lighting_format.h` выполняет запекание в редакторе; `engine_lighting_core.inc` содержит эквивалентный ASM-вариант и расчёт света при рендеринге. Постоянные источники (STATIC=0) входят в 1024 RGB-ячейки `lightGridRGB`. Точечные (POINT=1), пульсирующие (PULSE=4), мерцание, фонарик и временные вспышки прибавляются только при динамическом режиме. Это исключает повторное прибавление света. Источник цвета 0 излучает чёрный свет; белый задаётся явно как 00FFFFFFh.
|
||||
|
||||
Оба запекателя используют расстояние `max(abs(dx),abs(dy)) + 3*min(abs(dx),abs(dy))/8`, затухание `(radius-distance)*intensity/radius`, RGB-цвет и насыщение каналов. `CastLightRay` проверяет точные пересечения границ клеток; при пересечении угла проверяются обе соседние клетки. Порталы и открытые двери пропускают свет. Флаг SHADOWS отключает проверку препятствий для выбранного источника. Сглаживание выполняется в отдельном буфере с весом 2 для центра и 1 для доступных соседей, без усреднения сквозь стену.
|
||||
|
||||
В v5 после строк кат-сцены добавлены 32 байта настроек, lightCount записей по 40 байт и 4096 байт RGB-карты. Поля lightCount/reserved занимают смещения 34/36 прежнего 38-байтового заголовка. Настройки: режим, фон, включение фонаря, его радиус, интенсивность, полуугол, baked-valid=1, reserved=0 (все DWORD). Загрузка транзакционная, включая освещение: повреждённая комната не заменяет действующую. Старые v1..v4 сбрасывают источники и сохраняют классическое освещение. После перехода загружается свет новой комнаты; старые источники и вспышки очищаются.
|
||||
|
||||
Игровой вид использует протокол EBPV v2 размером 1 362 408 байт: прежние смещения пикселей и текстур сохранены, после кадра добавлены 13 источников и RGB-карта. Источники и фонарик синхронизируются при каждом запросе; время мерцания/пульсации задаётся 60-Гц счётчиком. Перезапекания на каждом кадре нет. В игре обновление использует время симуляции, а запечённая карта читается непосредственно из файла. У стен освещение берётся у видимой грани, у спрайтов — в их позиции; фонарик проверяет и конус, и заслонение. Плоские потолок/пол используют один образец в точке камеры, без проекции света по каждому пикселю пола.
|
||||
|
||||
Слоты 1..13 принадлежат источникам карты; 0 зарезервирован фонарю, 14 вспышке выстрела, 15 взрыву. Вспышки не могут перезаписать авторские источники. Режимы света, источники, RGB-данные и состояние запекания входят в Undo/Redo. Изменение постоянного света/геометрии/фона помечает результат устаревшим; динамическое движение не требует запекания. Сохранение и F5 автоматически обновляют устаревший результат.
|
||||
|
||||
Регрессии проверяют цвет, заслонение стенами/дверями/углами, флаги теней, отсутствие двойного освещения, пульсацию и затухание вспышек, сохранение v5, отказ от повреждённых данных, старые карты, Undo/Redo и реальное обновление игрового вида. Все 1024 RGB-ячейки сравниваются между C++ и ASM. Кадры сцены с авторским светом из встроенного вида сравниваются с прямым рендерингом той же карты в ASM.
|
||||
|
|
|
|||
|
|
@ -49,19 +49,188 @@ 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,25 +1,40 @@
|
|||
[Window][Workspace]
|
||||
Pos=0,60
|
||||
Size=1536,708
|
||||
LastUsed=20261022
|
||||
Pos=0,64
|
||||
Size=2560,1288
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
LastUsed=20261022
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Toolbar]
|
||||
Pos=0,20
|
||||
Size=1536,38
|
||||
LastUsed=20261022
|
||||
Size=2560,42
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][StatusBar]
|
||||
Pos=0,768
|
||||
Size=1536,32
|
||||
LastUsed=20261022
|
||||
Pos=0,1352
|
||||
Size=2560,32
|
||||
LastUsed=20261024
|
||||
|
||||
[Window][Не удалось запустить игру]
|
||||
Pos=649,356
|
||||
Size=237,80
|
||||
LastUsed=20261022
|
||||
|
||||
[Window][Редактор кода и скриптов MASM32 - EblanStudio IDE]
|
||||
Pos=60,60
|
||||
Size=900,650
|
||||
LastUsed=20261022
|
||||
|
||||
[Window][Менеджер ассетов (Texture & Audio Manager)]
|
||||
Pos=61,60
|
||||
Size=820,580
|
||||
LastUsed=20261023
|
||||
|
||||
[Window][Редактор кода MASM32 & Игровых скриптов (F7)]
|
||||
Pos=60,60
|
||||
Size=1020,680
|
||||
LastUsed=20261024
|
||||
|
||||
|
|
|
|||
14
editor/project.eblanproj
Normal file
14
editor/project.eblanproj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Project]
|
||||
Title=EBLAN_GAME
|
||||
Author=Developer
|
||||
Version=1.0.0
|
||||
StartupMap=assets/map.dat
|
||||
WindowTitle=EBLAN ENGINE 3D - Standalone Retro Game
|
||||
DefaultWidth=1280
|
||||
DefaultHeight=960
|
||||
GameMode=1
|
||||
ShowWeapons=1
|
||||
ShowCrosshair=1
|
||||
LightingMode=2
|
||||
AmbientLight=40
|
||||
Flashlight=1
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@
|
|||
#include "level_format.h"
|
||||
#include "cutscene_preview.h"
|
||||
#include "preview_protocol.h"
|
||||
#include "lighting_format.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -22,7 +23,18 @@ enum ToolMode {
|
|||
TOOL_ERASER,
|
||||
TOOL_FILL,
|
||||
TOOL_PLAYER_SPAWN,
|
||||
TOOL_SPRITE_SPAWN
|
||||
TOOL_SPRITE_SPAWN,
|
||||
TOOL_LIGHT_SPAWN
|
||||
};
|
||||
|
||||
enum LightPreset {
|
||||
LIGHT_PRESET_TORCH = 0,
|
||||
LIGHT_PRESET_LAMP,
|
||||
LIGHT_PRESET_ALARM,
|
||||
LIGHT_PRESET_CANDLE,
|
||||
LIGHT_PRESET_NEON,
|
||||
LIGHT_PRESET_TOXIC,
|
||||
LIGHT_PRESET_SPOTLIGHT
|
||||
};
|
||||
|
||||
// Full level snapshot for undo/redo (grid + spawn + palette + sprites + transitions)
|
||||
|
|
@ -33,6 +45,10 @@ struct LevelSnapshot {
|
|||
std::vector<SpriteEntry> sprites;
|
||||
std::vector<LevelTransition> transitions;
|
||||
CutsceneTrack cutscene;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
LightingSettings lightingSettings;
|
||||
std::array<uint32_t,1024> bakedRGB;
|
||||
uint64_t bakedSignature=0;
|
||||
};
|
||||
|
||||
class EditorApp {
|
||||
|
|
@ -67,15 +83,24 @@ 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;
|
||||
|
|
@ -96,12 +121,43 @@ public:
|
|||
// Editor state
|
||||
ToolMode currentTool = TOOL_BRUSH;
|
||||
CellType selectedPaletteCell = CELL_BRICK;
|
||||
uint8_t pendingSpawnArchetype = ENTITY_ARCHETYPE_PROP;
|
||||
int selectedSpriteIdx = -1;
|
||||
int selectedTransitionIdx = -1;
|
||||
int selectedLightIdx = -1;
|
||||
int draggingLightIdx = -1;
|
||||
bool isEngineRunning = false;
|
||||
std::string statusMessage = "Готов к работе. EblanStudio v1.2";
|
||||
std::string currentFilePath = "..\\assets\\map.dat";
|
||||
|
||||
// Lighting Subsystem State & Light Sources
|
||||
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
|
||||
int ambientLightLevel = 40;
|
||||
bool isFlashlightOn = true;
|
||||
int flashlightRadius = 384;
|
||||
int flashlightIntensity = 240;
|
||||
int flashlightCone = 45;
|
||||
bool showLightHeatmap = false;
|
||||
bool autoBakeLighting = true;
|
||||
int draggingLightRadiusIdx = -1;
|
||||
uint8_t bakedLightmap[MAP_TOTAL_CELLS] = { 0 };
|
||||
std::array<uint32_t,1024> bakedLightRGB{};
|
||||
uint64_t bakedLightingSignature = 0;
|
||||
std::vector<EditorLightSource> lightSources;
|
||||
|
||||
void BakeLighting();
|
||||
void InitDefaultLights();
|
||||
void ApplyLightPreset(int lightIdx, int presetId);
|
||||
void SnapLightToGrid(int lightIdx);
|
||||
bool CastLightRay(int x1, int y1, int x2, int y2) const;
|
||||
bool LightingNeedsBake() const;
|
||||
LightingSettings GetLightingSettings() const;
|
||||
|
||||
// Panel Dimensions & Splitters (Interactive Resizing like Unreal / Unity)
|
||||
float leftPanelWidth = 260.0f;
|
||||
float rightPanelWidth = 350.0f;
|
||||
float centerSplitRatio = 0.5f;
|
||||
|
||||
// Viewport pan & zoom
|
||||
float canvasZoom = 20.0f; // pixels per tile
|
||||
float canvasPanX = 20.0f;
|
||||
|
|
@ -112,6 +168,20 @@ 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();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <windows.h>
|
||||
#include "../imgui/imgui.h"
|
||||
|
||||
|
|
@ -17,14 +18,19 @@ struct CodeFileEntry {
|
|||
};
|
||||
|
||||
static const CodeFileEntry g_ProjectCodeFiles[] = {
|
||||
{ "engine_entity.inc (Entity AI & Pickups)", "include/engine_entity.inc" },
|
||||
{ "engine_combat.inc (Player Weapons & Combat)", "include/engine_combat.inc" },
|
||||
{ "main.asm (Engine Entry & Main Loop)", "main.asm" },
|
||||
{ "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" },
|
||||
{ "engine_light.inc (Light Sources & Bursts)", "include/engine_light.inc" },
|
||||
{ "engine_lighting_core.inc (Static RGB Bake & Dynamic Lighting)", "include/engine_lighting_core.inc" },
|
||||
{ "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" },
|
||||
{ "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" },
|
||||
{ "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" },
|
||||
{ "main.asm (Engine Entry & Frame Loop)", "main.asm" },
|
||||
{ "engine_defs.inc (Constants & Structs)", "include/engine_defs.inc" },
|
||||
{ "engine_data.inc (Memory & Game State)", "include/engine_data.inc" },
|
||||
{ "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" },
|
||||
{ "engine_render2d.inc (2D Canvas & HUD)", "include/engine_render2d.inc" },
|
||||
{ "engine_input.inc (Keyboard Input)", "include/engine_input.inc" },
|
||||
{ "engine_physics.inc (Player Physics)", "include/engine_physics.inc" }
|
||||
{ "engine_raycaster.inc (3D DDA Raycaster)", "include/engine_raycaster.inc" },
|
||||
{ "engine_input.inc (Keyboard & Mouse)", "include/engine_input.inc" },
|
||||
{ "engine_physics.inc (Player Movement)", "include/engine_physics.inc" }
|
||||
};
|
||||
static const int g_CodeFileCount = sizeof(g_ProjectCodeFiles) / sizeof(g_ProjectCodeFiles[0]);
|
||||
|
||||
|
|
@ -34,6 +40,39 @@ struct ScriptTemplate {
|
|||
};
|
||||
|
||||
static const ScriptTemplate g_ScriptTemplates[] = {
|
||||
{
|
||||
"NPC Диалог и квест (ShowGameDialog)",
|
||||
"; ==============================================================================\n"
|
||||
"; NPC Quest Dialogue: Displays conversation modal and gives objective\n"
|
||||
"; ==============================================================================\n"
|
||||
".data\n"
|
||||
"szNpcSpeaker db \"СТАРИК\", 0\n"
|
||||
"szNpcText db \"ВОЗЬМИ СИНИЙ КЛЮЧ И ОТКРОЙ ЗАПЕРТУЮ ДВЕРЬ НА СЕВЕРЕ!\", 0\n"
|
||||
".code\n"
|
||||
" invoke ShowGameDialog, OFFSET szNpcSpeaker, OFFSET szNpcText\n"
|
||||
" invoke ShowGameToast, OFFSET szKeyPickedToast, 90, COLOR_CYAN\n"
|
||||
},
|
||||
{
|
||||
"Записка / Терминал с лором (Lore Note)",
|
||||
"; ==============================================================================\n"
|
||||
"; Readable Note / Terminal: Player reads lore note on [E]\n"
|
||||
"; ==============================================================================\n"
|
||||
".data\n"
|
||||
"szNoteTitle db \"ДНЕВНИК УЧЕНОГО\", 0\n"
|
||||
"szNoteBody db \"ЭКСПЕРИМЕНТ ВЫШЕЛ ИЗ-ПОД КОНТРОЛЯ. КОД ОТ ЛИФТА: 4 9 2 1.\", 0\n"
|
||||
".code\n"
|
||||
" invoke ShowGameDialog, OFFSET szNoteTitle, OFFSET szNoteBody\n"
|
||||
},
|
||||
{
|
||||
"Всплывающее уведомление (Game Toast)",
|
||||
"; ==============================================================================\n"
|
||||
"; Floating Toast Banner (auto-centered with colored outline and fade)\n"
|
||||
"; ==============================================================================\n"
|
||||
".data\n"
|
||||
"szSecretToast db \"СЕКРЕТНАЯ КОМНАТА ОБНАРУЖЕНА!\", 0\n"
|
||||
".code\n"
|
||||
" invoke ShowGameToast, OFFSET szSecretToast, 120, COLOR_YELLOW\n"
|
||||
},
|
||||
{
|
||||
"Патрулирующий охранник (Patrol Guard AI)",
|
||||
"; ==============================================================================\n"
|
||||
|
|
@ -78,6 +117,15 @@ static const ScriptTemplate g_ScriptTemplates[] = {
|
|||
" invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR\n"
|
||||
"switch_done:\n"
|
||||
},
|
||||
{
|
||||
"Взрывная бочка (AOE Explosion Trigger)",
|
||||
"; ==============================================================================\n"
|
||||
"; Explosive Barrel: Deals 60 AOE damage and triggers fiery screen flash\n"
|
||||
"; ==============================================================================\n"
|
||||
" mov [entityState + ecx*4], ENTITY_STATE_DEAD\n"
|
||||
" mov playerHitFlash, 8\n"
|
||||
" add playerScore, 200\n"
|
||||
},
|
||||
{
|
||||
"Кастомный элемент HUD (Custom Gauge Widget)",
|
||||
"; ==============================================================================\n"
|
||||
|
|
@ -90,6 +138,42 @@ static const ScriptTemplate g_ScriptTemplates[] = {
|
|||
};
|
||||
static const int g_ScriptTemplateCount = sizeof(g_ScriptTemplates) / sizeof(g_ScriptTemplates[0]);
|
||||
|
||||
struct EngineApiItem {
|
||||
const char* category;
|
||||
const char* name;
|
||||
const char* description;
|
||||
const char* snippet;
|
||||
};
|
||||
|
||||
static const EngineApiItem g_EngineApiCatalog[] = {
|
||||
{ "Геймплей и Скрипты", "ShowGameToast", "Всплывающий баннер с цветной рамкой и затуханием", "invoke ShowGameToast, OFFSET szMyMessage, 90, COLOR_YELLOW\n" },
|
||||
{ "Геймплей и Скрипты", "ShowGameDialog", "Диалоговое окно NPC или чтение записки", "invoke ShowGameDialog, OFFSET szSpeaker, OFFSET szText\n" },
|
||||
{ "Геймплей и Скрипты", "CloseGameDialog", "Принудительно закрыть активное диалоговое окно", "call CloseGameDialog\n" },
|
||||
{ "Геймплей и Скрипты", "PlayGameSFX", "Асинхронное воспроизведение звука WAV", "invoke PlayGameSFX, OFFSET szMySoundWav\n" },
|
||||
{ "Геймплей и Скрипты", "Game_OnPlayerInteract", "Хук взаимодействия [E] (1=обработано, 0=дефолт)", "Game_OnPlayerInteract PROC uses ebx ecx, entityIdx:DWORD, entityTypeVal:DWORD\n ; Ваша логика по нажатию [E]\n xor eax, eax\n ret\nGame_OnPlayerInteract ENDP\n" },
|
||||
{ "Геймплей и Скрипты", "Game_OnEntityKilled", "Хук уничтожения врага/бочки (триггеры боссов)", "Game_OnEntityKilled PROC uses ebx ecx, entityIdx:DWORD, entityTypeVal:DWORD\n ; Смерть сущности\n ret\nGame_OnEntityKilled ENDP\n" },
|
||||
{ "Геймплей и Скрипты", "Game_OnTick", "Покадровый хук 60 Hz (таймеры, зоны, квесты)", "Game_OnTick PROC uses ebx ecx\n ; Ваша покадровая логика\n ret\nGame_OnTick ENDP\n" },
|
||||
|
||||
{ "Бой и оружие", "UpdateCombat", "Обновляет прицел, стрельбу и расчет урона", "call UpdateCombat\n" },
|
||||
{ "Бой и оружие", "RenderCombatHUD", "Рисует оружие, прицел и статус игрока", "call RenderCombatHUD\n" },
|
||||
{ "Бой и оружие", "Смена оружия", "Устанавливает активное оружие игрока", "mov playerWeapon, WEAPON_SHOTGUN ; 0=Knife, 1=Pistol, 2=Shotgun\n" },
|
||||
{ "Бой и оружие", "Нанесение урона", "Уменьшает здоровье врага по индексу", "mov ecx, targetIdx\nmov eax, [entityHealth + ecx*4]\nsub eax, 35\nmov [entityHealth + ecx*4], eax\n" },
|
||||
|
||||
{ "Сущности и AI", "InitEntities", "Сбрасывает и спавнит сущности уровня", "call InitEntities\n" },
|
||||
{ "Сущности и AI", "UpdateEntities", "AI преследования, патруля и подбор предметов", "call UpdateEntities\n" },
|
||||
{ "Сущности и AI", "Патруль врага", "Инкремент таймера контрольной точки патруля", "mov ecx, [entityTimer + edx*4]\ninc ecx\nmov [entityTimer + edx*4], ecx\n" },
|
||||
|
||||
{ "2D Рендеринг", "DrawRect", "Рисует закрашенный прямоугольник (X, Y, W, H, Color)", "invoke DrawRect, 10, 20, 80, 12, 00336699h\n" },
|
||||
{ "2D Рендеринг", "DrawRectOutline", "Рисует рамку прямоугольника", "invoke DrawRectOutline, 10, 20, 80, 12, COLOR_WHITE\n" },
|
||||
{ "2D Рендеринг", "DrawString8x8", "Выводит строку шрифтом 8x8 (CP1251)", "invoke DrawString8x8, 10, 22, OFFSET myString, COLOR_YELLOW\n" },
|
||||
{ "2D Рендеринг", "DrawNumber8x8", "Выводит целое число на экран", "invoke DrawNumber8x8, 60, 22, myNumberVar, COLOR_WHITE\n" },
|
||||
|
||||
{ "Рэйкастинг и Мир", "tableCos / tableSin", "Чтение тригонометрии 16.16 (угол 0..511)", "mov eax, playerAngle\nand eax, ANGLE_MASK\nmov ebx, [tableCos + eax*4]\nmov edx, [tableSin + eax*4]\n" },
|
||||
{ "Рэйкастинг и Мир", "depthBuffer", "Глубина стен для спрайтового z-теста", "mov eax, [depthBuffer + screenX*4]\n" },
|
||||
{ "Рэйкастинг и Мир", "CheckWallCollision", "Проверка коллизии с ячейкой карты", "invoke CheckWallCollision, targetX, targetY\n" }
|
||||
};
|
||||
static const int g_EngineApiCount = sizeof(g_EngineApiCatalog) / sizeof(g_EngineApiCatalog[0]);
|
||||
|
||||
struct CodeEditorState {
|
||||
int currentFile = 0;
|
||||
std::string fileContent;
|
||||
|
|
@ -106,6 +190,10 @@ struct CodeEditorState {
|
|||
int buildExitCode = 0;
|
||||
bool hasBuildResult = false;
|
||||
|
||||
// Search bar state
|
||||
char searchBuf[64] = "";
|
||||
int searchMatchCount = 0;
|
||||
|
||||
struct ParsedError {
|
||||
std::string file;
|
||||
int line = 0;
|
||||
|
|
@ -193,8 +281,9 @@ struct CodeEditorState {
|
|||
buildLog.clear();
|
||||
buildErrors.clear();
|
||||
hasBuildResult = false;
|
||||
buildExitCode = 0;
|
||||
|
||||
fs::path root(projectRoot);
|
||||
fs::path root = fs::path(projectRoot);
|
||||
fs::path buildBat = root / "build.bat";
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildBat = root.parent_path() / "build.bat";
|
||||
|
|
@ -202,7 +291,7 @@ struct CodeEditorState {
|
|||
}
|
||||
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n";
|
||||
buildLog = "[ERROR] Не найден скрипт сборки build.bat в: " + buildBat.string() + "\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
return;
|
||||
|
|
@ -319,14 +408,13 @@ struct CodeEditorState {
|
|||
if (!std::isdigit(static_cast<unsigned char>(c))) { isNum = false; break; }
|
||||
}
|
||||
if (isNum) {
|
||||
ParsedError err{};
|
||||
ParsedError err;
|
||||
err.file = line.substr(0, openParen);
|
||||
err.line = std::atoi(lineStr.c_str());
|
||||
err.message = line.substr(colon + 3);
|
||||
buildErrors.push_back(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
|
|
@ -340,25 +428,23 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
g_CodeEditor.PollBuild();
|
||||
if (!showCodeEditorModal) return;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Top Menu Bar
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
|
||||
// Menu Bar
|
||||
if (ImGui::BeginMenuBar()) {
|
||||
if (ImGui::BeginMenu("Файл")) {
|
||||
if (ImGui::MenuItem("Сохранить (Ctrl+S)", "Ctrl+S")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
if (ImGui::MenuItem("Собрать движок (F7)", "F7")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
if (ImGui::MenuItem("Собрать проект (F7 / Ctrl+B)", "Ctrl+B")) {
|
||||
g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
|
@ -371,7 +457,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
if (ImGui::BeginMenu("Шаблоны кода")) {
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::MenuItem(g_ScriptTemplates[i].title)) {
|
||||
// Append template code to text buffer
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
|
||||
|
|
@ -386,12 +471,15 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
// Toolbar Buttons
|
||||
ImGui::BeginGroup();
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
RenderCodeEditorContents(root);
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorApp::RenderCodeEditorContents(const std::string& root) {
|
||||
g_CodeEditor.PollBuild();
|
||||
|
||||
// Top Toolbar
|
||||
ImGui::BeginGroup();
|
||||
if (ImGui::Button("Сохранить (Ctrl+S)")) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
|
|
@ -401,51 +489,110 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::Button("Компиляция...");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Собрать движок (F7)")) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.55f, 0.22f, 1.0f));
|
||||
if (ImGui::Button("Собрать проект (F7)")) {
|
||||
g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine();
|
||||
|
||||
// Search bar
|
||||
ImGui::SetNextItemWidth(200);
|
||||
if (ImGui::InputTextWithHint("##SearchCode", "Поиск в коде...", g_CodeEditor.searchBuf, sizeof(g_CodeEditor.searchBuf))) {
|
||||
if (g_CodeEditor.searchBuf[0]) {
|
||||
int count = 0;
|
||||
const char* p = g_CodeEditor.textBuffer.data();
|
||||
while ((p = std::strstr(p, g_CodeEditor.searchBuf)) != nullptr) {
|
||||
count++;
|
||||
p += std::strlen(g_CodeEditor.searchBuf);
|
||||
}
|
||||
g_CodeEditor.searchMatchCount = count;
|
||||
} else {
|
||||
g_CodeEditor.searchMatchCount = 0;
|
||||
}
|
||||
}
|
||||
if (g_CodeEditor.searchBuf[0]) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "[%d совп.]", g_CodeEditor.searchMatchCount);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("| %s", g_CodeEditor.statusText.c_str());
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Split Left (File list) and Right (Editor + Output)
|
||||
// Split Left (Navigator & API Catalog) and Right (Editor + Output)
|
||||
ImGui::Columns(2, "CodeEditorColumns", true);
|
||||
static bool initCol = true;
|
||||
if (initCol) {
|
||||
ImGui::SetColumnWidth(0, 240);
|
||||
ImGui::SetColumnWidth(0, 270);
|
||||
initCol = false;
|
||||
// Load initial file
|
||||
g_CodeEditor.LoadFile(0, root);
|
||||
}
|
||||
|
||||
// Left Column: Source File List
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:");
|
||||
for (int i = 0; i < g_CodeFileCount; ++i) {
|
||||
bool isSelected = (g_CodeEditor.currentFile == i);
|
||||
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
|
||||
g_CodeEditor.LoadFile(i, root);
|
||||
// Left Column: Tabbed Navigation (Files / API Catalog / Templates)
|
||||
ImGui::BeginTabBar("NavTabs");
|
||||
|
||||
if (ImGui::BeginTabItem("Файлы")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:");
|
||||
for (int i = 0; i < g_CodeFileCount; ++i) {
|
||||
bool isSelected = (g_CodeEditor.currentFile == i);
|
||||
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
|
||||
g_CodeEditor.LoadFile(i, root);
|
||||
}
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Быстрые шаблоны:");
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::SmallButton(g_ScriptTemplates[i].title)) {
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.isModified = true;
|
||||
if (ImGui::BeginTabItem("Справочник API")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.85f, 1.0f, 1.0f), "Функции и процедуры движка:");
|
||||
ImGui::BeginChild("ApiCatalogChild", ImVec2(0, 0), false);
|
||||
for (int i = 0; i < g_EngineApiCount; ++i) {
|
||||
ImGui::PushID(i);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%s", g_EngineApiCatalog[i].name);
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 65);
|
||||
if (ImGui::SmallButton("Вставить")) {
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t sLen = std::strlen(g_EngineApiCatalog[i].snippet);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + sLen + 1024);
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), g_EngineApiCatalog[i].snippet);
|
||||
g_CodeEditor.isModified = true;
|
||||
g_CodeEditor.statusText = std::string("Вставлен код: ") + g_EngineApiCatalog[i].name;
|
||||
}
|
||||
ImGui::TextDisabled(" %s", g_EngineApiCatalog[i].description);
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Шаблоны")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.5f, 1.0f), "Быстрые шаблоны скриптов:");
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::Button(g_ScriptTemplates[i].title, ImVec2(-1, 26))) {
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.isModified = true;
|
||||
g_CodeEditor.statusText = std::string("Вставлен шаблон: ") + g_ScriptTemplates[i].title;
|
||||
}
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Right Column: Code Editor & Compiler Console
|
||||
|
|
@ -453,7 +600,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
float editorHeight = availY * 0.65f;
|
||||
float outputHeight = availY - editorHeight - 20;
|
||||
|
||||
// Multiline Text Editor with monospace font
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Файл: %s %s",
|
||||
g_ProjectCodeFiles[g_CodeEditor.currentFile].relativePath.c_str(),
|
||||
g_CodeEditor.isModified ? "*" : "");
|
||||
|
|
@ -470,10 +616,10 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Hotkey: Ctrl+S to save
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S)) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
// Hotkey: Ctrl+S to save, Ctrl+B to build
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::GetIO().KeyCtrl) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_S)) g_CodeEditor.SaveFile(root);
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_B)) g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
|
@ -483,9 +629,8 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
if (ImGui::BeginTabItem("Вывод компилятора (ml.exe)")) {
|
||||
ImGui::BeginChild("CompilerOutputChild", ImVec2(0, outputHeight), true);
|
||||
if (g_CodeEditor.buildLog.empty() && !g_CodeEditor.isBuilding) {
|
||||
ImGui::TextDisabled("Нажмите 'Собрать движок (F7)' для компиляции ассемблерного кода...");
|
||||
ImGui::TextDisabled("Нажмите 'Собрать проект (F7)' для компиляции ассемблерного кода...");
|
||||
} else {
|
||||
// Display log line by line with color tags
|
||||
size_t logStart = 0;
|
||||
while (logStart < g_CodeEditor.buildLog.size()) {
|
||||
size_t logEnd = g_CodeEditor.buildLog.find('\n', logStart);
|
||||
|
|
@ -525,7 +670,6 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
snprintf(lineNumBuf, sizeof(lineNumBuf), "%d", err.line);
|
||||
std::string label = err.file + ":" + lineNumBuf + " - " + err.message;
|
||||
if (ImGui::Selectable(label.c_str())) {
|
||||
// Switch to corresponding file if matched
|
||||
for (int f = 0; f < g_CodeFileCount; ++f) {
|
||||
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
|
||||
g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) {
|
||||
|
|
@ -542,5 +686,4 @@ void EditorApp::RenderCodeEditorWindow() {
|
|||
ImGui::EndTabBar();
|
||||
|
||||
ImGui::Columns(1);
|
||||
ImGui::End();
|
||||
}
|
||||
|
|
|
|||
268
editor/src/editor_lighting.inl
Normal file
268
editor/src/editor_lighting.inl
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
void EditorApp::ApplyLightPreset(int lightIdx, int presetId) {
|
||||
if (lightIdx < 0 || lightIdx >= static_cast<int>(lightSources.size())) return;
|
||||
PushUndo();
|
||||
auto& l = lightSources[lightIdx];
|
||||
l.flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS;
|
||||
switch (presetId) {
|
||||
case LIGHT_PRESET_TORCH: // 🕯 Факел
|
||||
l.color = 0x00FFB040;
|
||||
l.radius = 240;
|
||||
l.intensity = 220;
|
||||
l.curIntensity = 220;
|
||||
l.lightType = LIGHT_TYPE_POINT;
|
||||
l.flags |= LIGHT_FLAG_FLICKER;
|
||||
break;
|
||||
case LIGHT_PRESET_LAMP: // 💡 Лампа
|
||||
l.color = 0x00E0E8FF;
|
||||
l.radius = 280;
|
||||
l.intensity = 240;
|
||||
l.curIntensity = 240;
|
||||
l.lightType = LIGHT_TYPE_STATIC;
|
||||
break;
|
||||
case LIGHT_PRESET_ALARM: // 🚨 Тревожная сирена
|
||||
l.color = 0x00FF2020;
|
||||
l.radius = 300;
|
||||
l.intensity = 250;
|
||||
l.curIntensity = 250;
|
||||
l.lightType = LIGHT_TYPE_PULSE;
|
||||
break;
|
||||
case LIGHT_PRESET_CANDLE: // 🕯 Мягкая свеча
|
||||
l.color = 0x00FF9520;
|
||||
l.radius = 140;
|
||||
l.intensity = 180;
|
||||
l.curIntensity = 180;
|
||||
l.lightType = LIGHT_TYPE_POINT;
|
||||
l.flags |= LIGHT_FLAG_FLICKER;
|
||||
break;
|
||||
case LIGHT_PRESET_NEON: // 🔮 Киберпанк неон
|
||||
l.color = 0x0000E5FF;
|
||||
l.radius = 260;
|
||||
l.intensity = 230;
|
||||
l.curIntensity = 230;
|
||||
l.lightType = LIGHT_TYPE_POINT;
|
||||
break;
|
||||
case LIGHT_PRESET_TOXIC: // ☢ Токсик
|
||||
l.color = 0x0039FF14;
|
||||
l.radius = 250;
|
||||
l.intensity = 220;
|
||||
l.curIntensity = 220;
|
||||
l.lightType = LIGHT_TYPE_PULSE;
|
||||
break;
|
||||
case LIGHT_PRESET_SPOTLIGHT: // 🔦 Прожектор
|
||||
l.color = 0x00FFFFFF;
|
||||
l.radius = 380;
|
||||
l.intensity = 255;
|
||||
l.curIntensity = 255;
|
||||
l.lightType = LIGHT_TYPE_STATIC;
|
||||
break;
|
||||
}
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
statusMessage = "Применен пресет света.";
|
||||
}
|
||||
|
||||
void EditorApp::SnapLightToGrid(int lightIdx) {
|
||||
if (lightIdx < 0 || lightIdx >= static_cast<int>(lightSources.size())) return;
|
||||
PushUndo();
|
||||
auto& l = lightSources[lightIdx];
|
||||
int cellX = std::clamp(l.x / 64, 0, MAP_GRID_W - 1);
|
||||
int cellY = std::clamp(l.y / 64, 0, MAP_GRID_H - 1);
|
||||
l.x = cellX * 64 + 32;
|
||||
l.y = cellY * 64 + 32;
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
statusMessage = "Источник света выровнен по центру клетки (" + std::to_string(cellX) + ", " + std::to_string(cellY) + ").";
|
||||
}
|
||||
|
||||
void EditorApp::RenderLightingSettings() {
|
||||
auto edited = GetLightingSettings();
|
||||
bool changed = false, activated = false;
|
||||
auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); };
|
||||
|
||||
const char* modes[] = { "Без освещения (Fullbright)", "Только запечённый свет (Baked)", "Запечённый + Динамический (Full Physics)" };
|
||||
int mode = edited.mode;
|
||||
changed |= ImGui::Combo("Режим света", &mode, modes, 3); undoItem(); edited.mode = mode;
|
||||
|
||||
int ambient = edited.ambient;
|
||||
changed |= ImGui::SliderInt("Фоновый свет (Ambient)", &ambient, 0, 255); undoItem(); edited.ambient = ambient;
|
||||
|
||||
bool flash = (edited.flashlight != 0);
|
||||
changed |= ImGui::Checkbox("Фонарик игрока [F]", &flash); undoItem(); edited.flashlight = flash;
|
||||
if (flash) {
|
||||
int radius = edited.flashlightRadius, intensity = edited.flashlightIntensity, cone = edited.flashlightCone;
|
||||
changed |= ImGui::SliderInt("Дальность фонаря", &radius, 32, 2048); undoItem();
|
||||
changed |= ImGui::SliderInt("Яркость фонаря", &intensity, 0, 255); undoItem();
|
||||
changed |= ImGui::SliderInt("Полуугол луча", &cone, 1, 96); undoItem();
|
||||
edited.flashlightRadius = radius; edited.flashlightIntensity = intensity; edited.flashlightCone = cone;
|
||||
}
|
||||
|
||||
if (activated) PushUndo();
|
||||
if (changed) {
|
||||
lightingMode = edited.mode;
|
||||
ambientLightLevel = edited.ambient;
|
||||
isFlashlightOn = (edited.flashlight != 0);
|
||||
flashlightRadius = edited.flashlightRadius;
|
||||
flashlightIntensity = edited.flashlightIntensity;
|
||||
flashlightCone = edited.flashlightCone;
|
||||
if (autoBakeLighting) BakeLighting();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Checkbox("Авто-запекание (Live Realtime Update)", &autoBakeLighting);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Мгновенно обновляет карту теней и 3D вид при любых изменениях источников и стен.");
|
||||
|
||||
ImGui::Checkbox("Карта запечённого света (Хитмап на 2D сетке)", &showLightHeatmap);
|
||||
|
||||
if (ImGui::Button("💡 Запечь постоянный свет сейчас", ImVec2(-1, 26))) {
|
||||
PushUndo();
|
||||
BakeLighting();
|
||||
}
|
||||
|
||||
if (LightingNeedsBake()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.2f, 1.0f), "⚠ Геометрия или источники изменены: требуется запекание");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "✔ Постоянный свет запечён (1024 ячейки)");
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::RenderLightingInspector() {
|
||||
if (selectedLightIdx < 0 || selectedLightIdx >= static_cast<int>(lightSources.size())) return;
|
||||
auto& light = lightSources[selectedLightIdx];
|
||||
|
||||
const char* typeDesc = (light.lightType == LIGHT_TYPE_STATIC) ? "Постоянный (Запекаемый)" :
|
||||
(light.lightType == LIGHT_TYPE_PULSE) ? "Пульсирующий (Сирена)" :
|
||||
(light.flags & LIGHT_FLAG_FLICKER) ? "Мерцающий факел" : "Точечный (Динамический)";
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.25f, 1.0f), "Источник света #%d из %d: %s",
|
||||
selectedLightIdx + 1, static_cast<int>(lightSources.size()), typeDesc);
|
||||
ImGui::Separator();
|
||||
|
||||
// Quick Action Bar (2x2 Grid)
|
||||
float colW2 = (ImGui::GetContentRegionAvail().x - 6.0f) * 0.5f;
|
||||
if (ImGui::Button("Фокус (F)", ImVec2(colW2, 24))) { FocusSelectedEntity(); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Дублировать (Ctrl+D)", ImVec2(colW2, 24))) { DuplicateSelectedEntity(); 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ bool EditorApp::EnsureEnginePreview() {
|
|||
if (!previewPacket) return fail("Не удалось открыть буфер игрового вида.");
|
||||
// Initialize in place: no large packet on the thread's stack.
|
||||
memset(previewPacket, 0, sizeof(*previewPacket));
|
||||
previewPacket->magic = 0x56504245; previewPacket->version = 1; previewPacket->parentPid = GetCurrentProcessId();
|
||||
previewPacket->magic = 0x56504245; previewPacket->version = 2; previewPacket->parentPid = GetCurrentProcessId();
|
||||
std::string command = "\"" + engine + "\" \"--preview:" + name + "\"";
|
||||
STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE;
|
||||
if (!CreateProcessA(engine.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
|
||||
|
|
@ -105,6 +105,14 @@ void EditorApp::UpdateEnginePreview() {
|
|||
if (!sprites.empty()) memcpy(previewPacket->sprites,sprites.data(),previewPacket->spriteCount*sizeof(SpriteEntry));
|
||||
previewPacket->cinematic = scene && cutscenePreview.active;
|
||||
previewPacket->fade = scene ? cutscenePreview.fade : 0;
|
||||
previewPacket->lightingMode = static_cast<uint32_t>(lightingMode);
|
||||
previewPacket->ambientLight = static_cast<uint32_t>(ambientLightLevel);
|
||||
previewPacket->flashlight = isFlashlightOn ? 1 : 0;
|
||||
previewPacket->lightCount = static_cast<uint32_t>((std::min)(lightSources.size(), size_t(MAX_EDITOR_LIGHTS)));
|
||||
previewPacket->flashlightRadius=flashlightRadius;previewPacket->flashlightIntensity=flashlightIntensity;previewPacket->flashlightCone=flashlightCone;
|
||||
if(!lightSources.empty()) memcpy(previewPacket->lights,lightSources.data(),previewPacket->lightCount*sizeof(EditorLightSource));
|
||||
memcpy(previewPacket->bakedRGB,bakedLightRGB.data(),4096);
|
||||
previewPacket->lightTick=static_cast<uint32_t>(GetTickCount64()*60/1000);
|
||||
memset(previewPacket->subtitle,0,96);
|
||||
if (scene && cutscenePreview.subtitle >= 0 && cutscenePreview.subtitle < static_cast<int>(cutscene.strings.size())) {
|
||||
std::string encoded;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// ==============================================================================
|
||||
|
||||
constexpr uint32_t MAP_MAGIC = 0x4E4C4245; // 'EBLN'
|
||||
constexpr uint16_t MAP_FORMAT_VERSION = 4; // v4 adds a cutscene track and CP1251 string pool
|
||||
constexpr uint16_t MAP_FORMAT_VERSION = 5; // v5 adds authored lighting and baked RGB cells
|
||||
constexpr int MAP_GRID_W = 32;
|
||||
constexpr int MAP_GRID_H = 32;
|
||||
constexpr int MAP_TOTAL_CELLS = MAP_GRID_W * MAP_GRID_H; // 1024 bytes
|
||||
|
|
@ -48,7 +48,8 @@ enum EntityArchetype : uint8_t {
|
|||
ENTITY_ARCHETYPE_AMMO = 3, // Ammo pickup (+20 Ammo)
|
||||
ENTITY_ARCHETYPE_KEY = 4, // Door Key pickup
|
||||
ENTITY_ARCHETYPE_SWITCH = 5, // Interactive Switch / Trigger
|
||||
ENTITY_ARCHETYPE_COUNT = 6
|
||||
ENTITY_ARCHETYPE_BARREL = 6, // Explosive Barrel (AOE Blast)
|
||||
ENTITY_ARCHETYPE_COUNT = 7
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
|
@ -65,7 +66,7 @@ struct LevelTransition {
|
|||
|
||||
struct MapHeader {
|
||||
uint32_t magic; // 'EBLN'
|
||||
uint16_t version; // Current writer: 4; reader also accepts v1/v2/v3
|
||||
uint16_t version; // Current writer: 5; reader also accepts v1/v2/v3/v4
|
||||
uint16_t mapWidth; // 32
|
||||
uint16_t mapHeight; // 32
|
||||
uint16_t playerStartX; // Cell coordinate (0..31)
|
||||
|
|
@ -78,7 +79,8 @@ struct MapHeader {
|
|||
uint16_t cutsceneCount; // v4 only; old versions ignore the remaining header fields
|
||||
uint16_t cutsceneStringCount;
|
||||
uint16_t cutsceneFlags; // bit 0 = start automatically after entering this map
|
||||
uint8_t reserved[4]; // Total header remains 38 bytes
|
||||
uint16_t lightCount; // v5 only, authored lights (slots 1..13 in engine)
|
||||
uint16_t lightingReserved; // must be zero in v5; header remains 38 bytes
|
||||
};
|
||||
|
||||
struct CutsceneEvent {
|
||||
|
|
@ -101,9 +103,21 @@ 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;
|
||||
|
|
@ -136,3 +150,53 @@ static_assert(sizeof(MapHeader) == 38, "EBLN header layout changed");
|
|||
static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed");
|
||||
static_assert(sizeof(LevelTransition) == 42, "EBLN transition layout changed");
|
||||
static_assert(sizeof(CutsceneEvent) == 16, "EBLN cutscene event layout changed");
|
||||
|
||||
// Lighting Modes & Types (mirrors include/engine_defs.inc)
|
||||
enum LightingMode : uint32_t {
|
||||
LIGHTING_MODE_NONE = 0,
|
||||
LIGHTING_MODE_BAKED = 1,
|
||||
LIGHTING_MODE_DYNAMIC = 2
|
||||
};
|
||||
|
||||
enum LightType : uint32_t {
|
||||
LIGHT_TYPE_STATIC = 0,
|
||||
LIGHT_TYPE_POINT = 1,
|
||||
LIGHT_TYPE_FLASHLIGHT = 2,
|
||||
LIGHT_TYPE_BURST = 3,
|
||||
LIGHT_TYPE_PULSE = 4
|
||||
};
|
||||
|
||||
constexpr uint32_t LIGHT_FLAG_INACTIVE = 0;
|
||||
constexpr uint32_t LIGHT_FLAG_ACTIVE = 1;
|
||||
constexpr uint32_t LIGHT_FLAG_SHADOWS = 2;
|
||||
constexpr uint32_t LIGHT_FLAG_FLICKER = 4;
|
||||
|
||||
constexpr int MAX_EDITOR_LIGHTS = 13; // engine slots 0 / 14 / 15 are flashlight / muzzle / explosion
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct EditorLightSource {
|
||||
int32_t x = 0; // raw world units (cell * 64 + 32)
|
||||
int32_t y = 0;
|
||||
int32_t radius = 240; // radius in raw world units
|
||||
int32_t intensity = 220; // 0..255 base intensity
|
||||
int32_t curIntensity = 220;// 0..255 current intensity
|
||||
int32_t lightType = LIGHT_TYPE_POINT;
|
||||
uint32_t flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS | LIGHT_FLAG_FLICKER;
|
||||
uint32_t color = 0x00FFB040;// 00RRGGBBh tint (warm torch)
|
||||
int32_t decaySpeed = 0;
|
||||
int32_t flickerPhase = 0;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(EditorLightSource) == 40, "EditorLightSource size mismatch with MASM LightSource");
|
||||
|
||||
struct LightingSettings {
|
||||
uint32_t mode = LIGHTING_MODE_NONE;
|
||||
uint32_t ambient = 40;
|
||||
uint32_t flashlight = 1;
|
||||
uint32_t flashlightRadius = 384;
|
||||
uint32_t flashlightIntensity = 240;
|
||||
uint32_t flashlightCone = 45;
|
||||
uint32_t baked = 1;
|
||||
uint32_t reserved = 0;
|
||||
};
|
||||
static_assert(sizeof(LightingSettings) == 32, "Lighting settings wire layout changed");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "level_format.h"
|
||||
#include "cutscene_format.h"
|
||||
#include "lighting_format.h"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
|
@ -14,6 +15,7 @@ struct BinaryLevel {
|
|||
std::vector<SpriteEntry> sprites;
|
||||
std::vector<LevelTransition> transitions;
|
||||
CutsceneTrack cutscene;
|
||||
LevelLighting lighting;
|
||||
};
|
||||
|
||||
inline bool IsMapFilename(const char* name) {
|
||||
|
|
@ -49,6 +51,14 @@ 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) {
|
||||
|
|
@ -63,6 +73,8 @@ inline bool ValidateLevel(const BinaryLevel& level, std::string& error) {
|
|||
}
|
||||
for (size_t i = 0; i < level.cells.size(); ++i)
|
||||
if (level.cells[i] == CELL_PORTAL && !triggers[i]) return fail("Портал не имеет настроенного перехода");
|
||||
if(!ValidateLighting(level.lighting.settings,level.lighting.lights.data(),level.lighting.lights.size(),error)) return false;
|
||||
for(auto c:level.lighting.rgb) if(c&0xFF000000) {error="Неверный цвет запечённого света";return false;}
|
||||
return ValidateCutscene(level.cutscene, level.cells.data(), level.transitions.size(), error);
|
||||
}
|
||||
|
||||
|
|
@ -85,9 +97,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
|
|||
if (eventCount > MAX_CUTSCENE_EVENTS || stringCount > MAX_CUTSCENE_STRINGS) {
|
||||
error = "Превышен лимит событий или строк кат-сцены"; return false;
|
||||
}
|
||||
const size_t lightCount=h.version>=5?h.lightCount:0;
|
||||
if(lightCount>MAX_EDITOR_LIGHTS || (h.version>=5&&h.lightingReserved)) {error="Неверный заголовок освещения";return false;}
|
||||
const size_t expected = sizeof(MapHeader) + MAP_TOTAL_CELLS +
|
||||
h.spriteCount * sizeof(SpriteEntry) + transitionCount * sizeof(LevelTransition) +
|
||||
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES;
|
||||
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES +
|
||||
(h.version>=5?sizeof(LightingSettings)+lightCount*sizeof(EditorLightSource)+4096:0);
|
||||
if (fileSize != static_cast<std::streamoff>(expected)) {
|
||||
error = "Размер файла не соответствует заголовку карты"; return false;
|
||||
}
|
||||
|
|
@ -108,6 +123,12 @@ inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::str
|
|||
if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; }
|
||||
candidate.cutscene.strings.push_back(CutsceneDecode(bytes, static_cast<int>(end - bytes)));
|
||||
}
|
||||
if(h.version>=5) {
|
||||
in.read(reinterpret_cast<char*>(&candidate.lighting.settings),sizeof(LightingSettings));
|
||||
candidate.lighting.lights.resize(lightCount);
|
||||
if(lightCount)in.read(reinterpret_cast<char*>(candidate.lighting.lights.data()),lightCount*sizeof(EditorLightSource));
|
||||
in.read(reinterpret_cast<char*>(candidate.lighting.rgb.data()),4096);
|
||||
}
|
||||
if (!in) { error = "Не удалось прочитать карту целиком"; return false; }
|
||||
if (h.version == 1) for (auto& sprite : candidate.sprites) { sprite.x >>= 16; sprite.y >>= 16; }
|
||||
if (!ValidateLevel(candidate, error)) return false;
|
||||
|
|
|
|||
97
editor/src/lighting_format.h
Normal file
97
editor/src/lighting_format.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
#include "level_format.h"
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct LevelLighting {
|
||||
LightingSettings settings;
|
||||
std::vector<EditorLightSource> lights;
|
||||
std::array<uint32_t,1024> rgb{}; // 00RRGGBB, baked static contribution + ambient
|
||||
LevelLighting() { rgb.fill(0x282828); }
|
||||
};
|
||||
|
||||
inline bool ValidateLighting(const LightingSettings& s, const EditorLightSource* lights, size_t count, std::string& error) {
|
||||
if (s.mode>2 || s.ambient>255 || s.flashlight>1 || s.flashlightRadius<32 || s.flashlightRadius>2048 ||
|
||||
s.flashlightIntensity>255 || s.flashlightCone<1 || s.flashlightCone>96 || s.baked!=1 || s.reserved || count>MAX_EDITOR_LIGHTS) {
|
||||
error="Неверные параметры освещения"; return false;
|
||||
}
|
||||
for(size_t i=0;i<count;++i) {
|
||||
const auto& l=lights[i];
|
||||
if(l.x<0 || l.x>=2048 || l.y<0 || l.y>=2048 || l.radius<1 || l.radius>2048 || l.intensity<0 || l.intensity>255 ||
|
||||
l.curIntensity<0 || l.curIntensity>255 || (l.lightType!=LIGHT_TYPE_STATIC && l.lightType!=LIGHT_TYPE_POINT && l.lightType!=LIGHT_TYPE_PULSE) || (l.flags&~7u) || (l.color&0xFF000000)) {
|
||||
error="Неверные координаты, цвет или параметры источника света"; return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool LightingWalkable(uint8_t cell) { return cell==0 || cell==6 || cell==10; }
|
||||
|
||||
// Exact supercover grid traversal; corner crossings cannot leak diagonally through walls.
|
||||
inline bool LightingVisible(const uint8_t* cells,int x1,int y1,int x2,int y2) {
|
||||
if(x1<0||x1>=2048||y1<0||y1>=2048||x2<0||x2>=2048||y2<0||y2>=2048) return false;
|
||||
int cx=x1/64,cy=y1/64,ex=x2/64,ey=y2/64;
|
||||
const int dx=std::abs(x2-x1),dy=std::abs(y2-y1),sx=x2>=x1?1:-1,sy=y2>=y1?1:-1;
|
||||
int nx=sx>0?(cx+1)*64-x1:x1-cx*64,ny=sy>0?(cy+1)*64-y1:y1-cy*64;
|
||||
auto free=[&](int x,int y){return x>=0&&x<32&&y>=0&&y<32&&LightingWalkable(cells[y*32+x]);};
|
||||
// A wall-mounted source may start in a wall; every subsequent cell is tested.
|
||||
for(int steps=0;steps<65;++steps) {
|
||||
if(cx==ex&&cy==ey) return free(cx,cy);
|
||||
const int a=dx?nx*dy:0x3FFFFFFF,b=dy?ny*dx:0x3FFFFFFF;
|
||||
if(a==b) {
|
||||
if(!free(cx+sx,cy)||!free(cx,cy+sy)) return false;
|
||||
cx+=sx;cy+=sy;nx+=64;ny+=64;
|
||||
} else if(a<b) {cx+=sx;nx+=64;} else {cy+=sy;ny+=64;}
|
||||
if(!free(cx,cy)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline int LightDistance(int dx,int dy) {
|
||||
dx=std::abs(dx);dy=std::abs(dy);
|
||||
return (std::max)(dx,dy)+(3*(std::min)(dx,dy))/8;
|
||||
}
|
||||
|
||||
inline std::array<uint32_t,1024> BakeLightingRGB(const uint8_t* cells,const std::vector<EditorLightSource>& lights,int ambient) {
|
||||
std::array<uint32_t,1024> rgb;
|
||||
const uint32_t base=ambient*0x010101u;rgb.fill(base);
|
||||
for(int i=0;i<1024;++i) {
|
||||
if(!LightingWalkable(cells[i])) continue;
|
||||
int x=(i%32)*64+32,y=(i/32)*64+32,r=ambient,g=ambient,b=ambient;
|
||||
for(const auto& l:lights) {
|
||||
if(!(l.flags&LIGHT_FLAG_ACTIVE)||l.lightType!=LIGHT_TYPE_STATIC||l.radius<=0) continue;
|
||||
int d=LightDistance(x-l.x,y-l.y);if(d>=l.radius) continue;
|
||||
if((l.flags&LIGHT_FLAG_SHADOWS)&&!LightingVisible(cells,l.x,l.y,x,y)) continue;
|
||||
int a=(l.radius-d)*l.intensity/l.radius;
|
||||
r=(std::min)(255,r+a*int((l.color>>16)&255)/255);
|
||||
g=(std::min)(255,g+a*int((l.color>>8)&255)/255);
|
||||
b=(std::min)(255,b+a*int(l.color&255)/255);
|
||||
}
|
||||
rgb[i]=(r<<16)|(g<<8)|b;
|
||||
}
|
||||
auto smooth=rgb;
|
||||
for(int y=0;y<32;++y)for(int x=0;x<32;++x) {
|
||||
int i=y*32+x;if(!LightingWalkable(cells[i])) continue;
|
||||
int r=((rgb[i]>>16)&255)*2,g=((rgb[i]>>8)&255)*2,b=(rgb[i]&255)*2,n=2;
|
||||
for(auto delta: {std::pair<int,int>{-1,0},{1,0},{0,-1},{0,1}}) {
|
||||
int xx=x+delta.first,yy=y+delta.second;
|
||||
if(xx<0||xx>=32||yy<0||yy>=32||!LightingWalkable(cells[yy*32+xx])) continue;
|
||||
uint32_t c=rgb[yy*32+xx];r+=(c>>16)&255;g+=(c>>8)&255;b+=c&255;++n;
|
||||
}
|
||||
smooth[i]=((r/n)<<16)|((g/n)<<8)|(b/n);
|
||||
}
|
||||
return smooth;
|
||||
}
|
||||
|
||||
inline uint64_t LightingBakeSignature(const uint8_t* cells,const std::vector<EditorLightSource>& lights,int ambient) {
|
||||
uint64_t hash=1469598103934665603ull;
|
||||
auto add=[&](uint32_t v){hash^=v;hash*=1099511628211ull;};
|
||||
for(int i=0;i<1024;++i)add(cells[i]);add(ambient);
|
||||
for(const auto& l:lights)if(l.lightType==LIGHT_TYPE_STATIC) {
|
||||
add(l.x);add(l.y);add(l.radius);add(l.intensity);add(l.flags&3);add(l.color);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
|
@ -26,50 +26,71 @@ void CreateRenderTarget();
|
|||
void CleanupRenderTarget();
|
||||
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
void SetupGodotStyle() {
|
||||
void SetupUnrealStyle() {
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImVec4* colors = style.Colors;
|
||||
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.ScrollbarRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
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.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);
|
||||
|
||||
// Dark graphite palette (Godot 4 inspired)
|
||||
colors[ImGuiCol_Text] = ImVec4(0.92f, 0.92f, 0.95f, 1.00f);
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.52f, 0.55f, 1.00f);
|
||||
colors[ImGuiCol_WindowBg] = ImVec4(0.14f, 0.15f, 0.17f, 1.00f);
|
||||
colors[ImGuiCol_ChildBg] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f);
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_Border] = ImVec4(0.24f, 0.25f, 0.28f, 1.00f);
|
||||
// Modern Game Studio Dark Palette (Unreal / Unity / Godot Pro)
|
||||
colors[ImGuiCol_Text] = ImVec4(0.96f, 0.97f, 0.99f, 1.00f);
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.54f, 0.60f, 1.00f);
|
||||
colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
|
||||
colors[ImGuiCol_ChildBg] = ImVec4(0.14f, 0.15f, 0.18f, 1.00f);
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(0.14f, 0.16f, 0.20f, 0.98f);
|
||||
colors[ImGuiCol_Border] = ImVec4(0.24f, 0.26f, 0.31f, 1.00f);
|
||||
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
|
||||
colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.21f, 0.24f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.30f, 0.34f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgActive] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_TitleBg] = ImVec4(0.11f, 0.12f, 0.13f, 1.00f);
|
||||
colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_MenuBarBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.11f, 0.12f, 0.60f);
|
||||
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.27f, 0.30f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.35f, 0.38f, 0.42f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.45f, 0.48f, 0.52f, 1.00f);
|
||||
colors[ImGuiCol_CheckMark] = ImVec4(0.30f, 0.70f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrab] = ImVec4(0.30f, 0.65f, 0.95f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.40f, 0.75f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_Button] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f);
|
||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.33f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_ButtonActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f);
|
||||
colors[ImGuiCol_Header] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f);
|
||||
colors[ImGuiCol_HeaderHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_HeaderActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f);
|
||||
colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f);
|
||||
colors[ImGuiCol_Tab] = ImVec4(0.16f, 0.18f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_TabHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_TabActive] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f);
|
||||
colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.20f, 0.25f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.25f, 0.29f, 0.36f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgActive] = ImVec4(0.30f, 0.36f, 0.45f, 1.00f);
|
||||
colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
|
||||
colors[ImGuiCol_TitleBgActive] = ImVec4(0.13f, 0.15f, 0.19f, 1.00f);
|
||||
colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.80f);
|
||||
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.28f, 0.35f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.34f, 0.39f, 0.48f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.42f, 0.48f, 0.60f, 1.00f);
|
||||
colors[ImGuiCol_CheckMark] = ImVec4(0.05f, 0.65f, 0.95f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrab] = ImVec4(0.05f, 0.60f, 0.92f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.20f, 0.75f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_Button] = ImVec4(0.20f, 0.23f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.34f, 0.43f, 1.00f);
|
||||
colors[ImGuiCol_ButtonActive] = ImVec4(0.35f, 0.43f, 0.55f, 1.00f);
|
||||
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.32f, 1.00f);
|
||||
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.35f, 0.45f, 1.00f);
|
||||
colors[ImGuiCol_HeaderActive] = ImVec4(0.35f, 0.44f, 0.56f, 1.00f);
|
||||
colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.16f, 0.20f, 1.00f);
|
||||
colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.34f, 0.42f, 1.00f);
|
||||
colors[ImGuiCol_TabActive] = ImVec4(0.22f, 0.27f, 0.35f, 1.00f);
|
||||
// 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);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
|
||||
|
|
@ -90,7 +111,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||
return 1;
|
||||
}
|
||||
|
||||
ShowWindow(hWnd, SW_SHOWDEFAULT);
|
||||
ShowWindow(hWnd, SW_MAXIMIZE);
|
||||
UpdateWindow(hWnd);
|
||||
DragAcceptFiles(hWnd, TRUE);
|
||||
|
||||
|
|
@ -135,7 +156,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||
io.Fonts->AddFontDefault();
|
||||
}
|
||||
|
||||
SetupGodotStyle();
|
||||
SetupUnrealStyle();
|
||||
|
||||
ImGui_ImplWin32_Init(hWnd);
|
||||
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
|
||||
|
|
@ -239,6 +260,12 @@ 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];
|
||||
|
|
|
|||
|
|
@ -8,21 +8,33 @@
|
|||
// Never modify payload while state == 1. All fields have fixed wire widths.
|
||||
struct EnginePreviewPacket {
|
||||
uint32_t magic = 0x56504245; // EBPV
|
||||
uint32_t version = 1;
|
||||
uint32_t version = 2;
|
||||
volatile long state = 0;
|
||||
uint32_t parentPid = 0;
|
||||
int32_t x = 160, y = 160, angle = 0;
|
||||
uint32_t ceiling = 0, floor = 0, spriteCount = 0;
|
||||
uint32_t cinematic = 0, fade = 0, textureRevision = 0;
|
||||
uint32_t reserved[19]{};
|
||||
uint32_t lightingMode = 2; // offset 52: 0=None, 1=Baked, 2=Dynamic
|
||||
uint32_t ambientLight = 40; // offset 56: base shadow level
|
||||
uint32_t flashlight = 1; // offset 60: 1=flashlight active
|
||||
uint32_t lightCount = 0; // offset 64: count of active lights
|
||||
uint32_t flashlightRadius = 384; // 68
|
||||
uint32_t flashlightIntensity = 240; // 72
|
||||
uint32_t flashlightCone = 45; // 76
|
||||
uint32_t lightingRevision = 0; // 80
|
||||
uint32_t lightTick = 0; // 84, deterministic 60 Hz time
|
||||
uint32_t reserved[10]{};
|
||||
uint8_t cells[1024]{};
|
||||
SpriteEntry sprites[32]{};
|
||||
char subtitle[96]{};
|
||||
uint32_t textures[16*128*128]{};
|
||||
uint32_t pixels[320*240]{};
|
||||
EditorLightSource lights[MAX_EDITOR_LIGHTS]{};
|
||||
uint32_t bakedRGB[1024]{};
|
||||
};
|
||||
static_assert(sizeof(long) == 4, "Preview protocol requires Windows LONG");
|
||||
static_assert(offsetof(EnginePreviewPacket, cells) == 128, "Preview header mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket, textures) == 2016, "Preview texture offset mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket, pixels) == 1050592, "Preview framebuffer offset mismatch");
|
||||
static_assert(sizeof(EnginePreviewPacket) == 1357792, "Preview mapping size mismatch");
|
||||
static_assert(offsetof(EnginePreviewPacket,lights)==1357792,"Light payload offset changed");
|
||||
static_assert(sizeof(EnginePreviewPacket) == 1362408, "Preview mapping size mismatch");
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ struct EblanProject {
|
|||
std::string windowTitle = "EBLAN ENGINE 3D - Standalone Retro Game";
|
||||
int defaultWidth = 1280;
|
||||
int defaultHeight = 960;
|
||||
int gameMode = 1; // 0 = Adventure (No weapons / Peaceful), 1 = Action (Weapons)
|
||||
bool showWeapons = true;
|
||||
bool showCrosshair = true;
|
||||
int lightingMode = 2; // 0 = None, 1 = Baked, 2 = Dynamic
|
||||
int ambientLight = 40;
|
||||
bool flashlight = true;
|
||||
|
||||
bool Save(const std::string& path) const {
|
||||
HANDLE hFile = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
|
|
@ -31,6 +37,15 @@ struct EblanProject {
|
|||
content += std::string("DefaultWidth=") + numBuf + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", defaultHeight);
|
||||
content += std::string("DefaultHeight=") + numBuf + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", gameMode);
|
||||
content += std::string("GameMode=") + numBuf + "\r\n";
|
||||
content += std::string("ShowWeapons=") + (showWeapons ? "1" : "0") + "\r\n";
|
||||
content += std::string("ShowCrosshair=") + (showCrosshair ? "1" : "0") + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", lightingMode);
|
||||
content += std::string("LightingMode=") + numBuf + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", ambientLight);
|
||||
content += std::string("AmbientLight=") + numBuf + "\r\n";
|
||||
content += std::string("Flashlight=") + (flashlight ? "1" : "0") + "\r\n";
|
||||
DWORD written = 0;
|
||||
WriteFile(hFile, content.data(), static_cast<DWORD>(content.size()), &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
|
|
@ -65,6 +80,12 @@ struct EblanProject {
|
|||
else if (key == "WindowTitle") windowTitle = val;
|
||||
else if (key == "DefaultWidth") defaultWidth = std::atoi(val.c_str());
|
||||
else if (key == "DefaultHeight") defaultHeight = std::atoi(val.c_str());
|
||||
else if (key == "GameMode") gameMode = std::atoi(val.c_str());
|
||||
else if (key == "ShowWeapons") showWeapons = (val == "1" || val == "true");
|
||||
else if (key == "ShowCrosshair") showCrosshair = (val == "1" || val == "true");
|
||||
else if (key == "LightingMode") lightingMode = std::atoi(val.c_str());
|
||||
else if (key == "AmbientLight") ambientLight = std::atoi(val.c_str());
|
||||
else if (key == "Flashlight") flashlight = (val == "1" || val == "true");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Player Combat, Weapon Shooting & Combat HUD
|
||||
; EBLAN ENGINE - Player Combat, Multi-Weapon Arsenal & Combat HUD
|
||||
; ==============================================================================
|
||||
|
||||
.code
|
||||
|
|
@ -9,6 +9,8 @@ DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
|||
DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||
Game_OnEntityKilled PROTO :DWORD, :DWORD
|
||||
|
||||
; Trace the current world along the player's forward direction. EAX = first
|
||||
; blocking boundary in 24.8 world units. Glass blocks shots (unlike rendering).
|
||||
|
|
@ -143,6 +145,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
LOCAL sprX:DWORD, sprY:DWORD
|
||||
LOCAL transX:DWORD, transY:DWORD
|
||||
LOCAL targetHalfWidth:DWORD, wallDepth:DWORD
|
||||
LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitHalfWidth:DWORD
|
||||
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
|
|
@ -162,9 +165,17 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
dec playerHitFlash
|
||||
@@:
|
||||
|
||||
; If player is dead, cannot attack
|
||||
; If player is dead, game is paused, or in peaceful adventure mode, cannot attack
|
||||
cmp isPlayerDead, 0
|
||||
jne combat_done
|
||||
cmp isGamePaused, 0
|
||||
jne combat_done
|
||||
cmp gameMode, GAME_MODE_ADVENTURE
|
||||
je combat_done
|
||||
cmp enableWeapons, 0
|
||||
je combat_done
|
||||
cmp playerWeapon, WEAPON_NONE
|
||||
je combat_done
|
||||
|
||||
; 2. Check attack input
|
||||
cmp keyAttack, 0
|
||||
|
|
@ -172,16 +183,53 @@ UpdateCombat PROC uses ebx ecx edx esi edi
|
|||
cmp playerAttackCooldown, 0
|
||||
jne combat_done
|
||||
|
||||
; Check ammo
|
||||
cmp playerAmmo, 0
|
||||
jle combat_dry_fire
|
||||
; Set weapon parameters based on playerWeapon
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitHalfWidth, 16
|
||||
|
||||
; Fire weapon!
|
||||
cmp playerWeapon, WEAPON_KNIFE
|
||||
je setup_knife
|
||||
cmp playerWeapon, WEAPON_SHOTGUN
|
||||
je setup_shotgun
|
||||
jmp setup_pistol
|
||||
|
||||
setup_knife:
|
||||
; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown
|
||||
mov weaponDamage, 30
|
||||
mov maxHitDist, 64
|
||||
mov hitHalfWidth, 24
|
||||
mov playerAttackCooldown, 8
|
||||
mov playerFlashTimer, 3
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_shotgun:
|
||||
; Shotgun: needs 2 ammo, high damage (70), wider world hitbox, long cooldown
|
||||
cmp playerAmmo, 2
|
||||
jl combat_dry_fire
|
||||
sub playerAmmo, 2
|
||||
mov weaponDamage, 70
|
||||
mov maxHitDist, 999999
|
||||
mov hitHalfWidth, 32
|
||||
mov playerAttackCooldown, 22
|
||||
mov playerFlashTimer, 7
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
jmp fire_hitscan
|
||||
|
||||
setup_pistol:
|
||||
; Pistol: needs 1 ammo, accurate world hitbox, medium cooldown (12)
|
||||
cmp playerAmmo, 1
|
||||
jl combat_dry_fire
|
||||
dec playerAmmo
|
||||
mov playerAttackCooldown, 10
|
||||
mov weaponDamage, 35
|
||||
mov maxHitDist, 999999
|
||||
mov hitHalfWidth, 16
|
||||
mov playerAttackCooldown, 12
|
||||
mov playerFlashTimer, 5
|
||||
invoke SpawnMuzzleFlashLight, playerX, playerY
|
||||
|
||||
; 3. Hitscan check against enemies in center crosshair
|
||||
fire_hitscan:
|
||||
; 3. Hitscan check against enemies & destructible barrels in crosshair
|
||||
; Camera basis for -playerAngle
|
||||
mov eax, ANGLE_512
|
||||
sub eax, playerAngle
|
||||
|
|
@ -206,9 +254,16 @@ hitscan_loop:
|
|||
|
||||
mov ecx, i
|
||||
mov eax, [entityType + ecx*4]
|
||||
cmp eax, ENTITY_TYPE_GUARD
|
||||
jne next_hitscan_target
|
||||
|
||||
; Check if Guard or Barrel
|
||||
cmp eax, ENTITY_TYPE_GUARD
|
||||
je hitscan_target_valid
|
||||
cmp eax, ENTITY_TYPE_BARREL
|
||||
je hitscan_target_valid
|
||||
|
||||
jmp next_hitscan_target
|
||||
|
||||
hitscan_target_valid:
|
||||
mov eax, [entityState + ecx*4]
|
||||
cmp eax, ENTITY_STATE_DEAD
|
||||
je next_hitscan_target
|
||||
|
|
@ -245,6 +300,11 @@ hitscan_loop:
|
|||
cmp transY, 16
|
||||
jle next_hitscan_target
|
||||
|
||||
; Must be closer than maximum weapon hit distance
|
||||
mov eax, transY
|
||||
cmp eax, maxHitDist
|
||||
jg next_hitscan_target
|
||||
|
||||
; Compare in 24.8 so a target on a wall boundary is not hit through it.
|
||||
mov eax, transY
|
||||
shl eax, 8
|
||||
|
|
@ -262,14 +322,14 @@ hitscan_loop:
|
|||
add ecx, eax
|
||||
mov transX, ecx
|
||||
|
||||
; World-space billboard hitbox: 32 units wide at scale 1.0, scaled with
|
||||
; World-space billboard hitbox: weapon-specific width, scaled with
|
||||
; the authored sprite, never with screen distance. Zero means legacy 1.0.
|
||||
mov eax, [ebx].SpriteStruct.scale
|
||||
test eax, eax
|
||||
jnz @F
|
||||
mov eax, FIXED_ONE
|
||||
@@:
|
||||
imul eax, 16
|
||||
imul eax, hitHalfWidth
|
||||
sar eax, 16
|
||||
mov targetHalfWidth, eax
|
||||
mov eax, transX
|
||||
|
|
@ -295,10 +355,15 @@ hitscan_eval:
|
|||
cmp closestIdx, -1
|
||||
je combat_done
|
||||
|
||||
; Apply damage to hit guard
|
||||
mov ecx, closestIdx
|
||||
mov eax, [entityType + ecx*4]
|
||||
|
||||
; Check if hit an explosive barrel!
|
||||
cmp eax, ENTITY_TYPE_BARREL
|
||||
je explode_barrel
|
||||
; Hit an enemy guard!
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, 35
|
||||
sub eax, weaponDamage
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
|
|
@ -310,8 +375,9 @@ hitscan_eval:
|
|||
mov [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
mov DWORD PTR [entityTimer + ecx*4], 0
|
||||
add playerScore, 100
|
||||
invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_GUARD
|
||||
|
||||
; Drop 10 ammo reward
|
||||
; Drop ammo reward
|
||||
mov eax, playerAmmo
|
||||
add eax, 10
|
||||
cmp eax, playerMaxAmmo
|
||||
|
|
@ -321,6 +387,115 @@ hitscan_eval:
|
|||
mov playerAmmo, eax
|
||||
jmp combat_done
|
||||
|
||||
explode_barrel:
|
||||
; Exploding Barrel: Destroy barrel and trigger AOE blast!
|
||||
mov [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
mov ebx, closestIdx
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
mov [ebx].SpriteStruct.textureId, 0 ; disappear
|
||||
|
||||
; Fiery orange-red screen flash
|
||||
mov playerHitFlash, 8
|
||||
add playerScore, 200
|
||||
invoke ShowGameToast, OFFSET szBarrelExplodedToast, 60, 00FFA020h
|
||||
invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_BARREL
|
||||
|
||||
; AOE damage to all surrounding guards within 128 units (2 cells)
|
||||
push esi
|
||||
push edi
|
||||
mov esi, [ebx].SpriteStruct.x
|
||||
mov edi, [ebx].SpriteStruct.y
|
||||
invoke SpawnExplosionLight, esi, edi
|
||||
|
||||
mov i, 0
|
||||
aoe_loop:
|
||||
mov eax, i
|
||||
cmp eax, spriteCount
|
||||
jge aoe_check_player
|
||||
|
||||
mov ecx, i
|
||||
cmp ecx, closestIdx
|
||||
je next_aoe
|
||||
|
||||
mov eax, [entityType + ecx*4]
|
||||
cmp eax, ENTITY_TYPE_GUARD
|
||||
jne next_aoe
|
||||
cmp [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
je next_aoe
|
||||
|
||||
; EDX is volatile scratch for CDQ below, not a safe sprite pointer.
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
sub eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 128
|
||||
jg next_aoe
|
||||
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
sub eax, edi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 128
|
||||
jg next_aoe
|
||||
|
||||
; Within blast: deal 60 blast damage!
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, 60
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
mov [entityHealth + ecx*4], eax
|
||||
test eax, eax
|
||||
jnz next_aoe
|
||||
|
||||
mov [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
add playerScore, 150
|
||||
|
||||
next_aoe:
|
||||
inc i
|
||||
jmp aoe_loop
|
||||
|
||||
aoe_check_player:
|
||||
; If player was standing too close to the barrel (< 96 units), player takes blast damage!
|
||||
mov eax, playerX
|
||||
sub eax, esi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 96
|
||||
jg aoe_done
|
||||
|
||||
mov eax, playerY
|
||||
sub eax, edi
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
cmp eax, 96
|
||||
jg aoe_done
|
||||
|
||||
; Player blast damage
|
||||
mov eax, playerHealth
|
||||
sub eax, 25
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
mov playerHealth, eax
|
||||
test eax, eax
|
||||
jnz aoe_done
|
||||
mov isPlayerDead, 1
|
||||
|
||||
aoe_done:
|
||||
pop edi
|
||||
pop esi
|
||||
jmp combat_done
|
||||
|
||||
combat_dry_fire:
|
||||
mov playerAttackCooldown, 12
|
||||
|
||||
|
|
@ -334,24 +509,91 @@ UpdateCombat ENDP
|
|||
RenderCombatHUD PROC uses ebx ecx edx esi edi
|
||||
LOCAL barW:DWORD, gunY:DWORD
|
||||
|
||||
; If in adventure mode, unarmed, or weapons disabled, skip weapon & combat stats
|
||||
cmp gameMode, GAME_MODE_ADVENTURE
|
||||
je hud_damage_flash
|
||||
cmp enableWeapons, 0
|
||||
je hud_damage_flash
|
||||
cmp playerWeapon, WEAPON_NONE
|
||||
je hud_damage_flash
|
||||
|
||||
; 1. Center Crosshair (subtle cross at 160, 120)
|
||||
cmp showCrosshair, 0
|
||||
je skip_crosshair
|
||||
invoke DrawRect, 156, 120, 9, 1, 00FFFFFFh
|
||||
invoke DrawRect, 160, 116, 1, 9, 00FFFFFFh
|
||||
invoke DrawRect, 160, 120, 1, 1, 0000FF00h
|
||||
skip_crosshair:
|
||||
|
||||
; 2. Render Retro Weapon at bottom center
|
||||
; 2. Render Active Weapon at bottom center
|
||||
cmp playerWeapon, WEAPON_KNIFE
|
||||
je render_knife_weapon
|
||||
cmp playerWeapon, WEAPON_SHOTGUN
|
||||
je render_shotgun_weapon
|
||||
jmp render_pistol_weapon
|
||||
|
||||
render_knife_weapon:
|
||||
; Knife Combat Animation: thrusts forward when cooldown active
|
||||
mov gunY, 190
|
||||
cmp playerFlashTimer, 0
|
||||
je draw_knife
|
||||
sub gunY, 12 ; thrust forward!
|
||||
|
||||
draw_knife:
|
||||
; Silver steel blade
|
||||
invoke DrawRect, 166, gunY, 6, 26, 00D0D0D0h
|
||||
invoke DrawRect, 168, gunY, 2, 26, 00FFFFFFh ; shine
|
||||
invoke DrawRect, 167, gunY - 4, 4, 4, 00B0B0B0h ; blade tip
|
||||
invoke DrawRect, 168, gunY - 7, 2, 3, 00E0E0E0h
|
||||
; Brass crossguard
|
||||
mov eax, gunY
|
||||
add eax, 26
|
||||
invoke DrawRect, 160, eax, 18, 5, 00D4AF37h
|
||||
; Wooden grip
|
||||
mov eax, gunY
|
||||
add eax, 31
|
||||
invoke DrawRect, 164, eax, 10, 22, 00502A10h
|
||||
jmp hud_stats_draw
|
||||
|
||||
render_shotgun_weapon:
|
||||
; Heavy Pump Shotgun
|
||||
mov gunY, 188
|
||||
cmp playerFlashTimer, 0
|
||||
je draw_shotgun_barrels
|
||||
add gunY, 4 ; heavy recoil!
|
||||
|
||||
; Massive dual blast flash!
|
||||
invoke DrawRect, 146, 155, 28, 22, 00FFAA00h
|
||||
invoke DrawRect, 150, 158, 20, 16, 00FFFF00h
|
||||
invoke DrawRect, 154, 162, 12, 10, 00FFFFFFh
|
||||
|
||||
draw_shotgun_barrels:
|
||||
; Twin heavy steel barrels
|
||||
invoke DrawRect, 148, gunY, 10, 32, 002B2B2Bh
|
||||
invoke DrawRect, 150, gunY, 6, 32, 00484848h
|
||||
invoke DrawRect, 162, gunY, 10, 32, 002B2B2Bh
|
||||
invoke DrawRect, 164, gunY, 6, 32, 00484848h
|
||||
; Barrel rib connector
|
||||
invoke DrawRect, 158, gunY, 4, 32, 00222222h
|
||||
; Wooden pump forend
|
||||
mov eax, gunY
|
||||
add eax, 20
|
||||
invoke DrawRect, 144, eax, 32, 18, 00603818h
|
||||
invoke DrawRect, 146, eax, 28, 14, 007A4820h
|
||||
jmp hud_stats_draw
|
||||
|
||||
render_pistol_weapon:
|
||||
mov gunY, 192
|
||||
cmp playerFlashTimer, 0
|
||||
je draw_gun_barrel
|
||||
; Recoil offset
|
||||
add gunY, 3
|
||||
je draw_pistol_barrel
|
||||
add gunY, 3 ; recoil offset
|
||||
|
||||
; Muzzle Flash Burst (Bright yellow & white star)
|
||||
invoke DrawRect, 157, 165, 7, 18, 00FFFF00h
|
||||
invoke DrawRect, 152, 170, 17, 8, 00FFAA00h
|
||||
invoke DrawRect, 158, 168, 5, 12, 00FFFFFFh
|
||||
|
||||
draw_gun_barrel:
|
||||
draw_pistol_barrel:
|
||||
; Weapon barrel & slide
|
||||
invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah
|
||||
invoke DrawRect, 156, gunY, 9, 28, 00555555h
|
||||
|
|
@ -364,15 +606,16 @@ draw_gun_barrel:
|
|||
add eax, 26
|
||||
invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates
|
||||
|
||||
; 3. Health Bar
|
||||
invoke DrawString8x8, 8, 42, OFFSET strHpLabel, COLOR_GREEN
|
||||
invoke DrawNumber8x8, 34, 42, playerHealth, COLOR_WHITE
|
||||
hud_stats_draw:
|
||||
; 3. Health Bar at top-left: X=8, Y=8
|
||||
invoke DrawString8x8, 8, 8, OFFSET strHpLabel, COLOR_GREEN
|
||||
invoke DrawNumber8x8, 34, 8, playerHealth, COLOR_WHITE
|
||||
|
||||
invoke DrawRect, 68, 42, 60, 8, 00441010h
|
||||
invoke DrawRectOutline, 68, 42, 60, 8, COLOR_WHITE
|
||||
invoke DrawRect, 66, 8, 50, 8, 00441010h
|
||||
invoke DrawRectOutline, 66, 8, 50, 8, COLOR_WHITE
|
||||
|
||||
mov eax, playerHealth
|
||||
imul eax, 58
|
||||
imul eax, 48
|
||||
xor edx, edx
|
||||
mov ecx, playerMaxHealth
|
||||
test ecx, ecx
|
||||
|
|
@ -383,34 +626,66 @@ draw_gun_barrel:
|
|||
mov barW, eax
|
||||
cmp barW, 0
|
||||
jle hud_ammo_draw
|
||||
cmp barW, 58
|
||||
cmp barW, 48
|
||||
jle @F
|
||||
mov barW, 58
|
||||
mov barW, 48
|
||||
@@:
|
||||
mov edx, 0000CC33h ; Green
|
||||
cmp playerHealth, 30
|
||||
jg @F
|
||||
mov edx, 00FF2222h ; Red warning
|
||||
@@:
|
||||
invoke DrawRect, 69, 43, barW, 6, edx
|
||||
invoke DrawRect, 67, 9, barW, 6, edx
|
||||
|
||||
hud_ammo_draw:
|
||||
; 4. Ammo Display
|
||||
invoke DrawString8x8, 140, 42, OFFSET strAmmoLabel, COLOR_YELLOW
|
||||
invoke DrawNumber8x8, 182, 42, playerAmmo, COLOR_YELLOW
|
||||
; 4. Ammo Display: X=126, Y=8
|
||||
invoke DrawString8x8, 126, 8, OFFSET strAmmoLabel, COLOR_YELLOW
|
||||
invoke DrawNumber8x8, 168, 8, playerAmmo, COLOR_YELLOW
|
||||
|
||||
; 5. Score Display
|
||||
invoke DrawString8x8, 222, 42, OFFSET strScoreLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 272, 42, playerScore, COLOR_CYAN
|
||||
|
||||
; 6. Keys (if collected)
|
||||
; 5. Keys: X=204, Y=8 (if collected)
|
||||
cmp playerKeys, 0
|
||||
jle hud_damage_flash
|
||||
invoke DrawString8x8, 222, 10, OFFSET strKeysLabel, COLOR_MAGENTA
|
||||
invoke DrawNumber8x8, 258, 10, playerKeys, COLOR_MAGENTA
|
||||
jle hud_score_draw
|
||||
invoke DrawString8x8, 204, 8, OFFSET strKeysLabel, COLOR_MAGENTA
|
||||
invoke DrawNumber8x8, 238, 8, playerKeys, COLOR_MAGENTA
|
||||
|
||||
hud_score_draw:
|
||||
; 6. Score Display: only in Debug HUD style
|
||||
cmp hudStyle, HUD_STYLE_CLEAN
|
||||
je draw_weapon_badge
|
||||
invoke DrawString8x8, 8, 20, OFFSET strScoreLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 58, 20, playerScore, COLOR_CYAN
|
||||
|
||||
draw_weapon_badge:
|
||||
; 7. Active Weapon Badge at bottom-right corner: X=228, Y=222
|
||||
invoke DrawRect, 228, 222, 88, 14, 00141414h
|
||||
invoke DrawRectOutline, 228, 222, 88, 14, 00444444h
|
||||
|
||||
cmp playerWeapon, WEAPON_KNIFE
|
||||
jne @F
|
||||
invoke DrawString8x8, 233, 225, OFFSET strWeaponKnife, COLOR_CYAN
|
||||
jmp draw_weapon_slots
|
||||
@@:
|
||||
cmp playerWeapon, WEAPON_SHOTGUN
|
||||
jne @F
|
||||
invoke DrawString8x8, 233, 225, OFFSET strWeaponShotgun, 00FFAA00h
|
||||
jmp draw_weapon_slots
|
||||
@@:
|
||||
invoke DrawString8x8, 233, 225, OFFSET strWeaponPistol, COLOR_YELLOW
|
||||
|
||||
draw_weapon_slots:
|
||||
; Draw small weapon slot indicator boxes: [1] [2] [3]
|
||||
invoke DrawRectOutline, 287, 225, 6, 8, 00555555h
|
||||
invoke DrawRectOutline, 295, 225, 6, 8, 00555555h
|
||||
invoke DrawRectOutline, 303, 225, 6, 8, 00555555h
|
||||
|
||||
; Highlight active slot
|
||||
mov eax, playerWeapon
|
||||
imul eax, 8
|
||||
add eax, 287
|
||||
invoke DrawRectOutline, eax, 225, 6, 8, COLOR_YELLOW
|
||||
|
||||
hud_damage_flash:
|
||||
; 7. Damage Screen Flash
|
||||
; 8. Damage Screen Flash
|
||||
cmp playerHitFlash, 0
|
||||
jle hud_death_check
|
||||
; Red border vignette
|
||||
|
|
@ -420,7 +695,7 @@ hud_damage_flash:
|
|||
invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h
|
||||
|
||||
hud_death_check:
|
||||
; 8. Death Overlay Screen
|
||||
; 9. Death Overlay Screen
|
||||
cmp isPlayerDead, 0
|
||||
je combat_hud_done
|
||||
|
||||
|
|
@ -431,3 +706,34 @@ 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
|
||||
|
|
|
|||
|
|
@ -231,6 +231,19 @@ 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
|
||||
|
|
@ -256,6 +269,12 @@ 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
|
||||
|
|
@ -278,13 +297,13 @@ UpdateCutscene PROC uses ebx ecx edx esi edi
|
|||
imul csMoveElapsed
|
||||
idiv csMoveDuration
|
||||
add eax, csMoveFromX
|
||||
mov playerX, eax
|
||||
mov csCameraX, eax
|
||||
mov eax, csMoveToY
|
||||
sub eax, csMoveFromY
|
||||
imul csMoveElapsed
|
||||
idiv csMoveDuration
|
||||
add eax, csMoveFromY
|
||||
mov playerY, eax
|
||||
mov csCameraY, eax
|
||||
mov eax, csMoveElapsed
|
||||
cmp eax, csMoveDuration
|
||||
jb cs_step_angle
|
||||
|
|
@ -298,7 +317,7 @@ cs_step_angle:
|
|||
idiv csAngleDuration
|
||||
add eax, csAngleFrom
|
||||
and eax, ANGLE_MASK
|
||||
mov playerAngle, eax
|
||||
mov csCameraAngle, eax
|
||||
mov eax, csAngleElapsed
|
||||
cmp eax, csAngleDuration
|
||||
jb cs_step_fade
|
||||
|
|
@ -368,9 +387,9 @@ cs_wait:
|
|||
jnz cs_advance_tick
|
||||
jmp cs_dispatch
|
||||
cs_move:
|
||||
mov eax, playerX
|
||||
mov eax, csCameraX
|
||||
mov csMoveFromX, eax
|
||||
mov eax, playerY
|
||||
mov eax, csCameraY
|
||||
mov csMoveFromY, eax
|
||||
mov eax, [esi].CutsceneEvent.paramA
|
||||
mov csMoveToX, eax
|
||||
|
|
@ -382,12 +401,12 @@ cs_move:
|
|||
test eax, eax
|
||||
jnz cs_dispatch
|
||||
mov eax, csMoveToX
|
||||
mov playerX, eax
|
||||
mov csCameraX, eax
|
||||
mov eax, csMoveToY
|
||||
mov playerY, eax
|
||||
mov csCameraY, eax
|
||||
jmp cs_dispatch
|
||||
cs_pan:
|
||||
mov eax, playerAngle
|
||||
mov eax, csCameraAngle
|
||||
mov csAngleFrom, eax
|
||||
mov ebx, [esi].CutsceneEvent.paramA
|
||||
sub ebx, eax
|
||||
|
|
@ -401,7 +420,7 @@ cs_pan:
|
|||
test eax, eax
|
||||
jnz cs_dispatch
|
||||
mov eax, [esi].CutsceneEvent.paramA
|
||||
mov playerAngle, eax
|
||||
mov csCameraAngle, eax
|
||||
jmp cs_dispatch
|
||||
cs_text:
|
||||
mov eax, [esi].CutsceneEvent.paramA
|
||||
|
|
|
|||
|
|
@ -20,8 +20,26 @@ 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
|
||||
targetFrameTimeMs dd 16 ; ~60 FPS (16.6 ms)
|
||||
lastTickCount 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
|
||||
fpsCounter dd 0
|
||||
fpsTimer dd 0
|
||||
currentFps dd 60
|
||||
|
|
@ -39,9 +57,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 frame (walk)
|
||||
playerRunSpeed dd 6 ; raw units per frame (sprint)
|
||||
playerTurnSpeed dd 4 ; angle units per frame
|
||||
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
|
||||
playerRadius dd 16 ; collision radius (raw units)
|
||||
|
||||
; Input State Flags
|
||||
|
|
@ -64,10 +82,56 @@ 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
|
||||
|
|
@ -120,7 +184,17 @@ strHpLabel db "HP:",0
|
|||
strAmmoLabel db "AMMO:",0
|
||||
strScoreLabel db "SCORE:",0
|
||||
strKeysLabel db "KEY:",0
|
||||
strYouDied db "YOU DIED! PRESS [R] TO RESTART",0
|
||||
strWeaponLabel db "WPN:",0
|
||||
strWeaponKnife db "KNIFE",0
|
||||
strWeaponPistol db "PISTOL",0
|
||||
strWeaponShotgun db "SHOTGUN",0
|
||||
strPauseTitle db "== ПАУЗА ==",0
|
||||
strPauseOpt1 db "[1] ПРОДОЛЖИТЬ",0
|
||||
strPauseOpt2 db "[2] ОРУЖИЕ: ВКЛ/ВЫКЛ",0
|
||||
strPauseOpt3 db "[3] ИНТЕРФЕЙС: ЧИСТЫЙ/ИНФО",0
|
||||
strPauseOpt4 db "[4] ПЕРЕЗАПУСК УРОВНЯ",0
|
||||
strPauseOpt5 db "[5] ВЫХОД В WINDOWS",0
|
||||
strYouDied db "ВЫ ПОГИБЛИ! НАЖМИТЕ [R]",0
|
||||
cutsceneCount dd 0
|
||||
cutsceneStringCount dd 0
|
||||
cutsceneFlags dd 0
|
||||
|
|
@ -223,6 +297,75 @@ 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(?)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,57 @@ 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
|
||||
|
|
@ -75,6 +126,7 @@ ENTITY_TYPE_MEDKIT equ 2 ; Health pickup (+25 HP)
|
|||
ENTITY_TYPE_AMMO equ 3 ; Ammo pickup (+20 Ammo)
|
||||
ENTITY_TYPE_KEY equ 4 ; Door Key pickup
|
||||
ENTITY_TYPE_SWITCH equ 5 ; Interactive switch trigger
|
||||
ENTITY_TYPE_BARREL equ 6 ; Explosive barrel (AOE damage)
|
||||
|
||||
ENTITY_STATE_IDLE equ 0
|
||||
ENTITY_STATE_CHASE equ 1
|
||||
|
|
@ -113,7 +165,7 @@ CELL_COUNT equ 11
|
|||
; Sector Transition Portals
|
||||
MAX_TRANSITIONS equ 16
|
||||
MAX_PATH_BYTES equ 260
|
||||
MAP_FORMAT_VERSION equ 4
|
||||
MAP_FORMAT_VERSION equ 5
|
||||
|
||||
LevelTransition STRUCT ; sizeof = 42 bytes (packed)
|
||||
triggerCellX db 0 ; 1 byte
|
||||
|
|
@ -138,6 +190,20 @@ SpriteStruct STRUCT
|
|||
flags dd 0
|
||||
SpriteStruct ENDS
|
||||
|
||||
; Light Source Struct (sizeof = 40 bytes)
|
||||
LightSource STRUCT
|
||||
x dd 0 ; raw world units (0..2047)
|
||||
y dd 0 ; raw world units (0..2047)
|
||||
radius dd 0 ; radius in raw world units
|
||||
intensity dd 0 ; 0..255 base intensity
|
||||
curIntensity dd 0 ; 0..255 current intensity (after flicker/decay)
|
||||
lightType dd 0 ; LIGHT_TYPE_*
|
||||
flags dd 0 ; LIGHT_FLAG_*
|
||||
color dd 0 ; 00RRGGBBh tint (0 = neutral white/ambient)
|
||||
decaySpeed dd 0 ; decay per tick for BURST lights
|
||||
flickerPhase dd 0 ; flicker oscillation phase
|
||||
LightSource ENDS
|
||||
|
||||
; Texture Pack (assets/textures/texturepack.bin):
|
||||
; dd magic 'EBTX', dw version (1), dw slotCount, then slotCount * 65536 bytes
|
||||
TEXPACK_MAGIC equ 58544245h ; 'EBTX'
|
||||
|
|
@ -154,7 +220,7 @@ MAP_MAGIC equ 4E4C4245h ; 'EBLN'
|
|||
|
||||
MapHeader STRUCT ; sizeof = 38 bytes (packed)
|
||||
magic dd 0 ; 'EBLN'
|
||||
version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes
|
||||
version dw 0 ; 1 = fixed coords, 2/3 = transitions, 4 = cutscenes, 5 = lighting
|
||||
mapWidth dw 0 ; 32
|
||||
mapHeight dw 0 ; 32
|
||||
playerStartX dw 0 ; cell X
|
||||
|
|
@ -167,7 +233,8 @@ MapHeader STRUCT ; sizeof = 38 bytes (packed)
|
|||
cutsceneCount dw 0
|
||||
cutsceneStringCount dw 0
|
||||
cutsceneFlags dw 0
|
||||
reserved db 4 dup(0)
|
||||
lightCount dw 0
|
||||
lightingReserved dw 0
|
||||
MapHeader ENDS
|
||||
|
||||
MAX_CUTSCENE_EVENTS equ 128
|
||||
|
|
|
|||
|
|
@ -4,10 +4,18 @@
|
|||
|
||||
.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
|
||||
|
|
@ -86,8 +94,13 @@ init_spr_loop:
|
|||
|
||||
fallback_archetype:
|
||||
; If flags == 0, infer archetype from textureId:
|
||||
; Slot 13 = Enemy Guard, 14 = Medkit, 15 = Ammo
|
||||
; Slot 11 = Explosive Barrel, 13 = Guard, 14 = Medkit, 15 = Ammo
|
||||
mov edx, [ebx].SpriteStruct.textureId
|
||||
cmp edx, 11
|
||||
jne check_guard_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_BARREL
|
||||
jmp next_spr_slot
|
||||
check_guard_slot:
|
||||
cmp edx, 13
|
||||
jne check_medkit_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_GUARD
|
||||
|
|
@ -132,13 +145,7 @@ UpdateEntities PROC uses ebx ecx edx esi edi
|
|||
je entities_active
|
||||
cmp keyRestart, 0
|
||||
je entities_done
|
||||
; Respawn player
|
||||
mov playerHealth, 100
|
||||
mov playerAmmo, 50
|
||||
mov isPlayerDead, 0
|
||||
mov playerHitFlash, 0
|
||||
call LoadMapBinary
|
||||
call InitEntities
|
||||
call RestartCurrentRoom
|
||||
ret
|
||||
|
||||
entities_active:
|
||||
|
|
@ -148,6 +155,9 @@ entities_active:
|
|||
mov i, 0
|
||||
|
||||
entity_loop:
|
||||
call IsGameplayBlocked
|
||||
test eax, eax
|
||||
jnz entities_done
|
||||
mov eax, i
|
||||
cmp eax, spriteCount
|
||||
jge entities_done
|
||||
|
|
@ -217,6 +227,7 @@ entity_loop:
|
|||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 25
|
||||
invoke ShowGameToast, OFFSET szMedkitPickedToast, 75, COLOR_GREEN
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -240,6 +251,7 @@ check_ammo_pickup:
|
|||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 10
|
||||
invoke ShowGameToast, OFFSET szAmmoPickedToast, 75, COLOR_YELLOW
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -247,7 +259,7 @@ check_ammo_pickup:
|
|||
; --------------------------------------------------------------------------
|
||||
check_key_pickup:
|
||||
cmp edx, ENTITY_TYPE_KEY
|
||||
jne check_guard_entity
|
||||
jne check_switch_entity
|
||||
cmp absDX, 32
|
||||
jge next_entity
|
||||
cmp absDY, 32
|
||||
|
|
@ -257,6 +269,61 @@ 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
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
|
|||
331
include/engine_gameplay.inc
Normal file
331
include/engine_gameplay.inc
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
; ==============================================================================
|
||||
; 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
|
||||
|
|
@ -1,188 +1,70 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Input System (Keyboard via GetAsyncKeyState)
|
||||
; ==============================================================================
|
||||
|
||||
; Keyboard sampling never mutates simulation. Pending edges survive render-only
|
||||
; frames and are cleared only by ConsumeInputEdges after one simulation tick.
|
||||
.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
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; PollInput: Reads keyboard state and updates movement & action flags
|
||||
; ------------------------------------------------------------------------------
|
||||
PollInput PROC uses eax ebx
|
||||
mov keyForward, 0
|
||||
mov keyBackward, 0
|
||||
mov keyStrafeLeft, 0
|
||||
mov keyStrafeRight, 0
|
||||
mov keyTurnLeft, 0
|
||||
mov keyTurnRight, 0
|
||||
mov keyRun, 0
|
||||
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 keyScreenshot, 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 PROC uses eax ebx
|
||||
LOCAL regained:DWORD
|
||||
mov regained, 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:
|
||||
|
||||
; 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
|
||||
cmp inputFocused, 0
|
||||
je @F
|
||||
mov clockReset, 1
|
||||
@@:
|
||||
|
||||
; 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 inputFocused, 0
|
||||
call ConsumeInputEdges
|
||||
mov keyScreenshot, 0
|
||||
mov keyForward, 0
|
||||
mov keyBackward, 0
|
||||
mov keyStrafeLeft, 0
|
||||
|
|
@ -190,15 +72,55 @@ restart_sampled:
|
|||
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
|
||||
; On regain, sample every physical latch but discard the resulting edges.
|
||||
ret
|
||||
input_has_focus:
|
||||
cmp inputFocused, 0
|
||||
jne @F
|
||||
mov clockReset, 1
|
||||
mov regained, 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:
|
||||
ret
|
||||
PollInput ENDP
|
||||
|
|
|
|||
103
include/engine_light.inc
Normal file
103
include/engine_light.inc
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
include include/engine_lighting_core.inc
|
||||
AddLightSource PROC uses ebx ecx edx x:DWORD, y:DWORD, radius:DWORD, intensity:DWORD, lightType:DWORD, flags:DWORD, color:DWORD
|
||||
cmp x,2048
|
||||
jae no_slot_avail
|
||||
cmp y,2048
|
||||
jae no_slot_avail
|
||||
cmp radius,1
|
||||
jb no_slot_avail
|
||||
cmp radius,2048
|
||||
ja no_slot_avail
|
||||
cmp intensity,255
|
||||
ja no_slot_avail
|
||||
cmp lightType,4
|
||||
ja no_slot_avail
|
||||
mov eax,flags
|
||||
and eax,0FFFFFFF8h
|
||||
jnz no_slot_avail
|
||||
test color,0FF000000h
|
||||
jnz no_slot_avail
|
||||
mov ecx, 1 ; Slot 0 is reserved for player flashlight
|
||||
find_slot:
|
||||
cmp ecx, 14
|
||||
jge no_slot_avail
|
||||
|
||||
mov ebx, ecx
|
||||
imul ebx, sizeof LightSource
|
||||
add ebx, OFFSET lightSources
|
||||
|
||||
test [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE
|
||||
jz slot_found
|
||||
|
||||
inc ecx
|
||||
jmp find_slot
|
||||
|
||||
slot_found:
|
||||
mov eax, x
|
||||
mov [ebx].LightSource.x, eax
|
||||
mov eax, y
|
||||
mov [ebx].LightSource.y, eax
|
||||
mov eax, radius
|
||||
mov [ebx].LightSource.radius, eax
|
||||
mov eax, intensity
|
||||
mov [ebx].LightSource.intensity, eax
|
||||
mov [ebx].LightSource.curIntensity, eax
|
||||
mov eax, lightType
|
||||
mov [ebx].LightSource.lightType, eax
|
||||
mov eax, flags
|
||||
or eax, LIGHT_FLAG_ACTIVE
|
||||
mov [ebx].LightSource.flags, eax
|
||||
mov eax, color
|
||||
mov [ebx].LightSource.color, eax
|
||||
mov [ebx].LightSource.decaySpeed, 0
|
||||
mov [ebx].LightSource.flickerPhase, 0
|
||||
|
||||
mov eax, ecx
|
||||
ret
|
||||
|
||||
no_slot_avail:
|
||||
mov eax, -1
|
||||
ret
|
||||
AddLightSource ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; SpawnExplosionLight: Triggers high-intensity fiery explosion light burst
|
||||
; ------------------------------------------------------------------------------
|
||||
SpawnExplosionLight PROC uses ebx x:DWORD, y:DWORD
|
||||
mov ebx, 15 * (sizeof LightSource)
|
||||
add ebx, OFFSET lightSources
|
||||
|
||||
mov eax, x
|
||||
mov [ebx].LightSource.x, eax
|
||||
mov eax, y
|
||||
mov [ebx].LightSource.y, eax
|
||||
mov [ebx].LightSource.radius, 450
|
||||
mov [ebx].LightSource.intensity, 255
|
||||
mov [ebx].LightSource.curIntensity, 255
|
||||
mov [ebx].LightSource.lightType, LIGHT_TYPE_BURST
|
||||
mov [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE or LIGHT_FLAG_SHADOWS
|
||||
mov [ebx].LightSource.decaySpeed, 12
|
||||
mov [ebx].LightSource.color, 00FFAA33h ; Warm fire tint
|
||||
ret
|
||||
SpawnExplosionLight ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; SpawnMuzzleFlashLight: Triggers momentary bright gunshot flash
|
||||
; ------------------------------------------------------------------------------
|
||||
SpawnMuzzleFlashLight PROC uses ebx x:DWORD, y:DWORD
|
||||
mov ebx, 14 * (sizeof LightSource)
|
||||
add ebx, OFFSET lightSources
|
||||
|
||||
mov eax, x
|
||||
mov [ebx].LightSource.x, eax
|
||||
mov eax, y
|
||||
mov [ebx].LightSource.y, eax
|
||||
mov [ebx].LightSource.radius, 350
|
||||
mov [ebx].LightSource.intensity, 240
|
||||
mov [ebx].LightSource.curIntensity, 240
|
||||
mov [ebx].LightSource.lightType, LIGHT_TYPE_BURST
|
||||
mov [ebx].LightSource.flags, LIGHT_FLAG_ACTIVE or LIGHT_FLAG_SHADOWS
|
||||
mov [ebx].LightSource.decaySpeed, 60
|
||||
mov [ebx].LightSource.color, 00FFFFEEh ; White/yellow flare
|
||||
ret
|
||||
SpawnMuzzleFlashLight ENDP
|
||||
835
include/engine_lighting_core.inc
Normal file
835
include/engine_lighting_core.inc
Normal file
|
|
@ -0,0 +1,835 @@
|
|||
; 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
|
||||
|
|
@ -13,11 +13,19 @@ 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
|
||||
|
|
@ -90,7 +98,11 @@ loop_wall2:
|
|||
mov playerX, 160
|
||||
mov playerY, 160
|
||||
mov playerAngle, 0
|
||||
call StopCutscene
|
||||
mov currentMapPath, 0
|
||||
mov roomEntryX, 2
|
||||
mov roomEntryY, 2
|
||||
mov roomEntryAngle, 0
|
||||
call ResetMovementFractions
|
||||
mov cutsceneCount, 0
|
||||
mov cutsceneStringCount, 0
|
||||
mov cutsceneFlags, 0
|
||||
|
|
@ -100,6 +112,7 @@ loop_wall2:
|
|||
mov portalLatchCell, -1
|
||||
mov mapCeilingColor, COLOR_CEILING
|
||||
mov mapFloorColor, COLOR_FLOOR
|
||||
call InitLightingSystem
|
||||
ret
|
||||
InitMap ENDP
|
||||
|
||||
|
|
@ -174,6 +187,46 @@ 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
|
||||
|
||||
|
|
@ -199,18 +252,62 @@ InteractDoor PROC uses ebx ecx edx esi edi
|
|||
|
||||
invoke GetMapCell, targetX, targetY
|
||||
cmp eax, CELL_DOOR
|
||||
jne check_door_close
|
||||
|
||||
; Open the door!
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN
|
||||
ret
|
||||
|
||||
check_door_close:
|
||||
je interact_door_found
|
||||
cmp eax, CELL_DOOR_OPEN
|
||||
jne check_portal_interact
|
||||
|
||||
; Close the door!
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR
|
||||
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
|
||||
ret
|
||||
|
||||
check_portal_interact:
|
||||
|
|
@ -231,6 +328,7 @@ 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
|
||||
|
|
@ -402,7 +500,7 @@ next_map_copied:
|
|||
cmp fadeEffect, 0
|
||||
je switch_instant
|
||||
|
||||
; Initiate smooth Fade Out (takes 16 frames ~ 0.26s)
|
||||
; Initiate smooth Fade Out (up to 16 fixed ticks, ~0.26s).
|
||||
mov fadeState, 1
|
||||
mov fadeLevel, 1
|
||||
ret
|
||||
|
|
@ -429,11 +527,11 @@ transition_loaded:
|
|||
DoSublevelTransition ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; ApplyScreenFade: Screen darkening post-processing and transition sequencer
|
||||
; UpdateScreenFade: fixed-tick transition sequencer, never called from drawing.
|
||||
; ------------------------------------------------------------------------------
|
||||
ApplyScreenFade PROC uses ebx ecx edx esi edi
|
||||
UpdateScreenFade PROC
|
||||
cmp fadeState, 0
|
||||
je fade_done
|
||||
je fade_tick_done
|
||||
|
||||
cmp fadeState, 1
|
||||
je do_fade_out
|
||||
|
|
@ -441,31 +539,35 @@ ApplyScreenFade PROC uses ebx ecx edx esi edi
|
|||
je do_switch_state
|
||||
cmp fadeState, 3
|
||||
je do_fade_in
|
||||
jmp fade_done
|
||||
jmp fade_tick_done
|
||||
|
||||
do_fade_out:
|
||||
inc fadeLevel
|
||||
cmp fadeLevel, 16
|
||||
jl apply_darkening
|
||||
jl fade_tick_done
|
||||
mov fadeLevel, 16
|
||||
mov fadeState, 2
|
||||
call DoSublevelTransition
|
||||
mov fadeState, 3
|
||||
jmp apply_darkening
|
||||
jmp fade_tick_done
|
||||
|
||||
do_switch_state:
|
||||
call DoSublevelTransition
|
||||
mov fadeState, 3
|
||||
jmp apply_darkening
|
||||
jmp fade_tick_done
|
||||
|
||||
do_fade_in:
|
||||
dec fadeLevel
|
||||
cmp fadeLevel, 0
|
||||
jg apply_darkening
|
||||
jg fade_tick_done
|
||||
mov fadeLevel, 0
|
||||
mov fadeState, 0
|
||||
jmp fade_done
|
||||
fade_tick_done:
|
||||
ret
|
||||
UpdateScreenFade ENDP
|
||||
|
||||
; 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
|
||||
|
|
@ -526,7 +628,7 @@ ApplyScreenFade ENDP
|
|||
LoadSublevelBinary PROC uses ebx ecx edx esi edi, pszPath:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD
|
||||
LOCAL hFile:DWORD, bytesRead:DWORD, header:MapHeader
|
||||
LOCAL numSprites:DWORD, numTransitions:DWORD, index:DWORD
|
||||
LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD
|
||||
LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD, numLights:DWORD
|
||||
LOCAL resolvedPath[MAX_PATH_BYTES]:BYTE
|
||||
LOCAL stagedMap[MAP_SIZE]:BYTE
|
||||
LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE
|
||||
|
|
@ -647,6 +749,16 @@ old_map_no_transitions:
|
|||
movzx eax, header.cutsceneFlags
|
||||
mov sceneFlags, eax
|
||||
old_map_no_cutscene:
|
||||
mov numLights,0
|
||||
cmp header.version,5
|
||||
jb old_map_no_lighting
|
||||
cmp header.lightingReserved,0
|
||||
jne close_load_failed
|
||||
movzx eax,header.lightCount
|
||||
cmp eax,13
|
||||
ja close_load_failed
|
||||
mov numLights,eax
|
||||
old_map_no_lighting:
|
||||
mov eax, numTransitions
|
||||
imul eax, sizeof LevelTransition
|
||||
mov ebx, eax
|
||||
|
|
@ -660,6 +772,13 @@ old_map_no_cutscene:
|
|||
mov eax, numStrings
|
||||
imul eax, CUTSCENE_STRING_BYTES
|
||||
add ebx, eax
|
||||
cmp header.version,5
|
||||
jb map_lighting_size_ready
|
||||
mov eax,numLights
|
||||
imul eax,40
|
||||
add ebx,eax
|
||||
add ebx,32+4096
|
||||
map_lighting_size_ready:
|
||||
invoke GetFileSize, hFile, 0
|
||||
cmp eax, ebx
|
||||
jne close_load_failed
|
||||
|
|
@ -700,6 +819,44 @@ old_map_no_cutscene:
|
|||
jz close_load_failed
|
||||
cmp bytesRead, ebx
|
||||
jne close_load_failed
|
||||
mov esi,OFFSET legacyLightingSettings
|
||||
mov edi,OFFSET stagedLightSettings
|
||||
mov ecx,8
|
||||
rep movsd
|
||||
mov edi,OFFSET stagedLightRGB
|
||||
mov ecx,MAP_SIZE
|
||||
mov eax,00282828h
|
||||
rep stosd
|
||||
cmp header.version,5
|
||||
jb staged_lighting_ready
|
||||
invoke ReadFile,hFile,OFFSET stagedLightSettings,32,ADDR bytesRead,0
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
cmp bytesRead,32
|
||||
jne close_load_failed
|
||||
mov ebx,numLights
|
||||
imul ebx,40
|
||||
invoke ReadFile,hFile,OFFSET stagedLightSources,ebx,ADDR bytesRead,0
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
cmp bytesRead,ebx
|
||||
jne close_load_failed
|
||||
invoke ReadFile,hFile,OFFSET stagedLightRGB,4096,ADDR bytesRead,0
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
cmp bytesRead,4096
|
||||
jne close_load_failed
|
||||
invoke ValidateLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights
|
||||
test eax,eax
|
||||
jz close_load_failed
|
||||
mov esi,OFFSET stagedLightRGB
|
||||
mov ecx,MAP_SIZE
|
||||
map_check_light_rgb:
|
||||
lodsd
|
||||
test eax,0FF000000h
|
||||
jnz close_load_failed
|
||||
loop map_check_light_rgb
|
||||
staged_lighting_ready:
|
||||
invoke ValidateStagedCutscene, numEvents, numStrings, sceneFlags, ADDR stagedMap, numTransitions
|
||||
test eax, eax
|
||||
jz close_load_failed
|
||||
|
|
@ -736,6 +893,49 @@ 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
|
||||
|
|
@ -902,7 +1102,22 @@ 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:
|
||||
|
|
@ -971,6 +1186,49 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,244 +1,221 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Player Movement, Physics & Collision Handling
|
||||
; ==============================================================================
|
||||
|
||||
; Fixed-tick player simulation. Wire coordinates remain raw world units.
|
||||
.code
|
||||
StartCutscene PROTO
|
||||
StopCutscene PROTO
|
||||
UpdateCutscene PROTO
|
||||
|
||||
; 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
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdatePlayer: Updates rotation, computes movement vector & handles collisions
|
||||
; ------------------------------------------------------------------------------
|
||||
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 PROC uses ebx ecx edx esi edi
|
||||
LOCAL moveX:DWORD, moveY:DWORD
|
||||
LOCAL speed:DWORD
|
||||
LOCAL nextX:DWORD, nextY:DWORD
|
||||
LOCAL checkGridX:DWORD, checkGridY:DWORD
|
||||
|
||||
LOCAL forward:SDWORD, strafe:SDWORD, speed:DWORD
|
||||
LOCAL moveX:SDWORD, moveY:SDWORD, steps:DWORD
|
||||
mov gameplayTickBlocked, 1
|
||||
cmp isGamePaused, 0
|
||||
jne player_done
|
||||
cmp gameDialogActive, 0
|
||||
jne player_done
|
||||
cmp isPlayerDead, 0
|
||||
jne update_player_done
|
||||
; Freeze player movement and inputs during level transitions
|
||||
jne player_done
|
||||
cmp fadeState, 0
|
||||
jne update_player_done
|
||||
|
||||
jne player_done
|
||||
cmp cutscenePending, 0
|
||||
je scene_manual_start
|
||||
je @F
|
||||
call StartCutscene
|
||||
scene_manual_start:
|
||||
@@:
|
||||
cmp keyCutscene, 0
|
||||
je scene_check_active
|
||||
je @F
|
||||
cmp isCutsceneActive, 0
|
||||
jne scene_check_active
|
||||
jne @F
|
||||
call StartCutscene
|
||||
scene_check_active:
|
||||
@@:
|
||||
cmp isCutsceneActive, 0
|
||||
je normal_player_update
|
||||
je player_active
|
||||
cmp keySkipScene, 0
|
||||
je scene_update_tick
|
||||
je player_scene
|
||||
call StopCutscene
|
||||
ret
|
||||
scene_update_tick:
|
||||
player_scene:
|
||||
call UpdateCutscene
|
||||
ret
|
||||
normal_player_update:
|
||||
player_active:
|
||||
mov gameplayTickBlocked, 0
|
||||
mov moveX, 0
|
||||
mov moveY, 0
|
||||
|
||||
; 1. Handle Camera Rotation
|
||||
cmp keyTurnLeft, 0
|
||||
je check_turn_right
|
||||
mov eax, playerAngle
|
||||
sub eax, playerTurnSpeed
|
||||
movzx eax, keyTurnRight
|
||||
movzx ebx, keyTurnLeft
|
||||
sub eax, ebx
|
||||
imul eax, playerTurnSpeed
|
||||
add eax, playerAngle
|
||||
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 check_movement
|
||||
je @F
|
||||
call InteractDoor
|
||||
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
|
||||
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 keyRun, 0
|
||||
je @F
|
||||
mov eax, playerRunSpeed ; Sprint speed
|
||||
mov eax, playerRunSpeed
|
||||
@@:
|
||||
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
|
||||
|
||||
; 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 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:
|
||||
mov eax, moveX
|
||||
or eax, moveY
|
||||
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
|
||||
jz player_done
|
||||
inc steps
|
||||
cmp steps, 64
|
||||
ja player_done
|
||||
cmp moveX, 0
|
||||
jge @F
|
||||
mov eax, playerRadius
|
||||
sub ebx, eax ; Moving left: check left edge
|
||||
jmp check_x_col
|
||||
je step_y
|
||||
mov esi, 1
|
||||
jg @F
|
||||
mov esi, -1
|
||||
@@:
|
||||
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
|
||||
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
|
||||
jge @F
|
||||
mov eax, playerRadius
|
||||
sub ecx, eax
|
||||
jmp check_y_col
|
||||
@@:
|
||||
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
|
||||
add ebx, esi
|
||||
invoke CanOccupyPlayer, ebx, playerY
|
||||
test eax, eax
|
||||
jnz update_player_done ; Collision on Y
|
||||
allow_portal_y:
|
||||
|
||||
mov eax, nextY
|
||||
mov playerY, eax
|
||||
|
||||
update_player_done:
|
||||
cmp fadeState, 0
|
||||
jne @F
|
||||
jz blocked_x
|
||||
mov playerX, ebx
|
||||
sub moveX, esi
|
||||
jmp step_y
|
||||
blocked_x:
|
||||
mov moveX, 0
|
||||
mov playerFractionX, 0
|
||||
step_y:
|
||||
cmp moveY, 0
|
||||
je player_step
|
||||
mov esi, 1
|
||||
jg @F
|
||||
mov esi, -1
|
||||
@@:
|
||||
mov ebx, playerY
|
||||
add ebx, esi
|
||||
invoke CanOccupyPlayer, playerX, ebx
|
||||
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
|
||||
call CheckPortalTriggers
|
||||
@@:
|
||||
ret
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
; Windowless renderer used by EblanStudio. Same Render3DScene and overlays as game.
|
||||
; Fixed wire layout is mirrored by editor/src/preview_protocol.h.
|
||||
.data
|
||||
previewPrefix db "--preview:",0
|
||||
previewHandle dd 0
|
||||
previewMemory dd 0
|
||||
previewParent dd 0
|
||||
|
|
@ -17,14 +16,14 @@ TryEnginePreview PROC uses ebx ecx edx esi edi
|
|||
test eax, eax
|
||||
jz preview_exit_error
|
||||
mov previewHandle, eax
|
||||
invoke MapViewOfFile, eax, 6, 0, 0, 1357792
|
||||
invoke MapViewOfFile, eax, 6, 0, 0, 1362408
|
||||
test eax, eax
|
||||
jz preview_exit_error
|
||||
mov previewMemory, eax
|
||||
mov ebx, eax
|
||||
cmp DWORD PTR [ebx], 56504245h
|
||||
jne preview_exit_error
|
||||
cmp DWORD PTR [ebx+4], 1
|
||||
cmp DWORD PTR [ebx+4], 2
|
||||
jne preview_exit_error
|
||||
invoke OpenProcess, 00100000h, 0, DWORD PTR [ebx+12] ; SYNCHRONIZE
|
||||
test eax, eax
|
||||
|
|
@ -107,6 +106,39 @@ preview_textures:
|
|||
mov ecx, MAX_TEXTURES*TEX_SIZE_BYTES/4
|
||||
rep movsd
|
||||
preview_scene:
|
||||
mov eax,[ebx+52]
|
||||
mov stagedLightSettings,eax
|
||||
mov eax,[ebx+56]
|
||||
mov stagedLightSettings[4],eax
|
||||
mov eax,[ebx+60]
|
||||
mov stagedLightSettings[8],eax
|
||||
mov eax,[ebx+68]
|
||||
mov stagedLightSettings[12],eax
|
||||
mov eax,[ebx+72]
|
||||
mov stagedLightSettings[16],eax
|
||||
mov eax,[ebx+76]
|
||||
mov stagedLightSettings[20],eax
|
||||
mov stagedLightSettings[24],1
|
||||
mov stagedLightSettings[28],0
|
||||
lea esi,[ebx+1357792]
|
||||
invoke ValidateLightingData,OFFSET stagedLightSettings,esi,DWORD PTR [ebx+64]
|
||||
test eax,eax
|
||||
jz preview_exit_error
|
||||
lea esi,[ebx+1358312]
|
||||
mov edi,MAP_SIZE
|
||||
preview_check_light_rgb:
|
||||
lodsd
|
||||
test eax,0FF000000h
|
||||
jnz preview_exit_error
|
||||
dec edi
|
||||
jnz preview_check_light_rgb
|
||||
lea esi,[ebx+1357792]
|
||||
lea edi,[ebx+1358312]
|
||||
invoke CommitLightingData,OFFSET stagedLightSettings,esi,DWORD PTR [ebx+64],edi
|
||||
mov eax,[ebx+84]
|
||||
dec eax
|
||||
mov lightingTick,eax
|
||||
call UpdateDynamicLights
|
||||
call Render3DScene
|
||||
mov ebx, previewMemory
|
||||
cmp DWORD PTR [ebx+40], 0
|
||||
|
|
|
|||
|
|
@ -7,6 +7,25 @@
|
|||
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)
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -36,16 +55,35 @@ 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 eax, mapCeilingColor
|
||||
mov edx,mapCeilingColor
|
||||
call ShadeLightPixel
|
||||
mov eax,edx
|
||||
cmp lightingMode,0
|
||||
jne @F
|
||||
cmp playerFlashTimer, 0
|
||||
je @F
|
||||
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
|
||||
@@:
|
||||
mov ecx, (SCREEN_W * SCREEN_HALF_H)
|
||||
rep stosd
|
||||
|
||||
mov eax, mapFloorColor
|
||||
mov edx,mapFloorColor
|
||||
call ShadeLightPixel
|
||||
mov eax,edx
|
||||
cmp lightingMode,0
|
||||
jne @F
|
||||
cmp playerFlashTimer, 0
|
||||
je @F
|
||||
add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost
|
||||
@@:
|
||||
mov ecx, (SCREEN_W * SCREEN_HALF_H)
|
||||
rep stosd
|
||||
|
||||
|
|
@ -60,7 +98,7 @@ ray_column_loop:
|
|||
; Lookup Tangential Ray Angle Offset (flat plane perspective correction)
|
||||
mov eax, screenX
|
||||
mov edx, [tableRayAngleOffset + eax*4]
|
||||
add edx, playerAngle
|
||||
add edx, viewAngle
|
||||
and edx, ANGLE_MASK
|
||||
mov rayAngle, edx
|
||||
mov eax, edx
|
||||
|
|
@ -72,11 +110,11 @@ ray_column_loop:
|
|||
mov rayDirY, edx
|
||||
|
||||
; Current Grid Cell
|
||||
mov eax, playerX
|
||||
mov eax, viewX
|
||||
sar eax, 6
|
||||
mov mapX, eax
|
||||
|
||||
mov eax, playerY
|
||||
mov eax, viewY
|
||||
sar eax, 6
|
||||
mov mapY, eax
|
||||
|
||||
|
|
@ -127,7 +165,7 @@ nonzero_step_x:
|
|||
; sideDistX = (playerX - (mapX * 64)) * deltaDistX / 64
|
||||
mov eax, mapX
|
||||
shl eax, 6
|
||||
mov edx, playerX
|
||||
mov edx, viewX
|
||||
sub edx, eax
|
||||
mov eax, edx
|
||||
imul deltaDistX
|
||||
|
|
@ -141,7 +179,7 @@ ray_dir_x_pos:
|
|||
mov eax, mapX
|
||||
inc eax
|
||||
shl eax, 6
|
||||
sub eax, playerX
|
||||
sub eax, viewX
|
||||
imul deltaDistX
|
||||
shrd eax, edx, 6
|
||||
mov sideDistX, eax
|
||||
|
|
@ -158,7 +196,7 @@ nonzero_step_y:
|
|||
mov stepY, -1
|
||||
mov eax, mapY
|
||||
shl eax, 6
|
||||
mov edx, playerY
|
||||
mov edx, viewY
|
||||
sub edx, eax
|
||||
mov eax, edx
|
||||
imul deltaDistY
|
||||
|
|
@ -171,7 +209,7 @@ ray_dir_y_pos:
|
|||
mov eax, mapY
|
||||
inc eax
|
||||
shl eax, 6
|
||||
sub eax, playerY
|
||||
sub eax, viewY
|
||||
imul deltaDistY
|
||||
shrd eax, edx, 6
|
||||
mov sideDistY, eax
|
||||
|
|
@ -279,7 +317,7 @@ glass_apply_fisheye:
|
|||
mov ebx, rayDirY
|
||||
imul ebx
|
||||
shrd eax, edx, 8
|
||||
mov ebx, playerY
|
||||
mov ebx, viewY
|
||||
shl ebx, 16
|
||||
add eax, ebx
|
||||
jmp glass_compute_tex_x
|
||||
|
|
@ -288,7 +326,7 @@ glass_tex_hit_y:
|
|||
mov ebx, rayDirX
|
||||
imul ebx
|
||||
shrd eax, edx, 8
|
||||
mov ebx, playerX
|
||||
mov ebx, viewX
|
||||
shl ebx, 16
|
||||
add eax, ebx
|
||||
glass_compute_tex_x:
|
||||
|
|
@ -400,7 +438,7 @@ rounded_distance_ready:
|
|||
mov ebx, rayDirY
|
||||
imul ebx
|
||||
shrd eax, edx, 8 ; (24.8 * 16.16) -> 16.16
|
||||
mov ebx, playerY
|
||||
mov ebx, viewY
|
||||
shl ebx, 16
|
||||
add eax, ebx
|
||||
jmp compute_tex_x
|
||||
|
|
@ -411,7 +449,7 @@ tex_hit_y:
|
|||
mov ebx, rayDirX
|
||||
imul ebx
|
||||
shrd eax, edx, 8
|
||||
mov ebx, playerX
|
||||
mov ebx, viewX
|
||||
shl ebx, 16
|
||||
add eax, ebx
|
||||
|
||||
|
|
@ -465,6 +503,11 @@ wall_texture_ready:
|
|||
imul eax, texStep
|
||||
mov texPos, eax
|
||||
|
||||
; Compute wall lighting (baked lightmap + dynamic point lights + flashlight)
|
||||
invoke ComputeWallLightLevel, screenX, mapX, mapY, sideHit, stepX, stepY,\
|
||||
rawDist, perpWallDist, rayAngle, rayDirX, rayDirY
|
||||
mov colLightLevel, eax
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; Draw Vertical Wall Stripe
|
||||
; --------------------------------------------------------------------------
|
||||
|
|
@ -493,6 +536,9 @@ wall_pixel_loop:
|
|||
add eax, texPtr
|
||||
mov edx, [eax] ; Sampled Color (BGRA)
|
||||
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
jne apply_col_light
|
||||
|
||||
; Wall shading: Y-facing walls darkened 50% for 3D depth perception
|
||||
cmp sideHit, 1
|
||||
jne apply_distance_fog
|
||||
|
|
@ -513,6 +559,10 @@ apply_distance_fog:
|
|||
|
||||
make_pitch_black:
|
||||
mov edx, 00111111h
|
||||
jmp write_wall_pixel
|
||||
|
||||
apply_col_light:
|
||||
call ShadeLightPixel
|
||||
|
||||
write_wall_pixel:
|
||||
mov eax, pScreenPixel
|
||||
|
|
@ -591,6 +641,7 @@ RenderSprites PROC uses ebx ecx edx esi edi passFilter:DWORD
|
|||
LOCAL texPtr:DWORD, pPixel:DWORD
|
||||
LOCAL camCos:DWORD, camSin:DWORD
|
||||
LOCAL keyIdx:DWORD, keyDist:DWORD
|
||||
LOCAL sprLightLevel:DWORD
|
||||
|
||||
mov eax, spriteCount
|
||||
test eax, eax
|
||||
|
|
@ -598,7 +649,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, playerAngle
|
||||
sub eax, viewAngle
|
||||
and eax, ANGLE_MASK
|
||||
mov ecx, [tableCos + eax*4]
|
||||
mov camCos, ecx
|
||||
|
|
@ -619,12 +670,12 @@ dist_loop:
|
|||
add ebx, OFFSET spriteList
|
||||
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
sub eax, playerX ; dx
|
||||
sub eax, viewX ; dx
|
||||
imul eax, eax
|
||||
mov ecx, eax
|
||||
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
sub eax, playerY ; dy
|
||||
sub eax, viewY ; dy
|
||||
imul eax, eax
|
||||
add eax, ecx
|
||||
mov [ebx].SpriteStruct.dist, eax
|
||||
|
|
@ -708,11 +759,11 @@ sprite_loop:
|
|||
|
||||
; Sprite position relative to player (raw units)
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
sub eax, playerX
|
||||
sub eax, viewX
|
||||
mov spriteX, eax
|
||||
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
sub eax, playerY
|
||||
sub eax, viewY
|
||||
mov spriteY, eax
|
||||
|
||||
; Transform to camera space:
|
||||
|
|
@ -836,6 +887,9 @@ sprite_loop:
|
|||
mov texStepY, eax
|
||||
|
||||
; Render vertical stripes
|
||||
invoke ComputeSpriteLight, spriteX, spriteY, transY
|
||||
mov sprLightLevel, eax
|
||||
|
||||
mov eax, drawStartX
|
||||
mov stripe, eax
|
||||
|
||||
|
|
@ -916,6 +970,9 @@ sprite_pixel_y:
|
|||
cmp edx, COLOR_CHROMA_KEY
|
||||
je skip_sprite_pixel
|
||||
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
jne apply_spr_lighting
|
||||
|
||||
; Distance fog (same thresholds as walls)
|
||||
mov eax, transY
|
||||
cmp eax, 350
|
||||
|
|
@ -927,6 +984,10 @@ sprite_pixel_y:
|
|||
jmp write_sprite_pixel
|
||||
sprite_pixel_dark:
|
||||
mov edx, 00111111h
|
||||
jmp write_sprite_pixel
|
||||
|
||||
apply_spr_lighting:
|
||||
call ShadeLightPixel
|
||||
|
||||
write_sprite_pixel:
|
||||
mov eax, pPixel
|
||||
|
|
|
|||
253
include/engine_simulation.inc
Normal file
253
include/engine_simulation.inc
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
; 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
149
main.asm
|
|
@ -112,10 +112,13 @@ 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
|
||||
|
|
@ -133,14 +136,7 @@ WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
|
|||
ret
|
||||
|
||||
handle_keydown:
|
||||
cmp wParam, VK_KEY_ESCAPE
|
||||
jne check_f12
|
||||
invoke PostQuitMessage, 0
|
||||
mov isRunning, 0
|
||||
xor eax, eax
|
||||
ret
|
||||
check_f12:
|
||||
; Movement and action edges are handled only by PollInput.
|
||||
; Key edges and ESC pause menu are handled via PollInput
|
||||
invoke DefWindowProcA, hWnd, uMsg, wParam, lParam
|
||||
ret
|
||||
|
||||
|
|
@ -164,7 +160,8 @@ Engine_Init PROC uses ebx esi edi
|
|||
call InitMathTables
|
||||
call GenerateDefaultTextures ; Procedural baseline (always valid)
|
||||
call LoadTexturePack ; Optional: override from assets\textures\texturepack.bin
|
||||
call LoadMapBinary ; Loads assets/map.dat (falls back to InitMap if missing)
|
||||
call InitLightingSystem
|
||||
call LoadMapBinary ; Each room commits its own validated lighting
|
||||
|
||||
; 2. Register Window Class
|
||||
invoke GetModuleHandleA, 0
|
||||
|
|
@ -211,8 +208,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
|
||||
|
|
@ -226,19 +223,30 @@ Engine_RenderHUD PROC uses ebx ecx edx
|
|||
call RenderCutsceneOverlay
|
||||
ret
|
||||
normal_engine_hud:
|
||||
cmp hudStyle, HUD_STYLE_DEBUG
|
||||
jne clean_hud_mode
|
||||
|
||||
; --- DEBUG / DEVELOPER HUD MODE ---
|
||||
; 1. Top HUD Header
|
||||
invoke DrawString8x8, 10, 10, OFFSET strEngineName, COLOR_YELLOW
|
||||
invoke DrawString8x8, 8, 8, OFFSET strEngineName, COLOR_YELLOW
|
||||
|
||||
; 2. FPS Counter
|
||||
invoke DrawString8x8, 10, 24, OFFSET szFpsLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 42, 24, currentFps, COLOR_CYAN
|
||||
; 2. FPS Counter (drawn at top right X=250 so it never overlaps left elements)
|
||||
invoke DrawString8x8, 250, 8, OFFSET szFpsLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 282, 8, currentFps, COLOR_CYAN
|
||||
|
||||
; 3. Controls Bar at Bottom
|
||||
; 3. Controls Bar at Bottom (Debug mode only)
|
||||
invoke DrawRect, 0, 215, SCREEN_W, 25, 00101010h
|
||||
invoke DrawString8x8, 10, 218, OFFSET strControls1, COLOR_WHITE
|
||||
invoke DrawString8x8, 10, 228, OFFSET strControls3, COLOR_GREEN
|
||||
jmp check_hints
|
||||
|
||||
; 4. Door / Portal Interaction Hint (center screen)
|
||||
clean_hud_mode:
|
||||
; --- CLEAN / IMMERSIVE HUD MODE ---
|
||||
; In clean mode, no watermarks and no permanent bottom bar.
|
||||
; Full 320x240 view is dedicated to the game world.
|
||||
|
||||
check_hints:
|
||||
; 4. Door / Portal Interaction Hint (Cleanly centered above crosshair)
|
||||
call CheckDoorHint
|
||||
cmp eax, 1
|
||||
je show_door_hint
|
||||
|
|
@ -247,11 +255,11 @@ normal_engine_hud:
|
|||
jmp check_minimap
|
||||
|
||||
show_door_hint:
|
||||
invoke DrawString8x8, 128, 150, OFFSET strDoorHint, COLOR_YELLOW
|
||||
invoke DrawString8x8, 116, 140, OFFSET strDoorHint, COLOR_YELLOW
|
||||
jmp check_minimap
|
||||
|
||||
show_portal_hint:
|
||||
invoke DrawString8x8, 110, 150, OFFSET strPortalHint, COLOR_CYAN
|
||||
invoke DrawString8x8, 110, 140, OFFSET strPortalHint, COLOR_CYAN
|
||||
|
||||
check_minimap:
|
||||
cmp mapErrorFrames, 0
|
||||
|
|
@ -261,23 +269,16 @@ 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: Updates single tick and presents frame buffer to window
|
||||
; Engine_Frame: draws/presents only; simulation runs independently at 60 Hz.
|
||||
; ------------------------------------------------------------------------------
|
||||
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
|
||||
|
|
@ -286,6 +287,7 @@ no_error_tick:
|
|||
call RenderCombatHUD
|
||||
@@:
|
||||
call ApplyCutsceneFade
|
||||
call RenderPauseMenu
|
||||
|
||||
; 2b. Screenshot request (F12): dump fresh framebuffer to BMP
|
||||
cmp keyScreenshot, 0
|
||||
|
|
@ -320,16 +322,6 @@ no_error_tick:
|
|||
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
|
||||
|
||||
|
|
@ -364,14 +356,14 @@ ParseStartupMap PROC uses ebx ecx edx esi edi
|
|||
skip_executable_quote:
|
||||
lodsb
|
||||
test al, al
|
||||
jz startup_arg_done
|
||||
jz check_cmdline_flags
|
||||
cmp al, 34
|
||||
jne skip_executable_quote
|
||||
jmp skip_argument_spaces
|
||||
skip_executable_word:
|
||||
lodsb
|
||||
test al, al
|
||||
jz startup_arg_done
|
||||
jz check_cmdline_flags
|
||||
cmp al, 32
|
||||
jne skip_executable_word
|
||||
skip_argument_spaces:
|
||||
|
|
@ -408,10 +400,59 @@ write_startup_char:
|
|||
dec ecx
|
||||
jnz startup_argument_loop
|
||||
mov startupMapPath, 0
|
||||
ret
|
||||
jmp check_cmdline_flags
|
||||
terminate_startup_arg:
|
||||
mov byte ptr [edi], 0
|
||||
startup_arg_done:
|
||||
|
||||
; If startupMapPath starts with '-' and not '--preview:', clear it
|
||||
mov al, byte ptr [startupMapPath]
|
||||
cmp al, '-'
|
||||
jne check_cmdline_flags
|
||||
mov esi, OFFSET startupMapPath
|
||||
mov edi, OFFSET previewPrefix
|
||||
mov ecx, 10
|
||||
repe cmpsb
|
||||
je check_cmdline_flags
|
||||
mov byte ptr [startupMapPath], 0
|
||||
|
||||
check_cmdline_flags:
|
||||
invoke GetCommandLineA
|
||||
mov ebx, eax
|
||||
|
||||
invoke CheckCmdFlag, ebx, OFFSET szFlagNoWeapons
|
||||
test eax, eax
|
||||
jz @F
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
mov enableWeapons, 0
|
||||
mov playerWeapon, WEAPON_NONE
|
||||
mov showCrosshair, 0
|
||||
@@:
|
||||
invoke CheckCmdFlag, ebx, OFFSET szFlagAdventure
|
||||
test eax, eax
|
||||
jz @F
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
mov enableWeapons, 0
|
||||
mov playerWeapon, WEAPON_NONE
|
||||
mov showCrosshair, 0
|
||||
@@:
|
||||
invoke CheckCmdFlag, ebx, OFFSET szFlagAction
|
||||
test eax, eax
|
||||
jz @F
|
||||
mov gameMode, GAME_MODE_ACTION
|
||||
mov enableWeapons, 1
|
||||
mov playerWeapon, WEAPON_PISTOL
|
||||
mov showCrosshair, 1
|
||||
@@:
|
||||
invoke CheckCmdFlag, ebx, OFFSET szFlagDebugHud
|
||||
test eax, eax
|
||||
jz @F
|
||||
mov hudStyle, HUD_STYLE_DEBUG
|
||||
@@:
|
||||
invoke CheckCmdFlag, ebx, OFFSET szFlagCleanHud
|
||||
test eax, eax
|
||||
jz @F
|
||||
mov hudStyle, HUD_STYLE_CLEAN
|
||||
@@:
|
||||
ret
|
||||
ParseStartupMap ENDP
|
||||
|
||||
|
|
@ -420,8 +461,8 @@ ParseStartupMap ENDP
|
|||
; ------------------------------------------------------------------------------
|
||||
main PROC
|
||||
LOCAL msg:MSG
|
||||
LOCAL curTick:DWORD, elapsed:DWORD
|
||||
|
||||
call LoadProjectSettings
|
||||
call ParseStartupMap
|
||||
call TryEnginePreview
|
||||
call Engine_Init
|
||||
|
|
@ -444,24 +485,10 @@ main_loop:
|
|||
jmp @B
|
||||
|
||||
process_game_tick:
|
||||
; 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 PollInput
|
||||
call PumpSimulationClock
|
||||
call Engine_Frame
|
||||
invoke Sleep, 1
|
||||
jmp main_loop
|
||||
|
||||
exit_loop:
|
||||
|
|
|
|||
BIN
main.obj
BIN
main.obj
Binary file not shown.
|
|
@ -11,7 +11,7 @@ static void RunCutsceneChecks(EditorApp& app) {
|
|||
};
|
||||
Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference");
|
||||
BinaryLevel level; std::string error;
|
||||
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == 4 && level.cutscene.events.size() == 9 &&
|
||||
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == MAP_FORMAT_VERSION && level.cutscene.events.size() == 9 &&
|
||||
level.cutscene.strings[0] == "Привет, мир!" && level.cutscene.flags == CUTSCENE_AUTOPLAY,
|
||||
"v4 track round trip retains UTF8 and autoplay");
|
||||
std::string encoded;
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ scene_trace_tick:
|
|||
mov edi, esi
|
||||
imul edi, 24
|
||||
add edi, OFFSET sceneTrace
|
||||
mov eax, playerX
|
||||
mov eax, csCameraX
|
||||
stosd
|
||||
mov eax, playerY
|
||||
mov eax, csCameraY
|
||||
stosd
|
||||
mov eax, playerAngle
|
||||
mov eax, csCameraAngle
|
||||
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 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 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 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 playerX, 192 ; next frame is driven only by cinematic interpolation
|
||||
CHECK csCameraX, 192 ; next frame is driven only by cinematic interpolation
|
||||
mov keySkipScene, 1
|
||||
call UpdatePlayer
|
||||
CHECK isCutsceneActive, 0 ; Tab skips and releases camera control
|
||||
|
|
@ -105,11 +105,14 @@ 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, 0 ; scene masks normal movement input
|
||||
CHECK eax, 1 ; polling records held movement independently of scene ownership
|
||||
movzx eax, keySkipScene
|
||||
CHECK eax, 1 ; skip input remains available in a scene
|
||||
call StopCutscene
|
||||
|
|
@ -202,7 +205,7 @@ scene_next_pixel:
|
|||
CHECK fadeLevel, 16 ; transition preserves an existing cinematic fade without a bright flash
|
||||
mov esi, 32
|
||||
scene_transition_fade:
|
||||
call ApplyScreenFade
|
||||
call UpdateScreenFade
|
||||
dec esi
|
||||
jnz scene_transition_fade
|
||||
CHECK spriteCount, 1 ; scripted transition loads the destination room
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ 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;
|
||||
|
|
@ -139,6 +141,7 @@ int main(int argc, char** argv) {
|
|||
app.LaunchPlaytest();
|
||||
Check(!app.isEngineRunning && app.playtestDirectory.empty() && Bytes("../assets/map.dat") == originalMain,
|
||||
"missing linked map aborts F5 without data loss");
|
||||
RunLightingChecks(app);
|
||||
RunCutsceneChecks(app);
|
||||
if (argc > 1) {
|
||||
Check(ReadLevelFile(std::string(argv[1]) + "/map.dat", preview, error), "existing project main map remains compatible");
|
||||
|
|
@ -154,6 +157,7 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,19 @@ 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 256 dup(0)
|
||||
results dd 512 dup(0)
|
||||
resultPath db 'engine_results.bin',0
|
||||
anglePath db 'angles.bin',0
|
||||
framePath db 'frame.bgra',0
|
||||
|
|
@ -126,7 +129,11 @@ 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
|
||||
|
|
@ -173,7 +180,7 @@ dirty_stack:
|
|||
invoke SwitchSublevel, OFFSET mainPath, 2, 2, 0, 1
|
||||
mov esi, 32
|
||||
fade_frames:
|
||||
call ApplyScreenFade
|
||||
call UpdateScreenFade
|
||||
dec esi
|
||||
jnz fade_frames
|
||||
CHECK fadeState, 0 ; fade finishes and releases player
|
||||
|
|
@ -256,6 +263,7 @@ 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
|
||||
|
|
@ -363,8 +371,11 @@ 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
|
||||
|
|
|
|||
85
tests/integration_engine_checks.inc
Normal file
85
tests/integration_engine_checks.inc
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
; Compatibility of stage-1 fixes with newer weapon/modal/lighting systems.
|
||||
.code
|
||||
RunIntegrationChecks PROC
|
||||
call ResetCombatFixture
|
||||
mov isGamePaused, 1
|
||||
mov keyForward, 1
|
||||
mov playerAttackCooldown, 4
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK playerX, 160 ; pause freezes player movement through production procedure
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; pause freezes guard AI through production procedure
|
||||
CHECK playerAttackCooldown, 4 ; pause freezes weapon cooldowns
|
||||
CHECK playerAmmo, 50 ; pause preserves ammunition
|
||||
mov isGamePaused, 0
|
||||
mov keyForward, 0
|
||||
call UpdatePlayer
|
||||
mov gameDialogActive, 1
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.x, 288 ; modal dialogue freezes guards
|
||||
CHECK playerAmmo, 50 ; modal dialogue cannot fire
|
||||
mov gameDialogActive, 0
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; peaceful adventure mode cannot damage targets
|
||||
call ResetCombatFixture
|
||||
mov isPlayerDead, 1
|
||||
mov transitionCount, 1
|
||||
mov [transitionList].LevelTransition.triggerCellX, 2
|
||||
mov [transitionList].LevelTransition.triggerCellY, 2
|
||||
mov [transitionList].LevelTransition.fadeEffect, 1
|
||||
mov portalLatchCell, -1
|
||||
call UpdatePlayer
|
||||
CHECK fadeState, 0 ; dead player cannot activate a room transition
|
||||
|
||||
call ResetCombatFixture
|
||||
mov playerWeapon, WEAPON_KNIFE
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 100 ; knife cannot hit beyond its world range
|
||||
CHECK playerAmmo, 50 ; knife consumes no ammunition
|
||||
mov playerAttackCooldown, 0
|
||||
mov [spriteList].SpriteStruct.x, 208
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 70 ; knife retains its close-range damage after merge
|
||||
call ResetCombatFixture
|
||||
mov playerWeapon, WEAPON_SHOTGUN
|
||||
mov [spriteList].SpriteStruct.y, 184
|
||||
call UpdateCombat
|
||||
CHECK entityHealth, 30 ; shotgun retains wider world-space hitbox and damage
|
||||
CHECK playerAmmo, 48 ; shotgun consumes two rounds
|
||||
mov eax, [lightSources+14*sizeof LightSource].LightSource.flags
|
||||
and eax, LIGHT_FLAG_ACTIVE
|
||||
CHECK eax, 1 ; shotgun spawns muzzle light in reserved slot
|
||||
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.textureId, 11
|
||||
mov [spriteList].SpriteStruct.flags, 06026400h
|
||||
call InitEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.textureId, 11 ; explicit Prop with barrel texture cannot explode
|
||||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_BARREL ; legacy barrel archetype matches editor fallback
|
||||
|
||||
; Real explosion with a nearby guard: CDQ must not destroy sprite pointer.
|
||||
call ResetCombatFixture
|
||||
mov [spriteList].SpriteStruct.textureId, 11
|
||||
mov [spriteList].SpriteStruct.flags, ENTITY_TYPE_BARREL
|
||||
mov spriteCount, 2
|
||||
mov esi, OFFSET spriteList+sizeof SpriteStruct
|
||||
mov [esi].SpriteStruct.x, 352
|
||||
mov [esi].SpriteStruct.y, 160
|
||||
mov [esi].SpriteStruct.textureId, 13
|
||||
mov [esi].SpriteStruct.scale, FIXED_ONE
|
||||
mov [esi].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
call UpdateCombat
|
||||
CHECK [spriteList].SpriteStruct.textureId, 0 ; explicit barrel still explodes on hitscan
|
||||
CHECK entityHealth[4], 40 ; blast damages nearby guard without corrupting pointer
|
||||
mov eax, [lightSources+15*sizeof LightSource].LightSource.flags
|
||||
and eax, LIGHT_FLAG_ACTIVE
|
||||
CHECK eax, 1 ; explosion spawns light in reserved slot
|
||||
ret
|
||||
RunIntegrationChecks ENDP
|
||||
65
tests/lighting_editor_checks.inl
Normal file
65
tests/lighting_editor_checks.inl
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
static void RunLightingChecks(EditorApp& app) {
|
||||
app.InitDefaultLevel();app.sprites.clear();app.transitions.clear();app.lightSources.clear();
|
||||
memset(app.map,0,sizeof(app.map));
|
||||
for(int i=0;i<32;++i){app.map[0][i]=app.map[31][i]=app.map[i][0]=app.map[i][31]=1;app.map[i][10]=1;}
|
||||
app.playerStartX=2;app.playerStartY=5;app.playerStartAngle=0;
|
||||
app.lightingMode=2;app.ambientLightLevel=10;app.isFlashlightOn=false;
|
||||
EditorLightSource fixed;fixed.x=352;fixed.y=352;fixed.radius=900;fixed.intensity=220;fixed.curIntensity=220;
|
||||
fixed.lightType=LIGHT_TYPE_STATIC;fixed.flags=LIGHT_FLAG_ACTIVE|LIGHT_FLAG_SHADOWS;fixed.color=0xFF0000;
|
||||
app.lightSources.push_back(fixed);app.BakeLighting();
|
||||
auto red=app.bakedLightRGB[5*32+5];
|
||||
Check(((red>>16)&255)>100 && (red&0xFFFF)==0x0A0A,"static bake preserves RGB light color");
|
||||
Check(app.bakedLightRGB[5*32+14]==0x0A0A0A,"baked light cannot cross a wall or smooth through it");
|
||||
auto before=app.bakedLightRGB;
|
||||
EditorLightSource moving=fixed;moving.x=480;moving.lightType=LIGHT_TYPE_POINT;moving.color=0x0000FF;
|
||||
app.lightSources.push_back(moving);app.BakeLighting();
|
||||
Check(app.bakedLightRGB==before,"dynamic lights never enter the static bake");
|
||||
app.map[5][7]=CELL_DOOR;
|
||||
Check(!app.CastLightRay(352,352,608,352),"closed door blocks editor light rays");
|
||||
app.map[5][7]=CELL_DOOR_OPEN;
|
||||
Check(app.CastLightRay(352,352,608,352),"open door transmits editor light rays");
|
||||
app.map[5][7]=0;app.map[5][6]=1;
|
||||
Check(!app.CastLightRay(352,352,416,416),"diagonal light cannot leak through a blocked corner");
|
||||
app.map[5][6]=0;
|
||||
app.lightSources[0].flags=LIGHT_FLAG_ACTIVE;app.BakeLighting();
|
||||
Check(((app.bakedLightRGB[5*32+14]>>16)&255)>10,"disabling shadows permits light across walls");
|
||||
app.lightSources[0].flags|=LIGHT_FLAG_SHADOWS;app.BakeLighting();
|
||||
app.PushUndo();app.lightSources[0].color=0x00FF00;app.ambientLightLevel=25;app.lightingMode=1;app.BakeLighting();
|
||||
app.Undo();
|
||||
Check(app.lightSources[0].color==0xFF0000 && app.ambientLightLevel==10 && app.lightingMode==2 && app.bakedLightRGB==before,
|
||||
"undo restores sources, settings and the exact baked RGB data");
|
||||
app.Redo();Check(app.lightSources[0].color==0x00FF00 && app.ambientLightLevel==25,"redo restores authored lighting");
|
||||
app.Undo();
|
||||
app.lightSources[0].x+=1;
|
||||
Check(app.LightingNeedsBake(),"editing static lights marks the bake out of date");
|
||||
app.lightSources[0].x-=1;app.lightSources[1].x+=64;
|
||||
Check(!app.LightingNeedsBake(),"moving a realtime source does not invalidate static lighting");
|
||||
app.lightSources[1].x-=64;
|
||||
Check(app.SaveLevel("../assets/lit.dat"),"save v5 authored lights and persistent RGB bake");
|
||||
std::ofstream reference("../assets/lighting_reference.bin",std::ios::binary);
|
||||
reference.write(reinterpret_cast<const char*>(app.bakedLightRGB.data()),4096);reference.close();
|
||||
auto valid=Bytes("../assets/lit.dat");
|
||||
BinaryLevel loaded;std::string error;
|
||||
Check(ReadLevelFile("../assets/lit.dat",loaded,error) && loaded.header.version==5 && loaded.lighting.lights.size()==2 &&
|
||||
loaded.lighting.settings.flashlight==0 && loaded.lighting.rgb==app.bakedLightRGB,"v5 lighting round trip is lossless");
|
||||
app.lightSources[0].radius=0;
|
||||
Check(!app.SaveLevel("../assets/lit.dat") && Bytes("../assets/lit.dat")==valid,"invalid light cannot overwrite an existing level");
|
||||
app.lightSources[0].radius=900;
|
||||
app.lightSources[0].lightType=LIGHT_TYPE_BURST;
|
||||
Check(!app.SaveLevel("../assets/lit.dat") && Bytes("../assets/lit.dat")==valid,"internal burst slots cannot be serialized as authored lights");
|
||||
app.lightSources[0].lightType=LIGHT_TYPE_STATIC;
|
||||
auto bad=valid;bad[34]=14;
|
||||
{std::ofstream out("../assets/lit_badcount.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
|
||||
bad=valid;uint32_t invalidRadius=0; // settings start after header and grid; no sprites, transitions or script
|
||||
memcpy(bad.data()+38+1024+32+8,&invalidRadius,4);
|
||||
{std::ofstream out("../assets/lit_badradius.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
|
||||
bad=valid;bad.back()=255;
|
||||
{std::ofstream out("../assets/lit_badrgb.dat",std::ios::binary);out.write(reinterpret_cast<const char*>(bad.data()),bad.size());}
|
||||
Check(!app.LoadLevel("../assets/lit_badcount.dat") && app.lightSources.size()==2,"malformed lighting count preserves the editor scene");
|
||||
Check(!app.LoadLevel("../assets/lit_badradius.dat"),"malformed light radius rejected before scene replacement");
|
||||
Check(!app.LoadLevel("../assets/lit_badrgb.dat"),"invalid baked RGB padding rejected before scene replacement");
|
||||
std::string start,engine;
|
||||
Check(app.PreparePlaytest(start,engine),"F5 packages v5 lighting with the current unsaved map");app.CleanupPlaytest();
|
||||
app.LoadLevel("../assets/map.dat");
|
||||
Check(app.lightSources.empty() && app.lightingMode==0,"legacy maps clear old lights and retain classic rendering");
|
||||
}
|
||||
103
tests/lighting_engine_checks.inc
Normal file
103
tests/lighting_engine_checks.inc
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
.data
|
||||
lightFile db 'lit.dat',0
|
||||
lightBadCount db 'lit_badcount.dat',0
|
||||
lightBadRadius db 'lit_badradius.dat',0
|
||||
lightBadRGB db 'lit_badrgb.dat',0
|
||||
lightBakeDump db 'lighting_bake.bin',0
|
||||
lightFrameDump db 'lighting_frame.bin',0
|
||||
lightSavedRed dd 0
|
||||
lightSavedBlue dd 0
|
||||
lightSavedPulse dd 0
|
||||
.code
|
||||
RunLightingChecks PROC uses ebx ecx edx esi edi
|
||||
invoke LoadSublevelBinary,OFFSET lightFile,-1,-1,0
|
||||
CHECK eax,1 ; v5 authored lighting loads in ASM
|
||||
CHECK authoredLightCount,2 ; all authored lights reach runtime slots
|
||||
CHECK lightingMode,2 ; map lighting mode loads without a startup override
|
||||
CHECK isFlashlightOn,0 ; saved flashlight toggle reaches the game
|
||||
CHECK DWORD PTR [lightSources+40+LightSource.color],00FF0000h ; static light color retained on load
|
||||
call Render3DScene
|
||||
invoke Dump,OFFSET lightFrameDump,OFFSET screenBuffer,FRAME_BUFFER_SIZE
|
||||
call BakeMapLighting
|
||||
invoke Dump,OFFSET lightBakeDump,OFFSET lightGridRGB,4096
|
||||
CHECK DWORD PTR [lightGridRGB+(5*32+14)*4],000A0A0Ah ; static light cannot leak through a wall
|
||||
mov lightingMode,1
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
mov eax,lightRed
|
||||
mov lightSavedRed,eax
|
||||
mov eax,lightBlue
|
||||
mov lightSavedBlue,eax
|
||||
mov lightingMode,2
|
||||
call UpdateDynamicLights
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
CHECK lightRed,lightSavedRed ; dynamic mode does not count static light twice
|
||||
xor eax,eax
|
||||
mov ebx,lightBlue
|
||||
cmp ebx,lightSavedBlue
|
||||
seta al
|
||||
CHECK eax,1 ; realtime blue light contributes beyond the baked map
|
||||
mov DWORD PTR [lightSources+80+LightSource.x],928
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
CHECK lightBlue,10 ; dynamic source is occluded by a separating wall
|
||||
mov DWORD PTR [lightSources+80+LightSource.flags],LIGHT_FLAG_ACTIVE
|
||||
invoke SamplePointLighting,480,352,0,0
|
||||
xor eax,eax
|
||||
cmp lightBlue,10
|
||||
seta al
|
||||
CHECK eax,1 ; disabling dynamic shadows allows unoccluded contribution
|
||||
mov DWORD PTR [lightSources+80+LightSource.lightType],LIGHT_TYPE_PULSE
|
||||
mov lightingTick,0
|
||||
call UpdateDynamicLights
|
||||
mov eax,DWORD PTR [lightSources+80+LightSource.curIntensity]
|
||||
mov lightSavedPulse,eax
|
||||
mov lightingTick,31
|
||||
call UpdateDynamicLights
|
||||
mov eax,DWORD PTR [lightSources+80+LightSource.curIntensity]
|
||||
cmp eax,lightSavedPulse
|
||||
setne al
|
||||
movzx eax,al
|
||||
CHECK eax,1 ; pulse light changes intensity with the simulation clock
|
||||
mov DWORD PTR [lightSources+13*40+LightSource.intensity],77
|
||||
invoke SpawnMuzzleFlashLight,160,160
|
||||
invoke SpawnExplosionLight,160,160
|
||||
CHECK DWORD PTR [lightSources+13*40+LightSource.intensity],77 ; combat bursts cannot overwrite authored light slots
|
||||
call UpdateDynamicLights
|
||||
CHECK DWORD PTR [lightSources+14*40+LightSource.curIntensity],180 ; muzzle light decays in its reserved slot
|
||||
call InitLightingSystem
|
||||
mov lightingMode,2
|
||||
mov edi,OFFSET lightGridRGB
|
||||
mov ecx,1024
|
||||
mov eax,000A0A0Ah
|
||||
rep stosd
|
||||
mov playerX,160
|
||||
mov playerY,160
|
||||
mov playerAngle,0
|
||||
invoke SamplePointLighting,288,160,0,0
|
||||
xor eax,eax
|
||||
cmp lightRed,10
|
||||
seta al
|
||||
CHECK eax,1 ; flashlight illuminates points inside its forward cone
|
||||
invoke SamplePointLighting,160,288,0,0
|
||||
CHECK lightRed,10 ; flashlight cannot illuminate sprites outside its cone
|
||||
mov BYTE PTR [gameMap+2*32+3],CELL_DOOR
|
||||
invoke SamplePointLighting,288,160,0,0
|
||||
CHECK lightRed,10 ; a closed door blocks the player flashlight
|
||||
mov BYTE PTR [gameMap+2*32+3],CELL_DOOR_OPEN
|
||||
invoke CastLightRay,160,160,288,160
|
||||
CHECK eax,1 ; an open door transmits dynamic light
|
||||
mov BYTE PTR [gameMap+5*32+6],CELL_BRICK
|
||||
invoke CastLightRay,352,352,416,416
|
||||
CHECK eax,0 ; ASM light rays reject diagonal corner leaks
|
||||
invoke LoadSublevelBinary,OFFSET lightFile,-1,-1,0
|
||||
invoke LoadSublevelBinary,OFFSET lightBadCount,-1,-1,0
|
||||
CHECK eax,0 ; excessive lighting count rejected before committing a room
|
||||
CHECK authoredLightCount,2 ; malformed lighting preserves current authored lights
|
||||
invoke LoadSublevelBinary,OFFSET lightBadRadius,-1,-1,0
|
||||
CHECK eax,0 ; zero radius rejected without a divide by zero
|
||||
invoke LoadSublevelBinary,OFFSET lightBadRGB,-1,-1,0
|
||||
CHECK eax,0 ; malformed baked RGB cannot replace current lighting
|
||||
invoke LoadSublevelBinary,OFFSET mainPath,-1,-1,0
|
||||
CHECK authoredLightCount,0 ; loading a legacy room clears stale dynamic sources
|
||||
CHECK lightingMode,0 ; legacy room restores classic lighting mode
|
||||
ret
|
||||
RunLightingChecks ENDP
|
||||
471
tests/simulation_engine_checks.inc
Normal file
471
tests/simulation_engine_checks.inc
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
.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
|
||||
|
|
@ -4,6 +4,11 @@ static void RunStabilityEditorChecks() {
|
|||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_GUARD, "zero flags retain legacy guard fallback");
|
||||
sprite.flags = 0x06026400;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop matches ASM despite guard texture");
|
||||
sprite.textureId = 11;
|
||||
sprite.flags = 0;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_BARREL, "zero flags retain new legacy barrel fallback");
|
||||
sprite.flags = 0x06026400;
|
||||
Check(GetSpriteEntityType(sprite) == ENTITY_ARCHETYPE_PROP, "explicit Prop overrides barrel artwork");
|
||||
for (int health : {1, 100, 255, 256, 300, 500}) {
|
||||
SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_GUARD, health, 2, 6);
|
||||
Check(GetSpriteHealth(sprite) == (health > 255 ? 255 : health), "health setter clamps before byte packing");
|
||||
|
|
|
|||
|
|
@ -6,9 +6,20 @@ 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
|
||||
|
|
@ -198,5 +209,6 @@ RunStabilityChecks PROC
|
|||
mov [spriteList].SpriteStruct.flags, 0
|
||||
call InitEntities
|
||||
CHECK entityType, ENTITY_TYPE_GUARD ; zero flags keep legacy texture fallback
|
||||
call RunIntegrationChecks
|
||||
ret
|
||||
RunStabilityChecks ENDP
|
||||
|
|
|
|||
106
tests/studio_editor_checks.inl
Normal file
106
tests/studio_editor_checks.inl
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
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();
|
||||
}
|
||||
88
tests/studio_engine_checks.inc
Normal file
88
tests/studio_engine_checks.inc
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
.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
|
||||
|
|
@ -49,6 +49,13 @@ static void RunViewportChecks(EditorApp& app, const char* actualEngine) {
|
|||
Check(equal,"DX11 texture pixels match the ASM framebuffer with opaque alpha");
|
||||
context->Release();
|
||||
}
|
||||
app.LoadLevel("../assets/lit.dat");app.showCutsceneTimeline=false;app.lightingMode=1;
|
||||
Check(pump(),"saved static lighting arrives in the live viewport");
|
||||
auto bakedFrame=app.previewPixels;app.lightingMode=2;
|
||||
Check(pump() && app.previewPixels!=bakedFrame && app.previewPacket->lightCount==2,
|
||||
"authored dynamic lights visibly affect the embedded game view");
|
||||
auto dynamicFrame=app.previewPixels;app.lightSources[1].x+=256;
|
||||
Check(pump() && app.previewPixels!=dynamicFrame,"moving a dynamic light updates the image without rebaking");
|
||||
app.StopEnginePreview();
|
||||
Check(!app.previewProcess.hProcess && !app.previewPacket && !app.previewMapping,"closing viewport releases renderer process and mapping");
|
||||
app.ShutdownD3D();device->Release();app.showCutsceneTimeline=false;
|
||||
|
|
|
|||
|
|
@ -9,16 +9,18 @@ import subprocess
|
|||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SIZE, TEXTURES, PIXELS = 1357792, 2016, 1050592
|
||||
SIZE, TEXTURES, PIXELS = 1362408, 2016, 1050592
|
||||
|
||||
|
||||
def check_preview(engine, expected=None, output=None):
|
||||
def check_preview(engine, expected=None, output=None, level_path=None):
|
||||
name = f"Local\\EblanPreview-test-{os.getpid()}-{time.monotonic_ns()}"
|
||||
with mmap.mmap(-1, SIZE, tagname=name) as packet:
|
||||
base = C.addressof(C.c_char.from_buffer(packet))
|
||||
state = C.cast(base+8, C.POINTER(C.c_long))
|
||||
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 1, 0, os.getpid(), 160, 1024, 0,
|
||||
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 2, 0, os.getpid(), 160, 1024, 0,
|
||||
0x383838, 0x222222, 0, 0, 0, 1)
|
||||
struct.pack_into("<8I",packet,52,0,40,0,0,384,240,45,0)
|
||||
packet[1358312:1358312+4096]=struct.pack("<I",0x282828)*1024
|
||||
for y in range(32): packet[128+y*32+10] = 1
|
||||
# Flat red wall, other slots black; proves texture upload rather than fallback.
|
||||
packet[TEXTURES+65536:TEXTURES+2*65536] = struct.pack("<I",0xE02010)*16384
|
||||
|
|
@ -53,7 +55,7 @@ def check_preview(engine, expected=None, output=None):
|
|||
assert subtitle!=frame, "Subtitle overlay missing"
|
||||
# Compare the live IPC output with a direct Render3DScene regression dump.
|
||||
if expected:
|
||||
data=(ROOT/"assets/map.dat").read_bytes()
|
||||
data=Path(level_path or ROOT/"assets/map.dat").read_bytes()
|
||||
h=struct.unpack_from("<I7HII",data)
|
||||
version=h[1]; count=h[7]
|
||||
struct.pack_into("<3i",packet,16,h[4]*64+32,h[5]*64+32,h[6])
|
||||
|
|
@ -65,6 +67,16 @@ def check_preview(engine, expected=None, output=None):
|
|||
x,y=struct.unpack_from("<ii",sprites,i*24)
|
||||
struct.pack_into("<ii",sprites,i*24,x>>16,y>>16)
|
||||
packet[1152:1152+len(sprites)]=sprites
|
||||
if version>=5:
|
||||
transitions,events,strings=struct.unpack_from("<3H",data,26)
|
||||
lights=struct.unpack_from("<H",data,34)[0]
|
||||
offset=1062+count*24+transitions*42+events*16+strings*96
|
||||
mode,ambient,flash,radius,intensity,cone,_,_=struct.unpack_from("<8I",data,offset)
|
||||
struct.pack_into("<7I",packet,52,mode,ambient,flash,lights,radius,intensity,cone)
|
||||
offset+=32
|
||||
packet[1357792:1357792+lights*40]=data[offset:offset+lights*40]
|
||||
offset+=lights*40
|
||||
packet[1358312:1358312+4096]=data[offset:offset+4096]
|
||||
pack=(ROOT/"assets/textures/texturepack.bin").read_bytes()
|
||||
packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:]
|
||||
struct.pack_into("<I",packet,48,2)
|
||||
|
|
|
|||
|
|
@ -82,6 +82,16 @@ 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)
|
||||
|
|
@ -150,7 +160,7 @@ def main():
|
|||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + "".join(
|
||||
(ROOT / "tests" / name).read_text() for name in
|
||||
("cutscene_engine_checks.inc", "stability_engine_checks.inc"))
|
||||
("lighting_engine_checks.inc", "cutscene_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc", "simulation_engine_checks.inc", "studio_engine_checks.inc"))
|
||||
names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M)
|
||||
raw = (work / "engine_results.bin").read_bytes()
|
||||
values = struct.unpack(f"<{len(raw)//4}I", raw)
|
||||
|
|
@ -176,10 +186,14 @@ def main():
|
|||
if (work / "cutscene_trace.bin").read_bytes() != reference:
|
||||
raise RuntimeError("ASM and editor cinematic players disagree on frame-by-frame state")
|
||||
print("PASS editor and ASM cutscene state agrees on every tested frame")
|
||||
if (work / "lighting_bake.bin").read_bytes() != (work / "assets/lighting_reference.bin").read_bytes():
|
||||
raise RuntimeError("Editor and ASM static RGB bakes differ")
|
||||
print("PASS all 1024 baked RGB cells match between the editor and ASM")
|
||||
viewport_spec = importlib.util.spec_from_file_location("preview_ipc", ROOT / "tools/test_preview_ipc.py")
|
||||
viewport_tests = importlib.util.module_from_spec(viewport_spec)
|
||||
viewport_spec.loader.exec_module(viewport_tests)
|
||||
viewport_tests.check_preview(ROOT / "engine.exe",work / "project_frame.bgra",work / "live_viewport.bmp")
|
||||
viewport_tests.check_preview(ROOT / "engine.exe",work / "lighting_frame.bin",work / "lighting_preview.bmp",work / "assets/lit.dat")
|
||||
(work / "cutscene_preview.bmp").write_bytes(bitmap_header + (work / "cutscene_frame.bgra").read_bytes())
|
||||
print("PASS original project assets remain byte-for-byte unchanged")
|
||||
succeeded = True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue