diff --git a/AGENTS.md b/AGENTS.md index 74cf1f9..fdd0d31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,22 +218,33 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi - `3: Ammo Box`: collectible ammunition pickup (+20 rounds up to 100). - `4: Key`: key card for unlocking locked doors. - `5: Switch`: interactive trigger/lever toggled by proximity action ([E]). + - `6: Explosive Barrel`: destructible barrel that explodes on weapon hit with 60 AOE damage to surrounding entities and fiery screen flash. - Packed into `SpriteStruct.flags` without altering binary map compatibility (`sizeof(SpriteEntry) == 24`). -2. **Player Combat & HUD (`include/engine_combat.inc`)**: - - Weapon attack via `Space`, `Ctrl`, or Left Mouse Button (`VK_LBUTTON`). - - Center crosshair and hitscan raycasting against active enemy guards. - - Animated retro pistol with muzzle flash, recoil, and cooldowns. +2. **Player Combat & Multi-Weapon Arsenal (`include/engine_combat.inc`)**: + - Weapon switching via keys `[1]`, `[2]`, `[3]`: + - **[1] Боевой нож (Combat Knife)**: fast melee attack (cooldown 8, 30 damage, no ammo required). + - **[2] Пистолет (Pistol)**: standard handgun (cooldown 12, 35 damage, 1 ammo). + - **[3] Дробовик (Pump Shotgun)**: heavy scatter weapon (cooldown 22, 70 damage, 2 ammo, wide spread). + - Dynamic Muzzle Flash Room Illumination: shooting dynamically brightens ceiling and floor shading across the whole 3D environment for realistic muzzle flare lighting. + - Center crosshair and hitscan raycasting against active enemy guards and barrels. - Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`. - - Comprehensive combat HUD: Health bar, Ammo counter, Score counter, and Key indicator. + - In-Game Retro Pause Menu via `[ESC]`: sleek semi-transparent pause menu with `[1] ПРОДОЛЖИТЬ`, `[2] ЗАНОВО`, `[3] МИНИ-КАРТА`, `[4] ВЫХОД В WINDOWS`. + - Comprehensive combat HUD: Active weapon badge with slot indicators `[1] [2] [3]`, Health bar, Ammo counter, Score counter, and Key indicator. 3. **In-Editor MASM32 IDE (`F7` / "Код / IDE")**: - - Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7` or the main menu. + - Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7`, `Ctrl+B`, or the main menu. - MASM syntax highlighting (instructions, registers, directives, comments, strings, numbers). - Project file navigator (`engine_entity.inc`, `engine_combat.inc`, `main.asm`, etc.). - - Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Custom HUD). + - **Engine API Reference Catalog**: built-in searchable catalog of all engine procedures with 1-click **"Вставить"** snippet insertion. + - Real-time search bar (`Поиск в коде...`) with instant match counter. + - Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Explosive Barrel, Custom HUD). - Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines. -4. **Project Settings & Standalone Game Exporter (`editor/src/project_manager.h`)**: +4. **Editor UI, 2D Canvas & Project Exporter (`editor/src/editor_app.cpp`)**: + - Modern Game Studio dark theme with Electric Cyan and Neon Amber accents. + - **2D Canvas Guard Aggro Circles**: visualizes guard detection and chase radius on the 2D map. + - **Quick Entity Spawner Palette**: 1-click spawn buttons (`+ Враг`, `+ Аптечка`, `+ Патроны`, `+ Ключ`, `+ Рычаг`, `+ Бочка`, `+ Проп`). + - Canvas Zoom buttons (`[-]`, `[100%]`, `[+]`) and pan controls. - Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map. - "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme. diff --git a/editor/editor.exe b/editor/editor.exe index 2251c98..b0bc4a7 100644 Binary files a/editor/editor.exe and b/editor/editor.exe differ diff --git a/editor/imgui.ini b/editor/imgui.ini index 44b674e..d7548d4 100644 --- a/editor/imgui.ini +++ b/editor/imgui.ini @@ -1,6 +1,6 @@ [Window][Workspace] Pos=0,60 -Size=1536,708 +Size=2560,1292 LastUsed=20261022 [Window][Debug##Default] @@ -10,12 +10,12 @@ LastUsed=20261022 [Window][Toolbar] Pos=0,20 -Size=1536,38 +Size=2560,38 LastUsed=20261022 [Window][StatusBar] -Pos=0,768 -Size=1536,32 +Pos=0,1352 +Size=2560,32 LastUsed=20261022 [Window][Не удалось запустить игру] @@ -23,3 +23,13 @@ Pos=649,356 Size=237,80 LastUsed=20261022 +[Window][Редактор кода и скриптов MASM32 - EblanStudio IDE] +Pos=60,60 +Size=900,650 +LastUsed=20261022 + +[Window][Менеджер ассетов (Texture & Audio Manager)] +Pos=60,60 +Size=820,580 +LastUsed=20261022 + diff --git a/editor/src/editor_app.cpp b/editor/src/editor_app.cpp index e4c52f4..6ff8635 100644 --- a/editor/src/editor_app.cpp +++ b/editor/src/editor_app.cpp @@ -1269,7 +1269,29 @@ void EditorApp::ApplyTool(int gx, int gy) { if (sprites.size() < MAX_LEVEL_SPRITES) { SpriteEntry sprite{}; sprite.x = gx * 64 + 32; sprite.y = gy * 64 + 32; - sprite.textureId = 11; sprite.scale = 65536; + sprite.scale = 65536; + if (pendingSpawnArchetype == ENTITY_ARCHETYPE_GUARD) { + sprite.textureId = 13; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_GUARD, 100, 2, 6); + } else if (pendingSpawnArchetype == ENTITY_ARCHETYPE_MEDKIT) { + sprite.textureId = 14; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_MEDKIT, 1, 0, 0); + } else if (pendingSpawnArchetype == ENTITY_ARCHETYPE_AMMO) { + sprite.textureId = 15; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_AMMO, 1, 0, 0); + } else if (pendingSpawnArchetype == ENTITY_ARCHETYPE_KEY) { + sprite.textureId = 10; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_KEY, 1, 0, 0); + } else if (pendingSpawnArchetype == ENTITY_ARCHETYPE_SWITCH) { + sprite.textureId = 12; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_SWITCH, 1, 0, 0); + } else if (pendingSpawnArchetype == ENTITY_ARCHETYPE_BARREL) { + sprite.textureId = 11; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_BARREL, 50, 0, 0); + } else { + sprite.textureId = 11; + SetSpriteEntityData(sprite, ENTITY_ARCHETYPE_PROP, 100, 0, 0); + } sprites.push_back(sprite); selectedSpriteIdx = static_cast(sprites.size()) - 1; selectedTransitionIdx = -1; @@ -1403,6 +1425,10 @@ void EditorApp::RenderUI() { if (ImGui::RadioButton("Рядом", viewportMode == 0)) viewportMode = 0; ImGui::SameLine(); if (ImGui::RadioButton("Игровой вид", viewportMode == 1)) viewportMode = 1; ImGui::SameLine(); if (ImGui::RadioButton("Карта", viewportMode == 2)) viewportMode = 2; + ImGui::SameLine(); ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); ImGui::SameLine(); + if (ImGui::SmallButton(" - ")) { canvasZoom = (std::max)(8.0f, canvasZoom - 4.0f); } + ImGui::SameLine(); if (ImGui::SmallButton(" 100% ")) { canvasZoom = 20.0f; canvasPanX = 20.0f; canvasPanY = 20.0f; } + ImGui::SameLine(); if (ImGui::SmallButton(" + ")) { canvasZoom = (std::min)(48.0f, canvasZoom + 4.0f); } float sceneWidth = viewportMode == 0 ? ImGui::GetContentRegionAvail().x * 0.5f : 0; if (viewportMode != 2) { ImGui::BeginChild("SceneViewport", ImVec2(sceneWidth,0), true); @@ -1745,12 +1771,22 @@ void EditorApp::RenderGridCanvas() { // Color diamond based on Entity Archetype uint8_t aType = GetSpriteEntityType(sprites[i]); + + // Draw Guard Aggro / Detection circle on the map + if (aType == ENTITY_ARCHETYPE_GUARD) { + uint8_t agg = GetSpriteAggro(sprites[i]); + float aggRadius = agg * tileSize; + drawList->AddCircle(scPos, aggRadius, IM_COL32(235, 45, 45, 60), 32, 1.5f); + drawList->AddCircleFilled(scPos, aggRadius, IM_COL32(235, 45, 45, 12)); + } + 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) + else if (aType == ENTITY_ARCHETYPE_BARREL) fillCol = IM_COL32(255, 90, 20, 240); // Dark Orange (Barrel) ImU32 borderCol = IM_COL32(255, 255, 255, 255); if (static_cast(i) == selectedSpriteIdx) { @@ -1947,6 +1983,36 @@ void EditorApp::RenderTilePalette() { PaletteButton("9. Мрамор", CELL_MARBLE, ImVec4(0.80f, 0.77f, 0.73f, 1.0f), 9); PaletteButton("10. Портал перехода", CELL_PORTAL, ImVec4(0.60f, 0.20f, 0.85f, 1.0f), 10); PaletteButton("0. Пустота (Пол)", CELL_EMPTY, ImVec4(0.15f, 0.15f, 0.15f, 1.0f), 0); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "СПАВН СУЩНОСТЕЙ"); + ImGui::Separator(); + + auto SpawnerBtn = [&](const char* title, uint8_t arch, ImVec4 col) { + bool active = (currentTool == TOOL_SPRITE_SPAWN && pendingSpawnArchetype == arch); + if (active) { + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + } + ImGui::PushStyleColor(ImGuiCol_Button, col); + if (ImGui::Button(title, ImVec2(-1, 24))) { + currentTool = TOOL_SPRITE_SPAWN; + pendingSpawnArchetype = arch; + } + ImGui::PopStyleColor(); + if (active) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + }; + + SpawnerBtn("+ Враг (Guard AI)", ENTITY_ARCHETYPE_GUARD, ImVec4(0.65f, 0.15f, 0.15f, 1.0f)); + SpawnerBtn("+ Аптечка (+25 HP)", ENTITY_ARCHETYPE_MEDKIT, ImVec4(0.15f, 0.55f, 0.20f, 1.0f)); + SpawnerBtn("+ Патроны (+20)", ENTITY_ARCHETYPE_AMMO, ImVec4(0.65f, 0.55f, 0.10f, 1.0f)); + SpawnerBtn("+ Ключ от двери", ENTITY_ARCHETYPE_KEY, ImVec4(0.15f, 0.50f, 0.70f, 1.0f)); + SpawnerBtn("+ Переключатель [E]", ENTITY_ARCHETYPE_SWITCH, ImVec4(0.70f, 0.40f, 0.10f, 1.0f)); + SpawnerBtn("+ Бочка (Взрывная)", ENTITY_ARCHETYPE_BARREL, ImVec4(0.70f, 0.25f, 0.10f, 1.0f)); + SpawnerBtn("+ Декорация (Проп)", ENTITY_ARCHETYPE_PROP, ImVec4(0.35f, 0.35f, 0.40f, 1.0f)); } void EditorApp::RenderHierarchy() { @@ -1967,6 +2033,7 @@ void EditorApp::RenderHierarchy() { else if (aType == ENTITY_ARCHETYPE_AMMO) typeTag = "+Ammo"; else if (aType == ENTITY_ARCHETYPE_KEY) typeTag = "Ключ"; else if (aType == ENTITY_ARCHETYPE_SWITCH) typeTag = "Рычаг"; + else if (aType == ENTITY_ARCHETYPE_BARREL) typeTag = "Бочка"; char label[64]; snprintf(label, sizeof(label), "[%s] #%zu (%d, %d)", typeTag, i + 1, gx, gy); @@ -2087,20 +2154,21 @@ void EditorApp::RenderInspector() { // Entity Archetype & Logic ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Геймплей / Архетип сущности:"); - static const char* s_ArchetypeNames[6] = { + static const char* s_ArchetypeNames[7] = { "0: Декорация / Проп (Static)", "1: Враг (Enemy Guard AI)", "2: Аптечка (+25 HP)", "3: Патроны (+20 Ammo)", "4: Ключ от двери (Key Pickup)", - "5: Переключатель / Рычаг (Switch)" + "5: Переключатель / Рычаг (Switch)", + "6: Взрывная бочка (Barrel AOE)" }; int archType = static_cast(GetSpriteEntityType(spr)); int hp = static_cast(GetSpriteHealth(spr)); int spd = static_cast(GetSpriteSpeed(spr)); int agg = static_cast(GetSpriteAggro(spr)); - if (ImGui::Combo("Тип сущности", &archType, s_ArchetypeNames, 6)) { + if (ImGui::Combo("Тип сущности", &archType, s_ArchetypeNames, 7)) { PushUndo(); SetSpriteEntityData(spr, static_cast(archType), static_cast(hp), static_cast(spd), static_cast(agg)); } @@ -2123,6 +2191,8 @@ void EditorApp::RenderInspector() { 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] рядом с объектом"); + } else if (archType == ENTITY_ARCHETYPE_BARREL) { + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.15f, 1.0f), "Взрывается от выстрелов, нанося 60 AOE урона всем вокруг!"); } if (ImGui::Button("Редактировать логику в Редакторе кода (F7)")) { diff --git a/editor/src/editor_app.h b/editor/src/editor_app.h index 049db77..b1515d3 100644 --- a/editor/src/editor_app.h +++ b/editor/src/editor_app.h @@ -96,6 +96,7 @@ public: // Editor state ToolMode currentTool = TOOL_BRUSH; CellType selectedPaletteCell = CELL_BRICK; + uint8_t pendingSpawnArchetype = ENTITY_ARCHETYPE_PROP; int selectedSpriteIdx = -1; int selectedTransitionIdx = -1; bool isEngineRunning = false; diff --git a/editor/src/editor_code.inl b/editor/src/editor_code.inl index 49f7b97..c0abd82 100644 --- a/editor/src/editor_code.inl +++ b/editor/src/editor_code.inl @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "../imgui/imgui.h" @@ -17,14 +18,16 @@ struct CodeFileEntry { }; static const CodeFileEntry g_ProjectCodeFiles[] = { - { "engine_entity.inc (Entity AI & Pickups)", "include/engine_entity.inc" }, - { "engine_combat.inc (Player Weapons & Combat)", "include/engine_combat.inc" }, - { "main.asm (Engine Entry & Main Loop)", "main.asm" }, - { "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" }, + { "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" }, + { "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" }, + { "main.asm (Engine Entry & Frame Loop)", "main.asm" }, { "engine_defs.inc (Constants & Structs)", "include/engine_defs.inc" }, + { "engine_data.inc (Memory & Game State)", "include/engine_data.inc" }, + { "engine_map.inc (World Grid & Doors)", "include/engine_map.inc" }, { "engine_render2d.inc (2D Canvas & HUD)", "include/engine_render2d.inc" }, - { "engine_input.inc (Keyboard Input)", "include/engine_input.inc" }, - { "engine_physics.inc (Player Physics)", "include/engine_physics.inc" } + { "engine_raycaster.inc (3D DDA Raycaster)", "include/engine_raycaster.inc" }, + { "engine_input.inc (Keyboard & Mouse)", "include/engine_input.inc" }, + { "engine_physics.inc (Player Movement)", "include/engine_physics.inc" } }; static const int g_CodeFileCount = sizeof(g_ProjectCodeFiles) / sizeof(g_ProjectCodeFiles[0]); @@ -78,6 +81,15 @@ static const ScriptTemplate g_ScriptTemplates[] = { " invoke SetMapCell, switchTargetX, switchTargetY, CELL_DOOR\n" "switch_done:\n" }, + { + "Взрывная бочка (AOE Explosion Trigger)", + "; ==============================================================================\n" + "; Explosive Barrel: Deals 60 AOE damage and triggers fiery screen flash\n" + "; ==============================================================================\n" + " mov [entityState + ecx*4], ENTITY_STATE_DEAD\n" + " mov playerHitFlash, 8\n" + " add playerScore, 200\n" + }, { "Кастомный элемент HUD (Custom Gauge Widget)", "; ==============================================================================\n" @@ -90,6 +102,34 @@ static const ScriptTemplate g_ScriptTemplates[] = { }; static const int g_ScriptTemplateCount = sizeof(g_ScriptTemplates) / sizeof(g_ScriptTemplates[0]); +struct EngineApiItem { + const char* category; + const char* name; + const char* description; + const char* snippet; +}; + +static const EngineApiItem g_EngineApiCatalog[] = { + { "Бой и оружие", "UpdateCombat", "Обновляет прицел, стрельбу и расчет урона", "call UpdateCombat\n" }, + { "Бой и оружие", "RenderCombatHUD", "Рисует оружие, прицел и статус игрока", "call RenderCombatHUD\n" }, + { "Бой и оружие", "Смена оружия", "Устанавливает активное оружие игрока", "mov playerWeapon, WEAPON_SHOTGUN ; 0=Knife, 1=Pistol, 2=Shotgun\n" }, + { "Бой и оружие", "Нанесение урона", "Уменьшает здоровье врага по индексу", "mov ecx, targetIdx\nmov eax, [entityHealth + ecx*4]\nsub eax, 35\nmov [entityHealth + ecx*4], eax\n" }, + + { "Сущности и AI", "InitEntities", "Сбрасывает и спавнит сущности уровня", "call InitEntities\n" }, + { "Сущности и AI", "UpdateEntities", "AI преследования, патруля и подбор предметов", "call UpdateEntities\n" }, + { "Сущности и AI", "Патруль врага", "Инкремент таймера контрольной точки патруля", "mov ecx, [entityTimer + edx*4]\ninc ecx\nmov [entityTimer + edx*4], ecx\n" }, + + { "2D Рендеринг", "DrawRect", "Рисует закрашенный прямоугольник (X, Y, W, H, Color)", "invoke DrawRect, 10, 20, 80, 12, 00336699h\n" }, + { "2D Рендеринг", "DrawRectOutline", "Рисует рамку прямоугольника", "invoke DrawRectOutline, 10, 20, 80, 12, COLOR_WHITE\n" }, + { "2D Рендеринг", "DrawString8x8", "Выводит строку шрифтом 8x8 (CP1251)", "invoke DrawString8x8, 10, 22, OFFSET myString, COLOR_YELLOW\n" }, + { "2D Рендеринг", "DrawNumber8x8", "Выводит целое число на экран", "invoke DrawNumber8x8, 60, 22, myNumberVar, COLOR_WHITE\n" }, + + { "Рэйкастинг и Мир", "tableCos / tableSin", "Чтение тригонометрии 16.16 (угол 0..511)", "mov eax, playerAngle\nand eax, ANGLE_MASK\nmov ebx, [tableCos + eax*4]\nmov edx, [tableSin + eax*4]\n" }, + { "Рэйкастинг и Мир", "depthBuffer", "Глубина стен для спрайтового z-теста", "mov eax, [depthBuffer + screenX*4]\n" }, + { "Рэйкастинг и Мир", "CheckWallCollision", "Проверка коллизии с ячейкой карты", "invoke CheckWallCollision, targetX, targetY\n" } +}; +static const int g_EngineApiCount = sizeof(g_EngineApiCatalog) / sizeof(g_EngineApiCatalog[0]); + struct CodeEditorState { int currentFile = 0; std::string fileContent; @@ -105,6 +145,10 @@ struct CodeEditorState { int buildExitCode = 0; bool hasBuildResult = false; + // Search bar state + char searchBuf[64] = ""; + int searchMatchCount = 0; + struct ParsedError { std::string file; int line = 0; @@ -178,8 +222,9 @@ struct CodeEditorState { buildLog.clear(); buildErrors.clear(); hasBuildResult = false; + buildExitCode = 0; - fs::path root(projectRoot); + fs::path root = fs::path(projectRoot); fs::path buildBat = root / "build.bat"; if (!fs::exists(buildBat)) { buildBat = root.parent_path() / "build.bat"; @@ -187,7 +232,7 @@ struct CodeEditorState { } if (!fs::exists(buildBat)) { - buildLog = "[ERROR] Файл build.bat не найден в корне проекта!\n"; + buildLog = "[ERROR] Не найден скрипт сборки build.bat в: " + buildBat.string() + "\n"; hasBuildResult = true; buildExitCode = 1; return; @@ -299,14 +344,13 @@ struct CodeEditorState { if (!std::isdigit(static_cast(c))) { isNum = false; break; } } if (isNum) { - ParsedError err{}; + ParsedError err; err.file = line.substr(0, openParen); err.line = std::atoi(lineStr.c_str()); err.message = line.substr(colon + 3); buildErrors.push_back(err); } } - if (end == std::string::npos) break; start = end + 1; } @@ -320,25 +364,23 @@ void EditorApp::RenderCodeEditorWindow() { g_CodeEditor.PollBuild(); - ImGui::SetNextWindowSize(ImVec2(900, 650), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Редактор кода и скриптов MASM32 - EblanStudio IDE", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) { + ImGui::SetNextWindowSize(ImVec2(1020, 680), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Редактор кода MASM32 & Игровых скриптов (F7)", &showCodeEditorModal, ImGuiWindowFlags_MenuBar)) { ImGui::End(); return; } - // Top Menu Bar + char module[MAX_PATH]{}; + GetModuleFileNameA(nullptr, module, MAX_PATH); + std::string root = fs::path(module).parent_path().string(); + + // Menu Bar if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("Файл")) { if (ImGui::MenuItem("Сохранить (Ctrl+S)", "Ctrl+S")) { - char module[MAX_PATH]{}; - GetModuleFileNameA(nullptr, module, MAX_PATH); - std::string root = fs::path(module).parent_path().string(); g_CodeEditor.SaveFile(root); } - if (ImGui::MenuItem("Собрать движок (F7)", "F7")) { - char module[MAX_PATH]{}; - GetModuleFileNameA(nullptr, module, MAX_PATH); - std::string root = fs::path(module).parent_path().string(); + if (ImGui::MenuItem("Собрать проект (F7 / Ctrl+B)", "Ctrl+B")) { g_CodeEditor.StartBuild(root); } ImGui::Separator(); @@ -351,7 +393,6 @@ void EditorApp::RenderCodeEditorWindow() { if (ImGui::BeginMenu("Шаблоны кода")) { for (int i = 0; i < g_ScriptTemplateCount; ++i) { if (ImGui::MenuItem(g_ScriptTemplates[i].title)) { - // Append template code to text buffer size_t curLen = std::strlen(g_CodeEditor.textBuffer.data()); size_t tLen = std::strlen(g_ScriptTemplates[i].code); g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024); @@ -366,12 +407,8 @@ void EditorApp::RenderCodeEditorWindow() { ImGui::EndMenuBar(); } - // Toolbar Buttons + // Top Toolbar 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); } @@ -381,54 +418,113 @@ void EditorApp::RenderCodeEditorWindow() { ImGui::Button("Компиляция..."); ImGui::PopStyleColor(); } else { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.2f, 1.0f)); - if (ImGui::Button("Собрать движок (F7)")) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.55f, 0.22f, 1.0f)); + if (ImGui::Button("Собрать проект (F7)")) { g_CodeEditor.StartBuild(root); } ImGui::PopStyleColor(); } + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + + // Search bar + ImGui::SetNextItemWidth(200); + if (ImGui::InputTextWithHint("##SearchCode", "Поиск в коде...", g_CodeEditor.searchBuf, sizeof(g_CodeEditor.searchBuf))) { + if (g_CodeEditor.searchBuf[0]) { + int count = 0; + const char* p = g_CodeEditor.textBuffer.data(); + while ((p = std::strstr(p, g_CodeEditor.searchBuf)) != nullptr) { + count++; + p += std::strlen(g_CodeEditor.searchBuf); + } + g_CodeEditor.searchMatchCount = count; + } else { + g_CodeEditor.searchMatchCount = 0; + } + } + if (g_CodeEditor.searchBuf[0]) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "[%d совп.]", g_CodeEditor.searchMatchCount); + } + ImGui::SameLine(); ImGui::TextDisabled("| %s", g_CodeEditor.statusText.c_str()); ImGui::EndGroup(); ImGui::Separator(); - // Split Left (File list) and Right (Editor + Output) + // Split Left (Navigator & API Catalog) and Right (Editor + Output) ImGui::Columns(2, "CodeEditorColumns", true); static bool initCol = true; if (initCol) { - ImGui::SetColumnWidth(0, 240); + ImGui::SetColumnWidth(0, 270); initCol = false; - // Load initial file g_CodeEditor.LoadFile(0, root); } - // Left Column: Source File List - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:"); - for (int i = 0; i < g_CodeFileCount; ++i) { - bool isSelected = (g_CodeEditor.currentFile == i); - if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) { - if (g_CodeEditor.isModified) { - g_CodeEditor.SaveFile(root); + // Left Column: Tabbed Navigation (Files / API Catalog / Templates) + ImGui::BeginTabBar("NavTabs"); + + if (ImGui::BeginTabItem("Файлы")) { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Исходные файлы проекта:"); + for (int i = 0; i < g_CodeFileCount; ++i) { + bool isSelected = (g_CodeEditor.currentFile == i); + if (ImGui::Selectable(g_ProjectCodeFiles[i].displayName.c_str(), isSelected)) { + if (g_CodeEditor.isModified) { + g_CodeEditor.SaveFile(root); + } + g_CodeEditor.LoadFile(i, root); } - g_CodeEditor.LoadFile(i, root); } + ImGui::EndTabItem(); } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Быстрые шаблоны:"); - for (int i = 0; i < g_ScriptTemplateCount; ++i) { - if (ImGui::SmallButton(g_ScriptTemplates[i].title)) { - size_t curLen = std::strlen(g_CodeEditor.textBuffer.data()); - size_t tLen = std::strlen(g_ScriptTemplates[i].code); - g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024); - std::strcat(g_CodeEditor.textBuffer.data(), "\n"); - std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code); - g_CodeEditor.isModified = true; + if (ImGui::BeginTabItem("Справочник API")) { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.2f, 0.85f, 1.0f, 1.0f), "Функции и процедуры движка:"); + ImGui::BeginChild("ApiCatalogChild", ImVec2(0, 0), false); + for (int i = 0; i < g_EngineApiCount; ++i) { + ImGui::PushID(i); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%s", g_EngineApiCatalog[i].name); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 65); + if (ImGui::SmallButton("Вставить")) { + size_t curLen = std::strlen(g_CodeEditor.textBuffer.data()); + size_t sLen = std::strlen(g_EngineApiCatalog[i].snippet); + g_CodeEditor.EnsureBufferSize(curLen + sLen + 1024); + std::strcat(g_CodeEditor.textBuffer.data(), "\n"); + std::strcat(g_CodeEditor.textBuffer.data(), g_EngineApiCatalog[i].snippet); + g_CodeEditor.isModified = true; + g_CodeEditor.statusText = std::string("Вставлен код: ") + g_EngineApiCatalog[i].name; + } + ImGui::TextDisabled(" %s", g_EngineApiCatalog[i].description); + ImGui::Separator(); + ImGui::PopID(); } + ImGui::EndChild(); + ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Шаблоны")) { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.5f, 1.0f), "Быстрые шаблоны скриптов:"); + for (int i = 0; i < g_ScriptTemplateCount; ++i) { + if (ImGui::Button(g_ScriptTemplates[i].title, ImVec2(-1, 26))) { + size_t curLen = std::strlen(g_CodeEditor.textBuffer.data()); + size_t tLen = std::strlen(g_ScriptTemplates[i].code); + g_CodeEditor.EnsureBufferSize(curLen + tLen + 1024); + std::strcat(g_CodeEditor.textBuffer.data(), "\n"); + std::strcat(g_CodeEditor.textBuffer.data(), g_ScriptTemplates[i].code); + g_CodeEditor.isModified = true; + g_CodeEditor.statusText = std::string("Вставлен шаблон: ") + g_ScriptTemplates[i].title; + } + } + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + ImGui::NextColumn(); // Right Column: Code Editor & Compiler Console @@ -436,7 +532,6 @@ void EditorApp::RenderCodeEditorWindow() { float editorHeight = availY * 0.65f; float outputHeight = availY - editorHeight - 20; - // Multiline Text Editor with monospace font ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Файл: %s %s", g_ProjectCodeFiles[g_CodeEditor.currentFile].relativePath.c_str(), g_CodeEditor.isModified ? "*" : ""); @@ -453,10 +548,10 @@ void EditorApp::RenderCodeEditorWindow() { } ImGui::PopStyleColor(); - // Hotkey: Ctrl+S to save - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S)) { - g_CodeEditor.SaveFile(root); + // Hotkey: Ctrl+S to save, Ctrl+B to build + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::GetIO().KeyCtrl) { + if (ImGui::IsKeyPressed(ImGuiKey_S)) g_CodeEditor.SaveFile(root); + if (ImGui::IsKeyPressed(ImGuiKey_B)) g_CodeEditor.StartBuild(root); } ImGui::Separator(); @@ -466,9 +561,8 @@ void EditorApp::RenderCodeEditorWindow() { if (ImGui::BeginTabItem("Вывод компилятора (ml.exe)")) { ImGui::BeginChild("CompilerOutputChild", ImVec2(0, outputHeight), true); if (g_CodeEditor.buildLog.empty() && !g_CodeEditor.isBuilding) { - ImGui::TextDisabled("Нажмите 'Собрать движок (F7)' для компиляции ассемблерного кода..."); + ImGui::TextDisabled("Нажмите 'Собрать проект (F7)' для компиляции ассемблерного кода..."); } else { - // Display log line by line with color tags size_t logStart = 0; while (logStart < g_CodeEditor.buildLog.size()) { size_t logEnd = g_CodeEditor.buildLog.find('\n', logStart); @@ -508,7 +602,6 @@ void EditorApp::RenderCodeEditorWindow() { snprintf(lineNumBuf, sizeof(lineNumBuf), "%d", err.line); std::string label = err.file + ":" + lineNumBuf + " - " + err.message; if (ImGui::Selectable(label.c_str())) { - // Switch to corresponding file if matched for (int f = 0; f < g_CodeFileCount; ++f) { if (err.file.find(g_ProjectCodeFiles[f].relativePath) != std::string::npos || g_ProjectCodeFiles[f].relativePath.find(err.file) != std::string::npos) { diff --git a/editor/src/level_format.h b/editor/src/level_format.h index 7a3c6b3..b28592c 100644 --- a/editor/src/level_format.h +++ b/editor/src/level_format.h @@ -48,7 +48,8 @@ enum EntityArchetype : uint8_t { ENTITY_ARCHETYPE_AMMO = 3, // Ammo pickup (+20 Ammo) ENTITY_ARCHETYPE_KEY = 4, // Door Key pickup ENTITY_ARCHETYPE_SWITCH = 5, // Interactive Switch / Trigger - ENTITY_ARCHETYPE_COUNT = 6 + ENTITY_ARCHETYPE_BARREL = 6, // Explosive Barrel (AOE Blast) + ENTITY_ARCHETYPE_COUNT = 7 }; #pragma pack(push, 1) @@ -104,6 +105,7 @@ struct SpriteEntry { inline uint8_t GetSpriteEntityType(const SpriteEntry& s) { uint8_t t = static_cast(s.flags & 0xFF); if (t == 0) { + if (s.textureId == 11) return ENTITY_ARCHETYPE_BARREL; if (s.textureId == 13) return ENTITY_ARCHETYPE_GUARD; if (s.textureId == 14) return ENTITY_ARCHETYPE_MEDKIT; if (s.textureId == 15) return ENTITY_ARCHETYPE_AMMO; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 1cb101c..0ab5474 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -30,46 +30,51 @@ void SetupGodotStyle() { ImGuiStyle& style = ImGui::GetStyle(); ImVec4* colors = style.Colors; - style.WindowRounding = 4.0f; - style.FrameRounding = 3.0f; - style.PopupRounding = 4.0f; - style.ScrollbarRounding = 4.0f; - style.GrabRounding = 3.0f; - style.TabRounding = 4.0f; + style.WindowRounding = 6.0f; + style.ChildRounding = 5.0f; + style.FrameRounding = 4.0f; + style.PopupRounding = 6.0f; + style.ScrollbarRounding = 6.0f; + style.GrabRounding = 4.0f; + style.TabRounding = 5.0f; style.FrameBorderSize = 1.0f; style.WindowBorderSize = 1.0f; + style.WindowPadding = ImVec2(10.0f, 10.0f); + style.FramePadding = ImVec2(7.0f, 4.0f); + style.ItemSpacing = ImVec2(8.0f, 6.0f); + style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); - // Dark graphite palette (Godot 4 inspired) - colors[ImGuiCol_Text] = ImVec4(0.92f, 0.92f, 0.95f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.52f, 0.55f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.14f, 0.15f, 0.17f, 1.00f); - colors[ImGuiCol_ChildBg] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); - colors[ImGuiCol_Border] = ImVec4(0.24f, 0.25f, 0.28f, 1.00f); + // Modern Game Studio Dark Slate Palette with Electric Cyan & Neon Amber accents + colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.52f, 0.55f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.14f, 0.15f, 0.18f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.22f, 0.24f, 0.28f, 1.00f); colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.21f, 0.24f, 1.00f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.30f, 0.34f, 1.00f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f); - colors[ImGuiCol_TitleBg] = ImVec4(0.11f, 0.12f, 0.13f, 1.00f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.11f, 0.12f, 0.14f, 1.00f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.11f, 0.12f, 0.60f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.27f, 0.30f, 1.00f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.35f, 0.38f, 0.42f, 1.00f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.45f, 0.48f, 0.52f, 1.00f); - colors[ImGuiCol_CheckMark] = ImVec4(0.30f, 0.70f, 1.00f, 1.00f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.30f, 0.65f, 0.95f, 1.00f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.40f, 0.75f, 1.00f, 1.00f); - colors[ImGuiCol_Button] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.33f, 0.38f, 1.00f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f); - colors[ImGuiCol_Header] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.38f, 0.42f, 0.48f, 1.00f); - colors[ImGuiCol_Separator] = ImVec4(0.22f, 0.24f, 0.27f, 1.00f); - colors[ImGuiCol_Tab] = ImVec4(0.16f, 0.18f, 0.20f, 1.00f); - colors[ImGuiCol_TabHovered] = ImVec4(0.32f, 0.35f, 0.40f, 1.00f); - colors[ImGuiCol_TabActive] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.20f, 0.24f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.24f, 0.28f, 0.34f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.33f, 0.40f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.14f, 0.16f, 0.20f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.80f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.27f, 0.33f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.32f, 0.37f, 0.45f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.40f, 0.46f, 0.56f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.20f, 0.75f, 1.00f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.20f, 0.70f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.35f, 0.82f, 1.00f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.20f, 0.23f, 0.28f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.34f, 0.42f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.34f, 0.42f, 0.52f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.20f, 0.24f, 0.30f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.34f, 0.42f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.34f, 0.42f, 0.52f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); + colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.16f, 0.19f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.33f, 0.40f, 1.00f); + colors[ImGuiCol_TabActive] = ImVec4(0.22f, 0.26f, 0.33f, 1.00f); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { diff --git a/engine.exe b/engine.exe index 5c08f55..34d4c7c 100644 Binary files a/engine.exe and b/engine.exe differ diff --git a/include/engine_combat.inc b/include/engine_combat.inc index 814a07f..1542c9a 100644 --- a/include/engine_combat.inc +++ b/include/engine_combat.inc @@ -1,5 +1,5 @@ ; ============================================================================== -; EBLAN ENGINE - Player Combat, Weapon Shooting & Combat HUD +; EBLAN ENGINE - Player Combat, Multi-Weapon Arsenal & Combat HUD ; ============================================================================== .code @@ -19,6 +19,7 @@ UpdateCombat PROC uses ebx ecx edx esi edi LOCAL sprX:DWORD, sprY:DWORD LOCAL transX:DWORD, transY:DWORD LOCAL sprScreenX:DWORD, wallDepth:DWORD + LOCAL weaponDamage:DWORD, maxHitDist:DWORD, hitTolerance:DWORD ; 1. Decrement cooldowns cmp playerAttackCooldown, 0 @@ -34,9 +35,11 @@ UpdateCombat PROC uses ebx ecx edx esi edi dec playerHitFlash @@: - ; If player is dead, cannot attack + ; If player is dead or game is paused, cannot attack cmp isPlayerDead, 0 jne combat_done + cmp isGamePaused, 0 + jne combat_done ; 2. Check attack input cmp keyAttack, 0 @@ -44,16 +47,51 @@ UpdateCombat PROC uses ebx ecx edx esi edi cmp playerAttackCooldown, 0 jne combat_done - ; Check ammo - cmp playerAmmo, 0 - jle combat_dry_fire + ; Set weapon parameters based on playerWeapon + mov weaponDamage, 35 + mov maxHitDist, 999999 + mov hitTolerance, 22 - ; Fire weapon! + cmp playerWeapon, WEAPON_KNIFE + je setup_knife + cmp playerWeapon, WEAPON_SHOTGUN + je setup_shotgun + jmp setup_pistol + +setup_knife: + ; Knife: no ammo needed, close range (within 64 units = 1 cell), fast cooldown + mov weaponDamage, 30 + mov maxHitDist, 64 + mov hitTolerance, 32 + mov playerAttackCooldown, 8 + mov playerFlashTimer, 3 + jmp fire_hitscan + +setup_shotgun: + ; Shotgun: needs 2 ammo, high damage (70), wide spread (hitTolerance 40), long cooldown + cmp playerAmmo, 2 + jl combat_dry_fire + sub playerAmmo, 2 + mov weaponDamage, 70 + mov maxHitDist, 999999 + mov hitTolerance, 40 + mov playerAttackCooldown, 22 + mov playerFlashTimer, 7 + jmp fire_hitscan + +setup_pistol: + ; Pistol: needs 1 ammo, accurate (hitTolerance 22), medium cooldown (12) + cmp playerAmmo, 1 + jl combat_dry_fire dec playerAmmo - mov playerAttackCooldown, 10 + mov weaponDamage, 35 + mov maxHitDist, 999999 + mov hitTolerance, 22 + mov playerAttackCooldown, 12 mov playerFlashTimer, 5 - ; 3. Hitscan check against enemies in center crosshair +fire_hitscan: + ; 3. Hitscan check against enemies & destructible barrels in crosshair ; Camera basis for -playerAngle mov eax, ANGLE_512 sub eax, playerAngle @@ -78,9 +116,24 @@ hitscan_loop: mov ecx, i mov eax, [entityType + ecx*4] - cmp eax, ENTITY_TYPE_GUARD - jne next_hitscan_target + ; Check if Guard or Barrel + cmp eax, ENTITY_TYPE_GUARD + je hitscan_target_valid + cmp eax, ENTITY_TYPE_BARREL + je hitscan_target_valid + + ; Also check if prop has barrel texture (11) + mov ebx, i + imul ebx, sizeof SpriteStruct + add ebx, OFFSET spriteList + mov eax, [ebx].SpriteStruct.textureId + cmp eax, 11 + je hitscan_target_valid + + jmp next_hitscan_target + +hitscan_target_valid: mov eax, [entityState + ecx*4] cmp eax, ENTITY_STATE_DEAD je next_hitscan_target @@ -117,6 +170,11 @@ hitscan_loop: cmp transY, 16 jle next_hitscan_target + ; Must be closer than maximum weapon hit distance + mov eax, transY + cmp eax, maxHitDist + jg next_hitscan_target + ; Must be closer than the wall at center screen mov eax, transY cmp eax, wallDepth @@ -141,12 +199,12 @@ hitscan_loop: add eax, SCREEN_HALF_W mov sprScreenX, eax - ; Crosshair hitbox: within 22 pixels of center (SCREEN_HALF_W = 160) + ; Crosshair hitbox check sub eax, SCREEN_HALF_W cdq xor eax, edx sub eax, edx ; abs(sprScreenX - 160) - cmp eax, 22 + cmp eax, hitTolerance jg next_hitscan_target ; Check if closer than previous target @@ -165,10 +223,21 @@ hitscan_eval: cmp closestIdx, -1 je combat_done - ; Apply damage to hit guard mov ecx, closestIdx + mov eax, [entityType + ecx*4] + + ; Check if hit an explosive barrel! + cmp eax, ENTITY_TYPE_BARREL + je explode_barrel + mov ebx, closestIdx + imul ebx, sizeof SpriteStruct + add ebx, OFFSET spriteList + cmp [ebx].SpriteStruct.textureId, 11 + je explode_barrel + + ; Hit an enemy guard! mov eax, [entityHealth + ecx*4] - sub eax, 35 + sub eax, weaponDamage jns @F xor eax, eax @@: @@ -181,7 +250,7 @@ hitscan_eval: mov DWORD PTR [entityTimer + ecx*4], 0 add playerScore, 100 - ; Drop 10 ammo reward + ; Drop ammo reward mov eax, playerAmmo add eax, 10 cmp eax, playerMaxAmmo @@ -191,6 +260,111 @@ hitscan_eval: mov playerAmmo, eax jmp combat_done +explode_barrel: + ; Exploding Barrel: Destroy barrel and trigger AOE blast! + mov [entityState + ecx*4], ENTITY_STATE_DEAD + mov ebx, closestIdx + imul ebx, sizeof SpriteStruct + add ebx, OFFSET spriteList + mov [ebx].SpriteStruct.textureId, 0 ; disappear + + ; Fiery orange-red screen flash + mov playerHitFlash, 8 + add playerScore, 200 + + ; AOE damage to all surrounding guards within 128 units (2 cells) + push esi + push edi + mov esi, [ebx].SpriteStruct.x + mov edi, [ebx].SpriteStruct.y + + mov i, 0 +aoe_loop: + mov eax, i + cmp eax, spriteCount + jge aoe_check_player + + mov ecx, i + cmp ecx, closestIdx + je next_aoe + + mov eax, [entityType + ecx*4] + cmp eax, ENTITY_TYPE_GUARD + jne next_aoe + cmp [entityState + ecx*4], ENTITY_STATE_DEAD + je next_aoe + + mov edx, i + imul edx, sizeof SpriteStruct + add edx, OFFSET spriteList + + mov eax, [edx].SpriteStruct.x + sub eax, esi + cdq + xor eax, edx + sub eax, edx + cmp eax, 128 + jg next_aoe + + mov eax, [edx].SpriteStruct.y + sub eax, edi + cdq + xor eax, edx + sub eax, edx + cmp eax, 128 + jg next_aoe + + ; Within blast: deal 60 blast damage! + mov eax, [entityHealth + ecx*4] + sub eax, 60 + jns @F + xor eax, eax +@@: + mov [entityHealth + ecx*4], eax + test eax, eax + jnz next_aoe + + mov [entityState + ecx*4], ENTITY_STATE_DEAD + add playerScore, 150 + +next_aoe: + inc i + jmp aoe_loop + +aoe_check_player: + ; If player was standing too close to the barrel (< 96 units), player takes blast damage! + mov eax, playerX + sub eax, esi + cdq + xor eax, edx + sub eax, edx + cmp eax, 96 + jg aoe_done + + mov eax, playerY + sub eax, edi + cdq + xor eax, edx + sub eax, edx + cmp eax, 96 + jg aoe_done + + ; Player blast damage + mov eax, playerHealth + sub eax, 25 + jns @F + xor eax, eax +@@: + mov playerHealth, eax + test eax, eax + jnz aoe_done + mov isPlayerDead, 1 + +aoe_done: + pop edi + pop esi + jmp combat_done + combat_dry_fire: mov playerAttackCooldown, 12 @@ -209,19 +383,75 @@ RenderCombatHUD PROC uses ebx ecx edx esi edi invoke DrawRect, 160, 116, 1, 9, 00FFFFFFh invoke DrawRect, 160, 120, 1, 1, 0000FF00h - ; 2. Render Retro Weapon at bottom center + ; 2. Render Active Weapon at bottom center + cmp playerWeapon, WEAPON_KNIFE + je render_knife_weapon + cmp playerWeapon, WEAPON_SHOTGUN + je render_shotgun_weapon + jmp render_pistol_weapon + +render_knife_weapon: + ; Knife Combat Animation: thrusts forward when cooldown active + mov gunY, 190 + cmp playerFlashTimer, 0 + je draw_knife + sub gunY, 12 ; thrust forward! + +draw_knife: + ; Silver steel blade + invoke DrawRect, 166, gunY, 6, 26, 00D0D0D0h + invoke DrawRect, 168, gunY, 2, 26, 00FFFFFFh ; shine + invoke DrawRect, 167, gunY - 4, 4, 4, 00B0B0B0h ; blade tip + invoke DrawRect, 168, gunY - 7, 2, 3, 00E0E0E0h + ; Brass crossguard + mov eax, gunY + add eax, 26 + invoke DrawRect, 160, eax, 18, 5, 00D4AF37h + ; Wooden grip + mov eax, gunY + add eax, 31 + invoke DrawRect, 164, eax, 10, 22, 00502A10h + jmp hud_stats_draw + +render_shotgun_weapon: + ; Heavy Pump Shotgun + mov gunY, 188 + cmp playerFlashTimer, 0 + je draw_shotgun_barrels + add gunY, 4 ; heavy recoil! + + ; Massive dual blast flash! + invoke DrawRect, 146, 155, 28, 22, 00FFAA00h + invoke DrawRect, 150, 158, 20, 16, 00FFFF00h + invoke DrawRect, 154, 162, 12, 10, 00FFFFFFh + +draw_shotgun_barrels: + ; Twin heavy steel barrels + invoke DrawRect, 148, gunY, 10, 32, 002B2B2Bh + invoke DrawRect, 150, gunY, 6, 32, 00484848h + invoke DrawRect, 162, gunY, 10, 32, 002B2B2Bh + invoke DrawRect, 164, gunY, 6, 32, 00484848h + ; Barrel rib connector + invoke DrawRect, 158, gunY, 4, 32, 00222222h + ; Wooden pump forend + mov eax, gunY + add eax, 20 + invoke DrawRect, 144, eax, 32, 18, 00603818h + invoke DrawRect, 146, eax, 28, 14, 007A4820h + jmp hud_stats_draw + +render_pistol_weapon: mov gunY, 192 cmp playerFlashTimer, 0 - je draw_gun_barrel - ; Recoil offset - add gunY, 3 + je draw_pistol_barrel + add gunY, 3 ; recoil offset ; Muzzle Flash Burst (Bright yellow & white star) invoke DrawRect, 157, 165, 7, 18, 00FFFF00h invoke DrawRect, 152, 170, 17, 8, 00FFAA00h invoke DrawRect, 158, 168, 5, 12, 00FFFFFFh -draw_gun_barrel: +draw_pistol_barrel: ; Weapon barrel & slide invoke DrawRect, 154, gunY, 13, 28, 003A3A3Ah invoke DrawRect, 156, gunY, 9, 28, 00555555h @@ -234,6 +464,7 @@ draw_gun_barrel: add eax, 26 invoke DrawRect, 152, eax, 17, 22, 00453020h ; brown wood grip plates +hud_stats_draw: ; 3. Health Bar invoke DrawString8x8, 8, 42, OFFSET strHpLabel, COLOR_GREEN invoke DrawNumber8x8, 34, 42, playerHealth, COLOR_WHITE @@ -273,14 +504,40 @@ hud_ammo_draw: invoke DrawString8x8, 222, 42, OFFSET strScoreLabel, COLOR_CYAN invoke DrawNumber8x8, 272, 42, playerScore, COLOR_CYAN - ; 6. Keys (if collected) + ; 6. Active Weapon Name & Slot Indicators at top left + invoke DrawString8x8, 8, 26, OFFSET strWeaponLabel, COLOR_WHITE + cmp playerWeapon, WEAPON_KNIFE + jne @F + invoke DrawString8x8, 42, 26, OFFSET strWeaponKnife, COLOR_CYAN + jmp draw_weapon_slots +@@: + cmp playerWeapon, WEAPON_SHOTGUN + jne @F + invoke DrawString8x8, 42, 26, OFFSET strWeaponShotgun, 00FFAA00h + jmp draw_weapon_slots +@@: + invoke DrawString8x8, 42, 26, OFFSET strWeaponPistol, COLOR_YELLOW + +draw_weapon_slots: + ; Draw small weapon slot indicator boxes: [1] [2] [3] + invoke DrawRectOutline, 100, 26, 8, 8, 00555555h + invoke DrawRectOutline, 112, 26, 8, 8, 00555555h + invoke DrawRectOutline, 124, 26, 8, 8, 00555555h + + ; Highlight active slot + mov eax, playerWeapon + imul eax, 12 + add eax, 100 + invoke DrawRectOutline, eax, 26, 8, 8, COLOR_YELLOW + + ; 7. 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 + ; 8. Damage Screen Flash cmp playerHitFlash, 0 jle hud_death_check ; Red border vignette @@ -290,7 +547,7 @@ hud_damage_flash: invoke DrawRect, 317, 0, 3, SCREEN_H, 00FF0000h hud_death_check: - ; 8. Death Overlay Screen + ; 9. Death Overlay Screen cmp isPlayerDead, 0 je combat_hud_done @@ -301,3 +558,28 @@ hud_death_check: combat_hud_done: ret RenderCombatHUD ENDP + +; ------------------------------------------------------------------------------ +; RenderPauseMenu: Renders stylish retro pause overlay when isGamePaused != 0 +; ------------------------------------------------------------------------------ +RenderPauseMenu PROC uses ebx ecx edx + cmp isGamePaused, 0 + je pause_menu_done + + ; Semi-transparent dark overlay panel (centered 200x105 at 60, 65) + invoke DrawRect, 60, 65, 200, 105, 00141416h + invoke DrawRectOutline, 60, 65, 200, 105, COLOR_CYAN + invoke DrawRectOutline, 62, 67, 196, 101, COLOR_WHITE + + ; Header + invoke DrawString8x8, 112, 75, OFFSET strPauseTitle, COLOR_YELLOW + + ; Menu Options + invoke DrawString8x8, 76, 95, OFFSET strPauseOpt1, COLOR_GREEN + invoke DrawString8x8, 76, 110, OFFSET strPauseOpt2, COLOR_WHITE + invoke DrawString8x8, 76, 125, OFFSET strPauseOpt3, COLOR_CYAN + invoke DrawString8x8, 76, 140, OFFSET strPauseOpt4, 00FF4444h + +pause_menu_done: + ret +RenderPauseMenu ENDP diff --git a/include/engine_data.inc b/include/engine_data.inc index f7ba2f7..86db906 100644 --- a/include/engine_data.inc +++ b/include/engine_data.inc @@ -63,8 +63,16 @@ keyAttack db 0 keyAttackHeld db 0 keyRestart db 0 keyRestartHeld db 0 +keyWeapon1 db 0 +keyWeapon2 db 0 +keyWeapon3 db 0 +keyWeapon4 db 0 +keyPause db 0 +keyPauseHeld db 0 +isGamePaused dd 0 ; Player Combat & Gameplay Stats +playerWeapon dd 1 ; Default: WEAPON_PISTOL playerHealth dd 100 playerMaxHealth dd 100 playerAmmo dd 50 @@ -117,6 +125,15 @@ strHpLabel db "HP:",0 strAmmoLabel db "AMMO:",0 strScoreLabel db "SCORE:",0 strKeysLabel db "KEY:",0 +strWeaponLabel db "WPN:",0 +strWeaponKnife db "KNIFE",0 +strWeaponPistol db "PISTOL",0 +strWeaponShotgun db "SHOTGUN",0 +strPauseTitle db "== PAUSED ==",0 +strPauseOpt1 db "[1] RESUME",0 +strPauseOpt2 db "[2] RESTART LEVEL",0 +strPauseOpt3 db "[3] MINIMAP RADAR",0 +strPauseOpt4 db "[4] QUIT TO WINDOWS",0 strYouDied db "YOU DIED! PRESS [R] TO RESTART",0 cutsceneCount dd 0 cutsceneStringCount dd 0 diff --git a/include/engine_defs.inc b/include/engine_defs.inc index f622640..55546e9 100644 --- a/include/engine_defs.inc +++ b/include/engine_defs.inc @@ -67,6 +67,16 @@ VK_KEY_TAB equ 9 VK_KEY_R equ 52h VK_KEY_CONTROL equ 11h VK_LBUTTON equ 01h +VK_KEY_1 equ 31h +VK_KEY_2 equ 32h +VK_KEY_3 equ 33h +VK_KEY_4 equ 34h + +; Weapon Archetypes +WEAPON_KNIFE equ 0 ; Melee blade (no ammo, fast, 30 damage) +WEAPON_PISTOL equ 1 ; Handgun (1 ammo, accurate, 35 damage) +WEAPON_SHOTGUN equ 2 ; Pump Shotgun (2 ammo, 3-ray spread, 70 damage) +WEAPON_COUNT equ 3 ; Entity Archetypes & States ENTITY_TYPE_PROP equ 0 @@ -75,6 +85,7 @@ ENTITY_TYPE_MEDKIT equ 2 ; Health pickup (+25 HP) ENTITY_TYPE_AMMO equ 3 ; Ammo pickup (+20 Ammo) ENTITY_TYPE_KEY equ 4 ; Door Key pickup ENTITY_TYPE_SWITCH equ 5 ; Interactive switch trigger +ENTITY_TYPE_BARREL equ 6 ; Explosive barrel (AOE damage) ENTITY_STATE_IDLE equ 0 ENTITY_STATE_CHASE equ 1 diff --git a/include/engine_input.inc b/include/engine_input.inc index 3010a5f..676320b 100644 --- a/include/engine_input.inc +++ b/include/engine_input.inc @@ -181,6 +181,77 @@ restart_released: mov keyRestartHeld, 0 restart_sampled: + ; Weapon Selection Keys: 1, 2, 3 + invoke GetAsyncKeyState, VK_KEY_1 + test ax, 8000h + jz @F + mov playerWeapon, WEAPON_KNIFE +@@: + invoke GetAsyncKeyState, VK_KEY_2 + test ax, 8000h + jz @F + mov playerWeapon, WEAPON_PISTOL +@@: + invoke GetAsyncKeyState, VK_KEY_3 + test ax, 8000h + jz @F + mov playerWeapon, WEAPON_SHOTGUN +@@: + + ; Escape / Pause Key + invoke GetAsyncKeyState, VK_KEY_ESCAPE + test ax, 8000h + jz pause_key_released + cmp keyPauseHeld, 0 + jne pause_key_sampled + xor isGamePaused, 1 + mov keyPauseHeld, 1 + jmp pause_key_sampled +pause_key_released: + mov keyPauseHeld, 0 +pause_key_sampled: + + ; If paused, handle pause menu choices + cmp isGamePaused, 0 + je not_in_pause_menu + ; While paused, mask normal movement & attack + mov keyForward, 0 + mov keyBackward, 0 + mov keyStrafeLeft, 0 + mov keyStrafeRight, 0 + mov keyTurnLeft, 0 + mov keyTurnRight, 0 + mov keyAttack, 0 + mov keyAction, 0 + + ; Option 1: Resume + invoke GetAsyncKeyState, VK_KEY_1 + test ax, 8000h + jz @F + mov isGamePaused, 0 +@@: + ; Option 2: Restart level + invoke GetAsyncKeyState, VK_KEY_2 + test ax, 8000h + jz @F + mov isGamePaused, 0 + mov keyRestart, 1 +@@: + ; Option 3: Toggle Map + invoke GetAsyncKeyState, VK_KEY_3 + test ax, 8000h + jz @F + xor keyShowMap, 1 +@@: + ; Option 4: Quit + invoke GetAsyncKeyState, VK_KEY_4 + test ax, 8000h + jz @F + mov isRunning, 0 +@@: + ret + +not_in_pause_menu: cmp isCutsceneActive, 0 je normal_keys_enabled mov keyForward, 0 @@ -193,12 +264,6 @@ restart_sampled: mov keyAction, 0 mov keyAttack, 0 normal_keys_enabled: - ; 11. Escape - Exit application - invoke GetAsyncKeyState, VK_KEY_ESCAPE - test ax, 8000h - jz @F - mov isRunning, 0 -@@: ret PollInput ENDP diff --git a/include/engine_raycaster.inc b/include/engine_raycaster.inc index e40de16..5432736 100644 --- a/include/engine_raycaster.inc +++ b/include/engine_raycaster.inc @@ -42,10 +42,18 @@ Render3DScene PROC uses ebx ecx edx esi edi ; -------------------------------------------------------------------------- mov edi, OFFSET screenBuffer mov eax, mapCeilingColor + cmp playerFlashTimer, 0 + je @F + add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost +@@: mov ecx, (SCREEN_W * SCREEN_HALF_H) rep stosd mov eax, mapFloorColor + cmp playerFlashTimer, 0 + je @F + add eax, 00202020h ; Dynamic Muzzle Flash ambient lighting boost +@@: mov ecx, (SCREEN_W * SCREEN_HALF_H) rep stosd diff --git a/main.asm b/main.asm index 6601fdb..6fc6cda 100644 --- a/main.asm +++ b/main.asm @@ -133,14 +133,7 @@ WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD ret handle_keydown: - cmp wParam, VK_KEY_ESCAPE - jne check_f12 - invoke PostQuitMessage, 0 - mov isRunning, 0 - xor eax, eax - ret -check_f12: - ; Movement and action edges are handled only by PollInput. + ; Key edges and ESC pause menu are handled via PollInput invoke DefWindowProcA, hWnd, uMsg, wParam, lParam ret @@ -274,9 +267,12 @@ Engine_Frame PROC uses ebx esi edi dec mapErrorFrames no_error_tick: call PollInput + cmp isGamePaused, 0 + jne @F call UpdatePlayer call UpdateEntities call UpdateCombat +@@: ; 2. 3D Raycasting & 2D HUD Render call Render3DScene @@ -286,6 +282,7 @@ no_error_tick: call RenderCombatHUD @@: call ApplyCutsceneFade + call RenderPauseMenu ; 2b. Screenshot request (F12): dump fresh framebuffer to BMP cmp keyScreenshot, 0 diff --git a/main.obj b/main.obj index b268ec4..7ca99a6 100644 Binary files a/main.obj and b/main.obj differ