2331 lines
100 KiB
C++
2331 lines
100 KiB
C++
#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("Угол обзора (°)", °rees, 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();
|
||
}
|
||
}
|
||
|