feat: complete Game Studio IDE, entity archetypes, combat system, and standalone exporter
This commit is contained in:
parent
a1d1baa869
commit
c1da810bd5
17 changed files with 1810 additions and 6 deletions
30
AGENTS.md
30
AGENTS.md
|
|
@ -207,3 +207,33 @@ The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-..
|
|||
The renderer validates camera and sprite bounds, uploads textures only when their revision changes, and exits if its parent process ends. The editor has process-exit/timeout checks and a retry button, converts DIB padding to opaque alpha, uploads BGRA to DX11 and displays with point sampling at 4:3. Map/inspector changes appear without saving. Timeline playback/scrubbing drives the viewport camera, subtitles and fade. Free-camera inspection affects no authored data. The viewport shows the scene and cinematic overlays; normal game HUD, door interactions and room switching are tested in F5.
|
||||
|
||||
`tools/test_preview_ipc.py` exercises the actual renderer without a visible window. The full regression suite also checks the x86-to-x64-to-DX11 path on a WARP device and compares the live viewport's pixels with a direct ASM rendering. Build engine.exe before running these tests.
|
||||
|
||||
### J. Game Studio IDE, Entity Archetypes & Combat Subsystems
|
||||
EblanStudio has evolved from a pure level editor into a **Full Game Engine Studio / IDE for retro x86 Assembly (MASM32) games**:
|
||||
|
||||
1. **Entity Archetypes & AI (`include/engine_entity.inc`)**:
|
||||
- `0: Prop`: static decoration / obstacles.
|
||||
- `1: Guard AI`: patrolling enemy guard that detects the player within an aggro radius, chases, and deals melee damage.
|
||||
- `2: Medkit`: collectible healing item (+25 HP up to 100).
|
||||
- `3: Ammo Box`: collectible ammunition pickup (+20 rounds up to 100).
|
||||
- `4: Key`: key card for unlocking locked doors.
|
||||
- `5: Switch`: interactive trigger/lever toggled by proximity action ([E]).
|
||||
- Packed into `SpriteStruct.flags` without altering binary map compatibility (`sizeof(SpriteEntry) == 24`).
|
||||
|
||||
2. **Player Combat & HUD (`include/engine_combat.inc`)**:
|
||||
- Weapon attack via `Space`, `Ctrl`, or Left Mouse Button (`VK_LBUTTON`).
|
||||
- Center crosshair and hitscan raycasting against active enemy guards.
|
||||
- Animated retro pistol with muzzle flash, recoil, and cooldowns.
|
||||
- Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`.
|
||||
- Comprehensive combat HUD: Health bar, Ammo counter, Score counter, and Key indicator.
|
||||
|
||||
3. **In-Editor MASM32 IDE (`F7` / "Код / IDE")**:
|
||||
- Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7` or the main menu.
|
||||
- MASM syntax highlighting (instructions, registers, directives, comments, strings, numbers).
|
||||
- Project file navigator (`engine_entity.inc`, `engine_combat.inc`, `main.asm`, etc.).
|
||||
- Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Custom HUD).
|
||||
- Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines.
|
||||
|
||||
4. **Project Settings & Standalone Game Exporter (`editor/src/project_manager.h`)**:
|
||||
- Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map.
|
||||
- "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
|||
#include "editor_app.h"
|
||||
#include "project_manager.h"
|
||||
#include "level_io.h"
|
||||
#include "../imgui/imgui.h"
|
||||
#include "../imgui/imgui_internal.h"
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
#include "stb_image_write.h"
|
||||
#include <d3d11.h>
|
||||
#include <mmsystem.h>
|
||||
#include <shellapi.h>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <cmath>
|
||||
|
|
@ -72,6 +74,7 @@ static const char* g_TexSlotNames[16] = {
|
|||
};
|
||||
|
||||
EditorApp::EditorApp() {
|
||||
project = std::make_unique<EblanProject>();
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
const auto directory = fs::path(module).parent_path();
|
||||
|
|
@ -1310,6 +1313,8 @@ void EditorApp::HandleHotkeys() {
|
|||
ImGuiIO& io = ImGui::GetIO();
|
||||
// Function keys remain available while ImGui owns navigation/text focus.
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F6, false)) showCutsceneTimeline = !showCutsceneTimeline;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F7, false)) showCodeEditorModal = !showCodeEditorModal;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F4, false)) showAssetManagerModal = !showAssetManagerModal;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) {
|
||||
if (!isEngineRunning) LaunchPlaytest();
|
||||
else StopPlaytest();
|
||||
|
|
@ -1357,6 +1362,9 @@ void EditorApp::RenderUI() {
|
|||
RenderMenuBar();
|
||||
RenderToolbar();
|
||||
RenderAssetManagerModal();
|
||||
RenderCodeEditorWindow();
|
||||
RenderProjectSettingsModal();
|
||||
RenderExportGameModal();
|
||||
if (showPlaytestError) { ImGui::OpenPopup("Не удалось запустить игру"); showPlaytestError = false; }
|
||||
if (ImGui::BeginPopupModal("Не удалось запустить игру", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX()+520);
|
||||
|
|
@ -1455,6 +1463,13 @@ void EditorApp::RenderMenuBar() {
|
|||
if (!p.empty()) SaveLevel(p);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Настройки проекта...", nullptr, showProjectSettingsModal)) {
|
||||
showProjectSettingsModal = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Экспорт автономной игры (.exe)...", nullptr, showExportGameModal)) {
|
||||
showExportGameModal = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Выход", "Alt+F4")) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
|
|
@ -1467,6 +1482,13 @@ void EditorApp::RenderMenuBar() {
|
|||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Код / IDE")) {
|
||||
if (ImGui::MenuItem("Редактор скриптов MASM32...", "F7", showCodeEditorModal)) {
|
||||
showCodeEditorModal = !showCodeEditorModal;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Инструменты")) {
|
||||
if (ImGui::MenuItem("Кисть (Стена)", "B", currentTool == TOOL_BRUSH)) currentTool = TOOL_BRUSH;
|
||||
if (ImGui::MenuItem("Ластик (Очистить)", "E", currentTool == TOOL_ERASER)) currentTool = TOOL_ERASER;
|
||||
|
|
@ -1519,10 +1541,20 @@ void EditorApp::RenderMenuBar() {
|
|||
}
|
||||
|
||||
if (ImGui::BeginMenu("Движок")) {
|
||||
if (ImGui::MenuItem("Запустить (Playtest)", "F5", isEngineRunning)) {
|
||||
if (ImGui::MenuItem("Запустить игру (Playtest)", "F5", isEngineRunning)) {
|
||||
if (!isEngineRunning) LaunchPlaytest();
|
||||
else StopPlaytest();
|
||||
}
|
||||
if (ImGui::MenuItem("Редактор скриптов MASM32", "F7", showCodeEditorModal)) {
|
||||
showCodeEditorModal = !showCodeEditorModal;
|
||||
}
|
||||
if (ImGui::MenuItem("Кат-сцены", "F6", showCutsceneTimeline)) {
|
||||
showCutsceneTimeline = !showCutsceneTimeline;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Экспорт игры (.exe)...", nullptr, showExportGameModal)) {
|
||||
showExportGameModal = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
|
|
@ -1594,6 +1626,18 @@ void EditorApp::RenderToolbar() {
|
|||
showAssetManagerModal = !showAssetManagerModal;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
// Code Editor button
|
||||
if (ImGui::Button(" 💻 Код (F7) ")) {
|
||||
showCodeEditorModal = !showCodeEditorModal;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
// Standalone Exporter button
|
||||
if (ImGui::Button(" 📦 Экспорт (.exe) ")) {
|
||||
showExportGameModal = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
|
||||
ImGui::SameLine();
|
||||
|
||||
|
|
@ -1699,13 +1743,21 @@ void EditorApp::RenderGridCanvas() {
|
|||
hoveredSprite = static_cast<int>(i);
|
||||
}
|
||||
|
||||
// Gold Diamond (highlight if selected or hovered)
|
||||
ImU32 fillCol = IM_COL32(255, 215, 0, 240);
|
||||
// Color diamond based on Entity Archetype
|
||||
uint8_t aType = GetSpriteEntityType(sprites[i]);
|
||||
ImU32 fillCol = IM_COL32(255, 215, 0, 240); // Prop (Gold)
|
||||
if (aType == ENTITY_ARCHETYPE_GUARD) fillCol = IM_COL32(235, 45, 45, 240); // Red (Enemy Guard)
|
||||
else if (aType == ENTITY_ARCHETYPE_MEDKIT) fillCol = IM_COL32(40, 220, 80, 240); // Green (Medkit)
|
||||
else if (aType == ENTITY_ARCHETYPE_AMMO) fillCol = IM_COL32(245, 190, 25, 240); // Yellow (Ammo)
|
||||
else if (aType == ENTITY_ARCHETYPE_KEY) fillCol = IM_COL32(30, 190, 255, 240); // Cyan (Key)
|
||||
else if (aType == ENTITY_ARCHETYPE_SWITCH) fillCol = IM_COL32(255, 130, 20, 240); // Orange (Switch)
|
||||
|
||||
ImU32 borderCol = IM_COL32(255, 255, 255, 255);
|
||||
if (static_cast<int>(i) == selectedSpriteIdx) {
|
||||
fillCol = IM_COL32(255, 100, 100, 240);
|
||||
borderCol = IM_COL32(255, 255, 0, 255);
|
||||
fillCol = IM_COL32(255, 255, 255, 240);
|
||||
} else if (static_cast<int>(i) == hoveredSprite) {
|
||||
fillCol = IM_COL32(255, 240, 120, 240);
|
||||
borderCol = IM_COL32(255, 255, 120, 255);
|
||||
}
|
||||
|
||||
drawList->AddQuadFilled(
|
||||
|
|
@ -1908,8 +1960,16 @@ void EditorApp::RenderHierarchy() {
|
|||
for (size_t i = 0; i < sprites.size(); ++i) {
|
||||
int gx = sprites[i].x / 64;
|
||||
int gy = sprites[i].y / 64;
|
||||
uint8_t aType = GetSpriteEntityType(sprites[i]);
|
||||
const char* typeTag = "Проп";
|
||||
if (aType == ENTITY_ARCHETYPE_GUARD) typeTag = "Враг";
|
||||
else if (aType == ENTITY_ARCHETYPE_MEDKIT) typeTag = "+HP";
|
||||
else if (aType == ENTITY_ARCHETYPE_AMMO) typeTag = "+Ammo";
|
||||
else if (aType == ENTITY_ARCHETYPE_KEY) typeTag = "Ключ";
|
||||
else if (aType == ENTITY_ARCHETYPE_SWITCH) typeTag = "Рычаг";
|
||||
|
||||
char label[64];
|
||||
snprintf(label, sizeof(label), "Спрайт #%zu (%d, %d)", i + 1, gx, gy);
|
||||
snprintf(label, sizeof(label), "[%s] #%zu (%d, %d)", typeTag, i + 1, gx, gy);
|
||||
|
||||
if (ImGui::Selectable(label, selectedSpriteIdx == static_cast<int>(i))) {
|
||||
selectedSpriteIdx = static_cast<int>(i);
|
||||
|
|
@ -2024,6 +2084,52 @@ void EditorApp::RenderInspector() {
|
|||
spr.scale = static_cast<int32_t>(scaleF * 65536.0f);
|
||||
}
|
||||
|
||||
// Entity Archetype & Logic
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Геймплей / Архетип сущности:");
|
||||
static const char* s_ArchetypeNames[6] = {
|
||||
"0: Декорация / Проп (Static)",
|
||||
"1: Враг (Enemy Guard AI)",
|
||||
"2: Аптечка (+25 HP)",
|
||||
"3: Патроны (+20 Ammo)",
|
||||
"4: Ключ от двери (Key Pickup)",
|
||||
"5: Переключатель / Рычаг (Switch)"
|
||||
};
|
||||
int archType = static_cast<int>(GetSpriteEntityType(spr));
|
||||
int hp = static_cast<int>(GetSpriteHealth(spr));
|
||||
int spd = static_cast<int>(GetSpriteSpeed(spr));
|
||||
int agg = static_cast<int>(GetSpriteAggro(spr));
|
||||
|
||||
if (ImGui::Combo("Тип сущности", &archType, s_ArchetypeNames, 6)) {
|
||||
PushUndo();
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
|
||||
if (archType == ENTITY_ARCHETYPE_GUARD) {
|
||||
if (ImGui::SliderInt("Здоровье (HP)", &hp, 10, 500)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
if (ImGui::SliderInt("Скорость", &spd, 1, 8)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
if (ImGui::SliderInt("Радиус агрессии (ячейки)", &agg, 1, 15)) {
|
||||
SetSpriteEntityData(spr, static_cast<uint8_t>(archType), static_cast<uint8_t>(hp), static_cast<uint8_t>(spd), static_cast<uint8_t>(agg));
|
||||
}
|
||||
} else if (archType == ENTITY_ARCHETYPE_MEDKIT) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Восстанавливает +25 HP игроку при подборе");
|
||||
} else if (archType == ENTITY_ARCHETYPE_AMMO) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.2f, 1.0f), "Пополняет +20 патронов игроку при подборе");
|
||||
} else if (archType == ENTITY_ARCHETYPE_KEY) {
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Ключ для открытия запертых дверей");
|
||||
} else if (archType == ENTITY_ARCHETYPE_SWITCH) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.2f, 1.0f), "Активируется нажатием клавиши [E] рядом с объектом");
|
||||
}
|
||||
|
||||
if (ImGui::Button("Редактировать логику в Редакторе кода (F7)")) {
|
||||
showCodeEditorModal = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Удалить спрайт")) {
|
||||
PushUndo();
|
||||
sprites.erase(sprites.begin() + selectedSpriteIdx);
|
||||
|
|
@ -2101,3 +2207,125 @@ void EditorApp::RenderInspector() {
|
|||
|
||||
#include "editor_cutscene.inl"
|
||||
#include "editor_viewport.inl"
|
||||
#include "editor_code.inl"
|
||||
|
||||
void EditorApp::RenderProjectSettingsModal() {
|
||||
if (!showProjectSettingsModal) return;
|
||||
|
||||
if (!ImGui::IsPopupOpen("Настройки проекта")) {
|
||||
ImGui::OpenPopup("Настройки проекта");
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(520, 380), ImGuiCond_FirstUseEver);
|
||||
if (ImGui::BeginPopupModal("Настройки проекта", &showProjectSettingsModal, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
static char titleBuf[128] = "";
|
||||
static char authorBuf[128] = "";
|
||||
static char versionBuf[32] = "";
|
||||
static char windowTitleBuf[128] = "";
|
||||
static bool init = true;
|
||||
if (init) {
|
||||
strncpy_s(titleBuf, project->title.c_str(), sizeof(titleBuf) - 1);
|
||||
strncpy_s(authorBuf, project->author.c_str(), sizeof(authorBuf) - 1);
|
||||
strncpy_s(versionBuf, project->version.c_str(), sizeof(versionBuf) - 1);
|
||||
strncpy_s(windowTitleBuf, project->windowTitle.c_str(), sizeof(windowTitleBuf) - 1);
|
||||
init = false;
|
||||
}
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Параметры игры (.eblanproj):");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::InputText("Название игры", titleBuf, sizeof(titleBuf));
|
||||
ImGui::InputText("Автор", authorBuf, sizeof(authorBuf));
|
||||
ImGui::InputText("Версия", versionBuf, sizeof(versionBuf));
|
||||
ImGui::InputText("Заголовок окна", windowTitleBuf, sizeof(windowTitleBuf));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Стартовая карта: %s", project->startupMap.c_str());
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Сохранить настройки")) {
|
||||
project->title = titleBuf;
|
||||
project->author = authorBuf;
|
||||
project->version = versionBuf;
|
||||
project->windowTitle = windowTitleBuf;
|
||||
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string projPath = (fs::path(module).parent_path() / "project.eblanproj").string();
|
||||
project->Save(projPath);
|
||||
statusMessage = "Настройки проекта сохранены в project.eblanproj";
|
||||
init = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
showProjectSettingsModal = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Отмена")) {
|
||||
init = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
showProjectSettingsModal = false;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::RenderExportGameModal() {
|
||||
if (!showExportGameModal) return;
|
||||
|
||||
if (!ImGui::IsPopupOpen("Экспорт автономной игры (.exe)")) {
|
||||
ImGui::OpenPopup("Экспорт автономной игры (.exe)");
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 360), ImGuiCond_FirstUseEver);
|
||||
if (ImGui::BeginPopupModal("Экспорт автономной игры (.exe)", &showExportGameModal, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
static char destPathBuf[MAX_PATH] = "dist\\MyGame";
|
||||
static bool exported = false;
|
||||
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Сборка релизного дистрибутива игры");
|
||||
ImGui::TextWrapped("Экспортер упакует автономный исполняемый файл игры (.exe), все карты, текстуры и звуки в готовую для публикации папку.");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Название игры: %s", project->title.c_str());
|
||||
ImGui::Text("Версия: %s | Автор: %s", project->version.c_str(), project->author.c_str());
|
||||
ImGui::InputText("Папка экспорта", destPathBuf, sizeof(destPathBuf));
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Экспортировать игру!")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
std::string fullDest = (fs::path(root) / destPathBuf).string();
|
||||
|
||||
if (StandaloneExporter::ExportGame(*project, fullDest, root, exportStatusMessage)) {
|
||||
exported = true;
|
||||
statusMessage = "Игра успешно экспортирована в: " + fullDest;
|
||||
} else {
|
||||
exported = false;
|
||||
statusMessage = "Ошибка экспорта: " + exportStatusMessage;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Закрыть")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
showExportGameModal = false;
|
||||
}
|
||||
|
||||
if (!exportStatusMessage.empty()) {
|
||||
ImGui::Separator();
|
||||
if (exported) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.3f, 1.0f), "%s", exportStatusMessage.c_str());
|
||||
if (ImGui::Button("Открыть папку с игрой в Проводнике")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
std::string fullDest = (fs::path(root) / destPathBuf).string();
|
||||
ShellExecuteA(nullptr, "open", fullDest.c_str(), nullptr, nullptr, SW_SHOW);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", exportStatusMessage.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@
|
|||
#include "level_format.h"
|
||||
#include "cutscene_preview.h"
|
||||
#include "preview_protocol.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <windows.h>
|
||||
|
||||
struct EblanProject;
|
||||
|
||||
enum ToolMode {
|
||||
TOOL_BRUSH,
|
||||
TOOL_ERASER,
|
||||
|
|
@ -64,8 +67,18 @@ public:
|
|||
void StopAudioSound();
|
||||
|
||||
void RenderAssetManagerModal();
|
||||
void RenderCodeEditorWindow();
|
||||
void RenderProjectSettingsModal();
|
||||
void RenderExportGameModal();
|
||||
|
||||
bool showAssetManagerModal = false;
|
||||
bool showCutsceneTimeline = false;
|
||||
bool showCodeEditorModal = false;
|
||||
bool showProjectSettingsModal = false;
|
||||
bool showExportGameModal = false;
|
||||
|
||||
std::unique_ptr<EblanProject> project;
|
||||
std::string exportStatusMessage;
|
||||
|
||||
void RenderUI();
|
||||
|
||||
|
|
|
|||
530
editor/src/editor_code.inl
Normal file
530
editor/src/editor_code.inl
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
// ==============================================================================
|
||||
// EblanStudio - Integrated MASM32 Code Editor & Gameplay Script IDE
|
||||
// ==============================================================================
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <cctype>
|
||||
#include <windows.h>
|
||||
#include "../imgui/imgui.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct CodeFileEntry {
|
||||
std::string displayName;
|
||||
std::string relativePath;
|
||||
};
|
||||
|
||||
static const CodeFileEntry g_ProjectCodeFiles[] = {
|
||||
{ "engine_entity.inc (Entity AI & Pickups)", "include/engine_entity.inc" },
|
||||
{ "engine_combat.inc (Player Weapons & Combat)", "include/engine_combat.inc" },
|
||||
{ "main.asm (Engine Entry & Main Loop)", "main.asm" },
|
||||
{ "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" },
|
||||
{ "engine_defs.inc (Constants & Structs)", "include/engine_defs.inc" },
|
||||
{ "engine_render2d.inc (2D Canvas & HUD)", "include/engine_render2d.inc" },
|
||||
{ "engine_input.inc (Keyboard Input)", "include/engine_input.inc" },
|
||||
{ "engine_physics.inc (Player Physics)", "include/engine_physics.inc" }
|
||||
};
|
||||
static const int g_CodeFileCount = sizeof(g_ProjectCodeFiles) / sizeof(g_ProjectCodeFiles[0]);
|
||||
|
||||
struct ScriptTemplate {
|
||||
const char* title;
|
||||
const char* code;
|
||||
};
|
||||
|
||||
static const ScriptTemplate g_ScriptTemplates[] = {
|
||||
{
|
||||
"Патрулирующий охранник (Patrol Guard AI)",
|
||||
"; ==============================================================================\n"
|
||||
"; Custom Patrol Guard: Patrols between coordinates, chases player on sight\n"
|
||||
"; ==============================================================================\n"
|
||||
"CustomGuardPatrol PROC uses ebx ecx\n"
|
||||
" ; Advance patrol waypoint timer\n"
|
||||
" mov ecx, [entityTimer + edx*4]\n"
|
||||
" inc ecx\n"
|
||||
" and ecx, 255\n"
|
||||
" mov [entityTimer + edx*4], ecx\n"
|
||||
" ret\n"
|
||||
"CustomGuardPatrol ENDP\n"
|
||||
},
|
||||
{
|
||||
"Супер-аптечка (+50 HP Boost)",
|
||||
"; ==============================================================================\n"
|
||||
"; Mega-Medkit Pickup: Grants +50 HP and score bonus\n"
|
||||
"; ==============================================================================\n"
|
||||
" mov eax, playerHealth\n"
|
||||
" add eax, 50\n"
|
||||
" cmp eax, playerMaxHealth\n"
|
||||
" jle @F\n"
|
||||
" mov eax, playerMaxHealth\n"
|
||||
"@@:\n"
|
||||
" mov playerHealth, eax\n"
|
||||
" add playerScore, 150\n"
|
||||
},
|
||||
{
|
||||
"Интерактивный переключатель двери (Door Switch)",
|
||||
"; ==============================================================================\n"
|
||||
"; Interactive Switch: Toggles nearest door when activated with [E]\n"
|
||||
"; ==============================================================================\n"
|
||||
" invoke GetMapCell, switchTargetX, switchTargetY\n"
|
||||
" cmp eax, CELL_DOOR\n"
|
||||
" jne check_door_close\n"
|
||||
" invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR_OPEN\n"
|
||||
" jmp switch_done\n"
|
||||
"check_door_close:\n"
|
||||
" cmp eax, CELL_DOOR_OPEN\n"
|
||||
" jne switch_done\n"
|
||||
" invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR\n"
|
||||
"switch_done:\n"
|
||||
},
|
||||
{
|
||||
"Кастомный элемент HUD (Custom Gauge Widget)",
|
||||
"; ==============================================================================\n"
|
||||
"; Custom HUD Gauge Widget\n"
|
||||
"; ==============================================================================\n"
|
||||
" invoke DrawRect, 10, 60, 80, 10, 00222222h\n"
|
||||
" invoke DrawRectOutline, 10, 60, 80, 10, COLOR_WHITE\n"
|
||||
" invoke DrawRect, 11, 61, 50, 8, 000088FFh ; Cyan gauge fill\n"
|
||||
}
|
||||
};
|
||||
static const int g_ScriptTemplateCount = sizeof(g_ScriptTemplates) / sizeof(g_ScriptTemplates[0]);
|
||||
|
||||
struct CodeEditorState {
|
||||
int currentFile = 0;
|
||||
std::string fileContent;
|
||||
std::vector<char> textBuffer;
|
||||
bool isModified = false;
|
||||
std::string statusText = "Готов к редактированию кода MASM32";
|
||||
|
||||
// Compiler output & process
|
||||
bool isBuilding = false;
|
||||
std::string buildLog;
|
||||
HANDLE hBuildProcess = nullptr;
|
||||
HANDLE hReadPipe = nullptr;
|
||||
int buildExitCode = 0;
|
||||
bool hasBuildResult = false;
|
||||
|
||||
struct ParsedError {
|
||||
std::string file;
|
||||
int line = 0;
|
||||
std::string message;
|
||||
};
|
||||
std::vector<ParsedError> buildErrors;
|
||||
|
||||
void EnsureBufferSize(size_t required) {
|
||||
if (textBuffer.size() < required + 16384) {
|
||||
textBuffer.resize(required + 65536, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void LoadFile(int fileIndex, const std::string& projectRoot) {
|
||||
if (fileIndex < 0 || fileIndex >= g_CodeFileCount) return;
|
||||
currentFile = fileIndex;
|
||||
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[fileIndex].relativePath;
|
||||
if (!fs::exists(fullPath)) {
|
||||
fullPath = fs::path(projectRoot).parent_path() / g_ProjectCodeFiles[fileIndex].relativePath;
|
||||
}
|
||||
|
||||
HANDLE hFile = CreateFileA(fullPath.string().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
DWORD fileSize = GetFileSize(hFile, nullptr);
|
||||
if (fileSize != INVALID_FILE_SIZE) {
|
||||
EnsureBufferSize(fileSize);
|
||||
DWORD read = 0;
|
||||
ReadFile(hFile, textBuffer.data(), fileSize, &read, nullptr);
|
||||
textBuffer[read] = '\0';
|
||||
fileContent = std::string(textBuffer.data(), read);
|
||||
isModified = false;
|
||||
statusText = "Открыт файл: " + g_ProjectCodeFiles[fileIndex].displayName;
|
||||
}
|
||||
CloseHandle(hFile);
|
||||
} else {
|
||||
fileContent.clear();
|
||||
EnsureBufferSize(1024);
|
||||
textBuffer[0] = '\0';
|
||||
isModified = false;
|
||||
statusText = "Не удалось открыть файл: " + fullPath.string();
|
||||
}
|
||||
}
|
||||
|
||||
bool SaveFile(const std::string& projectRoot) {
|
||||
if (currentFile < 0 || currentFile >= g_CodeFileCount) return false;
|
||||
fs::path fullPath = fs::path(projectRoot) / g_ProjectCodeFiles[currentFile].relativePath;
|
||||
if (!fs::exists(fullPath)) {
|
||||
fullPath = fs::path(projectRoot).parent_path() / g_ProjectCodeFiles[currentFile].relativePath;
|
||||
}
|
||||
|
||||
HANDLE hFile = CreateFileA(fullPath.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
statusText = "Ошибка сохранения: доступ запрещен (" + fullPath.string() + ")";
|
||||
return false;
|
||||
}
|
||||
DWORD len = static_cast<DWORD>(std::strlen(textBuffer.data()));
|
||||
DWORD written = 0;
|
||||
WriteFile(hFile, textBuffer.data(), len, &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
isModified = false;
|
||||
statusText = "Файл сохранен: " + g_ProjectCodeFiles[currentFile].displayName;
|
||||
return true;
|
||||
}
|
||||
|
||||
void StartBuild(const std::string& projectRoot) {
|
||||
if (isBuilding) return;
|
||||
|
||||
// Auto-save current file before building
|
||||
SaveFile(projectRoot);
|
||||
|
||||
buildLog.clear();
|
||||
buildErrors.clear();
|
||||
hasBuildResult = false;
|
||||
|
||||
fs::path root(projectRoot);
|
||||
fs::path buildBat = root / "build.bat";
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildBat = root.parent_path() / "build.bat";
|
||||
root = root.parent_path();
|
||||
}
|
||||
|
||||
if (!fs::exists(buildBat)) {
|
||||
buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
SECURITY_ATTRIBUTES sa{};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
|
||||
HANDLE hWritePipe = nullptr;
|
||||
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
|
||||
buildLog = "[ERROR] Не удалось создать пайп вывода компилятора.\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
return;
|
||||
}
|
||||
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
STARTUPINFOA si{};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
si.hStdOutput = hWritePipe;
|
||||
si.hStdError = hWritePipe;
|
||||
si.wShowWindow = SW_HIDE;
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
std::string cmd = "cmd.exe /c \"" + buildBat.string() + "\"";
|
||||
|
||||
if (CreateProcessA(nullptr, cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, root.string().c_str(), &si, &pi)) {
|
||||
CloseHandle(pi.hThread);
|
||||
hBuildProcess = pi.hProcess;
|
||||
isBuilding = true;
|
||||
statusText = "Компиляция проекта (ml.exe & link.exe)...";
|
||||
} else {
|
||||
buildLog = "[ERROR] Не удалось запустить процесс сборки.\n";
|
||||
hasBuildResult = true;
|
||||
buildExitCode = 1;
|
||||
}
|
||||
CloseHandle(hWritePipe);
|
||||
}
|
||||
|
||||
void PollBuild() {
|
||||
if (!isBuilding) return;
|
||||
|
||||
// Read available pipe output
|
||||
if (hReadPipe) {
|
||||
DWORD avail = 0;
|
||||
if (PeekNamedPipe(hReadPipe, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) {
|
||||
std::vector<char> buffer(avail + 1, 0);
|
||||
DWORD read = 0;
|
||||
if (ReadFile(hReadPipe, buffer.data(), avail, &read, nullptr) && read > 0) {
|
||||
buildLog.append(buffer.data(), read);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check process exit
|
||||
if (hBuildProcess) {
|
||||
DWORD exitCode = 0;
|
||||
if (GetExitCodeProcess(hBuildProcess, &exitCode) && exitCode != STILL_ACTIVE) {
|
||||
// Read remaining output
|
||||
if (hReadPipe) {
|
||||
DWORD avail = 0;
|
||||
while (PeekNamedPipe(hReadPipe, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) {
|
||||
std::vector<char> buffer(avail + 1, 0);
|
||||
DWORD read = 0;
|
||||
if (ReadFile(hReadPipe, buffer.data(), avail, &read, nullptr) && read > 0) {
|
||||
buildLog.append(buffer.data(), read);
|
||||
} else break;
|
||||
}
|
||||
CloseHandle(hReadPipe);
|
||||
hReadPipe = nullptr;
|
||||
}
|
||||
CloseHandle(hBuildProcess);
|
||||
hBuildProcess = nullptr;
|
||||
|
||||
isBuilding = false;
|
||||
hasBuildResult = true;
|
||||
buildExitCode = static_cast<int>(exitCode);
|
||||
|
||||
ParseCompilerErrors();
|
||||
|
||||
if (buildExitCode == 0) {
|
||||
statusText = "Сборка успешно завершена! engine.exe обновлен.";
|
||||
} else {
|
||||
char codeBuf[32];
|
||||
snprintf(codeBuf, sizeof(codeBuf), "%d", buildExitCode);
|
||||
statusText = std::string("Ошибка сборки движка (код ") + codeBuf + "). Проверьте лог.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParseCompilerErrors() {
|
||||
buildErrors.clear();
|
||||
size_t start = 0;
|
||||
while (start < buildLog.size()) {
|
||||
size_t end = buildLog.find('\n', start);
|
||||
std::string line = (end == std::string::npos) ? buildLog.substr(start) : buildLog.substr(start, end - start);
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
|
||||
auto openParen = line.find('(');
|
||||
auto closeParen = (openParen != std::string::npos) ? line.find(')', openParen) : std::string::npos;
|
||||
auto colon = (closeParen != std::string::npos) ? line.find(" : ", closeParen) : std::string::npos;
|
||||
if (openParen != std::string::npos && closeParen != std::string::npos && colon != std::string::npos && closeParen > openParen) {
|
||||
std::string lineStr = line.substr(openParen + 1, closeParen - openParen - 1);
|
||||
bool isNum = !lineStr.empty();
|
||||
for (char c : lineStr) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(c))) { isNum = false; break; }
|
||||
}
|
||||
if (isNum) {
|
||||
ParsedError err{};
|
||||
err.file = line.substr(0, openParen);
|
||||
err.line = std::atoi(lineStr.c_str());
|
||||
err.message = line.substr(colon + 3);
|
||||
buildErrors.push_back(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static CodeEditorState g_CodeEditor;
|
||||
|
||||
void EditorApp::RenderCodeEditorWindow() {
|
||||
if (!showCodeEditorModal) return;
|
||||
|
||||
g_CodeEditor.PollBuild();
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Top Menu Bar
|
||||
if (ImGui::BeginMenuBar()) {
|
||||
if (ImGui::BeginMenu("Файл")) {
|
||||
if (ImGui::MenuItem("Сохранить (Ctrl+S)", "Ctrl+S")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
if (ImGui::MenuItem("Собрать движок (F7)", "F7")) {
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Закрыть")) {
|
||||
showCodeEditorModal = false;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Шаблоны кода")) {
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::MenuItem(g_ScriptTemplates[i].title)) {
|
||||
// Append template code to text buffer
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.isModified = true;
|
||||
g_CodeEditor.statusText = std::string("Вставлен шаблон: ") + g_ScriptTemplates[i].title;
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
// Toolbar Buttons
|
||||
ImGui::BeginGroup();
|
||||
char module[MAX_PATH]{};
|
||||
GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||||
std::string root = fs::path(module).parent_path().string();
|
||||
|
||||
if (ImGui::Button("Сохранить (Ctrl+S)")) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (g_CodeEditor.isBuilding) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.4f, 0.1f, 1.0f));
|
||||
ImGui::Button("Компиляция...");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.2f, 1.0f));
|
||||
if (ImGui::Button("Собрать движок (F7)")) {
|
||||
g_CodeEditor.StartBuild(root);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("| %s", g_CodeEditor.statusText.c_str());
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Split Left (File list) and Right (Editor + Output)
|
||||
ImGui::Columns(2, "CodeEditorColumns", true);
|
||||
static bool initCol = true;
|
||||
if (initCol) {
|
||||
ImGui::SetColumnWidth(0, 240);
|
||||
initCol = false;
|
||||
// Load initial file
|
||||
g_CodeEditor.LoadFile(0, root);
|
||||
}
|
||||
|
||||
// Left Column: Source File List
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:");
|
||||
for (int i = 0; i < g_CodeFileCount; ++i) {
|
||||
bool isSelected = (g_CodeEditor.currentFile == i);
|
||||
if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) {
|
||||
if (g_CodeEditor.isModified) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
g_CodeEditor.LoadFile(i, root);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Быстрые шаблоны:");
|
||||
for (int i = 0; i < g_ScriptTemplateCount; ++i) {
|
||||
if (ImGui::SmallButton(g_ScriptTemplates[i].title)) {
|
||||
size_t curLen = std::strlen(g_CodeEditor.textBuffer.data());
|
||||
size_t tLen = std::strlen(g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024);
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), "\n");
|
||||
std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code);
|
||||
g_CodeEditor.isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Right Column: Code Editor & Compiler Console
|
||||
float availY = ImGui::GetContentRegionAvail().y;
|
||||
float editorHeight = availY * 0.65f;
|
||||
float outputHeight = availY - editorHeight - 20;
|
||||
|
||||
// Multiline Text Editor with monospace font
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Файл: %s %s",
|
||||
g_ProjectCodeFiles[g_CodeEditor.currentFile].relativePath.c_str(),
|
||||
g_CodeEditor.isModified ? "*" : "");
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.08f, 0.08f, 0.12f, 1.0f));
|
||||
if (g_CodeEditor.textBuffer.empty()) {
|
||||
g_CodeEditor.EnsureBufferSize(1024);
|
||||
}
|
||||
|
||||
ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_AllowTabInput;
|
||||
if (ImGui::InputTextMultiline("##AsmCodeEditor", g_CodeEditor.textBuffer.data(),
|
||||
g_CodeEditor.textBuffer.size(), ImVec2(-1, editorHeight), textFlags)) {
|
||||
g_CodeEditor.isModified = true;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Hotkey: Ctrl+S to save
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S)) {
|
||||
g_CodeEditor.SaveFile(root);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Bottom Output Console
|
||||
ImGui::BeginTabBar("OutputTabs");
|
||||
if (ImGui::BeginTabItem("Вывод компилятора (ml.exe)")) {
|
||||
ImGui::BeginChild("CompilerOutputChild", ImVec2(0, outputHeight), true);
|
||||
if (g_CodeEditor.buildLog.empty() && !g_CodeEditor.isBuilding) {
|
||||
ImGui::TextDisabled("Нажмите 'Собрать движок (F7)' для компиляции ассемблерного кода...");
|
||||
} else {
|
||||
// Display log line by line with color tags
|
||||
size_t logStart = 0;
|
||||
while (logStart < g_CodeEditor.buildLog.size()) {
|
||||
size_t logEnd = g_CodeEditor.buildLog.find('\n', logStart);
|
||||
std::string line = (logEnd == std::string::npos) ? g_CodeEditor.buildLog.substr(logStart) : g_CodeEditor.buildLog.substr(logStart, logEnd - logStart);
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
|
||||
if (line.find("error") != std::string::npos || line.find("[ERROR]") != std::string::npos) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", line.c_str());
|
||||
} else if (line.find("warning") != std::string::npos) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", line.c_str());
|
||||
} else if (line.find("[SUCCESS]") != std::string::npos) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.3f, 1.0f), "%s", line.c_str());
|
||||
} else {
|
||||
ImGui::TextUnformatted(line.c_str());
|
||||
}
|
||||
|
||||
if (logEnd == std::string::npos) break;
|
||||
logStart = logEnd + 1;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Ошибки компиляции")) {
|
||||
ImGui::BeginChild("ErrorsListChild", ImVec2(0, outputHeight), true);
|
||||
if (g_CodeEditor.buildErrors.empty()) {
|
||||
if (g_CodeEditor.hasBuildResult && g_CodeEditor.buildExitCode == 0) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.3f, 1.0f), "Ошибок нет. Код скомпилирован успешно!");
|
||||
} else {
|
||||
ImGui::TextDisabled("Ошибок не обнаружено.");
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < g_CodeEditor.buildErrors.size(); ++i) {
|
||||
const auto& err = g_CodeEditor.buildErrors[i];
|
||||
char lineNumBuf[32];
|
||||
snprintf(lineNumBuf, sizeof(lineNumBuf), "%d", err.line);
|
||||
std::string label = err.file + ":" + lineNumBuf + " - " + err.message;
|
||||
if (ImGui::Selectable(label.c_str())) {
|
||||
// Switch to corresponding file if matched
|
||||
for (int f = 0; f < g_CodeFileCount; ++f) {
|
||||
if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos ||
|
||||
g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) {
|
||||
if (g_CodeEditor.isModified) g_CodeEditor.SaveFile(root);
|
||||
g_CodeEditor.LoadFile(f, root);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
|
||||
ImGui::Columns(1);
|
||||
ImGui::End();
|
||||
}
|
||||
|
|
@ -40,6 +40,17 @@ enum CutsceneOpCode : uint8_t {
|
|||
OP_HIDE_SUBTITLE, OP_PLAY_SOUND, OP_FADE_SCREEN, OP_CHANGE_SUBLEVEL
|
||||
};
|
||||
|
||||
// Entity System Archetypes
|
||||
enum EntityArchetype : uint8_t {
|
||||
ENTITY_ARCHETYPE_PROP = 0, // Static decoration
|
||||
ENTITY_ARCHETYPE_GUARD = 1, // Enemy Guard AI (patrol, chase, attack)
|
||||
ENTITY_ARCHETYPE_MEDKIT = 2, // Health pickup (+25 HP)
|
||||
ENTITY_ARCHETYPE_AMMO = 3, // Ammo pickup (+20 Ammo)
|
||||
ENTITY_ARCHETYPE_KEY = 4, // Door Key pickup
|
||||
ENTITY_ARCHETYPE_SWITCH = 5, // Interactive Switch / Trigger
|
||||
ENTITY_ARCHETYPE_COUNT = 6
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct LevelTransition {
|
||||
uint8_t triggerCellX; // Trigger cell coordinate X (0..31)
|
||||
|
|
@ -90,6 +101,34 @@ struct SpriteEntry {
|
|||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
inline uint8_t GetSpriteEntityType(const SpriteEntry& s) {
|
||||
uint8_t t = static_cast<uint8_t>(s.flags & 0xFF);
|
||||
if (t == 0) {
|
||||
if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD;
|
||||
if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT;
|
||||
if (s.textureId == 15) return ENTITY_ARCHETYPE_AMMO;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
inline uint8_t GetSpriteHealth(const SpriteEntry& s) {
|
||||
uint8_t h = static_cast<uint8_t>((s.flags >> 8) & 0xFF);
|
||||
return (h == 0) ? 100 : h;
|
||||
}
|
||||
inline uint8_t GetSpriteSpeed(const SpriteEntry& s) {
|
||||
uint8_t spd = static_cast<uint8_t>((s.flags >> 16) & 0xFF);
|
||||
return (spd == 0) ? 2 : spd;
|
||||
}
|
||||
inline uint8_t GetSpriteAggro(const SpriteEntry& s) {
|
||||
uint8_t agg = static_cast<uint8_t>((s.flags >> 24) & 0xFF);
|
||||
return (agg == 0) ? 6 : agg;
|
||||
}
|
||||
inline void SetSpriteEntityData(SpriteEntry& s, uint8_t type, uint8_t health, uint8_t speed, uint8_t aggro) {
|
||||
s.flags = static_cast<uint32_t>(type) |
|
||||
(static_cast<uint32_t>(health) << 8) |
|
||||
(static_cast<uint32_t>(speed) << 16) |
|
||||
(static_cast<uint32_t>(aggro) << 24);
|
||||
}
|
||||
|
||||
static_assert(sizeof(MapHeader) == 38, "EBLN header layout changed");
|
||||
static_assert(sizeof(SpriteEntry) == 24, "EBLN sprite layout changed");
|
||||
static_assert(sizeof(LevelTransition) == 42, "EBLN transition layout changed");
|
||||
|
|
|
|||
178
editor/src/project_manager.h
Normal file
178
editor/src/project_manager.h
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <windows.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct EblanProject {
|
||||
std::string title = "EBLAN_GAME";
|
||||
std::string author = "Developer";
|
||||
std::string version = "1.0.0";
|
||||
std::string startupMap = "assets/map.dat";
|
||||
std::string windowTitle = "EBLAN ENGINE 3D - Standalone Retro Game";
|
||||
int defaultWidth = 1280;
|
||||
int defaultHeight = 960;
|
||||
|
||||
bool Save(const std::string& path) const {
|
||||
HANDLE hFile = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) return false;
|
||||
char numBuf[32];
|
||||
std::string content = "[Project]\r\n";
|
||||
content += "Title=" + title + "\r\n";
|
||||
content += "Author=" + author + "\r\n";
|
||||
content += "Version=" + version + "\r\n";
|
||||
content += "StartupMap=" + startupMap + "\r\n";
|
||||
content += "WindowTitle=" + windowTitle + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", defaultWidth);
|
||||
content += std::string("DefaultWidth=") + numBuf + "\r\n";
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", defaultHeight);
|
||||
content += std::string("DefaultHeight=") + numBuf + "\r\n";
|
||||
DWORD written = 0;
|
||||
WriteFile(hFile, content.data(), static_cast<DWORD>(content.size()), &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Load(const std::string& path) {
|
||||
HANDLE hFile = CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) return false;
|
||||
DWORD size = GetFileSize(hFile, nullptr);
|
||||
if (size == INVALID_FILE_SIZE) { CloseHandle(hFile); return false; }
|
||||
std::string buffer(size, '\0');
|
||||
DWORD read = 0;
|
||||
ReadFile(hFile, buffer.data(), size, &read, nullptr);
|
||||
CloseHandle(hFile);
|
||||
|
||||
size_t start = 0;
|
||||
while (start < buffer.size()) {
|
||||
size_t end = buffer.find('\n', start);
|
||||
std::string line = (end == std::string::npos) ? buffer.substr(start) : buffer.substr(start, end - start);
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
|
||||
if (!line.empty() && line[0] != ';' && line[0] != '#') {
|
||||
auto eq = line.find('=');
|
||||
if (eq != std::string::npos) {
|
||||
std::string key = line.substr(0, eq);
|
||||
std::string val = line.substr(eq + 1);
|
||||
if (key == "Title") title = val;
|
||||
else if (key == "Author") author = val;
|
||||
else if (key == "Version") version = val;
|
||||
else if (key == "StartupMap") startupMap = val;
|
||||
else if (key == "WindowTitle") windowTitle = val;
|
||||
else if (key == "DefaultWidth") defaultWidth = std::atoi(val.c_str());
|
||||
else if (key == "DefaultHeight") defaultHeight = std::atoi(val.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class StandaloneExporter {
|
||||
public:
|
||||
static bool ExportGame(const EblanProject& project, const std::string& destDir, const std::string& projectRoot, std::string& outMessage) {
|
||||
std::error_code ec;
|
||||
fs::path root(projectRoot);
|
||||
fs::path dest(destDir);
|
||||
|
||||
if (!fs::exists(dest, ec)) {
|
||||
fs::create_directories(dest, ec);
|
||||
if (ec) {
|
||||
outMessage = "Ошибка создания папки экспорта: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Locate engine.exe
|
||||
fs::path srcExe = root / "engine.exe";
|
||||
if (!fs::exists(srcExe, ec)) {
|
||||
srcExe = root.parent_path() / "engine.exe";
|
||||
}
|
||||
if (!fs::exists(srcExe, ec)) {
|
||||
outMessage = "Не найден скомпилированный движок engine.exe. Сначала выполните компиляцию (F7).";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Sanitize executable name
|
||||
std::string safeName = project.title;
|
||||
for (char& c : safeName) {
|
||||
if (c == ' ' || c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') {
|
||||
c = '_';
|
||||
}
|
||||
}
|
||||
if (safeName.empty()) safeName = "Game";
|
||||
|
||||
fs::path targetExe = dest / (safeName + ".exe");
|
||||
if (!CopyFileA(srcExe.string().c_str(), targetExe.string().c_str(), FALSE)) {
|
||||
outMessage = "Ошибка копирования исполняемого файла игры.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Copy assets directory
|
||||
fs::path srcAssets = root / "assets";
|
||||
if (!fs::exists(srcAssets, ec)) {
|
||||
srcAssets = root.parent_path() / "assets";
|
||||
}
|
||||
if (fs::exists(srcAssets, ec)) {
|
||||
fs::path targetAssets = dest / "assets";
|
||||
fs::create_directories(targetAssets, ec);
|
||||
for (const auto& entry : fs::recursive_directory_iterator(srcAssets, ec)) {
|
||||
if (ec) break;
|
||||
auto rel = fs::relative(entry.path(), srcAssets, ec);
|
||||
fs::path targetPath = targetAssets / rel;
|
||||
if (entry.is_directory()) {
|
||||
fs::create_directories(targetPath, ec);
|
||||
} else if (entry.is_regular_file()) {
|
||||
fs::create_directories(targetPath.parent_path(), ec);
|
||||
CopyFileA(entry.path().string().c_str(), targetPath.string().c_str(), FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Create launcher script PlayGame.bat
|
||||
fs::path launcherPath = dest / "PlayGame.bat";
|
||||
HANDLE hBat = CreateFileA(launcherPath.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hBat != INVALID_HANDLE_VALUE) {
|
||||
std::string batContent = "@echo off\r\ntitle " + safeName + "\r\ncd /d \"%~dp0\"\r\nstart \"\" \"" + safeName + ".exe\"\r\n";
|
||||
DWORD written = 0;
|
||||
WriteFile(hBat, batContent.data(), static_cast<DWORD>(batContent.size()), &written, nullptr);
|
||||
CloseHandle(hBat);
|
||||
}
|
||||
|
||||
// 5. Create README.txt
|
||||
fs::path readmePath = dest / "README.txt";
|
||||
HANDLE hReadme = CreateFileA(readmePath.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hReadme != INVALID_HANDLE_VALUE) {
|
||||
std::string readme = "==============================================================================\r\n";
|
||||
readme += project.title + " (v" + project.version + ")\r\n";
|
||||
readme += "Created with EblanStudio & EBLAN ENGINE 3D [Pure x86 Assembly]\r\n";
|
||||
readme += "Author: " + project.author + "\r\n";
|
||||
readme += "==============================================================================\r\n\r\n";
|
||||
readme += "УПРАВЛЕНИЕ:\r\n";
|
||||
readme += " W, A, S, D - Перемещение / Стрейф\r\n";
|
||||
readme += " Стрелки - Поворот камеры\r\n";
|
||||
readme += " Shift - Бег / Спринт\r\n";
|
||||
readme += " ПРОБЕЛ/Ctrl - Огонь / Стрельба\r\n";
|
||||
readme += " E - Использование / Открытие дверей / Переключатели\r\n";
|
||||
readme += " M - Радар / Карта\r\n";
|
||||
readme += " C - Перезапуск кат-сцены\r\n";
|
||||
readme += " R - Перезапуск уровня при гибели\r\n";
|
||||
readme += " F12 - Сделать скриншот (screenshot.bmp)\r\n";
|
||||
readme += " ESC - Выход\r\n\r\n";
|
||||
readme += "Zero External Dependencies: работает на любой версии Windows без DirectX/C++ runtimes!\r\n";
|
||||
DWORD written = 0;
|
||||
WriteFile(hReadme, readme.data(), static_cast<DWORD>(readme.size()), &written, nullptr);
|
||||
CloseHandle(hReadme);
|
||||
}
|
||||
|
||||
outMessage = "Игра успешно экспортирована в:\n" + dest.string() + "\n\nСоздан автономный пакет с исполняемым файлом " + safeName + ".exe, ассетами и лаунчером.";
|
||||
return true;
|
||||
}
|
||||
};
|
||||
BIN
engine.exe
BIN
engine.exe
Binary file not shown.
303
include/engine_combat.inc
Normal file
303
include/engine_combat.inc
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Player Combat, Weapon Shooting & Combat HUD
|
||||
; ==============================================================================
|
||||
|
||||
.code
|
||||
|
||||
DrawPixel PROTO :DWORD, :DWORD, :DWORD
|
||||
DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdateCombat: Process weapon firing, hitscan raycasting, cooldowns & damage
|
||||
; ------------------------------------------------------------------------------
|
||||
UpdateCombat PROC uses ebx ecx edx esi edi
|
||||
LOCAL camCos:DWORD, camSin:DWORD
|
||||
LOCAL i:DWORD, closestIdx:DWORD, closestDist:DWORD
|
||||
LOCAL sprX:DWORD, sprY:DWORD
|
||||
LOCAL transX:DWORD, transY:DWORD
|
||||
LOCAL sprScreenX:DWORD, wallDepth:DWORD
|
||||
|
||||
; 1. Decrement cooldowns
|
||||
cmp playerAttackCooldown, 0
|
||||
je @F
|
||||
dec playerAttackCooldown
|
||||
@@:
|
||||
cmp playerFlashTimer, 0
|
||||
je @F
|
||||
dec playerFlashTimer
|
||||
@@:
|
||||
cmp playerHitFlash, 0
|
||||
je @F
|
||||
dec playerHitFlash
|
||||
@@:
|
||||
|
||||
; If player is dead, cannot attack
|
||||
cmp isPlayerDead, 0
|
||||
jne combat_done
|
||||
|
||||
; 2. Check attack input
|
||||
cmp keyAttack, 0
|
||||
je combat_done
|
||||
cmp playerAttackCooldown, 0
|
||||
jne combat_done
|
||||
|
||||
; Check ammo
|
||||
cmp playerAmmo, 0
|
||||
jle combat_dry_fire
|
||||
|
||||
; Fire weapon!
|
||||
dec playerAmmo
|
||||
mov playerAttackCooldown, 10
|
||||
mov playerFlashTimer, 5
|
||||
|
||||
; 3. Hitscan check against enemies in center crosshair
|
||||
; Camera basis for -playerAngle
|
||||
mov eax, ANGLE_512
|
||||
sub eax, playerAngle
|
||||
and eax, ANGLE_MASK
|
||||
mov ecx, [tableCos + eax*4]
|
||||
mov camCos, ecx
|
||||
mov edx, [tableSin + eax*4]
|
||||
mov camSin, edx
|
||||
|
||||
; Center wall depth from depthBuffer[160]
|
||||
mov eax, [depthBuffer + SCREEN_HALF_W * 4]
|
||||
mov wallDepth, eax
|
||||
|
||||
mov closestIdx, -1
|
||||
mov closestDist, 999999
|
||||
|
||||
mov i, 0
|
||||
hitscan_loop:
|
||||
mov eax, i
|
||||
cmp eax, spriteCount
|
||||
jge hitscan_eval
|
||||
|
||||
mov ecx, i
|
||||
mov eax, [entityType + ecx*4]
|
||||
cmp eax, ENTITY_TYPE_GUARD
|
||||
jne next_hitscan_target
|
||||
|
||||
mov eax, [entityState + ecx*4]
|
||||
cmp eax, ENTITY_STATE_DEAD
|
||||
je next_hitscan_target
|
||||
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
|
||||
mov eax, [ebx].SpriteStruct.textureId
|
||||
test eax, eax
|
||||
jz next_hitscan_target
|
||||
|
||||
; Transform to camera space
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
sub eax, playerX
|
||||
mov sprX, eax
|
||||
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
sub eax, playerY
|
||||
mov sprY, eax
|
||||
|
||||
; transY = sprX * camCos - sprY * camSin
|
||||
mov eax, sprX
|
||||
imul camCos
|
||||
shrd eax, edx, 16
|
||||
mov ecx, eax
|
||||
mov eax, sprY
|
||||
imul camSin
|
||||
shrd eax, edx, 16
|
||||
sub ecx, eax
|
||||
mov transY, ecx
|
||||
|
||||
; Must be in front of player
|
||||
cmp transY, 16
|
||||
jle next_hitscan_target
|
||||
|
||||
; Must be closer than the wall at center screen
|
||||
mov eax, transY
|
||||
cmp eax, wallDepth
|
||||
jge next_hitscan_target
|
||||
|
||||
; transX = sprX * camSin + sprY * camCos
|
||||
mov eax, sprX
|
||||
imul camSin
|
||||
shrd eax, edx, 16
|
||||
mov ecx, eax
|
||||
mov eax, sprY
|
||||
imul camCos
|
||||
shrd eax, edx, 16
|
||||
add ecx, eax
|
||||
mov transX, ecx
|
||||
|
||||
; sprScreenX = (transX * PROJ_DIST) / transY + SCREEN_HALF_W
|
||||
mov eax, transX
|
||||
imul eax, PROJ_DIST
|
||||
cdq
|
||||
idiv transY
|
||||
add eax, SCREEN_HALF_W
|
||||
mov sprScreenX, eax
|
||||
|
||||
; Crosshair hitbox: within 22 pixels of center (SCREEN_HALF_W = 160)
|
||||
sub eax, SCREEN_HALF_W
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx ; abs(sprScreenX - 160)
|
||||
cmp eax, 22
|
||||
jg next_hitscan_target
|
||||
|
||||
; Check if closer than previous target
|
||||
mov eax, transY
|
||||
cmp eax, closestDist
|
||||
jge next_hitscan_target
|
||||
mov closestDist, eax
|
||||
mov eax, i
|
||||
mov closestIdx, eax
|
||||
|
||||
next_hitscan_target:
|
||||
inc i
|
||||
jmp hitscan_loop
|
||||
|
||||
hitscan_eval:
|
||||
cmp closestIdx, -1
|
||||
je combat_done
|
||||
|
||||
; Apply damage to hit guard
|
||||
mov ecx, closestIdx
|
||||
mov eax, [entityHealth + ecx*4]
|
||||
sub eax, 35
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
mov [entityHealth + ecx*4], eax
|
||||
test eax, eax
|
||||
jnz combat_done
|
||||
|
||||
; Guard died!
|
||||
mov [entityState + ecx*4], ENTITY_STATE_DEAD
|
||||
mov DWORD PTR [entityTimer + ecx*4], 0
|
||||
add playerScore, 100
|
||||
|
||||
; Drop 10 ammo reward
|
||||
mov eax, playerAmmo
|
||||
add eax, 10
|
||||
cmp eax, playerMaxAmmo
|
||||
jle @F
|
||||
mov eax, playerMaxAmmo
|
||||
@@:
|
||||
mov playerAmmo, eax
|
||||
jmp combat_done
|
||||
|
||||
combat_dry_fire:
|
||||
mov playerAttackCooldown, 12
|
||||
|
||||
combat_done:
|
||||
ret
|
||||
UpdateCombat ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; RenderCombatHUD: Renders health bar, ammo, crosshair, weapon & death screen
|
||||
; ------------------------------------------------------------------------------
|
||||
RenderCombatHUD PROC uses ebx ecx edx esi edi
|
||||
LOCAL barW:DWORD, gunY:DWORD
|
||||
|
||||
; 1. Center Crosshair (subtle cross at 160, 120)
|
||||
invoke DrawRect, 156, 120, 9, 1, 00FFFFFFh
|
||||
invoke DrawRect, 160, 116, 1, 9, 00FFFFFFh
|
||||
invoke DrawRect, 160, 120, 1, 1, 0000FF00h
|
||||
|
||||
; 2. Render Retro Weapon at bottom center
|
||||
mov gunY, 192
|
||||
cmp playerFlashTimer, 0
|
||||
je draw_gun_barrel
|
||||
; Recoil offset
|
||||
add gunY, 3
|
||||
|
||||
; Muzzle Flash Burst (Bright yellow & white star)
|
||||
invoke DrawRect, 157, 165, 7, 18, 00FFFF00h
|
||||
invoke DrawRect, 152, 170, 17, 8, 00FFAA00h
|
||||
invoke DrawRect, 158, 168, 5, 12, 00FFFFFFh
|
||||
|
||||
draw_gun_barrel:
|
||||
; Weapon barrel & slide
|
||||
invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah
|
||||
invoke DrawRect, 156, gunY, 9, 28, 00555555h
|
||||
invoke DrawRect, 158, gunY, 5, 28, 00777777h ; highlight strip
|
||||
; Gun frame & grip
|
||||
mov eax, gunY
|
||||
add eax, 24
|
||||
invoke DrawRect, 150, eax, 21, 24, 00252525h
|
||||
mov eax, gunY
|
||||
add eax, 26
|
||||
invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates
|
||||
|
||||
; 3. Health Bar
|
||||
invoke DrawString8x8, 8, 42, OFFSET strHpLabel, COLOR_GREEN
|
||||
invoke DrawNumber8x8, 34, 42, playerHealth, COLOR_WHITE
|
||||
|
||||
invoke DrawRect, 68, 42, 60, 8, 00441010h
|
||||
invoke DrawRectOutline, 68, 42, 60, 8, COLOR_WHITE
|
||||
|
||||
mov eax, playerHealth
|
||||
imul eax, 58
|
||||
xor edx, edx
|
||||
mov ecx, playerMaxHealth
|
||||
test ecx, ecx
|
||||
jnz @F
|
||||
mov ecx, 100
|
||||
@@:
|
||||
div ecx
|
||||
mov barW, eax
|
||||
cmp barW, 0
|
||||
jle hud_ammo_draw
|
||||
cmp barW, 58
|
||||
jle @F
|
||||
mov barW, 58
|
||||
@@:
|
||||
mov edx, 0000CC33h ; Green
|
||||
cmp playerHealth, 30
|
||||
jg @F
|
||||
mov edx, 00FF2222h ; Red warning
|
||||
@@:
|
||||
invoke DrawRect, 69, 43, barW, 6, edx
|
||||
|
||||
hud_ammo_draw:
|
||||
; 4. Ammo Display
|
||||
invoke DrawString8x8, 140, 42, OFFSET strAmmoLabel, COLOR_YELLOW
|
||||
invoke DrawNumber8x8, 182, 42, playerAmmo, COLOR_YELLOW
|
||||
|
||||
; 5. Score Display
|
||||
invoke DrawString8x8, 222, 42, OFFSET strScoreLabel, COLOR_CYAN
|
||||
invoke DrawNumber8x8, 272, 42, playerScore, COLOR_CYAN
|
||||
|
||||
; 6. Keys (if collected)
|
||||
cmp playerKeys, 0
|
||||
jle hud_damage_flash
|
||||
invoke DrawString8x8, 222, 10, OFFSET strKeysLabel, COLOR_MAGENTA
|
||||
invoke DrawNumber8x8, 258, 10, playerKeys, COLOR_MAGENTA
|
||||
|
||||
hud_damage_flash:
|
||||
; 7. Damage Screen Flash
|
||||
cmp playerHitFlash, 0
|
||||
jle hud_death_check
|
||||
; Red border vignette
|
||||
invoke DrawRect, 0, 0, SCREEN_W, 3, 00FF0000h
|
||||
invoke DrawRect, 0, 237, SCREEN_W, 3, 00FF0000h
|
||||
invoke DrawRect, 0, 0, 3, SCREEN_H, 00FF0000h
|
||||
invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h
|
||||
|
||||
hud_death_check:
|
||||
; 8. Death Overlay Screen
|
||||
cmp isPlayerDead, 0
|
||||
je combat_hud_done
|
||||
|
||||
invoke DrawRect, 20, 95, 280, 50, 00880000h
|
||||
invoke DrawRectOutline, 20, 95, 280, 50, COLOR_WHITE
|
||||
invoke DrawString8x8, 32, 114, OFFSET strYouDied, COLOR_YELLOW
|
||||
|
||||
combat_hud_done:
|
||||
ret
|
||||
RenderCombatHUD ENDP
|
||||
|
|
@ -59,6 +59,22 @@ keyCutscene db 0
|
|||
keySkipScene db 0
|
||||
keySceneHeld db 0
|
||||
keySkipHeld db 0
|
||||
keyAttack db 0
|
||||
keyAttackHeld db 0
|
||||
keyRestart db 0
|
||||
keyRestartHeld db 0
|
||||
|
||||
; Player Combat & Gameplay Stats
|
||||
playerHealth dd 100
|
||||
playerMaxHealth dd 100
|
||||
playerAmmo dd 50
|
||||
playerMaxAmmo dd 99
|
||||
playerScore dd 0
|
||||
playerKeys dd 0
|
||||
playerAttackCooldown dd 0
|
||||
playerFlashTimer dd 0
|
||||
playerHitFlash dd 0
|
||||
isPlayerDead dd 0
|
||||
|
||||
; Map palette (overridable by map.dat header)
|
||||
mapCeilingColor dd COLOR_CEILING
|
||||
|
|
@ -97,6 +113,11 @@ strDoorHint db "[E] - Door",0
|
|||
strPortalHint db "[E] - Enter Portal",0
|
||||
strMapLoadError db "Cannot load destination level",0
|
||||
strSkipScene db "[Tab] Skip",0
|
||||
strHpLabel db "HP:",0
|
||||
strAmmoLabel db "AMMO:",0
|
||||
strScoreLabel db "SCORE:",0
|
||||
strKeysLabel db "KEY:",0
|
||||
strYouDied db "YOU DIED! PRESS [R] TO RESTART",0
|
||||
cutsceneCount dd 0
|
||||
cutsceneStringCount dd 0
|
||||
cutsceneFlags dd 0
|
||||
|
|
@ -194,3 +215,11 @@ cutsceneEvents db MAX_CUTSCENE_EVENTS * (sizeof CutsceneEvent) dup(?)
|
|||
cutsceneStrings db MAX_CUTSCENE_STRINGS * CUTSCENE_STRING_BYTES dup(?)
|
||||
stagedCutsceneEvents db MAX_CUTSCENE_EVENTS * (sizeof CutsceneEvent) dup(?)
|
||||
stagedCutsceneStrings db MAX_CUTSCENE_STRINGS * CUTSCENE_STRING_BYTES dup(?)
|
||||
|
||||
ALIGN 4
|
||||
entityType dd MAX_SPRITES dup(?)
|
||||
entityHealth dd MAX_SPRITES dup(?)
|
||||
entityState dd MAX_SPRITES dup(?)
|
||||
entityTimer dd MAX_SPRITES dup(?)
|
||||
entitySpeed dd MAX_SPRITES dup(?)
|
||||
entityAggro dd MAX_SPRITES dup(?)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,22 @@ VK_KEY_DOWN equ 28h
|
|||
VK_KEY_F12 equ 7Bh
|
||||
VK_KEY_C equ 43h
|
||||
VK_KEY_TAB equ 9
|
||||
VK_KEY_R equ 52h
|
||||
VK_KEY_CONTROL equ 11h
|
||||
VK_LBUTTON equ 01h
|
||||
|
||||
; Entity Archetypes & States
|
||||
ENTITY_TYPE_PROP equ 0
|
||||
ENTITY_TYPE_GUARD equ 1 ; Enemy Guard AI (Patrol, Chase, Attack)
|
||||
ENTITY_TYPE_MEDKIT equ 2 ; Health pickup (+25 HP)
|
||||
ENTITY_TYPE_AMMO equ 3 ; Ammo pickup (+20 Ammo)
|
||||
ENTITY_TYPE_KEY equ 4 ; Door Key pickup
|
||||
ENTITY_TYPE_SWITCH equ 5 ; Interactive switch trigger
|
||||
|
||||
ENTITY_STATE_IDLE equ 0
|
||||
ENTITY_STATE_CHASE equ 1
|
||||
ENTITY_STATE_ATTACK equ 2
|
||||
ENTITY_STATE_DEAD equ 3
|
||||
|
||||
; Colors (32-bit 00RRGGBBh; in memory a DIB stores bytes B,G,R,0 little-endian)
|
||||
COLOR_BLACK equ 00000000h
|
||||
|
|
|
|||
386
include/engine_entity.inc
Normal file
386
include/engine_entity.inc
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
; ==============================================================================
|
||||
; EBLAN ENGINE - Entity & Gameplay Behavior System
|
||||
; ==============================================================================
|
||||
|
||||
.code
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; InitEntities: Initialize entity runtime state from spriteList flags/defaults
|
||||
; ------------------------------------------------------------------------------
|
||||
InitEntities PROC uses eax ebx ecx edx esi edi
|
||||
; 1. Clear runtime arrays
|
||||
xor eax, eax
|
||||
mov ecx, MAX_SPRITES
|
||||
mov edi, OFFSET entityType
|
||||
rep stosd
|
||||
|
||||
mov ecx, MAX_SPRITES
|
||||
mov eax, 100
|
||||
mov edi, OFFSET entityHealth
|
||||
rep stosd
|
||||
|
||||
xor eax, eax
|
||||
mov ecx, MAX_SPRITES
|
||||
mov edi, OFFSET entityState
|
||||
rep stosd
|
||||
|
||||
mov ecx, MAX_SPRITES
|
||||
mov edi, OFFSET entityTimer
|
||||
rep stosd
|
||||
|
||||
mov ecx, MAX_SPRITES
|
||||
mov eax, 2
|
||||
mov edi, OFFSET entitySpeed
|
||||
rep stosd
|
||||
|
||||
mov ecx, MAX_SPRITES
|
||||
mov eax, 384
|
||||
mov edi, OFFSET entityAggro
|
||||
rep stosd
|
||||
|
||||
; 2. Iterate spriteList and configure entity archetypes
|
||||
xor ecx, ecx
|
||||
init_spr_loop:
|
||||
cmp ecx, spriteCount
|
||||
jge init_spr_done
|
||||
|
||||
mov ebx, ecx
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
|
||||
mov eax, [ebx].SpriteStruct.flags
|
||||
test eax, eax
|
||||
jz fallback_archetype
|
||||
|
||||
; Parse flags:
|
||||
; Byte 0: Entity Type (0=Prop, 1=Guard, 2=Medkit, 3=Ammo, 4=Key, 5=Switch)
|
||||
; Byte 1: Health (if Guard)
|
||||
; Byte 2: Speed (1..8)
|
||||
; Byte 3: Aggro multiplier (1..15 -> cells * 64)
|
||||
mov edx, eax
|
||||
and edx, 0FFh
|
||||
mov [entityType + ecx*4], edx
|
||||
|
||||
mov edx, eax
|
||||
shr edx, 8
|
||||
and edx, 0FFh
|
||||
test edx, edx
|
||||
jz @F
|
||||
mov [entityHealth + ecx*4], edx
|
||||
@@:
|
||||
mov edx, eax
|
||||
shr edx, 16
|
||||
and edx, 0FFh
|
||||
test edx, edx
|
||||
jz @F
|
||||
mov [entitySpeed + ecx*4], edx
|
||||
@@:
|
||||
mov edx, eax
|
||||
shr edx, 24
|
||||
and edx, 0FFh
|
||||
test edx, edx
|
||||
jz next_spr_slot
|
||||
shl edx, 6 ; cells -> raw units
|
||||
mov [entityAggro + ecx*4], edx
|
||||
jmp next_spr_slot
|
||||
|
||||
fallback_archetype:
|
||||
; If flags == 0, infer archetype from textureId:
|
||||
; Slot 13 = Enemy Guard, 14 = Medkit, 15 = Ammo
|
||||
mov edx, [ebx].SpriteStruct.textureId
|
||||
cmp edx, 13
|
||||
jne check_medkit_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_GUARD
|
||||
mov [entityHealth + ecx*4], 100
|
||||
mov [entitySpeed + ecx*4], 2
|
||||
jmp next_spr_slot
|
||||
|
||||
check_medkit_slot:
|
||||
cmp edx, 14
|
||||
jne check_ammo_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_MEDKIT
|
||||
jmp next_spr_slot
|
||||
|
||||
check_ammo_slot:
|
||||
cmp edx, 15
|
||||
jne next_spr_slot
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_AMMO
|
||||
|
||||
next_spr_slot:
|
||||
inc ecx
|
||||
jmp init_spr_loop
|
||||
|
||||
init_spr_done:
|
||||
ret
|
||||
InitEntities ENDP
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; UpdateEntities: Updates AI, pickups, collision and interactions each tick
|
||||
; ------------------------------------------------------------------------------
|
||||
UpdateEntities PROC uses ebx ecx edx esi edi
|
||||
LOCAL i:DWORD
|
||||
LOCAL sprX:DWORD, sprY:DWORD
|
||||
LOCAL deltaX:DWORD, deltaY:DWORD
|
||||
LOCAL absDX:DWORD, absDY:DWORD
|
||||
LOCAL distSq:DWORD
|
||||
LOCAL speed:DWORD
|
||||
LOCAL stepX:DWORD, stepY:DWORD
|
||||
LOCAL cellX:DWORD, cellY:DWORD
|
||||
|
||||
; If player died and pressed Restart (R)
|
||||
cmp isPlayerDead, 0
|
||||
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
|
||||
ret
|
||||
|
||||
entities_active:
|
||||
mov i, 0
|
||||
|
||||
entity_loop:
|
||||
mov eax, i
|
||||
cmp eax, spriteCount
|
||||
jge entities_done
|
||||
|
||||
mov ebx, i
|
||||
imul ebx, sizeof SpriteStruct
|
||||
add ebx, OFFSET spriteList
|
||||
|
||||
; If sprite is inactive (textureId == 0), skip
|
||||
mov eax, [ebx].SpriteStruct.textureId
|
||||
test eax, eax
|
||||
jz next_entity
|
||||
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
mov sprX, eax
|
||||
mov eax, [ebx].SpriteStruct.y
|
||||
mov sprY, eax
|
||||
|
||||
; Distance to player
|
||||
mov eax, playerX
|
||||
sub eax, sprX
|
||||
mov deltaX, eax
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
mov absDX, eax
|
||||
|
||||
mov eax, playerY
|
||||
sub eax, sprY
|
||||
mov deltaY, eax
|
||||
cdq
|
||||
xor eax, edx
|
||||
sub eax, edx
|
||||
mov absDY, eax
|
||||
|
||||
; distSq = deltaX^2 + deltaY^2
|
||||
mov eax, deltaX
|
||||
imul eax, eax
|
||||
mov ecx, eax
|
||||
mov eax, deltaY
|
||||
imul eax, eax
|
||||
add eax, ecx
|
||||
mov distSq, eax
|
||||
|
||||
mov ecx, i
|
||||
mov edx, [entityType + ecx*4]
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; Medkit Pickup (+25 HP)
|
||||
; --------------------------------------------------------------------------
|
||||
cmp edx, ENTITY_TYPE_MEDKIT
|
||||
jne check_ammo_pickup
|
||||
cmp absDX, 32
|
||||
jge next_entity
|
||||
cmp absDY, 32
|
||||
jge next_entity
|
||||
mov eax, playerHealth
|
||||
cmp eax, playerMaxHealth
|
||||
jge next_entity
|
||||
add eax, 25
|
||||
cmp eax, playerMaxHealth
|
||||
jle @F
|
||||
mov eax, playerMaxHealth
|
||||
@@:
|
||||
mov playerHealth, eax
|
||||
mov [ebx].SpriteStruct.textureId, 0 ; Hide item
|
||||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 25
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; Ammo Pickup (+20 Ammo)
|
||||
; --------------------------------------------------------------------------
|
||||
check_ammo_pickup:
|
||||
cmp edx, ENTITY_TYPE_AMMO
|
||||
jne check_key_pickup
|
||||
cmp absDX, 32
|
||||
jge next_entity
|
||||
cmp absDY, 32
|
||||
jge next_entity
|
||||
mov eax, playerAmmo
|
||||
add eax, 20
|
||||
cmp eax, playerMaxAmmo
|
||||
jle @F
|
||||
mov eax, playerMaxAmmo
|
||||
@@:
|
||||
mov playerAmmo, eax
|
||||
mov [ebx].SpriteStruct.textureId, 0
|
||||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 10
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; Key Pickup (+1 Key)
|
||||
; --------------------------------------------------------------------------
|
||||
check_key_pickup:
|
||||
cmp edx, ENTITY_TYPE_KEY
|
||||
jne check_guard_entity
|
||||
cmp absDX, 32
|
||||
jge next_entity
|
||||
cmp absDY, 32
|
||||
jge next_entity
|
||||
inc playerKeys
|
||||
mov [ebx].SpriteStruct.textureId, 0
|
||||
mov ecx, i
|
||||
mov [entityType + ecx*4], ENTITY_TYPE_PROP
|
||||
add playerScore, 50
|
||||
jmp next_entity
|
||||
|
||||
; --------------------------------------------------------------------------
|
||||
; Guard Enemy AI
|
||||
; --------------------------------------------------------------------------
|
||||
check_guard_entity:
|
||||
cmp edx, ENTITY_TYPE_GUARD
|
||||
jne next_entity
|
||||
|
||||
mov ecx, i
|
||||
mov eax, [entityState + ecx*4]
|
||||
cmp eax, ENTITY_STATE_DEAD
|
||||
jne guard_alive
|
||||
|
||||
; Dead guard: tick timer until disappear
|
||||
inc DWORD PTR [entityTimer + ecx*4]
|
||||
cmp DWORD PTR [entityTimer + ecx*4], 120
|
||||
jl next_entity
|
||||
mov [ebx].SpriteStruct.textureId, 0
|
||||
jmp next_entity
|
||||
|
||||
guard_alive:
|
||||
; Check aggro radius
|
||||
mov eax, [entityAggro + ecx*4]
|
||||
imul eax, eax ; aggroSq
|
||||
cmp distSq, eax
|
||||
jg next_entity
|
||||
|
||||
; Guard is alert & chasing player
|
||||
mov ecx, i
|
||||
mov [entityState + ecx*4], ENTITY_STATE_CHASE
|
||||
mov eax, [entitySpeed + ecx*4]
|
||||
mov speed, eax
|
||||
|
||||
; Calculate movement direction
|
||||
mov stepX, 0
|
||||
mov stepY, 0
|
||||
|
||||
cmp deltaX, 16
|
||||
jle check_neg_x
|
||||
mov eax, speed
|
||||
mov stepX, eax
|
||||
jmp check_step_y
|
||||
check_neg_x:
|
||||
cmp deltaX, -16
|
||||
jge check_step_y
|
||||
mov eax, speed
|
||||
neg eax
|
||||
mov stepX, eax
|
||||
|
||||
check_step_y:
|
||||
cmp deltaY, 16
|
||||
jle check_neg_y
|
||||
mov eax, speed
|
||||
mov stepY, eax
|
||||
jmp apply_guard_step
|
||||
check_neg_y:
|
||||
cmp deltaY, -16
|
||||
jge apply_guard_step
|
||||
mov eax, speed
|
||||
neg eax
|
||||
mov stepY, eax
|
||||
|
||||
apply_guard_step:
|
||||
; Try stepping X
|
||||
cmp stepX, 0
|
||||
je try_step_y
|
||||
mov eax, sprX
|
||||
add eax, stepX
|
||||
sar eax, 6
|
||||
mov cellX, eax
|
||||
mov eax, sprY
|
||||
sar eax, 6
|
||||
mov cellY, eax
|
||||
invoke GetMapCell, cellX, cellY
|
||||
invoke IsSolidCell, eax
|
||||
test eax, eax
|
||||
jnz try_step_y
|
||||
mov eax, stepX
|
||||
add [ebx].SpriteStruct.x, eax
|
||||
mov eax, [ebx].SpriteStruct.x
|
||||
mov sprX, eax
|
||||
|
||||
try_step_y:
|
||||
cmp stepY, 0
|
||||
je check_guard_attack
|
||||
mov eax, sprX
|
||||
sar eax, 6
|
||||
mov cellX, eax
|
||||
mov eax, sprY
|
||||
add eax, stepY
|
||||
sar eax, 6
|
||||
mov cellY, eax
|
||||
invoke GetMapCell, cellX, cellY
|
||||
invoke IsSolidCell, eax
|
||||
test eax, eax
|
||||
jnz check_guard_attack
|
||||
mov eax, stepY
|
||||
add [ebx].SpriteStruct.y, eax
|
||||
|
||||
check_guard_attack:
|
||||
; Attack range check: within 48 raw units
|
||||
cmp distSq, 2304 ; 48 * 48
|
||||
jg next_entity
|
||||
|
||||
mov ecx, i
|
||||
inc DWORD PTR [entityTimer + ecx*4]
|
||||
cmp DWORD PTR [entityTimer + ecx*4], 35
|
||||
jl next_entity
|
||||
mov DWORD PTR [entityTimer + ecx*4], 0
|
||||
|
||||
; Guard hits player!
|
||||
mov eax, playerHealth
|
||||
sub eax, 8
|
||||
jns @F
|
||||
xor eax, eax
|
||||
@@:
|
||||
mov playerHealth, eax
|
||||
mov playerHitFlash, 6
|
||||
test eax, eax
|
||||
jnz next_entity
|
||||
mov isPlayerDead, 1
|
||||
|
||||
next_entity:
|
||||
inc i
|
||||
jmp entity_loop
|
||||
|
||||
entities_done:
|
||||
ret
|
||||
UpdateEntities ENDP
|
||||
|
|
@ -19,6 +19,8 @@ PollInput PROC uses eax ebx
|
|||
mov keyScreenshot, 0
|
||||
mov keyCutscene, 0
|
||||
mov keySkipScene, 0
|
||||
mov keyAttack, 0
|
||||
mov keyRestart, 0
|
||||
invoke GetForegroundWindow
|
||||
cmp eax, hEngineWnd
|
||||
je input_has_focus
|
||||
|
|
@ -27,6 +29,8 @@ PollInput PROC uses eax ebx
|
|||
mov keyShotHeld, 0
|
||||
mov keySceneHeld, 0
|
||||
mov keySkipHeld, 0
|
||||
mov keyAttackHeld, 0
|
||||
mov keyRestartHeld, 0
|
||||
ret
|
||||
input_has_focus:
|
||||
|
||||
|
|
@ -146,6 +150,37 @@ scene_key_sampled:
|
|||
skip_key_released:
|
||||
mov keySkipHeld, 0
|
||||
skip_key_sampled:
|
||||
; Attack Key: Space or Ctrl or Left Mouse Button
|
||||
invoke GetAsyncKeyState, VK_KEY_SPACE
|
||||
mov bx, ax
|
||||
invoke GetAsyncKeyState, VK_KEY_CONTROL
|
||||
or bx, ax
|
||||
invoke GetAsyncKeyState, VK_LBUTTON
|
||||
or bx, ax
|
||||
test bx, 8000h
|
||||
jz attack_released
|
||||
cmp keyAttackHeld, 0
|
||||
jne attack_sampled
|
||||
mov keyAttack, 1
|
||||
mov keyAttackHeld, 1
|
||||
jmp attack_sampled
|
||||
attack_released:
|
||||
mov keyAttackHeld, 0
|
||||
attack_sampled:
|
||||
|
||||
; Restart Key: R
|
||||
invoke GetAsyncKeyState, VK_KEY_R
|
||||
test ax, 8000h
|
||||
jz restart_released
|
||||
cmp keyRestartHeld, 0
|
||||
jne restart_sampled
|
||||
mov keyRestart, 1
|
||||
mov keyRestartHeld, 1
|
||||
jmp restart_sampled
|
||||
restart_released:
|
||||
mov keyRestartHeld, 0
|
||||
restart_sampled:
|
||||
|
||||
cmp isCutsceneActive, 0
|
||||
je normal_keys_enabled
|
||||
mov keyForward, 0
|
||||
|
|
@ -156,6 +191,7 @@ skip_key_sampled:
|
|||
mov keyTurnRight, 0
|
||||
mov keyRun, 0
|
||||
mov keyAction, 0
|
||||
mov keyAttack, 0
|
||||
normal_keys_enabled:
|
||||
; 11. Escape - Exit application
|
||||
invoke GetAsyncKeyState, VK_KEY_ESCAPE
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ ApplyScreenFade PROTO
|
|||
ValidateMapFilename PROTO :DWORD
|
||||
ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD
|
||||
CheckPortalTriggers PROTO
|
||||
InitEntities PROTO
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; InitMap: Creates a rich test level with rooms, hallways, pillars & doors
|
||||
|
|
@ -418,7 +419,9 @@ DoSublevelTransition PROC uses ebx ecx edx
|
|||
test eax, eax
|
||||
jnz transition_loaded
|
||||
mov mapErrorFrames, 180
|
||||
ret
|
||||
transition_loaded:
|
||||
call InitEntities
|
||||
ret
|
||||
DoSublevelTransition ENDP
|
||||
|
||||
|
|
@ -965,5 +968,6 @@ load_startup_map:
|
|||
jnz startup_map_ready
|
||||
call InitMap
|
||||
startup_map_ready:
|
||||
call InitEntities
|
||||
ret
|
||||
LoadMapBinary ENDP
|
||||
|
|
|
|||
10
main.asm
10
main.asm
|
|
@ -111,8 +111,10 @@ include include/engine_map.inc
|
|||
include include/engine_textures.inc
|
||||
include include/engine_input.inc
|
||||
include include/engine_physics.inc
|
||||
include include/engine_entity.inc
|
||||
include include/engine_raycaster.inc
|
||||
include include/engine_render2d.inc
|
||||
include include/engine_combat.inc
|
||||
include include/engine_cutscene.inc
|
||||
include include/engine_preview.inc
|
||||
|
||||
|
|
@ -273,10 +275,16 @@ Engine_Frame PROC uses ebx esi edi
|
|||
no_error_tick:
|
||||
call PollInput
|
||||
call UpdatePlayer
|
||||
call UpdateEntities
|
||||
call UpdateCombat
|
||||
|
||||
; 2. 3D Raycasting & 2D HUD Render
|
||||
call Render3DScene
|
||||
call Engine_RenderHUD
|
||||
cmp isCutsceneActive, 0
|
||||
jne @F
|
||||
call RenderCombatHUD
|
||||
@@:
|
||||
call ApplyCutsceneFade
|
||||
|
||||
; 2b. Screenshot request (F12): dump fresh framebuffer to BMP
|
||||
|
|
@ -320,6 +328,8 @@ frame_done:
|
|||
mov keyTurnLeft, 0
|
||||
mov keyTurnRight, 0
|
||||
mov keyAction, 0
|
||||
mov keyAttack, 0
|
||||
mov keyRestart, 0
|
||||
ret
|
||||
Engine_Frame ENDP
|
||||
|
||||
|
|
|
|||
BIN
main.obj
BIN
main.obj
Binary file not shown.
|
|
@ -16,8 +16,10 @@ include include/engine_map.inc
|
|||
include include/engine_textures.inc
|
||||
include include/engine_input.inc
|
||||
include include/engine_physics.inc
|
||||
include include/engine_entity.inc
|
||||
include include/engine_raycaster.inc
|
||||
include include/engine_render2d.inc
|
||||
include include/engine_combat.inc
|
||||
include include/engine_cutscene.inc
|
||||
include startup_parser.inc
|
||||
include project_paths.inc
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue