feat: stabilize fixed-tick simulation
Decouple gameplay from presentation and preserve signed movement fractions. Separate cinematic camera ownership with safe body handoff and transactional current-room restarts. Resample input latches on focus regain, discard modal timing debt, and attach cinematic lighting to the view to match editor preview. Verify editor 137/137 and ASM 288/288, cinematic traces, RGB bake and IPC framebuffer parity without changing project assets.
This commit is contained in:
parent
527d92411d
commit
aa64510def
23 changed files with 1407 additions and 726 deletions
24
AGENTS.md
24
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)
|
||||
|
|
@ -196,7 +202,13 @@ 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,36 @@
|
|||
|
||||
---
|
||||
|
||||
### Симуляция и движение (этап 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 относятся к представлению.
|
||||
|
||||
## 2. Конвейер лучевого рендеринга (2.5D Raycasting)
|
||||
|
||||
Каждый кадр для экрана шириной 320 пикселей выпускается ровно **320 лучей** (по 1 лучу на каждый вертикальный столбец экрана):
|
||||
|
|
@ -113,14 +143,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 добавляется автоматически и переносится за последнее действие. Удаление портала удаляет или перенумеровывает ссылки сцены.
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ Project assets remain byte-for-byte unchanged. The original working directory
|
|||
still has only its pre-existing `editor/imgui.ini` modification. No separate
|
||||
interactive UI playtest was performed during this preparation.
|
||||
|
||||
### Stage 2 implementation contract (not implemented here)
|
||||
### 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
|
||||
|
|
@ -106,11 +106,70 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
|
@ -118,7 +177,6 @@ unchanged v1..v5 loading, lighting bake parity and IPC v2 framebuffer parity.
|
|||
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 and 2 do not claim that all audit findings are fixed: AI visibility/melee
|
||||
through walls, full undo and exporter safety remain assigned to subsequent
|
||||
stages. Existing map and texture assets are not migrated or rewritten.
|
||||
|
|
|
|||
Binary file not shown.
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
|
|
@ -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,6 +82,7 @@ keySceneHeld db 0
|
|||
keySkipHeld db 0
|
||||
keyAttack db 0
|
||||
keyAttackHeld db 0
|
||||
keySpaceAttackHeld db 0
|
||||
keyRestart db 0
|
||||
keyRestartHeld db 0
|
||||
keyWeapon1 db 0
|
||||
|
|
@ -78,6 +97,23 @@ 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
|
||||
|
|
|
|||
|
|
@ -108,9 +108,17 @@ HUD_STYLE_DEBUG equ 1 ; Debug Mode (FPS counter, Engine watermark, Min
|
|||
WEAPON_NONE equ -1 ; No weapon (Unarmed / Peaceful / Quest)
|
||||
WEAPON_KNIFE equ 0 ; Melee blade (no ammo, fast, 30 damage)
|
||||
WEAPON_PISTOL equ 1 ; Handgun (1 ammo, accurate, 35 damage)
|
||||
WEAPON_SHOTGUN equ 2 ; Pump Shotgun (2 ammo, 3-ray spread, 70 damage)
|
||||
WEAPON_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
|
||||
ENTITY_TYPE_GUARD equ 1 ; Enemy Guard AI (Patrol, Chase, Attack)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
ShowGameToast PROTO :DWORD, :DWORD, :DWORD
|
||||
Game_OnPlayerInteract PROTO :DWORD, :DWORD
|
||||
RestartCurrentRoom PROTO
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; InitEntities: Initialize entity runtime state from spriteList flags/defaults
|
||||
|
|
@ -140,13 +141,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:
|
||||
|
|
@ -156,6 +151,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
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ ShowGameToast ENDP
|
|||
RenderGameToast PROC uses ebx ecx edx esi edi
|
||||
cmp gameToastTimer, 0
|
||||
jle rt_done
|
||||
dec gameToastTimer
|
||||
|
||||
; 1. Calculate text character length
|
||||
mov esi, OFFSET gameToastText
|
||||
|
|
@ -131,6 +130,7 @@ no_text:
|
|||
|
||||
activate_dlg:
|
||||
mov gameDialogActive, 1
|
||||
mov clockReset, 1
|
||||
ret
|
||||
ShowGameDialog ENDP
|
||||
|
||||
|
|
@ -139,6 +139,7 @@ ShowGameDialog ENDP
|
|||
; ------------------------------------------------------------------------------
|
||||
CloseGameDialog PROC
|
||||
mov gameDialogActive, 0
|
||||
mov clockReset, 1
|
||||
ret
|
||||
CloseGameDialog ENDP
|
||||
|
||||
|
|
@ -165,18 +166,6 @@ draw_dlg_body:
|
|||
; Close hint in bottom-right
|
||||
invoke DrawString8x8, 140, 200, OFFSET szDialogCloseHint, COLOR_GRAY
|
||||
|
||||
; Check if player pressed Action key or Attack to dismiss
|
||||
cmp keyAction, 0
|
||||
jne do_dismiss
|
||||
cmp keyAttack, 0
|
||||
jne do_dismiss
|
||||
jmp rd_done
|
||||
|
||||
do_dismiss:
|
||||
mov keyAction, 0
|
||||
mov keyAttack, 0
|
||||
mov gameDialogActive, 0
|
||||
|
||||
rd_done:
|
||||
ret
|
||||
RenderGameDialog ENDP
|
||||
|
|
|
|||
|
|
@ -1,386 +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:
|
||||
|
||||
; F1 / H: Toggle Clean HUD vs Debug HUD
|
||||
invoke GetAsyncKeyState, VK_KEY_F1
|
||||
mov bx, ax
|
||||
invoke GetAsyncKeyState, VK_KEY_H
|
||||
or bx, ax
|
||||
test bx, 8000h
|
||||
jz hud_key_released
|
||||
cmp keyHudStyleHeld, 0
|
||||
jne hud_key_sampled
|
||||
xor hudStyle, 1
|
||||
mov keyHudStyleHeld, 1
|
||||
cmp hudStyle, HUD_STYLE_CLEAN
|
||||
je toast_hud_clean
|
||||
invoke ShowGameToast, OFFSET szHudDebugToast, 60, COLOR_CYAN
|
||||
jmp hud_key_sampled
|
||||
toast_hud_clean:
|
||||
invoke ShowGameToast, OFFSET szHudCleanToast, 60, COLOR_GREEN
|
||||
jmp hud_key_sampled
|
||||
hud_key_released:
|
||||
mov keyHudStyleHeld, 0
|
||||
hud_key_sampled:
|
||||
|
||||
; F: Toggle Flashlight
|
||||
invoke GetAsyncKeyState, VK_KEY_F
|
||||
test ax, 8000h
|
||||
jz flash_key_released
|
||||
cmp keyFlashlightHeld, 0
|
||||
jne flash_key_sampled
|
||||
xor isFlashlightOn, 1
|
||||
mov keyFlashlightHeld, 1
|
||||
cmp isFlashlightOn, 1
|
||||
je toast_flash_on
|
||||
invoke ShowGameToast, OFFSET szFlashlightOff, 60, 00808080h
|
||||
jmp flash_key_sampled
|
||||
toast_flash_on:
|
||||
invoke ShowGameToast, OFFSET szFlashlightOn, 60, COLOR_CYAN
|
||||
jmp flash_key_sampled
|
||||
flash_key_released:
|
||||
mov keyFlashlightHeld, 0
|
||||
flash_key_sampled:
|
||||
|
||||
; L: Cycle Lighting Mode (0: Off -> 1: Baked -> 2: Dynamic)
|
||||
invoke GetAsyncKeyState, VK_KEY_L
|
||||
test ax, 8000h
|
||||
jz light_key_released
|
||||
cmp keyLightModeHeld, 0
|
||||
jne light_key_sampled
|
||||
inc lightingMode
|
||||
cmp lightingMode, 3
|
||||
jl @F
|
||||
mov lightingMode, 0
|
||||
@@:
|
||||
mov keyLightModeHeld, 1
|
||||
cmp lightingMode, LIGHTING_MODE_NONE
|
||||
je toast_light_none
|
||||
cmp lightingMode, LIGHTING_MODE_BAKED
|
||||
je toast_light_baked
|
||||
invoke ShowGameToast, OFFSET szLightMode2, 60, COLOR_YELLOW
|
||||
jmp light_key_sampled
|
||||
toast_light_none:
|
||||
invoke ShowGameToast, OFFSET szLightMode0, 60, 00808080h
|
||||
jmp light_key_sampled
|
||||
toast_light_baked:
|
||||
invoke ShowGameToast, OFFSET szLightMode1, 60, COLOR_CYAN
|
||||
jmp light_key_sampled
|
||||
light_key_released:
|
||||
mov keyLightModeHeld, 0
|
||||
light_key_sampled:
|
||||
|
||||
; Weapon Selection Keys: 1, 2, 3 (only in Action mode with weapons enabled)
|
||||
cmp gameMode, GAME_MODE_ACTION
|
||||
jne skip_weapon_select
|
||||
cmp enableWeapons, 0
|
||||
je skip_weapon_select
|
||||
|
||||
invoke GetAsyncKeyState, VK_KEY_1
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
mov playerWeapon, WEAPON_KNIFE
|
||||
@@:
|
||||
invoke GetAsyncKeyState, VK_KEY_2
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
mov playerWeapon, WEAPON_PISTOL
|
||||
@@:
|
||||
invoke GetAsyncKeyState, VK_KEY_3
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
mov playerWeapon, WEAPON_SHOTGUN
|
||||
@@:
|
||||
skip_weapon_select:
|
||||
|
||||
; Escape / Pause Key
|
||||
invoke GetAsyncKeyState, VK_KEY_ESCAPE
|
||||
test ax, 8000h
|
||||
jz pause_key_released
|
||||
cmp keyPauseHeld, 0
|
||||
jne pause_key_sampled
|
||||
xor isGamePaused, 1
|
||||
mov keyPauseHeld, 1
|
||||
jmp pause_key_sampled
|
||||
pause_key_released:
|
||||
mov keyPauseHeld, 0
|
||||
pause_key_sampled:
|
||||
|
||||
; If paused, handle pause menu choices
|
||||
cmp isGamePaused, 0
|
||||
je not_in_pause_menu
|
||||
; While paused, mask normal movement & attack
|
||||
mov keyForward, 0
|
||||
mov keyBackward, 0
|
||||
mov keyStrafeLeft, 0
|
||||
mov keyStrafeRight, 0
|
||||
mov keyTurnLeft, 0
|
||||
mov keyTurnRight, 0
|
||||
mov keyAttack, 0
|
||||
mov keyAction, 0
|
||||
|
||||
; Check if any menu key is pressed
|
||||
invoke GetAsyncKeyState, VK_KEY_1
|
||||
mov bx, ax
|
||||
invoke GetAsyncKeyState, VK_KEY_2
|
||||
or bx, ax
|
||||
invoke GetAsyncKeyState, VK_KEY_3
|
||||
or bx, ax
|
||||
invoke GetAsyncKeyState, VK_KEY_4
|
||||
or bx, ax
|
||||
invoke GetAsyncKeyState, VK_KEY_5
|
||||
or bx, ax
|
||||
test bx, 8000h
|
||||
jz menu_keys_released
|
||||
|
||||
; If a key is pressed, check if already held
|
||||
cmp keyMenuHeld, 0
|
||||
jne pause_menu_done
|
||||
mov keyMenuHeld, 1
|
||||
|
||||
; Option 1: Resume
|
||||
invoke GetAsyncKeyState, VK_KEY_1
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
mov isGamePaused, 0
|
||||
jmp pause_menu_done
|
||||
@@:
|
||||
; Option 2: Toggle Weapons / Peaceful Mode
|
||||
invoke GetAsyncKeyState, VK_KEY_2
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
xor enableWeapons, 1
|
||||
cmp enableWeapons, 0
|
||||
je set_peaceful_mode
|
||||
mov gameMode, GAME_MODE_ACTION
|
||||
mov playerWeapon, WEAPON_PISTOL
|
||||
mov showCrosshair, 1
|
||||
invoke ShowGameToast, OFFSET szWeaponsEnabledToast, 80, COLOR_YELLOW
|
||||
jmp pause_menu_done
|
||||
set_peaceful_mode:
|
||||
mov gameMode, GAME_MODE_ADVENTURE
|
||||
mov playerWeapon, WEAPON_NONE
|
||||
mov showCrosshair, 0
|
||||
invoke ShowGameToast, OFFSET szWeaponsDisabledToast, 80, COLOR_GREEN
|
||||
jmp pause_menu_done
|
||||
@@:
|
||||
; Option 3: Toggle HUD (Clean vs Debug)
|
||||
invoke GetAsyncKeyState, VK_KEY_3
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
xor hudStyle, 1
|
||||
cmp hudStyle, HUD_STYLE_CLEAN
|
||||
je toast_pause_clean
|
||||
invoke ShowGameToast, OFFSET szHudDebugToast, 60, COLOR_CYAN
|
||||
jmp pause_menu_done
|
||||
toast_pause_clean:
|
||||
invoke ShowGameToast, OFFSET szHudCleanToast, 60, COLOR_GREEN
|
||||
jmp pause_menu_done
|
||||
@@:
|
||||
; Option 4: Restart level
|
||||
invoke GetAsyncKeyState, VK_KEY_4
|
||||
test ax, 8000h
|
||||
jz @F
|
||||
mov isGamePaused, 0
|
||||
mov keyRestart, 1
|
||||
jmp pause_menu_done
|
||||
@@:
|
||||
; Option 5: Quit to Windows
|
||||
invoke GetAsyncKeyState, VK_KEY_5
|
||||
test ax, 8000h
|
||||
jz pause_menu_done
|
||||
mov isRunning, 0
|
||||
jmp pause_menu_done
|
||||
|
||||
menu_keys_released:
|
||||
mov keyMenuHeld, 0
|
||||
|
||||
pause_menu_done:
|
||||
ret
|
||||
|
||||
not_in_pause_menu:
|
||||
cmp isCutsceneActive, 0
|
||||
je normal_keys_enabled
|
||||
mov inputFocused, 0
|
||||
call ConsumeInputEdges
|
||||
mov keyScreenshot, 0
|
||||
mov keyForward, 0
|
||||
mov keyBackward, 0
|
||||
mov keyStrafeLeft, 0
|
||||
|
|
@ -388,9 +72,55 @@ not_in_pause_menu:
|
|||
mov keyTurnLeft, 0
|
||||
mov keyTurnRight, 0
|
||||
mov keyRun, 0
|
||||
mov keyAction, 0
|
||||
mov keyAttack, 0
|
||||
normal_keys_enabled:
|
||||
|
||||
; 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
|
||||
|
|
|
|||
|
|
@ -584,6 +584,24 @@ 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
|
||||
|
|
@ -628,9 +646,9 @@ spl_next:
|
|||
cmp isFlashlightOn,0
|
||||
je spl_fog
|
||||
mov eax,x
|
||||
sub eax,playerX
|
||||
sub eax,originX
|
||||
mov ebx,y
|
||||
sub ebx,playerY
|
||||
sub ebx,originY
|
||||
invoke LightDistance,eax,ebx
|
||||
cmp eax,flashlightRadius
|
||||
jae spl_fog
|
||||
|
|
@ -641,27 +659,27 @@ spl_next:
|
|||
xor edx,edx
|
||||
div flashlightRadius
|
||||
mov atten,eax
|
||||
mov ecx,playerAngle
|
||||
mov ecx,originAngle
|
||||
and ecx,511
|
||||
mov eax,x
|
||||
sub eax,playerX
|
||||
sub eax,originX
|
||||
imul [tableCos+ecx*4]
|
||||
shrd eax,edx,16
|
||||
mov forward,eax
|
||||
mov eax,y
|
||||
sub eax,playerY
|
||||
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,playerY
|
||||
sub eax,originY
|
||||
imul [tableCos+ecx*4]
|
||||
shrd eax,edx,16
|
||||
mov lateral,eax
|
||||
mov eax,x
|
||||
sub eax,playerX
|
||||
sub eax,originX
|
||||
imul [tableSin+ecx*4]
|
||||
shrd eax,edx,16
|
||||
sub lateral,eax
|
||||
|
|
@ -685,7 +703,7 @@ spl_next:
|
|||
xor edx,edx
|
||||
div beam
|
||||
mov atten,eax
|
||||
invoke CastLightRay,playerX,playerY,x,y
|
||||
invoke CastLightRay,originX,originY,x,y
|
||||
test eax,eax
|
||||
jz spl_fog
|
||||
invoke AddLightColor,00FFFFFFh,atten
|
||||
|
|
@ -755,12 +773,12 @@ ComputeWallLightLevel PROC uses ebx ecx edx,screenX:DWORD,mapX:DWORD,mapY:DWORD,
|
|||
mov eax,rawDist
|
||||
imul rayDirX
|
||||
shrd eax,edx,24
|
||||
add eax,playerX
|
||||
add eax,viewX
|
||||
mov x,eax
|
||||
mov eax,rawDist
|
||||
imul rayDirY
|
||||
shrd eax,edx,24
|
||||
add eax,playerY
|
||||
add eax,viewY
|
||||
mov y,eax
|
||||
cmp sideHit,0
|
||||
jne cwl_y
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ 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
|
||||
|
|
@ -95,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
|
||||
|
|
@ -216,7 +223,28 @@ check_door_close:
|
|||
cmp eax, CELL_DOOR_OPEN
|
||||
jne check_portal_interact
|
||||
|
||||
; Close the door!
|
||||
; Never close a door over any part of the player's collision body.
|
||||
mov eax, playerX
|
||||
sub eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetX
|
||||
jg close_clear_door
|
||||
mov eax, playerX
|
||||
add eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetX
|
||||
jl close_clear_door
|
||||
mov eax, playerY
|
||||
sub eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetY
|
||||
jg close_clear_door
|
||||
mov eax, playerY
|
||||
add eax, playerRadius
|
||||
sar eax, CELL_SHIFT
|
||||
cmp eax, targetY
|
||||
jge no_door
|
||||
close_clear_door:
|
||||
invoke SetMapCell, targetX, targetY, CELL_DOOR
|
||||
invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW
|
||||
ret
|
||||
|
|
@ -410,7 +438,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
|
||||
|
|
@ -437,11 +465,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
|
||||
|
|
@ -449,31 +477,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
|
||||
|
|
@ -965,6 +997,20 @@ 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
|
||||
|
|
@ -1040,3 +1086,44 @@ startup_map_ready:
|
|||
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,11 +1,9 @@
|
|||
; ==============================================================================
|
||||
; 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
|
||||
|
|
@ -17,232 +15,204 @@ IsGameplayBlocked PROC
|
|||
ret
|
||||
IsGameplayBlocked ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdatePlayer: Updates rotation, computes movement vector & handles collisions
|
||||
; ------------------------------------------------------------------------------
|
||||
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
|
||||
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 forward:SDWORD, strafe:SDWORD, speed:DWORD
|
||||
LOCAL moveX:SDWORD, moveY:SDWORD, steps:DWORD
|
||||
mov gameplayTickBlocked, 1
|
||||
cmp isGamePaused, 0
|
||||
jne update_player_done
|
||||
jne player_done
|
||||
cmp gameDialogActive, 0
|
||||
jne update_player_done
|
||||
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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
; ------------------------------------------------------------------------------
|
||||
|
|
@ -41,7 +60,8 @@ Render3DScene PROC uses ebx ecx edx esi edi
|
|||
; --------------------------------------------------------------------------
|
||||
; 1. Fast Background Clear: Ceiling (Top Half) and Floor (Bottom Half)
|
||||
; --------------------------------------------------------------------------
|
||||
invoke SamplePointLighting,playerX,playerY,0,0
|
||||
call SelectRenderCamera
|
||||
invoke SamplePointLighting,viewX,viewY,0,0
|
||||
mov edi, OFFSET screenBuffer
|
||||
mov edx,mapCeilingColor
|
||||
call ShadeLightPixel
|
||||
|
|
@ -78,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
|
||||
|
|
@ -90,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
|
||||
|
||||
|
|
@ -145,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
|
||||
|
|
@ -159,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
|
||||
|
|
@ -176,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
|
||||
|
|
@ -189,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
|
||||
|
|
@ -297,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
|
||||
|
|
@ -306,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:
|
||||
|
|
@ -418,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
|
||||
|
|
@ -429,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
|
||||
|
||||
|
|
@ -629,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
|
||||
|
|
@ -650,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
|
||||
|
|
@ -739,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:
|
||||
|
|
|
|||
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
|
||||
54
main.asm
54
main.asm
|
|
@ -118,6 +118,7 @@ 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
|
||||
|
|
@ -207,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
|
||||
|
|
@ -275,27 +276,9 @@ hud_no_map_error:
|
|||
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
|
||||
cmp isGamePaused, 0
|
||||
jne @F
|
||||
cmp gameDialogActive, 0
|
||||
jne skip_movement_in_dialog
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
call UpdateDynamicLights
|
||||
skip_movement_in_dialog:
|
||||
call Game_OnTick
|
||||
@@:
|
||||
|
||||
; 2. 3D Raycasting & 2D HUD Render
|
||||
call Render3DScene
|
||||
call Engine_RenderHUD
|
||||
|
|
@ -339,16 +322,6 @@ skip_movement_in_dialog:
|
|||
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
|
||||
|
||||
|
|
@ -488,7 +461,6 @@ ParseStartupMap ENDP
|
|||
; ------------------------------------------------------------------------------
|
||||
main PROC
|
||||
LOCAL msg:MSG
|
||||
LOCAL curTick:DWORD, elapsed:DWORD
|
||||
|
||||
call LoadProjectSettings
|
||||
call ParseStartupMap
|
||||
|
|
@ -513,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.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,12 +23,13 @@ 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
|
||||
|
|
@ -131,6 +132,7 @@ 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
|
||||
|
||||
AuditMain PROC
|
||||
call InitMathTables
|
||||
|
|
@ -177,7 +179,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
|
||||
|
|
@ -260,6 +262,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
|
||||
|
|
@ -370,6 +373,7 @@ flat_depth_ok:
|
|||
call RunLightingChecks
|
||||
call RunCutsceneChecks
|
||||
call RunStabilityChecks
|
||||
call RunSimulationChecks
|
||||
mov eax, resultCount
|
||||
inc eax
|
||||
shl eax, 2
|
||||
|
|
|
|||
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
|
||||
|
|
@ -6,9 +6,15 @@ 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
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def main():
|
|||
run([str(work / "engine_tests.exe"), "pace map.dat"], work)
|
||||
source = (ROOT / "tests/engine_regressions.asm").read_text() + "".join(
|
||||
(ROOT / "tests" / name).read_text() for name in
|
||||
("cutscene_engine_checks.inc", "lighting_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc"))
|
||||
("lighting_engine_checks.inc", "cutscene_engine_checks.inc", "stability_engine_checks.inc", "integration_engine_checks.inc", "simulation_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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue