Merge pull request #4 from limbonaut/change-type
Implement "Change Type" action with a popup for tasks in the behavior tree editor
This commit is contained in:
commit
0f902041e0
|
@ -68,31 +68,35 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
|
||||||
_mark_as_dirty(true);
|
_mark_as_dirty(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LimboAIEditor::_add_task_by_class_or_path(String p_class_or_path) {
|
Ref<BTTask> LimboAIEditor::_create_task_by_class_or_path(const String &p_class_or_path) const {
|
||||||
Ref<BTTask> task;
|
Ref<BTTask> ret;
|
||||||
|
|
||||||
if (p_class_or_path.begins_with("res:")) {
|
if (p_class_or_path.begins_with("res:")) {
|
||||||
Ref<Script> s = ResourceLoader::load(p_class_or_path, "Script");
|
Ref<Script> s = ResourceLoader::load(p_class_or_path, "Script");
|
||||||
ERR_FAIL_COND_MSG(s.is_null() || !s->is_valid(), vformat("LimboAI: Failed to instantiate task. Bad script: %s", p_class_or_path));
|
ERR_FAIL_COND_V_MSG(s.is_null() || !s->is_valid(), nullptr, vformat("LimboAI: Failed to instantiate task. Bad script: %s", p_class_or_path));
|
||||||
Variant inst = ClassDB::instantiate(s->get_instance_base_type());
|
Variant inst = ClassDB::instantiate(s->get_instance_base_type());
|
||||||
ERR_FAIL_COND_MSG(inst.is_zero(), vformat("LimboAI: Failed to instantiate base type \"%s\".", s->get_instance_base_type()));
|
ERR_FAIL_COND_V_MSG(inst.is_zero(), nullptr, vformat("LimboAI: Failed to instantiate base type \"%s\".", s->get_instance_base_type()));
|
||||||
|
|
||||||
if (unlikely(!((Object *)inst)->is_class("BTTask"))) {
|
if (unlikely(!((Object *)inst)->is_class("BTTask"))) {
|
||||||
if (!inst.is_ref_counted()) {
|
if (!inst.is_ref_counted()) {
|
||||||
memdelete((Object *)inst);
|
memdelete((Object *)inst);
|
||||||
}
|
}
|
||||||
ERR_PRINT(vformat("LimboAI: Failed to instantiate task. Script is not a BTTask: %s", p_class_or_path));
|
ERR_PRINT(vformat("LimboAI: Failed to instantiate task. Script is not a BTTask: %s", p_class_or_path));
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inst && s.is_valid()) {
|
if (inst && s.is_valid()) {
|
||||||
((Object *)inst)->set_script(s);
|
((Object *)inst)->set_script(s);
|
||||||
task = inst;
|
ret = inst;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
task = ClassDB::instantiate(p_class_or_path);
|
ret = ClassDB::instantiate(p_class_or_path);
|
||||||
}
|
}
|
||||||
_add_task(task);
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LimboAIEditor::_add_task_by_class_or_path(const String &p_class_or_path) {
|
||||||
|
_add_task(_create_task_by_class_or_path(p_class_or_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) {
|
void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) {
|
||||||
|
@ -277,6 +281,7 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
|
||||||
menu->add_icon_item(theme_cache.percent_icon, TTR("Edit Probability"), ACTION_EDIT_PROBABILITY);
|
menu->add_icon_item(theme_cache.percent_icon, TTR("Edit Probability"), ACTION_EDIT_PROBABILITY);
|
||||||
}
|
}
|
||||||
menu->add_icon_shortcut(theme_cache.rename_task_icon, ED_GET_SHORTCUT("limbo_ai/rename_task"), ACTION_RENAME);
|
menu->add_icon_shortcut(theme_cache.rename_task_icon, ED_GET_SHORTCUT("limbo_ai/rename_task"), ACTION_RENAME);
|
||||||
|
menu->add_icon_item(theme_cache.change_type_icon, TTR("Change Type"), ACTION_CHANGE_TYPE);
|
||||||
menu->add_icon_item(theme_cache.edit_script_icon, TTR("Edit Script"), ACTION_EDIT_SCRIPT);
|
menu->add_icon_item(theme_cache.edit_script_icon, TTR("Edit Script"), ACTION_EDIT_SCRIPT);
|
||||||
menu->add_icon_item(theme_cache.open_doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC);
|
menu->add_icon_item(theme_cache.open_doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC);
|
||||||
menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script().is_null());
|
menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script().is_null());
|
||||||
|
@ -317,6 +322,12 @@ void LimboAIEditor::_action_selected(int p_id) {
|
||||||
rename_edit->select_all();
|
rename_edit->select_all();
|
||||||
rename_edit->grab_focus();
|
rename_edit->grab_focus();
|
||||||
} break;
|
} break;
|
||||||
|
case ACTION_CHANGE_TYPE: {
|
||||||
|
change_type_palette->clear_filter();
|
||||||
|
change_type_palette->refresh();
|
||||||
|
Rect2 rect = Rect2(get_global_mouse_position(), Size2(400.0, 600.0) * EDSCALE);
|
||||||
|
change_type_popup->popup(rect);
|
||||||
|
} break;
|
||||||
case ACTION_EDIT_PROBABILITY: {
|
case ACTION_EDIT_PROBABILITY: {
|
||||||
Rect2 rect = task_tree->get_selected_probability_rect();
|
Rect2 rect = task_tree->get_selected_probability_rect();
|
||||||
ERR_FAIL_COND(rect == Rect2());
|
ERR_FAIL_COND(rect == Rect2());
|
||||||
|
@ -670,6 +681,62 @@ void LimboAIEditor::_on_resources_reload(const Vector<String> &p_resources) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LimboAIEditor::_task_type_selected(const String &p_class_or_path) {
|
||||||
|
change_type_popup->hide();
|
||||||
|
|
||||||
|
Ref<BTTask> selected_task = task_tree->get_selected();
|
||||||
|
ERR_FAIL_COND(selected_task.is_null());
|
||||||
|
Ref<BTTask> new_task = _create_task_by_class_or_path(p_class_or_path);
|
||||||
|
ERR_FAIL_COND_MSG(new_task.is_null(), "LimboAI: Unable to construct task.");
|
||||||
|
|
||||||
|
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
|
||||||
|
undo_redo->create_action(TTR("Change BT task type"));
|
||||||
|
undo_redo->add_do_method(this, SNAME("_replace_task"), selected_task, new_task);
|
||||||
|
undo_redo->add_undo_method(this, SNAME("_replace_task"), new_task, selected_task);
|
||||||
|
undo_redo->add_do_method(task_tree, SNAME("update_tree"));
|
||||||
|
undo_redo->add_undo_method(task_tree, SNAME("update_tree"));
|
||||||
|
undo_redo->commit_action();
|
||||||
|
_mark_as_dirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LimboAIEditor::_replace_task(const Ref<BTTask> &p_task, const Ref<BTTask> &p_by_task) {
|
||||||
|
ERR_FAIL_COND(p_task.is_null());
|
||||||
|
ERR_FAIL_COND(p_by_task.is_null());
|
||||||
|
ERR_FAIL_COND(p_by_task->get_child_count() > 0);
|
||||||
|
ERR_FAIL_COND(p_by_task->get_parent().is_valid());
|
||||||
|
|
||||||
|
while (p_task->get_child_count() > 0) {
|
||||||
|
Ref<BTTask> child = p_task->get_child(0);
|
||||||
|
p_task->remove_child_at_index(0);
|
||||||
|
p_by_task->add_child(child);
|
||||||
|
}
|
||||||
|
p_by_task->set_custom_name(p_task->get_custom_name());
|
||||||
|
|
||||||
|
Ref<BTTask> parent = p_task->get_parent();
|
||||||
|
if (parent.is_null()) {
|
||||||
|
// Assuming root task is replaced.
|
||||||
|
ERR_FAIL_COND(task_tree->get_bt().is_null());
|
||||||
|
ERR_FAIL_COND(task_tree->get_bt()->get_root_task() != p_task);
|
||||||
|
task_tree->get_bt()->set_root_task(p_by_task);
|
||||||
|
} else {
|
||||||
|
// Non-root task is replaced.
|
||||||
|
int idx = p_task->get_index();
|
||||||
|
|
||||||
|
double weight = 0.0;
|
||||||
|
Ref<BTProbabilitySelector> probability_selector = parent;
|
||||||
|
if (probability_selector.is_valid()) {
|
||||||
|
weight = probability_selector->get_weight(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent->remove_child(p_task);
|
||||||
|
parent->add_child_at_index(p_by_task, idx);
|
||||||
|
|
||||||
|
if (probability_selector.is_valid()) {
|
||||||
|
probability_selector->set_weight(idx, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LimboAIEditor::_reload_modified() {
|
void LimboAIEditor::_reload_modified() {
|
||||||
for (const String &fn : disk_changed_files) {
|
for (const String &fn : disk_changed_files) {
|
||||||
Ref<Resource> res = ResourceCache::get_ref(fn);
|
Ref<Resource> res = ResourceCache::get_ref(fn);
|
||||||
|
@ -807,16 +874,17 @@ void LimboAIEditor::_update_banners() {
|
||||||
void LimboAIEditor::_update_theme_item_cache() {
|
void LimboAIEditor::_update_theme_item_cache() {
|
||||||
Control::_update_theme_item_cache();
|
Control::_update_theme_item_cache();
|
||||||
|
|
||||||
theme_cache.duplicate_task_icon = get_theme_icon(SNAME("Duplicate"), SNAME("EditorIcons"));
|
theme_cache.duplicate_task_icon = get_editor_theme_icon(SNAME("Duplicate"));
|
||||||
theme_cache.edit_script_icon = get_theme_icon(SNAME("Script"), SNAME("EditorIcons"));
|
theme_cache.edit_script_icon = get_editor_theme_icon(SNAME("Script"));
|
||||||
theme_cache.make_root_icon = get_theme_icon(SNAME("NewRoot"), SNAME("EditorIcons"));
|
theme_cache.make_root_icon = get_editor_theme_icon(SNAME("NewRoot"));
|
||||||
theme_cache.move_task_down_icon = get_theme_icon(SNAME("MoveDown"), SNAME("EditorIcons"));
|
theme_cache.move_task_down_icon = get_editor_theme_icon(SNAME("MoveDown"));
|
||||||
theme_cache.move_task_up_icon = get_theme_icon(SNAME("MoveUp"), SNAME("EditorIcons"));
|
theme_cache.move_task_up_icon = get_editor_theme_icon(SNAME("MoveUp"));
|
||||||
theme_cache.open_debugger_icon = get_theme_icon(SNAME("Debug"), SNAME("EditorIcons"));
|
theme_cache.open_debugger_icon = get_editor_theme_icon(SNAME("Debug"));
|
||||||
theme_cache.open_doc_icon = get_theme_icon(SNAME("Help"), SNAME("EditorIcons"));
|
theme_cache.open_doc_icon = get_editor_theme_icon(SNAME("Help"));
|
||||||
theme_cache.percent_icon = get_theme_icon(SNAME("LimboPercent"), SNAME("EditorIcons"));
|
theme_cache.percent_icon = get_editor_theme_icon(SNAME("LimboPercent"));
|
||||||
theme_cache.remove_task_icon = get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"));
|
theme_cache.remove_task_icon = get_editor_theme_icon(SNAME("Remove"));
|
||||||
theme_cache.rename_task_icon = get_theme_icon(SNAME("Rename"), SNAME("EditorIcons"));
|
theme_cache.rename_task_icon = get_editor_theme_icon(SNAME("Rename"));
|
||||||
|
theme_cache.change_type_icon = get_editor_theme_icon(SNAME("Reload"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void LimboAIEditor::_notification(int p_what) {
|
void LimboAIEditor::_notification(int p_what) {
|
||||||
|
@ -862,6 +930,7 @@ void LimboAIEditor::_bind_methods() {
|
||||||
ClassDB::bind_method(D_METHOD("edit_bt", "p_behavior_tree", "p_force_refresh"), &LimboAIEditor::edit_bt, Variant(false));
|
ClassDB::bind_method(D_METHOD("edit_bt", "p_behavior_tree", "p_force_refresh"), &LimboAIEditor::edit_bt, Variant(false));
|
||||||
ClassDB::bind_method(D_METHOD("_reload_modified"), &LimboAIEditor::_reload_modified);
|
ClassDB::bind_method(D_METHOD("_reload_modified"), &LimboAIEditor::_reload_modified);
|
||||||
ClassDB::bind_method(D_METHOD("_resave_modified"), &LimboAIEditor::_resave_modified);
|
ClassDB::bind_method(D_METHOD("_resave_modified"), &LimboAIEditor::_resave_modified);
|
||||||
|
ClassDB::bind_method(D_METHOD("_replace_task", "p_task", "p_by_task"), &LimboAIEditor::_replace_task);
|
||||||
}
|
}
|
||||||
|
|
||||||
LimboAIEditor::LimboAIEditor() {
|
LimboAIEditor::LimboAIEditor() {
|
||||||
|
@ -1020,6 +1089,25 @@ LimboAIEditor::LimboAIEditor() {
|
||||||
task_palette->hide();
|
task_palette->hide();
|
||||||
hsc->add_child(task_palette);
|
hsc->add_child(task_palette);
|
||||||
|
|
||||||
|
change_type_popup = memnew(PopupPanel);
|
||||||
|
add_child(change_type_popup);
|
||||||
|
{
|
||||||
|
VBoxContainer *change_type_vbox = memnew(VBoxContainer);
|
||||||
|
change_type_popup->add_child(change_type_vbox);
|
||||||
|
|
||||||
|
Label *change_type_title = memnew(Label);
|
||||||
|
change_type_vbox->add_child(change_type_title);
|
||||||
|
change_type_title->set_theme_type_variation("HeaderSmall");
|
||||||
|
change_type_title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
|
||||||
|
change_type_title->set_text(TTR("Choose New Type"));
|
||||||
|
|
||||||
|
change_type_palette = memnew(TaskPalette);
|
||||||
|
change_type_vbox->add_child(change_type_palette);
|
||||||
|
change_type_palette->use_dialog_mode();
|
||||||
|
change_type_palette->connect("task_selected", callable_mp(this, &LimboAIEditor::_task_type_selected));
|
||||||
|
change_type_palette->set_v_size_flags(SIZE_EXPAND_FILL);
|
||||||
|
}
|
||||||
|
|
||||||
banners = memnew(VBoxContainer);
|
banners = memnew(VBoxContainer);
|
||||||
vbox->add_child(banners);
|
vbox->add_child(banners);
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
#include "scene/gui/line_edit.h"
|
#include "scene/gui/line_edit.h"
|
||||||
#include "scene/gui/margin_container.h"
|
#include "scene/gui/margin_container.h"
|
||||||
#include "scene/gui/panel_container.h"
|
#include "scene/gui/panel_container.h"
|
||||||
|
#include "scene/gui/popup.h"
|
||||||
#include "scene/gui/popup_menu.h"
|
#include "scene/gui/popup_menu.h"
|
||||||
#include "scene/gui/split_container.h"
|
#include "scene/gui/split_container.h"
|
||||||
#include "scene/gui/tree.h"
|
#include "scene/gui/tree.h"
|
||||||
|
@ -42,6 +43,7 @@ class LimboAIEditor : public Control {
|
||||||
private:
|
private:
|
||||||
enum Action {
|
enum Action {
|
||||||
ACTION_RENAME,
|
ACTION_RENAME,
|
||||||
|
ACTION_CHANGE_TYPE,
|
||||||
ACTION_EDIT_PROBABILITY,
|
ACTION_EDIT_PROBABILITY,
|
||||||
ACTION_EDIT_SCRIPT,
|
ACTION_EDIT_SCRIPT,
|
||||||
ACTION_OPEN_DOC,
|
ACTION_OPEN_DOC,
|
||||||
|
@ -70,6 +72,7 @@ private:
|
||||||
Ref<Texture2D> percent_icon;
|
Ref<Texture2D> percent_icon;
|
||||||
Ref<Texture2D> remove_task_icon;
|
Ref<Texture2D> remove_task_icon;
|
||||||
Ref<Texture2D> rename_task_icon;
|
Ref<Texture2D> rename_task_icon;
|
||||||
|
Ref<Texture2D> change_type_icon;
|
||||||
} theme_cache;
|
} theme_cache;
|
||||||
|
|
||||||
Vector<Ref<BehaviorTree>> history;
|
Vector<Ref<BehaviorTree>> history;
|
||||||
|
@ -91,6 +94,9 @@ private:
|
||||||
Button *weight_mode;
|
Button *weight_mode;
|
||||||
Button *percent_mode;
|
Button *percent_mode;
|
||||||
|
|
||||||
|
PopupPanel *change_type_popup;
|
||||||
|
TaskPalette *change_type_palette;
|
||||||
|
|
||||||
FileDialog *save_dialog;
|
FileDialog *save_dialog;
|
||||||
FileDialog *load_dialog;
|
FileDialog *load_dialog;
|
||||||
Button *history_back;
|
Button *history_back;
|
||||||
|
@ -110,7 +116,8 @@ private:
|
||||||
HashSet<String> disk_changed_files;
|
HashSet<String> disk_changed_files;
|
||||||
|
|
||||||
void _add_task(const Ref<BTTask> &p_task);
|
void _add_task(const Ref<BTTask> &p_task);
|
||||||
void _add_task_by_class_or_path(String p_class_or_path);
|
Ref<BTTask> _create_task_by_class_or_path(const String &p_class_or_path) const;
|
||||||
|
void _add_task_by_class_or_path(const String &p_class_or_path);
|
||||||
void _remove_task(const Ref<BTTask> &p_task);
|
void _remove_task(const Ref<BTTask> &p_task);
|
||||||
_FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); }
|
_FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); }
|
||||||
void _update_header() const;
|
void _update_header() const;
|
||||||
|
@ -146,6 +153,9 @@ private:
|
||||||
void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type);
|
void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type);
|
||||||
void _on_resources_reload(const Vector<String> &p_resources);
|
void _on_resources_reload(const Vector<String> &p_resources);
|
||||||
|
|
||||||
|
void _task_type_selected(const String &p_class_or_path);
|
||||||
|
void _replace_task(const Ref<BTTask> &p_task, const Ref<BTTask> &p_by_task);
|
||||||
|
|
||||||
virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
|
virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -179,6 +179,10 @@ void TaskPalette::_on_task_button_pressed(const String &p_task) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskPalette::_on_task_button_rmb(const String &p_task) {
|
void TaskPalette::_on_task_button_rmb(const String &p_task) {
|
||||||
|
if (dialog_mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ERR_FAIL_COND(p_task.is_empty());
|
ERR_FAIL_COND(p_task.is_empty());
|
||||||
|
|
||||||
context_task = p_task;
|
context_task = p_task;
|
||||||
|
@ -415,14 +419,20 @@ void TaskPalette::refresh() {
|
||||||
sec->connect(SNAME("task_button_pressed"), callable_mp(this, &TaskPalette::_on_task_button_pressed));
|
sec->connect(SNAME("task_button_pressed"), callable_mp(this, &TaskPalette::_on_task_button_pressed));
|
||||||
sec->connect(SNAME("task_button_rmb"), callable_mp(this, &TaskPalette::_on_task_button_rmb));
|
sec->connect(SNAME("task_button_rmb"), callable_mp(this, &TaskPalette::_on_task_button_rmb));
|
||||||
sections->add_child(sec);
|
sections->add_child(sec);
|
||||||
sec->set_collapsed(collapsed_sections.has(cat));
|
sec->set_collapsed(!dialog_mode && collapsed_sections.has(cat));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filter_edit->get_text().is_empty()) {
|
if (!dialog_mode && !filter_edit->get_text().is_empty()) {
|
||||||
_apply_filter(filter_edit->get_text());
|
_apply_filter(filter_edit->get_text());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TaskPalette::use_dialog_mode() {
|
||||||
|
tool_filters->hide();
|
||||||
|
tool_refresh->hide();
|
||||||
|
dialog_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
void TaskPalette::_update_theme_item_cache() {
|
void TaskPalette::_update_theme_item_cache() {
|
||||||
PanelContainer::_update_theme_item_cache();
|
PanelContainer::_update_theme_item_cache();
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ private:
|
||||||
VBoxContainer *category_list;
|
VBoxContainer *category_list;
|
||||||
|
|
||||||
String context_task;
|
String context_task;
|
||||||
|
bool dialog_mode = false;
|
||||||
|
|
||||||
void _menu_action_selected(int p_id);
|
void _menu_action_selected(int p_id);
|
||||||
void _on_task_button_pressed(const String &p_task);
|
void _on_task_button_pressed(const String &p_task);
|
||||||
|
@ -149,6 +150,8 @@ protected:
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void refresh();
|
void refresh();
|
||||||
|
void use_dialog_mode();
|
||||||
|
void clear_filter() { filter_edit->set_text(""); }
|
||||||
|
|
||||||
TaskPalette();
|
TaskPalette();
|
||||||
~TaskPalette();
|
~TaskPalette();
|
||||||
|
|
Loading…
Reference in New Issue