diff --git a/editor/editor.exe b/editor/editor.exe index df68f66..d0db4a1 100644 Binary files a/editor/editor.exe and b/editor/editor.exe differ diff --git a/editor/src/editor_app.cpp b/editor/src/editor_app.cpp index c71e983..cd7ae90 100644 --- a/editor/src/editor_app.cpp +++ b/editor/src/editor_app.cpp @@ -1336,20 +1336,28 @@ void EditorApp::ApplyTool(int gx, int gy) { case TOOL_PLAYER_SPAWN: if (SetCell(gx, gy, CELL_EMPTY)) { playerStartX = gx; playerStartY = gy; } break; + case TOOL_LIGHT_SPAWN: + if (lightSources.size() < MAX_EDITOR_LIGHTS) { + if (lightingMode == 0) lightingMode = 2; + EditorLightSource nl; + nl.x = gx * 64 + 32; nl.y = gy * 64 + 32; + nl.radius = 240; nl.intensity = 220; nl.curIntensity = 220; + nl.color = 0x00FFB040; + nl.lightType = LIGHT_TYPE_POINT; + nl.flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS | LIGHT_FLAG_FLICKER; + lightSources.push_back(nl); + selectedLightIdx = static_cast(lightSources.size()) - 1; + selectedSpriteIdx = -1; selectedTransitionIdx = -1; + if (autoBakeLighting) BakeLighting(); + statusMessage = "Источник света создан на клетке (" + std::to_string(gx) + ", " + std::to_string(gy) + ")."; + } else { + statusMessage = "Лимит источников света карты: 13."; + } + break; case TOOL_SPRITE_SPAWN: if (pendingSpawnArchetype == 99) { - if (lightSources.size() < MAX_EDITOR_LIGHTS) { - if(lightingMode==0)lightingMode=2; - EditorLightSource nl; - nl.x = gx * 64 + 32; nl.y = gy * 64 + 32; - lightSources.push_back(nl); - selectedLightIdx = static_cast(lightSources.size()) - 1; - selectedSpriteIdx = -1; selectedTransitionIdx = -1; - BakeLighting(); - statusMessage = "Источник света создан на карте."; - } else { - statusMessage = "Лимит источников света карты: 13."; - } + currentTool = TOOL_LIGHT_SPAWN; + ApplyTool(gx, gy); break; } if (sprites.size() < MAX_LEVEL_SPRITES) { @@ -1418,7 +1426,10 @@ void EditorApp::DuplicateSelectedEntity() { if(selectedLightIdx>=0 && selectedLightIdx(lightSources.size())) { if(lightSources.size()>=MAX_EDITOR_LIGHTS){statusMessage="Лимит: 13 источников карты.";return;} auto copy=lightSources[selectedLightIdx];copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32); - PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast(lightSources.size())-1;return; + PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast(lightSources.size())-1; + if(autoBakeLighting) BakeLighting(); + statusMessage = "Источник света дублирован (Ctrl+D)."; + return; } if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast(sprites.size())) { if (sprites.size() >= MAX_LEVEL_SPRITES) { @@ -1437,7 +1448,11 @@ void EditorApp::DuplicateSelectedEntity() { void EditorApp::DeleteSelectedEntity() { if(selectedLightIdx>=0 && selectedLightIdx(lightSources.size())) { - PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=draggingLightIdx=-1;return; + PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx); + selectedLightIdx=draggingLightIdx=draggingLightRadiusIdx=-1; + if(autoBakeLighting) BakeLighting(); + statusMessage = "Источник света удален (Del)."; + return; } if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast(sprites.size())) { PushUndo(); @@ -1517,6 +1532,7 @@ void EditorApp::HandleHotkeys() { 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; + if (ImGui::IsKeyPressed(ImGuiKey_L, false)) currentTool = TOOL_LIGHT_SPAWN; } // ----------------------------------------------------------------------------- @@ -1724,6 +1740,7 @@ void EditorApp::RenderMenuBar() { 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; + if (ImGui::MenuItem("Источник света", "L", currentTool == TOOL_LIGHT_SPAWN)) currentTool = TOOL_LIGHT_SPAWN; ImGui::EndMenu(); } @@ -1915,6 +1932,7 @@ void EditorApp::RenderToolbar() { ToolButton(" [F] Заливка ", TOOL_FILL); ToolButton(" [P] Игрок ", TOOL_PLAYER_SPAWN); ToolButton(" [S] Спрайт ", TOOL_SPRITE_SPAWN); + ToolButton(" [L] Свет ", TOOL_LIGHT_SPAWN); ImGui::SameLine(); ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); @@ -2108,6 +2126,21 @@ void EditorApp::RenderGridCanvas() { // 2b. Draw Light Sources on Map Canvas int hoveredLight = -1; + int hoveredRadiusHandle = -1; + + // Check if radius handle of selected light is hovered + if (selectedLightIdx >= 0 && selectedLightIdx < static_cast(lightSources.size())) { + float lx = lightSources[selectedLightIdx].x / 64.0f; + float ly = lightSources[selectedLightIdx].y / 64.0f; + ImVec2 lcPos = ImVec2(gridOrigin.x + lx * tileSize, gridOrigin.y + ly * tileSize); + float radPix = (lightSources[selectedLightIdx].radius / 64.0f) * tileSize; + ImVec2 hPos = ImVec2(lcPos.x + radPix, lcPos.y); + float hdx = mousePos.x - hPos.x, hdy = mousePos.y - hPos.y; + if (std::sqrt(hdx * hdx + hdy * hdy) < 9.0f) { + hoveredRadiusHandle = selectedLightIdx; + } + } + for (size_t i = 0; i < lightSources.size(); ++i) { float lx = lightSources[i].x / 64.0f; float ly = lightSources[i].y / 64.0f; @@ -2116,26 +2149,43 @@ void EditorApp::RenderGridCanvas() { float dx = mousePos.x - lcPos.x; float dy = mousePos.y - lcPos.y; float r = tileSize * 0.45f; - if (std::sqrt(dx * dx + dy * dy) < r) { + if (std::sqrt(dx * dx + dy * dy) < r && hoveredRadiusHandle < 0) { hoveredLight = static_cast(i); } + uint32_t c = lightSources[i].color; + uint8_t cr = (c >> 16) & 0xFF, cg = (c >> 8) & 0xFF, cb = c & 0xFF; + if (static_cast(i) == selectedLightIdx || static_cast(i) == hoveredLight) { float radPix = (lightSources[i].radius / 64.0f) * tileSize; - drawList->AddCircle(lcPos, radPix, IM_COL32(255, 210, 40, 160), 48, 1.5f); - drawList->AddCircleFilled(lcPos, radPix, IM_COL32(255, 210, 40, 18)); + drawList->AddCircle(lcPos, radPix, IM_COL32(cr, cg, cb, 180), 48, 1.8f); + drawList->AddCircleFilled(lcPos, radPix, IM_COL32(cr, cg, cb, 22)); + + // Radius handle for selected light + if (static_cast(i) == selectedLightIdx) { + ImVec2 hPos = ImVec2(lcPos.x + radPix, lcPos.y); + bool hHover = (hoveredRadiusHandle == selectedLightIdx || draggingLightRadiusIdx == selectedLightIdx); + drawList->AddCircleFilled(hPos, hHover ? 7.0f : 5.0f, IM_COL32(255, 255, 255, 255)); + drawList->AddCircle(hPos, hHover ? 7.0f : 5.0f, IM_COL32(cr, cg, cb, 255), 16, 2.0f); + } } - uint32_t c = lightSources[i].color; - ImU32 lightCol = IM_COL32((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF, 255); - ImU32 haloCol = IM_COL32((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF, 70); + ImU32 lightCol = IM_COL32(cr, cg, cb, 255); + ImU32 haloCol = IM_COL32(cr, cg, cb, 70); drawList->AddCircleFilled(lcPos, tileSize * 0.38f, haloCol); drawList->AddCircleFilled(lcPos, tileSize * 0.22f, lightCol); drawList->AddCircleFilled(lcPos, tileSize * 0.08f, IM_COL32(255, 255, 255, 255)); + // Light type badge: S = Static, F = Torch Flicker, P = Pulse, D = Dynamic + char badge = (lightSources[i].lightType == LIGHT_TYPE_STATIC) ? 'S' : + (lightSources[i].lightType == LIGHT_TYPE_PULSE) ? 'P' : + (lightSources[i].flags & LIGHT_FLAG_FLICKER) ? 'F' : 'D'; + char badgeStr[2] = { badge, 0 }; + drawList->AddText(ImVec2(lcPos.x - 3.5f, lcPos.y - 6.5f), IM_COL32(0, 0, 0, 220), badgeStr); + if (static_cast(i) == selectedLightIdx) { - drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 255, 0, 255), 24, 2.0f); + drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 230, 40, 255), 24, 2.5f); } else if (static_cast(i) == hoveredLight) { drawList->AddCircle(lcPos, tileSize * 0.42f, IM_COL32(255, 255, 200, 200), 24, 1.5f); } @@ -2188,18 +2238,24 @@ void EditorApp::RenderGridCanvas() { drawList->PopClipRect(); return; } if (placeCameraPoint) { drawList->PopClipRect(); return; } - // Left Click: start action (drag light OR drag sprite OR paint) + // Left Click: start action (drag radius OR drag light OR drag sprite OR paint) if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - if (hoveredLight >= 0) { + if (hoveredRadiusHandle >= 0) { + draggingLightRadiusIdx = hoveredRadiusHandle; + PushUndo(); + } else if (hoveredLight >= 0) { draggingLightIdx = hoveredLight; selectedLightIdx = hoveredLight; selectedSpriteIdx = -1; selectedTransitionIdx = -1; PushUndo(); + } else if (currentTool == TOOL_LIGHT_SPAWN && inGrid) { + PushUndo(); + ApplyTool(hoverGx, hoverGy); } else if (currentTool == TOOL_SPRITE_SPAWN && hoveredSprite >= 0) { // Start dragging existing sprite draggingSpriteIdx = hoveredSprite; - selectedSpriteIdx = hoveredSprite; selectedLightIdx=-1; + selectedSpriteIdx = hoveredSprite; selectedLightIdx = -1; selectedTransitionIdx = -1; PushUndo(); } else if (inGrid) { @@ -2207,7 +2263,7 @@ void EditorApp::RenderGridCanvas() { 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(i); selectedLightIdx=-1; + selectedTransitionIdx = static_cast(i); selectedLightIdx = -1; selectedSpriteIdx = -1; break; } @@ -2219,13 +2275,24 @@ void EditorApp::RenderGridCanvas() { } } - // Left Drag: continue painting OR move light OR move sprite + // Left Drag: continue painting OR move light OR resize radius OR move sprite if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - if (draggingLightIdx >= 0) { + if (draggingLightRadiusIdx >= 0 && draggingLightRadiusIdx < static_cast(lightSources.size())) { + float lx = lightSources[draggingLightRadiusIdx].x / 64.0f; + float ly = lightSources[draggingLightRadiusIdx].y / 64.0f; + ImVec2 lcPos = ImVec2(gridOrigin.x + lx * tileSize, gridOrigin.y + ly * tileSize); + float dx = mousePos.x - lcPos.x; + float dy = mousePos.y - lcPos.y; + float distPx = std::sqrt(dx * dx + dy * dy); + int newRadius = std::clamp(static_cast(distPx / tileSize * 64.0f), 32, 2048); + lightSources[draggingLightRadiusIdx].radius = newRadius; + if (autoBakeLighting && lightSources[draggingLightRadiusIdx].lightType == LIGHT_TYPE_STATIC) BakeLighting(); + } else if (draggingLightIdx >= 0 && draggingLightIdx < static_cast(lightSources.size())) { float wx = (mousePos.x - gridOrigin.x) / tileSize; float wy = (mousePos.y - gridOrigin.y) / tileSize; lightSources[draggingLightIdx].x = std::clamp(static_cast(wx * 64.0f), 32, (MAP_GRID_W - 1) * 64); lightSources[draggingLightIdx].y = std::clamp(static_cast(wy * 64.0f), 32, (MAP_GRID_H - 1) * 64); + if (autoBakeLighting && lightSources[draggingLightIdx].lightType == LIGHT_TYPE_STATIC) BakeLighting(); } else if (draggingSpriteIdx >= 0) { float wx = (mousePos.x - gridOrigin.x) / tileSize; float wy = (mousePos.y - gridOrigin.y) / tileSize; @@ -2238,12 +2305,16 @@ void EditorApp::RenderGridCanvas() { // Left Release: end drag / stroke if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - if (draggingLightIdx >= 0) { + if (draggingLightIdx >= 0 || draggingLightRadiusIdx >= 0) { draggingLightIdx = -1; + draggingLightRadiusIdx = -1; BakeLighting(); } draggingSpriteIdx = -1; - strokeSnapshotTaken = false; + if (strokeSnapshotTaken) { + strokeSnapshotTaken = false; + if (autoBakeLighting) BakeLighting(); + } } // Right-click erase follows the same portal lifecycle as every other tool. @@ -2357,7 +2428,22 @@ void EditorApp::RenderTilePalette() { SpawnerBtn("+ Переключатель [E]", ENTITY_ARCHETYPE_SWITCH, ImVec4(0.70f, 0.40f, 0.10f, 1.0f)); SpawnerBtn("+ Бочка (Взрывная)", ENTITY_ARCHETYPE_BARREL, ImVec4(0.70f, 0.25f, 0.10f, 1.0f)); SpawnerBtn("+ Декорация (Проп)", ENTITY_ARCHETYPE_PROP, ImVec4(0.35f, 0.35f, 0.40f, 1.0f)); - SpawnerBtn("+ Источник света", 99, ImVec4(0.85f, 0.60f, 0.10f, 1.0f)); + + bool lightToolActive = (currentTool == TOOL_LIGHT_SPAWN); + if (lightToolActive) { + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + } + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.85f, 0.60f, 0.10f, 1.0f)); + if (ImGui::Button("+ Источник света [L]", ImVec2(-1, 24))) { + currentTool = TOOL_LIGHT_SPAWN; + statusMessage = "Режим размещения света: кликните на карте для создания источника."; + } + ImGui::PopStyleColor(); + if (lightToolActive) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } } void EditorApp::RenderHierarchy() { @@ -2404,18 +2490,9 @@ void EditorApp::RenderHierarchy() { AddEntityItem("🕹 Рычаг / Переключатель", ENTITY_ARCHETYPE_SWITCH); AddEntityItem("💥 Взрывная бочка", ENTITY_ARCHETYPE_BARREL); AddEntityItem("🏺 Декорация / Проп", ENTITY_ARCHETYPE_PROP); - if (ImGui::MenuItem("💡 Источник света")) { - if (lightSources.size() < MAX_EDITOR_LIGHTS) { - PushUndo(); - if(lightingMode==0)lightingMode=2; - EditorLightSource nl; - nl.x = (playerStartX + 1) * 64 + 32; nl.y = (playerStartY + 1) * 64 + 32; - lightSources.push_back(nl); - selectedLightIdx = static_cast(lightSources.size()) - 1; - selectedSpriteIdx = -1; selectedTransitionIdx = -1; - BakeLighting(); - statusMessage = "Источник света создан на сцене."; - } + if (ImGui::MenuItem("💡 Источник света [L]")) { + currentTool = TOOL_LIGHT_SPAWN; + statusMessage = "Режим размещения света: кликните на карте для создания источника."; } ImGui::EndPopup(); } @@ -2533,13 +2610,51 @@ void EditorApp::RenderHierarchy() { ImGui::TreePop(); } - if(ImGui::TreeNodeEx("Источники света",ImGuiTreeNodeFlags_DefaultOpen)) { - for(int i=0;i(lightSources.size());++i) { - char label[100];snprintf(label,sizeof(label),"Свет #%d: %s##light_%d",i+1, - lightSources[i].lightType==LIGHT_TYPE_STATIC?"постоянный":"динамический",i); - if(ImGui::Selectable(label,selectedLightIdx==i)){selectedLightIdx=i;selectedSpriteIdx=selectedTransitionIdx=-1;} + if(ImGui::TreeNodeEx("💡 Источники света", ImGuiTreeNodeFlags_DefaultOpen)) { + for(int i = 0; i < static_cast(lightSources.size()); ++i) { + const auto& ls = lightSources[i]; + const char* typeName = (ls.lightType == LIGHT_TYPE_STATIC) ? "Постоянный" : + (ls.lightType == LIGHT_TYPE_PULSE) ? "Сирена" : + (ls.flags & LIGHT_FLAG_FLICKER) ? "Факел" : "Точечный"; + char label[128]; + snprintf(label, sizeof(label), "Свет #%d: %s (%d, %d)##light_%d", + i + 1, typeName, ls.x / 64, ls.y / 64, i); + if (!MatchesSearch(label)) continue; + + ImGui::PushID(i); + uint32_t c = ls.color; + ImVec4 dotColor = ImVec4(((c >> 16) & 255) / 255.f, ((c >> 8) & 255) / 255.f, (c & 255) / 255.f, 1.0f); + ImGui::ColorButton("##dot", dotColor, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoInputs, ImVec2(12, 12)); + ImGui::SameLine(); + if (ImGui::Selectable(label, selectedLightIdx == i)) { + selectedLightIdx = i; + selectedSpriteIdx = selectedTransitionIdx = -1; + } + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("🎯 Сфокусировать", "F")) { + selectedLightIdx = i; selectedSpriteIdx = selectedTransitionIdx = -1; + FocusSelectedEntity(); + } + if (ImGui::MenuItem("📋 Дублировать", "Ctrl+D")) { + selectedLightIdx = i; selectedSpriteIdx = selectedTransitionIdx = -1; + DuplicateSelectedEntity(); + } + if (ImGui::MenuItem("🧲 Выровнять по центру клетки")) { + SnapLightToGrid(i); + } + ImGui::Separator(); + if (ImGui::MenuItem("🗑 Удалить источник", "Del")) { + selectedLightIdx = i; + DeleteSelectedEntity(); + } + ImGui::EndPopup(); + } + ImGui::PopID(); + } + if (ImGui::SmallButton("+ Добавить свет [L]")) { + currentTool = TOOL_LIGHT_SPAWN; + statusMessage = "Режим размещения света: кликните на карте для создания источника."; } - if(ImGui::SmallButton("+ Добавить свет")) {pendingSpawnArchetype=99;currentTool=TOOL_SPRITE_SPAWN;statusMessage="Выберите место источника на карте.";} ImGui::TreePop(); } // 3. Sublevel Transitions Category @@ -2586,9 +2701,10 @@ void EditorApp::RenderInspector() { ImGui::TextColored(ImVec4(0.2f, 0.8f, 1.0f, 1.0f), "ИНСПЕКТОР СВОЙСТВ (INSPECTOR)"); ImGui::Separator(); - if(selectedLightIdx>=0 && selectedLightIdx(lightSources.size())) {RenderLightingInspector();return;} - RenderLightingSettings(); - ImGui::Separator(); + if (selectedLightIdx >= 0 && selectedLightIdx < static_cast(lightSources.size())) { + RenderLightingInspector(); + return; + } // Case 1: Sprite Entity Selected if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast(sprites.size())) { @@ -2870,6 +2986,10 @@ void EditorApp::RenderInspector() { if (ImGui::SmallButton("Хоррор")) { ceilingColor = 0x003B0710; floorColor = 0x001C1917; } } + if (ImGui::CollapsingHeader("💡 Освещение карты (Lighting)", ImGuiTreeNodeFlags_DefaultOpen)) { + RenderLightingSettings(); + } + if (ImGui::CollapsingHeader("⚙ Настройки проекта & Сборка", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::Button("Настройки проекта (Жанр, Оружие)...", ImVec2(-1, 26))) { showProjectSettingsModal = true; diff --git a/editor/src/editor_app.h b/editor/src/editor_app.h index 8ca338c..a3f0d78 100644 --- a/editor/src/editor_app.h +++ b/editor/src/editor_app.h @@ -23,7 +23,18 @@ enum ToolMode { TOOL_ERASER, TOOL_FILL, TOOL_PLAYER_SPAWN, - TOOL_SPRITE_SPAWN + TOOL_SPRITE_SPAWN, + TOOL_LIGHT_SPAWN +}; + +enum LightPreset { + LIGHT_PRESET_TORCH = 0, + LIGHT_PRESET_LAMP, + LIGHT_PRESET_ALARM, + LIGHT_PRESET_CANDLE, + LIGHT_PRESET_NEON, + LIGHT_PRESET_TOXIC, + LIGHT_PRESET_SPOTLIGHT }; // Full level snapshot for undo/redo (grid + spawn + palette + sprites + transitions) @@ -126,6 +137,8 @@ public: int flashlightIntensity = 240; int flashlightCone = 45; bool showLightHeatmap = false; + bool autoBakeLighting = true; + int draggingLightRadiusIdx = -1; uint8_t bakedLightmap[MAP_TOTAL_CELLS] = { 0 }; std::array bakedLightRGB{}; uint64_t bakedLightingSignature = 0; @@ -133,6 +146,8 @@ public: void BakeLighting(); void InitDefaultLights(); + void ApplyLightPreset(int lightIdx, int presetId); + void SnapLightToGrid(int lightIdx); bool CastLightRay(int x1, int y1, int x2, int y2) const; bool LightingNeedsBake() const; LightingSettings GetLightingSettings() const; diff --git a/editor/src/editor_lighting.inl b/editor/src/editor_lighting.inl index 6e3c935..c25b2dd 100644 --- a/editor/src/editor_lighting.inl +++ b/editor/src/editor_lighting.inl @@ -1,72 +1,250 @@ +void EditorApp::ApplyLightPreset(int lightIdx, int presetId) { + if (lightIdx < 0 || lightIdx >= static_cast(lightSources.size())) return; + PushUndo(); + auto& l = lightSources[lightIdx]; + l.flags = LIGHT_FLAG_ACTIVE | LIGHT_FLAG_SHADOWS; + switch (presetId) { + case LIGHT_PRESET_TORCH: // 🕯 Факел + l.color = 0x00FFB040; + l.radius = 240; + l.intensity = 220; + l.curIntensity = 220; + l.lightType = LIGHT_TYPE_POINT; + l.flags |= LIGHT_FLAG_FLICKER; + break; + case LIGHT_PRESET_LAMP: // 💡 Лампа + l.color = 0x00E0E8FF; + l.radius = 280; + l.intensity = 240; + l.curIntensity = 240; + l.lightType = LIGHT_TYPE_STATIC; + break; + case LIGHT_PRESET_ALARM: // 🚨 Тревожная сирена + l.color = 0x00FF2020; + l.radius = 300; + l.intensity = 250; + l.curIntensity = 250; + l.lightType = LIGHT_TYPE_PULSE; + break; + case LIGHT_PRESET_CANDLE: // 🕯 Мягкая свеча + l.color = 0x00FF9520; + l.radius = 140; + l.intensity = 180; + l.curIntensity = 180; + l.lightType = LIGHT_TYPE_POINT; + l.flags |= LIGHT_FLAG_FLICKER; + break; + case LIGHT_PRESET_NEON: // 🔮 Киберпанк неон + l.color = 0x0000E5FF; + l.radius = 260; + l.intensity = 230; + l.curIntensity = 230; + l.lightType = LIGHT_TYPE_POINT; + break; + case LIGHT_PRESET_TOXIC: // ☢ Токсик + l.color = 0x0039FF14; + l.radius = 250; + l.intensity = 220; + l.curIntensity = 220; + l.lightType = LIGHT_TYPE_PULSE; + break; + case LIGHT_PRESET_SPOTLIGHT: // 🔦 Прожектор + l.color = 0x00FFFFFF; + l.radius = 380; + l.intensity = 255; + l.curIntensity = 255; + l.lightType = LIGHT_TYPE_STATIC; + break; + } + if (autoBakeLighting) BakeLighting(); + statusMessage = "Применен пресет света."; +} + +void EditorApp::SnapLightToGrid(int lightIdx) { + if (lightIdx < 0 || lightIdx >= static_cast(lightSources.size())) return; + PushUndo(); + auto& l = lightSources[lightIdx]; + int cellX = std::clamp(l.x / 64, 0, MAP_GRID_W - 1); + int cellY = std::clamp(l.y / 64, 0, MAP_GRID_H - 1); + l.x = cellX * 64 + 32; + l.y = cellY * 64 + 32; + if (autoBakeLighting) BakeLighting(); + statusMessage = "Источник света выровнен по центру клетки (" + std::to_string(cellX) + ", " + std::to_string(cellY) + ")."; +} + void EditorApp::RenderLightingSettings() { - if(!ImGui::CollapsingHeader("Освещение карты",ImGuiTreeNodeFlags_DefaultOpen)) return; - auto edited=GetLightingSettings(); bool changed=false,activated=false; - auto undoItem=[&](){activated|=ImGui::IsItemActivated();}; - const char* modes[]={"Без освещения","Только запечённый","Запечённый + динамический"}; - int mode=edited.mode; - changed|=ImGui::Combo("Режим света",&mode,modes,3);undoItem();edited.mode=mode; - int ambient=edited.ambient; - changed|=ImGui::SliderInt("Фоновый свет",&ambient,0,255);undoItem();edited.ambient=ambient; - bool flash=edited.flashlight!=0; - changed|=ImGui::Checkbox("Фонарик игрока",&flash);undoItem();edited.flashlight=flash; - if(flash) { - int radius=edited.flashlightRadius,intensity=edited.flashlightIntensity,cone=edited.flashlightCone; - changed|=ImGui::SliderInt("Дальность фонаря",&radius,32,2048);undoItem(); - changed|=ImGui::SliderInt("Яркость фонаря",&intensity,0,255);undoItem(); - changed|=ImGui::SliderInt("Полуугол фонаря",&cone,1,96);undoItem(); - edited.flashlightRadius=radius;edited.flashlightIntensity=intensity;edited.flashlightCone=cone; + auto edited = GetLightingSettings(); + bool changed = false, activated = false; + auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); }; + + const char* modes[] = { "Без освещения (Fullbright)", "Только запечённый свет (Baked)", "Запечённый + Динамический (Full Physics)" }; + int mode = edited.mode; + changed |= ImGui::Combo("Режим света", &mode, modes, 3); undoItem(); edited.mode = mode; + + int ambient = edited.ambient; + changed |= ImGui::SliderInt("Фоновый свет (Ambient)", &ambient, 0, 255); undoItem(); edited.ambient = ambient; + + bool flash = (edited.flashlight != 0); + changed |= ImGui::Checkbox("Фонарик игрока [F]", &flash); undoItem(); edited.flashlight = flash; + if (flash) { + int radius = edited.flashlightRadius, intensity = edited.flashlightIntensity, cone = edited.flashlightCone; + changed |= ImGui::SliderInt("Дальность фонаря", &radius, 32, 2048); undoItem(); + changed |= ImGui::SliderInt("Яркость фонаря", &intensity, 0, 255); undoItem(); + changed |= ImGui::SliderInt("Полуугол луча", &cone, 1, 96); undoItem(); + edited.flashlightRadius = radius; edited.flashlightIntensity = intensity; edited.flashlightCone = cone; } - if(activated)PushUndo(); - if(changed) { - lightingMode=edited.mode;ambientLightLevel=edited.ambient;isFlashlightOn=edited.flashlight!=0; - flashlightRadius=edited.flashlightRadius;flashlightIntensity=edited.flashlightIntensity;flashlightCone=edited.flashlightCone; + + if (activated) PushUndo(); + if (changed) { + lightingMode = edited.mode; + ambientLightLevel = edited.ambient; + isFlashlightOn = (edited.flashlight != 0); + flashlightRadius = edited.flashlightRadius; + flashlightIntensity = edited.flashlightIntensity; + flashlightCone = edited.flashlightCone; + if (autoBakeLighting) BakeLighting(); + } + + ImGui::Spacing(); + ImGui::Checkbox("Авто-запекание (Live Realtime Update)", &autoBakeLighting); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Мгновенно обновляет карту теней и 3D вид при любых изменениях источников и стен."); + + ImGui::Checkbox("Карта запечённого света (Хитмап на 2D сетке)", &showLightHeatmap); + + if (ImGui::Button("💡 Запечь постоянный свет сейчас", ImVec2(-1, 26))) { + PushUndo(); + BakeLighting(); + } + + if (LightingNeedsBake()) { + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.2f, 1.0f), "⚠ Геометрия или источники изменены: требуется запекание"); + } else { + ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "✔ Постоянный свет запечён (1024 ячейки)"); } - if(ImGui::Button("Запечь постоянный свет",ImVec2(-1,0))) {PushUndo();BakeLighting();} - if(LightingNeedsBake()) ImGui::TextWrapped("Геометрия или постоянный свет изменены: нужно запечь. При сохранении и F5 запекание выполняется автоматически."); - else ImGui::TextDisabled("Постоянный свет запечён"); - ImGui::Checkbox("Карта запечённого света",&showLightHeatmap); - ImGui::TextWrapped("Постоянные источники входят в запекание. Точечные, мерцающие и пульсирующие работают в динамическом режиме."); } void EditorApp::RenderLightingInspector() { - auto& light=lightSources[selectedLightIdx]; - ImGui::Text("Источник света #%d / %d",selectedLightIdx+1,MAX_EDITOR_LIGHTS); - if(ImGui::Button("Удалить источник")) { - PushUndo();lightSources.erase(lightSources.begin()+selectedLightIdx);selectedLightIdx=-1;return; - } + if (selectedLightIdx < 0 || selectedLightIdx >= static_cast(lightSources.size())) return; + auto& light = lightSources[selectedLightIdx]; + + const char* typeDesc = (light.lightType == LIGHT_TYPE_STATIC) ? "Постоянный (Запекаемый)" : + (light.lightType == LIGHT_TYPE_PULSE) ? "Пульсирующий (Сирена)" : + (light.flags & LIGHT_FLAG_FLICKER) ? "Мерцающий факел" : "Точечный (Динамический)"; + + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.25f, 1.0f), "💡 Источник света #%d из %d: %s", + selectedLightIdx + 1, static_cast(lightSources.size()), typeDesc); + ImGui::Separator(); + + // Quick Action Bar (Unity / Unreal style) + if (ImGui::SmallButton("🎯 Фокус (F)")) { FocusSelectedEntity(); } ImGui::SameLine(); - if(ImGui::Button("Копировать")) { - if(lightSources.size()(lightSources.size())-1; - } else statusMessage="Лимит: 13 источников карты; фонарик и вспышки имеют отдельные места."; - return; + if (ImGui::SmallButton("📋 Дублировать (Ctrl+D)")) { DuplicateSelectedEntity(); } + ImGui::SameLine(); + if (ImGui::SmallButton("🧲 К сетке (Snap)")) { SnapLightToGrid(selectedLightIdx); } + ImGui::SameLine(); + if (ImGui::SmallButton("🗑 Удалить (Del)")) { DeleteSelectedEntity(); return; } + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "Быстрые пресеты:"); + + // Preset Buttons + if (ImGui::SmallButton("🕯 Факел")) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_TORCH); + ImGui::SameLine(); + if (ImGui::SmallButton("💡 Лампа")) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_LAMP); + ImGui::SameLine(); + if (ImGui::SmallButton("🚨 Сирена")) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_ALARM); + ImGui::SameLine(); + if (ImGui::SmallButton("🕯 Свеча")) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_CANDLE); + ImGui::SameLine(); + if (ImGui::SmallButton("🔮 Неон")) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_NEON); + ImGui::SameLine(); + if (ImGui::SmallButton("🔦 Прожектор")) ApplyLightPreset(selectedLightIdx, LIGHT_PRESET_SPOTLIGHT); + + ImGui::Separator(); + + EditorLightSource edited = light; + bool changed = false, activated = false; + auto undoItem = [&]() { activated |= ImGui::IsItemActivated(); }; + + // Toggle Active + bool active = (edited.flags & LIGHT_FLAG_ACTIVE) != 0; + changed |= ImGui::Checkbox("Включён на карте", &active); undoItem(); + edited.flags = (edited.flags & ~LIGHT_FLAG_ACTIVE) | (active ? LIGHT_FLAG_ACTIVE : 0); + + // Coordinates with Cell info + int curCellX = edited.x / 64; + int curCellY = edited.y / 64; + ImGui::Text("Клетка: [%d, %d] | Мировые координаты: (%d, %d)", curCellX, curCellY, edited.x, edited.y); + float xCells = edited.x / 64.0f; + float yCells = edited.y / 64.0f; + if (ImGui::SliderFloat("Позиция X (клетки)", &xCells, 0.5f, 31.5f, "%.2f")) { + edited.x = std::clamp(static_cast(xCells * 64.0f), 32, (MAP_GRID_W - 1) * 64); + changed = true; + } undoItem(); + if (ImGui::SliderFloat("Позиция Y (клетки)", &yCells, 0.5f, 31.5f, "%.2f")) { + edited.y = std::clamp(static_cast(yCells * 64.0f), 32, (MAP_GRID_H - 1) * 64); + changed = true; + } undoItem(); + + // Radius & Intensity + changed |= ImGui::SliderInt("Радиус (px)", &edited.radius, 32, 2048, "%d px"); undoItem(); + changed |= ImGui::SliderInt("Яркость", &edited.intensity, 0, 255, "%d / 255"); undoItem(); + edited.curIntensity = edited.intensity; + + // Color Picker & Quick Swatches + float color[] = { ((edited.color >> 16) & 255) / 255.f, ((edited.color >> 8) & 255) / 255.f, (edited.color & 255) / 255.f }; + if (ImGui::ColorEdit3("Цвет света", color)) { + edited.color = (uint32_t(color[0] * 255) << 16) | (uint32_t(color[1] * 255) << 8) | uint32_t(color[2] * 255); + changed = true; + } undoItem(); + + // Quick Color Swatches + ImGui::TextDisabled("Оттенки:"); + ImGui::SameLine(); + auto ColorSwatch = [&](const char* label, uint32_t hexColor) { + float c[3] = { ((hexColor >> 16) & 255) / 255.f, ((hexColor >> 8) & 255) / 255.f, (hexColor & 255) / 255.f }; + if (ImGui::ColorButton(label, ImVec4(c[0], c[1], c[2], 1.0f), 0, ImVec2(18, 18))) { + edited.color = hexColor; + changed = true; + } + ImGui::SameLine(); + }; + ColorSwatch("##warm_fire", 0x00FFB040); + ColorSwatch("##cool_white", 0x00E0E8FF); + ColorSwatch("##neon_cyan", 0x0000E5FF); + ColorSwatch("##ruby_red", 0x00FF2020); + ColorSwatch("##toxic_green", 0x0039FF14); + ColorSwatch("##pure_white", 0x00FFFFFF); + ImGui::NewLine(); + + // Light Type + const char* types[] = { "Постоянный (запекание в lightmap)", "Точечный (динамический реалтайм)", "Пульсирующий (динамическая сирена)" }; + int type = (edited.lightType == LIGHT_TYPE_STATIC) ? 0 : (edited.lightType == LIGHT_TYPE_PULSE) ? 2 : 1; + if (ImGui::Combo("Тип источника", &type, types, 3)) { + edited.lightType = (type == 0) ? LIGHT_TYPE_STATIC : (type == 1) ? LIGHT_TYPE_POINT : LIGHT_TYPE_PULSE; + changed = true; + } undoItem(); + + // Behavior flags + bool shadows = (edited.flags & LIGHT_FLAG_SHADOWS) != 0; + changed |= ImGui::Checkbox("Тени от стен и дверей", &shadows); undoItem(); + edited.flags = (edited.flags & ~LIGHT_FLAG_SHADOWS) | (shadows ? LIGHT_FLAG_SHADOWS : 0); + + if (edited.lightType != LIGHT_TYPE_STATIC) { + bool flicker = (edited.flags & LIGHT_FLAG_FLICKER) != 0; + changed |= ImGui::Checkbox("Мерцание огня / факела (Flicker)", &flicker); undoItem(); + edited.flags = (edited.flags & ~LIGHT_FLAG_FLICKER) | (flicker ? LIGHT_FLAG_FLICKER : 0); } - EditorLightSource edited=light;bool changed=false,activated=false; - auto undoItem=[&](){activated|=ImGui::IsItemActivated();}; - bool active=(edited.flags&LIGHT_FLAG_ACTIVE)!=0; - changed|=ImGui::Checkbox("Включён",&active);undoItem(); - edited.flags=(edited.flags&~LIGHT_FLAG_ACTIVE)|(active?LIGHT_FLAG_ACTIVE:0); - float x=edited.x/64.0f,y=edited.y/64.0f; - if(ImGui::SliderFloat("Позиция X",&x,0,31.98f,"%.2f")){edited.x=static_cast(x*64);changed=true;}undoItem(); - if(ImGui::SliderFloat("Позиция Y",&y,0,31.98f,"%.2f")){edited.y=static_cast(y*64);changed=true;}undoItem(); - changed|=ImGui::SliderInt("Радиус",&edited.radius,32,2048);undoItem(); - changed|=ImGui::SliderInt("Интенсивность",&edited.intensity,0,255);undoItem();edited.curIntensity=edited.intensity; - float color[]={((edited.color>>16)&255)/255.f,((edited.color>>8)&255)/255.f,(edited.color&255)/255.f}; - if(ImGui::ColorEdit3("Цвет света",color)) { - edited.color=(uint32_t(color[0]*255)<<16)|(uint32_t(color[1]*255)<<8)|uint32_t(color[2]*255);changed=true; - }undoItem(); - const char* types[]={"Постоянный (запекание)","Точечный (динамический)","Пульсирующий (динамический)"}; - int type=edited.lightType==LIGHT_TYPE_STATIC?0:edited.lightType==LIGHT_TYPE_PULSE?2:1; - if(ImGui::Combo("Тип света",&type,types,3)){edited.lightType=type==0?LIGHT_TYPE_STATIC:type==1?LIGHT_TYPE_POINT:LIGHT_TYPE_PULSE;changed=true;}undoItem(); - bool shadows=(edited.flags&LIGHT_FLAG_SHADOWS)!=0,flicker=(edited.flags&LIGHT_FLAG_FLICKER)!=0; - changed|=ImGui::Checkbox("Тени от стен и дверей",&shadows);undoItem(); - edited.flags=(edited.flags&~LIGHT_FLAG_SHADOWS)|(shadows?LIGHT_FLAG_SHADOWS:0); - if(edited.lightType!=LIGHT_TYPE_STATIC) { - changed|=ImGui::Checkbox("Мерцание факела",&flicker);undoItem(); - edited.flags=(edited.flags&~LIGHT_FLAG_FLICKER)|(flicker?LIGHT_FLAG_FLICKER:0); + + if (activated) PushUndo(); + if (changed) { + light = edited; + if (autoBakeLighting) BakeLighting(); + } + + ImGui::Spacing(); + ImGui::Separator(); + if (ImGui::CollapsingHeader("Глобальное освещение карты")) { + RenderLightingSettings(); } - if(activated)PushUndo(); - if(changed)light=edited; - RenderLightingSettings(); } diff --git a/engine.exe b/engine.exe index 61e03d4..55d9dc2 100644 Binary files a/engine.exe and b/engine.exe differ diff --git a/main.obj b/main.obj index 99f30a1..10eb606 100644 Binary files a/main.obj and b/main.obj differ