@tool extends GraphNode class_name DialogGraphNode signal connections_changed # data is a live reference to the underlying data this GraphNode refers to # changing properties in this script will affect the original data @export var data: Resource var DialogOptions_template = preload("res://addons/madtalk/components/DialogNode_DialogOptions.tscn") var DialogNodeItem_message_template = preload("res://addons/madtalk/components/DialogNodeItem_message.tscn") var DialogNodeItem_condition_template = preload("res://addons/madtalk/components/DialogNodeItem_condition.tscn") var DialogNodeItem_effect_template = preload("res://addons/madtalk/components/DialogNodeItem_effect.tscn") var DialogNodeItem_option_template = preload("res://addons/madtalk/components/DialogNodeItem_option.tscn") @onready var topbar = get_node("TopBar") @onready var add_menu = get_node("TopBar/AddMenu") @onready var dialog_remove = get_node("TopBar/DialogRemove") const COLOR_COND := Color(1.0, 0.5, 0.0, 1.0) const COLOR_OPT := Color(0.0, 1.0, 1.0, 1.0) const COLOR_CONT := Color(1.0, 1.0, 1.0, 1.0) # Stores a port_index -> data resource map var port_data_map := {} # Stores a reference to the main editor var main_editor: Control = null func _ready(): if data: update_from_data() func clear(): var itemlist = get_children() # Index 0 is always topbar, we ignore itemlist.remove_at(0) for item in itemlist: item.hide() item.queue_free() custom_minimum_size.y = 1 size.y = 1 # Clears the port_index -> data resource map port_data_map.clear() func update_from_data(): if data == null: return # === Clear clear() # === Header if data.sequence_id == 0: title = "ID: 0 START" else: title = "ID: %d" % data.sequence_id var i = 1 # Index 0 is topbar so we skip var final_size = 24 # accumulation variable var output_i = 0 # === Items for item in data.items: # Type-specific actions: var new_item = null match item.item_type: DialogNodeItemData.ItemTypes.Message: item.port_index = -1 new_item = DialogNodeItem_message_template.instantiate() clear_slot(i) DialogNodeItemData.ItemTypes.Condition: item.port_index = output_i port_data_map[output_i] = item output_i += 1 new_item = DialogNodeItem_condition_template.instantiate() clear_slot(i) set_slot(i, # index false, # bool enable_left 0, # int type_left Color.BLACK, # Color color_left true, # bool enable_right 0, # int type_right COLOR_COND # Color color_right #, Texture custom_left=null, Texture custom_right=null ) DialogNodeItemData.ItemTypes.Effect: item.port_index = -1 new_item = DialogNodeItem_effect_template.instantiate() clear_slot(i) if new_item: new_item.sequence_node = self add_child(new_item) new_item.remove_requested.connect(_on_Item_remove_requested) new_item.move_up_requested.connect(_on_move_up_requested) new_item.move_down_requested.connect(_on_move_down_requested) new_item.mouse_entered.connect(main_editor._on_item_mouse_entered.bind(new_item)) new_item.mouse_exited.connect(main_editor._on_item_mouse_exited.bind(new_item)) new_item.drag_started.connect(main_editor._on_item_drag_started) new_item.drag_ended.connect(main_editor._on_item_drag_ended) new_item.set_data(item) final_size += new_item.custom_minimum_size.y i += 1 # === Options # If we have options if data.options.size() > 0: # Delete the continue connection as it is invalid data.continue_sequence_id = -1 data.continue_port_index = -1 # Traverse options for option in data.options: option.port_index = output_i port_data_map[output_i] = option output_i += 1 var new_opt = DialogNodeItem_option_template.instantiate() add_child(new_opt) new_opt.text = option.text new_opt.set_conditional(option.is_conditional) set_slot(i, # index false, # bool enable_left 0, # int type_left Color.BLACK, # Color color_left true, # bool enable_right 0, # int type_right COLOR_OPT # Color color_right #, Texture custom_left=null, Texture custom_right=null ) final_size += new_opt.custom_minimum_size.y i += 1 # If no options, continue is the single option else: data.continue_port_index = output_i output_i += 1 var new_opt = DialogNodeItem_option_template.instantiate() add_child(new_opt) new_opt.text = "(Continue)" set_slot(i, # index false, # bool enable_left 0, # int type_left Color.BLACK, # Color color_left true, # bool enable_right 0, # int type_right COLOR_CONT # Color color_right #, Texture custom_left=null, Texture custom_right=null ) final_size += new_opt.custom_minimum_size.y i += 1 # === UPDATING SIZE if is_inside_tree() and get_tree(): # We yield one frame to allow all resizing methods to be called properly # before we apply the final size to the node await get_tree().process_frame # This works fine when the MadTalk editor is exported in a project # (such as in-game dialog editting) size.y = final_size # However, if the MadTalk editor is running as a plugin in the Godot Editor # (and only in this situation), waiting just one frame is not enough, and # the node resizing suffers from random-like errors. If you ever find out # the reason, please don't hesitate to send a PR. # Currently we wait a second frame and then fix the size manually again # A visual glitch can be seen for one frame await get_tree().process_frame final_size = 0 for item in get_children(): final_size += item.size.y size.y = final_size # Given an output port index, returns the corresponding data resource # return value can be either DialogNodeItemData or DialogNodeOptionData func get_data_by_port(port_index: int) -> Resource: # first check if this is a continue port if port_index == data.continue_port_index: return data # otherwise check if invalid port if not port_index in port_data_map: return null # Finally get the data for the port return port_data_map[port_index] # ======================================= # ADD MENU enum AddMenuItems { Message, Condition, Effect } func _on_BtnAdd_pressed(): var cursor_position = get_viewport().get_mouse_position() if get_viewport().gui_embed_subwindows else DisplayServer.mouse_get_position() add_menu.popup(Rect2(cursor_position, Vector2(1,1))) func _on_AddMenu_id_pressed(id): match id: AddMenuItems.Message: var new_data_item = DialogNodeItemData.new() new_data_item.resource_scene_unique_id = Resource.generate_scene_unique_id() new_data_item.item_type = DialogNodeItemData.ItemTypes.Message new_data_item.message_speaker_id = "" new_data_item.message_text = "" data.items.append(new_data_item) update_from_data() AddMenuItems.Condition: var new_data_item = DialogNodeItemData.new() new_data_item.resource_scene_unique_id = Resource.generate_scene_unique_id() new_data_item.item_type = DialogNodeItemData.ItemTypes.Condition new_data_item.condition_type = MTDefs.ConditionTypes.Random new_data_item.condition_values = [50] data.items.append(new_data_item) update_from_data() AddMenuItems.Effect: var new_data_item = DialogNodeItemData.new() new_data_item.resource_scene_unique_id = Resource.generate_scene_unique_id() new_data_item.item_type = DialogNodeItemData.ItemTypes.Effect new_data_item.effect_type = MTDefs.EffectTypes.ChangeSheet new_data_item.effect_values = ["",0] data.items.append(new_data_item) update_from_data() # Adding items always causes connection rebuild since the options come # after all the items (or the "Continue" if there are no options) emit_signal("connections_changed") # ======================================= # OPTION LIST func _on_BtnOptions_pressed(): var dialog_opt = DialogOptions_template.instantiate() # has to be added to topbar and not to self, since all children # added to self will be considered a connection in the node topbar.add_child(dialog_opt) dialog_opt.connect("saved", Callable(self, "_on_DialogOptions_saved")) dialog_opt.open(data) func _on_DialogOptions_saved(source_dialog): update_from_data() # If options changed we have to rebuild connections emit_signal("connections_changed") # ======================================= # MOVE UP/DOWN func _on_move_up_requested(requester): var cur_index = data.items.find(requester.data) if cur_index > 0: var item_data = data.items[cur_index] data.items.remove_at(cur_index) data.items.insert(cur_index-1, item_data) update_from_data() emit_signal("connections_changed") func _on_move_down_requested(requester): var cur_index = data.items.find(requester.data) if cur_index < (data.items.size()-1): var item_data = data.items[cur_index] data.items.remove_at(cur_index) data.items.insert(cur_index+1, item_data) update_from_data() emit_signal("connections_changed") # ======================================= # REMOVE BUTTON var deleting_item = null func _on_Item_remove_requested(requester): deleting_item = requester dialog_remove.popup_centered() func _on_DialogRemove_confirmed(): if deleting_item: data.items.erase(deleting_item.data) deleting_item.data = null update_from_data() emit_signal("connections_changed") # ======================================= # UI events func _on_DialogNode_dragged(from, to): data.position = to