Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BehaviorTreeDebugWindow.cpp
Go to the documentation of this file.
1/**
2 * @file BehaviorTreeDebugWindow.cpp
3 * @brief Implementation of behavior tree runtime debugger
4 */
5
7#include "BTEditorCommand.h"
8#include "../World.h"
9#include "../GameEngine.h"
10#include "../ECS_Components.h"
11#include "../ECS_Components_AI.h"
12#include "../json_helper.h"
13#include "../third_party/imgui/imgui.h"
14#include "../third_party/imnodes/imnodes.h"
15#include "../third_party/imgui/backends/imgui_impl_sdl3.h"
16#include "../third_party/imgui/backends/imgui_impl_sdlrenderer3.h"
17#include "../third_party/nlohmann/json.hpp"
18#include <SDL3/SDL.h>
19#include <algorithm>
20#include <cstring>
21#include <cmath>
22#include <ctime>
23#include <fstream>
24#include <unordered_set>
25#include <set>
26
27// Note: json type is already defined in json_helper.h (included above)
28
29namespace Olympe
30{
31 // Camera zoom constants
32 constexpr float MIN_ZOOM = 0.3f;
33 constexpr float MAX_ZOOM = 3.0f;
34 constexpr float ZOOM_EPSILON = 0.001f;
35
37 : m_separateWindow(nullptr)
38 , m_separateRenderer(nullptr)
39 , m_windowCreated(false)
40 , m_separateImGuiContext(nullptr)
41 {
42 }
43
48
50 {
52 return;
53
55
57 {
58 ImNodes::CreateContext();
59 ImNodes::GetStyle().GridSpacing = 32.0f;
60 ImNodes::GetStyle().NodeCornerRounding = 8.0f;
61 ImNodes::GetStyle().NodePadding = ImVec2(8, 8);
63 }
64
66
67 m_isInitialized = true;
68
69 std::cout << "[BTDebugger] Initialized (window will be created on first F10)" << std::endl;
70 }
71
73 {
75
77 {
78 ImNodes::DestroyContext();
80 }
81
82 m_isInitialized = false;
83 }
84
86 {
88
89 if (m_isVisible)
90 {
91 if (!m_isInitialized)
92 {
93 Initialize();
94 }
95
96 if (!m_windowCreated)
97 {
99 }
100
101 std::cout << "[BTDebugger] F10: Debugger window opened (separate window)" << std::endl;
102 }
103 else
104 {
106
107 std::cout << "[BTDebugger] F10: Debugger window closed" << std::endl;
108 }
109 }
110
112 {
113 if (m_windowCreated)
114 {
115 std::cout << "[BTDebugger] Separate window already exists" << std::endl;
116 return;
117 }
118
119 ImGuiContext* previousContext = ImGui::GetCurrentContext();
120
121 const int windowWidth = 1200;
122 const int windowHeight = 720;
123
125 "Behavior Tree Runtime Debugger - Independent Window",
131 {
132 std::cout << "[BTDebugger] ERROR: Failed to create separate window: "
133 << SDL_GetError() << std::endl;
134 return;
135 }
136
137 m_separateImGuiContext = ImGui::CreateContext();
138 ImGui::SetCurrentContext(m_separateImGuiContext);
139
140 ImGuiIO& io = ImGui::GetIO();
141 (void)io;
142 ImGui::StyleColorsDark();
143
146
147 m_windowCreated = true;
148
149 ImGui::SetCurrentContext(previousContext);
150
151 std::cout << "[BTDebugger] ✅ Separate window created successfully!" << std::endl;
152 std::cout << "[BTDebugger] Window can be moved to second monitor" << std::endl;
153 }
154
156 {
157 if (!m_windowCreated)
158 return;
159
160 ImGuiContext* previousContext = ImGui::GetCurrentContext();
161
162 if (m_separateImGuiContext != nullptr)
163 {
164 ImGui::SetCurrentContext(m_separateImGuiContext);
167 ImGui::DestroyContext(m_separateImGuiContext);
168 m_separateImGuiContext = nullptr;
169 }
170
172 ImGui::SetCurrentContext(previousContext);
173
174 if (m_separateRenderer != nullptr)
175 {
177 m_separateRenderer = nullptr;
178 }
179
180 if (m_separateWindow != nullptr)
181 {
183 m_separateWindow = nullptr;
184 }
185
186 m_windowCreated = false;
187
188 std::cout << "[BTDebugger] Separate window destroyed" << std::endl;
189 }
190
192 {
194 return;
195
197 {
198 if (event->window.windowID == SDL_GetWindowID(m_separateWindow))
199 {
201 return;
202 }
203 }
204
205 ImGuiContext* previousContext = ImGui::GetCurrentContext();
206
207 ImGui::SetCurrentContext(m_separateImGuiContext);
209
210 ImGui::SetCurrentContext(previousContext);
211 }
212
214 {
216 return;
217
218 ImGuiContext* previousContext = ImGui::GetCurrentContext();
219
220 ImGui::SetCurrentContext(m_separateImGuiContext);
221
224 ImGui::NewFrame();
225
227
228 ImGui::Render();
233
234 ImGui::SetCurrentContext(previousContext);
235 }
236
238 {
240
241 static float accumulatedTime = 0.0f;
243
245 {
247 accumulatedTime = 0.0f;
248 }
249
250 for (auto& entry : m_executionLog)
251 {
252 entry.timeAgo += GameEngine::fDt;
253 }
254
255 ImGui::SetNextWindowPos(ImVec2(0, 0));
256 ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize);
257
265
266 if (!ImGui::Begin("Behavior Tree Runtime Debugger##Main", nullptr, windowFlags))
267 {
268 ImGui::End();
269 return;
270 }
271
272 if (ImGui::BeginMenuBar())
273 {
274 // File menu (only visible in editor mode)
275 if (m_editorMode)
276 {
279 }
280
281 if (ImGui::BeginMenu("View"))
282 {
283 ImGui::SliderFloat("Auto Refresh (s)", &m_autoRefreshInterval, 0.1f, 5.0f);
284 ImGui::SliderFloat("Entity List Width", &m_entityListWidth, 150.0f, 400.0f);
285 ImGui::SliderFloat("Inspector Width", &m_inspectorWidth, 250.0f, 500.0f);
286
287 if (ImGui::SliderFloat("Node Spacing X", &m_nodeSpacingX, 80.0f, 400.0f))
288 {
289 m_needsLayoutUpdate = true;
290 }
291 if (ImGui::SliderFloat("Node Spacing Y", &m_nodeSpacingY, 60.0f, 300.0f))
292 {
293 m_needsLayoutUpdate = true;
294 }
295
296 if (ImGui::Button("Reset Spacing to Defaults"))
297 {
298 m_nodeSpacingX = 180.0f;
299 m_nodeSpacingY = 120.0f;
300 m_needsLayoutUpdate = true;
301 }
302
303 ImGui::Separator();
304
305 if (ImGui::Checkbox("Grid Snapping", &m_config.gridSnappingEnabled))
306 {
307 std::cout << "[BTDebugger] Grid snapping "
308 << (m_config.gridSnappingEnabled ? "enabled" : "disabled") << std::endl;
309 }
310 if (ImGui::IsItemHovered())
311 {
312 ImGui::SetTooltip("Snap node positions to %.0fpx grid", m_config.gridSize);
313 }
314
315 ImGui::Separator();
316 ImGui::Text("Current Zoom: %.0f%%", m_currentZoom * 100.0f);
317 ImGui::Checkbox("Show Minimap", &m_showMinimap);
318
319 ImGui::Checkbox("Auto-Fit on Load", &m_autoFitOnLoad);
320 if (ImGui::IsItemHovered())
321 {
322 ImGui::SetTooltip("Automatically fit tree to view when selecting an entity");
323 }
324
325 ImGui::Separator();
326
327 if (ImGui::MenuItem("Reload Config"))
328 {
329 LoadBTConfig();
331 m_needsLayoutUpdate = true;
332 std::cout << "[BTDebugger] Configuration reloaded" << std::endl;
333 }
334
335 ImGui::Separator();
336 ImGui::Text("Window Mode: Separate (Independent)");
337 ImGui::Text("Press F10 to close window");
338
339 ImGui::EndMenu();
340 }
341
342 if (ImGui::BeginMenu("Tools"))
343 {
344 if (ImGui::MenuItem("Auto Organize Graph"))
345 {
346 if (m_selectedEntity != 0)
347 {
348 auto& world = World::Get();
349 if (world.HasComponent<BehaviorTreeRuntime_data>(m_selectedEntity))
350 {
351 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
353 if (tree)
354 {
356 std::cout << "[BTDebugger] Graph reorganized with current settings" << std::endl;
357 }
358 }
359 }
360 }
361 if (ImGui::IsItemHovered())
362 {
363 ImGui::SetTooltip("Recalculate all node positions using the current layout algorithm");
364 }
365
366 if (ImGui::MenuItem("Reset View"))
367 {
368 ResetZoom();
370 std::cout << "[BTDebugger] View reset to default (zoom 100%%, centered)" << std::endl;
371 }
372 if (ImGui::IsItemHovered())
373 {
374 ImGui::SetTooltip("Reset zoom to 100%% and center camera");
375 }
376
377 ImGui::EndMenu();
378 }
379
380 if (ImGui::BeginMenu("Actions"))
381 {
382 if (ImGui::MenuItem("Refresh Now (F5)"))
383 {
385 }
386 if (ImGui::MenuItem("Clear Execution Log"))
387 {
388 m_executionLog.clear();
389 }
390
391 ImGui::Separator();
392
393 if (ImGui::MenuItem("Fit Graph to View", "F"))
395
396 if (ImGui::MenuItem("Center View", "C"))
398
399 if (ImGui::MenuItem("Reset Zoom", "0"))
400 ResetZoom();
401
402 ImGui::EndMenu();
403 }
404
405 ImGui::EndMenuBar();
406 }
407
408 // Keyboard shortcuts
409 if (ImGui::IsKeyPressed(ImGuiKey_F5))
410 {
412 }
413
414 // Editor mode shortcuts
415 if (m_editorMode)
416 {
417 ImGuiIO& io = ImGui::GetIO();
418
419 // Ctrl+S - Save
420 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S))
421 {
422 Save();
423 }
424
425 // Ctrl+Z - Undo
426 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z))
427 {
429 {
431 m_isDirty = true;
432 }
433 }
434
435 // Ctrl+Y - Redo
436 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y))
437 {
439 {
441 m_isDirty = true;
442 }
443 }
444
445 // Delete - Delete selected nodes
446 if (ImGui::IsKeyPressed(ImGuiKey_Delete))
447 {
449 }
450
451 // Ctrl+D - Duplicate selected nodes
452 if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_D))
453 {
455 }
456
457 // Escape - Deselect all
458 if (ImGui::IsKeyPressed(ImGuiKey_Escape))
459 {
460 m_selectedNodes.clear();
461 }
462 }
463
464 float windowWidth = ImGui::GetContentRegionAvail().x;
465 float windowHeight = ImGui::GetContentRegionAvail().y;
466
467 ImGui::BeginChild("EntityListPanel", ImVec2(m_entityListWidth, windowHeight), true);
469 ImGui::EndChild();
470
471 ImGui::SameLine();
472
474 ImGui::BeginChild("NodeGraphPanel", ImVec2(centerWidth, windowHeight), true);
476 ImGui::EndChild();
477
478 ImGui::SameLine();
479
480 ImGui::BeginChild("InspectorPanel", ImVec2(m_inspectorWidth, windowHeight), true);
482 ImGui::EndChild();
483
484 ImGui::End();
485 }
486
488 {
489 m_entities.clear();
490
491 auto& world = World::Get();
492 const auto& allEntities = world.GetAllEntities();
493
494 for (EntityID entity : allEntities)
495 {
496 if (!world.HasComponent<BehaviorTreeRuntime_data>(entity))
497 continue;
498
500 info.entityId = entity;
501
502 if (world.HasComponent<Identity_data>(entity))
503 {
504 const auto& identity = world.GetComponent<Identity_data>(entity);
505 info.entityName = identity.name;
506 }
507 else
508 {
509 info.entityName = "Entity " + std::to_string(entity);
510 }
511
512 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(entity);
514 info.isActive = btRuntime.isActive;
515 info.currentNodeId = btRuntime.AICurrentNodeIndex;
516 info.lastStatus = static_cast<BTStatus>(btRuntime.lastStatus);
517
519 if (tree)
520 {
521 info.treeName = tree->name;
522 }
523 else
524 {
525 std::string path = BehaviorTreeManager::Get().GetTreePathFromId(info.treeId);
526
527 if (!path.empty())
528 {
529 info.treeName = "Not Loaded: " + path;
530 }
531 else
532 {
533 info.treeName = "Unknown (ID=" + std::to_string(info.treeId) + ")";
534 }
535
536 static std::set<EntityID> debuggedEntities;
537 if (debuggedEntities.find(entity) == debuggedEntities.end())
538 {
539 debuggedEntities.insert(entity);
540 std::cout << "[BTDebugger] WARNING: Entity " << entity << " (" << info.entityName
541 << ") has unknown tree ID=" << info.treeId << std::endl;
543 }
544 }
545
546 if (world.HasComponent<AIState_data>(entity))
547 {
548 const auto& aiState = world.GetComponent<AIState_data>(entity);
549 switch (aiState.currentMode)
550 {
551 case AIMode::Idle: info.aiMode = "Idle"; break;
552 case AIMode::Patrol: info.aiMode = "Patrol"; break;
553 case AIMode::Combat: info.aiMode = "Combat"; break;
554 case AIMode::Flee: info.aiMode = "Flee"; break;
555 case AIMode::Investigate: info.aiMode = "Investigate"; break;
556 case AIMode::Dead: info.aiMode = "Dead"; break;
557 default: info.aiMode = "Unknown"; break;
558 }
559 }
560 else
561 {
562 info.aiMode = "N/A";
563 }
564
565 if (world.HasComponent<AIBlackboard_data>(entity))
566 {
567 const auto& blackboard = world.GetComponent<AIBlackboard_data>(entity);
568 info.hasTarget = blackboard.hasTarget;
569 }
570
571 info.lastUpdateTime = 0.0f;
572
573 m_entities.push_back(info);
574 }
575
578 }
579
581 {
582 m_filteredEntities.clear();
583
584 for (const auto& info : m_entities)
585 {
586 if (m_filterText[0] != '\0')
587 {
588 std::string lowerName = info.entityName;
589 std::string lowerFilter = m_filterText;
590 std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
591 std::transform(lowerFilter.begin(), lowerFilter.end(), lowerFilter.begin(), ::tolower);
592
593 if (lowerName.find(lowerFilter) == std::string::npos)
594 continue;
595 }
596
597 if (m_filterActiveOnly && !info.isActive)
598 continue;
599
600 if (m_filterHasTarget && !info.hasTarget)
601 continue;
602
603 m_filteredEntities.push_back(info);
604 }
605 }
606
608 {
609 std::sort(m_filteredEntities.begin(), m_filteredEntities.end(),
610 [this](const EntityDebugInfo& a, const EntityDebugInfo& b) {
611 bool result = false;
612 switch (m_sortMode)
613 {
614 case SortMode::Name:
615 result = a.entityName < b.entityName;
616 break;
617 case SortMode::TreeName:
618 result = a.treeName < b.treeName;
619 break;
620 case SortMode::LastUpdate:
621 result = a.lastUpdateTime > b.lastUpdateTime;
622 break;
623 case SortMode::AIMode:
624 result = a.aiMode < b.aiMode;
625 break;
626 }
627 return m_sortAscending ? result : !result;
628 });
629 }
630
631 void BehaviorTreeDebugWindow::RenderEntityListPanel()
632 {
633 ImGui::Text("Entities with Behavior Trees");
634 ImGui::Separator();
635
636 ImGui::InputText("Search", m_filterText, sizeof(m_filterText));
637 if (ImGui::IsItemEdited())
638 {
639 UpdateEntityFiltering();
640 UpdateEntitySorting();
641 }
642
643 if (ImGui::Checkbox("Active Only", &m_filterActiveOnly))
644 {
645 UpdateEntityFiltering();
646 UpdateEntitySorting();
647 }
648 ImGui::SameLine();
649 if (ImGui::Checkbox("Has Target", &m_filterHasTarget))
650 {
651 UpdateEntityFiltering();
652 UpdateEntitySorting();
653 }
654
655 ImGui::Separator();
656
657 ImGui::Text("Sort by:");
658 const char* sortModes[] = { "Name", "Tree Name", "Last Update", "AI Mode" };
659 int currentSort = static_cast<int>(m_sortMode);
660 if (ImGui::Combo("##SortMode", &currentSort, sortModes, IM_ARRAYSIZE(sortModes)))
661 {
662 m_sortMode = static_cast<SortMode>(currentSort);
663 UpdateEntitySorting();
664 }
665 ImGui::SameLine();
666 if (ImGui::Button(m_sortAscending ? "Asc" : "Desc"))
667 {
668 m_sortAscending = !m_sortAscending;
669 UpdateEntitySorting();
670 }
671
672 ImGui::Separator();
673
674 ImGui::Text("Entities: %d / %d", (int)m_filteredEntities.size(), (int)m_entities.size());
675
676 ImGui::BeginChild("EntityList", ImVec2(0, 0), false);
677 for (const auto& info : m_filteredEntities)
678 {
679 RenderEntityEntry(info);
680 }
681 ImGui::EndChild();
682 }
683
684 void BehaviorTreeDebugWindow::RenderEntityEntry(const EntityDebugInfo& info)
685 {
686 ImGui::PushID((unsigned int)info.entityId);
687
688 const char* statusIcon = info.isActive ? "●" : "○";
689 ImVec4 statusColor = info.isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
690
691 const char* resultIcon = "▶";
692 ImVec4 resultColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f);
693 if (info.lastStatus == BTStatus::Success)
694 {
695 resultIcon = "✓";
696 resultColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
697 }
698 else if (info.lastStatus == BTStatus::Failure)
699 {
700 resultIcon = "✗";
701 resultColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
702 }
703
704 bool isSelected = (m_selectedEntity == info.entityId);
705 ImGui::TextColored(statusColor, "%s", statusIcon);
706 ImGui::SameLine();
707 ImGui::TextColored(resultColor, "%s", resultIcon);
708 ImGui::SameLine();
709
710 if (ImGui::Selectable(info.entityName.c_str(), isSelected))
711 {
712 m_selectedEntity = info.entityId;
713
715 if (tree)
716 {
717 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
718 m_needsLayoutUpdate = false;
719
720 if (m_autoFitOnLoad)
721 {
722 // Auto-fit logic will trigger in graph panel
723 }
724 }
725 }
726
727 if (ImGui::IsItemHovered())
728 {
729 ImGui::BeginTooltip();
730 ImGui::Text("Entity ID: %u", info.entityId);
731 ImGui::Text("Tree: %s", info.treeName.c_str());
732 ImGui::Text("AI Mode: %s", info.aiMode.c_str());
733 ImGui::Text("Active: %s", info.isActive ? "Yes" : "No");
734 ImGui::Text("Has Target: %s", info.hasTarget ? "Yes" : "No");
735 ImGui::EndTooltip();
736 }
737
738 ImGui::Indent();
739 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", info.treeName.c_str());
740 ImGui::Unindent();
741
742 ImGui::PopID();
743 }
744
745 void BehaviorTreeDebugWindow::RenderNodeGraphPanel()
746 {
747 if (m_selectedEntity == 0)
748 {
749 ImGui::Text("Select an entity from the list to view its behavior tree");
750 return;
751 }
752
753 auto& world = World::Get();
754 if (!world.HasComponent<BehaviorTreeRuntime_data>(m_selectedEntity))
755 {
756 ImGui::Text("Selected entity no longer has a behavior tree");
757 m_selectedEntity = 0;
758 return;
759 }
760
761 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
762
764
765 if (!tree)
766 {
767 std::string path = BehaviorTreeManager::Get().GetTreePathFromId(btRuntime.AITreeAssetId);
768
769 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Behavior Tree asset not found!");
770 ImGui::Separator();
771 ImGui::Text("Tree ID: %u", btRuntime.AITreeAssetId);
772
773 if (!path.empty())
774 {
775 ImGui::Text("Expected Path: %s", path.c_str());
776 ImGui::Spacing();
777 ImGui::TextWrapped("The tree file may not be loaded. Check if the JSON file exists and is loaded during level initialization.");
778 }
779 else
780 {
781 ImGui::Spacing();
782 ImGui::TextWrapped("This tree ID is not registered in the BehaviorTreeManager.");
783 ImGui::TextWrapped("Possible causes:");
784 ImGui::BulletText("Tree JSON file not loaded");
785 ImGui::BulletText("Prefab uses obsolete tree ID");
786 ImGui::BulletText("Tree ID mismatch between prefab and runtime");
787 }
788
789 ImGui::Spacing();
790 if (ImGui::Button("Show All Loaded Trees"))
791 {
793 }
794
795 return;
796 }
797
798 bool prevEditorMode = m_editorMode;
799 ImGui::Checkbox("Editor Mode", &m_editorMode);
800 if (ImGui::IsItemHovered())
801 {
802 ImGui::SetTooltip("Enable editing mode to add/remove/connect nodes");
803 }
804
805 if (m_editorMode && !prevEditorMode)
806 {
808
809 if (originalTree)
810 {
811 m_editingTree = *originalTree;
812 m_treeModified = false;
813 m_selectedNodes.clear();
814 m_commandStack.Clear();
815 m_nextNodeId = 1000;
816 m_nextLinkId = 100000;
817
818 for (const auto& node : m_editingTree.nodes)
819 {
820 if (node.id >= m_nextNodeId)
821 {
822 m_nextNodeId = node.id + 1;
823 }
824 }
825
826 std::cout << "[BTEditor] Entered editor mode, editing tree: " << m_editingTree.name << std::endl;
827 }
828 }
829
830 if (m_editorMode)
831 {
832 ImGui::SameLine();
833 if (m_treeModified)
834 {
835 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "[Modified]");
836 }
837 else
838 {
839 ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[Unmodified]");
840 }
841
842 RenderEditorToolbar();
843 }
844
845 ImGui::Separator();
846
847 ImGui::Text("Layout:");
848 ImGui::SameLine();
849
850 bool layoutChanged = false;
851 if (ImGui::RadioButton("Vertical", m_layoutDirection == BTLayoutDirection::TopToBottom))
852 {
853 if (m_layoutDirection != BTLayoutDirection::TopToBottom)
854 {
855 m_layoutDirection = BTLayoutDirection::TopToBottom;
856 layoutChanged = true;
857 }
858 }
859 ImGui::SameLine();
860 if (ImGui::RadioButton("Horizontal", m_layoutDirection == BTLayoutDirection::LeftToRight))
861 {
862 if (m_layoutDirection != BTLayoutDirection::LeftToRight)
863 {
864 m_layoutDirection = BTLayoutDirection::LeftToRight;
865 layoutChanged = true;
866 }
867 }
868
869 if (layoutChanged)
870 {
871 m_layoutEngine.SetLayoutDirection(m_layoutDirection);
872 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
873 m_needsLayoutUpdate = false;
874 }
875
876 if (m_needsLayoutUpdate && tree)
877 {
878 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
879 m_needsLayoutUpdate = false;
880 }
881
882 ImGui::SameLine();
883 if (ImGui::Button("Reset Camera"))
884 {
885 if (!m_currentLayout.empty())
886 {
889
890 for (const auto& layout : m_currentLayout)
891 {
892 float halfWidth = layout.width / 2.0f;
893 float halfHeight = layout.height / 2.0f;
894
895 minPos.x = std::min(minPos.x, layout.position.x - halfWidth);
896 minPos.y = std::min(minPos.y, layout.position.y - halfHeight);
897 maxPos.x = std::max(maxPos.x, layout.position.x + halfWidth);
898 maxPos.y = std::max(maxPos.y, layout.position.y + halfHeight);
899 }
900
901 ImVec2 graphCenter((minPos.x + maxPos.x) / 2.0f, (minPos.y + maxPos.y) / 2.0f);
902 ImVec2 editorSize = ImGui::GetContentRegionAvail();
903
905 graphCenter.x - editorSize.x / 2.0f,
906 graphCenter.y - editorSize.y / 2.0f
907 );
908
909 ImNodes::EditorContextResetPanning(targetPanning);
910
911 std::cout << "[BTDebugger] Camera reset to center: ("
912 << graphCenter.x << ", " << graphCenter.y << ")" << std::endl;
913 }
914 }
915
916 ImGui::Separator();
917
918 ImNodes::BeginNodeEditor();
919
920 if (!m_currentLayout.empty())
921 {
924
925 for (const auto& layout : m_currentLayout)
926 {
927 float halfWidth = layout.width / 2.0f;
928 float halfHeight = layout.height / 2.0f;
929
930 minPos.x = std::min(minPos.x, layout.position.x - halfWidth);
931 minPos.y = std::min(minPos.y, layout.position.y - halfHeight);
932 maxPos.x = std::max(maxPos.x, layout.position.x + halfWidth);
933 maxPos.y = std::max(maxPos.y, layout.position.y + halfHeight);
934 }
935
936 ImVec2 graphCenter((minPos.x + maxPos.x) / 2.0f, (minPos.y + maxPos.y) / 2.0f);
937
938 if (m_lastCenteredEntity != m_selectedEntity)
939 {
940 if (m_editorMode)
941 {
943
944 if (originalTree)
945 {
946 m_editingTree = *originalTree;
947 m_treeModified = false;
948 m_selectedNodes.clear();
949 m_commandStack.Clear();
950 }
951 }
952
953 if (m_autoFitOnLoad)
954 {
955 FitGraphToView();
956 }
957 else
958 {
959 CenterViewOnGraph();
960 }
961
962 std::cout << "[BTDebugger] ✅ Camera " << (m_autoFitOnLoad ? "fitted" : "centered") << " on graph" << std::endl;
963 m_lastCenteredEntity = m_selectedEntity;
964 }
965 }
966
967 ImGuiIO& io = ImGui::GetIO();
968 if (ImGui::IsWindowHovered() && !ImGui::IsAnyItemHovered())
969 {
970 if (io.MouseWheel != 0.0f)
971 {
972 float oldZoom = m_currentZoom;
973 float zoomDelta = io.MouseWheel * 0.1f;
974 m_currentZoom = std::max(MIN_ZOOM, std::min(MAX_ZOOM, m_currentZoom + zoomDelta));
975
976 if (std::abs(m_currentZoom - oldZoom) > ZOOM_EPSILON && tree)
977 {
978 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
979 ApplyZoomToStyle();
980
981 std::cout << "[BTDebugger] Zoom: " << (int)(m_currentZoom * 100)
982 << "% (layout recomputed)" << std::endl;
983 }
984 }
985 }
986
987 if (ImGui::IsWindowFocused())
988 {
989 bool ctrlPressed = io.KeyCtrl;
990
991 if (ImGui::IsKeyPressed(ImGuiKey_F) && !ctrlPressed)
992 FitGraphToView();
993
994 if (ImGui::IsKeyPressed(ImGuiKey_C) && !ctrlPressed)
995 CenterViewOnGraph();
996
997 if ((ImGui::IsKeyPressed(ImGuiKey_0) || ImGui::IsKeyPressed(ImGuiKey_Keypad0)) && !ctrlPressed)
998 ResetZoom();
999
1000 if (ImGui::IsKeyPressed(ImGuiKey_M) && !ctrlPressed)
1001 m_showMinimap = !m_showMinimap;
1002
1003 if ((ImGui::IsKeyPressed(ImGuiKey_Equal) || ImGui::IsKeyPressed(ImGuiKey_KeypadAdd)) && !ctrlPressed)
1004 {
1005 float oldZoom = m_currentZoom;
1006 m_currentZoom = std::min(MAX_ZOOM, m_currentZoom * 1.2f);
1007
1008 if (std::abs(m_currentZoom - oldZoom) > ZOOM_EPSILON && tree)
1009 {
1010 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1011 ApplyZoomToStyle();
1012 }
1013 }
1014
1015 if ((ImGui::IsKeyPressed(ImGuiKey_Minus) || ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract)) && !ctrlPressed)
1016 {
1017 float oldZoom = m_currentZoom;
1018 m_currentZoom = std::max(MIN_ZOOM, m_currentZoom / 1.2f);
1019
1020 if (std::abs(m_currentZoom - oldZoom) > ZOOM_EPSILON && tree)
1021 {
1022 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1023 ApplyZoomToStyle();
1024 }
1025 }
1026 }
1027
1028 RenderBehaviorTreeGraph();
1029
1030 if (m_showMinimap)
1031 RenderMinimap();
1032
1033 ImNodes::EndNodeEditor();
1034
1035 if (m_editorMode)
1036 {
1038 if (ImNodes::IsLinkCreated(&startAttrId, &endAttrId))
1039 {
1040 uint32_t parentId = startAttrId / 10000;
1041 uint32_t childId = endAttrId / 10000;
1042
1043 if (ValidateConnection(parentId, childId))
1044 {
1045 // Use command pattern for connection
1046 auto cmd = std::make_unique<ConnectNodesCommand>(&m_editingTree, parentId, childId);
1047 m_commandStack.Execute(std::move(cmd));
1048
1049 m_isDirty = true;
1050 m_treeModified = true;
1051
1052 // Update layout
1053 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1054
1055 // Run validation
1056 m_validationMessages = m_editingTree.ValidateTreeFull();
1057
1058 std::cout << "[BTEditor] Connection created: " << parentId << " -> " << childId << std::endl;
1059 }
1060 else
1061 {
1062 std::cout << "[BTEditor] Invalid connection: " << parentId << " -> " << childId << std::endl;
1063 }
1064 }
1065
1066 int linkId;
1067 if (ImNodes::IsLinkDestroyed(&linkId))
1068 {
1069 auto it = std::find_if(m_linkMap.begin(), m_linkMap.end(),
1070 [linkId](const LinkInfo& info) { return info.linkId == linkId; });
1071
1072 if (it != m_linkMap.end())
1073 {
1074 uint32_t parentId = it->parentId;
1075 uint32_t childId = it->childId;
1076
1077 // Use command pattern for disconnection
1078 auto cmd = std::make_unique<DisconnectNodesCommand>(&m_editingTree, parentId, childId);
1079 m_commandStack.Execute(std::move(cmd));
1080
1081 m_isDirty = true;
1082 m_treeModified = true;
1083
1084 // Update layout
1085 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1086
1087 // Run validation
1088 m_validationMessages = m_editingTree.ValidateTreeFull();
1089
1090 std::cout << "[BTEditor] Connection deleted: " << parentId << " -> " << childId << std::endl;
1091 }
1092 }
1093
1094 int numSelected = ImNodes::NumSelectedNodes();
1095 if (numSelected > 0)
1096 {
1097 std::vector<int> selectedIds(numSelected);
1098 ImNodes::GetSelectedNodes(selectedIds.data());
1099 m_selectedNodes.clear();
1100 for (int id : selectedIds)
1101 {
1102 m_selectedNodes.push_back(static_cast<uint32_t>(id));
1103 }
1104 }
1105
1106 if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right))
1107 {
1108 m_showNodePalette = true;
1109 m_nodeCreationPos.Set(ImGui::GetMousePos().x, ImGui::GetMousePos().y, 0.f);
1110 }
1111
1112 if (ImGui::IsKeyPressed(ImGuiKey_Delete) && !m_selectedNodes.empty())
1113 {
1114 HandleNodeDeletion();
1115 }
1116
1117 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_D) && !m_selectedNodes.empty())
1118 {
1119 HandleNodeDuplication();
1120 }
1121
1122 // Note: Undo/Redo shortcuts (Ctrl+Z/Ctrl+Y) are handled globally at lines 426-443
1123 }
1124
1125 // ImNodes::EndNodeEditor(); // remove second call to EndNodeEditor TO BE REMOVED it causes crashes when interacting with the graph, but is needed to render the minimap correctly. Need to refactor rendering flow to avoid this hack
1126
1127 if (m_showNodePalette)
1128 {
1129 RenderNodePalette();
1130 }
1131 }
1132
1133 void BehaviorTreeDebugWindow::RenderBehaviorTreeGraph()
1134 {
1135 auto& world = World::Get();
1136 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
1137
1138 const BehaviorTreeAsset* tree = nullptr;
1139
1140 if (m_editorMode && !m_editingTree.nodes.empty())
1141 {
1142 tree = &m_editingTree;
1143 }
1144 else
1145 {
1147 }
1148
1149 if (!tree)
1150 return;
1151
1152 uint32_t currentNodeId = btRuntime.AICurrentNodeIndex;
1153
1154 for (const auto& node : tree->nodes)
1155 {
1156 const BTNodeLayout* layout = m_layoutEngine.GetNodeLayout(node.id);
1157 if (layout)
1158 {
1159 bool isCurrentNode = (node.id == currentNodeId) && btRuntime.isActive && !m_editorMode;
1160 RenderNode(&node, layout, isCurrentNode);
1161 }
1162 }
1163
1164 for (const auto& node : tree->nodes)
1165 {
1166 const BTNodeLayout* layout = m_layoutEngine.GetNodeLayout(node.id);
1167 if (layout)
1168 {
1169 RenderNodeConnections(&node, layout, tree);
1170 }
1171 }
1172
1173 for (const auto& node : tree->nodes)
1174 {
1175 const BTNodeLayout* layout = m_layoutEngine.GetNodeLayout(node.id);
1176 if (layout)
1177 {
1178 RenderNodePins(&node, layout);
1179 }
1180 }
1181 }
1182
1183 void BehaviorTreeDebugWindow::RenderNode(const BTNode* node, const BTNodeLayout* layout, bool isCurrentNode)
1184 {
1185 static std::unordered_set<uint32_t> printedNodeIds;
1186
1187 if (!node || !layout)
1188 return;
1189
1190 if (printedNodeIds.find(node->id) == printedNodeIds.end())
1191 {
1192 std::cout << "[RenderNode] Node " << node->id
1193 << " (" << node->name << ") at ("
1194 << (int)layout->position.x << ", " << (int)layout->position.y << ")" << std::endl;
1195 printedNodeIds.insert(node->id);
1196 }
1197
1198 ImNodes::SetNodeGridSpacePos(node->id, ImVec2(layout->position.x, layout->position.y));
1199
1200 ImNodes::BeginNode(node->id);
1201
1202 ImNodes::BeginNodeTitleBar();
1203
1205
1206 uint32_t color = GetNodeColorByStatus(node->type, nodeStatus);
1207 ImNodes::PushColorStyle(ImNodesCol_TitleBar, color);
1208 ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, color);
1209 ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, color);
1210
1211 const char* icon = GetNodeIcon(node->type);
1212 ImGui::Text("%s %s", icon, node->name.c_str());
1213
1214 ImNodes::PopColorStyle();
1215 ImNodes::PopColorStyle();
1216 ImNodes::PopColorStyle();
1217
1218 ImNodes::EndNodeTitleBar();
1219
1220 ImGui::PushItemWidth(200);
1221
1222 const char* typeStr = "Unknown";
1223 switch (node->type)
1224 {
1225 case BTNodeType::Selector: typeStr = "Selector"; break;
1226 case BTNodeType::Sequence: typeStr = "Sequence"; break;
1227 case BTNodeType::Condition: typeStr = "Condition"; break;
1228 case BTNodeType::Action: typeStr = "Action"; break;
1229 case BTNodeType::Inverter: typeStr = "Inverter"; break;
1230 case BTNodeType::Repeater: typeStr = "Repeater"; break;
1231 }
1232 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Type: %s", typeStr);
1233
1234 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "ID: %u", node->id);
1235
1236 ImGui::Dummy(ImVec2(0.0f, 5.0f));
1237
1238 ImGui::PopItemWidth();
1239
1240 if (node->id != 0)
1241 {
1242 ImNodes::BeginInputAttribute(node->id * 10000);
1243 ImGui::Text("In");
1244 ImNodes::EndInputAttribute();
1245 }
1246
1247 if (node->type == BTNodeType::Selector || node->type == BTNodeType::Sequence ||
1249 {
1250 ImNodes::BeginOutputAttribute(node->id * 10000 + 1);
1251 ImGui::Text("Out");
1252 ImNodes::EndOutputAttribute();
1253 }
1254
1255 if (isCurrentNode)
1256 {
1257 float pulse = 0.5f + 0.5f * sinf(m_pulseTimer * 2.0f * 3.14159265f);
1258 uint32_t highlightColor = IM_COL32(255, 255, 0, static_cast<int>(pulse * 255));
1259 ImNodes::PushColorStyle(ImNodesCol_NodeOutline, highlightColor);
1260 }
1261
1262 ImNodes::EndNode();
1263
1264 if (isCurrentNode)
1265 {
1266 ImNodes::PopColorStyle();
1267 }
1268
1269 ImNodes::SetNodeGridSpacePos(node->id, ImVec2(layout->position.x, layout->position.y));
1270 }
1271
1272 void BehaviorTreeDebugWindow::RenderNodeConnections(const BTNode* node, const BTNodeLayout* layout, const BehaviorTreeAsset* tree)
1273 {
1274 static int lastFrameCleared = -1;
1275 static int currentFrame = 0;
1276 currentFrame++;
1277
1278 if (m_editorMode && lastFrameCleared != currentFrame)
1279 {
1280 m_linkMap.clear();
1281 lastFrameCleared = currentFrame;
1282 }
1283
1284 if (node->type == BTNodeType::Selector || node->type == BTNodeType::Sequence)
1285 {
1286 for (uint32_t childId : node->childIds)
1287 {
1288 int linkId = m_nextLinkId++;
1289 ImNodes::Link(linkId, node->id * 10000 + 1, childId * 10000);
1290
1291 if (m_editorMode)
1292 {
1293 LinkInfo info;
1294 info.linkId = linkId;
1295 info.parentId = node->id;
1296 info.childId = childId;
1297 m_linkMap.push_back(info);
1298 }
1299 }
1300 }
1301 else if ((node->type == BTNodeType::Inverter || node->type == BTNodeType::Repeater) &&
1302 node->decoratorChildId != 0)
1303 {
1304 int linkId = m_nextLinkId++;
1305 ImNodes::Link(linkId, node->id * 10000 + 1, node->decoratorChildId * 10000);
1306
1307 if (m_editorMode)
1308 {
1309 LinkInfo info;
1310 info.linkId = linkId;
1311 info.parentId = node->id;
1312 info.childId = node->decoratorChildId;
1313 m_linkMap.push_back(info);
1314 }
1315 }
1316 }
1317
1318 uint32_t BehaviorTreeDebugWindow::GetNodeColor(BTNodeType type) const
1319 {
1320 switch (type)
1321 {
1323 return IM_COL32(100, 150, 255, 255);
1325 return IM_COL32(100, 255, 150, 255);
1327 return IM_COL32(255, 200, 100, 255);
1328 case BTNodeType::Action:
1329 return IM_COL32(255, 100, 150, 255);
1331 return IM_COL32(200, 100, 255, 255);
1333 return IM_COL32(150, 150, 255, 255);
1334 default:
1335 return IM_COL32(128, 128, 128, 255);
1336 }
1337 }
1338
1339 const char* BehaviorTreeDebugWindow::GetNodeIcon(BTNodeType type) const
1340 {
1341 switch (type)
1342 {
1343 case BTNodeType::Selector: return "?";
1344 case BTNodeType::Sequence: return "->";
1345 case BTNodeType::Condition: return "◆";
1346 case BTNodeType::Action: return "►";
1347 case BTNodeType::Inverter: return "!";
1348 case BTNodeType::Repeater: return "↻";
1349 default: return "•";
1350 }
1351 }
1352
1353 void BehaviorTreeDebugWindow::RenderInspectorPanel()
1354 {
1355 if (m_selectedEntity == 0)
1356 {
1357 ImGui::Text("No entity selected");
1358
1359 // Show validation panel even without entity in editor mode
1360 if (m_editorMode)
1361 {
1362 RenderValidationPanel();
1363 RenderNodeProperties();
1364 }
1365
1366 return;
1367 }
1368
1369 ImGui::Text("Inspector");
1370 ImGui::Separator();
1371
1372 // Editor mode panels
1373 if (m_editorMode)
1374 {
1375 if (ImGui::CollapsingHeader("Validation", ImGuiTreeNodeFlags_DefaultOpen))
1376 {
1377 RenderValidationPanel();
1378 }
1379
1380 if (m_inspectedNodeId != 0)
1381 {
1382 if (ImGui::CollapsingHeader("Node Properties", ImGuiTreeNodeFlags_DefaultOpen))
1383 {
1384 RenderNodeProperties();
1385 }
1386 }
1387 }
1388
1389 if (ImGui::CollapsingHeader("Runtime Info", ImGuiTreeNodeFlags_DefaultOpen))
1390 {
1391 RenderRuntimeInfo();
1392 }
1393
1394 if (ImGui::CollapsingHeader("Blackboard", ImGuiTreeNodeFlags_DefaultOpen))
1395 {
1396 RenderBlackboardSection();
1397 }
1398
1399 if (ImGui::CollapsingHeader("Execution Log", ImGuiTreeNodeFlags_DefaultOpen))
1400 {
1401 RenderExecutionLog();
1402 }
1403
1404 // Render new BT dialog if open
1405 RenderNewBTDialog();
1406 }
1407
1408 void BehaviorTreeDebugWindow::RenderRuntimeInfo()
1409 {
1410 auto& world = World::Get();
1411
1412 if (!world.HasComponent<BehaviorTreeRuntime_data>(m_selectedEntity))
1413 return;
1414
1415 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
1416
1418
1419 ImGui::Text("Tree ID: %u", btRuntime.AITreeAssetId);
1420
1421 if (tree)
1422 {
1423 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Tree Name: %s", tree->name.c_str());
1424 ImGui::Text("Node Count: %zu", tree->nodes.size());
1425 }
1426 else
1427 {
1428 ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Tree: NOT FOUND");
1429
1430 std::string path = BehaviorTreeManager::Get().GetTreePathFromId(btRuntime.AITreeAssetId);
1431 if (!path.empty())
1432 {
1433 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Expected: %s", path.c_str());
1434 }
1435
1436 if (ImGui::Button("Debug: List All Trees"))
1437 {
1439 }
1440 }
1441
1442 ImGui::Separator();
1443
1444 ImGui::Text("Current Node ID: %u", btRuntime.AICurrentNodeIndex);
1445
1446 if (tree)
1447 {
1448 const BTNode* currentNode = tree->GetNode(btRuntime.AICurrentNodeIndex);
1449 if (currentNode)
1450 {
1451 ImGui::Text("Node Name: %s", currentNode->name.c_str());
1452 }
1453 }
1454
1455 const char* statusStr = "Running";
1456 ImVec4 statusColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f);
1457 BTStatus status = static_cast<BTStatus>(btRuntime.lastStatus);
1458 if (status == BTStatus::Success)
1459 {
1460 statusStr = "Success";
1461 statusColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
1462 }
1463 else if (status == BTStatus::Failure)
1464 {
1465 statusStr = "Failure";
1466 statusColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
1467 }
1468 ImGui::TextColored(statusColor, "Last Status: %s", statusStr);
1469
1470 ImGui::Text("Active: %s", btRuntime.isActive ? "Yes" : "No");
1471
1472 if (world.HasComponent<AIState_data>(m_selectedEntity))
1473 {
1474 const auto& aiState = world.GetComponent<AIState_data>(m_selectedEntity);
1475 const char* modeStr = "Unknown";
1476 switch (aiState.currentMode)
1477 {
1478 case AIMode::Idle: modeStr = "Idle"; break;
1479 case AIMode::Patrol: modeStr = "Patrol"; break;
1480 case AIMode::Combat: modeStr = "Combat"; break;
1481 case AIMode::Flee: modeStr = "Flee"; break;
1482 case AIMode::Investigate: modeStr = "Investigate"; break;
1483 case AIMode::Dead: modeStr = "Dead"; break;
1484 }
1485 ImGui::Text("AI Mode: %s", modeStr);
1486 ImGui::Text("Time in Mode: %.2f s", aiState.timeInCurrentMode);
1487 }
1488 }
1489
1490 void BehaviorTreeDebugWindow::RenderBlackboardSection()
1491 {
1492 auto& world = World::Get();
1493
1494 if (!world.HasComponent<AIBlackboard_data>(m_selectedEntity))
1495 {
1496 ImGui::Text("No blackboard data");
1497 return;
1498 }
1499
1500 const auto& blackboard = world.GetComponent<AIBlackboard_data>(m_selectedEntity);
1501
1502 if (ImGui::TreeNode("Target"))
1503 {
1504 ImGui::Text("Has Target: %s", blackboard.hasTarget ? "Yes" : "No");
1505 ImGui::Text("Target Entity: %u", blackboard.targetEntity);
1506 ImGui::Text("Target Visible: %s", blackboard.targetVisible ? "Yes" : "No");
1507 ImGui::Text("Distance: %.2f", blackboard.distanceToTarget);
1508 ImGui::Text("Time Since Seen: %.2f s", blackboard.timeSinceTargetSeen);
1509 ImGui::Text("Last Known Pos: (%.1f, %.1f)",
1510 blackboard.lastKnownTargetPosition.x,
1511 blackboard.lastKnownTargetPosition.y);
1512 ImGui::TreePop();
1513 }
1514
1515 if (ImGui::TreeNode("Movement"))
1516 {
1517 ImGui::Text("Has Move Goal: %s", blackboard.hasMoveGoal ? "Yes" : "No");
1518 ImGui::Text("Goal Position: (%.1f, %.1f)",
1519 blackboard.moveGoal.x,
1520 blackboard.moveGoal.y);
1521 ImGui::TreePop();
1522 }
1523
1524 if (ImGui::TreeNode("Patrol"))
1525 {
1526 ImGui::Text("Has Patrol Path: %s", blackboard.hasPatrolPath ? "Yes" : "No");
1527 ImGui::Text("Current Point: %d", blackboard.currentPatrolPoint);
1528 ImGui::Text("Point Count: %d", blackboard.patrolPointCount);
1529 ImGui::TreePop();
1530 }
1531
1532 if (ImGui::TreeNode("Combat"))
1533 {
1534 ImGui::Text("Can Attack: %s", blackboard.canAttack ? "Yes" : "No");
1535 ImGui::Text("Attack Cooldown: %.2f s", blackboard.attackCooldown);
1536
1537 if (blackboard.lastAttackTime > 0.0f)
1538 {
1539 ImGui::Text("Last Attack Time: %.2f", blackboard.lastAttackTime);
1540 }
1541 else
1542 {
1543 ImGui::Text("Last Attack: Never");
1544 }
1545
1546 ImGui::TreePop();
1547 }
1548
1549 if (ImGui::TreeNode("Stimuli"))
1550 {
1551 ImGui::Text("Heard Noise: %s", blackboard.heardNoise ? "Yes" : "No");
1552 ImGui::Text("Last Damage: %.2f", blackboard.damageAmount);
1553 ImGui::TreePop();
1554 }
1555
1556 if (ImGui::TreeNode("Wander"))
1557 {
1558 ImGui::Text("Has Destination: %s", blackboard.hasWanderDestination ? "Yes" : "No");
1559 ImGui::Text("Destination: (%.1f, %.1f)",
1560 blackboard.wanderDestination.x,
1561 blackboard.wanderDestination.y);
1562 ImGui::Text("Wait Timer: %.2f / %.2f s",
1563 blackboard.wanderWaitTimer,
1564 blackboard.wanderTargetWaitTime);
1565 ImGui::TreePop();
1566 }
1567 }
1568
1569 void BehaviorTreeDebugWindow::RenderExecutionLog()
1570 {
1571 if (ImGui::Button("Clear Log"))
1572 {
1573 m_executionLog.clear();
1574 }
1575
1576 ImGui::Separator();
1577
1578 ImGui::BeginChild("ExecutionLogScroll", ImVec2(0, 0), false);
1579
1580 for (auto it = m_executionLog.rbegin(); it != m_executionLog.rend(); ++it)
1581 {
1582 const auto& entry = *it;
1583
1584 if (entry.entity != m_selectedEntity)
1585 continue;
1586
1587 ImVec4 color = ImVec4(1.0f, 1.0f, 0.0f, 1.0f);
1588 const char* icon = "▶";
1589 if (entry.status == BTStatus::Success)
1590 {
1591 color = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
1592 icon = "✓";
1593 }
1594 else if (entry.status == BTStatus::Failure)
1595 {
1596 color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
1597 icon = "✗";
1598 }
1599
1600 ImGui::TextColored(color, "[%.2fs ago] %s Node %u (%s)",
1601 entry.timeAgo, icon, entry.nodeId, entry.nodeName.c_str());
1602 }
1603
1604 ImGui::EndChild();
1605 }
1606
1607 void BehaviorTreeDebugWindow::AddExecutionEntry(EntityID entity, uint32_t nodeId, const std::string& nodeName, BTStatus status)
1608 {
1610 entry.timeAgo = 0.0f;
1611 entry.entity = entity;
1612 entry.nodeId = nodeId;
1613 entry.nodeName = nodeName;
1614 entry.status = status;
1615
1616 m_executionLog.push_back(entry);
1617
1618 while (m_executionLog.size() > MAX_LOG_ENTRIES)
1619 {
1620 m_executionLog.pop_front();
1621 }
1622 }
1623
1624 void BehaviorTreeDebugWindow::ApplyZoomToStyle()
1625 {
1626 ImNodes::GetStyle().NodePadding = ImVec2(8.0f * m_currentZoom, 8.0f * m_currentZoom);
1627 ImNodes::GetStyle().NodeCornerRounding = 8.0f * m_currentZoom;
1628 ImNodes::GetStyle().GridSpacing = 32.0f * m_currentZoom;
1629 }
1630
1631 void BehaviorTreeDebugWindow::GetGraphBounds(Vector& outMin, Vector& outMax) const
1632 {
1635
1636 for (const auto& layout : m_currentLayout)
1637 {
1638 outMin.x = std::min(outMin.x, layout.position.x - layout.width / 2.0f);
1639 outMin.y = std::min(outMin.y, layout.position.y - layout.height / 2.0f);
1640 outMax.x = std::max(outMax.x, layout.position.x + layout.width / 2.0f);
1641 outMax.y = std::max(outMax.y, layout.position.y + layout.height / 2.0f);
1642 }
1643 }
1644
1645 float BehaviorTreeDebugWindow::GetSafeZoom() const
1646 {
1647 return std::max(MIN_ZOOM, std::min(MAX_ZOOM, m_currentZoom));
1648 }
1649
1650 Vector BehaviorTreeDebugWindow::CalculatePanOffset(const Vector& graphCenter, const Vector& viewportSize) const
1651 {
1652 float safeZoom = GetSafeZoom();
1653
1654 return Vector(
1655 -graphCenter.x * safeZoom + viewportSize.x / 2.0f,
1656 -graphCenter.y * safeZoom + viewportSize.y / 2.0f
1657 );
1658 }
1659
1660 void BehaviorTreeDebugWindow::FitGraphToView()
1661 {
1662 if (m_currentLayout.empty())
1663 return;
1664
1666 GetGraphBounds(minPos, maxPos);
1667
1668 Vector graphSize(maxPos.x - minPos.x, maxPos.y - minPos.y);
1669 ImVec2 imvectmp = ImGui::GetContentRegionAvail();
1671
1672 if (graphSize.x <= 0.0f || graphSize.y <= 0.0f)
1673 {
1674 CenterViewOnGraph();
1675 return;
1676 }
1677
1678 float zoomX = viewportSize.x / graphSize.x;
1679 float zoomY = viewportSize.y / graphSize.y;
1680 float targetZoom = std::min(zoomX, zoomY) * 0.9f;
1681
1682 m_currentZoom = std::max(MIN_ZOOM, std::min(MAX_ZOOM, targetZoom));
1683 ApplyZoomToStyle();
1684
1685 Vector graphCenter((minPos.x + maxPos.x) / 2.0f, (minPos.y + maxPos.y) / 2.0f);
1686 Vector panOffset = CalculatePanOffset(graphCenter, viewportSize);
1687
1688 ImNodes::EditorContextResetPanning(ImVec2(panOffset.x, panOffset.y));
1689
1690 std::cout << "[BTDebugger] Fit to view: zoom=" << (int)(m_currentZoom * 100)
1691 << "%, center=(" << (int)graphCenter.x << "," << (int)graphCenter.y << ")" << std::endl;
1692 }
1693
1694 void BehaviorTreeDebugWindow::CenterViewOnGraph()
1695 {
1696 if (m_currentLayout.empty())
1697 return;
1698
1700 GetGraphBounds(minPos, maxPos);
1701
1702 Vector graphCenter((minPos.x + maxPos.x) / 2.0f, (minPos.y + maxPos.y) / 2.0f);
1703 ImVec2 imvectmp = ImGui::GetContentRegionAvail();
1705
1706 Vector panOffset = CalculatePanOffset(graphCenter, viewportSize);
1707
1708 ImNodes::EditorContextResetPanning(ImVec2(panOffset.x, panOffset.y));
1709
1710 std::cout << "[BTDebugger] Centered view on graph (" << (int)graphCenter.x
1711 << ", " << (int)graphCenter.y << ")" << std::endl;
1712 }
1713
1714 void BehaviorTreeDebugWindow::ResetZoom()
1715 {
1716 auto& world = World::Get();
1717 if (m_selectedEntity != 0 && world.HasComponent<BehaviorTreeRuntime_data>(m_selectedEntity))
1718 {
1719 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
1721
1722 if (tree)
1723 {
1724 m_currentZoom = 1.0f;
1725 m_currentLayout = m_layoutEngine.ComputeLayout(tree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1726 ApplyZoomToStyle();
1727
1728 std::cout << "[BTDebugger] Reset zoom to 100% (layout recomputed)" << std::endl;
1729 return;
1730 }
1731 }
1732
1733 m_currentZoom = 1.0f;
1734 ApplyZoomToStyle();
1735 std::cout << "[BTDebugger] Reset zoom to 100%" << std::endl;
1736 }
1737
1738 void BehaviorTreeDebugWindow::RenderMinimap()
1739 {
1740 if (m_currentLayout.empty())
1741 return;
1742
1743 const ImVec2 minimapSize(200, 150);
1744 const ImVec2 minimapPadding(10, 10);
1745
1746 ImVec2 contentMax = ImGui::GetWindowContentRegionMax();
1750 );
1751
1752 ImGui::SetCursorPos(minimapPos);
1753
1754 ImDrawList* drawList = ImGui::GetWindowDrawList();
1755 ImVec2 minimapMin = ImGui::GetCursorScreenPos();
1757
1758 drawList->AddRectFilled(minimapMin, minimapMax, IM_COL32(20, 20, 20, 200), 4.0f);
1759
1761 GetGraphBounds(graphMin, graphMax);
1762
1764
1765 if (graphSize.x <= 0.0f || graphSize.y <= 0.0f)
1766 {
1767 ImGui::SetCursorPos(ImVec2(minimapPos.x + 5, minimapPos.y + 5));
1768 ImGui::TextColored(ImVec4(1, 1, 1, 0.7f), "Minimap");
1769 return;
1770 }
1771
1772 float scaleX = minimapSize.x / graphSize.x;
1773 float scaleY = minimapSize.y / graphSize.y;
1774 float scale = std::min(scaleX, scaleY) * 0.9f;
1775
1776 if (scale <= 0.0f)
1777 {
1778 ImGui::SetCursorPos(ImVec2(minimapPos.x + 5, minimapPos.y + 5));
1779 ImGui::TextColored(ImVec4(1, 1, 1, 0.7f), "Minimap");
1780 return;
1781 }
1782
1783 for (const auto& layout : m_currentLayout)
1784 {
1785 float x = minimapMin.x + (layout.position.x - graphMin.x) * scale;
1786 float y = minimapMin.y + (layout.position.y - graphMin.y) * scale;
1787
1788 ImU32 color = IM_COL32(100, 150, 255, 255);
1789
1790 auto& world = World::Get();
1791 if (m_selectedEntity != 0 && world.HasComponent<BehaviorTreeRuntime_data>(m_selectedEntity))
1792 {
1793 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
1794 if (layout.nodeId == btRuntime.AICurrentNodeIndex)
1795 color = IM_COL32(255, 255, 0, 255);
1796 }
1797
1798 drawList->AddCircleFilled(ImVec2(x, y), 3.0f, color);
1799 }
1800
1801 ImVec2 panOffset = ImNodes::EditorContextGetPanning();
1802 ImVec2 viewportSize = ImGui::GetContentRegionAvail();
1803
1804 float safeZoom = GetSafeZoom();
1805
1806 float viewMinX = minimapMin.x + (-panOffset.x / safeZoom - graphMin.x) * scale;
1807 float viewMinY = minimapMin.y + (-panOffset.y / safeZoom - graphMin.y) * scale;
1808 float viewMaxX = viewMinX + (viewportSize.x / safeZoom) * scale;
1809 float viewMaxY = viewMinY + (viewportSize.y / safeZoom) * scale;
1810
1811 drawList->AddRect(
1814 IM_COL32(255, 0, 0, 150),
1815 0.0f,
1816 0,
1817 2.0f
1818 );
1819
1820 ImGui::SetCursorPos(ImVec2(minimapPos.x + 5, minimapPos.y + 5));
1821 ImGui::TextColored(ImVec4(1, 1, 1, 0.7f), "Minimap");
1822 }
1823
1824 void BehaviorTreeDebugWindow::RenderEditorToolbar()
1825 {
1826 if (ImGui::Button("Add Node"))
1827 {
1828 m_showNodePalette = true;
1829 m_nodeCreationPos.Set(ImGui::GetMousePos().x, ImGui::GetMousePos().y, 0.f);
1830 }
1831
1832 ImGui::SameLine();
1833 if (ImGui::Button("Save Tree"))
1834 {
1835 Save();
1836 }
1837
1838 ImGui::SameLine();
1839 bool canUndo = m_commandStack.CanUndo();
1840 if (!canUndo) ImGui::BeginDisabled();
1841 if (ImGui::Button("Undo"))
1842 {
1843 m_commandStack.Undo();
1844 m_isDirty = true;
1845 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1846 m_validationMessages = m_editingTree.ValidateTreeFull();
1847 }
1848 if (!canUndo) ImGui::EndDisabled();
1849
1850 ImGui::SameLine();
1851 bool canRedo = m_commandStack.CanRedo();
1852 if (!canRedo) ImGui::BeginDisabled();
1853 if (ImGui::Button("Redo"))
1854 {
1855 m_commandStack.Redo();
1856 m_isDirty = true;
1857 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1858 m_validationMessages = m_editingTree.ValidateTreeFull();
1859 }
1860 if (!canRedo) ImGui::EndDisabled();
1861
1862 ImGui::SameLine();
1863 ImGui::Text("Selected: %zu", m_selectedNodes.size());
1864 }
1865
1866 void BehaviorTreeDebugWindow::RenderNodePalette()
1867 {
1868 ImGui::OpenPopup("##NodePalette");
1869
1870 if (ImGui::BeginPopup("##NodePalette"))
1871 {
1872 ImGui::Text("Add Node");
1873 ImGui::Separator();
1874
1875 if (ImGui::MenuItem("Selector"))
1876 {
1877 HandleNodeCreation(BTNodeType::Selector);
1878 m_showNodePalette = false;
1879 }
1880
1881 if (ImGui::MenuItem("Sequence"))
1882 {
1883 HandleNodeCreation(BTNodeType::Sequence);
1884 m_showNodePalette = false;
1885 }
1886
1887 if (ImGui::MenuItem("Condition"))
1888 {
1889 HandleNodeCreation(BTNodeType::Condition);
1890 m_showNodePalette = false;
1891 }
1892
1893 if (ImGui::MenuItem("Action"))
1894 {
1895 HandleNodeCreation(BTNodeType::Action);
1896 m_showNodePalette = false;
1897 }
1898
1899 if (ImGui::MenuItem("Inverter"))
1900 {
1901 HandleNodeCreation(BTNodeType::Inverter);
1902 m_showNodePalette = false;
1903 }
1904
1905 if (ImGui::MenuItem("Repeater"))
1906 {
1907 HandleNodeCreation(BTNodeType::Repeater);
1908 m_showNodePalette = false;
1909 }
1910
1911 ImGui::EndPopup();
1912 }
1913 else
1914 {
1915 m_showNodePalette = false;
1916 }
1917 }
1918
1919 void BehaviorTreeDebugWindow::HandleNodeCreation(BTNodeType nodeType)
1920 {
1921 if (!m_editorMode)
1922 return;
1923
1924 // Get node name based on type
1925 std::string nodeName;
1926 switch (nodeType)
1927 {
1929 nodeName = "New Selector";
1930 break;
1932 nodeName = "New Sequence";
1933 break;
1935 nodeName = "New Condition";
1936 break;
1937 case BTNodeType::Action:
1938 nodeName = "New Action";
1939 break;
1941 nodeName = "New Inverter";
1942 break;
1944 nodeName = "New Repeater";
1945 break;
1946 }
1947
1948 // Initialize editing tree if empty
1949 if (m_editingTree.nodes.empty() && m_selectedEntity != 0)
1950 {
1951 auto& world = World::Get();
1952 if (world.HasComponent<BehaviorTreeRuntime_data>(m_selectedEntity))
1953 {
1954 const auto& btRuntime = world.GetComponent<BehaviorTreeRuntime_data>(m_selectedEntity);
1956
1957 if (originalTree)
1958 {
1959 m_editingTree = *originalTree;
1960 }
1961 else
1962 {
1963 m_editingTree.id = btRuntime.AITreeAssetId;
1964 m_editingTree.name = "New Tree";
1965 m_editingTree.rootNodeId = 0;
1966 }
1967 }
1968 }
1969
1970 // Use command pattern
1971 auto cmd = std::make_unique<AddNodeCommand>(&m_editingTree, nodeType, nodeName, m_nodeCreationPos);
1972 m_commandStack.Execute(std::move(cmd));
1973
1974 m_isDirty = true;
1975 m_treeModified = true;
1976
1977 // Update layout
1978 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
1979
1980 // Run validation
1981 m_validationMessages = m_editingTree.ValidateTreeFull();
1982
1983 std::cout << "[BTEditor] Created node: " << nodeName << std::endl;
1984 }
1985
1986 void BehaviorTreeDebugWindow::HandleNodeDeletion()
1987 {
1988 if (m_selectedNodes.empty() || !m_editorMode)
1989 return;
1990
1991 for (uint32_t nodeId : m_selectedNodes)
1992 {
1993 // Use command pattern
1994 auto cmd = std::make_unique<DeleteNodeCommand>(&m_editingTree, nodeId);
1995 m_commandStack.Execute(std::move(cmd));
1996
1997 std::cout << "[BTEditor] Deleted node ID: " << nodeId << std::endl;
1998 }
1999
2000 m_selectedNodes.clear();
2001 m_isDirty = true;
2002 m_treeModified = true;
2003
2004 // Update layout
2005 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
2006
2007 // Run validation
2008 m_validationMessages = m_editingTree.ValidateTreeFull();
2009 }
2010
2011 for (auto& node : m_editingTree.nodes)
2012 {
2013 auto childIt = std::find(node.childIds.begin(), node.childIds.end(), nodeId);
2014 if (childIt != node.childIds.end())
2015 {
2016 node.childIds.erase(childIt);
2017 }
2018
2019 if (node.decoratorChildId == nodeId)
2020 {
2021 node.decoratorChildId = 0;
2022 }
2023 }
2024
2025 std::cout << "[BTEditor] Deleted node ID: " << nodeId << std::endl;
2026 }
2027 }
2028
2029 m_selectedNodes.clear();
2032
2033 // Update layout
2034 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
2035
2036 // Run validation
2037 m_validationMessages = m_editingTree.ValidateTreeFull();
2038 }
2039
2040 void BehaviorTreeDebugWindow::HandleNodeDuplication()
2041 {
2042 if (m_selectedNodes.empty() || !m_editorMode)
2043 return;
2044
2045 std::vector<uint32_t> newNodes;
2046
2047 for (uint32_t nodeId : m_selectedNodes)
2048 {
2049 const BTNode* original = m_editingTree.GetNode(nodeId);
2050
2051 if (original)
2052 {
2053 std::string newName = original->name + " (Copy)";
2054 auto cmd = std::make_unique<AddNodeCommand>(&m_editingTree, original->type, newName, Vector());
2055 m_commandStack.Execute(std::move(cmd));
2056
2057 // Get the newly created node to copy parameters
2058 if (!m_editingTree.nodes.empty())
2059 {
2060 BTNode* newNode = &m_editingTree.nodes.back();
2061 newNode->actionType = original->actionType;
2062 newNode->actionParam1 = original->actionParam1;
2063 newNode->actionParam2 = original->actionParam2;
2064 newNode->conditionType = original->conditionType;
2065 newNode->conditionParam = original->conditionParam;
2066 newNode->repeatCount = original->repeatCount;
2067 newNode->stringParams = original->stringParams;
2068 newNode->intParams = original->intParams;
2069 newNode->floatParams = original->floatParams;
2070
2071 newNodes.push_back(newNode->id);
2072 }
2073
2074 std::cout << "[BTEditor] Duplicated node ID: " << nodeId << std::endl;
2075 }
2076 }
2077
2078 m_selectedNodes = newNodes;
2079 m_isDirty = true;
2080 m_treeModified = true;
2081
2082 // Update layout
2083 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
2084 }
2085
2086 std::cout << "[BTEditor] Duplicated node: " << duplicate.name << " (ID: " << duplicate.id << ")" << std::endl;
2087 }
2088 }
2089
2090 m_selectedNodes = newNodes;
2091 m_treeModified = true;
2092
2093 // Update layout
2094 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
2095
2096 // Run validation
2097 m_validationMessages = m_editingTree.ValidateTreeFull();
2098 }
2099
2100 bool BehaviorTreeDebugWindow::ValidateConnection(uint32_t parentId, uint32_t childId) const
2101 {
2102 const BTNode* parent = m_editingTree.GetNode(parentId);
2103 const BTNode* child = m_editingTree.GetNode(childId);
2104
2105 if (!parent || !child)
2106 return false;
2107
2108 if (parentId == childId)
2109 return false;
2110
2111 if (parent->type != BTNodeType::Selector &&
2112 parent->type != BTNodeType::Sequence &&
2113 parent->type != BTNodeType::Inverter &&
2114 parent->type != BTNodeType::Repeater)
2115 {
2116 return false;
2117 }
2118
2119 if ((parent->type == BTNodeType::Inverter || parent->type == BTNodeType::Repeater) &&
2120 parent->decoratorChildId != 0)
2121 {
2122 return false;
2123 }
2124
2125 if (parent->type == BTNodeType::Selector || parent->type == BTNodeType::Sequence)
2126 {
2127 if (std::find(parent->childIds.begin(), parent->childIds.end(), childId) != parent->childIds.end())
2128 {
2129 return false;
2130 }
2131 }
2132
2133 std::vector<uint32_t> visited;
2134 std::vector<uint32_t> toVisit;
2135 toVisit.push_back(childId);
2136
2137 while (!toVisit.empty())
2138 {
2139 uint32_t currentId = toVisit.back();
2140 toVisit.pop_back();
2141
2142 if (currentId == parentId)
2143 {
2144 return false;
2145 }
2146
2147 if (std::find(visited.begin(), visited.end(), currentId) != visited.end())
2148 {
2149 continue;
2150 }
2151
2152 visited.push_back(currentId);
2153
2154 const BTNode* current = m_editingTree.GetNode(currentId);
2155 if (current)
2156 {
2157 for (uint32_t id : current->childIds)
2158 {
2159 toVisit.push_back(id);
2160 }
2161 if (current->decoratorChildId != 0)
2162 {
2163 toVisit.push_back(current->decoratorChildId);
2164 }
2165 }
2166 }
2167
2168 return true;
2169 }
2170
2171 void BehaviorTreeDebugWindow::SaveEditedTree()
2172 {
2173 if (!m_treeModified)
2174 {
2175 std::cout << "[BTEditor] No changes to save" << std::endl;
2176 return;
2177 }
2178
2179 json treeJson;
2180 treeJson["schema_version"] = 2;
2181 treeJson["type"] = "BehaviorTree";
2182 treeJson["blueprintType"] = "BehaviorTree";
2183 treeJson["name"] = m_editingTree.name;
2184 treeJson["description"] = "Edited in BT Editor";
2185
2186 json metadata;
2187 metadata["author"] = "BT Editor";
2188
2189 auto now = std::time(nullptr);
2190 char timestamp[32];
2191 std::tm timeInfo;
2192
2193#ifdef _WIN32
2195#else
2197#endif
2198
2199 std::strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", &timeInfo);
2200 metadata["created"] = timestamp;
2201 metadata["lastModified"] = timestamp;
2202
2203 json tagsArray = json::array();
2204 tagsArray.push_back("AI");
2205 tagsArray.push_back("BehaviorTree");
2206 tagsArray.push_back("Edited");
2207 metadata["tags"] = tagsArray;
2208
2209 treeJson["metadata"] = metadata;
2210
2211 json editorState;
2212 editorState["zoom"] = 1.0;
2213 editorState["scrollOffset"] = { {"x", 0}, {"y", 0} };
2214 treeJson["editorState"] = editorState;
2215
2217 dataSection["rootNodeId"] = static_cast<int>(m_editingTree.rootNodeId);
2218
2219 json nodesArray = json::array();
2220 for (const auto& node : m_editingTree.nodes)
2221 {
2222 json nodeJson;
2223 nodeJson["id"] = static_cast<int>(node.id);
2224 nodeJson["name"] = node.name;
2225
2226 switch (node.type)
2227 {
2228 case BTNodeType::Selector: nodeJson["type"] = "Selector"; break;
2229 case BTNodeType::Sequence: nodeJson["type"] = "Sequence"; break;
2230 case BTNodeType::Condition: nodeJson["type"] = "Condition"; break;
2231 case BTNodeType::Action: nodeJson["type"] = "Action"; break;
2232 case BTNodeType::Inverter: nodeJson["type"] = "Inverter"; break;
2233 case BTNodeType::Repeater: nodeJson["type"] = "Repeater"; break;
2234 }
2235
2236 nodeJson["position"] = { {"x", 0.0}, {"y", 0.0} };
2237
2238 if (node.type == BTNodeType::Condition)
2239 {
2240 const char* conditionTypeStr = "TargetVisible";
2241 switch (node.conditionType)
2242 {
2243 case BTConditionType::TargetVisible: conditionTypeStr = "TargetVisible"; break;
2244 case BTConditionType::TargetInRange: conditionTypeStr = "TargetInRange"; break;
2245 case BTConditionType::HealthBelow: conditionTypeStr = "HealthBelow"; break;
2246 case BTConditionType::HasMoveGoal: conditionTypeStr = "HasMoveGoal"; break;
2247 case BTConditionType::CanAttack: conditionTypeStr = "CanAttack"; break;
2248 case BTConditionType::HeardNoise: conditionTypeStr = "HeardNoise"; break;
2249 case BTConditionType::IsWaitTimerExpired: conditionTypeStr = "IsWaitTimerExpired"; break;
2250 case BTConditionType::HasNavigableDestination: conditionTypeStr = "HasNavigableDestination"; break;
2251 case BTConditionType::HasValidPath: conditionTypeStr = "HasValidPath"; break;
2252 case BTConditionType::HasReachedDestination: conditionTypeStr = "HasReachedDestination"; break;
2253 }
2254 nodeJson["conditionType"] = conditionTypeStr;
2255
2256 if (node.conditionParam != 0.0f)
2257 {
2258 nodeJson["parameters"] = { {"param", node.conditionParam} };
2259 }
2260 else
2261 {
2262 nodeJson["parameters"] = json::object();
2263 }
2264 }
2265 else if (node.type == BTNodeType::Action)
2266 {
2267 const char* actionTypeStr = "Idle";
2268 switch (node.actionType)
2269 {
2270 case BTActionType::SetMoveGoalToLastKnownTargetPos: actionTypeStr = "SetMoveGoalToLastKnownTargetPos"; break;
2271 case BTActionType::SetMoveGoalToTarget: actionTypeStr = "SetMoveGoalToTarget"; break;
2272 case BTActionType::SetMoveGoalToPatrolPoint: actionTypeStr = "SetMoveGoalToPatrolPoint"; break;
2273 case BTActionType::MoveToGoal: actionTypeStr = "MoveToGoal"; break;
2274 case BTActionType::AttackIfClose: actionTypeStr = "AttackIfClose"; break;
2275 case BTActionType::PatrolPickNextPoint: actionTypeStr = "PatrolPickNextPoint"; break;
2276 case BTActionType::ClearTarget: actionTypeStr = "ClearTarget"; break;
2277 case BTActionType::Idle: actionTypeStr = "Idle"; break;
2278 case BTActionType::WaitRandomTime: actionTypeStr = "WaitRandomTime"; break;
2279 case BTActionType::ChooseRandomNavigablePoint: actionTypeStr = "ChooseRandomNavigablePoint"; break;
2280 case BTActionType::RequestPathfinding: actionTypeStr = "RequestPathfinding"; break;
2281 case BTActionType::FollowPath: actionTypeStr = "FollowPath"; break;
2282 }
2283 nodeJson["actionType"] = actionTypeStr;
2284
2285 json params = json::object();
2286 if (node.actionParam1 != 0.0f) params["param1"] = node.actionParam1;
2287 if (node.actionParam2 != 0.0f) params["param2"] = node.actionParam2;
2288 nodeJson["parameters"] = params;
2289 }
2290 else if (node.type == BTNodeType::Repeater)
2291 {
2292 nodeJson["repeatCount"] = node.repeatCount;
2293 }
2294
2295 if (!node.childIds.empty())
2296 {
2297 json childIdsArray = json::array();
2298 for (uint32_t cid : node.childIds)
2299 childIdsArray.push_back(static_cast<int>(cid));
2300 nodeJson["childIds"] = childIdsArray;
2301 }
2302 if (node.decoratorChildId != 0)
2303 {
2304 nodeJson["decoratorChildId"] = static_cast<int>(node.decoratorChildId);
2305 }
2306
2307 nodesArray.push_back(nodeJson);
2308 }
2309
2310 dataSection["nodes"] = nodesArray;
2311 treeJson["data"] = dataSection;
2312
2313 std::string filename = "Blueprints/AI/" + m_editingTree.name + "_edited.json";
2314
2315 try
2316 {
2317 std::ofstream file(filename);
2318 if (file.is_open())
2319 {
2320 file << treeJson.dump(2);
2321 file.close();
2322
2323 m_treeModified = false;
2324 std::cout << "[BTEditor] Tree saved to: " << filename << std::endl;
2325 }
2326 else
2327 {
2328 std::cerr << "[BTEditor] ERROR: Failed to open file for writing: " << filename << std::endl;
2329 }
2330 }
2331 catch (const std::exception& e)
2332 {
2333 std::cerr << "[BTEditor] ERROR: Exception during save: " << e.what() << std::endl;
2334 }
2335 }
2336
2337 void BehaviorTreeDebugWindow::UndoLastAction()
2338 {
2339 if (!m_commandStack.CanUndo())
2340 return;
2341
2342 m_commandStack.Undo();
2343
2344 m_isDirty = true;
2345 m_treeModified = true;
2346
2347 // Update layout
2348 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
2349
2350 // Run validation
2351 m_validationMessages = m_editingTree.ValidateTreeFull();
2352
2353 std::cout << "[BTEditor] Undo performed" << std::endl;
2354 }
2355
2356 void BehaviorTreeDebugWindow::RedoLastAction()
2357 {
2358 if (!m_commandStack.CanRedo())
2359 return;
2360
2361 m_commandStack.Redo();
2362
2363 m_isDirty = true;
2364 m_treeModified = true;
2365
2366 // Update layout
2367 m_currentLayout = m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);
2368
2369 // Run validation
2370 m_validationMessages = m_editingTree.ValidateTreeFull();
2371
2372 std::cout << "[BTEditor] Redo performed" << std::endl;
2373 }
2374
2375 void BehaviorTreeDebugWindow::LoadBTConfig()
2376 {
2378 if (!JsonHelper::LoadJsonFromFile("Config/BT_config.json", configJson))
2379 {
2380 std::cerr << "[BTDebugger] Failed to load BT_config.json, using defaults" << std::endl;
2381 m_configLoaded = false;
2382 return;
2383 }
2384
2385 if (JsonHelper::IsObject(configJson, "layout"))
2386 {
2387 const auto& layout = configJson["layout"];
2388 m_config.defaultHorizontal = JsonHelper::GetString(layout, "defaultDirection", "horizontal") == "horizontal";
2389 m_config.gridSize = JsonHelper::GetFloat(layout, "gridSize", 16.0f);
2390 m_config.gridSnappingEnabled = JsonHelper::GetBool(layout, "gridSnappingEnabled", true);
2391 m_config.horizontalSpacing = JsonHelper::GetFloat(layout, "horizontalSpacing", 280.0f);
2392 m_config.verticalSpacing = JsonHelper::GetFloat(layout, "verticalSpacing", 120.0f);
2393 }
2394
2395 if (JsonHelper::IsObject(configJson, "rendering"))
2396 {
2397 const auto& rendering = configJson["rendering"];
2398 m_config.pinRadius = JsonHelper::GetFloat(rendering, "pinRadius", 6.0f);
2399 m_config.pinOutlineThickness = JsonHelper::GetFloat(rendering, "pinOutlineThickness", 2.0f);
2400 m_config.bezierTangent = JsonHelper::GetFloat(rendering, "bezierTangent", 80.0f);
2401 m_config.connectionThickness = JsonHelper::GetFloat(rendering, "connectionThickness", 2.0f);
2402 }
2403
2404 if (JsonHelper::IsObject(configJson, "nodeColors"))
2405 {
2406 const auto& nodeColors = configJson["nodeColors"];
2407
2408 std::vector<std::string> nodeTypes = { "Selector", "Sequence", "Condition", "Action", "Inverter", "Repeater" };
2409 std::vector<std::string> statusTypes = { "idle", "running", "success", "failure", "aborted" };
2410
2411 for (const auto& nodeType : nodeTypes)
2412 {
2413 if (JsonHelper::IsObject(nodeColors, nodeType))
2414 {
2415 const auto& typeColors = nodeColors[nodeType];
2416
2417 for (const auto& status : statusTypes)
2418 {
2419 if (JsonHelper::IsObject(typeColors, status))
2420 {
2421 const auto& colorObj = typeColors[status];
2422 BTConfig::Color color;
2423 color.r = static_cast<uint8_t>(JsonHelper::GetInt(colorObj, "r", 128));
2424 color.g = static_cast<uint8_t>(JsonHelper::GetInt(colorObj, "g", 128));
2425 color.b = static_cast<uint8_t>(JsonHelper::GetInt(colorObj, "b", 128));
2426 color.a = static_cast<uint8_t>(JsonHelper::GetInt(colorObj, "a", 255));
2427
2428 m_config.nodeColors[nodeType][status] = color;
2429 }
2430 }
2431 }
2432 }
2433 }
2434
2435 if (configJson.contains("nodeColors") && configJson["nodeColors"].is_object())
2436 {
2437 const auto& colorsJson = configJson["nodeColors"];
2438
2439 std::map<std::string, BTNodeType> typeMap;
2440 typeMap["Selector"] = BTNodeType::Selector;
2441 typeMap["Sequence"] = BTNodeType::Sequence;
2442 typeMap["Action"] = BTNodeType::Action;
2443 typeMap["Condition"] = BTNodeType::Condition;
2444 typeMap["Inverter"] = BTNodeType::Inverter;
2445 typeMap["Repeater"] = BTNodeType::Repeater;
2446
2447 std::map<std::string, BTStatus> statusMap;
2448 statusMap["idle"] = BTStatus::Idle;
2449 statusMap["running"] = BTStatus::Running;
2450 statusMap["success"] = BTStatus::Success;
2451 statusMap["failure"] = BTStatus::Failure;
2452 statusMap["aborted"] = BTStatus::Aborted;
2453
2454 for (auto typeIt = colorsJson.begin(); typeIt != colorsJson.end(); ++typeIt)
2455 {
2456 const std::string typeName = typeIt.key();
2457 const auto& statusColors = typeIt.value();
2458
2459 auto typeMapIt = typeMap.find(typeName);
2460 if (typeMapIt == typeMap.end())
2461 continue;
2462
2463 BTNodeType nodeType = typeMapIt->second;
2464
2465 for (auto statusIt = statusColors.begin(); statusIt != statusColors.end(); ++statusIt)
2466 {
2467 const std::string statusName = statusIt.key();
2468 const auto& colorJson = statusIt.value();
2469
2470 auto statusMapIt = statusMap.find(statusName);
2471 if (statusMapIt == statusMap.end())
2472 continue;
2473
2474 BTStatus status = statusMapIt->second;
2475
2476 BTColor color;
2477 color.r = static_cast<uint8_t>(JsonHelper::GetInt(colorJson, "r", 255));
2478 color.g = static_cast<uint8_t>(JsonHelper::GetInt(colorJson, "g", 255));
2479 color.b = static_cast<uint8_t>(JsonHelper::GetInt(colorJson, "b", 255));
2480 color.a = static_cast<uint8_t>(JsonHelper::GetInt(colorJson, "a", 255));
2481
2482 m_nodeColors[nodeType][status] = color;
2483 }
2484 }
2485
2486 std::cout << "[BTDebugger] Loaded " << m_nodeColors.size() << " node color schemes" << std::endl;
2487 }
2488
2489 m_configLoaded = true;
2490 std::cout << "[BTDebugger] Configuration loaded from BT_config.json" << std::endl;
2491 }
2492
2493 void BehaviorTreeDebugWindow::ApplyConfigToLayout()
2494 {
2495 if (!m_configLoaded)
2496 return;
2497
2498 m_layoutDirection = m_config.defaultHorizontal ? BTLayoutDirection::LeftToRight : BTLayoutDirection::TopToBottom;
2499 m_layoutEngine.SetLayoutDirection(m_layoutDirection);
2500
2501 m_nodeSpacingX = m_config.horizontalSpacing;
2502 m_nodeSpacingY = m_config.verticalSpacing;
2503
2504 std::cout << "[BTDebugger] Applied configuration to layout engine" << std::endl;
2505 }
2506
2507 Vector BehaviorTreeDebugWindow::SnapToGrid(const Vector& pos) const
2508 {
2509 if (!m_config.gridSnappingEnabled)
2510 return pos;
2511
2512 float gridSize = m_config.gridSize;
2513 return Vector(
2514 std::round(pos.x / gridSize) * gridSize,
2515 std::round(pos.y / gridSize) * gridSize,
2516 pos.z
2517 );
2518 }
2519
2520 void BehaviorTreeDebugWindow::RenderBezierConnection(const Vector& start, const Vector& end, uint32_t color, float thickness, float tangent)
2521 {
2522 ImVec2 p1(start.x, start.y);
2523 ImVec2 p2(end.x, end.y);
2524
2525 ImVec2 cp1(p1.x + tangent, p1.y);
2526 ImVec2 cp2(p2.x - tangent, p2.y);
2527
2528 ImGui::GetWindowDrawList()->AddBezierCubic(p1, cp1, cp2, p2, color, thickness);
2529 }
2530
2531 void BehaviorTreeDebugWindow::RenderNodePins(const BTNode* node, const BTNodeLayout* layout)
2532 {
2533 if (!node || !layout)
2534 return;
2535
2536 float halfWidth = layout->width / 2.0f;
2537
2538 ImDrawList* drawList = ImGui::GetWindowDrawList();
2539
2540 if (node->id != 0)
2541 {
2542 Vector inputPinPos(layout->position.x - halfWidth, layout->position.y, 0.0f);
2544 uint32_t pinColor = IM_COL32(200, 200, 200, 255);
2545 uint32_t outlineColor = IM_COL32(80, 80, 80, 255);
2546
2547 drawList->AddCircleFilled(pinCenter, m_config.pinRadius + m_config.pinOutlineThickness, outlineColor);
2548 drawList->AddCircleFilled(pinCenter, m_config.pinRadius, pinColor);
2549 }
2550
2551 if (node->type == BTNodeType::Selector || node->type == BTNodeType::Sequence ||
2553 {
2554 Vector outputPinPos(layout->position.x + halfWidth, layout->position.y, 0.0f);
2556 uint32_t pinColor = IM_COL32(200, 200, 200, 255);
2557 uint32_t outlineColor = IM_COL32(80, 80, 80, 255);
2558
2559 drawList->AddCircleFilled(pinCenter, m_config.pinRadius + m_config.pinOutlineThickness, outlineColor);
2560 drawList->AddCircleFilled(pinCenter, m_config.pinRadius, pinColor);
2561 }
2562 }
2563
2564 uint32_t BehaviorTreeDebugWindow::GetNodeColorByStatus(BTNodeType type, BTStatus status) const
2565 {
2566 if (!m_configLoaded)
2567 {
2568 return GetNodeColor(type);
2569 }
2570
2571 auto typeIt = m_nodeColors.find(type);
2572 if (typeIt != m_nodeColors.end())
2573 {
2574 const auto& statusColors = typeIt->second;
2575 auto statusIt = statusColors.find(status);
2576 if (statusIt != statusColors.end())
2577 {
2578 const BTColor& color = statusIt->second;
2579 return IM_COL32(color.r, color.g, color.b, color.a);
2580 }
2581 }
2582
2583 return GetNodeColor(type);
2584 }
2585
2586 // =============================================================================
2587 // Validation Panel
2588 // =============================================================================
2589
2590 void BehaviorTreeDebugWindow::RenderValidationPanel()
2591 {
2592 if (!m_showValidationPanel || !m_editorMode)
2593 return;
2594
2595 ImGui::Separator();
2596 ImGui::Text("Validation (%zu messages)", m_validationMessages.size());
2597
2598 if (ImGui::BeginChild("ValidationMessages", ImVec2(0, 150), true))
2599 {
2600 for (const auto& msg : m_validationMessages)
2601 {
2602 ImVec4 color;
2603 const char* icon;
2604
2606 {
2607 color = ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
2608 icon = "[ERROR]";
2609 }
2610 else if (msg.severity == BTValidationMessage::Severity::Warning)
2611 {
2612 color = ImVec4(1.0f, 0.8f, 0.2f, 1.0f);
2613 icon = "[WARN]";
2614 }
2615 else
2616 {
2617 color = ImVec4(0.3f, 0.8f, 1.0f, 1.0f);
2618 icon = "[INFO]";
2619 }
2620
2621 ImGui::PushStyleColor(ImGuiCol_Text, color);
2622 ImGui::Text("%s Node %u: %s", icon, msg.nodeId, msg.message.c_str());
2623 ImGui::PopStyleColor();
2624 }
2625 }
2626 ImGui::EndChild();
2627 }
2628
2629 uint32_t BehaviorTreeDebugWindow::GetPinColor(uint32_t nodeId, PinType pinType) const
2630 {
2631 // Check validation messages for this node
2632 for (const auto& msg : m_validationMessages)
2633 {
2634 if (msg.nodeId == nodeId)
2635 {
2637 {
2638 return IM_COL32(255, 80, 80, 255); // Red
2639 }
2640 else if (msg.severity == BTValidationMessage::Severity::Warning)
2641 {
2642 return IM_COL32(255, 200, 80, 255); // Yellow
2643 }
2644 }
2645 }
2646
2647 return IM_COL32(80, 255, 80, 255); // Green - valid
2648 }
2649
2650 bool BehaviorTreeDebugWindow::IsConnectionValid(uint32_t parentId, uint32_t childId) const
2651 {
2652 if (!m_editorMode)
2653 return false;
2654
2655 // Use the validation method from BehaviorTree
2656 const BTNode* parent = m_editingTree.GetNode(parentId);
2657 const BTNode* child = m_editingTree.GetNode(childId);
2658
2659 if (!parent || !child)
2660 return false;
2661
2662 // Check if parent can have children
2663 if (parent->type != BTNodeType::Selector &&
2664 parent->type != BTNodeType::Sequence &&
2665 parent->type != BTNodeType::Inverter &&
2666 parent->type != BTNodeType::Repeater)
2667 {
2668 return false;
2669 }
2670
2671 // Check decorator constraint
2672 if ((parent->type == BTNodeType::Inverter || parent->type == BTNodeType::Repeater) &&
2673 parent->decoratorChildId != 0 && parent->decoratorChildId != childId)
2674 {
2675 return false;
2676 }
2677
2678 // Check for cycles by creating a temporary connection
2679 BehaviorTreeAsset tempTree = m_editingTree;
2680 tempTree.ConnectNodes(parentId, childId);
2681
2682 return !tempTree.DetectCycle(parentId);
2683 }
2684
2685 // =============================================================================
2686 // Node Properties Editor
2687 // =============================================================================
2688
2689 void BehaviorTreeDebugWindow::RenderNodeProperties()
2690 {
2691 if (!m_showNodeProperties || m_inspectedNodeId == 0)
2692 return;
2693
2694 BTNode* node = m_editingTree.GetNode(m_inspectedNodeId);
2695 if (!node)
2696 {
2697 m_showNodeProperties = false;
2698 return;
2699 }
2700
2701 ImGui::Separator();
2702 ImGui::Text("Node Properties");
2703
2704 if (ImGui::BeginChild("NodeProperties", ImVec2(0, 300), true))
2705 {
2706 ImGui::Text("ID: %u", node->id);
2707 ImGui::Text("Type: %d", static_cast<int>(node->type));
2708
2709 // Editable name
2710 char nameBuf[256];
2711 strncpy(nameBuf, node->name.c_str(), sizeof(nameBuf) - 1);
2712 nameBuf[sizeof(nameBuf) - 1] = '\0';
2713
2714 if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
2715 {
2716 node->name = nameBuf;
2717 m_isDirty = true;
2718 }
2719
2720 // Type-specific parameters
2721 if (node->type == BTNodeType::Action)
2722 {
2723 ImGui::Separator();
2724 ImGui::Text("Action Parameters");
2725
2726 // Action type dropdown
2727 const char* actionTypes[] = {
2728 "SetMoveGoalToLastKnownTargetPos",
2729 "SetMoveGoalToTarget",
2730 "SetMoveGoalToPatrolPoint",
2731 "MoveToGoal",
2732 "AttackIfClose",
2733 "PatrolPickNextPoint",
2734 "ClearTarget",
2735 "Idle",
2736 "WaitRandomTime",
2737 "ChooseRandomNavigablePoint",
2738 "RequestPathfinding",
2739 "FollowPath"
2740 };
2741
2742 int currentAction = static_cast<int>(node->actionType);
2743 if (ImGui::Combo("Action Type", &currentAction, actionTypes, 12))
2744 {
2745 node->actionType = static_cast<BTActionType>(currentAction);
2746 m_isDirty = true;
2747 }
2748
2749 // Parameters
2750 if (ImGui::InputFloat("Param 1", &node->actionParam1))
2751 {
2752 m_isDirty = true;
2753 }
2754
2755 if (ImGui::InputFloat("Param 2", &node->actionParam2))
2756 {
2757 m_isDirty = true;
2758 }
2759 }
2760 else if (node->type == BTNodeType::Condition)
2761 {
2762 ImGui::Separator();
2763 ImGui::Text("Condition Parameters");
2764
2765 // Condition type dropdown
2766 const char* conditionTypes[] = {
2767 "TargetVisible",
2768 "TargetInRange",
2769 "HealthBelow",
2770 "HasMoveGoal",
2771 "CanAttack",
2772 "HeardNoise",
2773 "IsWaitTimerExpired",
2774 "HasNavigableDestination",
2775 "HasValidPath",
2776 "HasReachedDestination"
2777 };
2778
2779 int currentCondition = static_cast<int>(node->conditionType);
2780 if (ImGui::Combo("Condition Type", &currentCondition, conditionTypes, 10))
2781 {
2782 node->conditionType = static_cast<BTConditionType>(currentCondition);
2783 m_isDirty = true;
2784 }
2785
2786 // Parameter
2787 if (ImGui::InputFloat("Param", &node->conditionParam))
2788 {
2789 m_isDirty = true;
2790 }
2791 }
2792 else if (node->type == BTNodeType::Repeater)
2793 {
2794 ImGui::Separator();
2795 ImGui::Text("Repeater Parameters");
2796
2797 if (ImGui::InputInt("Repeat Count", &node->repeatCount))
2798 {
2799 m_isDirty = true;
2800 }
2801 }
2802 }
2803 ImGui::EndChild();
2804
2805 if (ImGui::Button("Close Properties"))
2806 {
2807 m_showNodeProperties = false;
2808 }
2809 }
2810
2811 // =============================================================================
2812 // JSON Save System
2813 // =============================================================================
2814
2815 std::string BehaviorTreeDebugWindow::GetCurrentTimestamp() const
2816 {
2817 time_t now = time(nullptr);
2818 struct tm timeinfo;
2819
2820 #ifdef _WIN32
2822 #else
2824 #endif
2825
2826 char buffer[32];
2827 strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
2828 return std::string(buffer);
2829 }
2830
2831 json BehaviorTreeDebugWindow::SerializeTreeToJson(const BehaviorTreeAsset& tree) const
2832 {
2833 json j = json::object();
2834
2835 j["schema_version"] = 2;
2836 j["type"] = "BehaviorTree";
2837 j["blueprintType"] = "BehaviorTree";
2838 j["name"] = tree.name;
2839 j["description"] = "";
2840
2841 // Metadata
2842 json metadata = json::object();
2843 metadata["author"] = "Atlasbruce";
2844 metadata["created"] = GetCurrentTimestamp();
2845 metadata["lastModified"] = GetCurrentTimestamp();
2846
2847 json tags = json::array();
2848 tags.push_back("AI");
2849 tags.push_back("BehaviorTree");
2850 metadata["tags"] = tags;
2851
2852 j["metadata"] = metadata;
2853
2854 // Editor state
2855 json editorState = json::object();
2856 editorState["zoom"] = 1.0f;
2857
2858 json scrollOffset = json::object();
2859 scrollOffset["x"] = 0.0f;
2860 scrollOffset["y"] = 0.0f;
2861 editorState["scrollOffset"] = scrollOffset;
2862
2863 j["editorState"] = editorState;
2864
2865 // Data
2866 json data = json::object();
2867 data["rootNodeId"] = tree.rootNodeId;
2868
2869 json nodesArray = json::array();
2870 for (const auto& node : tree.nodes)
2871 {
2872 json nodeJson = json::object();
2873 nodeJson["id"] = node.id;
2874 nodeJson["name"] = node.name;
2875
2876 // Node type
2877 const char* typeStr = "";
2878 switch (node.type)
2879 {
2880 case BTNodeType::Selector: typeStr = "Selector"; break;
2881 case BTNodeType::Sequence: typeStr = "Sequence"; break;
2882 case BTNodeType::Condition: typeStr = "Condition"; break;
2883 case BTNodeType::Action: typeStr = "Action"; break;
2884 case BTNodeType::Inverter: typeStr = "Inverter"; break;
2885 case BTNodeType::Repeater: typeStr = "Repeater"; break;
2886 }
2887 nodeJson["type"] = typeStr;
2888
2889 // Position (placeholder - would need to get from layout)
2890 json pos = json::object();
2891 pos["x"] = 200.0f;
2892 pos["y"] = 100.0f * static_cast<float>(node.id);
2893 nodeJson["position"] = pos;
2894
2895 // Children array (for composites)
2896 if (node.type == BTNodeType::Selector || node.type == BTNodeType::Sequence)
2897 {
2898 json children = json::array();
2899 for (uint32_t childId : node.childIds)
2900 {
2901 children.push_back(childId);
2902 }
2903 nodeJson["children"] = children;
2904 }
2905
2906 // Decorator child
2907 if (node.type == BTNodeType::Inverter || node.type == BTNodeType::Repeater)
2908 {
2909 if (node.decoratorChildId != 0)
2910 {
2911 nodeJson["decoratorChildId"] = node.decoratorChildId;
2912 }
2913
2914 if (node.type == BTNodeType::Repeater)
2915 {
2916 nodeJson["repeatCount"] = node.repeatCount;
2917 }
2918 }
2919
2920 // Action type and parameters
2921 if (node.type == BTNodeType::Action)
2922 {
2923 const char* actionTypeStr = "";
2924 switch (node.actionType)
2925 {
2926 case BTActionType::SetMoveGoalToLastKnownTargetPos: actionTypeStr = "SetMoveGoalToLastKnownTargetPos"; break;
2927 case BTActionType::SetMoveGoalToTarget: actionTypeStr = "SetMoveGoalToTarget"; break;
2928 case BTActionType::SetMoveGoalToPatrolPoint: actionTypeStr = "SetMoveGoalToPatrolPoint"; break;
2929 case BTActionType::MoveToGoal: actionTypeStr = "MoveToGoal"; break;
2930 case BTActionType::AttackIfClose: actionTypeStr = "AttackIfClose"; break;
2931 case BTActionType::PatrolPickNextPoint: actionTypeStr = "PatrolPickNextPoint"; break;
2932 case BTActionType::ClearTarget: actionTypeStr = "ClearTarget"; break;
2933 case BTActionType::Idle: actionTypeStr = "Idle"; break;
2934 case BTActionType::WaitRandomTime: actionTypeStr = "WaitRandomTime"; break;
2935 case BTActionType::ChooseRandomNavigablePoint: actionTypeStr = "ChooseRandomNavigablePoint"; break;
2936 case BTActionType::RequestPathfinding: actionTypeStr = "RequestPathfinding"; break;
2937 case BTActionType::FollowPath: actionTypeStr = "FollowPath"; break;
2938 }
2939 nodeJson["actionType"] = actionTypeStr;
2940
2941 json params = json::object();
2942 params["param1"] = node.actionParam1;
2943 params["param2"] = node.actionParam2;
2944 nodeJson["parameters"] = params;
2945 }
2946
2947 // Condition type and parameters
2948 if (node.type == BTNodeType::Condition)
2949 {
2950 const char* conditionTypeStr = "";
2951 switch (node.conditionType)
2952 {
2953 case BTConditionType::TargetVisible: conditionTypeStr = "TargetVisible"; break;
2954 case BTConditionType::TargetInRange: conditionTypeStr = "TargetInRange"; break;
2955 case BTConditionType::HealthBelow: conditionTypeStr = "HealthBelow"; break;
2956 case BTConditionType::HasMoveGoal: conditionTypeStr = "HasMoveGoal"; break;
2957 case BTConditionType::CanAttack: conditionTypeStr = "CanAttack"; break;
2958 case BTConditionType::HeardNoise: conditionTypeStr = "HeardNoise"; break;
2959 case BTConditionType::IsWaitTimerExpired: conditionTypeStr = "IsWaitTimerExpired"; break;
2960 case BTConditionType::HasNavigableDestination: conditionTypeStr = "HasNavigableDestination"; break;
2961 case BTConditionType::HasValidPath: conditionTypeStr = "HasValidPath"; break;
2962 case BTConditionType::HasReachedDestination: conditionTypeStr = "HasReachedDestination"; break;
2963 }
2964 nodeJson["conditionType"] = conditionTypeStr;
2965
2966 json params = json::object();
2967 params["param"] = node.conditionParam;
2968 nodeJson["parameters"] = params;
2969 }
2970
2971 // Default empty parameters if not set
2972 if (!nodeJson.contains("parameters"))
2973 {
2974 nodeJson["parameters"] = json::object();
2975 }
2976
2977 nodesArray.push_back(nodeJson);
2978 }
2979
2980 data["nodes"] = nodesArray;
2981 j["data"] = data;
2982
2983 return j;
2984 }
2985
2986 void BehaviorTreeDebugWindow::Save()
2987 {
2988 if (m_currentFilePath.empty())
2989 {
2990 SaveAs();
2991 return;
2992 }
2993
2994 // Validate before saving
2995 m_validationMessages = m_editingTree.ValidateTreeFull();
2996
2997 // Check for critical errors
2998 bool hasErrors = false;
2999 for (const auto& msg : m_validationMessages)
3000 {
3002 {
3003 hasErrors = true;
3004 break;
3005 }
3006 }
3007
3008 if (hasErrors)
3009 {
3010 std::cout << "[BTEditor] Cannot save: tree has validation errors" << std::endl;
3011 return;
3012 }
3013
3014 // Serialize and save
3015 json j = SerializeTreeToJson(m_editingTree);
3016
3017 if (JsonHelper::SaveJsonToFile(m_currentFilePath, j, 2))
3018 {
3019 std::cout << "[BTEditor] Saved tree to: " << m_currentFilePath << std::endl;
3020 m_isDirty = false;
3021 }
3022 else
3023 {
3024 std::cout << "[BTEditor] Failed to save tree" << std::endl;
3025 }
3026 }
3027
3028 void BehaviorTreeDebugWindow::SaveAs()
3029 {
3030 // Generate filename from tree name
3031 std::string filename = "Blueprints/AI/" + m_editingTree.name + "_edited.json";
3032 m_currentFilePath = filename;
3033 Save();
3034 }
3035
3036 void BehaviorTreeDebugWindow::RenderFileMenu()
3037 {
3038 if (ImGui::BeginMenu("File"))
3039 {
3040 if (ImGui::MenuItem("New BT...", ""))
3041 {
3042 m_showNewBTDialog = true;
3043 }
3044
3045 ImGui::Separator();
3046
3047 if (ImGui::MenuItem("Save", "Ctrl+S", false, m_editorMode && m_isDirty))
3048 {
3049 Save();
3050 }
3051
3052 if (ImGui::MenuItem("Save As...", "Ctrl+Shift+S", false, m_editorMode))
3053 {
3054 SaveAs();
3055 }
3056
3057 ImGui::Separator();
3058
3059 if (ImGui::MenuItem("Close", "", false, m_editorMode))
3060 {
3061 // TODO: Add confirmation dialog if dirty
3062 m_editorMode = false;
3063 }
3064
3065 ImGui::EndMenu();
3066 }
3067 }
3068
3069 void BehaviorTreeDebugWindow::RenderEditMenu()
3070 {
3071 if (ImGui::BeginMenu("Edit"))
3072 {
3073 bool canUndo = m_commandStack.CanUndo();
3074 bool canRedo = m_commandStack.CanRedo();
3075
3076 std::string undoText = "Undo";
3077 if (canUndo)
3078 {
3079 undoText += " (" + m_commandStack.GetUndoDescription() + ")";
3080 }
3081
3082 std::string redoText = "Redo";
3083 if (canRedo)
3084 {
3085 redoText += " (" + m_commandStack.GetRedoDescription() + ")";
3086 }
3087
3088 if (ImGui::MenuItem(undoText.c_str(), "Ctrl+Z", false, canUndo))
3089 {
3090 m_commandStack.Undo();
3091 m_isDirty = true;
3092 }
3093
3094 if (ImGui::MenuItem(redoText.c_str(), "Ctrl+Y", false, canRedo))
3095 {
3096 m_commandStack.Redo();
3097 m_isDirty = true;
3098 }
3099
3100 ImGui::EndMenu();
3101 }
3102 }
3103
3104 // =============================================================================
3105 // New BT from Template
3106 // =============================================================================
3107
3108 BehaviorTreeAsset BehaviorTreeDebugWindow::CreateFromTemplate(int templateIndex, const std::string& name)
3109 {
3111 tree.name = name;
3112 tree.id = 9999; // Temporary ID
3113
3114 if (templateIndex == 0)
3115 {
3116 // Empty template - just a root Selector
3117 BTNode root;
3119 root.id = 1;
3120 root.name = "Root Selector";
3121 tree.nodes.push_back(root);
3122 tree.rootNodeId = 1;
3123 }
3124 else if (templateIndex == 1)
3125 {
3126 // Basic AI - idle + wander
3127 BTNode root;
3129 root.id = 1;
3130 root.name = "Root Selector";
3131 root.childIds.push_back(2);
3132 tree.nodes.push_back(root);
3133
3136 sequence.id = 2;
3137 sequence.name = "Wander Sequence";
3138 sequence.childIds.push_back(3);
3139 sequence.childIds.push_back(4);
3140 tree.nodes.push_back(sequence);
3141
3142 BTNode wait;
3144 wait.id = 3;
3145 wait.name = "Wait";
3147 wait.actionParam1 = 2.0f;
3148 wait.actionParam2 = 6.0f;
3149 tree.nodes.push_back(wait);
3150
3151 BTNode choose;
3153 choose.id = 4;
3154 choose.name = "Choose Point";
3156 choose.actionParam1 = 500.0f;
3157 choose.actionParam2 = 10.0f;
3158 tree.nodes.push_back(choose);
3159
3160 tree.rootNodeId = 1;
3161 }
3162 else if (templateIndex == 2)
3163 {
3164 // Patrol template
3165 BTNode root;
3167 root.id = 1;
3168 root.name = "Patrol Sequence";
3169 root.childIds.push_back(2);
3170 root.childIds.push_back(3);
3171 root.childIds.push_back(4);
3172 tree.nodes.push_back(root);
3173
3174 BTNode pick;
3176 pick.id = 2;
3177 pick.name = "Pick Next Point";
3179 tree.nodes.push_back(pick);
3180
3183 setGoal.id = 3;
3184 setGoal.name = "Set Goal";
3186 tree.nodes.push_back(setGoal);
3187
3188 BTNode move;
3190 move.id = 4;
3191 move.name = "Move";
3192 move.actionType = BTActionType::MoveToGoal;
3193 tree.nodes.push_back(move);
3194
3195 tree.rootNodeId = 1;
3196 }
3197 else if (templateIndex == 3)
3198 {
3199 // Combat template
3200 BTNode root;
3202 root.id = 1;
3203 root.name = "Root Selector";
3204 root.childIds.push_back(2);
3205 root.childIds.push_back(5);
3206 tree.nodes.push_back(root);
3207
3208 // Combat branch
3211 combatSeq.id = 2;
3212 combatSeq.name = "Combat Sequence";
3213 combatSeq.childIds.push_back(3);
3214 combatSeq.childIds.push_back(4);
3215 tree.nodes.push_back(combatSeq);
3216
3217 BTNode hasTarget;
3218 hasTarget.type = BTNodeType::Condition;
3219 hasTarget.id = 3;
3220 hasTarget.name = "Has Target";
3222 tree.nodes.push_back(hasTarget);
3223
3224 BTNode attack;
3226 attack.id = 4;
3227 attack.name = "Attack";
3229 tree.nodes.push_back(attack);
3230
3231 // Wander branch
3232 BTNode wander;
3234 wander.id = 5;
3235 wander.name = "Wander";
3237 wander.actionParam1 = 300.0f;
3238 tree.nodes.push_back(wander);
3239
3240 tree.rootNodeId = 1;
3241 }
3242
3243 return tree;
3244 }
3245
3246 void BehaviorTreeDebugWindow::RenderNewBTDialog()
3247 {
3248 if (!m_showNewBTDialog)
3249 return;
3250
3251 ImGui::OpenPopup("New Behavior Tree");
3252
3253 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
3254 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
3255
3256 if (ImGui::BeginPopupModal("New Behavior Tree", &m_showNewBTDialog, ImGuiWindowFlags_AlwaysAutoResize))
3257 {
3258 ImGui::Text("Create a new behavior tree from template");
3259 ImGui::Separator();
3260
3261 ImGui::InputText("Name", m_newBTName, sizeof(m_newBTName));
3262
3263 ImGui::Text("Template:");
3264 ImGui::RadioButton("Empty (root node only)", &m_selectedTemplate, 0);
3265 ImGui::RadioButton("Basic AI (idle + wander)", &m_selectedTemplate, 1);
3266 ImGui::RadioButton("Patrol (patrol points)", &m_selectedTemplate, 2);
3267 ImGui::RadioButton("Combat (combat + wander)", &m_selectedTemplate, 3);
3268
3269 ImGui::Separator();
3270
3271 if (ImGui::Button("Create", ImVec2(120, 0)))
3272 {
3273 if (strlen(m_newBTName) > 0)
3274 {
3275 m_editingTree = CreateFromTemplate(m_selectedTemplate, std::string(m_newBTName));
3276 m_editorMode = true;
3277 m_isDirty = true;
3278 m_currentFilePath = "";
3279 m_showNewBTDialog = false;
3280 std::cout << "[BTEditor] Created new tree: " << m_newBTName << std::endl;
3281 }
3282 }
3283
3284 ImGui::SameLine();
3285
3286 if (ImGui::Button("Cancel", ImVec2(120, 0)))
3287 {
3288 m_showNewBTDialog = false;
3289 }
3290
3291 ImGui::EndPopup();
3292 }
3293 }
3294
3295} // namespace Olympe
Command pattern implementation for behavior tree editor undo/redo.
std::cout<< "[BTEditor] Duplicated node: "<< duplicate.name<< " (ID: "<< duplicate.id<< ")"<< std::endl;} } m_selectedNodes=newNodes;m_treeModified=true;m_currentLayout=m_layoutEngine.ComputeLayout(&m_editingTree, m_nodeSpacingX, m_nodeSpacingY, m_currentZoom);m_validationMessages=m_editingTree.ValidateTreeFull();} bool BehaviorTreeDebugWindow::ValidateConnection(uint32_t parentId, uint32_t childId) const { const BTNode *parent=m_editingTree.GetNode(parentId);const BTNode *child=m_editingTree.GetNode(childId);if(!parent||!child) return false;if(parentId==childId) return false;if(parent->type !=BTNodeType::Selector &&parent->type !=BTNodeType::Sequence &&parent->type !=BTNodeType::Inverter &&parent->type !=BTNodeType::Repeater) { return false;} if((parent->type==BTNodeType::Inverter||parent->type==BTNodeType::Repeater) &&parent->decoratorChildId !=0) { return false;} if(parent->type==BTNodeType::Selector||parent->type==BTNodeType::Sequence) { if(std::find(parent->childIds.begin(), parent->childIds.end(), childId) !=parent->childIds.end()) { return false;} } std::vector< uint32_t > visited
std::vector< uint32_t > toVisit
Runtime debugger for behavior tree visualization and inspection.
BTActionType
Built-in action types for behavior trees.
@ SetMoveGoalToTarget
Move towards current target.
@ SetMoveGoalToLastKnownTargetPos
Move to last seen target position.
@ WaitRandomTime
Initialize random timer (param1=min, param2=max)
@ MoveToGoal
Execute movement to goal.
@ ClearTarget
Clear current target.
@ PatrolPickNextPoint
Select next patrol point.
@ ChooseRandomNavigablePoint
Choose navigable point (param1=searchRadius, param2=maxAttempts)
@ AttackIfClose
Attack if in range.
@ Idle
Do nothing.
@ SetMoveGoalToPatrolPoint
Move to next patrol waypoint.
@ RequestPathfinding
Request pathfinding to moveGoal via MoveIntent.
@ FollowPath
Follow the path (check progression)
BTStatus
Behavior tree node execution status.
@ Success
Node completed successfully.
@ Running
Node is currently executing.
@ Aborted
Node execution interrupted (e.g., entity destroyed)
@ Failure
Node failed.
@ Idle
Node waiting for execution (not yet started)
BTNodeType
Behavior tree node types.
@ Action
Leaf node - performs an action.
@ Selector
OR node - succeeds if any child succeeds.
@ Sequence
AND node - succeeds if all children succeed.
@ Inverter
Decorator - inverts child result.
@ Condition
Leaf node - checks a condition.
@ Repeater
Decorator - repeats child N times.
BTConditionType
Built-in condition types for behavior trees.
@ HasValidPath
Valid path calculated?
@ CanAttack
Attack is available.
@ IsWaitTimerExpired
Wait timer expired?
@ HeardNoise
Detected noise.
@ TargetVisible
Can see target entity.
@ HasMoveGoal
Movement goal is set.
@ HasNavigableDestination
Navigable destination chosen?
@ HealthBelow
Health below threshold.
@ HasReachedDestination
Reached destination?
@ TargetInRange
Target within specified range.
@ Investigate
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
std::uint64_t EntityID
Definition ECS_Entity.h:21
std::string GetTreePathFromId(uint32_t treeId) const
void DebugPrintLoadedTrees() const
static BehaviorTreeManager & Get()
const BehaviorTreeAsset * GetTreeByAnyId(uint32_t treeId) const
static float fDt
Delta time between frames in seconds.
Definition GameEngine.h:120
bool CanRedo() const
Check if redo is available.
void Undo()
Undo the last command.
bool CanUndo() const
Check if undo is available.
void Redo()
Redo the last undone command.
std::vector< BTNodeLayout > ComputeLayout(const BehaviorTreeAsset *tree, float nodeSpacingX=180.0f, float nodeSpacingY=120.0f, float zoomFactor=1.0f)
Compute layout for a behavior tree.
void Initialize()
Initialize the debug window.
std::vector< EntityDebugInfo > m_filteredEntities
void ToggleVisibility()
Toggle window visibility (creates/destroys separate window)
std::deque< ExecutionLogEntry > m_executionLog
void Render()
Render the debug window (in separate SDL3 window)
std::vector< BTNodeLayout > m_currentLayout
std::vector< EntityDebugInfo > m_entities
void ProcessEvent(SDL_Event *event)
Process SDL events for separate window.
static World & Get()
Get singleton instance (short form)
Definition World.h:232
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
Safely get a string value from JSON.
bool LoadJsonFromFile(const std::string &filepath, json &j)
Load and parse a JSON file.
Definition json_helper.h:42
int GetInt(const json &j, const std::string &key, int defaultValue=0)
Safely get an integer value from JSON.
float GetFloat(const json &j, const std::string &key, float defaultValue=0.0f)
Safely get a float value from JSON.
bool SaveJsonToFile(const std::string &filepath, const json &j, int indent=4)
Save a JSON object to a file with formatting.
Definition json_helper.h:73
bool IsObject(const json &j, const std::string &key)
Check if a key contains an object.
bool GetBool(const json &j, const std::string &key, bool defaultValue=false)
Safely get a boolean value from JSON.
nlohmann::json json
PinType
Type of connection pin on a node.
constexpr float MAX_ZOOM
constexpr float MIN_ZOOM
constexpr float ZOOM_EPSILON
Represents a single node in a behavior tree.
std::vector< uint32_t > childIds
IDs of child nodes.
uint32_t id
Unique node ID within tree.
BTConditionType conditionType
Condition type (enum)
std::string name
uint32_t decoratorChildId
BTActionType actionType
Action type.
BTNodeType type
Node type.
bool ConnectNodes(uint32_t parentId, uint32_t childId)
Identity component for entity identification.
std::string name
Entity name identifier.
RGBA color value (0-255 range)
Layout information for a single behavior tree node.
Cached debug information for a single entity.
Single entry in the execution log.
float timeAgo
Time since entry (seconds)