feat: complete Game Studio IDE, entity archetypes, combat system, and standalone exporter

This commit is contained in:
Artvasil09 2026-10-22 19:44:39 +03:00
parent a1d1baa869
commit c1da810bd5
17 changed files with 1810 additions and 6 deletions

View file

@ -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.

View file

@ -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();
}
}

View file

@ -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
View 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();
}

View file

@ -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");

View 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;
}
};

Binary file not shown.

303
include/engine_combat.inc Normal file
View 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

View file

@ -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(?)

View file

@ -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
View 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

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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