EBLAN_ENGINE/editor/src/editor_app.cpp

2331 lines
100 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "editor_app.h"
#include "project_manager.h"
#include "level_io.h"
#include "../imgui/imgui.h"
#include "../imgui/imgui_internal.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <d3d11.h>
#include <mmsystem.h>
#include <shellapi.h>
#include <algorithm>
#include <fstream>
#include <cmath>
#include <queue>
#include <commdlg.h>
#include <cstring>
#include <cctype>
#include <filesystem>
#include <set>
#include <functional>
namespace fs = std::filesystem;
// Replace a file only after a complete successful write and close.
static bool AtomicWrite(const std::string& path, const std::function<void(std::ofstream&)>& writer) {
std::error_code ec;
auto parent = fs::path(path).parent_path();
if (!parent.empty()) fs::create_directories(parent, ec);
if (ec) return false;
static unsigned serial = 0;
const std::string temporary = path + ".tmp." + std::to_string(GetCurrentProcessId()) + "." + std::to_string(++serial);
std::ofstream out(temporary, std::ios::binary | std::ios::trunc);
if (!out) return false;
writer(out);
out.close();
if (!out || !MoveFileExA(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
DeleteFileA(temporary.c_str()); return false;
}
return true;
}
static std::vector<uint8_t> MakeDefaultTextureSlot(int slotIndex);
static bool ReadTextureSlots(const std::string& path, std::vector<uint8_t>& pixels) {
pixels.resize(16 * 65536);
for (int slot = 0; slot < 16; ++slot) {
auto fallback = MakeDefaultTextureSlot(slot);
std::copy(fallback.begin(), fallback.end(), pixels.begin() + slot * 65536);
}
std::error_code ec;
if (!fs::exists(path, ec)) return !ec;
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in) return false;
auto size = in.tellg(); in.seekg(0);
uint32_t magic = 0; uint16_t version = 0, count = 0;
in.read(reinterpret_cast<char*>(&magic), 4);
in.read(reinterpret_cast<char*>(&version), 2);
in.read(reinterpret_cast<char*>(&count), 2);
if (!in || magic != 0x58544245 || version != 1 || count == 0 || count > 16 ||
size != static_cast<std::streamoff>(8 + count * 65536)) return false;
auto candidate = pixels;
if (!in.read(reinterpret_cast<char*>(candidate.data()), count * 65536)) return false;
pixels.swap(candidate);
return true;
}
// Texture slot names (engine: 0 = solid marker, 1..9 walls, 10..12 props, 13..15 reserved)
static const char* g_TexSlotNames[16] = {
"0: Маркер (черный)", "1: Кирпич", "2: Дерево", "3: Камень",
"4: Металл", "5: Дверь", "6: (пусто)", "7: Колонна",
"8: Стекло", "9: Мрамор", "10: Фонарь", "11: Бочка",
"12: Статуя", "13: (резерв)", "14: (резерв)", "15: (резерв)"
};
EditorApp::EditorApp() {
project = std::make_unique<EblanProject>();
char module[MAX_PATH]{};
GetModuleFileNameA(nullptr, module, MAX_PATH);
const auto directory = fs::path(module).parent_path();
std::error_code ec;
const auto projectAssets = directory.parent_path() / "assets/map.dat";
currentFilePath = (fs::is_regular_file(projectAssets, ec) ? projectAssets : directory / "assets/map.dat").string();
InitDefaultLevel();
}
EditorApp::~EditorApp() {
StopEnginePreview();
StopCutscenePreview();
StopPlaytest();
ShutdownD3D();
}
void EditorApp::InitDefaultLevel() {
// Clear map
for (int y = 0; y < MAP_GRID_H; ++y) {
for (int x = 0; x < MAP_GRID_W; ++x) {
map[y][x] = CELL_EMPTY;
}
}
// Outer brick boundary
for (int x = 0; x < MAP_GRID_W; ++x) {
map[0][x] = CELL_BRICK;
map[MAP_GRID_H - 1][x] = CELL_BRICK;
}
for (int y = 0; y < MAP_GRID_H; ++y) {
map[y][0] = CELL_BRICK;
map[y][MAP_GRID_W - 1] = CELL_BRICK;
}
// Room divider with door
for (int x = 1; x < 20; ++x) {
map[8][x] = (x == 10) ? CELL_DOOR : CELL_STONE;
}
for (int y = 1; y < 18; ++y) {
if (y != 12) map[y][20] = CELL_WOOD;
}
// Some pillars
map[4][4] = CELL_PILLAR;
map[4][7] = CELL_PILLAR;
map[12][5] = CELL_METAL;
map[12][15] = CELL_METAL;
playerStartX = 2;
playerStartY = 2;
playerStartAngle = 0; // East
ceilingColor = 0x00383838;
floorColor = 0x00222222;
sprites.clear();
SpriteEntry demoSprite{};
demoSprite.x = 12 * 64 + 32; // raw world units
demoSprite.y = 5 * 64 + 32;
demoSprite.textureId = 11; // barrel
demoSprite.scale = 65536; // 1.0
sprites.push_back(demoSprite);
selectedSpriteIdx = -1;
selectedTransitionIdx = -1;
draggingSpriteIdx = -1;
transitions.clear();
cutscene = {};
selectedCutsceneEvent = -1;
StopCutscenePreview();
cutsceneCursor = 0;
previewX = playerStartX*64+32; previewY = playerStartY*64+32; previewAngle = playerStartAngle;
previewSceneCamera = true;
ScrubCutscene(0);
statusMessage = "Загружен стартовый уровень. Готов к редактированию.";
}
bool EditorApp::SaveLevel(const std::string& filepath) { return SaveLevelToFile(filepath, true); }
bool EditorApp::SaveLevelToFile(const std::string& filepath, bool updateCurrentPath) {
BinaryLevel level;
auto& h = level.header;
h.magic = MAP_MAGIC; h.version = MAP_FORMAT_VERSION;
h.mapWidth = MAP_GRID_W; h.mapHeight = MAP_GRID_H;
h.playerStartX = static_cast<uint16_t>(playerStartX);
h.playerStartY = static_cast<uint16_t>(playerStartY);
h.playerStartAngle = static_cast<uint16_t>(playerStartAngle);
h.ceilingColor = ceilingColor; h.floorColor = floorColor;
level.sprites = sprites; level.transitions = transitions; level.cutscene = cutscene;
std::memcpy(level.cells.data(), map, sizeof(map));
std::string error;
if (!ValidateLevel(level, error)) { statusMessage = "Ошибка сохранения: " + error; return false; }
h.spriteCount = static_cast<uint16_t>(sprites.size());
h.transitionCount = static_cast<uint16_t>(transitions.size());
h.cutsceneCount = static_cast<uint16_t>(cutscene.events.size());
h.cutsceneStringCount = static_cast<uint16_t>(cutscene.strings.size());
h.cutsceneFlags = cutscene.flags;
bool success = AtomicWrite(filepath, [&](std::ofstream& out) {
out.write(reinterpret_cast<const char*>(&h), sizeof(h));
out.write(reinterpret_cast<const char*>(level.cells.data()), level.cells.size());
if (!sprites.empty()) out.write(reinterpret_cast<const char*>(sprites.data()), sprites.size() * sizeof(SpriteEntry));
if (!transitions.empty()) out.write(reinterpret_cast<const char*>(transitions.data()), transitions.size() * sizeof(LevelTransition));
if (!cutscene.events.empty()) out.write(reinterpret_cast<const char*>(cutscene.events.data()), cutscene.events.size() * sizeof(CutsceneEvent));
for (const auto& text : cutscene.strings) {
std::string encoded; CutsceneEncode(text, encoded);
char bytes[CUTSCENE_STRING_BYTES]{};
std::memcpy(bytes, encoded.data(), encoded.size());
out.write(bytes, sizeof(bytes));
}
});
if (!success) { statusMessage = "Не удалось сохранить карту: " + filepath; return false; }
if (updateCurrentPath) currentFilePath = filepath;
statusMessage = "Уровень сохранён: " + filepath;
return true;
}
bool EditorApp::LoadLevel(const std::string& filepath) {
BinaryLevel candidate;
std::string error;
if (!ReadLevelFile(filepath, candidate, error)) { statusMessage = error; return false; }
StopCutscenePreview();
const auto& h = candidate.header;
std::memcpy(map, candidate.cells.data(), sizeof(map));
playerStartX = h.playerStartX; playerStartY = h.playerStartY; playerStartAngle = h.playerStartAngle;
ceilingColor = h.ceilingColor & 0xFFFFFF; floorColor = h.floorColor & 0xFFFFFF;
sprites = std::move(candidate.sprites); transitions = std::move(candidate.transitions);
cutscene = std::move(candidate.cutscene);
selectedCutsceneEvent = -1; cutsceneCursor = 0; placeCameraPoint = false;
ScrubCutscene(0);
currentFilePath = filepath;
previewTexturesDirty = true;
previewSceneCamera = true;
LoadAllTexturePreviews();
selectedSpriteIdx = selectedTransitionIdx = draggingSpriteIdx = -1;
strokeSnapshotTaken = false;
undoStack.clear(); redoStack.clear();
statusMessage = "Уровень загружен: " + filepath;
return true;
}
bool EditorApp::PreparePlaytest(std::string& startMap, std::string& enginePath) {
std::error_code ec;
enginePath = FindEnginePath();
if (enginePath.empty()) { statusMessage = "Сначала соберите engine.exe (build.bat)."; return false; }
const fs::path source = fs::absolute(currentFilePath.empty() ? "../assets/map.dat" : currentFilePath, ec);
if (ec) { statusMessage = "Неверный путь текущей карты."; return false; }
fs::path sandbox;
for (unsigned attempt = 0; attempt < 32; ++attempt) {
sandbox = fs::temp_directory_path(ec) / ("EblanStudio-" + std::to_string(GetCurrentProcessId()) + "-" +
std::to_string(GetTickCount64()) + "-" + std::to_string(attempt));
if (ec) break;
if (fs::create_directory(sandbox, ec)) break;
if (ec) break;
sandbox.clear();
}
if (ec || sandbox.empty()) { statusMessage = "Не удалось создать папку плейтеста."; return false; }
playtestDirectory = sandbox.string();
auto copyFailure = [&]() { statusMessage = "Не удалось скопировать ресурсы плейтеста: " + ec.message(); CleanupPlaytest(); return false; };
fs::create_directories(sandbox / "assets", ec);
if (ec) return copyFailure();
if (fs::is_directory(source.parent_path(), ec)) {
for (const auto& entry : fs::directory_iterator(source.parent_path(), ec)) {
if (ec) break;
if (entry.is_regular_file(ec) && _stricmp(entry.path().extension().string().c_str(), ".dat") == 0)
fs::copy_file(entry.path(), sandbox / "assets" / entry.path().filename(), fs::copy_options::overwrite_existing, ec);
if (ec) break;
}
}
if (ec) return copyFailure();
if (!SaveLevelToFile((sandbox / "assets" / source.filename()).string(), false)) { CleanupPlaytest(); return false; }
const fs::path pack = fs::absolute(GetTexturePackPath(), ec);
if (ec) return copyFailure();
if (fs::exists(pack, ec)) {
fs::create_directories(sandbox / "assets/textures", ec);
if (!ec) fs::copy_file(pack, sandbox / "assets/textures/texturepack.bin", fs::copy_options::overwrite_existing, ec);
}
if (ec) return copyFailure();
const fs::path sounds = fs::absolute(GetSoundsDirectory(), ec);
if (ec) return copyFailure();
if (fs::is_directory(sounds, ec)) fs::copy(sounds, sandbox / "assets/sounds", fs::copy_options::recursive, ec);
if (ec) return copyFailure();
// Validate all reachable maps and destination spawns, including reverse links.
std::vector<std::string> pending{source.filename().string()};
std::set<std::string> visited;
while (!pending.empty()) {
std::string name = pending.back(); pending.pop_back();
std::string key = name;
for (char& ch : key) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (!visited.insert(key).second) continue;
BinaryLevel level;
std::string error;
if (!ReadLevelFile((sandbox / "assets" / name).string(), level, error)) {
statusMessage = "Плейтест: " + error; CleanupPlaytest(); return false;
}
for (const auto& event : level.cutscene.events) {
if (event.opCode == OP_PLAY_SOUND && event.paramA >= 0 &&
!fs::is_regular_file(sandbox / "assets/sounds" / level.cutscene.strings[event.paramA], ec)) {
statusMessage = "Плейтест: не найден WAV " + level.cutscene.strings[event.paramA];
CleanupPlaytest(); return false;
}
}
for (const auto& t : level.transitions) {
BinaryLevel destination;
if (!ReadLevelFile((sandbox / "assets" / t.targetMap).string(), destination, error) ||
!IsWalkableCell(destination.cells[t.targetSpawnY * MAP_GRID_W + t.targetSpawnX])) {
statusMessage = "Плейтест: проверьте файл и спавн перехода " + std::string(t.targetMap);
CleanupPlaytest(); return false;
}
pending.emplace_back(t.targetMap);
}
}
startMap = "assets\\" + source.filename().string();
return true;
}
void EditorApp::CleanupPlaytest() {
if (playtestDirectory.empty()) return;
std::error_code ec;
const auto directory = fs::weakly_canonical(playtestDirectory, ec);
if (ec) return;
const auto temporaryRoot = fs::weakly_canonical(fs::temp_directory_path(ec), ec);
// Verify the absolute target is a dedicated immediate child of TEMP.
if (!ec && directory.parent_path() == temporaryRoot && directory.filename().string().rfind("EblanStudio-", 0) == 0)
fs::remove_all(directory, ec);
playtestDirectory.clear();
}
void EditorApp::LaunchPlaytest() {
StopCutscenePreview();
StopPlaytest();
std::string startMap, enginePath;
if (!PreparePlaytest(startMap, enginePath)) { showPlaytestError = true; return; }
std::string command = "\"" + enginePath + "\" \"" + startMap + "\"";
STARTUPINFOA si{}; si.cb = sizeof(si);
if (!CreateProcessA(enginePath.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
playtestDirectory.c_str(), &si, &engineProcInfo)) {
statusMessage = "Ошибка запуска engine.exe: " + std::to_string(GetLastError()); showPlaytestError = true; CleanupPlaytest(); return;
}
hEngineProcess = engineProcInfo.hProcess;
isEngineRunning = true;
statusMessage = "Игра запущена из временной копии. Исходные карты сохранены.";
}
void EditorApp::StopPlaytest() {
if (hEngineProcess) {
TerminateProcess(hEngineProcess, 0);
WaitForSingleObject(hEngineProcess, 2000);
CloseHandle(hEngineProcess); CloseHandle(engineProcInfo.hThread);
hEngineProcess = nullptr; engineProcInfo = {};
}
isEngineRunning = false;
CleanupPlaytest();
}
void EditorApp::CheckPlaytestProcess() {
if (!hEngineProcess || WaitForSingleObject(hEngineProcess, 0) != WAIT_OBJECT_0) return;
DWORD code = 0; GetExitCodeProcess(hEngineProcess, &code);
CloseHandle(hEngineProcess); CloseHandle(engineProcInfo.hThread);
hEngineProcess = nullptr; engineProcInfo = {}; isEngineRunning = false;
CleanupPlaytest();
statusMessage = "Игра завершена (код: " + std::to_string(code) + ").";
}
// -----------------------------------------------------------------------------
// Undo / Redo
// -----------------------------------------------------------------------------
LevelSnapshot EditorApp::CaptureSnapshot() const {
LevelSnapshot s;
std::memcpy(s.map, map, sizeof(map));
s.playerStartX = playerStartX;
s.playerStartY = playerStartY;
s.playerStartAngle = playerStartAngle;
s.ceilingColor = ceilingColor;
s.floorColor = floorColor;
s.sprites = sprites;
s.transitions = transitions;
s.cutscene = cutscene;
return s;
}
void EditorApp::RestoreSnapshot(const LevelSnapshot& s) {
std::memcpy(map, s.map, sizeof(map));
playerStartX = s.playerStartX;
playerStartY = s.playerStartY;
playerStartAngle = s.playerStartAngle;
ceilingColor = s.ceilingColor;
floorColor = s.floorColor;
sprites = s.sprites;
StopCutscenePreview();
transitions = s.transitions;
cutscene = s.cutscene;
selectedCutsceneEvent = -1; placeCameraPoint = false;
ScrubCutscene(cutsceneCursor);
selectedSpriteIdx = -1;
selectedTransitionIdx = -1;
draggingSpriteIdx = -1;
}
void EditorApp::PushUndo() {
undoStack.push_back(CaptureSnapshot());
if (undoStack.size() > MAX_UNDO) undoStack.pop_front();
redoStack.clear();
}
void EditorApp::Undo() {
if (undoStack.empty()) return;
redoStack.push_back(CaptureSnapshot());
RestoreSnapshot(undoStack.back());
undoStack.pop_back();
statusMessage = "Отмена действия.";
}
void EditorApp::Redo() {
if (redoStack.empty()) return;
undoStack.push_back(CaptureSnapshot());
RestoreSnapshot(redoStack.back());
redoStack.pop_back();
statusMessage = "Повтор действия.";
}
// -----------------------------------------------------------------------------
// File dialogs (comdlg32)
// -----------------------------------------------------------------------------
std::string EditorApp::BrowseOpenFile() {
char szFile[MAX_PATH] = {0};
OPENFILENAMEA ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = "EBLAN Level (*.dat)\0*.dat\0All Files (*.*)\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
if (GetOpenFileNameA(&ofn)) return szFile;
return "";
}
std::string EditorApp::BrowseSaveFile() {
char szFile[MAX_PATH] = {0};
OPENFILENAMEA ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = "EBLAN Level (*.dat)\0*.dat\0All Files (*.*)\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = "dat";
if (GetSaveFileNameA(&ofn)) return szFile;
return "";
}
// -----------------------------------------------------------------------------
// Asset Manager (Textures, Sprites & Audio Import)
// -----------------------------------------------------------------------------
void EditorApp::InitD3D(ID3D11Device* pDevice) {
pd3dDevice = pDevice;
LoadAllTexturePreviews();
}
void EditorApp::ShutdownD3D() {
StopEnginePreview();
if (previewSRV) { previewSRV->Release(); previewSRV = nullptr; }
if (previewTexture) { previewTexture->Release(); previewTexture = nullptr; }
if (previewSampler) { previewSampler->Release(); previewSampler = nullptr; }
for (int i = 0; i < 16; ++i) {
if (textureSRVs[i]) {
textureSRVs[i]->Release();
textureSRVs[i] = nullptr;
}
}
pd3dDevice = nullptr;
}
std::string EditorApp::GetTexturePackPath() const {
const auto local = fs::path(currentFilePath).parent_path() / "textures/texturepack.bin";
std::error_code ec;
if (fs::is_regular_file(local, ec)) return local.string();
const char* candidates[] = {
"..\\assets\\textures\\texturepack.bin",
"assets\\textures\\texturepack.bin",
"..\\..\\assets\\textures\\texturepack.bin"
};
for (const char* path : candidates) {
DWORD attr = GetFileAttributesA(path);
if (attr != INVALID_FILE_ATTRIBUTES) return path;
}
return local.string();
}
std::string EditorApp::GetSoundsDirectory() const {
auto directory = fs::path(currentFilePath).parent_path() / "sounds";
std::error_code ec;
fs::create_directories(directory, ec);
return directory.string() + "\\";
}
std::string EditorApp::BrowseOpenImageFile() {
char szFile[MAX_PATH] = { 0 };
OPENFILENAMEA ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = "Изображения (*.png;*.jpg;*.jpeg;*.bmp)\0*.png;*.jpg;*.jpeg;*.bmp\0Все файлы (*.*)\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
if (GetOpenFileNameA(&ofn)) return szFile;
return "";
}
std::string EditorApp::BrowseOpenAudioFile() {
char szFile[MAX_PATH] = { 0 };
OPENFILENAMEA ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = "Звуковые файлы WAV (*.wav)\0*.wav\0Все файлы (*.*)\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
if (GetOpenFileNameA(&ofn)) return szFile;
return "";
}
std::string EditorApp::BrowseSavePngFile(int slotIndex) {
char szFile[MAX_PATH] = { 0 };
snprintf(szFile, MAX_PATH, "texture_slot_%d.png", slotIndex);
OPENFILENAMEA ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetActiveWindow();
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = "PNG Изображение (*.png)\0*.png\0Все файлы (*.*)\0*.*\0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = "png";
if (GetSaveFileNameA(&ofn)) return szFile;
return "";
}
static bool ResampleTo128BGRA(const unsigned char* srcRGBA, int srcW, int srcH, bool isSprite, std::vector<uint8_t>& outBGRA) {
outBGRA.assign(128 * 128 * 4, 0);
if (!srcRGBA || srcW <= 0 || srcH <= 0) return false;
if (!isSprite) {
// Стены: умный кроп по центру с сохранением квадратной геометрии (без растяжения)
float srcAspect = static_cast<float>(srcW) / static_cast<float>(srcH);
float cropX0 = 0.0f, cropY0 = 0.0f, cropW = static_cast<float>(srcW), cropH = static_cast<float>(srcH);
if (srcAspect > 1.0f) {
cropW = static_cast<float>(srcH);
cropX0 = (srcW - cropW) * 0.5f;
} else if (srcAspect < 1.0f) {
cropH = static_cast<float>(srcW);
cropY0 = (srcH - cropH) * 0.5f;
}
for (int dy = 0; dy < 128; ++dy) {
float srcY = cropY0 + (dy + 0.5f) * (cropH / 128.0f) - 0.5f;
int y0 = static_cast<int>(std::floor(srcY));
int y1 = y0 + 1;
float fy = srcY - y0;
if (y0 < 0) { y0 = 0; fy = 0.0f; }
if (y1 >= srcH) { y1 = srcH - 1; }
for (int dx = 0; dx < 128; ++dx) {
float srcX = cropX0 + (dx + 0.5f) * (cropW / 128.0f) - 0.5f;
int x0 = static_cast<int>(std::floor(srcX));
int x1 = x0 + 1;
float fx = srcX - x0;
if (x0 < 0) { x0 = 0; fx = 0.0f; }
if (x1 >= srcW) { x1 = srcW - 1; }
const unsigned char* p00 = &srcRGBA[(y0 * srcW + x0) * 4];
const unsigned char* p10 = &srcRGBA[(y0 * srcW + x1) * 4];
const unsigned char* p01 = &srcRGBA[(y1 * srcW + x0) * 4];
const unsigned char* p11 = &srcRGBA[(y1 * srcW + x1) * 4];
float w00 = (1.0f - fx) * (1.0f - fy);
float w10 = fx * (1.0f - fy);
float w01 = (1.0f - fx) * fy;
float w11 = fx * fy;
uint8_t r = static_cast<uint8_t>(std::clamp(p00[0]*w00 + p10[0]*w10 + p01[0]*w01 + p11[0]*w11 + 0.5f, 0.0f, 255.0f));
uint8_t g = static_cast<uint8_t>(std::clamp(p00[1]*w00 + p10[1]*w10 + p01[1]*w01 + p11[1]*w11 + 0.5f, 0.0f, 255.0f));
uint8_t b = static_cast<uint8_t>(std::clamp(p00[2]*w00 + p10[2]*w10 + p01[2]*w01 + p11[2]*w11 + 0.5f, 0.0f, 255.0f));
int dstIdx = (dy * 128 + dx) * 4;
outBGRA[dstIdx + 0] = b;
outBGRA[dstIdx + 1] = g;
outBGRA[dstIdx + 2] = r;
outBGRA[dstIdx + 3] = 0x00;
}
}
} else {
// Спрайты: пропорциональное вписывание с привязкой к полу (нижней грани) и прозрачным фоном
for (int i = 0; i < 128 * 128; ++i) {
outBGRA[i * 4 + 0] = 0xFF; // B
outBGRA[i * 4 + 1] = 0x00; // G
outBGRA[i * 4 + 2] = 0xFF; // R (COLOR_CHROMA_KEY 0x00FF00FF)
outBGRA[i * 4 + 3] = 0x00;
}
float scale = (std::min)(128.0f / srcW, 128.0f / srcH);
float targetW = srcW * scale;
float targetH = srcH * scale;
float offsetX = (128.0f - targetW) * 0.5f;
float offsetY = (128.0f - targetH); // привязка к полу
int dstX0 = static_cast<int>(offsetX);
int dstX1 = static_cast<int>(offsetX + targetW);
int dstY0 = static_cast<int>(offsetY);
int dstY1 = static_cast<int>(offsetY + targetH);
for (int dy = dstY0; dy < dstY1 && dy < 128; ++dy) {
if (dy < 0) continue;
float srcY = (dy - offsetY + 0.5f) / scale - 0.5f;
int y0 = static_cast<int>(std::floor(srcY));
int y1 = y0 + 1;
float fy = srcY - y0;
if (y0 < 0) { y0 = 0; fy = 0.0f; }
if (y1 >= srcH) { y1 = srcH - 1; }
for (int dx = dstX0; dx < dstX1 && dx < 128; ++dx) {
if (dx < 0) continue;
float srcX = (dx - offsetX + 0.5f) / scale - 0.5f;
int x0 = static_cast<int>(std::floor(srcX));
int x1 = x0 + 1;
float fx = srcX - x0;
if (x0 < 0) { x0 = 0; fx = 0.0f; }
if (x1 >= srcW) { x1 = srcW - 1; }
const unsigned char* p00 = &srcRGBA[(y0 * srcW + x0) * 4];
const unsigned char* p10 = &srcRGBA[(y0 * srcW + x1) * 4];
const unsigned char* p01 = &srcRGBA[(y1 * srcW + x0) * 4];
const unsigned char* p11 = &srcRGBA[(y1 * srcW + x1) * 4];
float w00 = (1.0f - fx) * (1.0f - fy);
float w10 = fx * (1.0f - fy);
float w01 = (1.0f - fx) * fy;
float w11 = fx * fy;
float aF = p00[3]*w00 + p10[3]*w10 + p01[3]*w01 + p11[3]*w11;
float rF = p00[0]*w00 + p10[0]*w10 + p01[0]*w01 + p11[0]*w11;
float gF = p00[1]*w00 + p10[1]*w10 + p01[1]*w01 + p11[1]*w11;
float bF = p00[2]*w00 + p10[2]*w10 + p01[2]*w01 + p11[2]*w11;
// Прозрачность по альфа-каналу ИЛИ по классическому цвету Chroma Key RGB(255, 0, 255)
if (aF < 128.0f || (rF >= 240.0f && gF <= 25.0f && bF >= 240.0f)) continue;
int dstIdx = (dy * 128 + dx) * 4;
outBGRA[dstIdx + 0] = static_cast<uint8_t>(std::clamp(bF + 0.5f, 0.0f, 255.0f));
outBGRA[dstIdx + 1] = static_cast<uint8_t>(std::clamp(gF + 0.5f, 0.0f, 255.0f));
outBGRA[dstIdx + 2] = static_cast<uint8_t>(std::clamp(rF + 0.5f, 0.0f, 255.0f));
outBGRA[dstIdx + 3] = 0x00;
}
}
}
return true;
}
bool EditorApp::ImportTextureImage(int slotIndex, bool isSprite, const std::string& explicitPath) {
if (slotIndex < 1 || slotIndex >= 16) return false;
std::string filePath = explicitPath.empty() ? BrowseOpenImageFile() : explicitPath;
if (filePath.empty()) return false;
int srcW = 0, srcH = 0, channels = 0;
unsigned char* data = stbi_load(filePath.c_str(), &srcW, &srcH, &channels, 4);
if (!data) {
statusMessage = "Ошибка загрузки картинки: " + filePath;
return false;
}
std::vector<uint8_t> slotData;
if (!ResampleTo128BGRA(data, srcW, srcH, isSprite, slotData)) {
stbi_image_free(data);
statusMessage = "Ошибка конвертации текстуры!";
return false;
}
stbi_image_free(data);
if (!WriteTextureSlot(slotIndex, slotData)) return false;
statusMessage = "Текстура [" + std::string(g_TexSlotNames[slotIndex]) + "] обновлена из: " + filePath;
return true;
}
bool EditorApp::ExportTextureImage(int slotIndex) {
if (slotIndex < 0 || slotIndex >= 16) return false;
std::string savePath = BrowseSavePngFile(slotIndex);
if (savePath.empty()) return false;
std::vector<uint8_t> allSlots;
if (!ReadTextureSlots(GetTexturePackPath(), allSlots)) {
statusMessage = "Не удалось прочитать пакет текстур для экспорта.";
return false;
}
std::vector<uint8_t> bgra(allSlots.begin() + slotIndex * 65536, allSlots.begin() + (slotIndex + 1) * 65536);
// Convert BGRA to RGBA (with Chroma Key -> Alpha 0)
std::vector<uint8_t> rgba(128 * 128 * 4);
for (int i = 0; i < 128 * 128; ++i) {
uint8_t b = bgra[i * 4 + 0];
uint8_t g = bgra[i * 4 + 1];
uint8_t r = bgra[i * 4 + 2];
if (b == 0xFF && g == 0x00 && r == 0xFF) {
rgba[i * 4 + 0] = 0;
rgba[i * 4 + 1] = 0;
rgba[i * 4 + 2] = 0;
rgba[i * 4 + 3] = 0;
} else {
rgba[i * 4 + 0] = r;
rgba[i * 4 + 1] = g;
rgba[i * 4 + 2] = b;
rgba[i * 4 + 3] = 255;
}
}
if (stbi_write_png(savePath.c_str(), 128, 128, 4, rgba.data(), 128 * 4)) {
statusMessage = "Текстура [" + std::string(g_TexSlotNames[slotIndex]) + "] экспортирована в: " + savePath;
return true;
}
statusMessage = "Ошибка записи PNG файла: " + savePath;
return false;
}
std::vector<std::string> EditorApp::GetSoundList() const {
std::vector<std::string> sounds;
std::string pattern = GetSoundsDirectory() + "*.wav";
WIN32_FIND_DATAA fd{};
HANDLE hFind = FindFirstFileA(pattern.c_str(), &fd);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
sounds.push_back(fd.cFileName);
}
} while (FindNextFileA(hFind, &fd));
FindClose(hFind);
}
std::sort(sounds.begin(), sounds.end());
return sounds;
}
void EditorApp::UpdateSoundManifest() {
auto sounds = GetSoundList();
std::string sDir = GetSoundsDirectory();
// 1. Write sounds.inc for MASM32 (clean forward-slash relative paths)
std::string incPath = sDir + "sounds.inc";
std::ofstream inc(incPath);
if (inc.is_open()) {
inc << "; ==============================================================================\n";
inc << "; EBLAN ENGINE - Auto-Generated Sound Manifest (winmm.lib PlaySound / MCI)\n";
inc << "; ==============================================================================\n\n";
for (const auto& s : sounds) {
std::string base = s;
size_t dot = base.find_last_of('.');
if (dot != std::string::npos) base = base.substr(0, dot);
for (char& c : base) {
if (c == ' ' || c == '-') c = '_';
c = static_cast<char>(std::toupper(c));
}
inc << "SND_" << base << " db \"assets/sounds/" << s << "\", 0\n";
}
inc.close();
}
// 2. Write sounds.txt for editor / MCI
std::string txtPath = sDir + "sounds.txt";
std::ofstream txt(txtPath);
if (txt.is_open()) {
txt << "# EblanStudio Sound Registry & MCI Aliases\n";
for (const auto& s : sounds) {
std::string base = s;
size_t dot = base.find_last_of('.');
if (dot != std::string::npos) base = base.substr(0, dot);
for (char& c : base) {
if (c == ' ' || c == '-') c = '_';
c = static_cast<char>(std::tolower(c));
}
txt << "alias snd_" << base << " -> assets/sounds/" << s << "\n";
}
txt.close();
}
}
void EditorApp::PlayAudioSound(const std::string& filename) {
std::string path = GetSoundsDirectory() + filename;
PlaySoundA(path.c_str(), NULL, SND_FILENAME | SND_ASYNC);
statusMessage = "Воспроизведение звука: " + filename;
}
void EditorApp::StopAudioSound() {
PlaySoundA(NULL, NULL, 0);
statusMessage = "Воспроизведение звука остановлено.";
}
bool EditorApp::ImportSoundAudio(const std::string& explicitPath) {
std::string filePath = explicitPath.empty() ? BrowseOpenAudioFile() : explicitPath;
if (filePath.empty()) return false;
std::string soundDir = GetSoundsDirectory();
size_t lastSlash = filePath.find_last_of("\\/");
std::string filename = (lastSlash == std::string::npos) ? filePath : filePath.substr(lastSlash + 1);
std::string destPath = soundDir + filename;
if (!CopyFileA(filePath.c_str(), destPath.c_str(), FALSE)) {
statusMessage = "Ошибка копирования звука: " + destPath;
return false;
}
UpdateSoundManifest();
PlayAudioSound(filename);
statusMessage = "Аудио импортировано и обновлен манифест MCI: " + destPath;
return true;
}
static std::vector<uint8_t> MakeDefaultTextureSlot(int slotIndex) {
std::vector<uint8_t> slotData(128 * 128 * 4, 0);
auto setPx = [&](int x, int y, int r, int g, int b) {
int idx = (y * 128 + x) * 4;
slotData[idx + 0] = static_cast<uint8_t>(b & 0xFF);
slotData[idx + 1] = static_cast<uint8_t>(g & 0xFF);
slotData[idx + 2] = static_cast<uint8_t>(r & 0xFF);
slotData[idx + 3] = 0;
};
switch (slotIndex) {
case 0: // Empty / Black
break;
case 1: // Red brick
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0x85, g = 0x22, b = 0x15;
bool mortar = ((y & 15) == 0 || (y & 15) == 1);
int vx = x + ((y & 16) ? 16 : 0);
if ((vx & 31) == 0 || (vx & 31) == 1) mortar = true;
if (mortar) {
r = g = b = 0x55;
} else {
int n = (((x ^ y) & 7) << 2);
b = (std::min)(255, b + n);
}
setPx(x, y, r, g, b);
}
}
break;
case 2: // Wood
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0x68, g = 0x3A, b = 0x12;
if ((x & 31) == 0) {
r = 0x20; g = 0x10; b = 0x05;
} else {
int n = ((y & 3) << 3);
b = (std::min)(255, b + n);
}
setPx(x, y, r, g, b);
}
}
break;
case 3: // Stone
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0x55, g = 0x55, b = 0x55;
if ((x & 31) == 0 || (y & 31) == 0) {
r = g = b = 0x22;
} else {
int n = ((((x * 3) + y) & 15) << 2);
b = (std::min)(255, b + n);
}
setPx(x, y, r, g, b);
}
}
break;
case 4: // Metal
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0x60, g = 0x65, b = 0x70;
if ((x - 10)*(x - 10) + (y - 10)*(y - 10) <= 16 ||
(x - 118)*(x - 118) + (y - 118)*(y - 118) <= 16) {
r = 0xA0; g = 0xB0; b = 0xC0;
}
setPx(x, y, r, g, b);
}
}
break;
case 5: // Door
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r, g, b;
if (x < 8 || x >= 120 || y < 8) {
r = g = b = 0x33;
} else {
r = 0x50; g = 0x2D; b = 0x15;
if ((x - 100)*(x - 100) + (y - 64)*(y - 64) <= 36) {
r = 0xFF; g = 0xD0; b = 0x00;
}
}
setPx(x, y, r, g, b);
}
}
break;
case 7: // Pillar
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r, g, b;
if ((x & 15) < 8) { r = 0x70; g = 0xB0; b = 0xC0; }
else { r = 0x40; g = 0x80; b = 0x90; }
setPx(x, y, r, g, b);
}
}
break;
case 8: // Glass
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0x40, g = 0x30, b = 0x20;
if (x < 6 || x >= 122 || y < 6 || y >= 122) {
r = g = b = 0x60;
} else {
int d = (x + y) & 63;
if (d < 4 || (d >= 24 && d < 28)) {
r = 0x90; g = 0x70; b = 0x50;
}
}
setPx(x, y, r, g, b);
}
}
break;
case 9: // Marble
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0xD8, g = 0xD0, b = 0xC0;
int v = (x * 7 + y * 13);
v = (v + (v >> 3)) & 63;
if (v <= 2 || v == 40) {
r = g = b = 0x55;
} else {
int n = (x ^ y) & 3;
b = (std::min)(255, b + n);
}
setPx(x, y, r, g, b);
}
}
break;
case 10: // Lamp (Sprite)
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0xFF, g = 0x00, b = 0xFF; // Chroma key
if (x >= 60 && x < 68 && y >= 40 && y < 120) { r = g = b = 0x40; }
if (x >= 48 && x < 80 && y >= 16 && y < 44) { r = 0xFF; g = 0xC0; b = 0x30; }
if (x >= 52 && x < 76 && y >= 120 && y < 126) { r = g = b = 0x30; }
setPx(x, y, r, g, b);
}
}
break;
case 11: // Barrel (Sprite)
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0xFF, g = 0x00, b = 0xFF; // Chroma key
int dx = x - 64, dy = y - 70;
if (dx * dx * 2704 + dy * dy * 1156 <= 3125824) {
r = 0x6A; g = 0x40; b = 0x1E;
if (std::abs(dy) >= 29 && std::abs(dy) <= 35) {
r = g = b = 0x55;
}
if (x <= 52 && std::abs(dy) < 26) {
r = 0x8A; g = 0x5A; b = 0x2E;
}
}
setPx(x, y, r, g, b);
}
}
break;
case 12: // Statue / Prop (Sprite)
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
int r = 0xFF, g = 0x00, b = 0xFF;
if (x >= 44 && x < 84 && y >= 112 && y < 126) { r = g = b = 0x60; }
if (x >= 54 && x < 74 && y >= 40 && y < 112) { r = g = b = 0x8A + ((x ^ y) & 7); }
int dx = x - 64, dy = y - 28;
if (dx * dx + dy * dy <= 100) { r = g = b = 0x9A; }
setPx(x, y, r, g, b);
}
}
break;
default:
for (int y = 0; y < 128; ++y) {
for (int x = 0; x < 128; ++x) {
if (slotIndex >= 10) setPx(x, y, 0xFF, 0x00, 0xFF);
else setPx(x, y, 0x00, 0x00, 0x00);
}
}
break;
}
return slotData;
}
bool EditorApp::WriteTextureSlot(int slotIndex, const std::vector<uint8_t>& pixels) {
if (slotIndex < 1 || slotIndex >= 16 || pixels.size() != 65536) return false;
const std::string path = GetTexturePackPath();
std::vector<uint8_t> slots;
if (!ReadTextureSlots(path, slots)) {
statusMessage = "Повреждён texturepack.bin. Восстановите пакет перед импортом.";
return false;
}
std::copy(pixels.begin(), pixels.end(), slots.begin() + slotIndex * 65536);
if (!AtomicWrite(path, [&](std::ofstream& out) {
uint32_t magic = 0x58544245; uint16_t version = 1, count = 16;
out.write(reinterpret_cast<const char*>(&magic), 4);
out.write(reinterpret_cast<const char*>(&version), 2);
out.write(reinterpret_cast<const char*>(&count), 2);
out.write(reinterpret_cast<const char*>(slots.data()), slots.size());
})) {
statusMessage = "Не удалось записать пакет текстур: " + path;
return false;
}
ReloadTextureSlotSRV(slotIndex);
return true;
}
bool EditorApp::ResetTextureSlotToDefault(int slotIndex) {
if (slotIndex < 1 || slotIndex >= 16) return false;
if (!WriteTextureSlot(slotIndex, MakeDefaultTextureSlot(slotIndex))) return false;
statusMessage = "Восстановлена стандартная текстура слота " + std::to_string(slotIndex);
return true;
}
void EditorApp::HandleDropFile(const std::string& droppedPath) {
std::string lower = droppedPath;
for (char& c : lower) c = static_cast<char>(std::tolower(c));
if (lower.length() >= 4 && lower.rfind(".wav") == lower.length() - 4) {
ImportSoundAudio(droppedPath);
showAssetManagerModal = true;
} else if (lower.length() >= 4 && (
lower.rfind(".png") == lower.length() - 4 ||
lower.rfind(".jpg") == lower.length() - 4 ||
lower.rfind(".bmp") == lower.length() - 4 ||
(lower.length() >= 5 && lower.rfind(".jpeg") == lower.length() - 5))) {
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
int slot = sprites[selectedSpriteIdx].textureId & 15;
if (slot >= 10 && slot <= 15) {
ImportTextureImage(slot, true, droppedPath);
return;
}
}
if (selectedPaletteCell >= 1 && selectedPaletteCell <= 9 && selectedPaletteCell != 6) {
ImportTextureImage(selectedPaletteCell, false, droppedPath);
return;
}
showAssetManagerModal = true;
statusMessage = "Изображение получено. Выберите целевой слот для импорта: " + droppedPath;
} else if (lower.length() >= 4 && lower.rfind(".dat") == lower.length() - 4) {
LoadLevel(droppedPath);
}
}
void EditorApp::ReloadTextureSlotSRV(int slotIndex) {
previewTexturesDirty = true;
if (!pd3dDevice || slotIndex < 0 || slotIndex >= 16) return;
if (textureSRVs[slotIndex]) {
textureSRVs[slotIndex]->Release();
textureSRVs[slotIndex] = nullptr;
}
std::vector<uint8_t> allSlots;
ReadTextureSlots(GetTexturePackPath(), allSlots); // fallback previews also work without a pack
std::vector<uint8_t> bgra(allSlots.begin() + slotIndex * 65536, allSlots.begin() + (slotIndex + 1) * 65536);
std::vector<uint8_t> displayRGBA(128 * 128 * 4);
for (int i = 0; i < 128 * 128; ++i) {
uint8_t b = bgra[i * 4 + 0];
uint8_t g = bgra[i * 4 + 1];
uint8_t r = bgra[i * 4 + 2];
if (b == 0xFF && g == 0x00 && r == 0xFF) {
displayRGBA[i * 4 + 0] = 0;
displayRGBA[i * 4 + 1] = 0;
displayRGBA[i * 4 + 2] = 0;
displayRGBA[i * 4 + 3] = 0; // transparent
} else {
displayRGBA[i * 4 + 0] = r;
displayRGBA[i * 4 + 1] = g;
displayRGBA[i * 4 + 2] = b;
displayRGBA[i * 4 + 3] = 255; // opaque
}
}
D3D11_TEXTURE2D_DESC desc{};
desc.Width = 128;
desc.Height = 128;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
D3D11_SUBRESOURCE_DATA sub{};
sub.pSysMem = displayRGBA.data();
sub.SysMemPitch = 128 * 4;
ID3D11Texture2D* pTex = nullptr;
if (SUCCEEDED(pd3dDevice->CreateTexture2D(&desc, &sub, &pTex))) {
pd3dDevice->CreateShaderResourceView(pTex, nullptr, &textureSRVs[slotIndex]);
pTex->Release();
}
}
void EditorApp::LoadAllTexturePreviews() {
for (int i = 0; i < 16; ++i) {
ReloadTextureSlotSRV(i);
}
}
void EditorApp::RenderAssetManagerModal() {
if (!showAssetManagerModal) return;
ImGui::SetNextWindowSize(ImVec2(820, 580), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Менеджер ассетов (Texture & Audio Manager)", &showAssetManagerModal)) {
if (ImGui::BeginTabBar("AssetManagerTabs")) {
if (ImGui::BeginTabItem("Текстурный пакет (16 слотов)")) {
ImGui::Text("Файл пакета: %s", GetTexturePackPath().c_str());
ImGui::SameLine();
if (ImGui::Button("Перезагрузить все")) {
LoadAllTexturePreviews();
statusMessage = "Все слоты перезагружены из texturepack.bin";
}
ImGui::Separator();
// 4x4 Grid of texture slots
if (ImGui::BeginTable("TextureSlotGrid", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
for (int slot = 0; slot < 16; ++slot) {
if (slot % 4 == 0) ImGui::TableNextRow();
ImGui::TableSetColumnIndex(slot % 4);
ImGui::PushID(slot);
ImGui::BeginGroup();
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "Слот %d", slot);
ImGui::Text("%s", g_TexSlotNames[slot]);
if (textureSRVs[slot]) {
ImGui::Image(reinterpret_cast<ImTextureID>(textureSRVs[slot]), ImVec2(64, 64));
} else {
ImGui::Button("(нет превью)", ImVec2(64, 64));
}
if (slot > 0) {
bool isSpr = (slot >= 10);
if (ImGui::Button("Заменить")) {
ImportTextureImage(slot, isSpr);
}
ImGui::SameLine();
if (ImGui::Button("Экспорт")) {
ExportTextureImage(slot);
}
ImGui::SameLine();
if (ImGui::Button("Сброс")) {
ResetTextureSlotToDefault(slot);
}
} else {
ImGui::TextDisabled("Служебный");
}
ImGui::EndGroup();
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Аудио и звуки (WAV / MCI)")) {
ImGui::Text("Папка звуков: %s", GetSoundsDirectory().c_str());
ImGui::SameLine();
if (ImGui::Button("+ Импортировать WAV...")) {
ImportSoundAudio();
}
ImGui::SameLine();
if (ImGui::Button("■ Стоп аудио")) {
StopAudioSound();
}
ImGui::Separator();
auto soundList = GetSoundList();
if (soundList.empty()) {
ImGui::TextDisabled("В папке assets/sounds пока нет аудиофайлов WAV.");
ImGui::Text("Нажмите '+ Импортировать WAV...', чтобы добавить звуки.");
} else {
if (ImGui::BeginTable("SoundsTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Файл", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("MCI Алиас", ImGuiTableColumnFlags_WidthFixed, 140.0f);
ImGui::TableSetupColumn("Воспроизведение", ImGuiTableColumnFlags_WidthFixed, 140.0f);
ImGui::TableSetupColumn("Управление", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableHeadersRow();
for (size_t i = 0; i < soundList.size(); ++i) {
const auto& snd = soundList[i];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", snd.c_str());
ImGui::TableSetColumnIndex(1);
std::string baseName = snd;
size_t dot = baseName.find_last_of('.');
if (dot != std::string::npos) baseName = baseName.substr(0, dot);
std::string alias = "snd_" + baseName;
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", alias.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::PushID(static_cast<int>(i));
if (ImGui::Button("▶ Старт")) {
PlayAudioSound(snd);
}
ImGui::SameLine();
if (ImGui::Button("■ Стоп")) {
StopAudioSound();
}
ImGui::TableSetColumnIndex(3);
if (ImGui::Button("Удалить")) {
std::string full = GetSoundsDirectory() + snd;
DeleteFileA(full.c_str());
UpdateSoundManifest();
statusMessage = "Звук удален: " + snd;
}
ImGui::PopID();
}
ImGui::EndTable();
}
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
// -----------------------------------------------------------------------------
// Tools
// -----------------------------------------------------------------------------
bool EditorApp::SetCell(int gx, int gy, uint8_t value) {
if (gx < 0 || gx >= MAP_GRID_W || gy < 0 || gy >= MAP_GRID_H || value > 15) return false;
auto found = std::find_if(transitions.begin(), transitions.end(), [&](const LevelTransition& t) {
return t.triggerCellX == gx && t.triggerCellY == gy;
});
if (value == CELL_PORTAL) {
if (found == transitions.end()) {
if (transitions.size() >= MAX_LEVEL_TRANSITIONS) {
statusMessage = "Лимит: не более 16 переходов на карту.";
return false;
}
LevelTransition transition{};
transition.triggerCellX = static_cast<uint8_t>(gx);
transition.triggerCellY = static_cast<uint8_t>(gy);
strcpy_s(transition.targetMap, "sublevel_02.dat");
transition.targetSpawnX = transition.targetSpawnY = 2;
transition.fadeEffect = 1;
transitions.push_back(transition);
selectedTransitionIdx = static_cast<int>(transitions.size()) - 1;
} else selectedTransitionIdx = static_cast<int>(found - transitions.begin());
selectedSpriteIdx = -1;
} else if (found != transitions.end()) {
RemoveCutsceneTransition(static_cast<int>(found - transitions.begin()));
transitions.erase(found);
selectedTransitionIdx = -1;
}
map[gy][gx] = value;
return true;
}
void EditorApp::ApplyTool(int gx, int gy) {
if (gx < 0 || gx >= MAP_GRID_W || gy < 0 || gy >= MAP_GRID_H) return;
switch (currentTool) {
case TOOL_BRUSH: SetCell(gx, gy, selectedPaletteCell); break;
case TOOL_ERASER: SetCell(gx, gy, CELL_EMPTY); break;
case TOOL_FILL: FloodFill(gx, gy, map[gy][gx], selectedPaletteCell); break;
case TOOL_PLAYER_SPAWN:
if (SetCell(gx, gy, CELL_EMPTY)) { playerStartX = gx; playerStartY = gy; }
break;
case TOOL_SPRITE_SPAWN:
if (sprites.size() < MAX_LEVEL_SPRITES) {
SpriteEntry sprite{};
sprite.x = gx * 64 + 32; sprite.y = gy * 64 + 32;
sprite.textureId = 11; sprite.scale = 65536;
sprites.push_back(sprite);
selectedSpriteIdx = static_cast<int>(sprites.size()) - 1;
selectedTransitionIdx = -1;
}
break;
}
}
void EditorApp::FloodFill(int startX, int startY, uint8_t targetType, uint8_t fillType) {
if (targetType == fillType || startX < 0 || startX >= MAP_GRID_W || startY < 0 || startY >= MAP_GRID_H) return;
std::array<bool, MAP_TOTAL_CELLS> affected{};
std::vector<std::pair<int, int>> cells;
std::queue<std::pair<int, int>> pending;
pending.push({startX, startY});
while (!pending.empty()) {
auto [x, y] = pending.front(); pending.pop();
if (x < 0 || x >= MAP_GRID_W || y < 0 || y >= MAP_GRID_H) continue;
int index = y * MAP_GRID_W + x;
if (affected[index] || map[y][x] != targetType) continue;
affected[index] = true;
cells.push_back({x, y});
pending.push({x - 1, y}); pending.push({x + 1, y});
pending.push({x, y - 1}); pending.push({x, y + 1});
}
// Preflight the entire operation; a limit failure must not half-fill a room.
if (fillType == CELL_PORTAL) {
size_t retained = 0;
for (const auto& t : transitions) if (!affected[t.triggerCellY * MAP_GRID_W + t.triggerCellX]) ++retained;
if (retained + cells.size() > MAX_LEVEL_TRANSITIONS) {
statusMessage = "Заливка отменена: на карте может быть не более 16 переходов.";
return;
}
}
for (const auto& [x, y] : cells) SetCell(x, y, fillType);
}
// -----------------------------------------------------------------------------
// Hotkeys
// -----------------------------------------------------------------------------
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();
}
if (io.WantTextInput || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId)) return;
if (io.KeyCtrl) {
if (ImGui::IsKeyPressed(ImGuiKey_N, false)) { PushUndo(); InitDefaultLevel(); }
if (ImGui::IsKeyPressed(ImGuiKey_O, false)) {
std::string p = BrowseOpenFile();
if (!p.empty()) LoadLevel(p);
}
if (ImGui::IsKeyPressed(ImGuiKey_S, false)) {
if (io.KeyShift) {
std::string p = BrowseSaveFile();
if (!p.empty()) SaveLevel(p);
} else {
SaveLevel(currentFilePath);
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Z, false)) Undo();
if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) Redo();
return; // no single-letter tool hotkeys while Ctrl held
}
if (ImGui::IsKeyPressed(ImGuiKey_F4, false)) {
showAssetManagerModal = !showAssetManagerModal;
}
if (ImGui::IsKeyPressed(ImGuiKey_B, false)) currentTool = TOOL_BRUSH;
if (ImGui::IsKeyPressed(ImGuiKey_E, false)) currentTool = TOOL_ERASER;
if (ImGui::IsKeyPressed(ImGuiKey_F, false)) currentTool = TOOL_FILL;
if (ImGui::IsKeyPressed(ImGuiKey_P, false)) currentTool = TOOL_PLAYER_SPAWN;
if (ImGui::IsKeyPressed(ImGuiKey_S, false)) currentTool = TOOL_SPRITE_SPAWN;
}
// -----------------------------------------------------------------------------
// UI Rendering
// -----------------------------------------------------------------------------
void EditorApp::RenderUI() {
AdvanceCutscenePreview(ImGui::GetIO().DeltaTime);
CheckPlaytestProcess();
HandleHotkeys();
RenderMenuBar();
RenderToolbar();
RenderAssetManagerModal();
RenderCodeEditorWindow();
RenderProjectSettingsModal();
RenderExportGameModal();
if (showPlaytestError) { ImGui::OpenPopup("Не удалось запустить игру"); showPlaytestError = false; }
if (ImGui::BeginPopupModal("Не удалось запустить игру", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX()+520);
ImGui::TextUnformatted(statusMessage.c_str());
ImGui::PopTextWrapPos();
if (ImGui::Button("Понятно")) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
// Main layout
ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + 60));
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, vp->Size.y - 85));
ImGui::Begin("Workspace", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus);
// Columns: Left = Palette & Hierarchy, Middle = Grid Canvas, Right = Inspector
float leftWidth = 220.0f;
float rightWidth = 280.0f;
float canvasWidth = ImGui::GetContentRegionAvail().x - leftWidth - rightWidth - 20.0f;
// Left Panel
ImGui::BeginChild("LeftPanel", ImVec2(leftWidth, 0), true);
RenderTilePalette();
ImGui::Separator();
RenderHierarchy();
ImGui::EndChild();
ImGui::SameLine();
// Center Panel (Grid Canvas)
ImGui::BeginChild("CenterPanel", ImVec2(canvasWidth, 0), false);
float timelineHeight = showCutsceneTimeline ? (std::min)(340.0f, ImGui::GetContentRegionAvail().y * 0.48f) : 0.0f;
ImGui::BeginChild("CanvasPanel", ImVec2(0, showCutsceneTimeline ? -timelineHeight - 6 : 0), true);
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;
float sceneWidth = viewportMode == 0 ? ImGui::GetContentRegionAvail().x * 0.5f : 0;
if (viewportMode != 2) {
ImGui::BeginChild("SceneViewport", ImVec2(sceneWidth,0), true);
RenderSceneViewport();
ImGui::EndChild();
if (viewportMode == 0) ImGui::SameLine();
}
if (viewportMode != 1) {
ImGui::BeginChild("MapViewport", ImVec2(0,0), false);
RenderGridCanvas();
ImGui::EndChild();
}
ImGui::EndChild();
if (showCutsceneTimeline) {
ImGui::BeginChild("CutsceneTimeline", ImVec2(0, 0), true);
RenderCutsceneTimeline();
ImGui::EndChild();
}
ImGui::EndChild();
ImGui::SameLine();
// Right Panel (Inspector)
ImGui::BeginChild("RightPanel", ImVec2(rightWidth, 0), true);
RenderInspector();
ImGui::EndChild();
ImGui::End();
// Status Bar
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + vp->Size.y - 25));
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, 25));
ImGui::Begin("StatusBar", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar);
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "● %s", statusMessage.c_str());
ImGui::SameLine(vp->Size.x - 280);
ImGui::Text("FPS: %.1f | Zoom: %.0f%% | Undo: %zu", ImGui::GetIO().Framerate, (canvasZoom / 20.0f) * 100.0f, undoStack.size());
ImGui::End();
}
void EditorApp::RenderMenuBar() {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("Файл")) {
if (ImGui::MenuItem("Новый уровень", "Ctrl+N")) {
PushUndo();
InitDefaultLevel();
}
if (ImGui::MenuItem("Открыть уровень...", "Ctrl+O")) {
std::string p = BrowseOpenFile();
if (!p.empty()) LoadLevel(p);
}
if (ImGui::MenuItem("Сохранить уровень", "Ctrl+S")) {
SaveLevel(currentFilePath);
}
if (ImGui::MenuItem("Сохранить как...", "Ctrl+Shift+S")) {
std::string p = BrowseSaveFile();
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);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Правка")) {
if (ImGui::MenuItem("Отмена", "Ctrl+Z", false, !undoStack.empty())) Undo();
if (ImGui::MenuItem("Повтор", "Ctrl+Y", false, !redoStack.empty())) Redo();
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;
if (ImGui::MenuItem("Заливка области", "F", currentTool == TOOL_FILL)) currentTool = TOOL_FILL;
if (ImGui::MenuItem("Спавн игрока", "P", currentTool == TOOL_PLAYER_SPAWN)) currentTool = TOOL_PLAYER_SPAWN;
if (ImGui::MenuItem("Добавить спрайт", "S", currentTool == TOOL_SPRITE_SPAWN)) currentTool = TOOL_SPRITE_SPAWN;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Ассеты")) {
if (ImGui::MenuItem("Менеджер ассетов...", "F4", showAssetManagerModal)) {
showAssetManagerModal = !showAssetManagerModal;
}
ImGui::Separator();
if (ImGui::BeginMenu("Импорт текстуры стены...")) {
for (int slot = 1; slot <= 9; ++slot) {
if (slot == 6) continue; // slot 6 is open door / empty
char label[64];
snprintf(label, sizeof(label), "%s (Слот %d)", g_TexSlotNames[slot], slot);
if (ImGui::MenuItem(label)) {
ImportTextureImage(slot, false);
}
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Импорт спрайта...")) {
for (int slot = 10; slot <= 15; ++slot) {
char label[64];
snprintf(label, sizeof(label), "%s (Слот %d)", g_TexSlotNames[slot], slot);
if (ImGui::MenuItem(label)) {
ImportTextureImage(slot, true);
}
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Импорт аудио (WAV)...")) {
ImportSoundAudio();
}
ImGui::Separator();
if (ImGui::MenuItem("Перезагрузить текстуры")) {
LoadAllTexturePreviews();
statusMessage = "Все текстурные слоты перезагружены из texturepack.bin";
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Движок")) {
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();
}
ImGui::EndMainMenuBar();
}
}
void EditorApp::RenderToolbar() {
ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + 20));
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, 38));
ImGui::Begin("Toolbar", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar);
// Play / Stop Button
if (!isEngineRunning) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.65f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.80f, 0.30f, 1.0f));
if (ImGui::Button(" ▶ ЗАПУСК (F5) ", ImVec2(130, 24))) {
LaunchPlaytest();
}
ImGui::PopStyleColor(2);
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.80f, 0.20f, 0.20f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.95f, 0.25f, 0.25f, 1.0f));
if (ImGui::Button(" ■ СТОП (F5) ", ImVec2(130, 24))) {
StopPlaytest();
}
ImGui::PopStyleColor(2);
}
ImGui::SameLine();
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
ImGui::SameLine();
// Tools
auto ToolButton = [&](const char* label, ToolMode mode) {
bool active = (currentTool == mode);
if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.45f, 0.75f, 1.0f));
if (ImGui::Button(label)) currentTool = mode;
if (active) ImGui::PopStyleColor();
ImGui::SameLine();
};
ToolButton(" [B] Кисть ", TOOL_BRUSH);
ToolButton(" [E] Ластик ", TOOL_ERASER);
ToolButton(" [F] Заливка ", TOOL_FILL);
ToolButton(" [P] Игрок ", TOOL_PLAYER_SPAWN);
ToolButton(" [S] Спрайт ", TOOL_SPRITE_SPAWN);
ImGui::SameLine();
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
ImGui::SameLine();
// Undo / Redo buttons
if (ImGui::Button(" Отмена ")) Undo();
ImGui::SameLine();
if (ImGui::Button(" Повтор ")) Redo();
ImGui::SameLine();
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
ImGui::SameLine();
if (ImGui::Button("Кат-сцена (F6)")) showCutsceneTimeline = !showCutsceneTimeline;
ImGui::SameLine();
// Asset Manager button
if (ImGui::Button(" 🖼 Ассеты (F4) ")) {
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();
if (ImGui::Button(" Сохранить (Ctrl+S) ")) {
SaveLevel(currentFilePath);
}
ImGui::End();
}
void EditorApp::RenderGridCanvas() {
ImVec2 canvasP0 = ImGui::GetCursorScreenPos();
ImVec2 canvasSz = ImGui::GetContentRegionAvail();
if (canvasSz.x < 50.0f) canvasSz.x = 50.0f;
if (canvasSz.y < 50.0f) canvasSz.y = 50.0f;
ImVec2 canvasP1 = ImVec2(canvasP0.x + canvasSz.x, canvasP0.y + canvasSz.y);
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->PushClipRect(canvasP0, canvasP1, true);
// Canvas Background
drawList->AddRectFilled(canvasP0, canvasP1, IM_COL32(28, 28, 30, 255));
// Handle Pan & Zoom
ImGuiIO& io = ImGui::GetIO();
if (ImGui::IsWindowHovered()) {
// Zoom
if (io.MouseWheel != 0.0f) {
canvasZoom += io.MouseWheel * 2.0f;
if (canvasZoom < 8.0f) canvasZoom = 8.0f;
if (canvasZoom > 48.0f) canvasZoom = 48.0f;
}
// Pan with Middle Mouse Button
if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
canvasPanX += io.MouseDelta.x;
canvasPanY += io.MouseDelta.y;
}
}
float tileSize = canvasZoom;
ImVec2 gridOrigin = ImVec2(canvasP0.x + canvasPanX, canvasP0.y + canvasPanY);
// Tile Colors
auto GetCellColor = [](uint8_t type) -> ImU32 {
switch (type) {
case CELL_EMPTY: return IM_COL32(35, 35, 38, 255);
case CELL_BRICK: return IM_COL32(165, 45, 45, 255); // Red
case CELL_WOOD: return IM_COL32(135, 90, 45, 255); // Brown
case CELL_STONE: return IM_COL32(100, 100, 105, 255); // Gray
case CELL_METAL: return IM_COL32(85, 115, 140, 255); // Steel blue
case CELL_DOOR: return IM_COL32(215, 140, 30, 255); // Orange
case CELL_DOOR_OPEN: return IM_COL32(65, 175, 75, 255); // Green
case CELL_PILLAR: return IM_COL32(65, 150, 160, 255); // Turquoise
case CELL_GLASS: return IM_COL32(70, 130, 160, 255); // Glass blue
case CELL_MARBLE: return IM_COL32(200, 195, 185, 255); // Cream marble
case CELL_PORTAL: return IM_COL32(160, 50, 225, 255); // Neon purple portal
default: return IM_COL32(50, 50, 50, 255);
}
};
// 1. Draw Map Tiles
for (int y = 0; y < MAP_GRID_H; ++y) {
for (int x = 0; x < MAP_GRID_W; ++x) {
ImVec2 tileP0 = ImVec2(gridOrigin.x + x * tileSize, gridOrigin.y + y * tileSize);
ImVec2 tileP1 = ImVec2(tileP0.x + tileSize, tileP0.y + tileSize);
// Skip offscreen tiles
if (tileP1.x < canvasP0.x || tileP0.x > canvasP1.x ||
tileP1.y < canvasP0.y || tileP0.y > canvasP1.y) {
continue;
}
ImU32 col = GetCellColor(map[y][x]);
drawList->AddRectFilled(tileP0, tileP1, col);
drawList->AddRect(tileP0, tileP1, IM_COL32(50, 50, 55, 120), 0.0f, 0, 1.0f);
if (map[y][x] == CELL_PORTAL) {
float cx = tileP0.x + tileSize * 0.5f;
float cy = tileP0.y + tileSize * 0.5f;
drawList->AddCircle(ImVec2(cx, cy), tileSize * 0.35f, IM_COL32(255, 255, 255, 230), 0, 1.5f);
drawList->AddCircleFilled(ImVec2(cx, cy), tileSize * 0.15f, IM_COL32(255, 220, 100, 255));
}
}
}
// Grid outer border
ImVec2 mapEnd = ImVec2(gridOrigin.x + MAP_GRID_W * tileSize, gridOrigin.y + MAP_GRID_H * tileSize);
drawList->AddRect(gridOrigin, mapEnd, IM_COL32(100, 200, 255, 200), 0.0f, 0, 2.0f);
// 2. Draw Sprites (Entities) - raw world units -> cell coords
ImVec2 mousePos = io.MousePos;
int hoveredSprite = -1;
for (size_t i = 0; i < sprites.size(); ++i) {
float sx = sprites[i].x / 64.0f;
float sy = sprites[i].y / 64.0f;
ImVec2 scPos = ImVec2(gridOrigin.x + sx * tileSize, gridOrigin.y + sy * tileSize);
// Check hover (within diamond radius)
float dx = mousePos.x - scPos.x;
float dy = mousePos.y - scPos.y;
float r = tileSize * 0.4f;
if (std::fabs(dx) + std::fabs(dy) < r) {
hoveredSprite = static_cast<int>(i);
}
// 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) {
borderCol = IM_COL32(255, 255, 0, 255);
fillCol = IM_COL32(255, 255, 255, 240);
} else if (static_cast<int>(i) == hoveredSprite) {
borderCol = IM_COL32(255, 255, 120, 255);
}
drawList->AddQuadFilled(
ImVec2(scPos.x, scPos.y - r),
ImVec2(scPos.x + r, scPos.y),
ImVec2(scPos.x, scPos.y + r),
ImVec2(scPos.x - r, scPos.y),
fillCol
);
drawList->AddQuad(
ImVec2(scPos.x, scPos.y - r),
ImVec2(scPos.x + r, scPos.y),
ImVec2(scPos.x, scPos.y + r),
ImVec2(scPos.x - r, scPos.y),
borderCol
);
}
// 3. Draw Player Spawn Point & Direction Cone
{
ImVec2 pPos = ImVec2(gridOrigin.x + (playerStartX + 0.5f) * tileSize,
gridOrigin.y + (playerStartY + 0.5f) * tileSize);
float pRadius = tileSize * 0.45f;
// Player Circle (Bright Green)
drawList->AddCircleFilled(pPos, pRadius, IM_COL32(0, 255, 120, 220));
drawList->AddCircle(pPos, pRadius, IM_COL32(255, 255, 255, 255), 0, 1.5f);
// Direction Arrow (rad = angle * 2 * PI / 512)
float rad = (playerStartAngle * 3.14159265f) / 256.0f;
ImVec2 dirTip = ImVec2(pPos.x + std::cos(rad) * pRadius * 1.8f,
pPos.y + std::sin(rad) * pRadius * 1.8f);
drawList->AddLine(pPos, dirTip, IM_COL32(255, 40, 40, 255), 2.5f);
drawList->AddCircleFilled(dirTip, 3.5f, IM_COL32(255, 40, 40, 255));
}
DrawCutscenePath(drawList, gridOrigin.x, gridOrigin.y, tileSize);
// 4. Mouse Input Handling on Canvas
if (ImGui::IsWindowHovered() && !cutscenePreviewPlaying) {
int hoverGx = static_cast<int>(std::floor((mousePos.x - gridOrigin.x) / tileSize));
int hoverGy = static_cast<int>(std::floor((mousePos.y - gridOrigin.y) / tileSize));
bool inGrid = (hoverGx >= 0 && hoverGx < MAP_GRID_W && hoverGy >= 0 && hoverGy < MAP_GRID_H);
if (inGrid) {
// Highlight hovered tile
ImVec2 hP0 = ImVec2(gridOrigin.x + hoverGx * tileSize, gridOrigin.y + hoverGy * tileSize);
ImVec2 hP1 = ImVec2(hP0.x + tileSize, hP0.y + tileSize);
drawList->AddRect(hP0, hP1, IM_COL32(255, 255, 255, 200), 0.0f, 0, 2.0f);
}
if (placeCameraPoint && inGrid && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
selectedCutsceneEvent >= 0 && selectedCutsceneEvent < static_cast<int>(cutscene.events.size())) {
if (!IsWalkableCell(map[hoverGy][hoverGx])) {
statusMessage = "Выберите точку камеры вне стены.";
drawList->PopClipRect(); return;
}
PushUndo();
auto& event = cutscene.events[selectedCutsceneEvent];
event.paramA = hoverGx * 64 + 32; event.paramB = hoverGy * 64 + 32;
placeCameraPoint = false;
NormalizeCutscene(); ScrubCutscene(cutsceneCursor);
drawList->PopClipRect(); return;
}
if (placeCameraPoint) { drawList->PopClipRect(); return; }
// Left Click: start action (drag sprite OR paint)
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (currentTool == TOOL_SPRITE_SPAWN && hoveredSprite >= 0) {
// Start dragging existing sprite
draggingSpriteIdx = hoveredSprite;
selectedSpriteIdx = hoveredSprite;
selectedTransitionIdx = -1;
PushUndo();
} else if (inGrid) {
// If clicking an existing portal tile, select it for inspection!
if (map[hoverGy][hoverGx] == CELL_PORTAL) {
for (size_t i = 0; i < transitions.size(); ++i) {
if (transitions[i].triggerCellX == hoverGx && transitions[i].triggerCellY == hoverGy) {
selectedTransitionIdx = static_cast<int>(i);
selectedSpriteIdx = -1;
break;
}
}
}
PushUndo(); // snapshot before paint stroke
strokeSnapshotTaken = true;
ApplyTool(hoverGx, hoverGy);
}
}
// Left Drag: continue painting OR move sprite
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (draggingSpriteIdx >= 0) {
float wx = (mousePos.x - gridOrigin.x) / tileSize;
float wy = (mousePos.y - gridOrigin.y) / tileSize;
sprites[draggingSpriteIdx].x = static_cast<int32_t>(wx * 64.0f);
sprites[draggingSpriteIdx].y = static_cast<int32_t>(wy * 64.0f);
} else if (strokeSnapshotTaken && inGrid) {
ApplyTool(hoverGx, hoverGy); // no extra snapshot during stroke
}
}
// Left Release: end drag / stroke
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
draggingSpriteIdx = -1;
strokeSnapshotTaken = false;
}
// Right-click erase follows the same portal lifecycle as every other tool.
if (inGrid && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
PushUndo();
SetCell(hoverGx, hoverGy, CELL_EMPTY);
} else if (inGrid && ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
SetCell(hoverGx, hoverGy, CELL_EMPTY);
}
} else {
// Mouse left canvas: end drag / stroke
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
draggingSpriteIdx = -1;
strokeSnapshotTaken = false;
}
}
drawList->PopClipRect();
}
void EditorApp::RenderTilePalette() {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "ПАЛИТРА ТАЙЛОВ");
ImGui::Separator();
auto PaletteButton = [&](const char* name, CellType type, ImVec4 col, int slot) {
bool selected = (selectedPaletteCell == type && currentTool == TOOL_BRUSH);
ImGui::PushStyleColor(ImGuiCol_Button, col);
if (selected) {
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 0.85f, 0.2f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
}
ImGui::PushID(slot);
if (type != CELL_PORTAL && slot > 0 && slot < 16 && textureSRVs[slot]) {
ImGui::Image(reinterpret_cast<ImTextureID>(textureSRVs[slot]), ImVec2(24, 24));
ImGui::SameLine();
}
if (ImGui::Button(name, ImVec2(-1, 28))) {
selectedPaletteCell = type;
currentTool = TOOL_BRUSH;
}
if (type != CELL_PORTAL && slot > 0 && slot < 16) {
if (ImGui::BeginPopupContextItem()) {
ImGui::Text("Текстура: %s (Слот %d)", g_TexSlotNames[slot], slot);
ImGui::Separator();
if (ImGui::MenuItem("Заменить текстуру (PNG/JPG)...")) {
ImportTextureImage(slot, false);
}
if (ImGui::MenuItem("Экспорт текстуры в PNG...")) {
ExportTextureImage(slot);
}
if (ImGui::MenuItem("Сбросить к процедурной")) {
ResetTextureSlotToDefault(slot);
}
ImGui::Separator();
if (ImGui::MenuItem("Открыть в Менеджере ассетов (F4)")) {
showAssetManagerModal = true;
}
ImGui::EndPopup();
}
}
ImGui::PopID();
if (selected) {
ImGui::PopStyleVar();
ImGui::PopStyleColor();
}
ImGui::PopStyleColor();
};
PaletteButton("1. Кирпичная стена", CELL_BRICK, ImVec4(0.65f, 0.20f, 0.20f, 1.0f), 1);
PaletteButton("2. Деревянная стена", CELL_WOOD, ImVec4(0.55f, 0.35f, 0.20f, 1.0f), 2);
PaletteButton("3. Каменная стена", CELL_STONE, ImVec4(0.40f, 0.40f, 0.45f, 1.0f), 3);
PaletteButton("4. Металлическая стена", CELL_METAL, ImVec4(0.35f, 0.45f, 0.55f, 1.0f), 4);
PaletteButton("5. Дверь (E: открыть)", CELL_DOOR, ImVec4(0.85f, 0.55f, 0.15f, 1.0f), 5);
PaletteButton("7. Колонна / Мозаика", CELL_PILLAR, ImVec4(0.25f, 0.60f, 0.65f, 1.0f), 7);
PaletteButton("8. Стекло", CELL_GLASS, ImVec4(0.28f, 0.50f, 0.65f, 1.0f), 8);
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);
}
void EditorApp::RenderHierarchy() {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "СУЩНОСТИ (ИЕРАРХИЯ)");
ImGui::Separator();
ImGui::Text("Игрок: Spawn (%d, %d)", playerStartX, playerStartY);
ImGui::Separator();
ImGui::Text("Спрайты на карте (%zu / %d):", sprites.size(), MAX_LEVEL_SPRITES);
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), "[%s] #%zu (%d, %d)", typeTag, i + 1, gx, gy);
if (ImGui::Selectable(label, selectedSpriteIdx == static_cast<int>(i))) {
selectedSpriteIdx = static_cast<int>(i);
selectedTransitionIdx = -1;
}
}
ImGui::Separator();
ImGui::Text("Порталы перехода (%zu / %d):", transitions.size(), MAX_LEVEL_TRANSITIONS);
for (size_t i = 0; i < transitions.size(); ++i) {
char label[96];
snprintf(label, sizeof(label), "Портал #%zu (%d, %d) -> %s",
i + 1, transitions[i].triggerCellX, transitions[i].triggerCellY,
transitions[i].targetMap[0] ? transitions[i].targetMap : "<?>");
if (ImGui::Selectable(label, selectedTransitionIdx == static_cast<int>(i))) {
selectedTransitionIdx = static_cast<int>(i);
selectedSpriteIdx = -1;
}
}
}
void EditorApp::RenderInspector() {
if (showCutsceneTimeline && selectedCutsceneEvent >= 0) {
RenderCutsceneInspector();
ImGui::Separator();
}
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "ИНСПЕКТОР СВОЙСТВ");
ImGui::Separator();
if (ImGui::CollapsingHeader("Спавн игрока", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::SliderInt("Spawn X", &playerStartX, 0, MAP_GRID_W - 1);
ImGui::SliderInt("Spawn Y", &playerStartY, 0, MAP_GRID_H - 1);
float degrees = (playerStartAngle * 360.0f) / 512.0f;
if (ImGui::SliderFloat("Угол обзора (°)", &degrees, 0.0f, 360.0f, "%.0f°")) {
playerStartAngle = static_cast<int>((degrees * 512.0f) / 360.0f) & 511;
}
const char* dirName = "Восток (East)";
if (playerStartAngle >= 64 && playerStartAngle < 192) dirName = "Юг (South)";
else if (playerStartAngle >= 192 && playerStartAngle < 320) dirName = "Запад (West)";
else if (playerStartAngle >= 320 && playerStartAngle < 448) dirName = "Север (North)";
ImGui::Text("Направление: %s", dirName);
}
if (ImGui::CollapsingHeader("Окружение сцены", ImGuiTreeNodeFlags_DefaultOpen)) {
// Floor & Ceiling Color Pickers
float ceilCol[3] = {
((ceilingColor >> 16) & 0xFF) / 255.0f,
((ceilingColor >> 8) & 0xFF) / 255.0f,
(ceilingColor & 0xFF) / 255.0f
};
if (ImGui::ColorEdit3("Цвет потолка", ceilCol)) {
ceilingColor = (static_cast<uint32_t>(ceilCol[0] * 255.0f) << 16) |
(static_cast<uint32_t>(ceilCol[1] * 255.0f) << 8) |
static_cast<uint32_t>(ceilCol[2] * 255.0f);
}
float floorCol[3] = {
((floorColor >> 16) & 0xFF) / 255.0f,
((floorColor >> 8) & 0xFF) / 255.0f,
(floorColor & 0xFF) / 255.0f
};
if (ImGui::ColorEdit3("Цвет пола", floorCol)) {
floorColor = (static_cast<uint32_t>(floorCol[0] * 255.0f) << 16) |
(static_cast<uint32_t>(floorCol[1] * 255.0f) << 8) |
static_cast<uint32_t>(floorCol[2] * 255.0f);
}
}
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
if (ImGui::CollapsingHeader("Выбранный спрайт", ImGuiTreeNodeFlags_DefaultOpen)) {
auto& spr = sprites[selectedSpriteIdx];
// Position (raw world units -> cell coords for UI)
float sx = spr.x / 64.0f;
float sy = spr.y / 64.0f;
if (ImGui::SliderFloat("Sprite X", &sx, 0.0f, MAP_GRID_W - 0.01f)) {
spr.x = static_cast<int32_t>(sx * 64.0f);
}
if (ImGui::SliderFloat("Sprite Y", &sy, 0.0f, MAP_GRID_H - 0.01f)) {
spr.y = static_cast<int32_t>(sy * 64.0f);
}
// Texture slot
int tid = static_cast<int>(spr.textureId & 15);
if (ImGui::Combo("Текстура", &tid, g_TexSlotNames, 16)) {
spr.textureId = static_cast<uint32_t>(tid);
}
if (tid >= 0 && tid < 16 && textureSRVs[tid]) {
ImGui::Text("Превью спрайта:");
ImGui::Image(reinterpret_cast<ImTextureID>(textureSRVs[tid]), ImVec2(64, 64));
ImGui::SameLine();
ImGui::BeginGroup();
if (ImGui::Button("Заменить PNG...")) {
ImportTextureImage(tid, true);
}
if (ImGui::Button("Экспорт PNG")) {
ExportTextureImage(tid);
}
if (ImGui::Button("Сброс")) {
ResetTextureSlotToDefault(tid);
}
ImGui::EndGroup();
}
// Scale (16.16 fixed -> float for UI)
float scaleF = spr.scale / 65536.0f;
if (ImGui::SliderFloat("Масштаб", &scaleF, 0.25f, 4.0f, "%.2f")) {
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);
selectedSpriteIdx = -1;
}
}
}
if (selectedTransitionIdx >= 0 && selectedTransitionIdx < static_cast<int>(transitions.size())) {
if (ImGui::CollapsingHeader("Свойства портала перехода", ImGuiTreeNodeFlags_DefaultOpen)) {
auto& trans = transitions[selectedTransitionIdx];
ImGui::TextColored(ImVec4(0.7f, 0.4f, 1.0f, 1.0f), "Триггер-ячейка: (%d, %d)", trans.triggerCellX, trans.triggerCellY);
ImGui::TextWrapped("Файл подуровня должен лежать рядом с текущей картой.");
// Target map file
char mapBuf[32];
strncpy_s(mapBuf, trans.targetMap, sizeof(mapBuf));
if (ImGui::InputText("Файл карты (.dat)", mapBuf, sizeof(mapBuf))) {
strncpy_s(trans.targetMap, mapBuf, sizeof(trans.targetMap));
}
ImGui::SameLine();
if (ImGui::Button("Обзор...")) {
std::string path = BrowseOpenFile();
if (!path.empty()) {
size_t lastSlash = path.find_last_of("\\/");
std::string fname = (lastSlash != std::string::npos) ? path.substr(lastSlash + 1) : path;
if (IsMapFilename(fname.c_str())) {
strcpy_s(trans.targetMap, fname.c_str());
} else {
statusMessage = "Имя подуровня: ASCII, до 31 символа, расширение .dat.";
}
}
}
// Target spawn coords
int spX = trans.targetSpawnX;
int spY = trans.targetSpawnY;
if (ImGui::SliderInt("Целевой Spawn X", &spX, 0, MAP_GRID_W - 1)) trans.targetSpawnX = static_cast<uint16_t>(spX);
if (ImGui::SliderInt("Целевой Spawn Y", &spY, 0, MAP_GRID_H - 1)) trans.targetSpawnY = static_cast<uint16_t>(spY);
float targetDeg = (trans.targetAngle * 360.0f) / 512.0f;
if (ImGui::SliderFloat("Целевой угол (°)", &targetDeg, 0.0f, 360.0f, "%.0f°")) {
trans.targetAngle = static_cast<uint16_t>(((int)((targetDeg * 512.0f) / 360.0f)) & 511);
}
bool fadeB = (trans.fadeEffect != 0);
if (ImGui::Checkbox("Плавное затемнение экрана (Fade to Black)", &fadeB)) {
trans.fadeEffect = fadeB ? 1 : 0;
}
ImGui::Separator();
if (ImGui::Button("Перейти к подуровню в редакторе")) {
const auto directory = fs::path(currentFilePath).parent_path();
std::string targetPath = (directory / trans.targetMap).string();
if (SaveLevel(currentFilePath)) LoadLevel(targetPath);
}
ImGui::SameLine();
if (ImGui::Button("Удалить портал")) {
PushUndo();
if (trans.triggerCellX < MAP_GRID_W && trans.triggerCellY < MAP_GRID_H) {
if (map[trans.triggerCellY][trans.triggerCellX] == CELL_PORTAL) {
map[trans.triggerCellY][trans.triggerCellX] = CELL_EMPTY;
}
}
RemoveCutsceneTransition(selectedTransitionIdx);
transitions.erase(transitions.begin() + selectedTransitionIdx);
selectedTransitionIdx = -1;
}
}
}
}
#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();
}
}