feat(editor): overhaul lighting UX with tool mode, live auto-bake, presets and radius gizmo

This commit is contained in:
Artvasil09 2026-10-24 20:17:21 +03:00
parent 1efa1195cf
commit 432fccfd60
6 changed files with 427 additions and 114 deletions

Binary file not shown.

View file

@ -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<int>(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<int>(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<static_cast<int>(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<int>(lightSources.size())-1;return;
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(lightSources.size())-1;
if(autoBakeLighting) BakeLighting();
statusMessage = "Источник света дублирован (Ctrl+D).";
return;
}
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(sprites.size())) {
if (sprites.size() >= MAX_LEVEL_SPRITES) {
@ -1437,7 +1448,11 @@ void EditorApp::DuplicateSelectedEntity() {
void EditorApp::DeleteSelectedEntity() {
if(selectedLightIdx>=0 && selectedLightIdx<static_cast<int>(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<int>(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<int>(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<int>(i);
}
uint32_t c = lightSources[i].color;
uint8_t cr = (c >> 16) & 0xFF, cg = (c >> 8) & 0xFF, cb = c & 0xFF;
if (static_cast<int>(i) == selectedLightIdx || static_cast<int>(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<int>(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<int>(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<int>(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<int>(i); selectedLightIdx=-1;
selectedTransitionIdx = static_cast<int>(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<int>(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<int>(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<int>(lightSources.size())) {
float wx = (mousePos.x - gridOrigin.x) / tileSize;
float wy = (mousePos.y - gridOrigin.y) / tileSize;
lightSources[draggingLightIdx].x = std::clamp(static_cast<int32_t>(wx * 64.0f), 32, (MAP_GRID_W - 1) * 64);
lightSources[draggingLightIdx].y = std::clamp(static_cast<int32_t>(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<int>(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<static_cast<int>(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<int>(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<static_cast<int>(lightSources.size())) {RenderLightingInspector();return;}
RenderLightingSettings();
ImGui::Separator();
if (selectedLightIdx >= 0 && selectedLightIdx < static_cast<int>(lightSources.size())) {
RenderLightingInspector();
return;
}
// Case 1: Sprite Entity Selected
if (selectedSpriteIdx >= 0 && selectedSpriteIdx < static_cast<int>(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;

View file

@ -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<uint32_t,1024> 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;

View file

@ -1,72 +1,250 @@
void EditorApp::ApplyLightPreset(int lightIdx, int presetId) {
if (lightIdx < 0 || lightIdx >= static_cast<int>(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<int>(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<int>(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<int>(lightSources.size()), typeDesc);
ImGui::Separator();
// Quick Action Bar (Unity / Unreal style)
if (ImGui::SmallButton("🎯 Фокус (F)")) { FocusSelectedEntity(); }
ImGui::SameLine();
if(ImGui::Button("Копировать")) {
if(lightSources.size()<MAX_EDITOR_LIGHTS) {
auto copy=light;copy.x=(std::min)(2047,copy.x+32);copy.y=(std::min)(2047,copy.y+32);
PushUndo();lightSources.push_back(copy);selectedLightIdx=static_cast<int>(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<int>(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<int>(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<int>(x*64);changed=true;}undoItem();
if(ImGui::SliderFloat("Позиция Y",&y,0,31.98f,"%.2f")){edited.y=static_cast<int>(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();
}

Binary file not shown.

BIN
main.obj

Binary file not shown.