--- /dev/null
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
--- /dev/null
+# Godot 4+ specific ignores
+.godot/
+/android/
+
+# my ignores
+*.import
+*.swp
--- /dev/null
+class_name DialogicGameHandler
+extends Node
+
+## Class that is used as the Dialogic autoload.
+
+## Autoload script that allows you to interact with all of Dialogic's systems:[br]
+## - Holds all important information about the current state of Dialogic.[br]
+## - Provides access to all the subsystems.[br]
+## - Has methods to start/end timelines.[br]
+
+
+## States indicating different phases of dialog.
+enum States {
+ IDLE, ## Dialogic is awaiting input to advance.
+ REVEALING_TEXT, ## Dialogic is currently revealing text.
+ ANIMATING, ## Some animation is happening.
+ AWAITING_CHOICE, ## Dialogic awaits the selection of a choice
+ WAITING ## Dialogic is currently awaiting something.
+ }
+
+## Flags indicating what to clear when calling [method clear].
+enum ClearFlags {
+ FULL_CLEAR = 0, ## Clears all subsystems
+ KEEP_VARIABLES = 1, ## Clears all subsystems and info except for variables
+ TIMELINE_INFO_ONLY = 2 ## Doesn't clear subsystems but current timeline and index
+ }
+
+## Reference to the currently executed timeline.
+var current_timeline: DialogicTimeline = null
+## Copy of the [member current_timeline]'s events.
+var current_timeline_events: Array = []
+
+## Index of the event the timeline handling is currently at.
+var current_event_idx: int = 0
+## Contains all information that subsystems consider relevant for
+## the current situation
+var current_state_info: Dictionary = {}
+
+## Current state (see [member States] enum).
+var current_state := States.IDLE:
+ get:
+ return current_state
+
+ set(new_state):
+ current_state = new_state
+ state_changed.emit(new_state)
+
+## Emitted when [member current_state] change.
+signal state_changed(new_state:States)
+
+## When `true`, many dialogic processes won't continue until it's `false` again.
+var paused := false:
+ set(value):
+ paused = value
+
+ if paused:
+
+ for subsystem in get_children():
+
+ if subsystem is DialogicSubsystem:
+ (subsystem as DialogicSubsystem).pause()
+
+ dialogic_paused.emit()
+
+ else:
+ for subsystem in get_children():
+
+ if subsystem is DialogicSubsystem:
+ (subsystem as DialogicSubsystem).resume()
+
+ dialogic_resumed.emit()
+
+## Emitted when [member paused] changes to `true`.
+signal dialogic_paused
+## Emitted when [member paused] changes to `false`.
+signal dialogic_resumed
+
+
+## Emitted when the timeline ends.
+## This can be a timeline ending or [method end_timeline] being called.
+signal timeline_ended
+## Emitted when a timeline starts by calling either [method start]
+## or [method start_timeline].
+signal timeline_started
+## Emitted when an event starts being executed.
+## The event may not have finished executing yet.
+signal event_handled(resource: DialogicEvent)
+
+## Emitted when a [class SignalEvent] event was reached.
+signal signal_event(argument: Variant)
+## Emitted when a signal event gets fired from a [class TextEvent] event.
+signal text_signal(argument: String)
+
+
+# Careful, this section is repopulated automatically at certain moments.
+#region SUBSYSTEMS
+
+var Animations := preload("res://addons/dialogic/Modules/Core/subsystem_animation.gd").new():
+ get: return get_subsystem("Animations")
+
+var Audio := preload("res://addons/dialogic/Modules/Audio/subsystem_audio.gd").new():
+ get: return get_subsystem("Audio")
+
+var Backgrounds := preload("res://addons/dialogic/Modules/Background/subsystem_backgrounds.gd").new():
+ get: return get_subsystem("Backgrounds")
+
+var Choices := preload("res://addons/dialogic/Modules/Choice/subsystem_choices.gd").new():
+ get: return get_subsystem("Choices")
+
+var Expressions := preload("res://addons/dialogic/Modules/Core/subsystem_expression.gd").new():
+ get: return get_subsystem("Expressions")
+
+
+var Glossary := preload("res://addons/dialogic/Modules/Glossary/subsystem_glossary.gd").new():
+ get: return get_subsystem("Glossary")
+
+var History := preload("res://addons/dialogic/Modules/History/subsystem_history.gd").new():
+ get: return get_subsystem("History")
+
+var Inputs := preload("res://addons/dialogic/Modules/Core/subsystem_input.gd").new():
+ get: return get_subsystem("Inputs")
+
+var Jump := preload("res://addons/dialogic/Modules/Jump/subsystem_jump.gd").new():
+ get: return get_subsystem("Jump")
+
+var PortraitContainers := preload("res://addons/dialogic/Modules/Character/subsystem_containers.gd").new():
+ get: return get_subsystem("PortraitContainers")
+
+var Portraits := preload("res://addons/dialogic/Modules/Character/subsystem_portraits.gd").new():
+ get: return get_subsystem("Portraits")
+
+var Save := preload("res://addons/dialogic/Modules/Save/subsystem_save.gd").new():
+ get: return get_subsystem("Save")
+
+var Settings := preload("res://addons/dialogic/Modules/Settings/subsystem_settings.gd").new():
+ get: return get_subsystem("Settings")
+
+var Styles := preload("res://addons/dialogic/Modules/Style/subsystem_styles.gd").new():
+ get: return get_subsystem("Styles")
+
+var Text := preload("res://addons/dialogic/Modules/Text/subsystem_text.gd").new():
+ get: return get_subsystem("Text")
+
+var TextInput := preload("res://addons/dialogic/Modules/TextInput/subsystem_text_input.gd").new():
+ get: return get_subsystem("TextInput")
+
+var VAR := preload("res://addons/dialogic/Modules/Variable/subsystem_variables.gd").new():
+ get: return get_subsystem("VAR")
+
+var Voice := preload("res://addons/dialogic/Modules/Voice/subsystem_voice.gd").new():
+ get: return get_subsystem("Voice")
+
+#endregion
+
+
+## Autoloads are added first, so this happens REALLY early on game startup.
+func _ready() -> void:
+ _collect_subsystems()
+
+ clear()
+
+
+#region TIMELINE & EVENT HANDLING
+################################################################################
+
+## Method to start a timeline AND ensure that a layout scene is present.
+## For argument info, checkout [method start_timeline].
+## -> returns the layout node
+func start(timeline:Variant, label:Variant="") -> Node:
+ # If we don't have a style subsystem, default to just start_timeline()
+ if not has_subsystem('Styles'):
+ printerr("[Dialogic] You called Dialogic.start() but the Styles subsystem is missing!")
+ clear(ClearFlags.KEEP_VARIABLES)
+ start_timeline(timeline, label)
+ return null
+
+ # Otherwise make sure there is a style active.
+ var scene: Node = null
+ if !self.Styles.has_active_layout_node():
+ scene = self.Styles.load_style()
+ else:
+ scene = self.Styles.get_layout_node()
+ scene.show()
+
+ if not scene.is_node_ready():
+ scene.ready.connect(clear.bind(ClearFlags.KEEP_VARIABLES))
+ scene.ready.connect(start_timeline.bind(timeline, label))
+ else:
+ start_timeline(timeline, label)
+
+ return scene
+
+
+## Method to start a timeline without adding a layout scene.
+## @timeline can be either a loaded timeline resource or a path to a timeline file.
+## @label_or_idx can be a label (string) or index (int) to skip to immediatly.
+func start_timeline(timeline:Variant, label_or_idx:Variant = "") -> void:
+ # load the resource if only the path is given
+ if typeof(timeline) == TYPE_STRING:
+ #check the lookup table if it's not a full file name
+ if (timeline as String).contains("res://"):
+ timeline = load((timeline as String))
+ else:
+ timeline = DialogicResourceUtil.get_timeline_resource((timeline as String))
+
+ if timeline == null:
+ printerr("[Dialogic] There was an error loading this timeline. Check the filename, and the timeline for errors")
+ return
+
+ (timeline as DialogicTimeline).process()
+
+ current_timeline = timeline
+ current_timeline_events = current_timeline.events
+ for event in current_timeline_events:
+ event.dialogic = self
+ current_event_idx = -1
+
+ if typeof(label_or_idx) == TYPE_STRING:
+ if label_or_idx:
+ if has_subsystem('Jump'):
+ Jump.jump_to_label((label_or_idx as String))
+ elif typeof(label_or_idx) == TYPE_INT:
+ if label_or_idx >-1:
+ current_event_idx = label_or_idx -1
+
+ timeline_started.emit()
+ handle_next_event()
+
+
+## Preloader function, prepares a timeline and returns an object to hold for later
+## [param timeline_resource] can be either a path (string) or a loaded timeline (resource)
+func preload_timeline(timeline_resource:Variant) -> Variant:
+ # I think ideally this should be on a new thread, will test
+ if typeof(timeline_resource) == TYPE_STRING:
+ timeline_resource = load((timeline_resource as String))
+ if timeline_resource == null:
+ printerr("[Dialogic] There was an error preloading this timeline. Check the filename, and the timeline for errors")
+ return null
+
+ (timeline_resource as DialogicTimeline).process()
+
+ return timeline_resource
+
+
+## Clears and stops the current timeline.
+func end_timeline() -> void:
+ await clear(ClearFlags.TIMELINE_INFO_ONLY)
+ _on_timeline_ended()
+ timeline_ended.emit()
+
+
+## Handles the next event.
+func handle_next_event(_ignore_argument: Variant = "") -> void:
+ handle_event(current_event_idx+1)
+
+
+## Handles the event at the given index [param event_index].
+## You can call this manually, but if another event is still executing, it might have unexpected results.
+func handle_event(event_index:int) -> void:
+ if not current_timeline:
+ return
+
+ _cleanup_previous_event()
+
+ if paused:
+ await dialogic_resumed
+
+ if event_index >= len(current_timeline_events):
+ end_timeline()
+ return
+
+ #actually process the event now, since we didnt earlier at runtime
+ #this needs to happen before we create the copy DialogicEvent variable, so it doesn't throw an error if not ready
+ if current_timeline_events[event_index].event_node_ready == false:
+ current_timeline_events[event_index]._load_from_string(current_timeline_events[event_index].event_node_as_text)
+
+ current_event_idx = event_index
+
+ if not current_timeline_events[event_index].event_finished.is_connected(handle_next_event):
+ current_timeline_events[event_index].event_finished.connect(handle_next_event)
+
+ set_meta('previous_event', current_timeline_events[event_index])
+
+ current_timeline_events[event_index].execute(self)
+ event_handled.emit(current_timeline_events[event_index])
+
+
+## Resets Dialogic's state fully or partially.
+## By using the clear flags from the [member ClearFlags] enum you can specify
+## what info should be kept.
+## For example, at timeline end usually it doesn't clear node or subsystem info.
+func clear(clear_flags := ClearFlags.FULL_CLEAR) -> void:
+ _cleanup_previous_event()
+
+ if !clear_flags & ClearFlags.TIMELINE_INFO_ONLY:
+ for subsystem in get_children():
+ if subsystem is DialogicSubsystem:
+ (subsystem as DialogicSubsystem).clear_game_state(clear_flags)
+
+ var timeline := current_timeline
+
+ current_timeline = null
+ current_event_idx = -1
+ current_timeline_events = []
+ current_state = States.IDLE
+
+ # Resetting variables
+ if timeline:
+ await timeline.clean()
+
+
+## Cleanup after previous event (if any).
+func _cleanup_previous_event():
+ if has_meta('previous_event') and get_meta('previous_event') is DialogicEvent:
+ var event := get_meta('previous_event') as DialogicEvent
+ if event.event_finished.is_connected(handle_next_event):
+ event.event_finished.disconnect(handle_next_event)
+ event._clear_state()
+ remove_meta("previous_event")
+
+#endregion
+
+
+#region SAVING & LOADING
+################################################################################
+
+## Returns a dictionary containing all necessary information to later recreate the same state with load_full_state.
+## The [subsystem Save] subsystem might be more useful for you.
+## However, this can be used to integrate the info into your own save system.
+func get_full_state() -> Dictionary:
+ if current_timeline:
+ current_state_info['current_event_idx'] = current_event_idx
+ current_state_info['current_timeline'] = current_timeline.resource_path
+ else:
+ current_state_info['current_event_idx'] = -1
+ current_state_info['current_timeline'] = null
+
+ for subsystem in get_children():
+ (subsystem as DialogicSubsystem).save_game_state()
+
+ return current_state_info.duplicate(true)
+
+
+## This method tries to load the state from the given [param state_info].
+## Will automatically start a timeline and add a layout if a timeline was running when
+## the dictionary was retrieved with [method get_full_state].
+func load_full_state(state_info:Dictionary) -> void:
+ clear()
+ current_state_info = state_info
+ ## The Style subsystem needs to run first for others to load correctly.
+ var scene: Node = null
+ if has_subsystem('Styles'):
+ get_subsystem('Styles').load_game_state()
+ scene = self.Styles.get_layout_node()
+
+ var load_subsystems := func() -> void:
+ for subsystem in get_children():
+ if subsystem.name == 'Styles':
+ continue
+ (subsystem as DialogicSubsystem).load_game_state()
+
+ if null != scene and not scene.is_node_ready():
+ scene.ready.connect(load_subsystems)
+ else:
+ await get_tree().process_frame
+ load_subsystems.call()
+
+ if current_state_info.get('current_timeline', null):
+ start_timeline(current_state_info.current_timeline, current_state_info.get('current_event_idx', 0))
+ else:
+ end_timeline.call_deferred()
+#endregion
+
+
+#region SUB-SYTSEMS
+################################################################################
+
+func _collect_subsystems() -> void:
+ var subsystem_nodes := [] as Array[DialogicSubsystem]
+ for indexer in DialogicUtil.get_indexers():
+ for subsystem in indexer._get_subsystems():
+ var subsystem_node := add_subsystem(str(subsystem.name), str(subsystem.script))
+ subsystem_nodes.push_back(subsystem_node)
+
+ for subsystem in subsystem_nodes:
+ subsystem.post_install()
+
+
+## Returns `true` if a subystem with the given [param subsystem_name] exists.
+func has_subsystem(subsystem_name:String) -> bool:
+ return has_node(subsystem_name)
+
+
+## Returns the subsystem node of the given [param subsystem_name] or null if it doesn't exist.
+func get_subsystem(subsystem_name:String) -> DialogicSubsystem:
+ return get_node(subsystem_name)
+
+
+## Adds a subsystem node with the given [param subsystem_name] and [param script_path].
+func add_subsystem(subsystem_name:String, script_path:String) -> DialogicSubsystem:
+ var node: Node = Node.new()
+ node.name = subsystem_name
+ node.set_script(load(script_path))
+ node = node as DialogicSubsystem
+ node.dialogic = self
+ add_child(node)
+ return node
+
+
+#endregion
+
+
+#region HELPERS
+################################################################################
+
+## This handles the `Layout End Behaviour` setting that can be changed in the Dialogic settings.
+func _on_timeline_ended() -> void:
+ if self.Styles.has_active_layout_node() and self.Styles.get_layout_node().is_inside_tree():
+ match ProjectSettings.get_setting('dialogic/layout/end_behaviour', 0):
+ 0:
+ self.Styles.get_layout_node().get_parent().remove_child(self.Styles.get_layout_node())
+ self.Styles.get_layout_node().queue_free()
+ 1:
+ @warning_ignore("unsafe_method_access")
+ self.Styles.get_layout_node().hide()
+
+
+func print_debug_moment() -> void:
+ if not current_timeline:
+ return
+
+ printerr("\tAt event ", current_event_idx+1, " (",current_timeline_events[current_event_idx].event_name, ' Event) in timeline "', DialogicResourceUtil.get_unique_identifier(current_timeline.resource_path), '" (',current_timeline.resource_path,').')
+ print("\n")
+#endregion
--- /dev/null
+@tool
+class_name DialogicResourceUtil
+
+static var label_cache := {}
+static var event_cache: Array[DialogicEvent] = []
+
+static var special_resources := {}
+
+
+static func update() -> void:
+ update_directory('.dch')
+ update_directory('.dtl')
+ update_label_cache()
+
+
+#region RESOURCE DIRECTORIES
+################################################################################
+
+static func get_directory(extension:String) -> Dictionary:
+ extension = extension.trim_prefix('.')
+ if Engine.has_meta(extension+'_directory'):
+ return Engine.get_meta(extension+'_directory', {})
+
+ var directory: Dictionary = ProjectSettings.get_setting("dialogic/directories/"+extension+'_directory', {})
+ Engine.set_meta(extension+'_directory', directory)
+ return directory
+
+
+static func set_directory(extension:String, directory:Dictionary) -> void:
+ extension = extension.trim_prefix('.')
+ if Engine.is_editor_hint():
+ ProjectSettings.set_setting("dialogic/directories/"+extension+'_directory', directory)
+ ProjectSettings.save()
+ Engine.set_meta(extension+'_directory', directory)
+
+
+static func update_directory(extension:String) -> void:
+ var directory := get_directory(extension)
+
+ for resource in list_resources_of_type(extension):
+ if not resource in directory.values():
+ directory = add_resource_to_directory(resource, directory)
+
+ var keys_to_remove := []
+ for key in directory:
+ if not ResourceLoader.exists(directory[key]):
+ keys_to_remove.append(key)
+ for key in keys_to_remove:
+ directory.erase(key)
+
+ set_directory(extension, directory)
+
+
+static func add_resource_to_directory(file_path:String, directory:Dictionary) -> Dictionary:
+ var suggested_name := file_path.get_file().trim_suffix("."+file_path.get_extension())
+ while suggested_name in directory:
+ suggested_name = file_path.trim_suffix("/"+suggested_name+"."+file_path.get_extension()).get_file().path_join(suggested_name)
+ directory[suggested_name] = file_path
+ return directory
+
+
+## Returns the unique identifier for the given resource path.
+## Returns an empty string if no identifier was found.
+static func get_unique_identifier(file_path:String) -> String:
+ var identifier: String = get_directory(file_path.get_extension()).find_key(file_path)
+ if typeof(identifier) == TYPE_STRING:
+ return identifier
+ return ""
+
+
+## Returns the resource associated with the given unique identifier.
+## The expected extension is needed to use the right directory.
+static func get_resource_from_identifier(identifier:String, extension:String) -> Resource:
+ var path: String = get_directory(extension).get(identifier, '')
+ if ResourceLoader.exists(path):
+ return load(path)
+ return null
+
+
+static func change_unique_identifier(file_path:String, new_identifier:String) -> void:
+ var directory := get_directory(file_path.get_extension())
+ var key: String = directory.find_key(file_path)
+ while key != null:
+ if key == new_identifier:
+ break
+ directory.erase(key)
+ directory[new_identifier] = file_path
+ key = directory.find_key(file_path)
+ set_directory(file_path.get_extension(), directory)
+
+
+static func change_resource_path(old_path:String, new_path:String) -> void:
+ var directory := get_directory(new_path.get_extension())
+ var key: String = directory.find_key(old_path)
+ while key != null:
+ directory[key] = new_path
+ key = directory.find_key(old_path)
+ set_directory(new_path.get_extension(), directory)
+
+
+static func remove_resource(file_path:String) -> void:
+ var directory := get_directory(file_path.get_extension())
+ var key: String = directory.find_key(file_path)
+ while key != null:
+ directory.erase(key)
+ key = directory.find_key(file_path)
+ set_directory(file_path.get_extension(), directory)
+
+
+static func is_identifier_unused(extension:String, identifier:String) -> bool:
+ return not identifier in get_directory(extension)
+
+#endregion
+
+#region LABEL CACHE
+################################################################################
+# The label cache is only for the editor so we don't have to scan all timelines
+# whenever we want to suggest labels. This has no use in game and is not always perfect.
+
+static func get_label_cache() -> Dictionary:
+ if not label_cache.is_empty():
+ return label_cache
+
+ label_cache = DialogicUtil.get_editor_setting('label_ref', {})
+ return label_cache
+
+
+static func set_label_cache(cache:Dictionary) -> void:
+ label_cache = cache
+
+
+static func update_label_cache() -> void:
+ var cache := get_label_cache()
+ var timelines := get_timeline_directory().values()
+ for timeline in cache:
+ if !timeline in timelines:
+ cache.erase(timeline)
+ set_label_cache(cache)
+
+#endregion
+
+#region EVENT CACHE
+################################################################################
+
+## Dialogic keeps a list that has each event once. This allows retrieval of that list.
+static func get_event_cache() -> Array:
+ if not event_cache.is_empty():
+ return event_cache
+
+ event_cache = update_event_cache()
+ return event_cache
+
+
+static func update_event_cache() -> Array:
+ event_cache = []
+ for indexer in DialogicUtil.get_indexers():
+ # build event cache
+ for event in indexer._get_events():
+ if not ResourceLoader.exists(event):
+ continue
+ if not 'event_end_branch.gd' in event and not 'event_text.gd' in event:
+ event_cache.append(load(event).new())
+
+ # Events are checked in order while testing them. EndBranch needs to be first, Text needs to be last
+ event_cache.push_front(DialogicEndBranchEvent.new())
+ event_cache.push_back(DialogicTextEvent.new())
+
+ return event_cache
+
+#endregion
+
+#region SPECIAL RESOURCES
+################################################################################
+
+static func update_special_resources() -> void:
+ special_resources.clear()
+ for indexer in DialogicUtil.get_indexers():
+ var additions := indexer._get_special_resources()
+ for resource_type in additions:
+ if not resource_type in special_resources:
+ special_resources[resource_type] = {}
+ special_resources[resource_type].merge(additions[resource_type])
+
+
+static func list_special_resources(type:String, filter := {}) -> Dictionary:
+ if special_resources.is_empty():
+ update_special_resources()
+ if type in special_resources:
+ if filter.is_empty():
+ return special_resources[type]
+ else:
+ var results := {}
+ for i in special_resources[type]:
+ if match_resource_filter(special_resources[type][i], filter):
+ results[i] = special_resources[type][i]
+ return results
+ return {}
+
+
+static func match_resource_filter(dict:Dictionary, filter:Dictionary) -> bool:
+ for i in filter:
+ if not i in dict:
+ return false
+ if typeof(filter[i]) == TYPE_ARRAY:
+ if not dict[i] in filter[i]:
+ return false
+ else:
+ if not dict[i] == filter[i]:
+ return false
+ return true
+
+
+static func guess_special_resource(type: String, string: String, default := {}, filter := {}, ignores:PackedStringArray=[]) -> Dictionary:
+ if string.is_empty():
+ return default
+
+ if special_resources.is_empty():
+ update_special_resources()
+ var resources := list_special_resources(type, filter)
+ if resources.is_empty():
+ printerr("[Dialogic] No ", type, "s found, but attempted to use one.")
+ return default
+
+ if string.begins_with('res://'):
+ for i in resources.values():
+ if i.path == string:
+ return i
+ printerr("[Dialogic] Unable to find ", type, " at path '", string, "'.")
+ return default
+
+ string = string.to_lower()
+
+ if string in resources:
+ return resources[string]
+
+ if not ignores.is_empty():
+ var regex := RegEx.create_from_string(r" ?\b(" + "|".join(ignores) + r")\b")
+ for name in resources:
+ if regex.sub(name, "") == regex.sub(string, ""):
+ return resources[name]
+
+ ## As a last effort check against the unfiltered list
+ if string in special_resources[type]:
+ push_warning("[Dialogic] Using ", type, " '", string,"' when not supposed to.")
+ return special_resources[type][string]
+
+ printerr("[Dialogic] Unable to identify ", type, " based on string '", string, "'.")
+ return default
+
+#endregion
+
+#region HELPERS
+################################################################################
+
+static func get_character_directory() -> Dictionary:
+ return get_directory('dch')
+
+
+static func get_timeline_directory() -> Dictionary:
+ return get_directory('dtl')
+
+
+static func get_timeline_resource(timeline_identifier:String) -> DialogicTimeline:
+ return get_resource_from_identifier(timeline_identifier, 'dtl')
+
+
+static func get_character_resource(character_identifier:String) -> DialogicCharacter:
+ return get_resource_from_identifier(character_identifier, 'dch')
+
+
+static func list_resources_of_type(extension:String) -> Array:
+ var all_resources := scan_folder('res://', extension)
+ return all_resources
+
+
+static func scan_folder(path:String, extension:String) -> Array:
+ var list: Array = []
+ if DirAccess.dir_exists_absolute(path):
+ var dir := DirAccess.open(path)
+ dir.list_dir_begin()
+ var file_name := dir.get_next()
+ while file_name != "":
+ if dir.current_is_dir() and not file_name.begins_with("."):
+ list += scan_folder(path.path_join(file_name), extension)
+ else:
+ if file_name.ends_with(extension):
+ list.append(path.path_join(file_name))
+ file_name = dir.get_next()
+ return list
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicUtil
+
+## Script that container helper methods for both editor and game execution.
+## Used whenever the same thing is needed in different parts of the plugin.
+
+#region EDITOR
+
+## This method should be used instead of EditorInterface.get_editor_scale(), because if you use that
+## it will run perfectly fine from the editor, but crash when the game is exported.
+static func get_editor_scale() -> float:
+ return get_dialogic_plugin().get_editor_interface().get_editor_scale()
+
+
+## Although this does in fact always return a EditorPlugin node,
+## that class is apparently not present in export and referencing it here creates a crash.
+static func get_dialogic_plugin() -> Node:
+ for child in Engine.get_main_loop().get_root().get_children():
+ if child.get_class() == "EditorNode":
+ return child.get_node('DialogicPlugin')
+ return null
+
+#endregion
+
+
+## Returns the autoload when in-game.
+static func autoload() -> DialogicGameHandler:
+ if Engine.is_editor_hint():
+ return null
+ if not Engine.get_main_loop().root.has_node("Dialogic"):
+ return null
+ return Engine.get_main_loop().root.get_node("Dialogic")
+
+
+#region FILE SYSTEM
+################################################################################
+static func listdir(path: String, files_only:= true, _throw_error:= true, full_file_path:= false, include_imports := false) -> Array:
+ var files: Array = []
+ if path.is_empty(): path = "res://"
+ if DirAccess.dir_exists_absolute(path):
+ var dir := DirAccess.open(path)
+ dir.list_dir_begin()
+ var file_name := dir.get_next()
+ while file_name != "":
+ if not file_name.begins_with("."):
+ if files_only:
+ if not dir.current_is_dir() and (not file_name.ends_with('.import') or include_imports):
+ if full_file_path:
+ files.append(path.path_join(file_name))
+ else:
+ files.append(file_name)
+ else:
+ if full_file_path:
+ files.append(path.path_join(file_name))
+ else:
+ files.append(file_name)
+ file_name = dir.get_next()
+ dir.list_dir_end()
+ return files
+
+
+static func get_module_path(name:String, builtin:=true) -> String:
+ if builtin:
+ return "res://addons/dialogic/Modules".path_join(name)
+ else:
+ return ProjectSettings.get_setting('dialogic/extensions_folder', 'res://addons/dialogic_additions').path_join(name)
+
+
+## This is a private and editor-only function.
+##
+## Populates the [class DialogicGameHandler] with new custom subsystems by
+## directly manipulating the file's content and then importing the file.
+static func _update_autoload_subsystem_access() -> void:
+ if not Engine.is_editor_hint():
+ printerr("[Dialogic] This function is only available in the editor.")
+ return
+
+ var script: Script = load("res://addons/dialogic/Core/DialogicGameHandler.gd")
+ var new_subsystem_access_list := "#region SUBSYSTEMS\n"
+ var subsystems_sorted := []
+
+ for indexer: DialogicIndexer in get_indexers(true, true):
+
+ for subsystem: Dictionary in indexer._get_subsystems().duplicate(true):
+ subsystems_sorted.append(subsystem)
+
+ subsystems_sorted.sort_custom(func (a: Dictionary, b: Dictionary) -> bool:
+ return a.name < b.name
+ )
+
+ for subsystem: Dictionary in subsystems_sorted:
+ new_subsystem_access_list += '\nvar {name} := preload("{script}").new():\n\tget: return get_subsystem("{name}")\n'.format(subsystem)
+
+ new_subsystem_access_list += "\n#endregion"
+ script.source_code = RegEx.create_from_string(r"#region SUBSYSTEMS\n#*\n((?!#endregion)(.*\n))*#endregion").sub(script.source_code, new_subsystem_access_list)
+ ResourceSaver.save(script)
+ Engine.get_singleton("EditorInterface").get_resource_filesystem().reimport_files(["res://addons/dialogic/Core/DialogicGameHandler.gd"])
+
+
+static func get_indexers(include_custom := true, force_reload := false) -> Array[DialogicIndexer]:
+ if Engine.get_main_loop().has_meta('dialogic_indexers') and !force_reload:
+ return Engine.get_main_loop().get_meta('dialogic_indexers')
+
+ var indexers: Array[DialogicIndexer] = []
+
+ for file in listdir(DialogicUtil.get_module_path(''), false):
+ var possible_script: String = DialogicUtil.get_module_path(file).path_join("index.gd")
+ if ResourceLoader.exists(possible_script):
+ indexers.append(load(possible_script).new())
+
+ if include_custom:
+ var extensions_folder: String = ProjectSettings.get_setting('dialogic/extensions_folder', "res://addons/dialogic_additions/")
+ for file in listdir(extensions_folder, false, false):
+ var possible_script: String = extensions_folder.path_join(file + "/index.gd")
+ if ResourceLoader.exists(possible_script):
+ indexers.append(load(possible_script).new())
+
+ Engine.get_main_loop().set_meta('dialogic_indexers', indexers)
+ return indexers
+
+
+
+## Turns a [param file_path] from `some_file.png` to `Some File`.
+static func pretty_name(file_path: String) -> String:
+ var _name := file_path.get_file().trim_suffix("." + file_path.get_extension())
+ _name = _name.replace('_', ' ')
+ _name = _name.capitalize()
+
+ return _name
+
+#endregion
+
+
+#region EDITOR SETTINGS & COLORS
+################################################################################
+
+static func set_editor_setting(setting:String, value:Variant) -> void:
+ var cfg := ConfigFile.new()
+ if FileAccess.file_exists('user://dialogic/editor_settings.cfg'):
+ cfg.load('user://dialogic/editor_settings.cfg')
+
+ cfg.set_value('DES', setting, value)
+
+ if !DirAccess.dir_exists_absolute('user://dialogic'):
+ DirAccess.make_dir_absolute('user://dialogic')
+ cfg.save('user://dialogic/editor_settings.cfg')
+
+
+static func get_editor_setting(setting:String, default:Variant=null) -> Variant:
+ var cfg := ConfigFile.new()
+ if !FileAccess.file_exists('user://dialogic/editor_settings.cfg'):
+ return default
+
+ if !cfg.load('user://dialogic/editor_settings.cfg') == OK:
+ return default
+
+ return cfg.get_value('DES', setting, default)
+
+
+static func get_color_palette(default:bool = false) -> Dictionary:
+ var defaults := {
+ 'Color1': Color('#3b8bf2'), # Blue
+ 'Color2': Color('#00b15f'), # Green
+ 'Color3': Color('#e868e2'), # Pink
+ 'Color4': Color('#9468e8'), # Purple
+ 'Color5': Color('#574fb0'), # DarkPurple
+ 'Color6': Color('#1fa3a3'), # Aquamarine
+ 'Color7': Color('#fa952a'), # Orange
+ 'Color8': Color('#de5c5c'), # Red
+ 'Color9': Color('#7c7c7c'), # Gray
+ }
+ if default:
+ return defaults
+ return get_editor_setting('color_palette', defaults)
+
+
+static func get_color(value:String) -> Color:
+ var colors := get_color_palette()
+ return colors[value]
+
+#endregion
+
+
+#region TIMER PROCESS MODE
+################################################################################
+static func is_physics_timer() -> bool:
+ return ProjectSettings.get_setting('dialogic/timer/process_in_physics', false)
+
+
+static func update_timer_process_callback(timer:Timer) -> void:
+ timer.process_callback = Timer.TIMER_PROCESS_PHYSICS if is_physics_timer() else Timer.TIMER_PROCESS_IDLE
+
+#endregion
+
+
+#region MULTITWEEN
+################################################################################
+static func multitween(tweened_value:Variant, item:Node, property:String, part:String) -> void:
+ var parts: Dictionary = item.get_meta(property+'_parts', {})
+ parts[part] = tweened_value
+
+ if not item.has_meta(property+'_base_value') and not 'base' in parts:
+ item.set_meta(property+'_base_value', item.get(property))
+
+ var final_value: Variant = parts.get('base', item.get_meta(property+'_base_value', item.get(property)))
+
+ for key in parts:
+ if key == 'base':
+ continue
+ else:
+ final_value += parts[key]
+
+ item.set(property, final_value)
+ item.set_meta(property+'_parts', parts)
+
+#endregion
+
+
+#region TRANSLATIONS
+################################################################################
+
+static func get_next_translation_id() -> String:
+ ProjectSettings.set_setting('dialogic/translation/id_counter', ProjectSettings.get_setting('dialogic/translation/id_counter', 16)+1)
+ return '%x' % ProjectSettings.get_setting('dialogic/translation/id_counter', 16)
+
+#endregion
+
+
+#region VARIABLES
+################################################################################
+
+enum VarTypes {ANY, STRING, FLOAT, INT, BOOL}
+
+
+static func get_default_variables() -> Dictionary:
+ return ProjectSettings.get_setting('dialogic/variables', {})
+
+
+# helper that converts a nested variable dictionary into an array with paths
+static func list_variables(dict:Dictionary, path := "", type:=VarTypes.ANY) -> Array:
+ var array := []
+ for key in dict.keys():
+ if typeof(dict[key]) == TYPE_DICTIONARY:
+ array.append_array(list_variables(dict[key], path+key+".", type))
+ else:
+ if type == VarTypes.ANY or get_variable_value_type(dict[key]) == type:
+ array.append(path+key)
+ return array
+
+
+static func get_variable_value_type(value:Variant) -> VarTypes:
+ match typeof(value):
+ TYPE_STRING:
+ return VarTypes.STRING
+ TYPE_FLOAT:
+ return VarTypes.FLOAT
+ TYPE_INT:
+ return VarTypes.INT
+ TYPE_BOOL:
+ return VarTypes.BOOL
+ return VarTypes.ANY
+
+
+static func get_variable_type(path:String, dict:Dictionary={}) -> VarTypes:
+ if dict.is_empty():
+ dict = get_default_variables()
+ return get_variable_value_type(_get_value_in_dictionary(path, dict))
+
+
+## This will set a value in a dictionary (or a sub-dictionary based on the path)
+## e.g. it could set "Something.Something.Something" in {'Something':{'Something':{'Someting':"value"}}}
+static func _set_value_in_dictionary(path:String, dictionary:Dictionary, value):
+ if '.' in path:
+ var from := path.split('.')[0]
+ if from in dictionary.keys():
+ dictionary[from] = _set_value_in_dictionary(path.trim_prefix(from+"."), dictionary[from], value)
+ else:
+ if path in dictionary.keys():
+ dictionary[path] = value
+ return dictionary
+
+
+## This will get a value in a dictionary (or a sub-dictionary based on the path)
+## e.g. it could get "Something.Something.Something" in {'Something':{'Something':{'Someting':"value"}}}
+static func _get_value_in_dictionary(path:String, dictionary:Dictionary, default= null) -> Variant:
+ if '.' in path:
+ var from := path.split('.')[0]
+ if from in dictionary.keys():
+ return _get_value_in_dictionary(path.trim_prefix(from+"."), dictionary[from], default)
+ else:
+ if path in dictionary.keys():
+ return dictionary[path]
+ return default
+
+#endregion
+
+
+
+#region STYLES
+################################################################################
+
+static func get_default_layout_base() -> PackedScene:
+ return load(DialogicUtil.get_module_path('DefaultLayoutParts').path_join("Base_Default/default_layout_base.tscn"))
+
+
+static func get_fallback_style() -> DialogicStyle:
+ return load(DialogicUtil.get_module_path('DefaultLayoutParts').path_join("Style_VN_Default/default_vn_style.tres"))
+
+
+static func get_default_style() -> DialogicStyle:
+ var default: String = ProjectSettings.get_setting('dialogic/layout/default_style', '')
+ if !ResourceLoader.exists(default):
+ return get_fallback_style()
+ return load(default)
+
+
+static func get_style_by_name(name:String) -> DialogicStyle:
+ if name.is_empty():
+ return get_default_style()
+
+ var styles: Array = ProjectSettings.get_setting('dialogic/layout/style_list', [])
+ for style in styles:
+ if not ResourceLoader.exists(style):
+ continue
+ if load(style).name == name:
+ return load(style)
+
+ return get_default_style()
+#endregion
+
+
+#region SCENE EXPORT OVERRIDES
+################################################################################
+
+static func apply_scene_export_overrides(node:Node, export_overrides:Dictionary, apply := true) -> void:
+ var default_info := get_scene_export_defaults(node)
+ if !node.script:
+ return
+ var property_info: Array[Dictionary] = node.script.get_script_property_list()
+ for i in property_info:
+ if i['usage'] & PROPERTY_USAGE_EDITOR:
+ if i['name'] in export_overrides:
+ if str_to_var(export_overrides[i['name']]) == null and typeof(node.get(i['name'])) == TYPE_STRING:
+ node.set(i['name'], export_overrides[i['name']])
+ else:
+ node.set(i['name'], str_to_var(export_overrides[i['name']]))
+ elif i['name'] in default_info:
+ node.set(i['name'], default_info.get(i['name']))
+ if apply:
+ if node.has_method('apply_export_overrides'):
+ node.apply_export_overrides()
+
+
+static func get_scene_export_defaults(node:Node) -> Dictionary:
+ if !node.script:
+ return {}
+
+ if Engine.get_main_loop().has_meta('dialogic_scene_export_defaults') and \
+ node.script.resource_path in Engine.get_main_loop().get_meta('dialogic_scene_export_defaults'):
+ return Engine.get_main_loop().get_meta('dialogic_scene_export_defaults')[node.script.resource_path]
+
+ if !Engine.get_main_loop().has_meta('dialogic_scene_export_defaults'):
+ Engine.get_main_loop().set_meta('dialogic_scene_export_defaults', {})
+ var defaults := {}
+ var property_info: Array[Dictionary] = node.script.get_script_property_list()
+ for i in property_info:
+ if i['usage'] & PROPERTY_USAGE_EDITOR:
+ defaults[i['name']] = node.get(i['name'])
+ Engine.get_main_loop().get_meta('dialogic_scene_export_defaults')[node.script.resource_path] = defaults
+ return defaults
+
+#endregion
+
+#region MAKE CUSTOM
+
+static func make_file_custom(original_file:String, target_folder:String, new_file_name := "", new_folder_name := "") -> String:
+ if not ResourceLoader.exists(original_file):
+ push_error("[Dialogic] Unable to make file with invalid path custom!")
+ return ""
+
+ if new_folder_name:
+ target_folder = target_folder.path_join(new_folder_name)
+ DirAccess.make_dir_absolute(target_folder)
+
+ if new_file_name.is_empty():
+ new_file_name = "custom_" + original_file.get_file()
+
+ if not new_file_name.ends_with(original_file.get_extension()):
+ new_file_name += "." + original_file.get_extension()
+
+ var target_file := target_folder.path_join(new_file_name)
+
+ customize_file(original_file, target_file)
+
+ get_dialogic_plugin().get_editor_interface().get_resource_filesystem().scan_sources()
+
+ return target_file
+
+
+static func customize_file(original_file:String, target_file:String) -> String:
+ #print("\nCUSTOMIZE FILE")
+ #printt(original_file, "->", target_file)
+
+ DirAccess.copy_absolute(original_file, target_file)
+
+ var file := FileAccess.open(target_file, FileAccess.READ)
+ var file_text := file.get_as_text()
+ file.close()
+
+ # If we are customizing a scene, we check for any resources used in that scene that are in the same folder.
+ # Those will be copied as well and the scene will be modified to point to them.
+ if file_text.begins_with('[gd_'):
+ var base_path: String = original_file.get_base_dir()
+
+ var remove_uuid_regex := r'\[gd_.* (?<uid>uid="uid:[^"]*")'
+ var result := RegEx.create_from_string(remove_uuid_regex).search(file_text)
+ if result:
+ file_text = file_text.replace(result.get_string("uid"), "")
+
+ # This regex also removes the UID referencing the original resource
+ var file_regex := r'(uid="[^"]*" )?\Qpath="'+base_path+r'\E(?<file>[^"]*)"'
+ result = RegEx.create_from_string(file_regex).search(file_text)
+ while result:
+ var found_file_name := result.get_string('file')
+ var found_file_path := base_path.path_join(found_file_name)
+ var target_file_path := target_file.get_base_dir().path_join(found_file_name)
+
+ # Files found in this file will ALSO be customized.
+ customize_file(found_file_path, target_file_path)
+
+ file_text = file_text.replace(found_file_path, target_file_path)
+
+ result = RegEx.create_from_string(file_regex).search(file_text)
+
+ file = FileAccess.open(target_file, FileAccess.WRITE)
+ file.store_string(file_text)
+ file.close()
+
+ return target_file
+
+#endregion
+
+#region INSPECTOR FIELDS
+################################################################################
+
+static func setup_script_property_edit_node(property_info: Dictionary, value:Variant, property_changed:Callable) -> Control:
+ var input: Control = null
+ match property_info['type']:
+ TYPE_BOOL:
+ input = CheckBox.new()
+ if value != null:
+ input.button_pressed = value
+ input.toggled.connect(DialogicUtil._on_export_bool_submitted.bind(property_info.name, property_changed))
+ TYPE_COLOR:
+ input = ColorPickerButton.new()
+ if value != null:
+ input.color = value
+ input.color_changed.connect(DialogicUtil._on_export_color_submitted.bind(property_info.name, property_changed))
+ input.custom_minimum_size.x = get_editor_scale() * 50
+ TYPE_INT:
+ if property_info['hint'] & PROPERTY_HINT_ENUM:
+ input = OptionButton.new()
+ for x in property_info['hint_string'].split(','):
+ input.add_item(x.split(':')[0])
+ if value != null:
+ input.select(value)
+ input.item_selected.connect(DialogicUtil._on_export_int_enum_submitted.bind(property_info.name, property_changed))
+ else:
+ input = SpinBox.new()
+ input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_info.name, property_changed))
+ if property_info.hint_string == 'int':
+ input.step = 1
+ input.allow_greater = true
+ input.allow_lesser = true
+ elif ',' in property_info.hint_string:
+ input.min_value = int(property_info.hint_string.get_slice(',', 0))
+ input.max_value = int(property_info.hint_string.get_slice(',', 1))
+ if property_info.hint_string.count(',') > 1:
+ input.step = int(property_info.hint_string.get_slice(',', 2))
+ if value != null:
+ input.value = value
+ TYPE_FLOAT:
+ input = SpinBox.new()
+ input.step = 0.01
+ if ',' in property_info.hint_string:
+ input.min_value = float(property_info.hint_string.get_slice(',', 0))
+ input.max_value = float(property_info.hint_string.get_slice(',', 1))
+ if property_info.hint_string.count(',') > 1:
+ input.step = float(property_info.hint_string.get_slice(',', 2))
+ input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_info.name, property_changed))
+ if value != null:
+ input.value = value
+ TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4:
+ var vectorSize: String = type_string(typeof(value))[-1]
+ input = load("res://addons/dialogic/Editor/Events/Fields/field_vector" + vectorSize + ".tscn").instantiate()
+ input.property_name = property_info['name']
+ input.set_value(value)
+ input.value_changed.connect(DialogicUtil._on_export_vector_submitted.bind(property_changed))
+ TYPE_STRING:
+ if property_info['hint'] & PROPERTY_HINT_FILE or property_info['hint'] & PROPERTY_HINT_DIR:
+ input = load("res://addons/dialogic/Editor/Events/Fields/field_file.tscn").instantiate()
+ input.file_filter = property_info['hint_string']
+ input.file_mode = FileDialog.FILE_MODE_OPEN_FILE
+ if property_info['hint'] == PROPERTY_HINT_DIR:
+ input.file_mode = FileDialog.FILE_MODE_OPEN_DIR
+ input.property_name = property_info['name']
+ input.placeholder = "Default"
+ input.hide_reset = true
+ if value != null:
+ input.set_value(value)
+ input.value_changed.connect(DialogicUtil._on_export_file_submitted.bind(property_changed))
+ elif property_info['hint'] & PROPERTY_HINT_ENUM:
+ input = OptionButton.new()
+ var options: PackedStringArray = []
+ for x in property_info['hint_string'].split(','):
+ options.append(x.split(':')[0].strip_edges())
+ input.add_item(options[-1])
+ if value != null:
+ input.select(options.find(value))
+ input.item_selected.connect(DialogicUtil._on_export_string_enum_submitted.bind(property_info.name, options, property_changed))
+ else:
+ input = LineEdit.new()
+ if value != null:
+ input.text = value
+ input.text_submitted.connect(DialogicUtil._on_export_input_text_submitted.bind(property_info.name, property_changed))
+ TYPE_DICTIONARY:
+ input = load("res://addons/dialogic/Editor/Events/Fields/field_dictionary.tscn").instantiate()
+ input.property_name = property_info["name"]
+ input.value_changed.connect(_on_export_dict_submitted.bind(property_changed))
+ TYPE_OBJECT:
+ input = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
+ input.hint_text = "Objects/Resources as settings are currently not supported. \nUse @export_file('*.extension') instead and load the resource once needed."
+
+ _:
+ input = LineEdit.new()
+ if value != null:
+ input.text = value
+ input.text_submitted.connect(_on_export_input_text_submitted.bind(property_info.name, property_changed))
+ return input
+
+
+static func _on_export_input_text_submitted(text:String, property_name:String, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(text))
+
+static func _on_export_bool_submitted(value:bool, property_name:String, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(value))
+
+static func _on_export_color_submitted(color:Color, property_name:String, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(color))
+
+static func _on_export_int_enum_submitted(item:int, property_name:String, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(item))
+
+static func _on_export_number_submitted(value:float, property_name:String, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(value))
+
+static func _on_export_file_submitted(property_name:String, value:String, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(value))
+
+static func _on_export_string_enum_submitted(value:int, property_name:String, list:PackedStringArray, callable: Callable):
+ callable.call(property_name, var_to_str(list[value]))
+
+static func _on_export_vector_submitted(property_name:String, value:Variant, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(value))
+
+static func _on_export_dict_submitted(property_name:String, value:Variant, callable: Callable) -> void:
+ callable.call(property_name, var_to_str(value))
+
+#endregion
+
+
+#region EVENT DEFAULTS
+################################################################################
+
+static func get_custom_event_defaults(event_name:String) -> Dictionary:
+ if Engine.is_editor_hint():
+ return ProjectSettings.get_setting('dialogic/event_default_overrides', {}).get(event_name, {})
+ else:
+ if !Engine.get_main_loop().has_meta('dialogic_event_defaults'):
+ Engine.get_main_loop().set_meta('dialogic_event_defaults', ProjectSettings.get_setting('dialogic/event_default_overrides', {}))
+ return Engine.get_main_loop().get_meta('dialogic_event_defaults').get(event_name, {})
+
+#endregion
+
+
+#region CONVERSION
+################################################################################
+
+static func str_to_bool(boolstring:String) -> bool:
+ return true if boolstring == "true" else false
+
+
+static func logical_convert(value:Variant) -> Variant:
+ if typeof(value) == TYPE_STRING:
+ if value.is_valid_int():
+ return value.to_int()
+ if value.is_valid_float():
+ return value.to_float()
+ if value == 'true':
+ return true
+ if value == 'false':
+ return false
+ return value
+
+
+## Takes [param source] and builds a dictionary of keys only.
+## The values are `null`.
+static func str_to_hash_set(source: String) -> Dictionary:
+ var dictionary := Dictionary()
+
+ for character in source:
+ dictionary[character] = null
+
+ return dictionary
+
+#endregion
+
+
+static func get_character_suggestions(_search_text:String, current_value:DialogicCharacter = null, allow_none := true, allow_all:= false, editor_node:Node = null) -> Dictionary:
+ var suggestions := {}
+
+ var icon := load("res://addons/dialogic/Editor/Images/Resources/character.svg")
+
+ if allow_none and current_value:
+ suggestions['(No one)'] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
+
+ if allow_all:
+ suggestions['ALL'] = {'value':'--All--', 'tooltip':'All currently joined characters leave', 'editor_icon':["GuiEllipsis", "EditorIcons"]}
+
+ # Get characters in the current timeline and place them at the top of suggestions.
+ if editor_node:
+ var recent_characters := []
+ var timeline_node := editor_node.get_parent().find_parent("Timeline") as DialogicEditor
+ for event_node in timeline_node.find_child("Timeline").get_children():
+ if event_node == editor_node:
+ break
+ if event_node.resource is DialogicCharacterEvent or event_node.resource is DialogicTextEvent:
+ recent_characters.append(event_node.resource.character)
+
+ recent_characters.reverse()
+ for character in recent_characters:
+ if character and not character.get_character_name() in suggestions:
+ suggestions[character.get_character_name()] = {'value': character.get_character_name(), 'tooltip': character.resource_path, 'icon': icon.duplicate()}
+
+ var character_directory := DialogicResourceUtil.get_character_directory()
+ for resource in character_directory.keys():
+ suggestions[resource] = {'value': resource, 'tooltip': character_directory[resource], 'icon': icon}
+
+ return suggestions
+
+
+static func get_portrait_suggestions(search_text:String, character:DialogicCharacter, allow_empty := false, empty_text := "Don't Change") -> Dictionary:
+ var icon := load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")
+ var suggestions := {}
+
+ if allow_empty:
+ suggestions[empty_text] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
+
+ if "{" in search_text:
+ suggestions[search_text] = {'value':search_text, 'editor_icon':["Variant", "EditorIcons"]}
+
+ if character != null:
+ for portrait in character.portraits:
+ suggestions[portrait] = {'value':portrait, 'icon':icon}
+
+ return suggestions
+
+
+static func get_portrait_position_suggestions(search_text := "") -> Dictionary:
+ var icon := load(DialogicUtil.get_module_path("Character").path_join('portrait_position.svg'))
+
+ var setting: String = ProjectSettings.get_setting('dialogic/portraits/position_suggestion_names', 'leftmost, left, center, right, rightmost')
+
+ var suggestions := {}
+
+ if not search_text.is_empty():
+ suggestions[search_text] = {'value':search_text.strip_edges(), 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
+
+ for position_id in setting.split(','):
+ suggestions[position_id.strip_edges()] = {'value':position_id.strip_edges(), 'icon':icon}
+ if not search_text.is_empty() and position_id.strip_edges().begins_with(search_text):
+ suggestions.erase(search_text)
+
+ return suggestions
--- /dev/null
+class_name DialogicSubsystem
+extends Node
+
+var dialogic: DialogicGameHandler = null
+
+enum LoadFlags {FULL_LOAD, ONLY_DNODES}
+
+# To be overriden by sub-classes
+# Called once after every subsystem has been added to the tree
+func post_install() -> void:
+ pass
+
+
+# To be overriden by sub-classes
+# Fill in everything that should be cleared (for example before loading a different state)
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ pass
+
+
+# To be overriden by sub-classes
+# Fill in everything that should be loaded using the dialogic_game_handler.current_state_info
+# This is called when a save is loaded
+func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
+ pass
+
+
+# To be overriden by sub-classes
+# Fill in everything that should be saved into the dialogic_game_handler.current_state_info
+# This is called when a save is saved
+func save_game_state() -> void:
+ pass
+
+
+# To be overriden by sub-classes
+func pause() -> void:
+ pass
+
+
+# To be overriden by sub-classes
+func resume() -> void:
+ pass
--- /dev/null
+@tool
+class_name DialogicIndexer
+extends RefCounted
+
+## Script that indexes events, subsystems, settings pages and more. [br]
+## Place a script of this type in every folder in "addons/Events". [br]
+## Overwrite the methods to return the contents of that folder.
+
+
+var this_folder: String = get_script().resource_path.get_base_dir()
+
+## Overwrite if this module contains any events. [br]
+## Return an array with all the paths to the event scripts.[br]
+## You can use the [property this_folder].path_join('my_event.gd')
+func _get_events() -> Array:
+ if ResourceLoader.exists(this_folder.path_join('event.gd')):
+ return [this_folder.path_join('event.gd')]
+ return []
+
+
+## Overwrite if this module contains any subsystems.
+## Should return an array of dictionaries each with the following keys: [br]
+## "name" -> name for this subsystem[br]
+## "script" -> array of preview images[br]
+func _get_subsystems() -> Array[Dictionary]:
+ return []
+
+
+func _get_editors() -> Array[String]:
+ return []
+
+
+func _get_settings_pages() -> Array:
+ return []
+
+
+func _get_character_editor_sections() -> Array:
+ return []
+
+
+#region TEXT EFFECTS & MODIFIERS
+
+## Should return array of dictionaries with the following keys:[br]
+## "command" -> the text e.g. "speed"[br]
+## "node_path" or "subsystem" -> whichever contains your effect method[br]
+## "method" -> name of the effect method[br]
+func _get_text_effects() -> Array[Dictionary]:
+ return []
+
+
+## Should return array of dictionaries with the same arguments as _get_text_effects()
+func _get_text_modifiers() -> Array[Dictionary]:
+ return []
+
+#endregion
+
+
+## Return a list of resources, scripts, etc.
+## These can later be retrieved with DialogicResourceUtil.
+## Each dictionary should contain (at least "type" and "path").
+## E.g. {"type":"Animation", "path": "res://..."}
+func _get_special_resources() -> Dictionary:
+ return {}
+
+
+## Return a list of dictionaries, each
+func _get_portrait_scene_presets() -> Array[Dictionary]:
+ return []
+
+
+#region HELPERS
+################################################################################
+
+func list_dir(subdir:='') -> Array:
+ return Array(DirAccess.get_files_at(this_folder.path_join(subdir))).map(func(file):return this_folder.path_join(subdir).path_join(file))
+
+
+func list_special_resources(subdir:='', extension:="") -> Dictionary:
+ var dict := {}
+ for i in list_dir(subdir):
+ if extension.is_empty() or i.ends_with(extension):
+ dict[DialogicUtil.pretty_name(i).to_lower()] = {"path":i}
+ return dict
+
+
+func list_animations(subdir := "") -> Dictionary:
+ var full_animation_list := {}
+ for path in list_dir(subdir):
+ if not path.ends_with(".gd") and not path.ends_with(".gdc"):
+ continue
+ var anim_object: DialogicAnimation = load(path).new()
+ var versions := anim_object._get_named_variations()
+ for version_name in versions:
+ full_animation_list[version_name] = versions[version_name]
+ full_animation_list[version_name]["path"] = path
+ anim_object.queue_free()
+ return full_animation_list
+
+#endregion
+
+
+#region STYLES & LAYOUTS
+################################################################################
+
+func _get_style_presets() -> Array[Dictionary]:
+ return []
+
+
+## Should return an array of dictionaries with the following keys:[br]
+## "path" -> the path to the scene[br]
+## "name" -> name for this layout[br]
+## "description"-> description of this layout. list what features/events are supported[br]
+## "preview_image"-> array of preview images[br]
+func _get_layout_parts() -> Array[Dictionary]:
+ return []
+
+
+## Helper that allows scanning sub directories that might be layout parts or styles
+func scan_for_layout_parts() -> Array[Dictionary]:
+ var dir := DirAccess.open(this_folder)
+ var style_list: Array[Dictionary] = []
+ if !dir:
+ return style_list
+ dir.list_dir_begin()
+ var dir_name := dir.get_next()
+ while dir_name != "":
+ if !dir.current_is_dir() or !dir.file_exists(dir_name.path_join('part_config.cfg')):
+ dir_name = dir.get_next()
+ continue
+ var config := ConfigFile.new()
+ config.load(this_folder.path_join(dir_name).path_join('part_config.cfg'))
+ var default_image_path: String = this_folder.path_join(dir_name).path_join('preview.png')
+ style_list.append(
+ {
+ 'type': config.get_value('style', 'type', 'Unknown type'),
+ 'name': config.get_value('style', 'name', 'Unnamed Layout'),
+ 'path': this_folder.path_join(dir_name).path_join(config.get_value('style', 'scene', '')),
+ 'author': config.get_value('style', 'author', 'Anonymous'),
+ 'description': config.get_value('style', 'description', 'No description'),
+ 'preview_image': [config.get_value('style', 'image', default_image_path)],
+ 'style_path':config.get_value('style', 'style_path', ''),
+ 'icon':this_folder.path_join(dir_name).path_join(config.get_value('style', 'icon', '')),
+ })
+
+ if not style_list[-1].style_path.begins_with('res://'):
+ style_list[-1].style_path = this_folder.path_join(dir_name).path_join(style_list[-1].style_path)
+
+ dir_name = dir.get_next()
+
+ return style_list
+
+#endregion
--- /dev/null
+@tool
+extends DialogicCharacterEditorPortraitSection
+
+## Section that allows setting values of exported scene variables
+## for custom portrait scenes
+
+var current_portrait_data := {}
+var last_scene := ""
+
+func _get_title() -> String:
+ return "Settings"
+
+
+func _load_portrait_data(data:Dictionary) -> void:
+ _recheck(data, true)
+
+
+## Recheck section visibility and reload export fields.
+## This allows reacting to changes of the portrait_scene setting.
+func _recheck(data: Dictionary, force:=false):
+ if last_scene == data.get("scene", "") and not force:
+ current_portrait_data = data
+ last_scene = data.get("scene", "")
+ return
+
+ last_scene = data.get("scene", "")
+ current_portrait_data = data
+
+ for child in $Grid.get_children():
+ child.get_parent().remove_child(child)
+ child.queue_free()
+
+ var scene: Variant = null
+
+ if current_portrait_data.get('scene', '').is_empty():
+ if ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
+ scene = load(character_editor.def_portrait_path)
+ else:
+ scene = load(ProjectSettings.get_setting('dialogic/portraits/default_portrait', ''))
+ else:
+ scene = load(current_portrait_data.get('scene'))
+
+ if not scene:
+ return
+
+ scene = scene.instantiate()
+
+ var skip := false
+ for i in scene.script.get_script_property_list():
+ if i['usage'] & PROPERTY_USAGE_EDITOR and !skip:
+ var label := Label.new()
+ label.text = i['name'].capitalize()
+ $Grid.add_child(label)
+
+ var current_value: Variant = scene.get(i['name'])
+ if current_portrait_data.has('export_overrides') and current_portrait_data['export_overrides'].has(i['name']):
+ current_value = str_to_var(current_portrait_data.export_overrides[i['name']])
+ if current_value == null and typeof(scene.get(i['name'])) == TYPE_STRING:
+ current_value = current_portrait_data['export_overrides'][i['name']]
+
+ var input: Node = DialogicUtil.setup_script_property_edit_node(i, current_value, set_export_override)
+ input.size_flags_horizontal = SIZE_EXPAND_FILL
+ $Grid.add_child(input)
+
+ if i['usage'] & PROPERTY_USAGE_GROUP:
+ if i['name'] == 'Main' or i["name"] == "Private":
+ skip = true
+ continue
+ else:
+ skip = false
+
+ if $Grid.get_child_count():
+ get_parent().get_child(get_index()-1).show()
+ show()
+ else:
+ hide()
+ get_parent().get_child(get_index()-1).hide()
+ get_parent().get_child(get_index()+1).hide()
+
+
+## On any change, save the export override to the portrait items metadata.
+func set_export_override(property_name:String, value:String = "") -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ if !data.has('export_overrides'):
+ data['export_overrides'] = {}
+ if !value.is_empty():
+ data.export_overrides[property_name] = value
+ else:
+ data.export_overrides.erase(property_name)
+ changed.emit()
+ update_preview.emit()
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://cfcs7lb6gqnmd"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_exports.gd" id="1_isys8"]
+
+[node name="Settings" type="VBoxContainer"]
+custom_minimum_size = Vector2(0, 35)
+offset_right = 367.0
+offset_bottom = 82.0
+script = ExtResource("1_isys8")
+
+[node name="Grid" type="GridContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/h_separation = 10
+columns = 2
--- /dev/null
+@tool
+extends DialogicCharacterEditorPortraitSection
+
+## Tab that allows setting size, offset and mirror of a portrait.
+
+
+func _get_title() -> String:
+ return "Scale, Offset & Mirror"
+
+
+func _load_portrait_data(data:Dictionary) -> void:
+ %IgnoreScale.set_pressed_no_signal(data.get('ignore_char_scale', false))
+ %PortraitScale.value = data.get('scale', 1.0)*100
+ %PortraitOffset.set_value(data.get('offset', Vector2()))
+ %PortraitOffset._load_display_info({'step':1})
+ %PortraitMirror.set_pressed_no_signal(data.get('mirror', false))
+
+
+func _on_portrait_scale_value_changed(value:float) -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ data['scale'] = value/100.0
+ update_preview.emit()
+ changed.emit()
+
+
+func _on_portrait_mirror_toggled(button_pressed:bool)-> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ data['mirror'] = button_pressed
+ update_preview.emit()
+ changed.emit()
+
+
+func _on_ignore_scale_toggled(button_pressed:bool) -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ data['ignore_char_scale'] = button_pressed
+ update_preview.emit()
+ changed.emit()
+
+
+func _on_portrait_offset_value_changed(property:String, value:Vector2) -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ data['offset'] = value
+ update_preview.emit()
+ changed.emit()
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://crke8suvv52c6"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_layout.gd" id="1_76vf2"]
+[ext_resource type="PackedScene" uid="uid://dtimnsj014cu" path="res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn" id="2_c8kyi"]
+
+[node name="Layout" type="HFlowContainer"]
+offset_right = 428.0
+offset_bottom = 128.0
+size_flags_horizontal = 3
+script = ExtResource("1_76vf2")
+
+[node name="Label3" type="Label" parent="."]
+layout_mode = 2
+text = "Ignore Main Scale: "
+
+[node name="IgnoreScale" type="CheckBox" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "This portrait will ignore the main scale."
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+text = "Scale:"
+
+[node name="PortraitScale" type="SpinBox" parent="HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "A scale to be applied on top of the main scale
+(unless ignore main scale is pressed)."
+value = 100.0
+allow_greater = true
+suffix = "%"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="HBoxContainer2"]
+layout_mode = 2
+text = "Offset:"
+
+[node name="PortraitOffset" parent="HBoxContainer2" instance=ExtResource("2_c8kyi")]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Offset that is applied on top of the main portrait offset."
+
+[node name="MirrorOption" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MirrorOption"]
+layout_mode = 2
+text = "Mirror:"
+
+[node name="PortraitMirror" type="CheckBox" parent="MirrorOption"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Mirroring that is applied on top of the main portrait mirror."
+
+[connection signal="toggled" from="IgnoreScale" to="." method="_on_ignore_scale_toggled"]
+[connection signal="value_changed" from="HBoxContainer/PortraitScale" to="." method="_on_portrait_scale_value_changed"]
+[connection signal="value_changed" from="HBoxContainer2/PortraitOffset" to="." method="_on_portrait_offset_value_changed"]
+[connection signal="toggled" from="MirrorOption/PortraitMirror" to="." method="_on_portrait_mirror_toggled"]
--- /dev/null
+@tool
+extends DialogicCharacterEditorPortraitSection
+
+## Tab that allows setting a custom scene for a portrait.
+
+func _get_title() -> String:
+ return "Scene"
+
+func _init() -> void:
+ hint_text = "You can use a custom scene for this portrait."
+
+func _start_opened() -> bool:
+ return true
+
+func _ready() -> void:
+ %ChangeSceneButton.icon = get_theme_icon("Loop", "EditorIcons")
+ %ScenePicker.file_filter = "*.tscn, *.scn; Scenes"
+ %ScenePicker.resource_icon = get_theme_icon('PackedScene', 'EditorIcons')
+ %ScenePicker.placeholder = 'Default scene'
+
+ %OpenSceneButton.icon = get_theme_icon("ExternalLink", "EditorIcons")
+
+
+func _load_portrait_data(data:Dictionary) -> void:
+ reload_ui(data)
+
+
+func _on_open_scene_button_pressed() -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ if ResourceLoader.exists(data.get("scene", "")):
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().open_scene_from_path(data.get("scene", ""))
+ await get_tree().process_frame
+ EditorInterface.set_main_screen_editor("2D")
+
+
+func _on_change_scene_button_pressed() -> void:
+ %PortraitSceneBrowserWindow.popup_centered_ratio(0.6)
+
+
+func _on_portrait_scene_browser_activate_part(part_info: Dictionary) -> void:
+ %PortraitSceneBrowserWindow.hide()
+ match part_info.type:
+ "General":
+ set_scene_path(part_info.path)
+ "Preset":
+ find_parent("EditorView").godot_file_dialog(
+ create_new_portrait_scene.bind(part_info),
+ '*.tscn,*.scn',
+ EditorFileDialog.FILE_MODE_SAVE_FILE,
+ "Select where to save the new scene",
+ part_info.path.get_file().trim_suffix("."+part_info.path.get_extension())+"_"+character_editor.current_resource.get_character_name().to_lower())
+ "Custom":
+ find_parent("EditorView").godot_file_dialog(
+ set_scene_path,
+ '*.tscn, *.scn',
+ EditorFileDialog.FILE_MODE_OPEN_FILE,
+ "Select custom portrait scene",)
+ "Default":
+ set_scene_path("")
+
+
+func create_new_portrait_scene(target_file: String, info: Dictionary) -> void:
+ var path := make_portrait_preset_custom(target_file, info)
+ set_scene_path(path)
+
+
+func make_portrait_preset_custom(target_file:String, info: Dictionary) -> String:
+ var previous_file: String = info.path
+
+ var result_path := DialogicUtil.make_file_custom(previous_file, target_file.get_base_dir(), target_file.get_file())
+
+ return result_path
+
+
+func set_scene_path(path:String) -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ data['scene'] = path
+ update_preview.emit()
+ changed.emit()
+ reload_ui(data)
+
+
+func reload_ui(data: Dictionary) -> void:
+ var path: String = data.get('scene', '')
+ %OpenSceneButton.hide()
+
+ if path.is_empty():
+ %SceneLabel.text = "Default Portrait Scene"
+ %SceneLabel.tooltip_text = "Can be changed in the settings."
+ %SceneLabel.add_theme_color_override("font_color", get_theme_color("readonly_color", "Editor"))
+
+ elif %PortraitSceneBrowser.is_premade_portrait_scene(path):
+ %SceneLabel.text = %PortraitSceneBrowser.portrait_scenes_info[path].name
+ %SceneLabel.tooltip_text = path
+ %SceneLabel.add_theme_color_override("font_color", get_theme_color("accent_color", "Editor"))
+
+ else:
+ %SceneLabel.text = path.get_file()
+ %SceneLabel.tooltip_text = path
+ %SceneLabel.add_theme_color_override("font_color", get_theme_color("property_color_x", "Editor"))
+ %OpenSceneButton.show()
--- /dev/null
+[gd_scene load_steps=6 format=3 uid="uid://djq4aasoihexj"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main.gd" id="1_ht8lu"]
+[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="2_k8xs0"]
+[ext_resource type="PackedScene" uid="uid://b1wn8r84uh11b" path="res://addons/dialogic/Editor/CharacterEditor/portrait_scene_browser.tscn" id="3_ngvgq"]
+
+[sub_resource type="Image" id="Image_tg5pd"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_f5xt2"]
+image = SubResource("Image_tg5pd")
+
+[node name="Scene" type="GridContainer"]
+offset_right = 298.0
+offset_bottom = 86.0
+size_flags_horizontal = 3
+script = ExtResource("1_ht8lu")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ChangeSceneButton" type="Button" parent="HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Change Scene"
+icon = SubResource("ImageTexture_f5xt2")
+
+[node name="SceneLabel" type="Label" parent="HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+mouse_filter = 0
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "asdsdasdasd"
+clip_text = true
+
+[node name="ScenePicker" parent="HBox" instance=ExtResource("2_k8xs0")]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+file_filter = "*.tscn, *.scn; Scenes"
+placeholder = "Default scene"
+
+[node name="OpenSceneButton" type="Button" parent="HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Open/Edit Scene"
+icon = SubResource("ImageTexture_f5xt2")
+
+[node name="PortraitSceneBrowserWindow" type="Window" parent="."]
+unique_name_in_owner = true
+title = "Portrait Scene Browser"
+position = Vector2i(0, 36)
+visible = false
+wrap_controls = true
+transient = true
+popup_window = true
+
+[node name="PortraitSceneBrowser" parent="PortraitSceneBrowserWindow" instance=ExtResource("3_ngvgq")]
+unique_name_in_owner = true
+
+[connection signal="pressed" from="HBox/ChangeSceneButton" to="." method="_on_change_scene_button_pressed"]
+[connection signal="pressed" from="HBox/OpenSceneButton" to="." method="_on_open_scene_button_pressed"]
+[connection signal="activate_part" from="PortraitSceneBrowserWindow/PortraitSceneBrowser" to="." method="_on_portrait_scene_browser_activate_part"]
--- /dev/null
+@tool
+extends DialogicCharacterEditorPortraitSection
+
+## Portrait Settings Section that only shows the MAIN settings of a portrait scene.
+
+var current_portrait_data := {}
+var last_scene := ""
+
+func _show_title() -> bool:
+ return false
+
+
+func _load_portrait_data(data:Dictionary) -> void:
+ _recheck(data, true)
+
+
+func _recheck(data:Dictionary, force := false) -> void:
+ get_parent().get_child(get_index()+1).hide()
+ if last_scene == data.get("scene", "") and not force:
+ current_portrait_data = data
+ last_scene = data.get("scene", "")
+ return
+
+ last_scene = data.get("scene", "")
+ current_portrait_data = data
+
+ load_portrait_scene_export_variables()
+
+
+func load_portrait_scene_export_variables() -> void:
+ for child in $Grid.get_children():
+ child.queue_free()
+
+ var scene: Variant = null
+ if current_portrait_data.get('scene', '').is_empty():
+ if ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
+ scene = load(character_editor.def_portrait_path)
+ else:
+ scene = load(ProjectSettings.get_setting('dialogic/portraits/default_portrait', ''))
+ else:
+ scene = load(current_portrait_data.get('scene'))
+
+ if not scene:
+ return
+
+ scene = scene.instantiate()
+ var skip := true
+ for i in scene.script.get_script_property_list():
+ if i['usage'] & PROPERTY_USAGE_EDITOR and !skip:
+ var label := Label.new()
+ label.text = i['name'].capitalize()
+ $Grid.add_child(label)
+
+ var current_value: Variant = scene.get(i['name'])
+ if current_portrait_data.has('export_overrides') and current_portrait_data['export_overrides'].has(i['name']):
+ current_value = str_to_var(current_portrait_data['export_overrides'][i['name']])
+ if current_value == null and typeof(scene.get(i['name'])) == TYPE_STRING:
+ current_value = current_portrait_data['export_overrides'][i['name']]
+
+ var input: Node = DialogicUtil.setup_script_property_edit_node(i, current_value, set_export_override)
+ input.size_flags_horizontal = SIZE_EXPAND_FILL
+ $Grid.add_child(input)
+
+ if i['usage'] & PROPERTY_USAGE_GROUP:
+ if i['name'] == 'Main':
+ skip = false
+ else:
+ skip = true
+ continue
+
+func set_export_override(property_name:String, value:String = "") -> void:
+ var data: Dictionary = selected_item.get_metadata(0)
+ if !data.has('export_overrides'):
+ data['export_overrides'] = {}
+ if !value.is_empty():
+ data['export_overrides'][property_name] = value
+ else:
+ data['export_overrides'].erase(property_name)
+ changed.emit()
+ update_preview.emit()
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://ba5w02lm3ewkj"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main_exports.gd" id="1_mttrr"]
+
+[node name="MainExports" type="VBoxContainer"]
+offset_right = 374.0
+offset_bottom = 82.0
+script = ExtResource("1_mttrr")
+
+[node name="Grid" type="GridContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/h_separation = 10
+columns = 2
--- /dev/null
+@tool
+extends DialogicCharacterEditorMainSection
+
+var min_width := 200
+
+## The general character settings tab
+func _get_title() -> String:
+ return "General"
+
+
+func _start_opened() -> bool:
+ return true
+
+
+func _ready() -> void:
+ # Connecting all necessary signals
+ %ColorPickerButton.custom_minimum_size.x = DialogicUtil.get_editor_scale() * 30
+ %ColorPickerButton.color_changed.connect(character_editor.something_changed)
+ %DisplayNameLineEdit.text_changed.connect(character_editor.something_changed)
+ %NicknameLineEdit.text_changed.connect(character_editor.something_changed)
+ %DescriptionTextEdit.text_changed.connect(character_editor.something_changed)
+ min_width = get_minimum_size().x
+ resized.connect(_on_resized)
+
+func _load_character(resource:DialogicCharacter) -> void:
+ %DisplayNameLineEdit.text = resource.display_name
+ %ColorPickerButton.color = resource.color
+
+ %NicknameLineEdit.text = ""
+ for nickname in resource.nicknames:
+ %NicknameLineEdit.text += nickname +", "
+ %NicknameLineEdit.text = %NicknameLineEdit.text.trim_suffix(', ')
+
+ %DescriptionTextEdit.text = resource.description
+
+
+func _save_changes(resource:DialogicCharacter) -> DialogicCharacter:
+ resource.display_name = %DisplayNameLineEdit.text
+ resource.color = %ColorPickerButton.color
+ var nicknames := []
+ for n_name in %NicknameLineEdit.text.split(','):
+ nicknames.append(n_name.strip_edges())
+ resource.nicknames = nicknames
+ resource.description = %DescriptionTextEdit.text
+
+ return resource
+
+
+func _on_resized() -> void:
+ if size.x > min_width+20:
+ self.columns = 2
+ else:
+ self.columns = 1
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://bnkck3hocbkk5"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_section_general.gd" id="1_3e1i1"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_cxfqm"]
+
+[sub_resource type="Image" id="Image_yiygw"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_hx3oq"]
+image = SubResource("Image_yiygw")
+
+[node name="General" type="GridContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 7.5
+offset_top = 38.5
+offset_right = -7.5
+offset_bottom = -7.5
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/h_separation = 6
+theme_override_constants/v_separation = 6
+columns = 2
+script = ExtResource("1_3e1i1")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Label2" type="Label" parent="HBox"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Display Name"
+
+[node name="HintTooltip" parent="HBox" instance=ExtResource("2_cxfqm")]
+layout_mode = 2
+tooltip_text = "This name will be displayed on the name label. You can use a dialogic variable. E.g. :{Player.name}"
+texture = SubResource("ImageTexture_hx3oq")
+hint_text = "This name will be displayed on the name label. You can use a dialogic variable. E.g. :{Player.name}"
+
+[node name="DisplayName" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="DisplayNameLineEdit" type="LineEdit" parent="DisplayName"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="HintTooltip4" parent="DisplayName" instance=ExtResource("2_cxfqm")]
+layout_mode = 2
+tooltip_text = "This color can be used on the name label and for occurences of the characters name in text (autocolor names)."
+texture = SubResource("ImageTexture_hx3oq")
+hint_text = "This color can be used on the name label and for occurences of the characters name in text (autocolor names)."
+
+[node name="ColorPickerButton" type="ColorPickerButton" parent="DisplayName"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(30, 0)
+layout_mode = 2
+color = Color(1, 1, 1, 1)
+edit_alpha = false
+
+[node name="HBox2" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Label3" type="Label" parent="HBox2"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Nicknames"
+
+[node name="HintTooltip2" parent="HBox2" instance=ExtResource("2_cxfqm")]
+layout_mode = 2
+tooltip_text = "If autocolor names is enabled, these will be colored in the characters color as well."
+texture = SubResource("ImageTexture_hx3oq")
+hint_text = "If autocolor names is enabled, these will be colored in the characters color as well."
+
+[node name="NicknameLineEdit" type="LineEdit" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="HBox3" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Label4" type="Label" parent="HBox3"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Description"
+
+[node name="HintTooltip3" parent="HBox3" instance=ExtResource("2_cxfqm")]
+layout_mode = 2
+tooltip_text = "No effect, just for you."
+texture = SubResource("ImageTexture_hx3oq")
+hint_text = "No effect, just for you."
+
+[node name="DescriptionTextEdit" type="TextEdit" parent="."]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 65)
+layout_mode = 2
+size_flags_horizontal = 3
+wrap_mode = 1
--- /dev/null
+@tool
+extends DialogicCharacterEditorMainSection
+
+## The general portrait settings section
+
+var loading := false
+
+func _get_title() -> String:
+ return "Portraits"
+
+
+func _ready() -> void:
+ # Connecting all necessary signals
+ %DefaultPortraitPicker.value_changed.connect(default_portrait_changed)
+ %MainScale.value_changed.connect(main_portrait_settings_update)
+ %MainOffset._load_display_info({'step':1})
+ %MainOffset.value_changed.connect(main_portrait_settings_update)
+ %MainMirror.toggled.connect(main_portrait_settings_update)
+
+ # Setting up Default Portrait Picker
+ %DefaultPortraitPicker.resource_icon = load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")
+ %DefaultPortraitPicker.get_suggestions_func = suggest_portraits
+
+
+## Make sure preview get's updated when portrait settings change
+func main_portrait_settings_update(_something=null, _value=null) -> void:
+ if loading:
+ return
+ character_editor.current_resource.scale = %MainScale.value/100.0
+ character_editor.current_resource.offset = %MainOffset.current_value
+ character_editor.current_resource.mirror = %MainMirror.button_pressed
+ character_editor.update_preview()
+ character_editor.something_changed()
+
+
+func default_portrait_changed(property:String, value:String) -> void:
+ character_editor.current_resource.default_portrait = value
+ character_editor.update_default_portrait_star(value)
+
+
+func set_default_portrait(portrait_name:String) -> void:
+ %DefaultPortraitPicker.set_value(portrait_name)
+ default_portrait_changed("", portrait_name)
+
+
+func _load_character(resource:DialogicCharacter) -> void:
+ loading = true
+ %DefaultPortraitPicker.set_value(resource.default_portrait)
+
+ %MainScale.value = 100*resource.scale
+ %MainOffset.set_value(resource.offset)
+ %MainMirror.button_pressed = resource.mirror
+ loading = false
+
+
+func _save_changes(resource:DialogicCharacter) -> DialogicCharacter:
+ # Portrait settings
+ if %DefaultPortraitPicker.current_value in resource.portraits.keys():
+ resource.default_portrait = %DefaultPortraitPicker.current_value
+ elif !resource.portraits.is_empty():
+ resource.default_portrait = resource.portraits.keys()[0]
+ else:
+ resource.default_portrait = ""
+
+ resource.scale = %MainScale.value/100.0
+ resource.offset = %MainOffset.current_value
+ resource.mirror = %MainMirror.button_pressed
+ return resource
+
+
+## Get suggestions for DefaultPortraitPicker
+func suggest_portraits(search:String) -> Dictionary:
+ var suggestions := {}
+ for portrait in character_editor.get_updated_portrait_dict().keys():
+ suggestions[portrait] = {'value':portrait}
+ return suggestions
+
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://cmrgbo8qi145o"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_section_portraits.gd" id="1_6sxsl"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="2_birla"]
+[ext_resource type="PackedScene" uid="uid://dtimnsj014cu" path="res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn" id="3_vcvin"]
+
+[node name="Portraits" type="GridContainer"]
+offset_right = 453.0
+offset_bottom = 141.0
+theme_override_constants/h_separation = 1
+theme_override_constants/v_separation = 6
+columns = 2
+script = ExtResource("1_6sxsl")
+
+[node name="Label5" type="Label" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Default"
+
+[node name="DefaultPortraitPicker" parent="." instance=ExtResource("2_birla")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Select Default Portrait"
+fit_text_length = false
+
+[node name="Label" type="Label" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Main Scale"
+
+[node name="MainScale" type="SpinBox" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 8
+value = 100.0
+allow_greater = true
+alignment = 1
+suffix = "%"
+
+[node name="Label2" type="Label" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Main Offset"
+
+[node name="MainOffset" parent="." instance=ExtResource("3_vcvin")]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 2
+
+[node name="Label3" type="Label" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Main Mirror"
+
+[node name="MainMirror" type="CheckBox" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 8
--- /dev/null
+@tool
+extends DialogicEditor
+
+## Editor for editing character resources.
+
+signal character_loaded(resource_path:String)
+signal portrait_selected()
+
+
+# Current state
+var loading := false
+var current_previewed_scene: Variant = null
+var current_scene_path: String = ""
+
+# References
+var selected_item: TreeItem
+var def_portrait_path: String = DialogicUtil.get_module_path('Character').path_join('default_portrait.tscn')
+
+
+######### EDITOR STUFF and LOADING/SAVING ######################################
+
+#region Resource Logic
+## Method is called once editors manager is ready to accept registers.
+func _register() -> void:
+ ## Makes the editor open this when a .dch file is selected.
+ ## Then _open_resource() is called.
+ editors_manager.register_resource_editor("dch", self)
+
+ ## Add an "add character" button
+ var add_character_button: Button = editors_manager.add_icon_button(
+ load("res://addons/dialogic/Editor/Images/Toolbar/add-character.svg"),
+ 'Add Character',
+ self)
+ add_character_button.pressed.connect(_on_create_character_button_pressed)
+ add_character_button.shortcut = Shortcut.new()
+ add_character_button.shortcut.events.append(InputEventKey.new())
+ add_character_button.shortcut.events[0].keycode = KEY_2
+ add_character_button.shortcut.events[0].ctrl_pressed = true
+
+ ## By default show the no character screen
+ $NoCharacterScreen.show()
+
+
+func _get_title() -> String:
+ return "Character"
+
+
+func _get_icon() -> Texture:
+ return load("res://addons/dialogic/Editor/Images/Resources/character.svg")
+
+
+## Called when a character is opened somehow
+func _open_resource(resource:Resource) -> void:
+ if resource == null:
+ $NoCharacterScreen.show()
+ return
+
+ ## Update resource
+ current_resource = (resource as DialogicCharacter)
+
+ ## Make sure changes in the ui won't trigger saving
+ loading = true
+
+ ## Load other main tabs
+ for child in %MainSettingsSections.get_children():
+ if child is DialogicCharacterEditorMainSection:
+ child._load_character(current_resource)
+
+ ## Clear and then load Portrait section
+ %PortraitSearch.text = ""
+ load_portrait_tree()
+
+ loading = false
+ character_loaded.emit(resource.resource_path)
+
+ %CharacterName.text = DialogicResourceUtil.get_unique_identifier(resource.resource_path)
+
+ $NoCharacterScreen.hide()
+ %PortraitChangeInfo.hide()
+
+
+## Called when the character is opened.
+func _open(extra_info:Variant="") -> void:
+ if !ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
+ def_portrait_path = ProjectSettings.get_setting('dialogic/portraits/default_portrait', '')
+ else:
+ def_portrait_path = DialogicUtil.get_module_path('Character').path_join('default_portrait.tscn')
+
+ if current_resource == null:
+ $NoCharacterScreen.show()
+ return
+
+ update_preview(true)
+ %PortraitChangeInfo.hide()
+
+
+func _clear() -> void:
+ current_resource = null
+ current_resource_state = ResourceStates.SAVED
+ $NoCharacterScreen.show()
+
+
+func _save() -> void:
+ if ! visible or not current_resource:
+ return
+
+ ## Portrait list
+ current_resource.portraits = get_updated_portrait_dict()
+
+ ## Main tabs
+ for child in %MainSettingsSections.get_children():
+ if child is DialogicCharacterEditorMainSection:
+ current_resource = child._save_changes(current_resource)
+
+ ResourceSaver.save(current_resource, current_resource.resource_path)
+ current_resource_state = ResourceStates.SAVED
+ DialogicResourceUtil.update_directory('dch')
+
+
+## Saves a new empty character to the given path
+func new_character(path: String) -> void:
+ var resource := DialogicCharacter.new()
+ resource.resource_path = path
+ resource.display_name = path.get_file().trim_suffix("."+path.get_extension())
+ resource.color = Color(1,1,1,1)
+ resource.default_portrait = ""
+ resource.custom_info = {}
+ ResourceSaver.save(resource, path)
+ EditorInterface.get_resource_filesystem().update_file(path)
+ DialogicResourceUtil.update_directory('dch')
+ editors_manager.edit_resource(resource)
+
+#endregion
+
+
+######### INTERFACE ############################################################
+
+#region Interface
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+
+ DialogicUtil.get_dialogic_plugin().resource_saved.connect(_on_some_resource_saved)
+ # NOTE: This check is required because up to 4.2 this signal is not exposed.
+ if DialogicUtil.get_dialogic_plugin().has_signal("scene_saved"):
+ DialogicUtil.get_dialogic_plugin().scene_saved.connect(_on_some_resource_saved)
+
+ $NoCharacterScreen.color = get_theme_color("dark_color_2", "Editor")
+ $NoCharacterScreen.show()
+ setup_portrait_list_tab()
+
+ _on_fit_preview_toggle_toggled(DialogicUtil.get_editor_setting('character_preview_fit', true))
+ %PreviewLabel.add_theme_color_override("font_color", get_theme_color("readonly_color", "Editor"))
+
+ %PortraitChangeWarning.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
+
+ %RealPreviewPivot.texture = get_theme_icon("EditorPivot", "EditorIcons")
+
+ %MainSettingsCollapse.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
+
+ set_portrait_settings_position(DialogicUtil.get_editor_setting('portrait_settings_position', true))
+
+ await find_parent('EditorView').ready
+
+ ## Add general tabs
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_section_general.tscn").instantiate(), %MainSettingsSections)
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_section_portraits.tscn").instantiate(), %MainSettingsSections)
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/character_prefix_suffix.tscn").instantiate(), %MainSettingsSections)
+
+
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main_exports.tscn").instantiate(), %PortraitSettingsSection)
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_exports.tscn").instantiate(), %PortraitSettingsSection)
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main.tscn").instantiate(), %PortraitSettingsSection)
+ add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_layout.tscn").instantiate(), %PortraitSettingsSection)
+
+ ## Load custom sections from modules
+ for indexer in DialogicUtil.get_indexers():
+ for path in indexer._get_character_editor_sections():
+ var scene: Control = load(path).instantiate()
+ if scene is DialogicCharacterEditorMainSection:
+ add_settings_section(scene, %MainSettingsSections)
+ elif scene is DialogicCharacterEditorPortraitSection:
+ add_settings_section(scene, %PortraitSettingsSection)
+
+
+## Add a section (a control) either to the given settings section (Main or Portraits)
+## - sets up the title of the section
+## - connects to various signals
+func add_settings_section(edit:Control, parent:Node) -> void:
+ edit.changed.connect(something_changed)
+ edit.character_editor = self
+
+ if edit.has_signal('update_preview'):
+ edit.update_preview.connect(update_preview)
+
+ var button: Button
+
+ if edit._show_title():
+ var hbox := HBoxContainer.new()
+ hbox.name = edit._get_title()+"BOX"
+ button = Button.new()
+ button.flat = true
+ button.theme_type_variation = "DialogicSection"
+ button.alignment = HORIZONTAL_ALIGNMENT_LEFT
+ button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
+ button.text = edit._get_title()
+ button.icon_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ button.pressed.connect(_on_section_button_pressed.bind(button))
+ button.focus_mode = Control.FOCUS_NONE
+ button.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
+ button.add_theme_color_override('icon_normal_color', get_theme_color("font_color", "DialogicSection"))
+
+ hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ hbox.add_child(button)
+
+ if !edit.hint_text.is_empty():
+ var hint: Node = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
+ hint.hint_text = edit.hint_text
+ hbox.add_child(hint)
+
+ parent.add_child(hbox)
+ parent.add_child(edit)
+ parent.add_child(HSeparator.new())
+ if button and !edit._start_opened():
+ _on_section_button_pressed(button)
+
+
+func get_settings_section_by_name(name:String, main:=true) -> Node:
+ var parent := %MainSettingsSections
+ if not main:
+ parent = %PortraitSettingsSection
+
+ if parent.has_node(name):
+ return parent.get_node(name)
+ elif parent.has_node(name+"BOX/"+name):
+ return parent.get_node(name+"BOX/"+name)
+ else:
+ return null
+
+
+func _on_section_button_pressed(button:Button) -> void:
+ var section_header := button.get_parent()
+ var section := section_header.get_parent().get_child(section_header.get_index()+1)
+ if section.visible:
+ button.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
+ section.visible = false
+ else:
+ button.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
+ section.visible = true
+
+ if section_header.get_parent().get_child_count() > section_header.get_index()+2 and section_header.get_parent().get_child(section_header.get_index()+2) is Separator:
+ section_header.get_parent().get_child(section_header.get_index()+2).visible = section_header.get_parent().get_child(section_header.get_index()+1).visible
+
+
+func something_changed(fake_argument = "", fake_arg2 = null) -> void:
+ if not loading:
+ current_resource_state = ResourceStates.UNSAVED
+
+
+func _on_main_settings_collapse_toggled(button_pressed:bool) -> void:
+ %MainSettingsTitle.visible = !button_pressed
+ %MainSettingsScroll.visible = !button_pressed
+ if button_pressed:
+ %MainSettings.hide()
+ %MainSettingsCollapse.icon = get_theme_icon("GuiVisibilityHidden", "EditorIcons")
+ else:
+ %MainSettings.show()
+ %MainSettingsCollapse.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
+
+
+func _on_switch_portrait_settings_position_pressed() -> void:
+ set_portrait_settings_position(!%RightSection.vertical)
+
+
+func set_portrait_settings_position(is_below:bool) -> void:
+ %RightSection.vertical = is_below
+ DialogicUtil.set_editor_setting('portrait_settings_position', is_below)
+ if is_below:
+ %SwitchPortraitSettingsPosition.icon = get_theme_icon("ControlAlignRightWide", "EditorIcons")
+ else:
+ %SwitchPortraitSettingsPosition.icon = get_theme_icon("ControlAlignBottomWide", "EditorIcons")
+
+#endregion
+
+
+########## PORTRAIT SECTION ####################################################
+
+#region Portrait Section
+func setup_portrait_list_tab() -> void:
+ %PortraitTree.editor = self
+
+ ## Portrait section styling/connections
+ %AddPortraitButton.icon = get_theme_icon("Add", "EditorIcons")
+ %AddPortraitButton.pressed.connect(add_portrait)
+ %AddPortraitGroupButton.icon = load("res://addons/dialogic/Editor/Images/Pieces/add-folder.svg")
+ %AddPortraitGroupButton.pressed.connect(add_portrait_group)
+ %ImportPortraitsButton.icon = get_theme_icon("Load", "EditorIcons")
+ %ImportPortraitsButton.pressed.connect(open_portrait_folder_select)
+ %PortraitSearch.right_icon = get_theme_icon("Search", "EditorIcons")
+ %PortraitSearch.text_changed.connect(filter_portrait_list)
+
+ %PortraitTree.item_selected.connect(load_selected_portrait)
+ %PortraitTree.item_edited.connect(_on_item_edited)
+ %PortraitTree.item_activated.connect(_on_item_activated)
+
+
+func open_portrait_folder_select() -> void:
+ find_parent("EditorView").godot_file_dialog(
+ import_portraits_from_folder, "*.svg, *.png",
+ EditorFileDialog.FILE_MODE_OPEN_DIR)
+
+
+func import_portraits_from_folder(path:String) -> void:
+ var parent: TreeItem = %PortraitTree.get_root()
+
+ if %PortraitTree.get_selected() and %PortraitTree.get_selected() != parent and %PortraitTree.get_selected().get_metadata(0).has('group'):
+ parent = %PortraitTree.get_selected()
+
+ var dir := DirAccess.open(path)
+ dir.list_dir_begin()
+ var file_name: String = dir.get_next()
+ var files := []
+ while file_name != "":
+ if not dir.current_is_dir():
+ var file_lower := file_name.to_lower()
+ if '.svg' in file_lower or '.png' in file_lower:
+ if not '.import' in file_lower:
+ files.append(file_name)
+ file_name = dir.get_next()
+
+ var prefix: String = files[0]
+ for file in files:
+ while true:
+ if file.begins_with(prefix):
+ break
+ if prefix.is_empty():
+ break
+ prefix = prefix.substr(0, len(prefix)-1)
+
+ for file in files:
+ %PortraitTree.add_portrait_item(file.trim_prefix(prefix).trim_suffix('.'+file.get_extension()),
+ {'scene':"",'export_overrides':{'image':var_to_str(path.path_join(file))}, 'scale':1, 'offset':Vector2(), 'mirror':false}, parent)
+
+ ## Handle selection
+ if parent.get_child_count():
+ parent.get_first_child().select(0)
+ else:
+ # Call anyways to clear preview and hide portrait settings section
+ load_selected_portrait()
+
+ something_changed()
+
+
+func add_portrait(portrait_name:String='New portrait', portrait_data:Dictionary={'scene':"", 'export_overrides':{'image':''}, 'scale':1, 'offset':Vector2(), 'mirror':false}) -> void:
+ var parent: TreeItem = %PortraitTree.get_root()
+ if %PortraitTree.get_selected():
+ if %PortraitTree.get_selected().get_metadata(0) and %PortraitTree.get_selected().get_metadata(0).has('group'):
+ parent = %PortraitTree.get_selected()
+ else:
+ parent = %PortraitTree.get_selected().get_parent()
+ var item: TreeItem = %PortraitTree.add_portrait_item(portrait_name, portrait_data, parent)
+ item.set_meta('new', true)
+ item.set_editable(0, true)
+ item.select(0)
+ %PortraitTree.call_deferred('edit_selected')
+ something_changed()
+
+
+func add_portrait_group() -> void:
+ var parent_item: TreeItem = %PortraitTree.get_root()
+ if %PortraitTree.get_selected() and %PortraitTree.get_selected().get_metadata(0).has('group'):
+ parent_item = %PortraitTree.get_selected()
+ var item: TreeItem = %PortraitTree.add_portrait_group("Group", parent_item)
+ item.set_meta('new', true)
+ item.set_editable(0, true)
+ item.select(0)
+ %PortraitTree.call_deferred('edit_selected')
+
+
+func load_portrait_tree() -> void:
+ %PortraitTree.clear_tree()
+ var root: TreeItem = %PortraitTree.create_item()
+
+ for portrait in current_resource.portraits.keys():
+ var portrait_label: String = portrait
+ var parent: TreeItem = %PortraitTree.get_root()
+ if '/' in portrait:
+ parent = %PortraitTree.create_necessary_group_items(portrait)
+ portrait_label = portrait.split('/')[-1]
+
+ %PortraitTree.add_portrait_item(portrait_label, current_resource.portraits[portrait], parent)
+
+ update_default_portrait_star(current_resource.default_portrait)
+
+ if root.get_child_count():
+ root.get_first_child().select(0)
+ while %PortraitTree.get_selected().get_child_count():
+ %PortraitTree.get_selected().get_child(0).select(0)
+ else:
+ # Call anyways to clear preview and hide portrait settings section
+ load_selected_portrait()
+
+
+func filter_portrait_list(filter_term := "") -> void:
+ filter_branch(%PortraitTree.get_root(), filter_term)
+
+
+func filter_branch(parent: TreeItem, filter_term: String) -> bool:
+ var anything_visible := false
+ for item in parent.get_children():
+ if item.get_metadata(0).has('group'):
+ item.visible = filter_branch(item, filter_term)
+ anything_visible = item.visible
+ elif filter_term.is_empty() or filter_term.to_lower() in item.get_text(0).to_lower():
+ item.visible = true
+ anything_visible = true
+ else:
+ item.visible = false
+ return anything_visible
+
+
+## This is used to save the portrait data
+func get_updated_portrait_dict() -> Dictionary:
+ return list_portraits(%PortraitTree.get_root().get_children())
+
+
+func list_portraits(tree_items: Array[TreeItem], dict := {}, path_prefix := "") -> Dictionary:
+ for item in tree_items:
+ if item.get_metadata(0).has('group'):
+ dict = list_portraits(item.get_children(), dict, path_prefix+item.get_text(0)+"/")
+ else:
+ dict[path_prefix +item.get_text(0)] = item.get_metadata(0)
+ return dict
+
+
+func load_selected_portrait() -> void:
+ if selected_item and is_instance_valid(selected_item):
+ selected_item.set_editable(0, false)
+
+ selected_item = %PortraitTree.get_selected()
+
+ if selected_item and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
+ %PortraitSettingsSection.show()
+ var current_portrait_data: Dictionary = selected_item.get_metadata(0)
+ portrait_selected.emit(%PortraitTree.get_full_item_name(selected_item), current_portrait_data)
+
+ update_preview()
+
+ for child in %PortraitSettingsSection.get_children():
+ if child is DialogicCharacterEditorPortraitSection:
+ child.selected_item = selected_item
+ child._load_portrait_data(current_portrait_data)
+
+ else:
+ %PortraitSettingsSection.hide()
+ update_preview()
+
+
+func delete_portrait_item(item: TreeItem) -> void:
+ if item.get_next_visible(true) and item.get_next_visible(true) != item:
+ item.get_next_visible(true).select(0)
+ else:
+ selected_item = null
+ load_selected_portrait()
+ item.free()
+ something_changed()
+
+
+func duplicate_item(item: TreeItem) -> void:
+ var new_item: TreeItem = %PortraitTree.add_portrait_item(item.get_text(0)+'_duplicated', item.get_metadata(0).duplicate(true), item.get_parent())
+ new_item.set_meta('new', true)
+ new_item.select(0)
+
+
+func _input(event: InputEvent) -> void:
+ if !is_visible_in_tree() or (get_viewport().gui_get_focus_owner()!= null and !name+'/' in str(get_viewport().gui_get_focus_owner().get_path())):
+ return
+ if event is InputEventKey and event.pressed:
+ if event.keycode == KEY_F2 and %PortraitTree.get_selected():
+ %PortraitTree.get_selected().set_editable(0, true)
+ %PortraitTree.edit_selected()
+ get_viewport().set_input_as_handled()
+ elif event.keycode == KEY_DELETE and get_viewport().gui_get_focus_owner() is Tree and %PortraitTree.get_selected():
+ delete_portrait_item(%PortraitTree.get_selected())
+ get_viewport().set_input_as_handled()
+
+
+func _on_portrait_right_click_menu_index_pressed(id: int) -> void:
+ # RENAME BUTTON
+ if id == 0:
+ _on_item_activated()
+ # DELETE BUTTON
+ if id == 2:
+ delete_portrait_item(%PortraitTree.get_selected())
+ # DUPLICATE ITEM
+ elif id == 1:
+ duplicate_item(%PortraitTree.get_selected())
+ elif id == 4:
+ get_settings_section_by_name("Portraits").set_default_portrait(%PortraitTree.get_full_item_name(%PortraitTree.get_selected()))
+
+
+## This removes/and adds the DEFAULT star on the portrait list
+func update_default_portrait_star(default_portrait_name: String) -> void:
+ var item_list: Array = %PortraitTree.get_root().get_children()
+ if item_list.is_empty() == false:
+ while true:
+ var item: TreeItem = item_list.pop_back()
+ if item.get_button_by_id(0, 2) != -1:
+ item.erase_button(0, item.get_button_by_id(0, 2))
+ if %PortraitTree.get_full_item_name(item) == default_portrait_name:
+ item.add_button(0, get_theme_icon("Favorites", "EditorIcons"), 2, true, "Default")
+ item_list.append_array(item.get_children())
+ if item_list.is_empty():
+ break
+
+
+func _on_item_edited() -> void:
+ selected_item = %PortraitTree.get_selected()
+ something_changed()
+ if selected_item:
+ if %PreviewLabel.text.trim_prefix('Preview of "').trim_suffix('"') == current_resource.default_portrait:
+ current_resource.default_portrait = %PortraitTree.get_full_item_name(selected_item)
+ selected_item.set_editable(0, false)
+
+ if !selected_item.has_meta('new') and %PortraitTree.get_full_item_name(selected_item) != selected_item.get_meta('previous_name'):
+ report_name_change(selected_item)
+ %PortraitChangeInfo.show()
+ update_preview()
+
+
+func _on_item_activated() -> void:
+ if %PortraitTree.get_selected() == null:
+ return
+ %PortraitTree.get_selected().set_editable(0, true)
+ %PortraitTree.edit_selected()
+
+
+func report_name_change(item: TreeItem) -> void:
+ if item.get_metadata(0).has('group'):
+ for s_item in item.get_children():
+ if s_item.get_metadata(0).has('group') or !s_item.has_meta('new'):
+ report_name_change(s_item)
+ else:
+ if item.get_meta('previous_name') == %PortraitTree.get_full_item_name(item):
+ return
+ editors_manager.reference_manager.add_portrait_ref_change(
+ item.get_meta('previous_name'),
+ %PortraitTree.get_full_item_name(item),
+ [DialogicResourceUtil.get_unique_identifier(current_resource.resource_path)])
+ item.set_meta('previous_name', %PortraitTree.get_full_item_name(item))
+ %PortraitChangeInfo.show()
+
+#endregion
+
+########### PREVIEW ############################################################
+
+#region Preview
+func update_preview(force := false, ignore_settings_reload := false) -> void:
+ %ScenePreviewWarning.hide()
+
+ if selected_item and is_instance_valid(selected_item) and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
+ %PreviewLabel.text = 'Preview of "'+%PortraitTree.get_full_item_name(selected_item)+'"'
+
+ var current_portrait_data: Dictionary = selected_item.get_metadata(0)
+
+ if not force and current_previewed_scene != null \
+ and scene_file_path == current_portrait_data.get('scene') \
+ and current_previewed_scene.has_method('_should_do_portrait_update') \
+ and is_instance_valid(current_previewed_scene.get_script()) \
+ and current_previewed_scene._should_do_portrait_update(current_resource, selected_item.get_text(0)):
+ # We keep the same scene.
+ pass
+ else:
+
+ for node in %RealPreviewPivot.get_children():
+ node.queue_free()
+
+ current_previewed_scene = null
+ current_scene_path = ""
+
+ var scene_path := def_portrait_path
+ if not current_portrait_data.get('scene', '').is_empty():
+ scene_path = current_portrait_data.get('scene')
+
+ if ResourceLoader.exists(scene_path):
+ current_previewed_scene = load(scene_path).instantiate()
+ current_scene_path = scene_path
+
+ if not current_previewed_scene == null:
+ %RealPreviewPivot.add_child(current_previewed_scene)
+
+ if not current_previewed_scene == null:
+ var scene: Node = current_previewed_scene
+
+ scene.show_behind_parent = true
+ DialogicUtil.apply_scene_export_overrides(scene, current_portrait_data.get('export_overrides', {}))
+
+ var mirror: bool = current_portrait_data.get('mirror', false) != current_resource.mirror
+ var scale: float = current_portrait_data.get('scale', 1) * current_resource.scale
+
+ if current_portrait_data.get('ignore_char_scale', false):
+ scale = current_portrait_data.get('scale', 1)
+
+ var offset: Vector2 = current_portrait_data.get('offset', Vector2()) + current_resource.offset
+
+ if is_instance_valid(scene.get_script()) and scene.script.is_tool():
+
+ if scene.has_method('_update_portrait'):
+ ## Create a fake duplicate resource that has all the portrait changes applied already
+ var preview_character := current_resource.duplicate()
+ preview_character.portraits = get_updated_portrait_dict()
+ scene._update_portrait(preview_character, %PortraitTree.get_full_item_name(selected_item))
+
+ if scene.has_method('_set_mirror'):
+ scene._set_mirror(mirror)
+
+ if !%FitPreview_Toggle.button_pressed:
+ scene.position = Vector2() + offset
+ scene.scale = Vector2(1,1)*scale
+ else:
+
+ if not scene.get_script() == null and scene.script.is_tool() and scene.has_method('_get_covered_rect'):
+ var rect: Rect2 = scene._get_covered_rect()
+ var available_rect: Rect2 = %FullPreviewAvailableRect.get_rect()
+ scene.scale = Vector2(1,1) * min(available_rect.size.x/rect.size.x, available_rect.size.y/rect.size.y)
+ %RealPreviewPivot.position = (rect.position)*-1*scene.scale
+ %RealPreviewPivot.position.x = %FullPreviewAvailableRect.size.x/2
+ scene.position = Vector2()
+
+ else:
+ %ScenePreviewWarning.show()
+ else:
+ %PreviewLabel.text = 'Nothing to preview'
+
+ if not ignore_settings_reload:
+ for child in %PortraitSettingsSection.get_children():
+ if child is DialogicCharacterEditorPortraitSection:
+ child._recheck(current_portrait_data)
+
+ else:
+ %PreviewLabel.text = 'No portrait to preview.'
+
+ for node in %RealPreviewPivot.get_children():
+ node.queue_free()
+
+ current_previewed_scene = null
+ current_scene_path = ""
+
+
+func _on_some_resource_saved(file:Variant) -> void:
+ if current_previewed_scene == null:
+ return
+
+ if file is Resource and file == current_previewed_scene.script:
+ update_preview(true)
+
+ if typeof(file) == TYPE_STRING and file == current_previewed_scene.get_meta("path", ""):
+ update_preview(true)
+
+
+func _on_full_preview_available_rect_resized() -> void:
+ if %FitPreview_Toggle.button_pressed:
+ update_preview(false, true)
+
+
+func _on_create_character_button_pressed() -> void:
+ editors_manager.show_add_resource_dialog(
+ new_character,
+ '*.dch; DialogicCharacter',
+ 'Create new character',
+ 'character',
+ )
+
+
+func _on_fit_preview_toggle_toggled(button_pressed):
+ %FitPreview_Toggle.set_pressed_no_signal(button_pressed)
+ if button_pressed:
+ %FitPreview_Toggle.icon = get_theme_icon("ScrollContainer", "EditorIcons")
+ %FitPreview_Toggle.tooltip_text = "Real scale"
+ else:
+ %FitPreview_Toggle.tooltip_text = "Fit into preview"
+ %FitPreview_Toggle.icon = get_theme_icon("CenterContainer", "EditorIcons")
+ DialogicUtil.set_editor_setting('character_preview_fit', button_pressed)
+ update_preview(false, true)
+
+#endregion
+
+## Open the reference manager
+func _on_reference_manger_button_pressed() -> void:
+ editors_manager.reference_manager.open()
+ %PortraitChangeInfo.hide()
--- /dev/null
+[gd_scene load_steps=11 format=3 uid="uid://dlskc36c5hrwv"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/character_editor.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_uhhqs"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/character_editor_portrait_tree.gd" id="2_vad0i"]
+[ext_resource type="Texture2D" uid="uid://babwe22dqjta" path="res://addons/dialogic/Editor/Images/Pieces/add-folder.svg" id="3_v1qnr"]
+
+[sub_resource type="Image" id="Image_s4mcg"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_oab13"]
+image = SubResource("Image_s4mcg")
+
+[sub_resource type="Image" id="Image_fnxud"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_u1a6g"]
+image = SubResource("Image_fnxud")
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_es2rd"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_4xgdx"]
+
+[node name="CharacterEditor" type="Control"]
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2")
+
+[node name="Scroll" type="ScrollContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBox" type="VBoxContainer" parent="Scroll"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.3
+theme_override_constants/separation = 0
+
+[node name="TopSection" type="HBoxContainer" parent="Scroll/VBox"]
+layout_mode = 2
+
+[node name="NameContainer" type="HBoxContainer" parent="Scroll/VBox/TopSection"]
+layout_mode = 2
+
+[node name="CharacterName" type="Label" parent="Scroll/VBox/TopSection/NameContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicTitle"
+text = "My Character"
+
+[node name="NameTooltip" parent="Scroll/VBox/TopSection/NameContainer" instance=ExtResource("2_uhhqs")]
+layout_mode = 2
+tooltip_text = "This unique identifier is based on the file name. You can change it in the Reference Manager.
+Use this name in timelines to reference this character."
+texture = SubResource("ImageTexture_oab13")
+hint_text = "This unique identifier is based on the file name. You can change it in the Reference Manager.
+Use this name in timelines to reference this character."
+
+[node name="MainSettingsCollapse" type="Button" parent="Scroll/VBox/TopSection"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+toggle_mode = true
+text = "Main Settings"
+icon = SubResource("ImageTexture_u1a6g")
+
+[node name="MainHSplit" type="HSplitContainer" parent="Scroll/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="MainSettings" type="VBoxContainer" parent="Scroll/VBox/MainHSplit"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.2
+
+[node name="MainSettingsTitle" type="Label" parent="Scroll/VBox/MainHSplit/MainSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_type_variation = &"DialogicSubTitle"
+text = "Main Settings"
+
+[node name="MainSettingsScroll" type="ScrollContainer" parent="Scroll/VBox/MainHSplit/MainSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxEmpty_es2rd")
+horizontal_scroll_mode = 0
+
+[node name="MainSettingsSections" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/MainSettings/MainSettingsScroll"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Split" type="HSplitContainer" parent="Scroll/VBox/MainHSplit"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.2
+theme_override_constants/separation = 0
+
+[node name="MarginContainer" type="MarginContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.2
+theme_override_constants/margin_bottom = 10
+
+[node name="PortraitListSection" type="PanelContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_type_variation = &"DialogicPanelA"
+
+[node name="Portraits" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection"]
+layout_mode = 2
+
+[node name="PortraitsTitle" type="Label" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Portraits"
+
+[node name="PortraitListTools" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits"]
+layout_mode = 2
+
+[node name="AddPortraitButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitListTools"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add portrait"
+icon = SubResource("ImageTexture_u1a6g")
+
+[node name="AddPortraitGroupButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitListTools"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add Group"
+icon = ExtResource("3_v1qnr")
+
+[node name="ImportPortraitsButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitListTools"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Import images from folder"
+icon = SubResource("ImageTexture_u1a6g")
+
+[node name="PortraitSearch" type="LineEdit" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitListTools"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+placeholder_text = "Search"
+expand_to_text_length = true
+clear_button_enabled = true
+right_icon = SubResource("ImageTexture_u1a6g")
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="PortraitTreePanel" type="PanelContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxEmpty_4xgdx")
+
+[node name="PortraitTree" type="Tree" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel"]
+unique_name_in_owner = true
+layout_mode = 2
+allow_rmb_select = true
+hide_root = true
+drop_mode_flags = 3
+script = ExtResource("2_vad0i")
+
+[node name="PortraitRightClickMenu" type="PopupMenu" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree"]
+size = Vector2i(118, 100)
+item_count = 5
+item_0/text = "Rename"
+item_0/icon = SubResource("ImageTexture_oab13")
+item_0/id = 2
+item_1/text = "Duplicate"
+item_1/icon = SubResource("ImageTexture_oab13")
+item_1/id = 0
+item_2/text = "Delete"
+item_2/icon = SubResource("ImageTexture_oab13")
+item_2/id = 1
+item_3/text = ""
+item_3/id = 3
+item_3/separator = true
+item_4/text = "Make Default"
+item_4/icon = SubResource("ImageTexture_oab13")
+item_4/id = 4
+
+[node name="PortraitChangeInfo" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="PortraitChangeWarning" type="Label" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitChangeInfo"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "Some portraits were renamed. Make sure no references broke!"
+autowrap_mode = 3
+
+[node name="ReferenceMangerButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitChangeInfo"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+text = "Reference
+Manager"
+
+[node name="RightSection2" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.5
+
+[node name="Spacer" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="RightSection" type="SplitContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.5
+vertical = true
+
+[node name="PortraitPreviewSection" type="Panel" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection"]
+unique_name_in_owner = true
+show_behind_parent = true
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelB"
+
+[node name="ClipRect" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection"]
+clip_contents = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Node2D" type="Node2D" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/ClipRect"]
+position = Vector2(13, 17)
+
+[node name="RealPreviewPivot" type="Sprite2D" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/ClipRect/Node2D"]
+unique_name_in_owner = true
+position = Vector2(326.5, 267)
+texture = SubResource("ImageTexture_u1a6g")
+
+[node name="ScenePreviewWarning" type="Label" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -143.0
+offset_top = -44.5
+offset_right = 143.0
+offset_bottom = 85.5
+grow_horizontal = 2
+grow_vertical = 2
+text = "Custom scenes can only be viewed in \"Full mode\" if they are in @tool mode and override _get_covered_rect"
+horizontal_alignment = 1
+vertical_alignment = 1
+autowrap_mode = 3
+metadata/_edit_layout_mode = 1
+
+[node name="PreviewReal" type="CenterContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -302.0
+offset_top = -80.0
+offset_right = 302.0
+grow_horizontal = 2
+grow_vertical = 0
+mouse_filter = 2
+metadata/_edit_layout_mode = 1
+
+[node name="Control" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/PreviewReal"]
+layout_mode = 2
+
+[node name="RealSizeRemotePivotTransform" type="RemoteTransform2D" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/PreviewReal/Control"]
+unique_name_in_owner = true
+remote_path = NodePath("../../../ClipRect/Node2D/RealPreviewPivot")
+update_rotation = false
+update_scale = false
+
+[node name="FullPreviewAvailableRect" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 10.0
+offset_top = 28.0
+offset_right = -10.0
+offset_bottom = -16.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+metadata/_edit_layout_mode = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection"]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_left = 6.0
+offset_top = 7.0
+offset_right = -6.0
+offset_bottom = 43.0
+grow_horizontal = 2
+mouse_filter = 2
+
+[node name="PreviewLabel" type="Label" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/HBoxContainer"]
+unique_name_in_owner = true
+show_behind_parent = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "No portrait to preview."
+text_overrun_behavior = 1
+
+[node name="FitPreview_Toggle" type="Button" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 0
+tooltip_text = "Real scale"
+focus_mode = 0
+toggle_mode = true
+button_pressed = true
+icon = SubResource("ImageTexture_u1a6g")
+flat = true
+metadata/_edit_layout_mode = 1
+
+[node name="VBox" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.75
+
+[node name="Hbox" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox"]
+layout_mode = 2
+
+[node name="PortraitSettingsTitle" type="Label" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Hbox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Portrait Settings"
+
+[node name="SwitchPortraitSettingsPosition" type="Button" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Hbox"]
+unique_name_in_owner = true
+modulate = Color(1, 1, 1, 0.647059)
+layout_mode = 2
+tooltip_text = "Switch position"
+focus_mode = 0
+icon = SubResource("ImageTexture_u1a6g")
+flat = true
+
+[node name="Scroll" type="ScrollContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.4
+
+[node name="PortraitSettingsSection" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Scroll"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.3
+
+[node name="Spacer2" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2"]
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="NoCharacterScreen" type="ColorRect" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+color = Color(0, 0, 0, 1)
+
+[node name="CenterContainer" type="CenterContainer" parent="NoCharacterScreen"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="NoCharacterScreen/CenterContainer"]
+custom_minimum_size = Vector2(250, 0)
+layout_mode = 2
+
+[node name="Label" type="Label" parent="NoCharacterScreen/CenterContainer/VBoxContainer"]
+layout_mode = 2
+text = "No character opened.
+Create a character or double-click one in the file system dock."
+horizontal_alignment = 1
+autowrap_mode = 3
+
+[node name="CreateCharacterButton" type="Button" parent="NoCharacterScreen/CenterContainer/VBoxContainer"]
+layout_mode = 2
+text = "Create New Character"
+
+[connection signal="toggled" from="Scroll/VBox/TopSection/MainSettingsCollapse" to="." method="_on_main_settings_collapse_toggled"]
+[connection signal="item_mouse_selected" from="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree" to="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree" method="_on_item_mouse_selected"]
+[connection signal="index_pressed" from="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree/PortraitRightClickMenu" to="." method="_on_portrait_right_click_menu_index_pressed"]
+[connection signal="pressed" from="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitChangeInfo/ReferenceMangerButton" to="." method="_on_reference_manger_button_pressed"]
+[connection signal="resized" from="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/FullPreviewAvailableRect" to="." method="_on_full_preview_available_rect_resized"]
+[connection signal="toggled" from="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/HBoxContainer/FitPreview_Toggle" to="." method="_on_fit_preview_toggle_toggled"]
+[connection signal="pressed" from="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Hbox/SwitchPortraitSettingsPosition" to="." method="_on_switch_portrait_settings_position_pressed"]
+[connection signal="pressed" from="NoCharacterScreen/CenterContainer/VBoxContainer/CreateCharacterButton" to="." method="_on_create_character_button_pressed"]
--- /dev/null
+@tool
+class_name DialogicCharacterEditorMainSection
+extends Control
+
+## Base class for all character editor main sections. Methods should be overriden.
+
+## Emit this, if something changed
+signal changed
+
+## Reference to the character editor, set when instantiated
+var character_editor: Control
+
+## If not empty, a hint icon is added to the section title.
+var hint_text := ""
+
+
+## Overwrite to set the title of this section
+func _get_title() -> String:
+ return "MainSection"
+
+
+## Overwrite to set the visibility of the section title
+func _show_title() -> bool:
+ return true
+
+
+## Overwrite to set whether this should initially be opened.
+func _start_opened() -> bool:
+ return false
+
+
+## Overwrite to load all the information from the character into this section.
+func _load_character(resource:DialogicCharacter) -> void:
+ pass
+
+
+## Overwrite to save all changes made in this section to the resource.
+## In custom sections you will mostly likely save to the [resource.custom_info]
+## dictionary.
+func _save_changes(resource:DialogicCharacter) -> DialogicCharacter:
+ return resource
--- /dev/null
+@tool
+class_name DialogicCharacterEditorPortraitSection
+extends Control
+
+## Base class for all portrait settings sections. Methods should be overriden.
+## Changes made through fields in such a section should instantly be "saved"
+## to the portrait_items metadata from where they will be saved to the resource.
+
+## Emit this, if something changed
+signal changed
+## Emit this if the preview should reload
+signal update_preview
+
+## Reference to the character editor, set when instantiated
+var character_editor: Control
+## Reference to the selected portrait item.
+## `selected_item.get_metadata(0)` can access the portraits data
+var selected_item: TreeItem = null
+
+## If not empty a hint icon is added to the section title
+var hint_text := ""
+
+
+## Overwrite to set the title of this section
+func _get_title() -> String:
+ return "CustomSection"
+
+
+## Overwrite to set the visibility of the section title
+func _show_title() -> bool:
+ return true
+
+
+## Overwrite to set whether this should initially be opened.
+func _start_opened() -> bool:
+ return false
+
+
+## Overwrite to load all the information from the character into this section.
+func _load_portrait_data(data:Dictionary) -> void:
+ pass
+
+
+## Overwrite to recheck visibility of your section and the content of your fields.
+## This is called whenever the preview is updated so it allows reacting to major
+## changes in other portrait sections.
+func _recheck(data:Dictionary) -> void:
+ pass
--- /dev/null
+@tool
+extends Tree
+
+## Tree that displays the portrait list as a hirarchy
+
+var editor := find_parent('Character Editor')
+var current_group_nodes := {}
+
+
+func _ready() -> void:
+ $PortraitRightClickMenu.set_item_icon(0, get_theme_icon('Rename', 'EditorIcons'))
+ $PortraitRightClickMenu.set_item_icon(1, get_theme_icon('Duplicate', 'EditorIcons'))
+ $PortraitRightClickMenu.set_item_icon(2, get_theme_icon('Remove', 'EditorIcons'))
+ $PortraitRightClickMenu.set_item_icon(4, get_theme_icon("Favorites", "EditorIcons"))
+
+
+func clear_tree() -> void:
+ clear()
+ current_group_nodes = {}
+
+
+func add_portrait_item(portrait_name: String, portrait_data: Dictionary, parent_item: TreeItem, previous_name := "") -> TreeItem:
+ var item: TreeItem = %PortraitTree.create_item(parent_item)
+ item.set_text(0, portrait_name)
+ item.set_metadata(0, portrait_data)
+ if previous_name.is_empty():
+ item.set_meta('previous_name', get_full_item_name(item))
+ else:
+ item.set_meta('previous_name', previous_name)
+ if portrait_name == editor.current_resource.default_portrait:
+ item.add_button(0, get_theme_icon('Favorites', 'EditorIcons'), 2, true, 'Default')
+ return item
+
+
+func add_portrait_group(goup_name := "Group", parent_item: TreeItem = get_root(), previous_name := "") -> TreeItem:
+ var item: TreeItem = %PortraitTree.create_item(parent_item)
+ item.set_icon(0, get_theme_icon("Folder", "EditorIcons"))
+ item.set_text(0, goup_name)
+ item.set_metadata(0, {'group':true})
+ if previous_name.is_empty():
+ item.set_meta('previous_name', get_full_item_name(item))
+ else:
+ item.set_meta('previous_name', previous_name)
+ return item
+
+
+func get_full_item_name(item: TreeItem) -> String:
+ var item_name := item.get_text(0)
+ while item.get_parent() != get_root() and item != get_root():
+ item_name = item.get_parent().get_text(0)+"/"+item_name
+ item = item.get_parent()
+ return item_name
+
+
+## Will create all not yet existing folders in the given path.
+## Returns the last folder (the parent of the portrait item of this path).
+func create_necessary_group_items(path: String) -> TreeItem:
+ var last_item := get_root()
+ var item_path := ""
+
+ for i in Array(path.split('/')).slice(0, -1):
+ item_path += "/"+i
+ item_path = item_path.trim_prefix('/')
+ if current_group_nodes.has(item_path+"/"+i):
+ last_item = current_group_nodes[item_path+"/"+i]
+ else:
+ var new_item: TreeItem = add_portrait_group(i, last_item)
+ current_group_nodes[item_path+"/"+i] = new_item
+ last_item = new_item
+ return last_item
+
+
+func _on_item_mouse_selected(pos: Vector2, mouse_button_index: int) -> void:
+ if mouse_button_index == MOUSE_BUTTON_RIGHT:
+ $PortraitRightClickMenu.set_item_disabled(1, get_selected().get_metadata(0).has('group'))
+ $PortraitRightClickMenu.popup_on_parent(Rect2(get_global_mouse_position(),Vector2()))
+
+
+################################################################################
+## DRAG AND DROP
+################################################################################
+
+func _get_drag_data(at_position: Vector2) -> Variant:
+ var drag_item := get_item_at_position(at_position)
+ if not drag_item:
+ return null
+
+ drop_mode_flags = DROP_MODE_INBETWEEN
+ var preview := Label.new()
+ preview.text = " "+drag_item.get_text(0)
+ preview.add_theme_stylebox_override('normal', get_theme_stylebox("Background", "EditorStyles"))
+ set_drag_preview(preview)
+
+ return drag_item
+
+
+func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
+ return data is TreeItem
+
+
+func _drop_data(at_position: Vector2, item: Variant) -> void:
+ var to_item := get_item_at_position(at_position)
+ if to_item:
+ var test_item := to_item
+ while true:
+ if test_item == item:
+ return
+ test_item = test_item.get_parent()
+ if test_item == get_root():
+ break
+
+ var drop_section := get_drop_section_at_position(at_position)
+ var parent := get_root()
+ if to_item:
+ parent = to_item.get_parent()
+
+ if to_item and to_item.get_metadata(0).has('group') and drop_section == 1:
+ parent = to_item
+
+ var new_item := copy_branch_or_item(item, parent)
+
+ if to_item and !to_item.get_metadata(0).has('group') and drop_section == 1:
+ new_item.move_after(to_item)
+
+ if drop_section == -1:
+ new_item.move_before(to_item)
+
+ editor.report_name_change(new_item)
+
+ item.free()
+
+
+func copy_branch_or_item(item: TreeItem, new_parent: TreeItem) -> TreeItem:
+ var new_item: TreeItem = null
+ if item.get_metadata(0).has('group'):
+ new_item = add_portrait_group(item.get_text(0), new_parent, item.get_meta('previous_name'))
+ else:
+ new_item = add_portrait_item(item.get_text(0), item.get_metadata(0), new_parent, item.get_meta('previous_name'))
+
+ for child in item.get_children():
+ copy_branch_or_item(child, new_item)
+ return new_item
--- /dev/null
+@tool
+class_name DialogicCharacterPrefixSuffixSection
+extends DialogicCharacterEditorMainSection
+## Character Editor Section for setting the prefix and suffix of a character.
+##
+## loads and sets the prefix and suffix of a character.
+## Provides [const PREFIX_CUSTOM_KEY] and [const SUFFIX_CUSTOM_KEY] to
+## access the `custom_info` dictionary of the [class DialogicCharacter].
+
+@export var prefix_input: LineEdit
+@export var suffix_input: LineEdit
+
+## We won't force any prefixes or suffixes onto the player,
+## to ensure their games are working as previously when updating.
+const DEFAULT_PREFIX = ""
+const DEFAULT_SUFFIX = ""
+
+## `custom_info` dictionary keys for the prefix.
+const PREFIX_CUSTOM_KEY = "prefix"
+
+## `custom_info` dictionary keys for the prefix.
+const SUFFIX_CUSTOM_KEY = "suffix"
+
+var suffix := ""
+var prefix := ""
+
+
+func _ready() -> void:
+ suffix_input.text_changed.connect(_suffix_changed)
+ prefix_input.text_changed.connect(_prefix_changed)
+
+
+func _suffix_changed(text: String) -> void:
+ suffix = text
+
+
+func _prefix_changed(text: String) -> void:
+ prefix = text
+
+
+func _get_title() -> String:
+ return "Character Prefix & Suffix"
+
+
+func _show_title() -> bool:
+ return true
+
+
+func _start_opened() -> bool:
+ return false
+
+
+func _load_portrait_data(portrait_data: Dictionary) -> void:
+ _load_prefix_data(portrait_data)
+
+
+## We load the prefix and suffix from the character's `custom_info` dictionary.
+func _load_character(resource: DialogicCharacter) -> void:
+ _load_prefix_data(resource.custom_info)
+
+
+func _load_prefix_data(data: Dictionary) -> void:
+ suffix = data.get(SUFFIX_CUSTOM_KEY, DEFAULT_SUFFIX)
+ prefix = data.get(PREFIX_CUSTOM_KEY, DEFAULT_PREFIX)
+
+ suffix_input.text = suffix
+ prefix_input.text = prefix
+
+
+## Whenever the user makes a save to the character, we save the prefix and suffix.
+func _save_changes(character: DialogicCharacter) -> DialogicCharacter:
+ if not character.custom_info:
+ printerr("[Dialogic] Unable to save Prefix and Suffix, the character is missing.")
+ return character
+
+ character.custom_info[PREFIX_CUSTOM_KEY] = prefix
+ character.custom_info[SUFFIX_CUSTOM_KEY] = suffix
+
+ return character
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://1ctcs6ywjjtd"]
+
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="1_o3alv"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/character_prefix_suffix.gd" id="1_tkxff"]
+
+[node name="CharacterPrefixSuffix" type="GridContainer" node_paths=PackedStringArray("prefix_input", "suffix_input")]
+offset_right = 121.0
+offset_bottom = 66.0
+columns = 2
+script = ExtResource("1_tkxff")
+prefix_input = NodePath("PrefixInput")
+suffix_input = NodePath("SuffixInput")
+
+[node name="Prefix" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Prefix"]
+layout_mode = 2
+text = "Prefix"
+
+[node name="HintTooltip" parent="Prefix" instance=ExtResource("1_o3alv")]
+layout_mode = 2
+texture = null
+hint_text = "If a character speaks, this appears before their text.
+Example: Color Tags or Quotation Marks."
+
+[node name="PrefixInput" type="LineEdit" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+
+[node name="Suffix" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Suffix"]
+layout_mode = 2
+text = "Suffix"
+
+[node name="HintTooltip" parent="Suffix" instance=ExtResource("1_o3alv")]
+layout_mode = 2
+texture = null
+hint_text = "If a character speaks, this appears after their text.
+Example: Color Tags or Quotation Marks."
+
+[node name="SuffixInput" type="LineEdit" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
--- /dev/null
+@tool
+extends Control
+
+var ListItem := load("res://addons/dialogic/Editor/Common/BrowserItem.tscn")
+
+enum Types {ALL, GENERAL, PRESET}
+var current_type := Types.ALL
+var current_info := {}
+
+var portrait_scenes_info := {}
+
+signal activate_part(part_info:Dictionary)
+
+
+func _ready() -> void:
+ collect_portrait_scenes()
+
+ %Search.right_icon = get_theme_icon("Search", "EditorIcons")
+ %CloseButton.icon = get_theme_icon("Close", "EditorIcons")
+
+ get_parent().close_requested.connect(_on_close_button_pressed)
+ get_parent().visibility_changed.connect(func():if get_parent().visible: open())
+
+
+func collect_portrait_scenes() -> void:
+ for indexer in DialogicUtil.get_indexers():
+ for element in indexer._get_portrait_scene_presets():
+ portrait_scenes_info[element.get('path', '')] = element
+
+
+func open() -> void:
+ collect_portrait_scenes()
+ load_parts()
+
+
+func is_premade_portrait_scene(scene_path:String) -> bool:
+ return scene_path in portrait_scenes_info
+
+
+func load_parts() -> void:
+ for i in %PartGrid.get_children():
+ i.queue_free()
+
+ %Search.placeholder_text = "Search for "
+ %Search.text = ""
+ match current_type:
+ Types.GENERAL: %Search.placeholder_text += "general portrait scenes"
+ Types.PRESET: %Search.placeholder_text += "portrait scene presets"
+ Types.ALL: %Search.placeholder_text += "general portrait scenes and presets"
+
+ for info in portrait_scenes_info.values():
+ var type: String = info.get('type', '_')
+ if (current_type == Types.GENERAL and type != "General") or (current_type == Types.PRESET and type != "Preset"):
+ continue
+
+ var item: Node = ListItem.instantiate()
+ item.load_info(info)
+ %PartGrid.add_child(item)
+ item.set_meta('info', info)
+ item.clicked.connect(_on_item_clicked.bind(item, info))
+ item.focused.connect(_on_item_clicked.bind(item, info))
+ item.double_clicked.connect(emit_signal.bind('activate_part', info))
+
+ await get_tree().process_frame
+
+ if %PartGrid.get_child_count() > 0:
+ %PartGrid.get_child(0).clicked.emit()
+ %PartGrid.get_child(0).grab_focus()
+
+
+func _on_item_clicked(item: Node, info:Dictionary) -> void:
+ load_part_info(info)
+
+
+func load_part_info(info:Dictionary) -> void:
+ current_info = info
+ %PartTitle.text = info.get('name', 'Unknown Part')
+ %PartAuthor.text = "by "+info.get('author', 'Anonymus')
+ %PartDescription.text = info.get('description', '')
+
+ if info.get('preview_image', null) and ResourceLoader.exists(info.preview_image[0]):
+ %PreviewImage.texture = load(info.preview_image[0])
+ %PreviewImage.show()
+ else:
+ %PreviewImage.hide()
+
+ match info.type:
+ "General":
+ %ActivateButton.text = "Use this scene"
+ %TypeDescription.text = "This is a general use scene, it can be used directly."
+ "Preset":
+ %ActivateButton.text = "Customize this scene"
+ %TypeDescription.text = "This is a preset you can use for a custom portrait scene. Dialogic will promt you to save a copy of this scene that you can then use and customize."
+ "Default":
+ %ActivateButton.text = "Use default scene"
+ %TypeDescription.text = ""
+ "Custom":
+ %ActivateButton.text = "Select a custom scene"
+ %TypeDescription.text = ""
+
+ if info.get("documentation", ""):
+ %DocumentationButton.show()
+ %DocumentationButton.uri = info.documentation
+ else:
+ %DocumentationButton.hide()
+
+
+func _on_activate_button_pressed() -> void:
+ activate_part.emit(current_info)
+
+
+func _on_close_button_pressed() -> void:
+ get_parent().hide()
+
+
+func _on_search_text_changed(new_text: String) -> void:
+ for item in %PartGrid.get_children():
+ if new_text.is_empty():
+ item.show()
+ continue
+
+ if new_text.to_lower() in item.get_meta('info').name.to_lower():
+ item.show()
+ continue
+
+ item.hide()
--- /dev/null
+[gd_scene load_steps=11 format=3 uid="uid://b1wn8r84uh11b"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/CharacterEditor/portrait_scene_browser.gd" id="1_an6nc"]
+
+[sub_resource type="Gradient" id="Gradient_0o1u0"]
+colors = PackedColorArray(0.100572, 0.303996, 0.476999, 1, 0.296448, 0.231485, 0.52887, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_gxpvv"]
+gradient = SubResource("Gradient_0o1u0")
+fill = 2
+fill_from = Vector2(0.478632, 1)
+fill_to = Vector2(0, 0)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_we8bq"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.615686)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3x0xw"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+draw_center = false
+border_width_left = 3
+border_width_top = 3
+border_width_right = 3
+border_width_bottom = 3
+border_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+expand_margin_left = 2.0
+expand_margin_top = 2.0
+expand_margin_right = 2.0
+expand_margin_bottom = 2.0
+
+[sub_resource type="Image" id="Image_lwe0k"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_d2gam"]
+image = SubResource("Image_lwe0k")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lf1ht"]
+bg_color = Color(0.0588235, 0.0313726, 0.0980392, 1)
+border_width_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a5iyu"]
+bg_color = Color(1, 1, 1, 1)
+draw_center = false
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+shadow_color = Color(0.992157, 0.992157, 0.992157, 0.101961)
+shadow_size = 10
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_htwsp"]
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[node name="PortraitSceneBrowser" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_an6nc")
+
+[node name="BGColor" type="TextureRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = SubResource("GradientTexture2D_gxpvv")
+
+[node name="HSplitContainer" type="HSplitContainer" parent="."]
+clip_contents = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+
+[node name="Margin" type="MarginContainer" parent="HSplitContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 1.5
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="VBox" type="VBoxContainer" parent="HSplitContainer/Margin"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="BrowserTitle" type="Label" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+theme_override_font_sizes/font_size = 25
+text = "Dialogic Portrait Scene Browser"
+
+[node name="HBox" type="HBoxContainer" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+
+[node name="Search" type="LineEdit" parent="HSplitContainer/Margin/VBox/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/normal = SubResource("StyleBoxFlat_we8bq")
+theme_override_styles/focus = SubResource("StyleBoxFlat_3x0xw")
+placeholder_text = "Search"
+right_icon = SubResource("ImageTexture_d2gam")
+
+[node name="ScrollContainer" type="ScrollContainer" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="PartGrid" type="HFlowContainer" parent="HSplitContainer/Margin/VBox/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Buttons" type="HBoxContainer" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+alignment = 1
+
+[node name="CloseButton" type="Button" parent="HSplitContainer/Margin/VBox/Buttons"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Close"
+icon = SubResource("ImageTexture_d2gam")
+
+[node name="PanelContainer" type="PanelContainer" parent="HSplitContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_lf1ht")
+
+[node name="Control" type="Control" parent="HSplitContainer/PanelContainer"]
+layout_mode = 2
+
+[node name="Panel" type="Panel" parent="HSplitContainer/PanelContainer/Control"]
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_left = -4.0
+offset_right = 40.0
+offset_bottom = 71.0
+grow_vertical = 2
+rotation = 0.0349066
+theme_override_styles/panel = SubResource("StyleBoxFlat_lf1ht")
+
+[node name="MarginContainer" type="MarginContainer" parent="HSplitContainer/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="VBox" type="VBoxContainer" parent="HSplitContainer/PanelContainer/MarginContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Panel" type="PanelContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_a5iyu")
+
+[node name="Panel" type="PanelContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/Panel"]
+clip_children = 1
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_htwsp")
+
+[node name="PreviewImage" type="TextureRect" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/Panel/Panel"]
+unique_name_in_owner = true
+layout_mode = 2
+expand_mode = 5
+stretch_mode = 6
+
+[node name="HFlowContainer" type="HFlowContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+layout_mode = 2
+
+[node name="PartTitle" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 8
+theme_type_variation = &"DialogicTitle"
+text = "Cool Style Part"
+
+[node name="PartAuthor" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 8
+theme_type_variation = &"DialogicHintText"
+text = "by Jowan"
+
+[node name="PartType" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
+visible = false
+layout_mode = 2
+size_flags_vertical = 8
+theme_type_variation = &"DialogicHintText"
+text = "a style"
+
+[node name="PartDescription" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicHintText2"
+text = "A cool textbox layer"
+autowrap_mode = 3
+
+[node name="DocumentationButton" type="LinkButton" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Learn more"
+
+[node name="HSeparator" type="HSeparator" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+layout_mode = 2
+
+[node name="ActivateButton" type="Button" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Use"
+
+[node name="TypeDescription" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicHintText"
+text = "A cool textbox layer"
+autowrap_mode = 3
+
+[connection signal="text_changed" from="HSplitContainer/Margin/VBox/HBox/Search" to="." method="_on_search_text_changed"]
+[connection signal="pressed" from="HSplitContainer/Margin/VBox/Buttons/CloseButton" to="." method="_on_close_button_pressed"]
+[connection signal="pressed" from="HSplitContainer/PanelContainer/MarginContainer/VBox/ActivateButton" to="." method="_on_activate_button_pressed"]
--- /dev/null
+@tool
+extends Container
+
+signal clicked
+signal middle_clicked
+signal double_clicked
+signal focused
+
+var base_size := 1
+
+
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+
+ %Name.add_theme_font_override("font", get_theme_font("bold", "EditorFonts"))
+ custom_minimum_size = base_size * Vector2(200, 150) * DialogicUtil.get_editor_scale()
+ %CurrentIcon.texture = get_theme_icon("Favorites", "EditorIcons")
+ if %Image.texture == null:
+ %Image.texture = get_theme_icon("ImportFail", "EditorIcons")
+ %Image.stretch_mode = TextureRect.STRETCH_KEEP_CENTERED
+
+
+func load_info(info:Dictionary) -> void:
+ %Name.text = info.name
+ if not info.has("preview_image"):
+ pass
+ elif info.preview_image[0] == 'custom':
+ await ready
+ %Image.texture = get_theme_icon("CreateNewSceneFrom", "EditorIcons")
+ %Image.stretch_mode = TextureRect.STRETCH_KEEP_CENTERED
+ %Panel.self_modulate = get_theme_color("property_color_z", "Editor")
+ elif info.preview_image[0].ends_with('scn'):
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().get_resource_previewer().queue_resource_preview(info.preview_image[0], self, 'set_scene_preview', null)
+ elif ResourceLoader.exists(info.preview_image[0]):
+ %Image.texture = load(info.preview_image[0])
+ elif info.preview_image[0].is_valid_html_color():
+ %Image.texture = null
+ %Panel.self_modulate = Color(info.preview_image[0])
+
+ if ResourceLoader.exists(info.get('icon', '')):
+ %Icon.get_parent().show()
+ %Icon.texture = load(info.get('icon'))
+ else:
+ %Icon.get_parent().hide()
+
+ tooltip_text = info.description
+
+
+func set_scene_preview(path:String, preview:Texture2D, thumbnail:Texture2D, userdata:Variant) -> void:
+ if preview:
+ %Image.texture = preview
+ else:
+ %Image.texture = get_theme_icon("PackedScene", "EditorIcons")
+
+
+
+func set_current(current:bool):
+ %CurrentIcon.visible = current
+
+
+func _on_mouse_entered() -> void:
+ %HoverBG.show()
+
+
+func _on_mouse_exited() -> void:
+ %HoverBG.hide()
+
+
+func _on_gui_input(event):
+ if event.is_action_pressed('ui_accept') or event.is_action_pressed("ui_select") or (
+ event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT):
+ clicked.emit()
+ if not event is InputEventMouseButton or event.double_click:
+ double_clicked.emit()
+ elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
+ middle_clicked.emit()
+
+
+func _on_focus_entered() -> void:
+ $FocusFG.show()
+ focused.emit()
+
+
+func _on_focus_exited() -> void:
+ $FocusFG.hide()
--- /dev/null
+[gd_scene load_steps=6 format=3 uid="uid://ddlxjde1cx035"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/BrowserItem.gd" id="1_s3kf0"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pfw08"]
+bg_color = Color(1, 1, 1, 0.32549)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+expand_margin_left = 4.0
+expand_margin_top = 4.0
+expand_margin_right = 4.0
+expand_margin_bottom = 4.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ab24c"]
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qnehp"]
+bg_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+shadow_color = Color(0.847059, 0.847059, 0.847059, 0.384314)
+shadow_size = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nxx8t"]
+bg_color = Color(0.435294, 0.435294, 0.435294, 0.211765)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+expand_margin_left = 4.0
+expand_margin_top = 4.0
+expand_margin_right = 4.0
+expand_margin_bottom = 4.0
+
+[node name="BrowserItem" type="MarginContainer"]
+custom_minimum_size = Vector2(200, 150)
+offset_left = 1.0
+offset_top = 1.0
+offset_right = 128.0
+offset_bottom = 102.0
+size_flags_horizontal = 0
+focus_mode = 2
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 4
+script = ExtResource("1_s3kf0")
+
+[node name="HoverBG" type="Panel" parent="."]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_pfw08")
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+theme_override_constants/separation = 0
+alignment = 1
+
+[node name="Panel" type="PanelContainer" parent="VBox"]
+unique_name_in_owner = true
+self_modulate = Color(0.0705882, 0.0705882, 0.0705882, 1)
+clip_children = 2
+layout_mode = 2
+size_flags_vertical = 3
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ab24c")
+
+[node name="Image" type="TextureRect" parent="VBox/Panel"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+mouse_filter = 2
+expand_mode = 1
+stretch_mode = 6
+
+[node name="CurrentIcon" type="TextureRect" parent="VBox/Panel/Image"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -22.0
+offset_top = 5.0
+offset_right = -6.0
+offset_bottom = 21.0
+grow_horizontal = 0
+tooltip_text = "Currently in use"
+stretch_mode = 2
+
+[node name="Panel" type="Panel" parent="VBox/Panel/Image"]
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -37.0
+offset_top = -36.0
+offset_right = -7.0
+offset_bottom = -6.0
+grow_horizontal = 0
+grow_vertical = 0
+theme_override_styles/panel = SubResource("StyleBoxFlat_qnehp")
+
+[node name="Icon" type="TextureRect" parent="VBox/Panel/Image/Panel"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 4.0
+offset_top = 4.0
+offset_right = -4.0
+offset_bottom = -4.0
+grow_horizontal = 2
+grow_vertical = 2
+expand_mode = 1
+stretch_mode = 5
+
+[node name="Name" type="Label" parent="VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Dialogic Theme"
+horizontal_alignment = 1
+
+[node name="FocusFG" type="Panel" parent="."]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_nxx8t")
+
+[connection signal="focus_entered" from="." to="." method="_on_focus_entered"]
+[connection signal="focus_exited" from="." to="." method="_on_focus_exited"]
+[connection signal="gui_input" from="." to="." method="_on_gui_input"]
+[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
+[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
--- /dev/null
+@tool
+class_name DCSS
+
+static func inline(style: Dictionary) -> StyleBoxFlat:
+ var scale: float = DialogicUtil.get_editor_scale()
+ var s := StyleBoxFlat.new()
+ for property in style.keys():
+ match property:
+ 'border-left':
+ s.set('border_width_left', style[property] * scale)
+ 'border-radius':
+ var radius: float = style[property] * scale
+ s.set('corner_radius_top_left', radius)
+ s.set('corner_radius_top_right', radius)
+ s.set('corner_radius_bottom_left', radius)
+ s.set('corner_radius_bottom_right', radius)
+ 'background':
+ if typeof(style[property]) == TYPE_STRING and style[property] == "none":
+ s.set('draw_center', false)
+ else:
+ s.set('bg_color', style[property])
+ 'border':
+ var width: float = style[property] * scale
+ s.set('border_width_left', width)
+ s.set('border_width_right', width)
+ s.set('border_width_top', width)
+ s.set('border_width_bottom', width)
+ 'border-color':
+ s.set('border_color', style[property])
+ 'padding':
+ var value_v: float = 0.0
+ var value_h: float = 0.0
+ if style[property] is int:
+ value_v = style[property] * scale
+ value_h = value_v
+ else:
+ value_v = style[property][0] * scale
+ value_h = style[property][1] * scale
+ s.set('content_margin_top', value_v)
+ s.set('content_margin_bottom', value_v)
+ s.set('content_margin_left', value_h)
+ s.set('content_margin_right', value_h)
+ 'padding-right':
+ s.set('content_margin_right', style[property] * scale)
+ 'padding-left':
+ s.set('content_margin_left', style[property] * scale)
+ return s
--- /dev/null
+@tool
+extends PanelContainer
+
+
+enum Modes {EDIT, ADD}
+
+var mode := Modes.EDIT
+var item: TreeItem = null
+
+
+func _ready() -> void:
+ hide()
+ %Character.resource_icon = load("res://addons/dialogic/Editor/Images/Resources/character.svg")
+ %Character.get_suggestions_func = get_character_suggestions
+
+ %WholeWords.icon = get_theme_icon("FontItem", "EditorIcons")
+ %MatchCase.icon = get_theme_icon("MatchCase", "EditorIcons")
+
+func _on_add_pressed() -> void:
+ if visible:
+ if mode == Modes.ADD:
+ hide()
+ return
+ elif mode == Modes.EDIT:
+ save()
+
+ %AddButton.text = "Add"
+ mode = Modes.ADD
+ show()
+ %Type.selected = 0
+ _on_type_item_selected(0)
+ %Where.selected = 2
+ _on_where_item_selected(2)
+ %Old.text = ""
+ %New.text = ""
+
+
+func open_existing(_item:TreeItem, info:Dictionary):
+ mode = Modes.EDIT
+ item = _item
+ show()
+ %AddButton.text = "Update"
+ %Type.selected = info.type
+ _on_type_item_selected(info.type)
+ if !info.character_names.is_empty():
+ %Where.selected = 1
+ %Character.set_value(info.character_names[0])
+ else:
+ %Where.selected = 0
+ _on_where_item_selected(%Where.selected)
+
+ %Old.text = info.what
+ %New.text = info.forwhat
+
+func _on_type_item_selected(index:int) -> void:
+ match index:
+ 0:
+ %Where.select(0)
+ %Where.set_item_disabled(0, false)
+ %Where.set_item_disabled(1, false)
+ %Where.set_item_disabled(2, true)
+ 1:
+ %Where.select(0)
+ %Where.set_item_disabled(0, false)
+ %Where.set_item_disabled(1, false)
+ %Where.set_item_disabled(2, true)
+ 2:
+ %Where.select(1)
+ %Where.set_item_disabled(0, true)
+ %Where.set_item_disabled(1, false)
+ %Where.set_item_disabled(2, true)
+ 3,4:
+ %Where.select(0)
+ %Where.set_item_disabled(0, false)
+ %Where.set_item_disabled(1, true)
+ %Where.set_item_disabled(2, true)
+ %PureTextFlags.visible = index == 0
+ _on_where_item_selected(%Where.selected)
+
+
+func _on_where_item_selected(index:int) -> void:
+ %Character.visible = index == 1
+
+
+func get_character_suggestions(search_text:String) -> Dictionary:
+ var suggestions := {}
+
+ #override the previous _character_directory with the meta, specifically for searching otherwise new nodes wont work
+ var _character_directory := DialogicResourceUtil.get_character_directory()
+
+ var icon := load("res://addons/dialogic/Editor/Images/Resources/character.svg")
+ suggestions['(No one)'] = {'value':null, 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
+
+ for resource in _character_directory.keys():
+ suggestions[resource] = {
+ 'value' : resource,
+ 'tooltip' : _character_directory[resource],
+ 'icon' : icon.duplicate()}
+ return suggestions
+
+
+func save() -> void:
+ if %Old.text.is_empty() or %New.text.is_empty():
+ return
+ if %Where.selected == 1 and %Character.current_value == null:
+ return
+
+ var previous := {}
+ if mode == Modes.EDIT:
+ previous = item.get_metadata(0)
+ item.get_parent()
+ item.free()
+
+ var ref_manager := find_parent('ReferenceManager')
+ var character_names := []
+ if %Character.current_value != null:
+ character_names = [%Character.current_value]
+ ref_manager.add_ref_change(%Old.text, %New.text, %Type.selected, %Where.selected, character_names, %WholeWords.button_pressed, %MatchCase.button_pressed, previous)
+ hide()
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://dmsjhgv22dns8"]
+
+[resource]
+content_margin_left = 5.0
+content_margin_top = 5.0
+content_margin_right = 5.0
+content_margin_bottom = 5.0
+bg_color = Color(0.545098, 0.545098, 0.545098, 0.211765)
--- /dev/null
+@tool
+extends VSplitContainer
+
+## This manager shows a list of changed references and allows searching for them and replacing them.
+
+var reference_changes: Array[Dictionary] = []:
+ set(changes):
+ reference_changes = changes
+ update_indicator()
+
+var search_regexes: Array[Array]
+var finder_thread: Thread
+var progress_mutex: Mutex
+var progress_percent: float = 0.0
+var progress_message: String = ""
+
+
+func _ready() -> void:
+ if owner.get_parent() is SubViewport:
+ return
+
+ %TabA.text = "Broken References"
+ %TabA.icon = get_theme_icon("Unlinked", "EditorIcons")
+
+ owner.get_parent().visibility_changed.connect(func(): if is_visible_in_tree(): open())
+
+ %ReplacementSection.hide()
+
+ %CheckButton.icon = get_theme_icon("Search", "EditorIcons")
+ %Replace.icon = get_theme_icon("ArrowRight", "EditorIcons")
+
+ %State.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
+ visibility_changed.connect(func(): if !visible: close())
+ await get_parent().ready
+
+ var tab_button: Control = %TabA
+ var dot := Sprite2D.new()
+ dot.texture = get_theme_icon("GuiGraphNodePort", "EditorIcons")
+ dot.scale = Vector2(0.8, 0.8)
+ dot.z_index = 10
+ dot.position = Vector2(tab_button.size.x, tab_button.size.y*0.25)
+ dot.modulate = get_theme_color("warning_color", "Editor").lightened(0.5)
+
+ tab_button.add_child(dot)
+ update_indicator()
+
+
+func open() -> void:
+ %ReplacementEditPanel.hide()
+ %ReplacementSection.hide()
+ %ChangeTree.clear()
+ %ChangeTree.create_item()
+ %ChangeTree.set_column_expand(0, false)
+ %ChangeTree.set_column_expand(2, false)
+ %ChangeTree.set_column_custom_minimum_width(2, 50)
+ var categories := {null:%ChangeTree.get_root()}
+ for i in reference_changes:
+ var parent: TreeItem = null
+ if !i.get('category', null) in categories:
+ parent = %ChangeTree.create_item()
+ parent.set_text(1, i.category)
+ parent.set_custom_color(1, get_theme_color("disabled_font_color", "Editor"))
+ categories[i.category] = parent
+ else:
+ parent = categories[i.get('category')]
+
+ var item: TreeItem = %ChangeTree.create_item(parent)
+ item.set_text(1, i.what+" -> "+i.forwhat)
+ item.add_button(1, get_theme_icon("Edit", "EditorIcons"), 1, false, 'Edit')
+ item.add_button(1, get_theme_icon("Remove", "EditorIcons"), 0, false, 'Remove Change from List')
+ item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
+ item.set_checked(0, true)
+ item.set_editable(0, true)
+ item.set_metadata(0, i)
+ %CheckButton.disabled = reference_changes.is_empty()
+
+
+func _on_change_tree_button_clicked(item:TreeItem, column:int, id:int, mouse_button_index:int) -> void:
+ if id == 0:
+ reference_changes.erase(item.get_metadata(0))
+ if item.get_parent().get_child_count() == 1:
+ item.get_parent().free()
+ else:
+ item.free()
+ update_indicator()
+ %CheckButton.disabled = reference_changes.is_empty()
+
+ if id == 1:
+ %ReplacementEditPanel.open_existing(item, item.get_metadata(0))
+
+ %ReplacementSection.hide()
+
+
+func _on_change_tree_item_edited() -> void:
+ if !%ChangeTree.get_selected():
+ return
+ %CheckButton.disabled = false
+
+
+func _on_check_button_pressed() -> void:
+ var to_be_checked: Array[Dictionary]= []
+ var item: TreeItem = %ChangeTree.get_root()
+ while item.get_next_visible():
+ item = item.get_next_visible()
+
+ if item.get_child_count():
+ continue
+
+ if item.is_checked(0):
+ to_be_checked.append(item.get_metadata(0))
+ to_be_checked[-1]['item'] = item
+ to_be_checked[-1]['count'] = 0
+
+ open_finder(to_be_checked)
+ %CheckButton.disabled = true
+
+
+func open_finder(replacements:Array[Dictionary]) -> void:
+ %ReplacementSection.show()
+ %Progress.show()
+ %ReferenceTree.hide()
+
+ search_regexes = []
+ for i in replacements:
+ if i.has('character_names') and !i.character_names.is_empty():
+ i['character_regex'] = RegEx.create_from_string("(?m)^(join|update|leave)?\\s*("+str(i.character_names).replace('"', '').replace(', ', '|').trim_suffix(']').trim_prefix('[').replace('/', '\\/')+")(?(1).*|.*:)")
+
+ for regex_string in i.regex:
+ var regex := RegEx.create_from_string(regex_string)
+ search_regexes.append([regex, i])
+
+ finder_thread = Thread.new()
+ progress_mutex = Mutex.new()
+ finder_thread.start(search_timelines.bind(search_regexes))
+
+
+func _process(delta: float) -> void:
+ if finder_thread and finder_thread.is_started():
+ if finder_thread.is_alive():
+ progress_mutex.lock()
+ %State.text = progress_message
+ %Progress.value = progress_percent
+ progress_mutex.unlock()
+ else:
+ var finds: Variant = finder_thread.wait_to_finish()
+ display_search_results(finds)
+
+
+
+func display_search_results(finds:Array[Dictionary]) -> void:
+ %Progress.hide()
+ %ReferenceTree.show()
+ for regex_info in search_regexes:
+ regex_info[1]['item'].set_text(2, str(regex_info[1]['count']))
+
+ update_count_coloring()
+ %State.text = str(len(finds))+ " occurrences found"
+
+ %ReferenceTree.clear()
+ %ReferenceTree.set_column_expand(0, false)
+ %ReferenceTree.create_item()
+
+ var timelines := {}
+ var height := 0
+ for i in finds:
+ var parent: TreeItem = null
+ if !i.timeline in timelines:
+ parent = %ReferenceTree.create_item()
+ parent.set_text(1, i.timeline)
+ parent.set_custom_color(1, get_theme_color("disabled_font_color", "Editor"))
+ timelines[i.timeline] = parent
+ height += %ReferenceTree.get_item_area_rect(parent).size.y+10
+ else:
+ parent = timelines[i.timeline]
+
+ var item: TreeItem = %ReferenceTree.create_item(parent)
+ item.set_text(1, 'Line '+str(i.line_number)+': '+i.line)
+ item.set_tooltip_text(1, i.info.what+' -> '+i.info.forwhat)
+ item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
+ item.set_checked(0, true)
+ item.set_editable(0, true)
+ item.set_metadata(0, i)
+ height += %ReferenceTree.get_item_area_rect(item).size.y+10
+ var change_item: TreeItem = i.info.item
+ change_item.set_meta('found_items', change_item.get_meta('found_items', [])+[item])
+
+ %ReferenceTree.custom_minimum_size.y = min(height, 200)
+
+ %ReferenceTree.visible = !finds.is_empty()
+ %Replace.disabled = finds.is_empty()
+ if finds.is_empty():
+ %State.text = "Nothing found"
+ else:
+ %Replace.grab_focus()
+
+
+func search_timelines(regexes:Array[Array]) -> Array[Dictionary]:
+ var finds: Array[Dictionary] = []
+
+ var timeline_paths := DialogicResourceUtil.list_resources_of_type('.dtl')
+
+ var progress := 0
+ var progress_max: float = len(timeline_paths)*len(regexes)
+
+ for timeline_path:String in timeline_paths:
+
+ var timeline_file := FileAccess.open(timeline_path, FileAccess.READ)
+ var timeline_text: String = timeline_file.get_as_text()
+ var timeline_event: PackedStringArray = timeline_text.split('\n')
+ timeline_file.close()
+
+ for regex_info in regexes:
+ progress += 1
+ progress_mutex.lock()
+ progress_percent = 1/progress_max*progress
+ progress_message = "Searching '"+timeline_path+"' for "+regex_info[1].what+' -> '+regex_info[1].forwhat
+ progress_mutex.unlock()
+ for i in regex_info[0].search_all(timeline_text):
+ if regex_info[1].has('character_regex'):
+ if regex_info[1].character_regex.search(get_line(timeline_text, i.get_start()+1)) == null:
+ continue
+
+ var line_number := timeline_text.count('\n', 0, i.get_start()+1)+1
+ var line := timeline_text.get_slice('\n', line_number-1)
+ finds.append({
+ 'match':i,
+ 'timeline':timeline_path,
+ 'info': regex_info[1],
+ 'line_number': line_number,
+ 'line': line,
+ 'line_start': timeline_text.rfind('\n', i.get_start())
+ })
+ regex_info[1]['count'] += 1
+ return finds
+
+
+func _exit_tree() -> void:
+ # Shutting of
+ if finder_thread and finder_thread.is_alive():
+ finder_thread.wait_to_finish()
+
+
+func get_line(string:String, at_index:int) -> String:
+ return string.substr(max(string.rfind('\n', at_index), 0), string.find('\n', at_index)-string.rfind('\n', at_index))
+
+
+func update_count_coloring() -> void:
+ var item: TreeItem = %ChangeTree.get_root()
+ while item.get_next_visible():
+ item = item.get_next_visible()
+
+ if item.get_child_count():
+ continue
+ if int(item.get_text(2)) > 0:
+ item.set_custom_bg_color(1, get_theme_color("warning_color", "Editor").darkened(0.8))
+ item.set_custom_color(1, get_theme_color("warning_color", "Editor"))
+ item.set_custom_color(2, get_theme_color("warning_color", "Editor"))
+ else:
+ item.set_custom_color(2, get_theme_color("success_color", "Editor"))
+ item.set_custom_color(1, get_theme_color("readonly_font_color", "Editor"))
+ if item.get_button_count(1):
+ item.erase_button(1, 1)
+ item.add_button(1, get_theme_icon("Eraser", "EditorIcons"), -1, true, "This reference was not found anywhere and will be removed from this list.")
+
+
+func _on_replace_pressed() -> void:
+ var to_be_replaced: Array[Dictionary]= []
+ var item: TreeItem = %ReferenceTree.get_root()
+ var affected_timelines: Array[String]= []
+
+ while item.get_next_visible():
+ item = item.get_next_visible()
+
+ if item.get_child_count():
+ continue
+
+ if item.is_checked(0):
+ to_be_replaced.append(item.get_metadata(0))
+ to_be_replaced[-1]['f_item'] = item
+ if !item.get_metadata(0).timeline in affected_timelines:
+ affected_timelines.append(item.get_metadata(0).timeline)
+ replace(affected_timelines, to_be_replaced)
+
+
+func replace(timelines:Array[String], replacement_info:Array[Dictionary]) -> void:
+ var reopen_timeline := ""
+ var timeline_editor: DialogicEditor = find_parent('EditorView').editors_manager.editors['Timeline'].node
+ if timeline_editor.current_resource != null and timeline_editor.current_resource.resource_path in timelines:
+ reopen_timeline = timeline_editor.current_resource.resource_path
+ find_parent('EditorView').editors_manager.clear_editor(timeline_editor)
+
+ replacement_info.sort_custom(func(a,b): return a.match.get_start() < b.match.get_start())
+
+ for timeline_path in timelines:
+ %State.text = "Loading '"+timeline_path+"'"
+
+ var timeline_file := FileAccess.open(timeline_path, FileAccess.READ_WRITE)
+ var timeline_text: String = timeline_file.get_as_text()
+ var timeline_events := timeline_text.split('\n')
+ timeline_file.close()
+
+ var idx := 1
+ var offset_correction := 0
+ for replacement in replacement_info:
+ if replacement.timeline != timeline_path:
+ continue
+
+ %State.text = "Replacing in '"+timeline_path + "' ("+str(idx)+"/"+str(len(replacement_info))+")"
+ var group := 'replace'
+ if not 'replace' in replacement.match.names:
+ group = ''
+
+
+ timeline_text = timeline_text.substr(0, replacement.match.get_start(group) + offset_correction) + \
+ replacement.info.regex_replacement + \
+ timeline_text.substr(replacement.match.get_end(group) + offset_correction)
+ offset_correction += len(replacement.info.regex_replacement)-len(replacement.match.get_string(group))
+
+ replacement.info.count -= 1
+ replacement.info.item.set_text(2, str(replacement.info.count))
+ replacement.f_item.set_custom_bg_color(1, get_theme_color("success_color", "Editor").darkened(0.8))
+
+ timeline_file = FileAccess.open(timeline_path, FileAccess.WRITE)
+ timeline_file.store_string(timeline_text.strip_edges(false, true))
+ timeline_file.close()
+
+ if ResourceLoader.has_cached(timeline_path):
+ var tml := load(timeline_path)
+ tml.from_text(timeline_text)
+
+ if !reopen_timeline.is_empty():
+ find_parent('EditorView').editors_manager.edit_resource(load(reopen_timeline), false, true)
+
+ update_count_coloring()
+
+ %Replace.disabled = true
+ %CheckButton.disabled = false
+ %State.text = "Done Replacing"
+
+
+func update_indicator() -> void:
+ %TabA.get_child(0).visible = !reference_changes.is_empty()
+
+
+func close() -> void:
+ var item: TreeItem = %ChangeTree.get_root()
+ if item:
+ while item.get_next_visible():
+ item = item.get_next_visible()
+
+ if item.get_child_count():
+ continue
+ if item.get_text(2) != "" and int(item.get_text(2)) == 0:
+ reference_changes.erase(item.get_metadata(0))
+ for i in reference_changes:
+ i.item = null
+ DialogicUtil.set_editor_setting('reference_changes', reference_changes)
+ update_indicator()
+ find_parent("ReferenceManager").update_indicator()
+
+
+func _on_add_button_pressed() -> void:
+ %ReplacementEditPanel._on_add_pressed()
--- /dev/null
+@tool
+extends TextureRect
+
+@export_multiline var hint_text := ""
+
+func _ready() -> void:
+ if owner and owner.get_parent() is SubViewport:
+ texture = null
+ return
+ texture = get_theme_icon("NodeInfo", "EditorIcons")
+ modulate = get_theme_color("contrast_color_1", "Editor")
+ tooltip_text = hint_text
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://dbpkta2tjsqim"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.gd" id="1_x8t45"]
+
+[sub_resource type="Image" id="Image_eiyxd"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_lseut"]
+image = SubResource("Image_eiyxd")
+
+[node name="HintTooltip" type="TextureRect"]
+modulate = Color(0, 0, 0, 1)
+texture = SubResource("ImageTexture_lseut")
+stretch_mode = 3
+script = ExtResource("1_x8t45")
--- /dev/null
+@tool
+extends PanelContainer
+
+
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+
+ add_theme_stylebox_override("panel", get_theme_stylebox("Background", "EditorStyles"))
+ $Tabs/Close.icon = get_theme_icon("Close", "EditorIcons")
+
+ for tab in $Tabs/Tabs.get_children():
+ tab.add_theme_color_override("font_selected_color", get_theme_color("accent_color", "Editor"))
+ tab.add_theme_font_override("font", get_theme_font("main", "EditorFonts"))
+ tab.toggled.connect(tab_changed.bind(tab.get_index()+1))
+
+
+func tab_changed(enabled:bool, index:int) -> void:
+ for child in $Tabs.get_children():
+ if child.get_index() == 0 or child.get_index() == index or child is Button:
+ child.show()
+ if child.get_index() == index:
+ child.open()
+ else:
+ if child.visible:
+ child.close()
+ child.hide()
+ for child in $Tabs/Tabs.get_children():
+ child.set_pressed_no_signal(index-1 == child.get_index())
+
+
+func open() -> void:
+ show()
+ $Tabs/BrokenReferences.update_indicator()
+
+
+func _on_close_pressed() -> void:
+ get_parent()._on_close_requested()
--- /dev/null
+[gd_scene load_steps=13 format=3 uid="uid://c7lmt5cp7bxcm"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/reference_manager.gd" id="1_3t531"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/broken_reference_manager.gd" id="1_agmg4"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/ReferenceManager_AddReplacementPanel.gd" id="2_tt4jd"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3_yomsc"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/unique_identifiers_manager.gd" id="5_wnvbq"]
+
+[sub_resource type="ButtonGroup" id="ButtonGroup_l6uiy"]
+
+[sub_resource type="Image" id="Image_36731"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_a0gfq"]
+image = SubResource("Image_36731")
+
+[sub_resource type="Image" id="Image_0rvkq"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_mr0fw"]
+image = SubResource("Image_0rvkq")
+
+[sub_resource type="Image" id="Image_5fmdt"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_lce2m"]
+image = SubResource("Image_5fmdt")
+
+[node name="Manager" type="PanelContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_3t531")
+
+[node name="Tabs" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Tabs" type="HBoxContainer" parent="Tabs"]
+layout_mode = 2
+alignment = 1
+
+[node name="TabA" type="Button" parent="Tabs/Tabs"]
+unique_name_in_owner = true
+layout_mode = 2
+toggle_mode = true
+button_pressed = true
+text = "Broken References"
+flat = true
+
+[node name="TabB" type="Button" parent="Tabs/Tabs"]
+unique_name_in_owner = true
+layout_mode = 2
+toggle_mode = true
+button_group = SubResource("ButtonGroup_l6uiy")
+text = "Unique Identifiers"
+flat = true
+
+[node name="BrokenReferences" type="VSplitContainer" parent="Tabs"]
+layout_mode = 2
+size_flags_vertical = 3
+script = ExtResource("1_agmg4")
+
+[node name="ChangesList" type="PanelContainer" parent="Tabs/BrokenReferences"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelA"
+
+[node name="VBox" type="VBoxContainer" parent="Tabs/BrokenReferences/ChangesList"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox"]
+layout_mode = 2
+
+[node name="SectionTitle" type="Label" parent="Tabs/BrokenReferences/ChangesList/VBox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Recent renames"
+
+[node name="AddButton" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 2
+tooltip_text = "Add custom rename"
+icon = SubResource("ImageTexture_a0gfq")
+
+[node name="ReplacementEditPanel" type="PanelContainer" parent="Tabs/BrokenReferences/ChangesList/VBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+script = ExtResource("2_tt4jd")
+
+[node name="VBox" type="HFlowContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel"]
+layout_mode = 2
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox"]
+layout_mode = 2
+
+[node name="Type" type="OptionButton" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer3"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "This decides the regexes for searching. Pure text allows you to enter your own regex into into \"Old\". "
+item_count = 5
+selected = 0
+popup/item_0/text = "Pure Text"
+popup/item_0/id = 0
+popup/item_1/text = "Variable"
+popup/item_1/id = 1
+popup/item_2/text = "Portrait"
+popup/item_2/id = 2
+popup/item_3/text = "Character (Ref)"
+popup/item_3/id = 3
+popup/item_4/text = "Timeline (Ref)"
+popup/item_4/id = 4
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Old" type="LineEdit" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Old"
+
+[node name="Label2" type="Label" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer"]
+layout_mode = 2
+text = "->"
+
+[node name="New" type="LineEdit" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "New"
+
+[node name="PureTextFlags" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 2
+
+[node name="MatchCase" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/PureTextFlags"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Match Case"
+toggle_mode = true
+icon = SubResource("ImageTexture_mr0fw")
+
+[node name="WholeWords" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/PureTextFlags"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Whole World"
+toggle_mode = true
+icon = SubResource("ImageTexture_mr0fw")
+
+[node name="HBoxContainer4" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox"]
+layout_mode = 2
+
+[node name="Where" type="OptionButton" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 3
+selected = 0
+fit_to_longest_item = false
+popup/item_0/text = "Everywhere"
+popup/item_0/id = 0
+popup/item_1/text = "Only for Character"
+popup/item_1/id = 1
+popup/item_2/text = "Texts only"
+popup/item_2/id = 2
+
+[node name="Character" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer4" instance=ExtResource("3_yomsc")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="AddButton" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Add/Save"
+
+[node name="ChangeTree" type="Tree" parent="Tabs/BrokenReferences/ChangesList/VBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/draw_relationship_lines = 1
+columns = 3
+hide_root = true
+
+[node name="CheckButton" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+tooltip_text = "Search timelines for occurences of these renames"
+text = "Check Selected"
+icon = SubResource("ImageTexture_lce2m")
+
+[node name="ReplacementSection" type="PanelContainer" parent="Tabs/BrokenReferences"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicPanelA"
+
+[node name="FindList" type="VBoxContainer" parent="Tabs/BrokenReferences/ReplacementSection"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="HBox" type="HBoxContainer" parent="Tabs/BrokenReferences/ReplacementSection/FindList"]
+layout_mode = 2
+
+[node name="SectionTitle2" type="Label" parent="Tabs/BrokenReferences/ReplacementSection/FindList/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Found references"
+
+[node name="State" type="Label" parent="Tabs/BrokenReferences/ReplacementSection/FindList/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 8
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "State"
+
+[node name="ReferenceTree" type="Tree" parent="Tabs/BrokenReferences/ReplacementSection/FindList"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/draw_relationship_lines = 1
+columns = 2
+hide_root = true
+
+[node name="Progress" type="ProgressBar" parent="Tabs/BrokenReferences/ReplacementSection/FindList"]
+unique_name_in_owner = true
+layout_mode = 2
+max_value = 1.0
+
+[node name="Replace" type="Button" parent="Tabs/BrokenReferences/ReplacementSection/FindList"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+tooltip_text = "Replace all selected findings (Careful, no undo!)"
+text = "Replace Selected"
+icon = SubResource("ImageTexture_lce2m")
+
+[node name="UniqueIdentifiers" type="PanelContainer" parent="Tabs"]
+visible = false
+layout_mode = 2
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelA"
+script = ExtResource("5_wnvbq")
+
+[node name="VBox" type="VBoxContainer" parent="Tabs/UniqueIdentifiers"]
+layout_mode = 2
+
+[node name="Tools" type="HBoxContainer" parent="Tabs/UniqueIdentifiers/VBox"]
+layout_mode = 2
+alignment = 1
+
+[node name="Search" type="LineEdit" parent="Tabs/UniqueIdentifiers/VBox/Tools"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(400, 0)
+layout_mode = 2
+placeholder_text = "Search"
+
+[node name="IdentifierTable" type="Tree" parent="Tabs/UniqueIdentifiers/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+columns = 2
+column_titles_visible = true
+hide_root = true
+
+[node name="RenameNotification" type="Label" parent="Tabs/UniqueIdentifiers/VBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+text = "You've renamed some identifier(s)! Use the \"Broken References\" tab to check if you have used this identifier (and fix it if so)."
+autowrap_mode = 3
+
+[node name="Close" type="Button" parent="Tabs"]
+layout_mode = 2
+size_flags_horizontal = 4
+text = "Close"
+
+[node name="HelpButton" type="LinkButton" parent="."]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 0
+text = "Documentation"
+uri = "https://docs.dialogic.pro/reference-manager.html"
+
+[connection signal="pressed" from="Tabs/BrokenReferences/ChangesList/VBox/HBoxContainer/AddButton" to="Tabs/BrokenReferences" method="_on_add_button_pressed"]
+[connection signal="item_selected" from="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer3/Type" to="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" method="_on_type_item_selected"]
+[connection signal="item_selected" from="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer4/Where" to="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" method="_on_where_item_selected"]
+[connection signal="pressed" from="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/AddButton" to="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" method="save"]
+[connection signal="button_clicked" from="Tabs/BrokenReferences/ChangesList/VBox/ChangeTree" to="Tabs/BrokenReferences" method="_on_change_tree_button_clicked"]
+[connection signal="item_edited" from="Tabs/BrokenReferences/ChangesList/VBox/ChangeTree" to="Tabs/BrokenReferences" method="_on_change_tree_item_edited"]
+[connection signal="pressed" from="Tabs/BrokenReferences/ChangesList/VBox/CheckButton" to="Tabs/BrokenReferences" method="_on_check_button_pressed"]
+[connection signal="pressed" from="Tabs/BrokenReferences/ReplacementSection/FindList/Replace" to="Tabs/BrokenReferences" method="_on_replace_pressed"]
+[connection signal="text_changed" from="Tabs/UniqueIdentifiers/VBox/Tools/Search" to="Tabs/UniqueIdentifiers" method="_on_search_text_changed"]
+[connection signal="button_clicked" from="Tabs/UniqueIdentifiers/VBox/IdentifierTable" to="Tabs/UniqueIdentifiers" method="_on_identifier_table_button_clicked"]
+[connection signal="item_edited" from="Tabs/UniqueIdentifiers/VBox/IdentifierTable" to="Tabs/UniqueIdentifiers" method="_on_identifier_table_item_edited"]
+[connection signal="pressed" from="Tabs/Close" to="." method="_on_close_pressed"]
--- /dev/null
+@tool
+extends Window
+
+## This window manages communication with the replacement manager it contains.
+## Other scripts can call the add_ref_change() method to register changes directly
+## or use the helpers add_variable_ref_change() and add_portrait_ref_change()
+
+@onready var editors_manager := get_node("../EditorsManager")
+@onready var broken_manager := get_node("Manager/Tabs/BrokenReferences")
+enum Where {EVERYWHERE, BY_CHARACTER, TEXTS_ONLY}
+enum Types {TEXT, VARIABLE, PORTRAIT, CHARACTER_NAME, TIMELINE_NAME}
+
+var icon_button: Button = null
+
+
+func _ready() -> void:
+ if owner.get_parent() is SubViewport:
+ return
+
+ $Manager.theme = owner.get_theme()
+
+ icon_button = editors_manager.add_icon_button(get_theme_icon("Unlinked", "EditorIcons"), 'Reference Manager')
+ icon_button.pressed.connect(open)
+
+ var dot := Sprite2D.new()
+ dot.texture = get_theme_icon("GuiGraphNodePort", "EditorIcons")
+ dot.scale = Vector2(0.8, 0.8)
+ dot.z_index = 10
+ dot.position = Vector2(icon_button.size.x*0.8, icon_button.size.x*0.2)
+ dot.modulate = get_theme_color("warning_color", "Editor").lightened(0.5)
+
+ icon_button.add_child(dot)
+
+ var old_changes: Array = DialogicUtil.get_editor_setting('reference_changes', [])
+ if !old_changes.is_empty():
+ broken_manager.reference_changes = old_changes
+
+ update_indicator()
+
+ hide()
+
+ get_parent().plugin_reference.get_editor_interface().get_file_system_dock().files_moved.connect(_on_file_moved)
+ get_parent().plugin_reference.get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed)
+ get_parent().get_node('ResourceRenameWarning').confirmed.connect(open)
+
+
+func add_ref_change(old_name:String, new_name:String, type:Types, where:=Where.TEXTS_ONLY, character_names:=[],
+ whole_words:=false, case_sensitive:=false, previous:Dictionary = {}) -> void:
+ var regexes := []
+ var category_name := ""
+ match type:
+ Types.TEXT:
+ category_name = "Texts"
+ if '<replace>' in old_name:
+ regexes = [old_name]
+ else:
+ regexes = [
+ r'(?<replace>%s)' % old_name.replace('/', '\\/')
+ ]
+ if !case_sensitive:
+ regexes[0] = '(?i)'+regexes[0]
+ if whole_words:
+ regexes = ['\\b'+regexes[0]+'\\b']
+
+ Types.VARIABLE:
+ regexes = [
+ r'{(?<replace>\s*%s\s*)}' % old_name.replace("/", "\\/"),
+ r'var\s*=\s*"(?<replace>\s*%s\s*)"' % old_name.replace("/", "\\/")
+ ]
+ category_name = "Variables"
+
+ Types.PORTRAIT:
+ regexes = [
+ r'(?m)^[^:(\n]*\((?<replace>%s)\)' % old_name.replace('/', '\\/'),
+ r'\[\s*portrait\s*=(?<replace>\s*%s\s*)\]' % old_name.replace('/', '\\/')
+ ]
+ category_name = "Portraits by "+character_names[0]
+
+ Types.CHARACTER_NAME:
+ # for reference: ((join|leave|update) )?(?<replace>NAME)(?!\B)(?(1)|(?!([^:\n]|\\:)*(\n|$)))
+ regexes = [
+ r'((join|leave|update) )?(?<replace>%s)(?!\B)(?(1)|(?!([^:\n]|\\:)*(\n|$)))' % old_name
+ ]
+ category_name = "Renamed Character Files"
+
+ Types.TIMELINE_NAME:
+ regexes = [
+ r'timeline ?= ?" ?(?<replace>%s) ?"' % old_name
+ ]
+ category_name = "Renamed Timeline Files"
+
+ if where != Where.BY_CHARACTER:
+ character_names = []
+
+ # previous is only given when an existing item is edited
+ # in that case the old one is removed first
+ var idx := len(broken_manager.reference_changes)
+ if previous in broken_manager.reference_changes:
+ idx = broken_manager.reference_changes.find(previous)
+ broken_manager.reference_changes.erase(previous)
+
+ if _check_for_ref_change_cycle(old_name, new_name, category_name):
+ update_indicator()
+ return
+
+ broken_manager.reference_changes.insert(idx,
+ {'what':old_name,
+ 'forwhat':new_name,
+ 'regex': regexes,
+ 'regex_replacement':new_name,
+ 'category':category_name,
+ 'character_names':character_names,
+ 'texts_only':where == Where.TEXTS_ONLY,
+ 'type':type
+ })
+
+ update_indicator()
+
+ if visible:
+ $Manager.open()
+ broken_manager.open()
+
+
+## Checks for reference cycles or chains.
+## E.g. if you first rename a portrait from "happy" to "happy1" and then to "Happy/happy1"
+## This will make sure only a change "happy" -> "Happy/happy1" is remembered
+## This is very important for correct replacement
+func _check_for_ref_change_cycle(old_name:String, new_name:String, category:String) -> bool:
+ for ref in broken_manager.reference_changes:
+ if ref['forwhat'] == old_name and ref['category'] == category:
+ if new_name == ref['what']:
+ broken_manager.reference_changes.erase(ref)
+ else:
+ broken_manager.reference_changes[broken_manager.reference_changes.find(ref)]['forwhat'] = new_name
+ broken_manager.reference_changes[broken_manager.reference_changes.find(ref)]['regex_replacement'] = new_name
+ return true
+ return false
+
+
+## Helper for adding variable ref changes
+func add_variable_ref_change(old_name:String, new_name:String) -> void:
+ add_ref_change(old_name, new_name, Types.VARIABLE, Where.EVERYWHERE)
+
+
+## Helper for adding portrait ref changes
+func add_portrait_ref_change(old_name:String, new_name:String, character_names:PackedStringArray) -> void:
+ add_ref_change(old_name, new_name, Types.PORTRAIT, Where.BY_CHARACTER, character_names)
+
+
+## Helper for adding character name ref changes
+func add_character_name_ref_change(old_name:String, new_name:String) -> void:
+ add_ref_change(old_name, new_name, Types.CHARACTER_NAME, Where.EVERYWHERE)
+
+
+## Helper for adding timeline name ref changes
+func add_timeline_name_ref_change(old_name:String, new_name:String) -> void:
+ add_ref_change(old_name, new_name, Types.TIMELINE_NAME, Where.EVERYWHERE)
+
+
+func open() -> void:
+ DialogicResourceUtil.update_directory('dch')
+ DialogicResourceUtil.update_directory('dtl')
+ popup_centered_ratio(0.5)
+ move_to_foreground()
+ grab_focus()
+
+
+func _on_close_requested() -> void:
+ hide()
+ broken_manager.close()
+
+
+func update_indicator() -> void:
+ icon_button.get_child(0).visible = !broken_manager.reference_changes.is_empty()
+
+
+## FILE MANAGEMENT:
+func _on_file_moved(old_file:String, new_file:String) -> void:
+ if old_file.ends_with('.dch') and new_file.ends_with('.dch'):
+ DialogicResourceUtil.change_resource_path(old_file, new_file)
+ if old_file.get_file() != new_file.get_file():
+ get_parent().get_node('ResourceRenameWarning').popup_centered()
+ elif old_file.ends_with('.dtl') and new_file.ends_with('.dtl'):
+ DialogicResourceUtil.change_resource_path(old_file, new_file)
+ if old_file.get_file() != new_file.get_file():
+ get_parent().get_node('ResourceRenameWarning').popup_centered()
+
+
+func _on_file_removed(file:String) -> void:
+ if file.get_extension() in ['dch', 'dtl']:
+ DialogicResourceUtil.remove_resource(file)
--- /dev/null
+[gd_scene load_steps=7 format=3 uid="uid://cwe3r2tbh2og1"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/sidebar.gd" id="1_jnq65"]
+[ext_resource type="Texture2D" uid="uid://bff65e82555qr" path="res://addons/dialogic/Editor/Images/Pieces/close-icon.svg" id="2_54pks"]
+[ext_resource type="Texture2D" uid="uid://dx3o2ild56i76" path="res://addons/dialogic/Editor/Images/Pieces/closed-icon.svg" id="2_ilyps"]
+
+[sub_resource type="Theme" id="Theme_pn0f4"]
+VBoxContainer/constants/separation = 4
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_gxwm6"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_n8rql"]
+
+[node name="SideBar" type="VSplitContainer"]
+custom_minimum_size = Vector2(100, 130)
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = SubResource("Theme_pn0f4")
+split_offset = 100
+script = ExtResource("1_jnq65")
+
+[node name="VBoxHidden" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+
+[node name="OpenButton" type="Button" parent="VBoxHidden"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 3
+tooltip_text = "Show Sidebar"
+theme_override_constants/icon_max_width = 20
+icon = ExtResource("2_ilyps")
+flat = true
+icon_alignment = 1
+
+[node name="VBoxPrimary" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Margin" type="MarginContainer" parent="VBoxPrimary"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MainVSplit" type="VSplitContainer" parent="VBoxPrimary/Margin"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="VBox" type="VBoxContainer" parent="VBoxPrimary/Margin/MainVSplit"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Logo" type="TextureRect" parent="VBoxPrimary/Margin/MainVSplit/VBox"]
+unique_name_in_owner = true
+modulate = Color(1, 1, 1, 0.623529)
+texture_filter = 6
+custom_minimum_size = Vector2(0, 25)
+layout_mode = 2
+expand_mode = 3
+stretch_mode = 4
+
+[node name="HBox" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox"]
+layout_mode = 2
+
+[node name="CurrentResource" type="LineEdit" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "No resource"
+alignment = 1
+editable = false
+
+[node name="CloseButton" type="Button" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Hide Sidebar"
+text = " "
+icon = ExtResource("2_54pks")
+flat = true
+icon_alignment = 1
+expand_icon = true
+
+[node name="HBoxSearchSort" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox"]
+layout_mode = 2
+
+[node name="Search" type="LineEdit" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Filter Resources"
+placeholder_text = "Filter Resources"
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="Options" type="Button" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="OptionsPopup" type="Popup" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options"]
+unique_name_in_owner = true
+transparent_bg = true
+position = Vector2i(890, 65)
+size = Vector2i(165, 101)
+visible = true
+transparent = true
+
+[node name="OptionsPanel" type="PanelContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup"]
+unique_name_in_owner = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBox" type="VBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup/OptionsPanel"]
+layout_mode = 2
+
+[node name="GroupingOptions" type="OptionButton" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup/OptionsPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Grouping
+- None: No Grouping, sorted alphabetically
+- Type: Group by type (Characters/Timeilnes)
+- Folder: Group based on the parent folder name.
+- Path: Group based on folders."
+text_overrun_behavior = 1
+clip_text = true
+selected = 0
+item_count = 4
+popup/item_0/text = "No Grouping"
+popup/item_1/text = "Type Grouping"
+popup/item_1/id = 1
+popup/item_2/text = "Folder Grouping"
+popup/item_2/id = 2
+popup/item_3/text = "Path Grouping"
+popup/item_3/id = 3
+
+[node name="FolderColors" type="CheckBox" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup/OptionsPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Use Folder Colors"
+
+[node name="TrimFolderPaths" type="CheckBox" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup/OptionsPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Trim Folder Paths"
+
+[node name="ResourceTree" type="Tree" parent="VBoxPrimary/Margin/MainVSplit/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+allow_rmb_select = true
+hide_root = true
+scroll_horizontal_enabled = false
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox"]
+visible = false
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 1
+text = "Sort Order"
+vertical_alignment = 1
+
+[node name="SortOption" type="OptionButton" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+item_count = 1
+popup/item_0/text = "Alphabetical (All)"
+
+[node name="ContentListSection" type="VBoxContainer" parent="VBoxPrimary/Margin/MainVSplit"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 15)
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ContentList" type="ItemList" parent="VBoxPrimary/Margin/MainVSplit/ContentListSection"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+tooltip_text = "Label events in your timeline will appear here, allowing you to jump to them."
+theme_override_styles/selected = SubResource("StyleBoxEmpty_gxwm6")
+theme_override_styles/selected_focus = SubResource("StyleBoxEmpty_n8rql")
+allow_reselect = true
+same_column_width = true
+
+[node name="CurrentVersion" type="Button" parent="VBoxPrimary"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Some Version"
+flat = true
+clip_text = true
+
+[node name="RightClickMenu" type="PopupMenu" parent="."]
+unique_name_in_owner = true
+size = Vector2i(164, 100)
+
+[connection signal="dragged" from="VBoxPrimary/Margin/MainVSplit" to="." method="_on_main_v_split_dragged"]
+[connection signal="gui_input" from="VBoxPrimary/Margin/MainVSplit/VBox/Logo" to="." method="_on_logo_gui_input"]
+[connection signal="text_changed" from="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Search" to="." method="_on_search_text_changed"]
+[connection signal="text_submitted" from="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Search" to="." method="_on_search_text_submitted"]
+[connection signal="pressed" from="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options" to="." method="_on_options_pressed"]
+[connection signal="toggled" from="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup/OptionsPanel/VBox/FolderColors" to="." method="_on_folder_colors_toggled"]
+[connection signal="toggled" from="VBoxPrimary/Margin/MainVSplit/VBox/HBoxSearchSort/Options/OptionsPopup/OptionsPanel/VBox/TrimFolderPaths" to="." method="_on_trim_folder_paths_toggled"]
+[connection signal="id_pressed" from="RightClickMenu" to="." method="_on_right_click_menu_id_pressed"]
--- /dev/null
+@tool
+class_name DialogicSidebar extends Control
+
+## Script that handles the editor sidebar.
+
+signal content_item_activated(item_name)
+signal show_sidebar(show: bool)
+
+# References
+@onready var editors_manager = get_parent().get_parent()
+@onready var resource_tree: Tree = %ResourceTree
+
+var current_resource_list: Array = []
+
+enum GroupMode {
+ NONE,
+ TYPE,
+ FOLDER,
+ PATH,
+}
+var group_mode: GroupMode = GroupMode.TYPE
+
+
+func _ready() -> void:
+ if owner != null and owner.get_parent() is SubViewport:
+ return
+ if editors_manager is SubViewportContainer:
+ return
+
+ ## CONNECTIONS
+ editors_manager.resource_opened.connect(_on_editors_resource_opened)
+ editors_manager.editor_changed.connect(_on_editors_editor_changed)
+
+ resource_tree.item_activated.connect(_on_resources_tree_item_activated)
+ resource_tree.item_mouse_selected.connect(_on_resources_tree_item_clicked)
+ resource_tree.item_collapsed.connect(_on_resources_tree_item_collapsed)
+
+ %ContentList.item_selected.connect(
+ func(idx: int): content_item_activated.emit(%ContentList.get_item_text(idx))
+ )
+
+ %OpenButton.pressed.connect(_show_sidebar)
+ %CloseButton.pressed.connect(_hide_sidebar)
+
+ var editor_scale := DialogicUtil.get_editor_scale()
+
+ ## ICONS
+ %Logo.texture = load("res://addons/dialogic/Editor/Images/dialogic-logo.svg")
+ %Logo.custom_minimum_size.y = 30 * editor_scale
+ %Search.right_icon = get_theme_icon("Search", "EditorIcons")
+ %Options.icon = get_theme_icon("GuiTabMenuHl", "EditorIcons")
+ %OptionsPanel.add_theme_stylebox_override("panel", get_theme_stylebox("PanelForeground", "EditorStyles"))
+ %OptionsPopup.hide()
+
+ %ContentList.add_theme_color_override(
+ "font_hovered_color", get_theme_color("warning_color", "Editor")
+ )
+ %ContentList.add_theme_color_override(
+ "font_selected_color", get_theme_color("property_color_z", "Editor")
+ )
+
+ ## RIGHT CLICK MENU
+ %RightClickMenu.clear()
+ %RightClickMenu.add_icon_item(get_theme_icon("Remove", "EditorIcons"), "Remove From List", 1)
+ %RightClickMenu.add_separator()
+ %RightClickMenu.add_icon_item(get_theme_icon("ActionCopy", "EditorIcons"), "Copy Identifier", 4)
+ %RightClickMenu.add_separator()
+ %RightClickMenu.add_icon_item(
+ get_theme_icon("Filesystem", "EditorIcons"), "Show in FileSystem", 2
+ )
+ %RightClickMenu.add_icon_item(
+ get_theme_icon("ExternalLink", "EditorIcons"), "Open in External Program", 3
+ )
+
+ ## SORT MENU
+ %GroupingOptions.set_item_icon(0, get_theme_icon("AnimationTrackGroup", "EditorIcons"))
+ %GroupingOptions.set_item_icon(1, get_theme_icon("Folder", "EditorIcons"))
+ %GroupingOptions.set_item_icon(2, get_theme_icon("FolderBrowse", "EditorIcons"))
+ %GroupingOptions.set_item_icon(3, get_theme_icon("AnimationTrackList", "EditorIcons"))
+ %GroupingOptions.item_selected.connect(_on_grouping_changed)
+
+ await get_tree().process_frame
+ if DialogicUtil.get_editor_setting("sidebar_collapsed", false):
+ _hide_sidebar()
+
+ %MainVSplit.split_offset = DialogicUtil.get_editor_setting("sidebar_v_split", 0)
+ group_mode = DialogicUtil.get_editor_setting("sidebar_group_mode", 0)
+ %GroupingOptions.select(%GroupingOptions.get_item_index(group_mode))
+
+ %FolderColors.button_pressed = DialogicUtil.get_editor_setting("sidebar_use_folder_colors", true)
+ %TrimFolderPaths.button_pressed = DialogicUtil.get_editor_setting("sidebar_trim_folder_paths", true)
+
+ update_resource_list()
+
+
+func set_unsaved_indicator(saved: bool = true) -> void:
+ if saved and %CurrentResource.text.ends_with("(*)"):
+ %CurrentResource.text = %CurrentResource.text.trim_suffix("(*)")
+ if not saved and not %CurrentResource.text.ends_with("(*)"):
+ %CurrentResource.text = %CurrentResource.text + "(*)"
+
+
+func _on_logo_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
+ editors_manager.open_editor(editors_manager.editors["HomePage"].node)
+
+
+#region SHOW/HIDE SIDEBAR
+################################################################################
+
+func _show_sidebar() -> void:
+ %VBoxPrimary.show()
+ %VBoxHidden.hide()
+ DialogicUtil.set_editor_setting("sidebar_collapsed", false)
+ show_sidebar.emit(true)
+
+
+func _hide_sidebar() -> void:
+ %VBoxPrimary.hide()
+ %VBoxHidden.show()
+ DialogicUtil.set_editor_setting("sidebar_collapsed", true)
+ show_sidebar.emit(false)
+
+#endregion
+
+
+################################################################################
+## RESOURCE LIST
+################################################################################
+
+
+func _on_editors_resource_opened(_resource: Resource) -> void:
+ update_resource_list()
+
+
+func _on_editors_editor_changed(_previous: DialogicEditor, current: DialogicEditor) -> void:
+ %ContentListSection.visible = current.current_resource is DialogicTimeline
+ update_resource_list()
+
+
+## Cleans resources that have been deleted from the resource list
+func clean_resource_list(resources_list: Array = []) -> PackedStringArray:
+ return PackedStringArray(resources_list.filter(func(x): return ResourceLoader.exists(x)))
+
+
+#region BULDING/FILTERING THE RESOURCE LIST
+
+func update_resource_list(resources_list: PackedStringArray = []) -> void:
+ var filter: String = %Search.text
+ var current_file := ""
+ if editors_manager.current_editor and editors_manager.current_editor.current_resource:
+ current_file = editors_manager.current_editor.current_resource.resource_path
+
+ var character_directory: Dictionary = DialogicResourceUtil.get_character_directory()
+ var timeline_directory: Dictionary = DialogicResourceUtil.get_timeline_directory()
+ if resources_list.is_empty():
+ resources_list = DialogicUtil.get_editor_setting("last_resources", [])
+ if not current_file in resources_list:
+ resources_list.append(current_file)
+
+ resources_list = clean_resource_list(resources_list)
+
+ %CurrentResource.text = "No Resource"
+ %CurrentResource.add_theme_color_override(
+ "font_uneditable_color", get_theme_color("disabled_font_color", "Editor")
+ )
+
+ resource_tree.clear()
+
+ var character_items: Array = get_directory_items.call(character_directory, filter, load("res://addons/dialogic/Editor/Images/Resources/character.svg"), resources_list)
+ var timeline_items: Array = get_directory_items.call(timeline_directory, filter, get_theme_icon("TripleBar", "EditorIcons"), resources_list)
+ var all_items := character_items + timeline_items
+
+ # BUILD TREE
+ var root: TreeItem = resource_tree.create_item()
+
+ match group_mode:
+ GroupMode.NONE:
+ all_items.sort_custom(_sort_by_item_text)
+ for item in all_items:
+ add_item(item, root, current_file)
+
+
+ GroupMode.TYPE:
+ character_items.sort_custom(_sort_by_item_text)
+ timeline_items.sort_custom(_sort_by_item_text)
+ if character_items.size() > 0:
+ var character_tree := add_folder_item("Characters", root)
+ for item in character_items:
+ add_item(item, character_tree, current_file)
+
+ if timeline_items.size() > 0:
+ var timeline_tree := add_folder_item("Timelines", root)
+ for item in timeline_items:
+ add_item(item, timeline_tree, current_file)
+
+
+ GroupMode.FOLDER:
+ var dirs := {}
+ for item in all_items:
+ var dir := item.get_parent_directory() as String
+ if not dirs.has(dir):
+ dirs[dir] = []
+ dirs[dir].append(item)
+
+ for dir in dirs:
+ var dir_item := add_folder_item(dir, root)
+
+ for item in dirs[dir]:
+ add_item(item, dir_item, current_file)
+
+
+ GroupMode.PATH:
+ # Collect all different directories that contain resources
+ var dirs := {}
+ for item in all_items:
+ var path := (item.metadata.get_base_dir() as String).trim_prefix("res://")
+ if not dirs.has(path):
+ dirs[path] = []
+ dirs[path].append(item)
+
+ # Sort them into ones with the same folder name
+ var dir_names := {}
+ for dir in dirs:
+ var sliced: String = dir.get_slice("/", dir.get_slice_count("/")-1)
+ if not sliced in dir_names:
+ dir_names[sliced] = {"folders":[dir]}
+ else:
+ dir_names[sliced].folders.append(dir)
+
+ # Create a dictionary mapping a unique name to each directory
+ # If two have been found to have the same folder name, the parent directory is added
+ var unique_folder_names := {}
+ for dir_name in dir_names:
+ if dir_names[dir_name].folders.size() > 1:
+ for i in dir_names[dir_name].folders:
+ if "/" in i:
+ unique_folder_names[i.get_slice("/", i.get_slice_count("/")-2)+"/"+i.get_slice("/", i.get_slice_count("/")-1)] = i
+ else:
+ unique_folder_names[i] = i
+ else:
+ unique_folder_names[dir_name] = dir_names[dir_name].folders[0]
+
+ # Sort the folder names by their folder name (not by the full path)
+ var sorted_dir_keys := unique_folder_names.keys()
+ sorted_dir_keys.sort_custom(
+ func(x, y):
+ return x.get_slice("/", x.get_slice_count("/")-1) < y.get_slice("/", y.get_slice_count("/")-1)
+ )
+ var folder_colors: Dictionary = ProjectSettings.get_setting("file_customization/folder_colors", {})
+
+ for dir in sorted_dir_keys:
+ var display_name: String = dir
+ if not %TrimFolderPaths.button_pressed:
+ display_name = unique_folder_names[dir]
+ var dir_path: String = unique_folder_names[dir]
+ var dir_color_path := ""
+ var dir_color := Color.BLACK
+ if %FolderColors.button_pressed:
+ for path in folder_colors:
+ if String("res://"+dir_path+"/").begins_with(path) and len(path) > len(dir_color_path):
+ dir_color_path = path
+ dir_color = folder_colors[path]
+
+ var dir_item := add_folder_item(display_name, root, dir_color, dir_path)
+
+ for item in dirs[dir_path]:
+ add_item(item, dir_item, current_file)
+
+
+ if %CurrentResource.text != "No Resource":
+ %CurrentResource.add_theme_color_override(
+ "font_uneditable_color", get_theme_color("font_color", "Editor")
+ )
+
+ DialogicUtil.set_editor_setting("last_resources", resources_list)
+
+
+func add_item(item:ResourceListItem, parent:TreeItem, current_file := "") -> TreeItem:
+ var tree_item := resource_tree.create_item(parent)
+ tree_item.set_text(0, item.text)
+ tree_item.set_icon(0, item.icon)
+ tree_item.set_metadata(0, item.metadata)
+ tree_item.set_tooltip_text(0, item.tooltip)
+
+ if item.metadata == current_file:
+ %CurrentResource.text = item.metadata.get_file()
+ resource_tree.set_selected(tree_item, 0)
+
+ var bg_color := parent.get_custom_bg_color(0)
+ if bg_color != get_theme_color("base_color", "Editor"):
+ bg_color.a = 0.1
+ tree_item.set_custom_bg_color(0, bg_color)
+
+ return tree_item
+
+
+func add_folder_item(label: String, parent:TreeItem, color:= Color.BLACK, tooltip:="") -> TreeItem:
+ var folder_item := resource_tree.create_item(parent)
+ folder_item.set_text(0, label)
+ folder_item.set_icon(0, get_theme_icon("Folder", "EditorIcons"))
+ folder_item.set_tooltip_text(0, tooltip)
+ if color == Color.BLACK:
+ folder_item.set_custom_bg_color(0, get_theme_color("base_color", "Editor"))
+ else:
+ color.a = 0.2
+ folder_item.set_custom_bg_color(0, color)
+
+ if label in DialogicUtil.get_editor_setting("resource_list_collapsed_info", []):
+ folder_item.collapsed = true
+
+ return folder_item
+
+
+func get_directory_items(directory:Dictionary, filter:String, icon:Texture2D, resources_list:Array) -> Array:
+ var items := []
+ for item_name in directory:
+ if (directory[item_name] in resources_list) and (filter.is_empty() or filter.to_lower() in item_name.to_lower()):
+ var item := ResourceListItem.new()
+ item.text = item_name
+ item.icon = icon
+ item.metadata = directory[item_name]
+ item.tooltip = directory[item_name]
+ items.append(item)
+ return items
+
+
+class ResourceListItem:
+ extends Object
+
+ var text: String
+ var index: int = -1
+ var icon: Texture
+ var metadata: String
+ var tooltip: String
+
+ func _to_string() -> String:
+ return JSON.stringify(
+ {
+ "text": text,
+ "index": index,
+ "icon": icon.resource_path,
+ "metadata": metadata,
+ "tooltip": tooltip,
+ "parent_dir": get_parent_directory()
+ },
+ "\t",
+ false
+ )
+
+ func get_parent_directory() -> String:
+ return (metadata.get_base_dir() as String).split("/")[-1]
+
+
+func _sort_by_item_text(a: ResourceListItem, b: ResourceListItem) -> bool:
+ return a.text < b.text
+
+#endregion
+
+
+#region INTERACTING WITH RESOURCES
+
+
+func _on_resources_tree_item_activated() -> void:
+ if resource_tree.get_selected() == null:
+ return
+ var item := resource_tree.get_selected()
+ if item.get_metadata(0) == null:
+ return
+ edit_resource(item.get_metadata(0))
+
+
+func _on_resources_tree_item_clicked(_pos: Vector2, mouse_button_index: int) -> void:
+ match mouse_button_index:
+ MOUSE_BUTTON_LEFT:
+ var selected_item := resource_tree.get_selected()
+ if selected_item == null:
+ return
+ if selected_item.get_metadata(0) == null:
+ return
+ var resource_item := load(selected_item.get_metadata(0))
+ call_deferred("edit_resource", resource_item)
+
+ MOUSE_BUTTON_MIDDLE:
+ remove_item_from_list(resource_tree.get_selected())
+
+ MOUSE_BUTTON_RIGHT:
+ if resource_tree.get_selected().get_metadata(0):
+ %RightClickMenu.popup_on_parent(Rect2(get_global_mouse_position(), Vector2()))
+ %RightClickMenu.set_meta("item_clicked", resource_tree.get_selected())
+
+
+func _on_resources_tree_item_collapsed(item:TreeItem) -> void:
+ var collapsed_info := DialogicUtil.get_editor_setting("resource_list_collapsed_info", [])
+ if item.get_text(0) in collapsed_info:
+ if not item.collapsed:
+ collapsed_info.erase(item.get_text(0))
+ else:
+ if item.collapsed:
+ collapsed_info.append(item.get_text(0))
+ DialogicUtil.set_editor_setting("resource_list_collapsed_info", collapsed_info)
+
+
+func edit_resource(resource_item: Variant) -> void:
+ if resource_item is Resource:
+ editors_manager.edit_resource(resource_item)
+ else:
+ editors_manager.edit_resource(load(resource_item))
+
+
+func remove_item_from_list(item: TreeItem) -> void:
+ var new_list := []
+ for entry in DialogicUtil.get_editor_setting("last_resources", []):
+ if entry != item.get_metadata(0):
+ new_list.append(entry)
+ DialogicUtil.set_editor_setting("last_resources", new_list)
+ update_resource_list(new_list)
+
+
+func _on_right_click_menu_id_pressed(id: int) -> void:
+ match id:
+ 1: # REMOVE ITEM FROM LIST
+ remove_item_from_list(%RightClickMenu.get_meta("item_clicked"))
+ 2: # OPEN IN FILESYSTEM
+ EditorInterface.get_file_system_dock().navigate_to_path(
+ %RightClickMenu.get_meta("item_clicked").get_metadata(0)
+ )
+ 3: # OPEN IN EXTERNAL EDITOR
+ OS.shell_open(
+ ProjectSettings.globalize_path(
+ %RightClickMenu.get_meta("item_clicked").get_metadata(0)
+ )
+ )
+ 4: # COPY IDENTIFIER
+ DisplayServer.clipboard_set(
+ DialogicResourceUtil.get_unique_identifier(
+ %RightClickMenu.get_meta("item_clicked").get_metadata(0)
+ )
+ )
+#endregion
+
+
+#region FILTERING
+
+func _on_search_text_changed(_new_text: String) -> void:
+ update_resource_list()
+ for item in resource_tree.get_root().get_children():
+ if item.get_children().size() > 0:
+ resource_tree.set_selected(item.get_child(0), 0)
+ break
+
+
+func _on_search_text_submitted(_new_text: String) -> void:
+ if resource_tree.get_selected() == null:
+ return
+ var item := resource_tree.get_selected()
+ if item.get_metadata(0) == null:
+ return
+ edit_resource(item.get_metadata(0))
+ %Search.clear()
+
+#endregion
+
+
+#region CONTENT LIST
+
+func update_content_list(list: PackedStringArray) -> void:
+ var prev_selected := ""
+ if %ContentList.is_anything_selected():
+ prev_selected = %ContentList.get_item_text(%ContentList.get_selected_items()[0])
+ %ContentList.clear()
+ %ContentList.add_item("~ Top")
+ for i in list:
+ if i.is_empty():
+ continue
+ %ContentList.add_item(i)
+ if i == prev_selected:
+ %ContentList.select(%ContentList.item_count - 1)
+ if list.is_empty():
+ return
+
+ var current_resource: Resource = editors_manager.get_current_editor().current_resource
+
+ var timeline_directory := DialogicResourceUtil.get_timeline_directory()
+ var label_directory := DialogicResourceUtil.get_label_cache()
+ if current_resource != null:
+ for i in timeline_directory:
+ if timeline_directory[i] == current_resource.resource_path:
+ label_directory[i] = list
+
+ # also always store the current timelines labels for easy access
+ label_directory[""] = list
+
+ DialogicResourceUtil.set_label_cache(label_directory)
+
+#endregion
+
+
+#region RESOURCE LIST OPTIONS
+
+func _on_options_pressed() -> void:
+ %OptionsPopup.popup_on_parent(Rect2(%Options.global_position+%Options.size*Vector2(0,1), Vector2()))
+
+
+func _on_grouping_changed(idx: int) -> void:
+ var id: int = %GroupingOptions.get_item_id(idx)
+ if (GroupMode as Dictionary).values().has(id):
+ group_mode = (id as GroupMode)
+ DialogicUtil.set_editor_setting("sidebar_group_mode", id)
+ update_resource_list()
+
+ %FolderColors.disabled = group_mode != GroupMode.PATH
+ %TrimFolderPaths.disabled = group_mode != GroupMode.PATH
+
+
+func _on_folder_colors_toggled(toggled_on: bool) -> void:
+ DialogicUtil.set_editor_setting("sidebar_use_folder_colors", toggled_on)
+ update_resource_list()
+
+
+func _on_trim_folder_paths_toggled(toggled_on: bool) -> void:
+ DialogicUtil.set_editor_setting("sidebar_trim_folder_paths", toggled_on)
+ update_resource_list()
+
+#endregion
+
+
+func _on_main_v_split_dragged(offset: int) -> void:
+ DialogicUtil.set_editor_setting("sidebar_v_split", offset)
--- /dev/null
+@tool
+extends HBoxContainer
+
+# Dialogic Editor toolbar. Works together with editors_mangager.
+
+################################################################################
+## EDITOR BUTTONS/LABELS
+################################################################################
+func _ready() -> void:
+ if owner.get_parent() is SubViewport:
+ return
+ %CustomButtons.custom_minimum_size.y = 33 * DialogicUtil.get_editor_scale()
+
+ for child in get_children():
+ if child is Button:
+ child.queue_free()
+
+
+func add_icon_button(icon: Texture, tooltip: String) -> Button:
+ var button := Button.new()
+ button.icon = icon
+ button.tooltip_text = tooltip
+ button.flat = true
+ button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
+ button.add_theme_color_override('icon_hover_color', get_theme_color('warning_color', 'Editor'))
+ button.add_theme_stylebox_override('focus', StyleBoxEmpty.new())
+ add_child(button)
+ move_child(button, -2)
+ return button
+
+
+func add_custom_button(label:String, icon:Texture) -> Button:
+ var button := Button.new()
+ button.text = label
+ button.icon = icon
+# button.flat = true
+
+ button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
+ %CustomButtons.add_child(button)
+# custom_minimum_size.y = button.size.y
+ return button
+
+
+func hide_all_custom_buttons() -> void:
+ for button in %CustomButtons.get_children():
+ button.hide()
+
+
+
--- /dev/null
+@tool
+extends PanelContainer
+
+
+func _ready() -> void:
+ if owner.get_parent() is SubViewport:
+ return
+
+ %TabB.text = "Unique Identifiers"
+ %TabB.icon = get_theme_icon("CryptoKey", "EditorIcons")
+
+ owner.get_parent().visibility_changed.connect(func(): if is_visible_in_tree(): open())
+
+ %RenameNotification.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
+
+
+func open() -> void:
+ fill_table()
+ %RenameNotification.hide()
+
+
+func close() -> void:
+ pass
+
+func fill_table() -> void:
+ var t: Tree = %IdentifierTable
+ t.set_column_expand(1, true)
+ t.clear()
+ t.set_column_title(1, "Identifier")
+ t.set_column_title(0, "Resource Path")
+ t.set_column_title_alignment(0, 0)
+ t.set_column_title_alignment(1, 0)
+ t.create_item()
+
+ for d in [["Characters", 'dch'], ["Timelines", "dtl"]]:
+ var directory := DialogicResourceUtil.get_directory(d[1])
+ var directory_item := t.create_item()
+ directory_item.set_text(0, d[0])
+ directory_item.set_metadata(0, d[1])
+ for key in directory:
+ var item: TreeItem = t.create_item(directory_item)
+ item.set_text(0, directory[key])
+ item.set_text(1, key)
+ item.set_editable(1, true)
+ item.set_metadata(1, key)
+ item.add_button(1, get_theme_icon("Edit", "EditorIcons"), 0, false, "Edit")
+
+
+func _on_identifier_table_item_edited() -> void:
+ var item: TreeItem = %IdentifierTable.get_edited()
+ var new_identifier: String = item.get_text(1)
+
+
+ if new_identifier == item.get_metadata(1):
+ return
+
+ if new_identifier.is_empty() or not DialogicResourceUtil.is_identifier_unused(item.get_parent().get_metadata(0), new_identifier):
+ item.set_text(1, item.get_metadata(1))
+ return
+
+ DialogicResourceUtil.change_unique_identifier(item.get_text(0), new_identifier)
+
+ match item.get_parent().get_metadata(0):
+ 'dch':
+ owner.get_parent().add_character_name_ref_change(item.get_metadata(1), new_identifier)
+ 'dtl':
+ owner.get_parent().add_timeline_name_ref_change(item.get_metadata(1), new_identifier)
+
+ %RenameNotification.show()
+ item.set_metadata(1, new_identifier)
+
+
+func _on_identifier_table_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void:
+ item.select(column)
+ %IdentifierTable.edit_selected(true)
+
+
+func filter_tree(filter:String= "", item:TreeItem = null) -> bool:
+ if item == null:
+ item = %IdentifierTable.get_root()
+
+ var any := false
+ for child in item.get_children():
+ if child.get_child_count() > 0:
+ child.visible = filter_tree(filter, child)
+ if child.visible: any = true
+ else:
+ child.visible = filter.is_empty() or filter.to_lower() in child.get_text(0).to_lower() or filter.to_lower() in child.get_text(1).to_lower()
+ if child.visible: any = true
+
+ return any
+
+
+func _on_search_text_changed(new_text: String) -> void:
+ filter_tree(new_text)
--- /dev/null
+@tool
+extends Control
+
+var current_info := {}
+@onready var editor_view := find_parent('EditorView')
+
+
+func _ready() -> void:
+ await editor_view.ready
+ theme = editor_view.theme
+
+ %Install.icon = editor_view.get_theme_icon("AssetLib", "EditorIcons")
+ %LoadingIcon.texture = editor_view.get_theme_icon("KeyTrackScale", "EditorIcons")
+ %InstallWarning.modulate = editor_view.get_theme_color("warning_color", "Editor")
+ %CloseButton.icon = editor_view.get_theme_icon("Close", "EditorIcons")
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().get_resource_filesystem().resources_reimported.connect(_on_resources_reimported)
+
+
+func open() -> void:
+ get_parent().popup_centered_ratio(0.5)
+ get_parent().mode = Window.MODE_WINDOWED
+ get_parent().move_to_foreground()
+ get_parent().grab_focus()
+
+
+func load_info(info:Dictionary, update_type:int) -> void:
+ current_info = info
+ if update_type == 2:
+ %State.text = "No Information Available"
+ %UpdateName.text = "Unable to access versions."
+ %UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("readonly_color", "Editor"))
+ %Content.text = "You are probably not connected to the internet. Fair enough."
+ %ShortInfo.text = "Huh, what happened here?"
+ %ReadFull.hide()
+ %Install.disabled = true
+ return
+
+ # If we are up to date (or beyond):
+ if info.is_empty():
+ info['name'] = "You are in the future, Marty!"
+ info["body"] = "# 😎 You are using the WIP branch!\nSeems like you are using a version that isn't even released yet. Be careful and give us your feedback ;)"
+ info["published_at"] = "????T"
+ info["author"] = {'login':"???"}
+ %State.text = "Where are we Doc?"
+ %UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("property_color_z", "Editor"))
+ %Install.disabled = true
+
+ elif update_type == 0:
+ %State.text = "Update Available!"
+ %UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("warning_color", "Editor"))
+ %Install.disabled = false
+ else:
+ %State.text = "You are up to date:"
+ %UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("success_color", "Editor"))
+ %Install.disabled = true
+
+ %UpdateName.text = info.name
+ %Content.text = markdown_to_bbcode(info.body).get_slice("\n[font_size", 0).strip_edges()
+ %ShortInfo.text = "Published on "+info.published_at.substr(0, info.published_at.find('T'))+" by "+info.author.login
+ if info.has("html_url"):
+ %ReadFull.uri = info.html_url
+ %ReadFull.show()
+ else:
+ %ReadFull.hide()
+ if info.has('reactions'):
+ %Reactions.show()
+ var reactions := {"laugh":"😂", "hooray":"🎉", "confused":"😕", "heart":"❤️", "rocket":"🚀", "eyes":"👀"}
+ for i in reactions:
+ %Reactions.get_node(i.capitalize()).visible = info.reactions[i] > 0
+ %Reactions.get_node(i.capitalize()).text = reactions[i]+" "+str(info.reactions[i]) if info.reactions[i] > 0 else reactions[i]
+ if info.reactions['+1']+info.reactions['-1'] > 0:
+ %Reactions.get_node("Likes").visible = true
+ %Reactions.get_node("Likes").text = "👍 "+str(info.reactions['+1']+info.reactions['-1'])
+ else:
+ %Reactions.get_node("Likes").visible = false
+ else:
+ %Reactions.hide()
+
+func _on_window_close_requested() -> void:
+ get_parent().visible = false
+
+
+func _on_install_pressed() -> void:
+ find_parent('UpdateManager').request_update_download()
+
+ %InfoLabel.text = "Downloading. This can take a moment."
+ %Loading.show()
+ %LoadingIcon.create_tween().set_loops().tween_property(%LoadingIcon, 'rotation', 2*PI, 1).from(0)
+
+
+func _on_refresh_pressed() -> void:
+ find_parent('UpdateManager').request_update_check()
+
+
+func _on_update_manager_downdload_completed(result:int):
+ %Loading.hide()
+ match result:
+ 0: # success
+ %InfoLabel.text = "Installed successfully. Restart needed!"
+ %InfoLabel.modulate = editor_view.get_theme_color("success_color", "Editor")
+ %Restart.show()
+ %Restart.grab_focus()
+ 1: # failure
+ %InfoLabel.text = "Download failed."
+ %InfoLabel.modulate = editor_view.get_theme_color("readonly_color", "Editor")
+
+
+func _on_resources_reimported(resources:Array) -> void:
+ if is_inside_tree():
+ await get_tree().process_frame
+ get_parent().move_to_foreground()
+
+
+func markdown_to_bbcode(text:String) -> String:
+ var font_sizes := {1:20, 2:16, 3:16,4:14, 5:14}
+ var title_regex := RegEx.create_from_string('(^|\n)((?<level>#+)(?<title>.*))\\n')
+ var res := title_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(2), '[font_size='+str(font_sizes[len(res.get_string('level'))])+']'+res.get_string('title').strip_edges()+'[/font_size]')
+ res = title_regex.search(text)
+
+ var link_regex := RegEx.create_from_string('(?<!\\!)\\[(?<text>[^\\]]*)]\\((?<link>[^)]*)\\)')
+ res = link_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(), '[url='+res.get_string('link')+']'+res.get_string('text').strip_edges()+'[/url]')
+ res = link_regex.search(text)
+
+ var image_regex := RegEx.create_from_string('\\!\\[(?<text>[^\\]]*)]\\((?<link>[^)]*)\\)\n*')
+ res = image_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(), '[url='+res.get_string('link')+']'+res.get_string('text').strip_edges()+'[/url]')
+ res = image_regex.search(text)
+
+ var italics_regex := RegEx.create_from_string('\\*(?<text>[^\\*\\n]*)\\*')
+ res = italics_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(), '[i]'+res.get_string('text').strip_edges()+'[/i]')
+ res = italics_regex.search(text)
+
+ var bullets_regex := RegEx.create_from_string('(?<=\\n)(\\*|-)(?<text>[^\\*\\n]*)\\n')
+ res = bullets_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(), '[ul]'+res.get_string('text').strip_edges()+'[/ul]\n')
+ res = bullets_regex.search(text)
+
+ var small_code_regex := RegEx.create_from_string('(?<!`)`(?<text>[^`]+)`')
+ res = small_code_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(), '[code][color='+get_theme_color("accent_color", "Editor").to_html()+']'+res.get_string('text').strip_edges()+'[/color][/code]')
+ res = small_code_regex.search(text)
+
+ var big_code_regex := RegEx.create_from_string('(?<!`)```(?<text>[^`]+)```')
+ res = big_code_regex.search(text)
+ while res:
+ text = text.replace(res.get_string(), '[code][bgcolor='+get_theme_color("box_selection_fill_color", "Editor").to_html()+']'+res.get_string('text').strip_edges()+'[/bgcolor][/code]')
+ res = big_code_regex.search(text)
+
+ return text
+
+
+
+func _on_content_meta_clicked(meta:Variant) -> void:
+ OS.shell_open(str(meta))
+
+
+func _on_install_mouse_entered() -> void:
+ if not %Install.disabled:
+ %InstallWarning.show()
+
+
+func _on_install_mouse_exited() -> void:
+ %InstallWarning.hide()
+
+
+func _on_restart_pressed() -> void:
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().restart_editor(true)
+
+
+func _on_close_button_pressed() -> void:
+ get_parent().hide()
--- /dev/null
+[gd_scene load_steps=9 format=3 uid="uid://vv3m5m68fwg7"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/update_install_window.gd" id="1_p1pbx"]
+[ext_resource type="Texture2D" uid="uid://dybg3l5pwetne" path="res://addons/dialogic/Editor/Images/plugin-icon.svg" id="2_20ke0"]
+
+[sub_resource type="Gradient" id="Gradient_lt7uf"]
+colors = PackedColorArray(0.296484, 0.648457, 1, 1, 0.732014, 0.389374, 1, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_nl8ke"]
+gradient = SubResource("Gradient_lt7uf")
+fill_from = Vector2(0.151515, 0.272727)
+fill_to = Vector2(1, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1g1am"]
+content_margin_left = 0.0
+content_margin_top = 15.0
+content_margin_right = 15.0
+content_margin_bottom = 15.0
+bg_color = Color(0.0627451, 0.0627451, 0.0627451, 0.407843)
+corner_radius_top_left = 20
+corner_radius_top_right = 20
+corner_radius_bottom_right = 20
+corner_radius_bottom_left = 20
+expand_margin_left = 20.0
+expand_margin_right = 20.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_j1mw2"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h4v2s"]
+content_margin_left = 5.0
+content_margin_top = 3.0
+content_margin_right = 5.0
+content_margin_bottom = 3.0
+bg_color = Color(0, 0, 0, 0.631373)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utju1"]
+content_margin_left = 5.0
+content_margin_top = 3.0
+content_margin_right = 5.0
+content_margin_bottom = 3.0
+bg_color = Color(0.0470588, 0.0470588, 0.0470588, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[node name="UpdateInstallWindow" type="ColorRect"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+color = Color(0.207843, 0.129412, 0.372549, 1)
+script = ExtResource("1_p1pbx")
+
+[node name="TextureRect" type="TextureRect" parent="."]
+modulate = Color(0.447059, 0.447059, 0.447059, 1)
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = SubResource("GradientTexture2D_nl8ke")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 14.0
+offset_top = 13.0
+offset_right = -14.0
+offset_bottom = -13.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 7
+
+[node name="VBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer2"]
+custom_minimum_size = Vector2(450, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 3.74
+alignment = 1
+
+[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/HBoxContainer2/VBox"]
+clip_contents = false
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+alignment = 1
+
+[node name="Panel" type="PanelContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_1g1am")
+
+[node name="VBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel"]
+layout_mode = 2
+theme_override_constants/separation = -8
+
+[node name="State" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Update Available!"
+
+[node name="UpdateName" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicTitle"
+theme_override_font_sizes/font_size = 25
+text = "Dialogic 2.0 - alpha 9"
+uppercase = true
+
+[node name="ShortInfo" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicHintText2"
+theme_override_font_sizes/font_size = 10
+text = "12/31/23"
+
+[node name="Refresh" type="Button" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel"]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 0
+text = "Refresh
+"
+flat = true
+
+[node name="Content" type="RichTextLabel" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/normal_font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxEmpty_j1mw2")
+bbcode_enabled = true
+text = "[font_size=25]🎉 New alpha, new stuff![/font_size]
+If you are using dialogic 2 alphas then we've got an exciting update. It's not the beta yet, but we are getting closer! As always if you have questions or feedback it's best to reach out on [url=https://discord.gg/2hHQzkf2pX]emilios discord[/url].
+
+This alpha brings a couple of very useful new features to dialogic as well as some syntax changes and a design overhaul (and many, many bug fixes).
+"
+fit_content = true
+
+[node name="Reactions" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Likes" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "👍12"
+
+[node name="Hooray" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "🎉12"
+
+[node name="Laugh" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "👀12"
+
+[node name="Heart" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "❤️12"
+
+[node name="Rocket" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "😕12"
+
+[node name="Eyes" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "🚀12"
+
+[node name="Confused" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
+text = "😂12"
+
+[node name="ReadFull" type="LinkButton" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+text = "Read Full Announcement"
+
+[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
+layout_mode = 2
+alignment = 2
+
+[node name="InfoLabel" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+horizontal_alignment = 2
+autowrap_mode = 3
+
+[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer"]
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_styles/panel = SubResource("StyleBoxFlat_h4v2s")
+
+[node name="HBox" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer"]
+layout_mode = 2
+alignment = 2
+
+[node name="Loading" type="CenterContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(30, 0)
+layout_mode = 2
+
+[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Loading"]
+layout_mode = 2
+
+[node name="LoadingIcon" type="Sprite2D" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Loading/Control"]
+unique_name_in_owner = true
+texture = ExtResource("2_20ke0")
+
+[node name="Restart" type="Button" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_vertical = 4
+text = "Restart Now"
+flat = true
+
+[node name="Install" type="Button" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+text = "Install"
+flat = true
+
+[node name="InstallWarning" type="PanelContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install"]
+unique_name_in_owner = true
+visible = false
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -493.0
+offset_top = -92.0
+offset_right = 5.0
+offset_bottom = -8.0
+grow_horizontal = 0
+theme_override_styles/panel = SubResource("StyleBoxFlat_utju1")
+
+[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install/InstallWarning"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 14
+text = "Be careful. This will delete the addons/dialogic folder and install the new version. Any custom changes in that folder will be lost.
+To be on the save side, use version control!"
+autowrap_mode = 3
+
+[node name="Control2" type="Control" parent="VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 7
+
+[node name="Close" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="CloseButton" type="Button" parent="VBoxContainer/Close"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Close"
+
+[connection signal="pressed" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/Refresh" to="." method="_on_refresh_pressed"]
+[connection signal="meta_clicked" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Content" to="." method="_on_content_meta_clicked"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Restart" to="." method="_on_restart_pressed"]
+[connection signal="mouse_entered" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install" to="." method="_on_install_mouse_entered"]
+[connection signal="mouse_exited" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install" to="." method="_on_install_mouse_exited"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install" to="." method="_on_install_pressed"]
+[connection signal="pressed" from="VBoxContainer/Close/CloseButton" to="." method="_on_close_button_pressed"]
--- /dev/null
+@tool
+extends Node
+
+## Script that checks for new versions and can install them.
+
+signal update_check_completed(result:UpdateCheckResult)
+signal downdload_completed(result:DownloadResult)
+
+enum UpdateCheckResult {UPDATE_AVAILABLE, UP_TO_DATE, NO_ACCESS}
+enum DownloadResult {SUCCESS, FAILURE}
+enum ReleaseState {ALPHA, BETA, STABLE}
+
+const REMOTE_RELEASES_URL := "https://api.github.com/repos/dialogic-godot/dialogic/releases"
+const TEMP_FILE_NAME := "user://temp.zip"
+
+var current_version := ""
+var update_info: Dictionary
+var current_info: Dictionary
+
+var version_indicator: Button
+
+func _ready() -> void:
+ request_update_check()
+
+ setup_version_indicator()
+
+
+
+func get_current_version() -> String:
+ var plugin_cfg := ConfigFile.new()
+ plugin_cfg.load("res://addons/dialogic/plugin.cfg")
+ return plugin_cfg.get_value('plugin', 'version', 'unknown version')
+
+
+func request_update_check() -> void:
+ if $UpdateCheckRequest.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED:
+ $UpdateCheckRequest.request(REMOTE_RELEASES_URL)
+
+
+func _on_UpdateCheck_request_completed(result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray) -> void:
+ if result != HTTPRequest.RESULT_SUCCESS:
+ update_check_completed.emit(UpdateCheckResult.NO_ACCESS)
+ return
+
+ # Work out the next version from the releases information on GitHub
+ var response: Variant = JSON.parse_string(body.get_string_from_utf8())
+ if typeof(response) != TYPE_ARRAY: return
+
+
+ var current_release_info := get_release_tag_info(get_current_version())
+
+ # GitHub releases are in order of creation, not order of version
+ var versions: Array = (response as Array).filter(compare_versions.bind(current_release_info))
+ if versions.size() > 0:
+ update_info = versions[0]
+ update_check_completed.emit(UpdateCheckResult.UPDATE_AVAILABLE)
+ else:
+ update_info = current_info
+ update_check_completed.emit(UpdateCheckResult.UP_TO_DATE)
+
+
+func compare_versions(release, current_release_info:Dictionary) -> bool:
+ var checked_release_info := get_release_tag_info(release.tag_name)
+
+ if checked_release_info.major < current_release_info.major:
+ return false
+
+ if checked_release_info.minor < current_release_info.minor:
+ return false
+
+ if checked_release_info.state < current_release_info.state:
+ return false
+
+ elif checked_release_info.state == current_release_info.state:
+ if checked_release_info.state_version < current_release_info.state_version:
+ return false
+
+ if checked_release_info.state_version == current_release_info.state_version:
+ current_info = release
+ return false
+
+ if checked_release_info.state == ReleaseState.STABLE:
+ if checked_release_info.minor == current_release_info.minor:
+ current_info = release
+ return false
+
+ return true
+
+
+func get_release_tag_info(release_tag:String) -> Dictionary:
+ release_tag = release_tag.strip_edges().trim_prefix('v')
+ release_tag = release_tag.substr(0, release_tag.find('('))
+ release_tag = release_tag.to_lower()
+
+ var regex := RegEx.create_from_string(r"(?<major>\d+\.\d+)(-(?<state>alpha|beta)-)?(?(2)(?<stateversion>\d*)|\.(?<minor>\d*))?")
+
+ var result: RegExMatch = regex.search(release_tag)
+ if !result:
+ return {}
+
+ var info: Dictionary = {'tag':release_tag}
+ info['major'] = float(result.get_string('major'))
+ info['minor'] = int(result.get_string('minor'))
+
+ match result.get_string('state'):
+ 'alpha':
+ info['state'] = ReleaseState.ALPHA
+ 'beta':
+ info['state'] = ReleaseState.BETA
+ _:
+ info['state'] = ReleaseState.STABLE
+
+ info['state_version'] = int(result.get_string('stateversion'))
+
+ return info
+
+
+func request_update_download() -> void:
+ # Safeguard the actual dialogue manager repo from accidentally updating itself
+ if DirAccess.dir_exists_absolute("res://test-project/"):
+ prints("[Dialogic] Looks like you are working on the addon. You can't update the addon from within itself.")
+ downdload_completed.emit(DownloadResult.FAILURE)
+ return
+
+ $DownloadRequest.request(update_info.zipball_url)
+
+
+func _on_DownloadRequest_completed(result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray):
+ if result != HTTPRequest.RESULT_SUCCESS:
+ downdload_completed.emit(DownloadResult.FAILURE)
+ return
+
+ # Save the downloaded zip
+ var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
+ zip_file.store_buffer(body)
+ zip_file.close()
+
+ OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogic"))
+
+ var zip_reader: ZIPReader = ZIPReader.new()
+ zip_reader.open(TEMP_FILE_NAME)
+ var files: PackedStringArray = zip_reader.get_files()
+
+ var base_path: String = files[0].path_join('addons/')
+ for path in files:
+ if not "dialogic/" in path:
+ continue
+
+ var new_file_path: String = path.replace(base_path, "")
+ if path.ends_with("/"):
+ DirAccess.make_dir_recursive_absolute("res://addons/".path_join(new_file_path))
+ else:
+ var file: FileAccess = FileAccess.open("res://addons/".path_join(new_file_path), FileAccess.WRITE)
+ file.store_buffer(zip_reader.read_file(path))
+
+ zip_reader.close()
+ DirAccess.remove_absolute(TEMP_FILE_NAME)
+
+ downdload_completed.emit(DownloadResult.SUCCESS)
+
+
+###################### SOME UI MANAGEMENT #####################################
+################################################################################
+
+func setup_version_indicator() -> void:
+ version_indicator = %Sidebar.get_node('%CurrentVersion')
+ version_indicator.pressed.connect($Window/UpdateInstallWindow.open)
+ version_indicator.text = get_current_version()
+
+
+func _on_update_check_completed(result:int):
+ var result_color: Color
+ match result:
+ UpdateCheckResult.UPDATE_AVAILABLE:
+ result_color = version_indicator.get_theme_color("warning_color", "Editor")
+ version_indicator.icon = version_indicator.get_theme_icon("StatusWarning", "EditorIcons")
+ $Window/UpdateInstallWindow.load_info(update_info, result)
+ UpdateCheckResult.UP_TO_DATE:
+ result_color = version_indicator.get_theme_color("success_color", "Editor")
+ version_indicator.icon = version_indicator.get_theme_icon("StatusSuccess", "EditorIcons")
+ $Window/UpdateInstallWindow.load_info(current_info, result)
+ UpdateCheckResult.NO_ACCESS:
+ result_color = version_indicator.get_theme_color("success_color", "Editor")
+ version_indicator.icon = version_indicator.get_theme_icon("GuiRadioCheckedDisabled", "EditorIcons")
+ $Window/UpdateInstallWindow.load_info(update_info, result)
+
+ version_indicator.add_theme_color_override('font_color', result_color)
+ version_indicator.add_theme_color_override('font_hover_color', result_color.lightened(0.5))
+ version_indicator.add_theme_color_override('font_pressed_color', result_color)
+ version_indicator.add_theme_color_override('font_focus_color', result_color)
--- /dev/null
+@tool
+extends Control
+## A scene shown at the end of events that contain other events
+
+var resource: DialogicEndBranchEvent
+
+# References
+var parent_node: Control = null
+var end_control: Control = null
+
+# Indent
+var indent_size := 22
+var current_indent_level := 1
+
+var selected := false
+
+func _ready() -> void:
+ $Icon.icon = get_theme_icon("GuiSpinboxUpdown", "EditorIcons")
+ $Spacer.custom_minimum_size.x = 90 * DialogicUtil.get_editor_scale()
+ visual_deselect()
+ parent_node_changed()
+
+
+## Called by the visual timeline editor
+func visual_select() -> void:
+ modulate = get_theme_color("highlighted_font_color", "Editor")
+ selected = true
+
+
+## Called by the visual timeline editor
+func visual_deselect() -> void:
+ if !parent_node:return
+ selected = false
+ modulate = parent_node.resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.3)
+
+
+func is_selected() -> bool:
+ return selected
+
+
+## Called by the visual timeline editor
+func highlight() -> void:
+ if !parent_node:return
+ modulate = parent_node.resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.6)
+
+
+## Called by the visual timeline editor
+func unhighlight() -> void:
+ modulate = parent_node.resource.event_color
+
+
+func update_hidden_events_indicator(hidden_events_count:int = 0) -> void:
+ $HiddenEventsLabel.visible = hidden_events_count > 0
+ if hidden_events_count == 1:
+ $HiddenEventsLabel.text = "[1 event hidden]"
+ else:
+ $HiddenEventsLabel.text = "["+str(hidden_events_count)+ " events hidden]"
+
+
+## Called by the visual timeline editor
+func set_indent(indent: int) -> void:
+ $Indent.custom_minimum_size = Vector2(indent_size * indent * DialogicUtil.get_editor_scale(), 0)
+ $Indent.visible = indent != 0
+ current_indent_level = indent
+ queue_redraw()
+
+
+## Called by the visual timeline editor if something was edited on the parent event block
+func parent_node_changed() -> void:
+ if parent_node and end_control and end_control.has_method('refresh'):
+ end_control.refresh()
+
+
+## Called on creation if the parent event provides an end control
+func add_end_control(control:Control) -> void:
+ if !control:
+ return
+ add_child(control)
+ control.size_flags_vertical = SIZE_SHRINK_CENTER
+ if "parent_resource" in control:
+ control.parent_resource = parent_node.resource
+ if control.has_method('refresh'):
+ control.refresh()
+ end_control = control
+
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://de13fdeebrkcb"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/BranchEnd.gd" id="1"]
+
+[sub_resource type="Image" id="Image_8jrl8"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_44ap0"]
+image = SubResource("Image_8jrl8")
+
+[node name="EndBranch" type="HBoxContainer"]
+anchors_preset = 10
+anchor_right = 1.0
+offset_bottom = 24.0
+grow_horizontal = 2
+mouse_filter = 0
+script = ExtResource("1")
+
+[node name="Indent" type="Control" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Spacer" type="Control" parent="."]
+custom_minimum_size = Vector2(90, 0)
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Icon" type="Button" parent="."]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(20, 0)
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "Click and drag"
+focus_mode = 0
+mouse_filter = 1
+icon = SubResource("ImageTexture_44ap0")
+flat = true
+
+[node name="HiddenEventsLabel" type="Label" parent="."]
+visible = false
+layout_mode = 2
+text = "XX Events hidden"
--- /dev/null
+@tool
+extends MarginContainer
+
+## Scene that represents an event in the visual timeline editor.
+
+signal content_changed()
+
+## REFERENCES
+var resource: DialogicEvent
+var editor_reference
+# for choice and condition
+var end_node: Node = null:
+ get:
+ return end_node
+ set(node):
+ end_node = node
+ %ToggleChildrenVisibilityButton.visible = true if end_node else false
+
+
+## FLAGS
+var selected := false
+# Whether the body is visible
+var expanded := true
+var body_was_build := false
+var has_any_enabled_body_content := false
+# Whether contained events (e.g. in choices) are visible
+var collapsed := false
+
+
+## CONSTANTS
+const icon_size := 28
+const indent_size := 22
+
+## STATE
+# List that stores visibility conditions
+var field_list := []
+var current_indent_level := 1
+
+
+#region UI AND LOGIC INITIALIZATION
+################################################################################
+
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+
+ if not resource:
+ printerr("[Dialogic] Event block was added without a resource specified.")
+ return
+
+ initialize_ui()
+ initialize_logic()
+
+
+func initialize_ui() -> void:
+ var _scale := DialogicUtil.get_editor_scale()
+
+ $PanelContainer.self_modulate = get_theme_color("accent_color", "Editor")
+
+ # Warning Icon
+ %Warning.texture = get_theme_icon("NodeWarning", "EditorIcons")
+ %Warning.size = Vector2(16 * _scale, 16 * _scale)
+ %Warning.position = Vector2(-5 * _scale, -10 * _scale)
+
+ # Expand Button
+ %ToggleBodyVisibilityButton.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
+ %ToggleBodyVisibilityButton.set("theme_override_colors/icon_normal_color", get_theme_color("contrast_color_2", "Editor"))
+ %ToggleBodyVisibilityButton.set("theme_override_colors/icon_hover_color", get_theme_color("accent_color", "Editor"))
+ %ToggleBodyVisibilityButton.set("theme_override_colors/icon_pressed_color", get_theme_color("contrast_color_2", "Editor"))
+ %ToggleBodyVisibilityButton.set("theme_override_colors/icon_hover_pressed_color", get_theme_color("accent_color", "Editor"))
+ %ToggleBodyVisibilityButton.add_theme_stylebox_override('hover_pressed', StyleBoxEmpty.new())
+
+ # Icon Panel
+ %IconPanel.tooltip_text = resource.event_name
+ %IconPanel.self_modulate = resource.event_color
+
+ # Event Icon
+ %IconTexture.texture = resource._get_icon()
+
+ %IconPanel.custom_minimum_size = Vector2(icon_size, icon_size) * _scale
+ %IconTexture.custom_minimum_size = %IconPanel.custom_minimum_size
+
+ var custom_style: StyleBoxFlat = %IconPanel.get_theme_stylebox('panel')
+ custom_style.set_corner_radius_all(5 * _scale)
+
+ # Focus Mode
+ set_focus_mode(1) # Allowing this node to grab focus
+
+ # Separation on the header
+ %Header.add_theme_constant_override("custom_constants/separation", 5 * _scale)
+
+ # Collapse Button
+ %ToggleChildrenVisibilityButton.toggled.connect(_on_collapse_toggled)
+ %ToggleChildrenVisibilityButton.icon = get_theme_icon("Collapse", "EditorIcons")
+ %ToggleChildrenVisibilityButton.hide()
+
+ %Body.add_theme_constant_override("margin_left", icon_size * _scale)
+
+ visual_deselect()
+
+
+func initialize_logic() -> void:
+ resized.connect(get_parent().get_parent().queue_redraw)
+
+ resource.ui_update_needed.connect(_on_resource_ui_update_needed)
+ resource.ui_update_warning.connect(set_warning)
+
+ content_changed.connect(recalculate_field_visibility)
+
+ _on_ToggleBodyVisibility_toggled(resource.expand_by_default or resource.created_by_button)
+
+#endregion
+
+
+#region VISUAL METHODS
+################################################################################
+
+func visual_select() -> void:
+ $PanelContainer.add_theme_stylebox_override('panel', load("res://addons/dialogic/Editor/Events/styles/selected_styleboxflat.tres"))
+ selected = true
+ %IconPanel.self_modulate = resource.event_color
+ %IconTexture.modulate = get_theme_color("icon_saturation", "Editor")
+
+
+func visual_deselect() -> void:
+ $PanelContainer.add_theme_stylebox_override('panel', load("res://addons/dialogic/Editor/Events/styles/unselected_stylebox.tres"))
+ selected = false
+ %IconPanel.self_modulate = resource.event_color.lerp(Color.DARK_SLATE_GRAY, 0.1)
+ %IconTexture.modulate = get_theme_color('font_color', 'Label')
+
+
+func is_selected() -> bool:
+ return selected
+
+
+func set_warning(text:String= "") -> void:
+ if !text.is_empty():
+ %Warning.show()
+ %Warning.tooltip_text = text
+ else:
+ %Warning.hide()
+
+
+func set_indent(indent: int) -> void:
+ add_theme_constant_override("margin_left", indent_size * indent * DialogicUtil.get_editor_scale())
+ current_indent_level = indent
+
+#endregion
+
+
+#region EVENT FIELDS
+################################################################################
+
+var FIELD_SCENES := {
+ DialogicEvent.ValueType.MULTILINE_TEXT: "res://addons/dialogic/Editor/Events/Fields/field_text_multiline.tscn",
+ DialogicEvent.ValueType.SINGLELINE_TEXT: "res://addons/dialogic/Editor/Events/Fields/field_text_singleline.tscn",
+ DialogicEvent.ValueType.FILE: "res://addons/dialogic/Editor/Events/Fields/field_file.tscn",
+ DialogicEvent.ValueType.BOOL: "res://addons/dialogic/Editor/Events/Fields/field_bool_check.tscn",
+ DialogicEvent.ValueType.BOOL_BUTTON: "res://addons/dialogic/Editor/Events/Fields/field_bool_button.tscn",
+ DialogicEvent.ValueType.CONDITION: "res://addons/dialogic/Editor/Events/Fields/field_condition.tscn",
+ DialogicEvent.ValueType.ARRAY: "res://addons/dialogic/Editor/Events/Fields/field_array.tscn",
+ DialogicEvent.ValueType.DICTIONARY: "res://addons/dialogic/Editor/Events/Fields/field_dictionary.tscn",
+ DialogicEvent.ValueType.DYNAMIC_OPTIONS: "res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn",
+ DialogicEvent.ValueType.FIXED_OPTIONS : "res://addons/dialogic/Editor/Events/Fields/field_options_fixed.tscn",
+ DialogicEvent.ValueType.NUMBER: "res://addons/dialogic/Editor/Events/Fields/field_number.tscn",
+ DialogicEvent.ValueType.VECTOR2: "res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn",
+ DialogicEvent.ValueType.VECTOR3: "res://addons/dialogic/Editor/Events/Fields/field_vector3.tscn",
+ DialogicEvent.ValueType.VECTOR4: "res://addons/dialogic/Editor/Events/Fields/field_vector4.tscn",
+ DialogicEvent.ValueType.COLOR: "res://addons/dialogic/Editor/Events/Fields/field_color.tscn",
+ DialogicEvent.ValueType.AUDIO_PREVIEW: "res://addons/dialogic/Editor/Events/Fields/field_audio_preview.tscn",
+ }
+
+func build_editor(build_header:bool = true, build_body:bool = false) -> void:
+ var current_body_container: HFlowContainer = null
+
+ if build_body and body_was_build:
+ build_body = false
+
+ if build_body:
+ if body_was_build:
+ return
+ current_body_container = HFlowContainer.new()
+ %BodyContent.add_child(current_body_container)
+ body_was_build = true
+
+ for p in resource.get_event_editor_info():
+ field_list.append({'node':null, 'location':p.location})
+ if p.has('condition'):
+ field_list[-1]['condition'] = p.condition
+
+ if !build_body and p.location == 1:
+ continue
+ elif !build_header and p.location == 0:
+ continue
+
+ ### --------------------------------------------------------------------
+ ### 1. CREATE A NODE OF THE CORRECT TYPE FOR THE PROPERTY
+ var editor_node: Control
+
+ ### LINEBREAK
+ if p.name == "linebreak":
+ field_list.remove_at(field_list.size()-1)
+ if !current_body_container.get_child_count():
+ current_body_container.queue_free()
+ current_body_container = HFlowContainer.new()
+ %BodyContent.add_child(current_body_container)
+ continue
+
+ elif p.field_type in FIELD_SCENES:
+ editor_node = load(FIELD_SCENES[p.field_type]).instantiate()
+
+ elif p.field_type == resource.ValueType.LABEL:
+ editor_node = Label.new()
+ editor_node.text = p.display_info.text
+ editor_node.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ editor_node.set('custom_colors/font_color', Color("#7b7b7b"))
+ editor_node.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
+
+ elif p.field_type == resource.ValueType.BUTTON:
+ editor_node = Button.new()
+ editor_node.text = p.display_info.text
+ editor_node.tooltip_text = p.display_info.get('tooltip', '')
+ if typeof(p.display_info.icon) == TYPE_ARRAY:
+ editor_node.icon = callv('get_theme_icon', p.display_info.icon)
+ else:
+ editor_node.icon = p.display_info.icon
+ editor_node.flat = true
+ editor_node.custom_minimum_size.x = 30 * DialogicUtil.get_editor_scale()
+ editor_node.pressed.connect(p.display_info.callable)
+
+ ## CUSTOM
+ elif p.field_type == resource.ValueType.CUSTOM:
+ if p.display_info.has('path'):
+ editor_node = load(p.display_info.path).instantiate()
+
+ ## ELSE
+ else:
+ editor_node = Label.new()
+ editor_node.text = p.name
+ editor_node.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
+
+
+ field_list[-1]['node'] = editor_node
+ ### --------------------------------------------------------------------
+ # Some things need to be called BEFORE the field is added to the tree
+ if editor_node is DialogicVisualEditorField:
+ editor_node.event_resource = resource
+
+ editor_node.property_name = p.name
+ field_list[-1]['property'] = p.name
+
+ editor_node._load_display_info(p.display_info)
+
+ var location: Control = %HeaderContent
+ if p.location == 1:
+ location = current_body_container
+ location.add_child(editor_node)
+
+ # Some things need to be called AFTER the field is added to the tree
+ if editor_node is DialogicVisualEditorField:
+ # Only set the value if the field is visible
+ #
+ # This prevents events with varied value types (event_setting, event_variable)
+ # from injecting incorrect types into hidden fields, which then throw errors
+ # in the console.
+ if p.has('condition') and not p.condition.is_empty():
+ if _evaluate_visibility_condition(p):
+ editor_node._set_value(resource.get(p.name))
+ else:
+ editor_node._set_value(resource.get(p.name))
+
+ editor_node.value_changed.connect(set_property)
+
+ editor_node.tooltip_text = p.display_info.get('tooltip', '')
+
+ # Apply autofocus
+ if resource.created_by_button and p.display_info.get('autofocus', false):
+ editor_node.call_deferred('take_autofocus')
+
+ ### --------------------------------------------------------------------
+ ### 4. ADD LEFT AND RIGHT TEXT
+ var left_label: Label = null
+ var right_label: Label = null
+ if !p.get('left_text', '').is_empty():
+ left_label = Label.new()
+ left_label.text = p.get('left_text')
+ left_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ left_label.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
+ location.add_child(left_label)
+ location.move_child(left_label, editor_node.get_index())
+ if !p.get('right_text', '').is_empty():
+ right_label = Label.new()
+ right_label.text = p.get('right_text')
+ right_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ right_label.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
+ location.add_child(right_label)
+ location.move_child(right_label, editor_node.get_index()+1)
+
+ ### --------------------------------------------------------------------
+ ### 5. REGISTER CONDITION
+ if p.has('condition'):
+ field_list[-1]['condition'] = p.condition
+ if left_label:
+ field_list.append({'node': left_label, 'condition':p.condition, 'location':p.location})
+ if right_label:
+ field_list.append({'node': right_label, 'condition':p.condition, 'location':p.location})
+
+
+ if build_body:
+ if current_body_container.get_child_count() == 0:
+ expanded = false
+ %Body.visible = false
+
+ recalculate_field_visibility()
+
+
+func recalculate_field_visibility() -> void:
+ has_any_enabled_body_content = false
+ for p in field_list:
+ if !p.has('condition') or p.condition.is_empty():
+ if p.node != null:
+ p.node.show()
+ if p.location == 1:
+ has_any_enabled_body_content = true
+ else:
+ if _evaluate_visibility_condition(p):
+ if p.node != null:
+ if p.node.visible == false and p.has("property"):
+ p.node._set_value(resource.get(p.property))
+ p.node.show()
+ if p.location == 1:
+ has_any_enabled_body_content = true
+ else:
+ if p.node != null:
+ p.node.hide()
+ %ToggleBodyVisibilityButton.visible = has_any_enabled_body_content
+
+
+func set_property(property_name:String, value:Variant) -> void:
+ resource.set(property_name, value)
+ content_changed.emit()
+ if end_node:
+ end_node.parent_node_changed()
+
+
+func _evaluate_visibility_condition(p: Dictionary) -> bool:
+ var expr := Expression.new()
+ expr.parse(p.condition)
+ var result: bool
+ if expr.execute([], resource):
+ result = true
+ else:
+ result = false
+ if expr.has_execute_failed():
+ printerr("[Dialogic] Failed executing visibility condition for '",p.get('property', 'unnamed'),"': " + expr.get_error_text())
+ return result
+
+
+func _on_resource_ui_update_needed() -> void:
+ for node_info in field_list:
+ if node_info.node and node_info.node.has_method('set_value'):
+ # Only set the value if the field is visible
+ #
+ # This prevents events with varied value types (event_setting, event_variable)
+ # from injecting incorrect types into hidden fields, which then throw errors
+ # in the console.
+ if node_info.has('condition') and not node_info.condition.is_empty():
+ if _evaluate_visibility_condition(node_info):
+ node_info.node.set_value(resource.get(node_info.property))
+ else:
+ node_info.node.set_value(resource.get(node_info.property))
+ recalculate_field_visibility()
+
+
+#region SIGNALS
+################################################################################
+
+func _on_collapse_toggled(toggled:bool) -> void:
+ collapsed = toggled
+ var timeline_editor: Node = find_parent('VisualEditor')
+ if (timeline_editor != null):
+ # @todo select item and clear selection is marked as "private" in TimelineEditor.gd
+ # consider to make it "public" or add a public helper function
+ timeline_editor.indent_events()
+
+
+
+func _on_ToggleBodyVisibility_toggled(button_pressed:bool) -> void:
+ if button_pressed and !body_was_build:
+ build_editor(false, true)
+ %ToggleBodyVisibilityButton.set_pressed_no_signal(button_pressed)
+
+ if button_pressed:
+ %ToggleBodyVisibilityButton.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
+ else:
+ %ToggleBodyVisibilityButton.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
+
+ expanded = button_pressed
+ %Body.visible = button_pressed
+
+ if find_parent('VisualEditor') != null:
+ find_parent('VisualEditor').indent_events()
+
+
+func _on_EventNode_gui_input(event:InputEvent) -> void:
+ if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1:
+ grab_focus() # Grab focus to avoid copy pasting text or events
+ if event.double_click:
+ if has_any_enabled_body_content:
+ _on_ToggleBodyVisibility_toggled(!expanded)
+ # For opening the context menu
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
+ var popup: PopupMenu = get_parent().get_parent().get_node('EventPopupMenu')
+ popup.current_event = self
+ popup.popup_on_parent(Rect2(get_global_mouse_position(),Vector2()))
+ if resource.help_page_path == "":
+ popup.set_item_disabled(2, true)
+ else:
+ popup.set_item_disabled(2, false)
--- /dev/null
+[gd_scene load_steps=8 format=3 uid="uid://bwaxj1n401fp4"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/EventBlock/event_block.gd" id="1"]
+[ext_resource type="StyleBox" uid="uid://cl75ikyq2is7c" path="res://addons/dialogic/Editor/Events/styles/unselected_stylebox.tres" id="2_axj84"]
+[ext_resource type="Texture2D" uid="uid://dybg3l5pwetne" path="res://addons/dialogic/Editor/Images/plugin-icon.svg" id="6"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_otutu"]
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="Image" id="Image_mem38"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_rc1wh"]
+image = SubResource("Image_mem38")
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ee4ub"]
+
+[node name="EventNode" type="MarginContainer"]
+anchors_preset = 10
+anchor_right = 1.0
+grow_horizontal = 2
+size_flags_horizontal = 3
+size_flags_vertical = 9
+focus_mode = 1
+script = ExtResource("1")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+mouse_filter = 2
+theme_override_styles/panel = ExtResource("2_axj84")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Header" type="HBoxContainer" parent="PanelContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="IconPanel" type="Panel" parent="PanelContainer/VBoxContainer/Header"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+mouse_filter = 1
+mouse_default_cursor_shape = 6
+theme_override_styles/panel = SubResource("StyleBoxFlat_otutu")
+
+[node name="IconTexture" type="TextureRect" parent="PanelContainer/VBoxContainer/Header/IconPanel"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 0
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+texture = ExtResource("6")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="Warning" type="TextureRect" parent="PanelContainer/VBoxContainer/Header/IconPanel"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 0
+offset_left = -5.5
+offset_top = -11.0
+offset_right = 12.1
+offset_bottom = 6.6
+texture = SubResource("ImageTexture_rc1wh")
+stretch_mode = 5
+
+[node name="HeaderContent" type="HBoxContainer" parent="PanelContainer/VBoxContainer/Header"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="ToggleBodyVisibilityButton" type="Button" parent="PanelContainer/VBoxContainer/Header"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(20, 0)
+layout_mode = 2
+size_flags_horizontal = 0
+tooltip_text = "Fold/Unfold Settings"
+theme_override_styles/normal = SubResource("StyleBoxEmpty_ee4ub")
+theme_override_styles/hover = SubResource("StyleBoxEmpty_ee4ub")
+theme_override_styles/pressed = SubResource("StyleBoxEmpty_ee4ub")
+theme_override_styles/disabled = SubResource("StyleBoxEmpty_ee4ub")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_ee4ub")
+toggle_mode = true
+icon = SubResource("ImageTexture_rc1wh")
+flat = true
+
+[node name="ToggleChildrenVisibilityButton" type="Button" parent="PanelContainer/VBoxContainer/Header"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 10
+tooltip_text = "Collapse Contained Events"
+toggle_mode = true
+icon = SubResource("ImageTexture_rc1wh")
+flat = true
+
+[node name="Body" type="MarginContainer" parent="PanelContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 4
+
+[node name="BodyContent" type="VBoxContainer" parent="PanelContainer/VBoxContainer/Body"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+mouse_filter = 2
+
+[connection signal="gui_input" from="." to="." method="_on_EventNode_gui_input"]
+[connection signal="toggled" from="PanelContainer/VBoxContainer/Header/ToggleBodyVisibilityButton" to="." method="_on_ToggleBodyVisibility_toggled"]
--- /dev/null
+@tool
+extends PopupMenu
+
+var current_event: Node = null
+
+func _ready() -> void:
+ clear()
+ add_icon_item(get_theme_icon("Duplicate", "EditorIcons"), "Duplicate")
+ add_separator()
+ add_icon_item(get_theme_icon("Help", "EditorIcons"), "Documentation")
+ add_icon_item(get_theme_icon("CodeHighlighter", "EditorIcons"), "Open Code")
+ add_separator()
+ add_icon_item(get_theme_icon("ArrowUp", "EditorIcons"), "Move up")
+ add_icon_item(get_theme_icon("ArrowDown", "EditorIcons"), "Move down")
+ add_separator()
+ add_icon_item(get_theme_icon("Remove", "EditorIcons"), "Delete")
+
+ var menu_background := StyleBoxFlat.new()
+ menu_background.bg_color = get_parent().get_theme_color("base_color", "Editor")
+ add_theme_stylebox_override('panel', menu_background)
+ add_theme_stylebox_override('hover', get_theme_stylebox("FocusViewport", "EditorStyles"))
+ add_theme_color_override('font_color_hover', get_parent().get_theme_color("accent_color", "Editor"))
--- /dev/null
+@tool
+extends PanelContainer
+
+## Event block field part for the Array field.
+
+signal value_changed()
+
+var value_field: Node
+var value_type: int = -1
+
+var current_value: Variant
+
+func _ready() -> void:
+ %FlexValue.value_changed.connect(emit_signal.bind("value_changed"))
+ %Delete.icon = get_theme_icon("Remove", "EditorIcons")
+
+
+func set_value(value:Variant):
+ %FlexValue.set_value(value)
+
+
+func get_value() -> Variant:
+ return %FlexValue.current_value
+
+
+func _on_delete_pressed() -> void:
+ queue_free()
+ value_changed.emit()
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://ch4j2lesn1sis"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/array_part.gd" id="1"]
+[ext_resource type="PackedScene" uid="uid://dl08ubinx6ugu" path="res://addons/dialogic/Editor/Events/Fields/field_flex_value.tscn" id="3_s4j7i"]
+
+[sub_resource type="Image" id="Image_dcsrk"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
+image = SubResource("Image_dcsrk")
+
+[node name="ArrayValue" type="PanelContainer"]
+offset_left = 2.0
+offset_right = 76.0
+offset_bottom = 24.0
+theme_type_variation = &"DialogicEventEditGroup"
+script = ExtResource("1")
+
+[node name="Value" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="FlexValue" parent="Value" instance=ExtResource("3_s4j7i")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Delete" type="Button" parent="Value"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Remove"
+icon = SubResource("ImageTexture_cpbga")
+flat = true
+
+[connection signal="pressed" from="Value/Delete" to="." method="_on_delete_pressed"]
--- /dev/null
+@tool
+extends PanelContainer
+
+## Event block field part for the Dictionary field.
+
+signal value_changed()
+
+
+func set_key(value:String) -> void:
+ %Key.text = str(value)
+
+
+func get_key() -> String:
+ return %Key.text
+
+
+func set_value(value:Variant) -> void:
+ %FlexValue.set_value(value)
+
+
+func get_value() -> Variant:
+ return %FlexValue.current_value
+
+
+func _ready() -> void:
+ %Delete.icon = get_theme_icon("Remove", "EditorIcons")
+
+
+func focus_key() -> void:
+ %Key.grab_focus()
+
+
+func _on_key_text_changed(new_text: String) -> void:
+ value_changed.emit()
+
+
+func _on_flex_value_value_changed() -> void:
+ value_changed.emit()
+
+
+func _on_delete_pressed() -> void:
+ queue_free()
+ value_changed.emit()
+
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://b27yweami3mxi"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/dictionary_part.gd" id="2_q88pg"]
+[ext_resource type="PackedScene" uid="uid://dl08ubinx6ugu" path="res://addons/dialogic/Editor/Events/Fields/field_flex_value.tscn" id="3_p082d"]
+
+[sub_resource type="Image" id="Image_dcsrk"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
+image = SubResource("Image_dcsrk")
+
+[node name="DictionaryPart" type="PanelContainer"]
+offset_left = 1.0
+offset_top = -1.0
+offset_right = 131.0
+offset_bottom = 32.0
+theme_type_variation = &"DialogicEventEditGroup"
+script = ExtResource("2_q88pg")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Key" type="LineEdit" parent="HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_type_variation = &"DialogicEventEdit"
+expand_to_text_length = true
+select_all_on_focus = true
+
+[node name="Label" type="Label" parent="HBox"]
+layout_mode = 2
+text = ":"
+
+[node name="FlexValue" parent="HBox" instance=ExtResource("3_p082d")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Delete" type="Button" parent="HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Remove"
+icon = SubResource("ImageTexture_cpbga")
+flat = true
+
+[connection signal="text_changed" from="HBox/Key" to="." method="_on_key_text_changed"]
+[connection signal="value_changed" from="HBox/FlexValue" to="." method="_on_flex_value_value_changed"]
+[connection signal="pressed" from="HBox/Delete" to="." method="_on_delete_pressed"]
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for editing arrays.
+
+
+const ArrayValue := "res://addons/dialogic/Editor/Events/Fields/array_part.tscn"
+
+
+func _ready() -> void:
+ %Add.icon = get_theme_icon("Add", "EditorIcons")
+ %Add.pressed.connect(_on_AddButton_pressed)
+
+
+func _set_value(value:Variant) -> void:
+ value = value as Array
+ for child in get_children():
+ if child != %Add:
+ child.queue_free()
+
+ for item in value:
+ var x: Node = load(ArrayValue).instantiate()
+ add_child(x)
+ x.set_value(item)
+ x.value_changed.connect(recalculate_values)
+ move_child(%Add, -1)
+
+
+func _on_value_changed(value:Variant) -> void:
+ value_changed.emit(property_name, value)
+
+
+func recalculate_values() -> void:
+ var arr := []
+ for child in get_children():
+ if child != %Add and !child.is_queued_for_deletion():
+ arr.append(child.get_value())
+ _on_value_changed(arr)
+
+
+func _on_AddButton_pressed() -> void:
+ var x: Control = load(ArrayValue).instantiate()
+ add_child(x)
+ x.set_value("")
+ x.value_changed.connect(recalculate_values)
+ recalculate_values()
+ move_child(%Add, -1)
+
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://btmy7ageqpyq1"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_array.gd" id="2"]
+
+[sub_resource type="Image" id="Image_dcsrk"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
+image = SubResource("Image_dcsrk")
+
+[node name="Field_Array" type="HFlowContainer"]
+offset_right = 329.0
+offset_bottom = 256.0
+size_flags_horizontal = 3
+script = ExtResource("2")
+
+[node name="Add" type="Button" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add value"
+icon = SubResource("ImageTexture_cpbga")
+flat = true
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+
+var file_path: String
+
+
+func _ready() -> void:
+ self.pressed.connect(_on_pressed)
+ %AudioStreamPlayer.finished.connect(_on_finished)
+
+
+#region OVERWRITES
+################################################################################
+
+
+## To be overwritten
+func _set_value(value:Variant) -> void:
+ file_path = value
+ self.disabled = file_path.is_empty()
+ _stop()
+
+#endregion
+
+
+#region SIGNAL METHODS
+################################################################################
+
+func _on_pressed() -> void:
+ if %AudioStreamPlayer.playing:
+ _stop()
+ elif not file_path.is_empty():
+ _play()
+
+
+func _on_finished() -> void:
+ _stop()
+
+#endregion
+
+
+func _stop() -> void:
+ %AudioStreamPlayer.stop()
+ %AudioStreamPlayer.stream = null
+ self.icon = get_theme_icon("Play", "EditorIcons")
+
+
+func _play() -> void:
+ if ResourceLoader.exists(file_path):
+ %AudioStreamPlayer.stream = load(file_path)
+ %AudioStreamPlayer.play()
+ self.icon = get_theme_icon("Stop", "EditorIcons")
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://dotvrsumm5y5c"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_audio_preview.gd" id="1_7wm54"]
+
+[node name="Field_Audio_Preview" type="Button"]
+offset_right = 8.0
+offset_bottom = 8.0
+flat = true
+script = ExtResource("1_7wm54")
+
+[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
+unique_name_in_owner = true
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for boolean values.
+
+#region MAIN METHODS
+################################################################################
+
+func _ready() -> void:
+ add_theme_color_override("icon_normal_color", get_theme_color("disabled_font_color", "Editor"))
+ add_theme_color_override("icon_hover_color", get_theme_color("warning_color", "Editor"))
+ add_theme_color_override("icon_pressed_color", get_theme_color("icon_saturation", "Editor"))
+ add_theme_color_override("icon_hover_pressed_color", get_theme_color("warning_color", "Editor"))
+ add_theme_color_override("icon_focus_color", get_theme_color("disabled_font_color", "Editor"))
+ self.toggled.connect(_on_value_changed)
+
+
+func _load_display_info(info:Dictionary) -> void:
+ if info.has('editor_icon'):
+ if not is_inside_tree():
+ await ready
+ self.icon = callv('get_theme_icon', info.editor_icon)
+ else:
+ self.icon = info.get('icon', null)
+
+
+func _set_value(value:Variant) -> void:
+ self.button_pressed = true if value else false
+
+#endregion
+
+
+#region SIGNAL METHODS
+################################################################################
+
+func _on_value_changed(value:bool) -> void:
+ value_changed.emit(property_name, value)
+#endregion
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://iypxcctv080u"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_bool_button.gd" id="1_t1n1f"]
+
+[node name="Field_BoolButton" type="Button"]
+theme_override_colors/icon_normal_color = Color(0, 0, 0, 1)
+theme_override_colors/icon_pressed_color = Color(0, 0, 0, 1)
+theme_override_colors/icon_hover_color = Color(0, 0, 0, 1)
+theme_override_colors/icon_hover_pressed_color = Color(0, 0, 0, 1)
+theme_override_colors/icon_focus_color = Color(0, 0, 0, 1)
+toggle_mode = true
+flat = true
+script = ExtResource("1_t1n1f")
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for boolean values.
+
+#region MAIN METHODS
+################################################################################
+func _ready() -> void:
+ self.toggled.connect(_on_value_changed)
+
+
+func _load_display_info(info:Dictionary) -> void:
+ pass
+
+
+func _set_value(value:Variant) -> void:
+ match DialogicUtil.get_variable_value_type(value):
+ DialogicUtil.VarTypes.STRING:
+ self.button_pressed = value and not value.strip_edges() == "false"
+ _:
+ self.button_pressed = value and true
+#endregion
+
+
+#region SIGNAL METHODS
+################################################################################
+func _on_value_changed(value:bool) -> void:
+ value_changed.emit(property_name, value)
+
+#endregion
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://dm5hxmhyyxgq"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_bool_check.gd" id="1_ckmtx"]
+
+[node name="Field_BoolCheck" type="CheckButton"]
+offset_right = 44.0
+offset_bottom = 24.0
+script = ExtResource("1_ckmtx")
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for color values.
+
+#region MAIN METHODS
+################################################################################
+
+func _ready() -> void:
+ self.color_changed.connect(_on_value_changed)
+
+
+func _load_display_info(info:Dictionary) -> void:
+ self.edit_alpha = info.get("edit_alpha", true)
+
+
+func _set_value(value:Variant) -> void:
+ if value is Color:
+ self.color = Color(value)
+
+#endregion
+
+
+#region SIGNAL METHODS
+################################################################################
+
+func _on_value_changed(value: Color) -> void:
+ value_changed.emit(property_name, value)
+
+#endregion
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://4e0kjekan5e7"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_color.gd" id="1_l666a"]
+
+[node name="Field_Color" type="ColorPickerButton"]
+custom_minimum_size = Vector2(48, 0)
+offset_right = 64.0
+offset_bottom = 31.0
+theme_type_variation = &"DialogicEventEdit"
+text = " "
+color = Color(1, 1, 1, 1)
+script = ExtResource("1_l666a")
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for displaying conditions in either a simple or complex way.
+
+var _current_value1: Variant = ""
+var _current_value2: Variant = ""
+
+#region MAIN METHODS
+################################################################################
+
+func _set_value(value:Variant) -> void:
+ var too_complex := is_too_complex(value)
+ %ToggleComplex.disabled = too_complex
+ %ToggleComplex.button_pressed = too_complex
+ %ComplexEditor.visible = too_complex
+ %SimpleEditor.visible = !too_complex
+ %ComplexEditor.text = value
+ if not too_complex:
+ load_simple_editor(value)
+
+
+
+func _autofocus() -> void:
+ %Value1Variable.grab_focus()
+
+#endregion
+
+func _ready() -> void:
+ for i in [%Value1Type, %Value2Type]:
+ i.options = [{
+ 'label': 'String',
+ 'icon': ["String", "EditorIcons"],
+ 'value': 0
+ },{
+ 'label': 'Number',
+ 'icon': ["float", "EditorIcons"],
+ 'value': 1
+ },{
+ 'label': 'Variable',
+ 'icon': load("res://addons/dialogic/Editor/Images/Pieces/variable.svg"),
+ 'value': 2
+ },{
+ 'label': 'Bool',
+ 'icon': ["bool", "EditorIcons"],
+ 'value': 3
+ },{
+ 'label': 'Expression',
+ 'icon': ["Variant", "EditorIcons"],
+ 'value': 4
+ }]
+ i.symbol_only = true
+ i.value_changed.connect(value_type_changed.bind(i.name))
+ i.value_changed.connect(something_changed)
+ i.tooltip_text = "Change type"
+
+
+ for i in [%Value1Variable, %Value2Variable]:
+ i.get_suggestions_func = get_variable_suggestions
+ i.value_changed.connect(something_changed)
+
+ %Value1Number.value_changed.connect(something_changed)
+ %Value2Number.value_changed.connect(something_changed)
+ %Value1Text.value_changed.connect(something_changed)
+ %Value2Text.value_changed.connect(something_changed)
+ %Value1Bool.value_changed.connect(something_changed)
+ %Value2Bool.value_changed.connect(something_changed)
+
+ %ToggleComplex.icon = get_theme_icon("Enum", "EditorIcons")
+
+ %Operator.value_changed.connect(something_changed)
+ %Operator.options = [
+ {'label': '==', 'value': '=='},
+ {'label': '>', 'value': '>'},
+ {'label': '<', 'value': '<'},
+ {'label': '<=', 'value': '<='},
+ {'label': '>=', 'value': '>='},
+ {'label': '!=', 'value': '!='}
+ ]
+
+
+func load_simple_editor(condition_string:String) -> void:
+ var data := complex2simple(condition_string)
+ %Value1Type.set_value(get_value_type(data[0], 2))
+ _current_value1 = data[0]
+ value_type_changed('', get_value_type(data[0], 2), 'Value1')
+ %Operator.set_value(data[1].strip_edges())
+ %Value2Type.set_value(get_value_type(data[2], 0))
+ _current_value2 = data[2]
+ value_type_changed('', get_value_type(data[2], 0), 'Value2')
+
+
+func value_type_changed(property:String, value_type:int, value_name:String) -> void:
+ value_name = value_name.trim_suffix('Type')
+ get_node('%'+value_name+'Variable').hide()
+ get_node('%'+value_name+'Text').hide()
+ get_node('%'+value_name+'Number').hide()
+ get_node('%'+value_name+'Bool').hide()
+ var current_val: Variant = ""
+ if '1' in value_name:
+ current_val = _current_value1
+ else:
+ current_val = _current_value2
+ match value_type:
+ 0:
+ get_node('%'+value_name+'Text').show()
+ get_node('%'+value_name+'Text').set_value(trim_value(current_val, value_type))
+ 1:
+ get_node('%'+value_name+'Number').show()
+ get_node('%'+value_name+'Number').set_value(float(current_val.strip_edges()))
+ 2:
+ get_node('%'+value_name+'Variable').show()
+ get_node('%'+value_name+'Variable').set_value(trim_value(current_val, value_type))
+ 3:
+ get_node('%'+value_name+'Bool').show()
+ get_node('%'+value_name+'Bool').set_value(trim_value(current_val, value_type))
+ 4:
+ get_node('%'+value_name+'Text').show()
+ get_node('%'+value_name+'Text').set_value(str(current_val))
+
+
+func get_value_type(value:String, default:int) -> int:
+ value = value.strip_edges()
+ if value.begins_with('"') and value.ends_with('"') and value.count('"')-value.count('\\"') == 2:
+ return 0
+ elif value.begins_with('{') and value.ends_with('}') and value.count('{') == 1:
+ return 2
+ elif value == "true" or value == "false":
+ return 3
+ else:
+ if value.is_empty():
+ return default
+ if value.is_valid_float():
+ return 1
+ else:
+ return 4
+
+
+func prep_value(value:Variant, value_type:int) -> String:
+ if value != null: value = str(value)
+ else: value = ""
+ value = value.strip_edges()
+ match value_type:
+ 0: return '"'+value.replace('"', '\\"')+'"'
+ 2: return '{'+value+'}'
+ _: return value
+
+
+func trim_value(value:Variant, value_type:int) -> String:
+ value = value.strip_edges()
+ match value_type:
+ 0: return value.trim_prefix('"').trim_suffix('"').replace('\\"', '"')
+ 2: return value.trim_prefix('{').trim_suffix('}')
+ 3:
+ if value == "true" or (value and (typeof(value) != TYPE_STRING or value != "false")):
+ return "true"
+ else:
+ return "false"
+ _: return value
+
+
+func something_changed(fake_arg1=null, fake_arg2 = null):
+ if %ComplexEditor.visible:
+ value_changed.emit(property_name, %ComplexEditor.text)
+ return
+
+
+ match %Value1Type.current_value:
+ 0: _current_value1 = prep_value(%Value1Text.text, %Value1Type.current_value)
+ 1: _current_value1 = str(%Value1Number.get_value())
+ 2: _current_value1 = prep_value(%Value1Variable.current_value, %Value1Type.current_value)
+ 3: _current_value1 = prep_value(%Value1Bool.button_pressed, %Value1Type.current_value)
+ _: _current_value1 = prep_value(%Value1Text.text, %Value1Type.current_value)
+
+ match %Value2Type.current_value:
+ 0: _current_value2 = prep_value(%Value2Text.text, %Value2Type.current_value)
+ 1: _current_value2 = str(%Value2Number.get_value())
+ 2: _current_value2 = prep_value(%Value2Variable.current_value, %Value2Type.current_value)
+ 3: _current_value2 = prep_value(%Value2Bool.button_pressed, %Value2Type.current_value)
+ _: _current_value2 = prep_value(%Value2Text.text, %Value2Type.current_value)
+
+ if event_resource:
+ if not %Operator.text in ['==', '!='] and get_value_type(_current_value2, 0) in [0, 3]:
+ event_resource.ui_update_warning.emit("This operator doesn't work with strings and booleans.")
+ else:
+ event_resource.ui_update_warning.emit("")
+
+ value_changed.emit(property_name, get_simple_condition())
+
+
+func is_too_complex(condition:String) -> bool:
+ if condition.strip_edges().is_empty():
+ return false
+
+ var comparison_count: int = 0
+ for i in ['==', '!=', '<=', '<', '>', '>=']:
+ comparison_count += condition.count(i)
+ if comparison_count == 1:
+ return false
+
+ return true
+
+
+## Combines the info from the simple editor fields into a string condition
+func get_simple_condition() -> String:
+ return _current_value1 +" "+ %Operator.text +" "+ _current_value2
+
+
+func complex2simple(condition:String) -> Array:
+ if is_too_complex(condition) or condition.strip_edges().is_empty():
+ return ['', '==','']
+
+ for i in ['==', '!=', '<=', '<', '>', '>=']:
+ if i in condition:
+ var cond_split := Array(condition.split(i, false))
+ return [cond_split[0], i, cond_split[1]]
+
+ return ['', '==','']
+
+
+func _on_toggle_complex_toggled(button_pressed:bool) -> void:
+ if button_pressed:
+ %ComplexEditor.show()
+ %SimpleEditor.hide()
+ %ComplexEditor.text = get_simple_condition()
+ else:
+ if !is_too_complex(%ComplexEditor.text):
+ %ComplexEditor.hide()
+ %SimpleEditor.show()
+ load_simple_editor(%ComplexEditor.text)
+
+
+func _on_complex_editor_text_changed(new_text:String) -> void:
+ %ToggleComplex.disabled = is_too_complex(%ComplexEditor.text)
+ something_changed()
+
+
+func get_variable_suggestions(filter:String) -> Dictionary:
+ var suggestions := {}
+ var vars: Dictionary = ProjectSettings.get_setting('dialogic/variables', {})
+ for var_path in DialogicUtil.list_variables(vars):
+ suggestions[var_path] = {'value':var_path, 'editor_icon':["ClassList", "EditorIcons"]}
+ return suggestions
+
+
+func _on_value_1_variable_value_changed(property_name: Variant, value: Variant) -> void:
+ var type := DialogicUtil.get_variable_type(value)
+ match type:
+ DialogicUtil.VarTypes.BOOL:
+ if not %Operator.text in ["==", "!="]:
+ %Operator.text = "=="
+ if get_value_type(_current_value2, 3) in [0, 1]:
+ %Value2Type.insert_options()
+ %Value2Type.index_pressed(3)
+ DialogicUtil.VarTypes.STRING:
+ if not %Operator.text in ["==", "!="]:
+ %Operator.text = "=="
+ if get_value_type(_current_value2, 0) in [1, 3]:
+ %Value2Type.insert_options()
+ %Value2Type.index_pressed(0)
+ DialogicUtil.VarTypes.FLOAT, DialogicUtil.VarTypes.INT:
+ if get_value_type(_current_value2, 1) in [0,3]:
+ %Value2Type.insert_options()
+ %Value2Type.index_pressed(1)
+
+ something_changed()
+
--- /dev/null
+[gd_scene load_steps=9 format=3 uid="uid://ir6334lqtuwt"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_condition.gd" id="1_owjj0"]
+[ext_resource type="PackedScene" uid="uid://d3bhehatwoio" path="res://addons/dialogic/Editor/Events/Fields/field_options_fixed.tscn" id="2_f6v80"]
+[ext_resource type="PackedScene" uid="uid://c0vkcehgjsjy" path="res://addons/dialogic/Editor/Events/Fields/field_text_singleline.tscn" id="3_3kfwc"]
+[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="4_6q3a6"]
+[ext_resource type="PackedScene" uid="uid://dm5hxmhyyxgq" path="res://addons/dialogic/Editor/Events/Fields/field_bool_check.tscn" id="5_1x02a"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="6_5a2xd"]
+
+[sub_resource type="Image" id="Image_je1w7"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_81s3d"]
+image = SubResource("Image_je1w7")
+
+[node name="Field_Condition" type="HBoxContainer"]
+offset_right = 77.0
+offset_bottom = 31.0
+script = ExtResource("1_owjj0")
+
+[node name="SimpleEditor" type="HBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value1Type" parent="SimpleEditor" instance=ExtResource("2_f6v80")]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Change type"
+text = ""
+
+[node name="Value1Text" parent="SimpleEditor" instance=ExtResource("3_3kfwc")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value1Number" parent="SimpleEditor" instance=ExtResource("4_6q3a6")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value1Bool" parent="SimpleEditor" instance=ExtResource("5_1x02a")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value1Variable" parent="SimpleEditor" instance=ExtResource("6_5a2xd")]
+unique_name_in_owner = true
+layout_mode = 2
+placeholder_text = "Variable"
+
+[node name="Operator" parent="SimpleEditor" instance=ExtResource("2_f6v80")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value2Type" parent="SimpleEditor" instance=ExtResource("2_f6v80")]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Change type"
+text = ""
+
+[node name="Value2Text" parent="SimpleEditor" instance=ExtResource("3_3kfwc")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value2Number" parent="SimpleEditor" instance=ExtResource("4_6q3a6")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Value2Variable" parent="SimpleEditor" instance=ExtResource("6_5a2xd")]
+unique_name_in_owner = true
+layout_mode = 2
+placeholder_text = "Variable"
+
+[node name="Value2Bool" parent="SimpleEditor" instance=ExtResource("5_1x02a")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="ComplexEditor" type="LineEdit" parent="."]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+mouse_filter = 1
+theme_type_variation = &"DialogicEventEdit"
+text = "VAR.Player.Health > 20 and VAR.Counter < 3 and randi()%3 == 2"
+placeholder_text = "Enter condition"
+expand_to_text_length = true
+
+[node name="ToggleComplex" type="Button" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Use complex expression"
+toggle_mode = true
+icon = SubResource("ImageTexture_81s3d")
+
+[connection signal="value_changed" from="SimpleEditor/Value1Variable" to="." method="_on_value_1_variable_value_changed"]
+[connection signal="text_changed" from="ComplexEditor" to="." method="_on_complex_editor_text_changed"]
+[connection signal="toggled" from="ToggleComplex" to="." method="_on_toggle_complex_toggled"]
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for editing dictionaries.
+
+const DictionaryValue = "res://addons/dialogic/Editor/Events/Fields/dictionary_part.tscn"
+
+func _ready() -> void:
+ %Add.icon = get_theme_icon("Add", "EditorIcons")
+
+
+func _set_value(value:Variant) -> void:
+ for child in get_children():
+ if child != %Add:
+ child.queue_free()
+
+ var dict: Dictionary
+
+ # attempt to take dictionary values, create a fresh one if not possible
+ if typeof(value) == TYPE_DICTIONARY:
+ dict = value
+ elif typeof(value) == TYPE_STRING:
+ if value.begins_with('{'):
+ var result: Variant = JSON.parse_string(value)
+ if result != null:
+ dict = result as Dictionary
+
+ var keys := dict.keys()
+ var values := dict.values()
+
+ for index in dict.size():
+ var x: Node = load(DictionaryValue).instantiate()
+ add_child(x)
+ x.set_value(values[index])
+ x.set_key(keys[index])
+ x.value_changed.connect(recalculate_values)
+ move_child(%Add, -1)
+
+
+func _on_value_changed(value:Variant) -> void:
+ value_changed.emit(property_name, value)
+
+
+func recalculate_values() -> void:
+ var dict := {}
+ for child in get_children():
+ if child != %Add and !child.is_queued_for_deletion():
+ dict[child.get_key()] = child.get_value()
+ _on_value_changed(dict)
+
+
+func _on_AddButton_pressed() -> void:
+ var x: Control = load(DictionaryValue).instantiate()
+ add_child(x)
+ x.set_key("")
+ x.set_value("")
+ x.value_changed.connect(recalculate_values)
+ x.focus_key()
+ recalculate_values()
+ move_child(%Add, -1)
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://c74bnmhefu72w"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_dictionary.gd" id="1_p4kmu"]
+
+[sub_resource type="Image" id="Image_dcsrk"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
+image = SubResource("Image_dcsrk")
+
+[node name="Field_Dictionary" type="HFlowContainer"]
+size_flags_horizontal = 3
+script = ExtResource("1_p4kmu")
+
+[node name="Add" type="Button" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add key-value pair"
+icon = SubResource("ImageTexture_cpbga")
+flat = true
+
+[connection signal="pressed" from="Add" to="." method="_on_AddButton_pressed"]
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for selecting a file or directory.
+
+#region VARIABLES
+################################################################################
+
+@export var file_filter := ""
+@export var placeholder := ""
+@export var file_mode: EditorFileDialog.FileMode = EditorFileDialog.FILE_MODE_OPEN_FILE
+var resource_icon: Texture:
+ get:
+ return resource_icon
+ set(new_icon):
+ resource_icon = new_icon
+ %Icon.texture = new_icon
+ if new_icon == null:
+ %Field.theme_type_variation = ""
+ else:
+ %Field.theme_type_variation = "LineEditWithIcon"
+
+var max_width := 200
+var current_value: String
+var hide_reset := false
+
+#endregion
+
+
+#region MAIN METHODS
+################################################################################
+
+func _ready() -> void:
+ $FocusStyle.add_theme_stylebox_override('panel', get_theme_stylebox('focus', 'DialogicEventEdit'))
+
+ %OpenButton.icon = get_theme_icon("Folder", "EditorIcons")
+ %OpenButton.button_down.connect(_on_OpenButton_pressed)
+
+ %ClearButton.icon = get_theme_icon("Reload", "EditorIcons")
+ %ClearButton.button_up.connect(clear_path)
+ %ClearButton.visible = !hide_reset
+
+ %Field.set_drag_forwarding(Callable(), self._can_drop_data_fw, self._drop_data_fw)
+ %Field.placeholder_text = placeholder
+
+
+func _load_display_info(info:Dictionary) -> void:
+ file_filter = info.get('file_filter', '')
+ placeholder = info.get('placeholder', '')
+ resource_icon = info.get('icon', null)
+ await ready
+
+ if resource_icon == null and info.has('editor_icon'):
+ resource_icon = callv('get_theme_icon', info.editor_icon)
+
+
+func _set_value(value: Variant) -> void:
+ current_value = value
+ var text: String = value
+
+ if file_mode != EditorFileDialog.FILE_MODE_OPEN_DIR:
+ text = value.get_file()
+ %Field.tooltip_text = value
+
+ if %Field.get_theme_font('font').get_string_size(
+ text, 0, -1,
+ %Field.get_theme_font_size('font_size')).x > max_width:
+ %Field.expand_to_text_length = false
+ %Field.custom_minimum_size.x = max_width
+ %Field.size.x = 0
+ else:
+ %Field.custom_minimum_size.x = 0
+ %Field.expand_to_text_length = true
+
+ if not %Field.text == text:
+ value_changed.emit(property_name, current_value)
+ %Field.text = text
+
+ %ClearButton.visible = not value.is_empty() and not hide_reset
+
+
+#endregion
+
+
+#region BUTTONS
+################################################################################
+
+func _on_OpenButton_pressed() -> void:
+ find_parent('EditorView').godot_file_dialog(_on_file_dialog_selected, file_filter, file_mode, "Open "+ property_name)
+
+
+func _on_file_dialog_selected(path:String) -> void:
+ _set_value(path)
+ value_changed.emit(property_name, path)
+
+
+func clear_path() -> void:
+ _set_value("")
+ value_changed.emit(property_name, "")
+
+#endregion
+
+
+#region DRAG AND DROP
+################################################################################
+
+func _can_drop_data_fw(_at_position: Vector2, data: Variant) -> bool:
+ if typeof(data) == TYPE_DICTIONARY and data.has('files') and len(data.files) == 1:
+
+ if file_filter:
+
+ if '*.'+data.files[0].get_extension() in file_filter:
+ return true
+
+ else: return true
+
+ return false
+
+
+func _drop_data_fw(_at_position: Vector2, data: Variant) -> void:
+ var file: String = data.files[0]
+ _on_file_dialog_selected(file)
+
+#endregion
+
+
+#region VISUALS FOR FOCUS
+################################################################################
+
+func _on_field_focus_entered() -> void:
+ $FocusStyle.show()
+
+
+func _on_field_focus_exited() -> void:
+ $FocusStyle.hide()
+ var field_text: String = %Field.text
+ if current_value == field_text or (file_mode != EditorFileDialog.FILE_MODE_OPEN_DIR and current_value.get_file() == field_text):
+ return
+ _on_file_dialog_selected(field_text)
+
+#endregion
--- /dev/null
+[gd_scene load_steps=8 format=3 uid="uid://7mvxuaulctcq"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_file.gd" id="1_0grcf"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_tr837"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wq6bt"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_6b7on"]
+
+[sub_resource type="Image" id="Image_bpoe1"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_dkuon"]
+image = SubResource("Image_bpoe1")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yv1pn"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 0.365, 0.365, 1)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_detail = 1
+
+[node name="Field_File" type="MarginContainer"]
+offset_right = 314.0
+offset_bottom = 40.0
+theme_type_variation = &"DialogicEventEdit"
+script = ExtResource("1_0grcf")
+
+[node name="BG" type="PanelContainer" parent="."]
+layout_mode = 2
+theme_type_variation = &"DialogicEventEdit"
+
+[node name="HBox" type="HBoxContainer" parent="BG"]
+layout_mode = 2
+alignment = 2
+
+[node name="Icon" type="TextureRect" parent="BG/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+mouse_filter = 2
+
+[node name="Field" type="LineEdit" parent="BG/HBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+mouse_filter = 1
+theme_override_styles/normal = SubResource("StyleBoxEmpty_tr837")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_wq6bt")
+theme_override_styles/read_only = SubResource("StyleBoxEmpty_6b7on")
+expand_to_text_length = true
+
+[node name="OpenButton" type="Button" parent="BG/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = SubResource("ImageTexture_dkuon")
+flat = true
+
+[node name="ClearButton" type="Button" parent="BG/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = SubResource("ImageTexture_dkuon")
+flat = true
+
+[node name="FocusStyle" type="Panel" parent="."]
+visible = false
+layout_mode = 2
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_yv1pn")
+
+[connection signal="focus_entered" from="BG/HBox/Field" to="." method="_on_field_focus_entered"]
+[connection signal="focus_exited" from="BG/HBox/Field" to="." method="_on_field_focus_exited"]
+[connection signal="text_submitted" from="BG/HBox/Field" to="." method="_on_file_dialog_selected"]
--- /dev/null
+@tool
+extends HBoxContainer
+
+## Event block field part for a value that can change type.
+
+signal value_changed
+
+var value_field: Node
+var value_type: int = -1
+
+var current_value: Variant
+
+func _ready() -> void:
+ %ValueType.options = [{
+ 'label': 'String',
+ 'icon': ["String", "EditorIcons"],
+ 'value': TYPE_STRING
+ },{
+ 'label': 'Number (int)',
+ 'icon': ["int", "EditorIcons"],
+ 'value': TYPE_INT
+ },{
+ 'label': 'Number (float)',
+ 'icon': ["float", "EditorIcons"],
+ 'value': TYPE_FLOAT
+ },{
+ 'label': 'Boolean',
+ 'icon': ["bool", "EditorIcons"],
+ 'value': TYPE_BOOL
+ },{
+ 'label': 'Expression',
+ 'icon': ["Variant", "EditorIcons"],
+ 'value': TYPE_MAX
+ }
+ ]
+ %ValueType.symbol_only = true
+ %ValueType.value_changed.connect(_on_type_changed.bind())
+ %ValueType.tooltip_text = "Change type"
+
+
+func set_value(value:Variant):
+ change_field_type(deduce_type(value))
+ %ValueType.set_value(deduce_type(value))
+ current_value = value
+ match value_type:
+ TYPE_BOOL:
+ value_field.button_pressed = value
+ TYPE_STRING:
+ value_field.text = value
+ TYPE_FLOAT, TYPE_INT:
+ value_field.set_value(value)
+ TYPE_MAX, _:
+ value_field.text = value.trim_prefix('@')
+
+
+func deduce_type(value:Variant) -> int:
+ if value is String and value.begins_with('@'):
+ return TYPE_MAX
+ else:
+ return typeof(value)
+
+
+func _on_type_changed(prop:String, type:Variant) -> void:
+ if type == value_type:
+ return
+
+ match type:
+ TYPE_BOOL:
+ if typeof(current_value) == TYPE_STRING:
+ current_value = DialogicUtil.str_to_bool(current_value)
+ elif value_type == TYPE_FLOAT or value_type == TYPE_INT:
+ current_value = bool(current_value)
+ else:
+ current_value = true if current_value else false
+ set_value(current_value)
+ TYPE_STRING:
+ current_value = str(current_value).trim_prefix('@')
+ set_value(current_value)
+ TYPE_FLOAT:
+ current_value = float(current_value)
+ set_value(current_value)
+ TYPE_INT:
+ current_value = int(current_value)
+ set_value(current_value)
+ TYPE_MAX,_:
+ current_value = var_to_str(current_value)
+ set_value('@'+current_value)
+
+
+ emit_signal.call_deferred('value_changed')
+
+
+func get_value() -> Variant:
+ return current_value
+
+
+func _on_delete_pressed() -> void:
+ queue_free()
+ value_changed.emit()
+
+
+func change_field_type(type:int) -> void:
+ if type == value_type:
+ return
+
+ value_type = type
+
+ if value_field:
+ value_field.queue_free()
+ match type:
+ TYPE_BOOL:
+ value_field = CheckBox.new()
+ value_field.toggled.connect(_on_bool_toggled)
+ TYPE_STRING:
+ value_field = LineEdit.new()
+ value_field.theme_type_variation = "DialogicEventEdit"
+ value_field.text_changed.connect(_on_str_text_changed)
+ value_field.expand_to_text_length = true
+ TYPE_FLOAT, TYPE_INT:
+ value_field = load("res://addons/dialogic/Editor/Events/Fields/field_number.tscn").instantiate()
+ if type == TYPE_FLOAT:
+ value_field.use_float_mode()
+ else:
+ value_field.use_int_mode()
+ value_field.value_changed.connect(_on_number_value_changed.bind(type == TYPE_INT))
+ TYPE_MAX, _:
+ value_field = LineEdit.new()
+ value_field.expand_to_text_length = true
+ value_field.text_changed.connect(_on_expression_changed)
+ add_child(value_field)
+ move_child(value_field, 1)
+
+
+func _on_bool_toggled(value:bool) -> void:
+ current_value = value
+ value_changed.emit()
+
+
+func _on_str_text_changed(value:String) -> void:
+ current_value = value
+ value_changed.emit()
+
+
+func _on_expression_changed(value:String) -> void:
+ current_value = '@'+value
+ value_changed.emit()
+
+
+func _on_number_value_changed(prop:String, value:float, int := false) -> void:
+ if int:
+ current_value = int(value)
+ else:
+ current_value = value
+ value_changed.emit()
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://dl08ubinx6ugu"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_flex_value.gd" id="1_m5nnp"]
+[ext_resource type="PackedScene" uid="uid://d3bhehatwoio" path="res://addons/dialogic/Editor/Events/Fields/field_options_fixed.tscn" id="3_h10fc"]
+
+[node name="FlexValue" type="HBoxContainer"]
+offset_right = 65.0
+offset_bottom = 22.0
+script = ExtResource("1_m5nnp")
+
+[node name="ValueType" parent="." instance=ExtResource("3_h10fc")]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Change type"
+text = ""
--- /dev/null
+@tool
+class_name DialogicVisualEditorFieldNumber
+extends DialogicVisualEditorField
+
+## Event block field for integers and floats. Improved version of the native spinbox.
+
+@export var allow_string: bool = false
+@export var step: float = 0.1
+@export var enforce_step: bool = true
+@export var min: float = -INF
+@export var max: float = INF
+@export var value = 0.0
+@export var prefix: String = ""
+@export var suffix: String = ""
+
+var _is_holding_button: bool = false #For handling incrementing while holding key or click
+
+#region MAIN METHODS
+################################################################################
+
+func _ready() -> void:
+ if %Value.text.is_empty():
+ set_value(value)
+
+ update_prefix(prefix)
+ update_suffix(suffix)
+
+
+func _load_display_info(info: Dictionary) -> void:
+ match info.get('mode', 0):
+ 0: #FLOAT
+ use_float_mode(info.get('step', 0.1))
+ 1: #INT
+ use_int_mode(info.get('step', 1))
+ 2: #DECIBLE:
+ use_decibel_mode(info.get('step', step))
+
+ for option in info.keys():
+ match option:
+ 'min': min = info[option]
+ 'max': max = info[option]
+ 'prefix': update_prefix(info[option])
+ 'suffix': update_suffix(info[option])
+ 'step':
+ enforce_step = true
+ step = info[option]
+ 'hide_step_button': %Spin.hide()
+
+
+func _set_value(new_value: Variant) -> void:
+ _on_value_text_submitted(str(new_value), true)
+ %Value.tooltip_text = tooltip_text
+
+
+func _autofocus() -> void:
+ %Value.grab_focus()
+
+
+func get_value() -> float:
+ return value
+
+
+func use_float_mode(value_step: float = 0.1) -> void:
+ step = value_step
+ update_suffix("")
+ enforce_step = false
+
+
+func use_int_mode(value_step: float = 1) -> void:
+ step = value_step
+ update_suffix("")
+ enforce_step = true
+
+
+func use_decibel_mode(value_step: float = step) -> void:
+ max = 6
+ update_suffix("dB")
+ min = -80
+
+#endregion
+
+#region UI FUNCTIONALITY
+################################################################################
+var _stop_button_holding: Callable = func(button: BaseButton) -> void:
+ _is_holding_button = false
+ if button.button_up.get_connections().find(_stop_button_holding):
+ button.button_up.disconnect(_stop_button_holding)
+ if button.focus_exited.get_connections().find(_stop_button_holding):
+ button.focus_exited.disconnect(_stop_button_holding)
+ if button.mouse_exited.get_connections().find(_stop_button_holding):
+ button.mouse_exited.disconnect(_stop_button_holding)
+
+
+func _holding_button(value_direction: int, button: BaseButton) -> void:
+ if _is_holding_button:
+ return
+ if _stop_button_holding.get_bound_arguments_count() > 0:
+ _stop_button_holding.unbind(0)
+
+ _is_holding_button = true
+
+ #Ensure removal of our value changing routine when it shouldn't run anymore
+ button.button_up.connect(_stop_button_holding.bind(button))
+ button.focus_exited.connect(_stop_button_holding.bind(button))
+ button.mouse_exited.connect(_stop_button_holding.bind(button))
+
+ var scene_tree: SceneTree = get_tree()
+ var delay_timer_ms: int = 600
+
+ #Instead of awaiting for the duration, await per-frame so we can catch any changes in _is_holding_button and exit completely
+ while(delay_timer_ms > 0):
+ if _is_holding_button == false:
+ return
+ var pre_time: int = Time.get_ticks_msec()
+ await scene_tree.process_frame
+ delay_timer_ms -= Time.get_ticks_msec() - pre_time
+
+ var change_speed: float = 0.25
+
+ while(_is_holding_button == true):
+ await scene_tree.create_timer(change_speed).timeout
+ change_speed = maxf(0.05, change_speed - 0.01)
+ _on_value_text_submitted(str(value+(step * value_direction)))
+
+
+func update_prefix(to_prefix: String) -> void:
+ prefix = to_prefix
+ %Prefix.visible = to_prefix != null and to_prefix != ""
+ %Prefix.text = prefix
+
+
+func update_suffix(to_suffix: String) -> void:
+ suffix = to_suffix
+ %Suffix.visible = to_suffix != null and to_suffix != ""
+ %Suffix.text = suffix
+
+#endregion
+
+#region SIGNAL METHODS
+################################################################################
+func _on_gui_input(event: InputEvent) -> void:
+ if event.is_action('ui_up') and event.get_action_strength('ui_up') > 0.5:
+ _on_value_text_submitted(str(value+step))
+ elif event.is_action('ui_down') and event.get_action_strength('ui_down') > 0.5:
+ _on_value_text_submitted(str(value-step))
+
+
+func _on_increment_button_down(button: NodePath) -> void:
+ _on_value_text_submitted(str(value+step))
+ _holding_button(1.0, get_node(button) as BaseButton)
+
+
+func _on_decrement_button_down(button: NodePath) -> void:
+ _on_value_text_submitted(str(value-step))
+ _holding_button(-1.0, get_node(button) as BaseButton)
+
+
+func _on_value_text_submitted(new_text: String, no_signal:= false) -> void:
+ if new_text.is_empty() and not allow_string:
+ new_text = "0.0"
+ if new_text.is_valid_float():
+ var temp: float = min(max(new_text.to_float(), min), max)
+ if !enforce_step:
+ value = temp
+ else:
+ value = snapped(temp, step)
+ elif allow_string:
+ value = new_text
+ %Value.text = str(value).pad_decimals(len(str(float(step)-floorf(step)))-2)
+ if not no_signal:
+ value_changed.emit(property_name, value)
+ # Visually disable Up or Down arrow when limit is reached to better indicate a limit has been hit
+ %Spin/Decrement.disabled = value <= min
+ %Spin/Increment.disabled = value >= max
+
+
+# If prefix or suffix was clicked, select the actual value box instead and move the caret to the closest side.
+func _on_sublabel_clicked(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+ var mousePos: Vector2 = get_global_mouse_position()
+ mousePos.x -= get_minimum_size().x / 2
+ if mousePos.x > global_position.x:
+ (%Value as LineEdit).caret_column = (%Value as LineEdit).text.length()
+ else:
+ (%Value as LineEdit).caret_column = 0
+ (%Value as LineEdit).grab_focus()
+
+
+func _on_value_focus_exited() -> void:
+ _on_value_text_submitted(%Value.text)
+ $Value_Panel.add_theme_stylebox_override('panel', get_theme_stylebox('panel', 'DialogicEventEdit'))
+
+
+func _on_value_focus_entered() -> void:
+ $Value_Panel.add_theme_stylebox_override('panel', get_theme_stylebox('focus', 'DialogicEventEdit'))
+ %Value.select_all.call_deferred()
+
+#endregion
+
--- /dev/null
+[gd_scene load_steps=9 format=3 uid="uid://kdpp3mibml33"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_number.gd" id="1_0jdnn"]
+[ext_resource type="Texture2D" uid="uid://dh1ycbmw8anqh" path="res://addons/dialogic/Editor/Images/Interactable/increment_icon.svg" id="3_v5cne"]
+[ext_resource type="Texture2D" uid="uid://brjikovneb63n" path="res://addons/dialogic/Editor/Images/Interactable/decrement_icon.svg" id="4_ph52o"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_sj3oj"]
+content_margin_left = 3.0
+content_margin_right = 1.0
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8yqsu"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_smq50"]
+content_margin_left = 2.0
+content_margin_right = 1.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_increment"]
+content_margin_left = 2.0
+content_margin_top = 6.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.94, 0.94, 0.94, 0)
+border_color = Color(0, 0, 0, 0)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_decrement"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 6.0
+bg_color = Color(0.94, 0.94, 0.94, 0)
+border_color = Color(0, 0, 0, 0)
+
+[node name="Field_Number" type="HBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -1102.0
+offset_bottom = -617.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 0
+script = ExtResource("1_0jdnn")
+
+[node name="Value_Panel" type="PanelContainer" parent="."]
+layout_mode = 2
+theme_type_variation = &"DialogicEventEdit"
+
+[node name="Layout" type="HBoxContainer" parent="Value_Panel"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Prefix" type="RichTextLabel" parent="Value_Panel/Layout"]
+unique_name_in_owner = true
+visible = false
+clip_contents = false
+layout_direction = 2
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 4
+mouse_filter = 1
+mouse_default_cursor_shape = 1
+theme_override_colors/default_color = Color(0.54099, 0.540991, 0.54099, 1)
+theme_override_styles/focus = SubResource("StyleBoxEmpty_sj3oj")
+theme_override_styles/normal = SubResource("StyleBoxEmpty_sj3oj")
+bbcode_enabled = true
+fit_content = true
+scroll_active = false
+autowrap_mode = 0
+tab_size = 2
+shortcut_keys_enabled = false
+drag_and_drop_selection_enabled = false
+text_direction = 1
+
+[node name="Value" type="LineEdit" parent="Value_Panel/Layout"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 1
+theme_override_constants/minimum_character_width = 0
+theme_override_styles/normal = SubResource("StyleBoxEmpty_8yqsu")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_8yqsu")
+theme_override_styles/read_only = SubResource("StyleBoxEmpty_8yqsu")
+text = "0"
+alignment = 1
+expand_to_text_length = true
+virtual_keyboard_type = 3
+
+[node name="Suffix" type="RichTextLabel" parent="Value_Panel/Layout"]
+unique_name_in_owner = true
+visible = false
+clip_contents = false
+layout_direction = 2
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 4
+mouse_default_cursor_shape = 1
+theme_override_colors/default_color = Color(0.435192, 0.435192, 0.435192, 1)
+theme_override_styles/focus = SubResource("StyleBoxEmpty_smq50")
+theme_override_styles/normal = SubResource("StyleBoxEmpty_smq50")
+bbcode_enabled = true
+fit_content = true
+scroll_active = false
+autowrap_mode = 0
+tab_size = 2
+shortcut_keys_enabled = false
+drag_and_drop_selection_enabled = false
+text_direction = 1
+
+[node name="Spin" type="VBoxContainer" parent="Value_Panel/Layout"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 0
+alignment = 1
+
+[node name="Increment" type="Button" parent="Value_Panel/Layout/Spin"]
+layout_mode = 2
+size_flags_vertical = 3
+auto_translate = false
+focus_neighbor_left = NodePath("../../Value")
+focus_neighbor_top = NodePath(".")
+focus_neighbor_bottom = NodePath("../Decrement")
+theme_override_colors/icon_hover_color = Color(0.412738, 0.550094, 0.760917, 1)
+theme_override_colors/icon_focus_color = Color(0.412738, 0.550094, 0.760917, 1)
+theme_override_styles/normal = SubResource("StyleBoxFlat_increment")
+theme_override_styles/hover = SubResource("StyleBoxFlat_increment")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_increment")
+theme_override_styles/disabled = SubResource("StyleBoxFlat_increment")
+theme_override_styles/focus = SubResource("StyleBoxFlat_increment")
+icon = ExtResource("3_v5cne")
+flat = true
+vertical_icon_alignment = 2
+
+[node name="Decrement" type="Button" parent="Value_Panel/Layout/Spin"]
+layout_mode = 2
+size_flags_vertical = 3
+auto_translate = false
+focus_neighbor_left = NodePath("../../Value")
+focus_neighbor_top = NodePath("../Increment")
+focus_neighbor_bottom = NodePath(".")
+theme_override_colors/icon_hover_color = Color(0.412738, 0.550094, 0.760917, 1)
+theme_override_colors/icon_focus_color = Color(0.412738, 0.550094, 0.760917, 1)
+theme_override_styles/normal = SubResource("StyleBoxFlat_decrement")
+theme_override_styles/hover = SubResource("StyleBoxFlat_decrement")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_decrement")
+theme_override_styles/disabled = SubResource("StyleBoxFlat_decrement")
+theme_override_styles/focus = SubResource("StyleBoxFlat_decrement")
+icon = ExtResource("4_ph52o")
+flat = true
+vertical_icon_alignment = 2
+
+[connection signal="gui_input" from="Value_Panel/Layout/Prefix" to="." method="_on_sublabel_clicked"]
+[connection signal="focus_entered" from="Value_Panel/Layout/Value" to="." method="_on_value_focus_entered"]
+[connection signal="focus_exited" from="Value_Panel/Layout/Value" to="." method="_on_value_focus_exited"]
+[connection signal="gui_input" from="Value_Panel/Layout/Value" to="." method="_on_gui_input"]
+[connection signal="text_submitted" from="Value_Panel/Layout/Value" to="." method="_on_value_text_submitted"]
+[connection signal="gui_input" from="Value_Panel/Layout/Suffix" to="." method="_on_sublabel_clicked"]
+[connection signal="button_down" from="Value_Panel/Layout/Spin/Increment" to="." method="_on_increment_button_down" binds= [NodePath("%Spin/Increment")]]
+[connection signal="gui_input" from="Value_Panel/Layout/Spin/Increment" to="." method="_on_gui_input"]
+[connection signal="button_down" from="Value_Panel/Layout/Spin/Decrement" to="." method="_on_decrement_button_down" binds= [NodePath("%Spin/Decrement")]]
+[connection signal="gui_input" from="Value_Panel/Layout/Spin/Decrement" to="." method="_on_gui_input"]
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+## Event block field for strings. Options are determined by a function.
+
+
+## SETTINGS
+@export var placeholder_text := "Select Resource"
+@export var empty_text := ""
+enum Modes {PURE_STRING, PRETTY_PATH, IDENTIFIER}
+@export var mode := Modes.PURE_STRING
+@export var fit_text_length := true
+var collapse_when_empty := false
+var valid_file_drop_extension := ""
+var get_suggestions_func: Callable
+
+var resource_icon: Texture = null:
+ get:
+ return resource_icon
+ set(new_icon):
+ resource_icon = new_icon
+ %Icon.texture = new_icon
+
+## STATE
+var current_value: String
+var current_selected := 0
+
+## SUGGESTIONS ITEM LIST
+var _v_separation := 0
+var _h_separation := 0
+var _icon_margin := 0
+var _line_height := 24
+var _max_height := 200 * DialogicUtil.get_editor_scale()
+
+
+#region FIELD METHODS
+################################################################################
+
+func _set_value(value:Variant) -> void:
+ if value == null or value.is_empty():
+ %Search.text = empty_text
+ else:
+ match mode:
+ Modes.PRETTY_PATH:
+ %Search.text = DialogicUtil.pretty_name(value)
+ Modes.IDENTIFIER when value.begins_with("res://"):
+ %Search.text = DialogicResourceUtil.get_unique_identifier(value)
+ _:
+ %Search.text = str(value)
+
+ %Search.visible = not collapse_when_empty or value
+ current_value = str(value)
+
+
+
+func _load_display_info(info:Dictionary) -> void:
+ valid_file_drop_extension = info.get('file_extension', '')
+ collapse_when_empty = info.get('collapse_when_empty', false)
+ get_suggestions_func = info.get('suggestions_func', get_suggestions_func)
+ empty_text = info.get('empty_text', '')
+ placeholder_text = info.get('placeholder', 'Select Resource')
+ mode = info.get("mode", 0)
+ resource_icon = info.get('icon', null)
+ %Search.tooltip_text = info.get('tooltip_text', '')
+ await ready
+ if resource_icon == null and info.has('editor_icon'):
+ resource_icon = callv('get_theme_icon', info.editor_icon)
+
+
+func _autofocus() -> void:
+ %Search.grab_focus()
+
+#endregion
+
+
+#region BASIC
+################################################################################
+
+func _ready() -> void:
+ var focus := get_theme_stylebox("focus", "LineEdit")
+ if has_theme_stylebox("focus", "DialogicEventEdit"):
+ focus = get_theme_stylebox('focus', 'DialogicEventEdit')
+ %Focus.add_theme_stylebox_override('panel', focus)
+
+ %Search.text_changed.connect(_on_Search_text_changed)
+ %Search.text_submitted.connect(_on_Search_text_entered)
+ %Search.placeholder_text = placeholder_text
+ %Search.expand_to_text_length = fit_text_length
+
+ %SelectButton.icon = get_theme_icon("Collapse", "EditorIcons")
+
+ %Suggestions.add_theme_stylebox_override('bg', load("res://addons/dialogic/Editor/Events/styles/ResourceMenuPanelBackground.tres"))
+ %Suggestions.hide()
+ %Suggestions.item_selected.connect(suggestion_selected)
+ %Suggestions.item_clicked.connect(suggestion_selected)
+ %Suggestions.fixed_icon_size = Vector2i(16, 16) * DialogicUtil.get_editor_scale()
+
+ _v_separation = %Suggestions.get_theme_constant("v_separation")
+ _h_separation = %Suggestions.get_theme_constant("h_separation")
+ _icon_margin = %Suggestions.get_theme_constant("icon_margin")
+
+ if resource_icon == null:
+ self.resource_icon = null
+
+
+func change_to_empty() -> void:
+ value_changed.emit(property_name, "")
+
+#endregion
+
+
+#region SEARCH & SUGGESTION POPUP
+################################################################################
+func _on_Search_text_entered(new_text:String) -> void:
+ if %Suggestions.get_item_count():
+ if %Suggestions.is_anything_selected():
+ suggestion_selected(%Suggestions.get_selected_items()[0])
+ else:
+ suggestion_selected(0)
+ else:
+ change_to_empty()
+
+
+func _on_Search_text_changed(new_text:String, just_update:bool = false) -> void:
+ %Suggestions.clear()
+
+ if new_text == "" and !just_update:
+ change_to_empty()
+ else:
+ %Search.show()
+
+ var suggestions: Dictionary = get_suggestions_func.call(new_text)
+
+ var line_length := 0
+ var idx := 0
+ for element in suggestions:
+ if new_text.is_empty() or new_text.to_lower() in element.to_lower() or new_text.to_lower() in str(suggestions[element].value).to_lower() or new_text.to_lower() in suggestions[element].get('tooltip', '').to_lower():
+ var curr_line_length: int = 0
+ curr_line_length = get_theme_font('font', 'Label').get_string_size(
+ element, HORIZONTAL_ALIGNMENT_LEFT, -1, get_theme_font_size("font_size", 'Label')
+ ).x
+
+ %Suggestions.add_item(element)
+ if suggestions[element].has('icon'):
+ %Suggestions.set_item_icon(idx, suggestions[element].icon)
+ curr_line_length += %Suggestions.fixed_icon_size.x * %Suggestions.get_icon_scale() + _icon_margin * 2 + _h_separation
+ elif suggestions[element].has('editor_icon'):
+ %Suggestions.set_item_icon(idx, get_theme_icon(suggestions[element].editor_icon[0],suggestions[element].editor_icon[1]))
+ curr_line_length += %Suggestions.fixed_icon_size.x * %Suggestions.get_icon_scale() + _icon_margin * 2 + _h_separation
+
+ line_length = max(line_length, curr_line_length)
+
+ %Suggestions.set_item_tooltip(idx, suggestions[element].get('tooltip', ''))
+ %Suggestions.set_item_metadata(idx, suggestions[element].value)
+ idx += 1
+
+ if not %Suggestions.visible:
+ %Suggestions.show()
+ %Suggestions.global_position = $PanelContainer.global_position+Vector2(0,1)*$PanelContainer.size.y
+
+ if %Suggestions.item_count:
+ %Suggestions.select(0)
+ current_selected = 0
+ else:
+ current_selected = -1
+ %Search.grab_focus()
+
+ var total_height: int = 0
+ for item in %Suggestions.item_count:
+ total_height += _line_height * DialogicUtil.get_editor_scale() + _v_separation
+ total_height += _v_separation * 2
+ if total_height > _max_height:
+ line_length += %Suggestions.get_v_scroll_bar().get_minimum_size().x
+
+ %Suggestions.size.x = max(%PanelContainer.size.x, line_length)
+ %Suggestions.size.y = min(total_height, _max_height)
+
+ # Defer setting width to give PanelContainer
+ # time to update it's size
+ await get_tree().process_frame
+ await get_tree().process_frame
+
+ %Suggestions.size.x = max(%PanelContainer.size.x, line_length)
+
+
+func suggestion_selected(index: int, position := Vector2(), button_index := MOUSE_BUTTON_LEFT) -> void:
+ if button_index != MOUSE_BUTTON_LEFT:
+ return
+ if %Suggestions.is_item_disabled(index):
+ return
+
+ %Search.text = %Suggestions.get_item_text(index)
+
+ if %Suggestions.get_item_metadata(index) == null:
+ current_value = ""
+
+ else:
+ current_value = %Suggestions.get_item_metadata(index)
+
+ hide_suggestions()
+
+ grab_focus()
+ value_changed.emit(property_name, current_value)
+
+
+func _input(event:InputEvent) -> void:
+ if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+ if %Suggestions.visible:
+ if !%Suggestions.get_global_rect().has_point(get_global_mouse_position()) and \
+ !%SelectButton.get_global_rect().has_point(get_global_mouse_position()):
+ hide_suggestions()
+
+
+func hide_suggestions() -> void:
+ %SelectButton.set_pressed_no_signal(false)
+ %Suggestions.hide()
+ if !current_value and collapse_when_empty:
+ %Search.hide()
+
+
+func _on_SelectButton_toggled(button_pressed:bool) -> void:
+ if button_pressed:
+ _on_Search_text_changed('', true)
+ else:
+ hide_suggestions()
+
+
+func _on_focus_entered() -> void:
+ %Search.grab_focus()
+
+
+func _on_search_gui_input(event: InputEvent) -> void:
+ if event is InputEventKey and (event.keycode == KEY_DOWN or event.keycode == KEY_UP) and event.pressed:
+ if !%Suggestions.visible:
+ _on_Search_text_changed('', true)
+ current_selected = -1
+ if event.keycode == KEY_DOWN:
+ current_selected = wrapi(current_selected+1, 0, %Suggestions.item_count)
+ if event.keycode == KEY_UP:
+ current_selected = wrapi(current_selected-1, 0, %Suggestions.item_count)
+ %Suggestions.select(current_selected)
+ %Suggestions.ensure_current_is_visible()
+
+ if Input.is_key_pressed(KEY_CTRL):
+ if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+ if valid_file_drop_extension in [".dch", ".dtl"] and not current_value.is_empty():
+ EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(current_value, valid_file_drop_extension))
+
+ if valid_file_drop_extension in [".dch", ".dtl"] and not current_value.is_empty():
+ %Search.mouse_default_cursor_shape = CURSOR_POINTING_HAND
+ else:
+ %Search.mouse_default_cursor_shape = CURSOR_IBEAM
+
+
+func _on_search_focus_entered() -> void:
+ if %Search.text == "":
+ _on_Search_text_changed("")
+ %Search.call_deferred('select_all')
+ %Focus.show()
+
+
+func _on_search_focus_exited() -> void:
+ %Focus.hide()
+ if !%Suggestions.get_global_rect().has_point(get_global_mouse_position()):
+ hide_suggestions()
+
+#endregion
+
+
+#region DRAG AND DROP
+################################################################################
+
+func _can_drop_data(position:Vector2, data:Variant) -> bool:
+ if typeof(data) == TYPE_DICTIONARY and data.has('files') and len(data.files) == 1:
+ if valid_file_drop_extension:
+ if data.files[0].ends_with(valid_file_drop_extension):
+ return true
+ else:
+ return false
+ return false
+
+
+func _drop_data(position:Vector2, data:Variant) -> void:
+ var path := str(data.files[0])
+ if mode == Modes.IDENTIFIER:
+ path = DialogicResourceUtil.get_unique_identifier(path)
+ _set_value(path)
+ value_changed.emit(property_name, path)
+
+#endregion
--- /dev/null
+[gd_scene load_steps=7 format=3 uid="uid://dpwhshre1n4t6"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.gd" id="1_b07gq"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_tmt5n"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_vennf"]
+
+[sub_resource type="Image" id="Image_5e7lo"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_g63da"]
+image = SubResource("Image_5e7lo")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g74jb"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 0.365, 0.365, 1)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_detail = 1
+
+[node name="Field_DynamicStringOptions" type="HBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -2.0
+offset_top = -2.0
+offset_right = -1005.0
+offset_bottom = -622.0
+grow_horizontal = 2
+grow_vertical = 2
+focus_mode = 2
+script = ExtResource("1_b07gq")
+placeholder_text = ""
+
+[node name="PanelContainer" type="MarginContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 0
+theme_override_constants/margin_top = 0
+theme_override_constants/margin_right = 0
+theme_override_constants/margin_bottom = 0
+
+[node name="BG" type="Panel" parent="PanelContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+mouse_filter = 2
+theme_type_variation = &"DialogicEventEdit"
+metadata/_edit_use_anchors_ = true
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 2
+theme_override_constants/margin_top = 2
+theme_override_constants/margin_right = 2
+theme_override_constants/margin_bottom = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="Icon" type="TextureRect" parent="PanelContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+mouse_filter = 2
+stretch_mode = 5
+
+[node name="Search" type="LineEdit" parent="PanelContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+focus_neighbor_bottom = NodePath("Suggestions")
+focus_mode = 1
+mouse_filter = 1
+theme_override_styles/normal = SubResource("StyleBoxEmpty_tmt5n")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_vennf")
+expand_to_text_length = true
+flat = true
+caret_blink = true
+
+[node name="Suggestions" type="ItemList" parent="PanelContainer/MarginContainer/HBoxContainer/Search"]
+unique_name_in_owner = true
+visible = false
+top_level = true
+custom_minimum_size = Vector2(-1086, 0)
+layout_mode = 0
+offset_left = -5.0
+offset_top = 36.0
+offset_right = 195.0
+offset_bottom = 71.0
+size_flags_vertical = 0
+auto_translate = false
+focus_neighbor_top = NodePath("..")
+max_text_lines = 3
+item_count = 1
+fixed_icon_size = Vector2i(16, 16)
+item_0/text = "Hello"
+
+[node name="SelectButton" type="Button" parent="PanelContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+focus_mode = 0
+toggle_mode = true
+shortcut_in_tooltip = false
+icon = SubResource("ImageTexture_g63da")
+flat = true
+
+[node name="Focus" type="Panel" parent="PanelContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_g74jb")
+metadata/_edit_use_anchors_ = true
+
+[connection signal="focus_entered" from="." to="." method="_on_focus_entered"]
+[connection signal="focus_entered" from="PanelContainer/MarginContainer/HBoxContainer/Search" to="." method="_on_search_focus_entered"]
+[connection signal="focus_exited" from="PanelContainer/MarginContainer/HBoxContainer/Search" to="." method="_on_search_focus_exited"]
+[connection signal="gui_input" from="PanelContainer/MarginContainer/HBoxContainer/Search" to="." method="_on_search_gui_input"]
+[connection signal="gui_input" from="PanelContainer/MarginContainer/HBoxContainer/Search/Suggestions" to="." method="_on_suggestions_gui_input"]
+[connection signal="toggled" from="PanelContainer/MarginContainer/HBoxContainer/SelectButton" to="." method="_on_SelectButton_toggled"]
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for constant options. For varying options use ComplexPicker.
+
+var options: Array = []
+
+## if true, only the symbol will be displayed. In the dropdown text will be visible.
+## Useful for making UI simpler
+var symbol_only := false:
+ set(value):
+ symbol_only = value
+ if value: self.text = ""
+
+var current_value: Variant = -1
+
+
+func _ready() -> void:
+ add_theme_color_override("font_disabled_color", get_theme_color("font_color", "MenuButton"))
+ self.about_to_popup.connect(insert_options)
+ call("get_popup").index_pressed.connect(index_pressed)
+
+
+func _load_display_info(info:Dictionary) -> void:
+ options = info.get('options', [])
+ self.disabled = info.get('disabled', false)
+ symbol_only = info.get('symbol_only', false)
+
+
+func _set_value(value:Variant) -> void:
+ for option in options:
+ if option['value'] == value:
+ if typeof(option.get('icon')) == TYPE_ARRAY:
+ option.icon = callv('get_theme_icon', option.get('icon'))
+ if !symbol_only:
+ self.text = option['label']
+ self.icon = option.get('icon', null)
+ current_value = value
+
+
+func get_value() -> Variant:
+ return current_value
+
+
+func insert_options() -> void:
+ call("get_popup").clear()
+
+ var idx := 0
+ for option in options:
+ if typeof(option.get('icon')) == TYPE_ARRAY:
+ option.icon = callv('get_theme_icon', option.get('icon'))
+ call("get_popup").add_icon_item(option.get('icon', null), option['label'])
+ call("get_popup").set_item_metadata(idx, option['value'])
+ idx += 1
+
+
+func index_pressed(idx:int) -> void:
+ current_value = idx
+ if !symbol_only:
+ self.text = call("get_popup").get_item_text(idx)
+ self.icon =call("get_popup").get_item_icon(idx)
+ value_changed.emit(property_name, call("get_popup").get_item_metadata(idx))
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://d3bhehatwoio"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_options_fixed.gd" id="1"]
+
+[node name="Field_FixedOptions" type="MenuButton"]
+offset_right = 137.0
+offset_bottom = 43.0
+focus_mode = 2
+theme_type_variation = &"DialogicEventEdit"
+theme_override_colors/font_disabled_color = Color(0.875, 0.875, 0.875, 1)
+text = "Placeholder Text"
+flat = false
+script = ExtResource("1")
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field that allows entering multiline text (mainly text event).
+
+@onready var code_completion_helper: Node = find_parent('EditorsManager').get_node('CodeCompletionHelper')
+
+
+#region MAIN METHODS
+################################################################################
+
+func _ready() -> void:
+ self.text_changed.connect(_on_text_changed)
+ self.syntax_highlighter = code_completion_helper.text_syntax_highlighter
+
+
+func _load_display_info(info:Dictionary) -> void:
+ pass
+
+
+func _set_value(value:Variant) -> void:
+ self.text = str(value)
+
+
+func _autofocus() -> void:
+ grab_focus()
+
+#endregion
+
+
+#region SIGNAL METHODS
+################################################################################
+
+func _on_text_changed(value := "") -> void:
+ value_changed.emit(property_name, self.text)
+
+#endregion
+
+
+#region AUTO COMPLETION
+################################################################################
+
+## Called if something was typed
+func _request_code_completion(force:bool):
+ code_completion_helper.request_code_completion(force, self, 0)
+
+
+## Filters the list of all possible options, depending on what was typed
+## Purpose of the different Kinds is explained in [_request_code_completion]
+func _filter_code_completion_candidates(candidates:Array) -> Array:
+ return code_completion_helper.filter_code_completion_candidates(candidates, self)
+
+
+## Called when code completion was activated
+## Inserts the selected item
+func _confirm_code_completion(replace:bool) -> void:
+ code_completion_helper.confirm_code_completion(replace, self)
+
+#endregion
+
+
+#region SYMBOL CLICKING
+################################################################################
+
+## Performs an action (like opening a link) when a valid symbol was clicked
+func _on_symbol_lookup(symbol, line, column):
+ code_completion_helper.symbol_lookup(symbol, line, column)
+
+
+## Called to test if a symbol can be clicked
+func _on_symbol_validate(symbol:String) -> void:
+ code_completion_helper.symbol_validate(symbol, self)
+
+#endregion
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://dyp7m2nvab1aj"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd" id="2_ww6ga"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_text_multiline.gd" id="3_q7600"]
+
+[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_2q5dk"]
+script = ExtResource("2_ww6ga")
+
+[node name="Field_Text_Multiline" type="CodeEdit"]
+offset_right = 413.0
+offset_bottom = 15.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_type_variation = &"DialogicTextEventTextEdit"
+wrap_mode = 1
+scroll_fit_content_height = true
+syntax_highlighter = SubResource("SyntaxHighlighter_2q5dk")
+symbol_lookup_on_click = true
+delimiter_strings = Array[String]([])
+code_completion_enabled = true
+code_completion_prefixes = Array[String](["[", "{"])
+indent_automatic_prefixes = Array[String]([":", "{", "[", ")"])
+auto_brace_completion_enabled = true
+auto_brace_completion_pairs = {
+"[": "]",
+"{": "}"
+}
+script = ExtResource("3_q7600")
--- /dev/null
+@tool
+extends DialogicVisualEditorField
+
+## Event block field for a single line of text.
+
+
+var placeholder := "":
+ set(value):
+ placeholder = value
+ self.placeholder_text = placeholder
+
+
+#region MAIN METHODS
+################################################################################
+
+func _ready() -> void:
+ self.text_changed.connect(_on_text_changed)
+
+
+func _load_display_info(info:Dictionary) -> void:
+ self.placeholder = info.get('placeholder', '')
+
+
+func _set_value(value:Variant) -> void:
+ self.text = str(value)
+
+
+func _autofocus() -> void:
+ grab_focus()
+
+#endregion
+
+
+#region SIGNAL METHODS
+################################################################################
+
+func _on_text_changed(value := "") -> void:
+ value_changed.emit(property_name, self.text)
+
+#endregion
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://c0vkcehgjsjy"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_text_singleline.gd" id="1_4vnxv"]
+
+[node name="Field_Text_Singleline" type="LineEdit"]
+offset_right = 1152.0
+offset_bottom = 81.0
+theme_type_variation = &"DialogicEventEdit"
+expand_to_text_length = true
+script = ExtResource("1_4vnxv")
--- /dev/null
+@tool
+extends DialogicVisualEditorFieldVector
+## Event block field for a Vector2.
+
+var current_value := Vector2()
+
+
+func _set_value(value: Variant) -> void:
+ current_value = value
+ super(value)
+
+
+func get_value() -> Vector2:
+ return current_value
+
+
+func _on_sub_value_changed(sub_component: String, value: float) -> void:
+ match sub_component:
+ 'X': current_value.x = value
+ 'Y': current_value.y = value
+ _on_value_changed(current_value)
+
+
+func _update_sub_component_text(value: Variant) -> void:
+ $X._on_value_text_submitted(str(value.x), true)
+ $Y._on_value_text_submitted(str(value.y), true)
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://dtimnsj014cu"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_vector2.gd" id="1_v6lp0"]
+[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="2_a0b6y"]
+
+[node name="Field_Vector2" type="HBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -1033.0
+offset_bottom = -617.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/separation = 2
+script = ExtResource("1_v6lp0")
+
+[node name="X" parent="." instance=ExtResource("2_a0b6y")]
+layout_mode = 2
+step = 0.001
+min = -9999.0
+max = 9999.0
+prefix = ""
+
+[node name="Y" parent="." instance=ExtResource("2_a0b6y")]
+layout_mode = 2
+step = 0.001
+min = -9999.0
+max = 9999.0
+prefix = ""
--- /dev/null
+@tool
+extends DialogicVisualEditorFieldVector
+## Event block field for a Vector3.
+
+var current_value := Vector3()
+
+
+func _set_value(value: Variant) -> void:
+ current_value = value
+ super(value)
+
+
+func get_value() -> Vector3:
+ return current_value
+
+
+func _on_sub_value_changed(sub_component: String, value: float) -> void:
+ match sub_component:
+ 'X': current_value.x = value
+ 'Y': current_value.y = value
+ 'Z': current_value.z = value
+ _on_value_changed(current_value)
+
+
+func _update_sub_component_text(value: Variant) -> void:
+ $X._on_value_text_submitted(str(value.x), true)
+ $Y._on_value_text_submitted(str(value.y), true)
+ $Z._on_value_text_submitted(str(value.z), true)
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://cklkpfrcvopgw"]
+
+[ext_resource type="PackedScene" uid="uid://dtimnsj014cu" path="res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn" id="1_l3y0o"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_vector3.gd" id="2_gktf1"]
+[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="3_k0u0p"]
+
+[node name="Field_Vector3" instance=ExtResource("1_l3y0o")]
+offset_right = -973.0
+script = ExtResource("2_gktf1")
+
+[node name="Z" parent="." index="2" instance=ExtResource("3_k0u0p")]
+layout_mode = 2
+step = 0.001
+min = -9999.0
+max = 9999.0
+affix = "z:"
--- /dev/null
+@tool
+extends DialogicVisualEditorFieldVector
+## Event block field for a Vector4.
+
+var current_value := Vector4()
+
+
+func _set_value(value: Variant) -> void:
+ current_value = value
+ super(value)
+
+
+func get_value() -> Vector4:
+ return current_value
+
+
+func _on_sub_value_changed(sub_component: String, value: float) -> void:
+ match sub_component:
+ 'X': current_value.x = value
+ 'Y': current_value.y = value
+ 'Z': current_value.z = value
+ 'W': current_value.w = value
+ _on_value_changed(current_value)
+
+
+func _update_sub_component_text(value: Variant) -> void:
+ $X._on_value_text_submitted(str(value.x), true)
+ $Y._on_value_text_submitted(str(value.y), true)
+ $Z._on_value_text_submitted(str(value.z), true)
+ $W._on_value_text_submitted(str(value.w), true)
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://dykss037r2rsc"]
+
+[ext_resource type="PackedScene" uid="uid://cklkpfrcvopgw" path="res://addons/dialogic/Editor/Events/Fields/field_vector3.tscn" id="1_20tvl"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/Fields/field_vector4.gd" id="2_yksrc"]
+[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="3_1jogk"]
+
+[node name="Field_Vector4" instance=ExtResource("1_20tvl")]
+offset_right = -908.0
+script = ExtResource("2_yksrc")
+
+[node name="W" parent="." index="3" instance=ExtResource("3_1jogk")]
+layout_mode = 2
+step = 0.001
+min = -9999.0
+max = 9999.0
+affix = "w:"
--- /dev/null
+class_name DialogicVisualEditorFieldVector
+extends DialogicVisualEditorField
+## Base type for Vector event blocks
+
+
+func _ready() -> void:
+ for child in get_children():
+ child.tooltip_text = tooltip_text
+ child.property_name = child.name #to identify the name of the changed sub-component
+ child.value_changed.connect(_on_sub_value_changed)
+
+
+func _load_display_info(info: Dictionary) -> void:
+ for child in get_children():
+ if child is DialogicVisualEditorFieldNumber:
+ if info.get('no_prefix', false):
+ child._load_display_info(info)
+ else:
+ var prefixed_info := info.duplicate()
+ prefixed_info.merge({'prefix':child.name.to_lower()})
+ child._load_display_info(prefixed_info)
+
+
+func _set_value(value: Variant) -> void:
+ _update_sub_component_text(value)
+ _on_value_changed(value)
+
+
+func _on_value_changed(value: Variant) -> void:
+ value_changed.emit(property_name, value)
+
+
+func _on_sub_value_changed(sub_component: String, value: float) -> void:
+ pass
+
+
+func _update_sub_component_text(value: Variant) -> void:
+ pass
--- /dev/null
+@tool
+class_name DialogicVisualEditorField
+extends Control
+
+signal value_changed(property_name:String, value:Variant)
+var property_name := ""
+
+var event_resource: DialogicEvent = null
+
+#region OVERWRITES
+################################################################################
+
+## To be overwritten
+func _load_display_info(info:Dictionary) -> void:
+ pass
+
+
+## To be overwritten
+func _set_value(value:Variant) -> void:
+ pass
+
+
+## To be overwritten
+func _autofocus() -> void:
+ pass
+
+#endregion
+
+
+func set_value(value:Variant) -> void:
+ _set_value(value)
+
+
+func take_autofocus() -> void:
+ _autofocus()
--- /dev/null
+[gd_resource type="Theme" load_steps=3 format=3 uid="uid://d3g4i4dshtdpu"]
+
+[sub_resource type="StyleBoxFlat" id="1"]
+content_margin_left = 30.0
+content_margin_top = 5.0
+content_margin_right = 20.0
+content_margin_bottom = 5.0
+bg_color = Color(0.12549, 0.141176, 0.192157, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.0980392, 0.113725, 0.152941, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="StyleBoxFlat" id="2"]
+content_margin_left = 11.0
+content_margin_top = 5.0
+content_margin_right = 20.0
+content_margin_bottom = 5.0
+bg_color = Color(0.12549, 0.141176, 0.192157, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.0980392, 0.113725, 0.152941, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[resource]
+LineEdit/colors/clear_button_color = Color(0, 0, 0, 1)
+LineEdit/colors/clear_button_color_pressed = Color(0, 0, 0, 1)
+LineEdit/colors/cursor_color = Color(1, 1, 1, 1)
+LineEdit/colors/font_color = Color(1, 1, 1, 1)
+LineEdit/colors/font_color_selected = Color(1, 1, 1, 1)
+LineEdit/colors/font_color_uneditable = Color(1, 1, 1, 1)
+LineEdit/colors/selection_color = Color(1, 1, 1, 0.235294)
+LineEdit/constants/minimum_spaces = 10
+LineEdit/fonts/font = null
+LineEdit/icons/clear = null
+LineEdit/styles/focus = SubResource("1")
+LineEdit/styles/normal = SubResource("2")
+LineEdit/styles/read_only = SubResource("1")
+LineEditWithIcon/base_type = &"LineEdit"
+LineEditWithIcon/styles/normal = SubResource("1")
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=2]
+
+[resource]
+content_margin_left = 25.0
+content_margin_right = 10.0
+content_margin_top = 4.0
+content_margin_bottom = 4.0
+bg_color = Color( 0.466667, 0.466667, 0.466667, 0.141176 )
+border_width_bottom = 2
+corner_radius_top_left = 4
+corner_radius_top_right = 4
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=2]
+
+[resource]
+content_margin_left = 25.0
+content_margin_right = 10.0
+content_margin_top = 4.0
+content_margin_bottom = 4.0
+bg_color = Color( 0.180392, 0.180392, 0.180392, 0.219608 )
+draw_center = false
+border_width_bottom = 2
+border_color = Color( 0.8, 0.8, 0.8, 0.286275 )
+corner_radius_top_left = 4
+corner_radius_top_right = 4
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://c8k6tbipodsg"]
+
+[resource]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.8, 0.8, 0.8, 0.109804)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=2]
+
+[resource]
+content_margin_left = 6.0
+content_margin_right = 6.0
+content_margin_top = 5.0
+content_margin_bottom = 4.0
+bg_color = Color( 0.6, 0.6, 0.6, 0 )
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color( 0.2, 0.227451, 0.309804, 1 )
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=2]
+
+[resource]
+content_margin_left = 3.0
+content_margin_right = 3.0
+content_margin_top = 3.0
+content_margin_bottom = 3.0
+bg_color = Color( 0.2, 0.231373, 0.309804, 0.317647 )
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color( 0.8, 0.8, 0.8, 0.109804 )
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=2]
+
+[resource]
+content_margin_left = 3.0
+content_margin_right = 3.0
+content_margin_top = 3.0
+content_margin_bottom = 3.0
+bg_color = Color( 0.2, 0.231373, 0.309804, 0.235294 )
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color( 0.8, 0.8, 0.8, 0.109804 )
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://cu8otiwksn8ma"]
+
+[resource]
+content_margin_left = 10.0
+content_margin_top = 13.0
+content_margin_bottom = 2.0
+bg_color = Color(1, 1, 1, 0.0784314)
+border_color = Color(0.454902, 0.454902, 0.454902, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://obyrr26pqk2p"]
+
+[resource]
+content_margin_left = 3.0
+content_margin_top = 1.0
+content_margin_right = 4.0
+content_margin_bottom = 1.0
+bg_color = Color(0.776471, 0.776471, 0.776471, 0.207843)
+border_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+expand_margin_left = 1.0
+expand_margin_top = 1.0
+expand_margin_bottom = 2.0
--- /dev/null
+[gd_resource type="StyleBoxEmpty" format=3 uid="uid://cl75ikyq2is7c"]
+
+[resource]
+content_margin_left = 3.0
+content_margin_top = 1.0
+content_margin_right = 4.0
+content_margin_bottom = 1.0
--- /dev/null
+@tool
+extends DialogicEditor
+
+## A Main page in the dialogic editor.
+
+var tips: Array = []
+
+
+
+func _get_icon() -> Texture:
+ return load("res://addons/dialogic/Editor/Images/plugin-icon.svg")
+
+
+func _ready() -> void:
+ self_modulate = get_theme_color("font_color", "Editor")
+ self_modulate.a = 0.2
+
+ var edit_scale := DialogicUtil.get_editor_scale()
+ %HomePageBox.custom_minimum_size = Vector2(600, 350)*edit_scale
+ %TopPanel.custom_minimum_size.y = 100*edit_scale
+ %VersionLabel.set('theme_override_font_sizes/font_size', 10 * edit_scale)
+ var plugin_cfg := ConfigFile.new()
+ plugin_cfg.load("res://addons/dialogic/plugin.cfg")
+ %VersionLabel.text = plugin_cfg.get_value('plugin', 'version', 'unknown version')
+
+ %BottomPanel.self_modulate = get_theme_color("dark_color_3", "Editor")
+
+ %RandomTipLabel.add_theme_color_override("font_color", get_theme_color("property_color_z", "Editor"))
+ %RandomTipMoreButton.icon = get_theme_icon("ExternalLink", "EditorIcons")
+
+
+
+func _register() -> void:
+ editors_manager.register_simple_editor(self)
+
+ self.alternative_text = "Welcome to dialogic!"
+
+
+
+func _open(extra_info:Variant="") -> void:
+ if tips.is_empty():
+ var file := FileAccess.open('res://addons/dialogic/Editor/HomePage/tips.txt', FileAccess.READ)
+ tips = file.get_as_text().split('\n')
+ tips = tips.filter(func(item): return !item.is_empty())
+
+ randomize()
+ var tip: String = tips[randi()%len(tips)]
+ var text := tip.get_slice(';',0).strip_edges()
+ var action := tip.get_slice(';',1).strip_edges()
+ if action == text:
+ action = ""
+ show_tip(text, action)
+
+
+func show_tip(text:String='', action:String='') -> void:
+ if text.is_empty():
+ %TipBox.hide()
+ %RandomTipLabel.hide()
+ return
+
+ %TipBox.show()
+ %RandomTipLabel.show()
+ %RandomTip.text = '[i]'+text
+
+ if action.is_empty():
+ %RandomTipMoreButton.hide()
+ return
+
+ %RandomTipMoreButton.show()
+
+ if %RandomTipMoreButton.pressed.is_connected(_on_tip_action):
+ %RandomTipMoreButton.pressed.disconnect(_on_tip_action)
+ %RandomTipMoreButton.pressed.connect(_on_tip_action.bind(action))
+
+
+func _on_tip_action(action:String) -> void:
+ if action.begins_with('https://'):
+ OS.shell_open(action)
+ return
+ elif action.begins_with('editor://'):
+ var editor_name := action.trim_prefix('editor://').get_slice('->',0)
+ var extra_info := action.trim_prefix('editor://').get_slice('->',1)
+ if editor_name in editors_manager.editors:
+ editors_manager.open_editor(editors_manager.editors[editor_name].node, false, extra_info)
+ return
+ print("Tip button doesn't do anything (", action, ")")
--- /dev/null
+[gd_scene load_steps=23 format=3 uid="uid://cqy73hshqqgga"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/HomePage/home_page.gd" id="1_6g38w"]
+[ext_resource type="Texture2D" uid="uid://cvmlp5nxb2rer" path="res://addons/dialogic/Editor/HomePage/icon_bg.png" id="1_ed1g1"]
+[ext_resource type="Texture2D" uid="uid://bt87p6qlso0ya" path="res://addons/dialogic/Editor/Images/dialogic-logo.svg" id="3_3leok"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_imi2d"]
+draw_center = false
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+shadow_color = Color(0.796078, 0.572549, 0.933333, 0.0627451)
+shadow_size = 24
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_n2afh"]
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+
+[sub_resource type="Gradient" id="Gradient_lt7uf"]
+colors = PackedColorArray(0.296484, 0.648457, 1, 1, 0.732014, 0.389374, 1, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_2klx3"]
+gradient = SubResource("Gradient_lt7uf")
+fill_from = Vector2(0.151515, 0.272727)
+fill_to = Vector2(1, 1)
+
+[sub_resource type="Gradient" id="Gradient_1gns2"]
+offsets = PackedFloat32Array(0.302013, 0.872483)
+colors = PackedColorArray(0.365323, 0.360806, 0.260695, 0, 0.615686, 0.615686, 0.615686, 0.592157)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_u0aw3"]
+gradient = SubResource("Gradient_1gns2")
+fill = 1
+fill_from = Vector2(0.497835, 0.493506)
+fill_to = Vector2(1, 1)
+
+[sub_resource type="FontVariation" id="FontVariation_vepxx"]
+variation_embolden = 2.0
+
+[sub_resource type="LabelSettings" id="LabelSettings_w8q1h"]
+font = SubResource("FontVariation_vepxx")
+font_size = 40
+outline_size = 14
+outline_color = Color(0.0901961, 0.0901961, 0.0901961, 0.258824)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p7ka2"]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_es88k"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ce6uo"]
+content_margin_left = 7.0
+content_margin_top = 7.0
+content_margin_right = 7.0
+content_margin_bottom = 14.0
+bg_color = Color(0.803922, 0.352941, 1, 0.141176)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[sub_resource type="FontVariation" id="FontVariation_elu6e"]
+variation_embolden = 1.1
+
+[sub_resource type="FontVariation" id="FontVariation_5kbdj"]
+variation_transform = Transform2D(1, 0.239, 0, 1, 0, 0)
+
+[sub_resource type="FontVariation" id="FontVariation_g0m61"]
+variation_embolden = 1.43
+variation_transform = Transform2D(1, 0.343, 0, 1, 0, 0)
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_a8dvw"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ckyhx"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.470588, 0.196078, 0.6, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l1doy"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.470588, 0.196078, 0.6, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="Image" id="Image_e1dkh"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_sr7s6"]
+image = SubResource("Image_e1dkh")
+
+[node name="HomePage" type="TextureRect"]
+self_modulate = Color(0, 0, 0, 0.2)
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -2.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = ExtResource("1_ed1g1")
+expand_mode = 1
+stretch_mode = 3
+script = ExtResource("1_6g38w")
+
+[node name="CenterContainer" type="CenterContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="HomePageBox" type="VBoxContainer" parent="CenterContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(600, 350)
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="TopPanel" type="Panel" parent="CenterContainer/HomePageBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 100)
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_imi2d")
+
+[node name="Header2" type="Panel" parent="CenterContainer/HomePageBox/TopPanel"]
+clip_children = 1
+clip_contents = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.4
+theme_override_styles/panel = SubResource("StyleBoxFlat_n2afh")
+
+[node name="BG" type="TextureRect" parent="CenterContainer/HomePageBox/TopPanel/Header2"]
+modulate = Color(0.65098, 0.65098, 0.65098, 1)
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.3
+texture = SubResource("GradientTexture2D_2klx3")
+expand_mode = 1
+
+[node name="Vignette" type="TextureRect" parent="CenterContainer/HomePageBox/TopPanel/Header2"]
+modulate = Color(0, 0, 0, 1)
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = -166.0
+offset_bottom = 166.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = SubResource("GradientTexture2D_u0aw3")
+expand_mode = 1
+
+[node name="Logo" type="TextureRect" parent="CenterContainer/HomePageBox/TopPanel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 19.0
+offset_top = 10.0
+offset_right = -23.0
+offset_bottom = -10.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.3
+texture = ExtResource("3_3leok")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="Label" type="Label" parent="CenterContainer/HomePageBox/TopPanel/Logo"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = 155.0
+offset_top = -37.0
+offset_right = 185.0
+offset_bottom = 21.0
+grow_horizontal = 2
+grow_vertical = 2
+rotation = -0.201447
+text = "2"
+label_settings = SubResource("LabelSettings_w8q1h")
+
+[node name="BottomPanel" type="PanelContainer" parent="CenterContainer/HomePageBox"]
+unique_name_in_owner = true
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_p7ka2")
+
+[node name="VersionLabel" type="Label" parent="CenterContainer/HomePageBox/BottomPanel"]
+unique_name_in_owner = true
+modulate = Color(1, 1, 1, 0.501961)
+layout_mode = 2
+size_flags_vertical = 8
+theme_override_font_sizes/font_size = 10
+text = "2.0-Alpha-15 WIP (Godot 4.2+)"
+horizontal_alignment = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="CenterContainer/HomePageBox/BottomPanel"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 50
+
+[node name="CenterContainer" type="VBoxContainer" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_stretch_ratio = 0.4
+
+[node name="Label" type="Label" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+theme_override_constants/line_spacing = 0
+text = "Documentation"
+
+[node name="WikiButton" type="LinkButton" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicLink"
+text = " Wiki"
+underline = 2
+uri = "https://docs.dialogic.pro/"
+
+[node name="WikiGettingStartedButton" type="LinkButton" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicLink"
+text = " Getting Started"
+underline = 2
+uri = "https://docs.dialogic.pro/getting-started.html"
+
+[node name="Separator" type="Control" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Get in touch"
+
+[node name="BugRequestButton" type="LinkButton" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicLink"
+text = " Bug / Request"
+underline = 2
+uri = "https://github.com/dialogic-godot/dialogic/issues/new/choose"
+
+[node name="DiscordButton" type="LinkButton" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicLink"
+text = " Discord"
+underline = 2
+uri = "https://discord.gg/2hHQzkf2pX"
+
+[node name="Website" type="LinkButton" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicLink"
+text = " Website"
+underline = 2
+uri = "https://dialogic.pro/"
+
+[node name="DonateButton" type="LinkButton" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicLink"
+text = " Donate"
+underline = 2
+uri = "https://www.patreon.com/JowanSpooner"
+
+[node name="CenterContainer2" type="VBoxContainer" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 15
+
+[node name="Control" type="Control" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2"]
+layout_mode = 2
+
+[node name="WelcomeText" type="RichTextLabel" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2"]
+layout_mode = 2
+theme_override_styles/normal = SubResource("StyleBoxEmpty_es88k")
+bbcode_enabled = true
+text = "[center]Welcome to dialogic, a plugin that lets you easily create stories and dialogs for your game!"
+fit_content = true
+
+[node name="RandomTipSection" type="VBoxContainer" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = -4
+alignment = 1
+
+[node name="RandomTipLabel" type="Label" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2/RandomTipSection"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "Random Tip"
+
+[node name="TipBox" type="PanelContainer" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2/RandomTipSection"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ce6uo")
+
+[node name="RandomTip" type="RichTextLabel" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2/RandomTipSection/TipBox"]
+unique_name_in_owner = true
+clip_contents = false
+layout_mode = 2
+theme_override_fonts/bold_font = SubResource("FontVariation_elu6e")
+theme_override_fonts/italics_font = SubResource("FontVariation_5kbdj")
+theme_override_fonts/bold_italics_font = SubResource("FontVariation_g0m61")
+theme_override_styles/normal = SubResource("StyleBoxEmpty_a8dvw")
+bbcode_enabled = true
+text = "[i]You can[/i] [b]create custom[/b] events, [i][b]subsystems, text effects and even editors for[/b][i] [code]dialogic!"
+fit_content = true
+
+[node name="RandomTipMoreButton" type="Button" parent="CenterContainer/HomePageBox/BottomPanel/ScrollContainer/HBoxContainer/CenterContainer2/RandomTipSection/TipBox/RandomTip"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -30.0
+offset_top = 1.0
+offset_right = -8.0
+offset_bottom = 23.0
+grow_horizontal = 0
+grow_vertical = 0
+tooltip_text = "Check it out!"
+theme_override_styles/normal = SubResource("StyleBoxFlat_ckyhx")
+theme_override_styles/hover = SubResource("StyleBoxFlat_l1doy")
+icon = SubResource("ImageTexture_sr7s6")
+expand_icon = true
--- /dev/null
+Dialogic variables can be changed from timelines [b]and[/b] scripts! They can be used in conditions and inside of texts!; editor://VariablesEditor
+You can create [b]custom modules[/b] for dialogic, including events, subsystems, text effects, ui layouts and even editors!; editor://Settings->General
+If there are events you never need, you can hide them from the list in the editor!; editor://Settings->Modules
+Did you know that dialogic supports translations? It does!; editor://Settings->Translations
+You can use [b]bbcode effects[/b] in text events! What are they though???; https://docs.godotengine.org/en/latest/tutorials/ui/bbcode_in_richtextlabel.html
+Writing [/i]<Oh hi/Hello you/Well, well>[i] in a text event will pick a random one of the three strings!
+There are a number of cool text effects like [pause=x], [speed=x] and [portrait=x]. Try them out!;
+You can use scenes as portraits! This gives you basically limitless freedom.; https://dialogic-docs.coppolaemilio.com/custom-portraits.html
+You can use scenes as backgrounds. This way they can be animated or whatever you want!
+Dialogic has a built in save and load system! It's pretty powerful!; editor://Settings->Saving
+You can add multiple glossary files, each containing words that can be hovered for information!; editor://GlossaryEditor
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="8" cy="8" r="3" fill="#2F80ED"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_3666_572)">
+<path d="M11.1812 7.46591V8.57955H4.81756V7.46591H11.1812ZM7.99938 11.483C7.7508 11.483 7.53868 11.3968 7.36301 11.2244C7.19067 11.0488 7.10449 10.8366 7.10449 10.5881C7.10449 10.3494 7.19067 10.1439 7.36301 9.97159C7.53868 9.79924 7.7508 9.71307 7.99938 9.71307C8.23802 9.71307 8.44351 9.79924 8.61586 9.97159C8.7882 10.1439 8.87438 10.3494 8.87438 10.5881C8.87438 10.8366 8.7882 11.0488 8.61586 11.2244C8.44351 11.3968 8.23802 11.483 7.99938 11.483ZM7.99938 6.33239C7.83366 6.33239 7.68285 6.29261 7.54696 6.21307C7.41107 6.13352 7.30336 6.0258 7.22381 5.88991C7.14427 5.75402 7.10449 5.60322 7.10449 5.4375C7.10449 5.19886 7.19067 4.99337 7.36301 4.82102C7.53868 4.64867 7.7508 4.5625 7.99938 4.5625C8.23802 4.5625 8.44351 4.64867 8.61586 4.82102C8.7882 4.99337 8.87438 5.19886 8.87438 5.4375C8.87438 5.68608 8.7882 5.8982 8.61586 6.07386C8.44351 6.24621 8.23802 6.33239 7.99938 6.33239Z" fill="#2F80ED"/>
+</g>
+<defs>
+<clipPath id="clip0_3666_572">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 7V9H7.5V12L11.5 8L7.5 4V7H4Z" fill="#A5EFAC"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.5 9L11.5 7L8 7L8 4L4 8L8 12L8 9L11.5 9Z" fill="#D14A4A"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_3666_568)">
+<path d="M10.2291 7.08807V8.18182H5.77459V7.08807H10.2291Z" fill="#2F80ED"/>
+</g>
+<defs>
+<clipPath id="clip0_3666_568">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_3666_570)">
+<path d="M10.4255 11.2045L4.81756 5.59659L5.57324 4.84091L11.1812 10.4489L10.4255 11.2045ZM5.57324 11.2045L4.81756 10.4489L10.4255 4.84091L11.1812 5.59659L5.57324 11.2045Z" fill="#2F80ED"/>
+</g>
+<defs>
+<clipPath id="clip0_3666_570">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_3666_566)">
+<path d="M7.44256 11.304V4.74148H8.5562V11.304H7.44256ZM4.71813 8.57955V7.46591H11.2806V8.57955H4.71813Z" fill="#2F80ED"/>
+</g>
+<defs>
+<clipPath id="clip0_3666_566">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_3666_561)">
+<path d="M4.93688 7.08807V6.0142H11.0619V7.08807H4.93688ZM4.93688 10.0312V8.95739H11.0619V10.0312H4.93688Z" fill="#2F80ED"/>
+</g>
+<defs>
+<clipPath id="clip0_3666_561">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.45082 5.30639L4.73587 5.26497L4.79109 6.97825L6.50437 6.92302L6.54579 8.20807L3.54747 8.30471L3.45082 5.30639Z" fill="#2F80ED"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3458 12H12.0601L12.0601 10.2858H10.3459L10.3459 9.00012L13.3458 9.00012L13.3458 12Z" fill="#2F80ED"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.35461 11.7143C9.76618 11.7143 10.9724 10.8651 11.4559 9.66667H12.8546C12.3227 11.5864 10.5095 13 8.35461 13C6.1997 13 4.38656 11.5864 3.85461 9.66667H5.25336C5.73678 10.8651 6.94305 11.7143 8.35461 11.7143ZM5.41117 7C5.96903 5.98047 7.07784 5.28571 8.35461 5.28571C9.63139 5.28571 10.7402 5.98047 11.2981 7H12.7476C12.1082 5.25221 10.3828 4 8.35461 4C6.32646 4 4.60105 5.25221 3.96159 7H5.41117Z" fill="#2F80ED"/>
+</svg>
--- /dev/null
+<svg width="14" height="6" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.046 5.406 4.91-5.134C13.063.16 12.954 0 12.838 0h-2.432L7.303 3.244c-.173.181-.402.189-.582 0L3.618 0H1.166c-.159 0-.208.19-.13.272l4.942 5.134c.54.56 1.538.554 2.068 0z" fill="#e0e0e0" style="fill:#fff;fill-opacity:1;stroke-width:1.05736"/></svg>
--- /dev/null
+<svg width="14" height="6" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m5.954.418-4.91 5.135c-.107.112.002.271.118.271h2.432l3.103-3.243c.173-.182.402-.19.582 0l3.103 3.243h2.452c.159 0 .208-.19.13-.271L8.021.418c-.54-.56-1.538-.554-2.068 0z" fill="#e0e0e0" style="fill:#fff;fill-opacity:1;stroke-width:1.05736"/></svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 10H16V12H13V15H11V12H8V10H11V7H13V10Z" fill="#A5EFAC"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1.29289 2.29289C1.48043 2.10536 1.73478 2 2 2H8C8.26522 2 8.51957 2.10536 8.70711 2.29289C8.89464 2.48043 9 2.73478 9 3V4C9 4.26522 9.10536 4.51957 9.29289 4.70711C9.48043 4.89464 9.73478 5 10 5H14C14.2652 5 14.5196 5.10536 14.7071 5.29289C14.8946 5.48043 15 5.73478 15 6V9H14V6H10V9H7V9.5V13H10V14H2C1.73478 14 1.48043 13.8946 1.29289 13.7071C1.10536 13.5196 1 13.2652 1 13V11V5V3C1 2.73478 1.10536 2.48043 1.29289 2.29289ZM14 14C14.2652 14 14.5196 13.8946 14.7071 13.7071C14.8946 13.5196 15 13.2652 15 13H14V14Z" fill="#E0E0E0"/>
+</svg>
--- /dev/null
+<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="m14 17-6-6 6-6" fill="none" stroke="#fff" stroke-width="2"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 5L14 11L8 17" stroke="white" stroke-width="2"/>
+</svg>
--- /dev/null
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="10.5" cy="3.5" r="1.5" fill="white"/>
+<circle cx="10.5" cy="11" r="1.5" fill="white"/>
+<circle cx="10.5" cy="18.5" r="1.5" fill="white"/>
+</svg>
--- /dev/null
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M17 8L11 14L5 8" stroke="white" stroke-width="2"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.9645 2.62927L12.8459 5.22444C12.6271 5.04546 12.3835 4.89134 12.1151 4.76208C11.8565 4.62287 11.5881 4.55327 11.3097 4.55327C11.0909 4.55327 10.8821 4.59802 10.6832 4.68751C10.4943 4.77699 10.3203 4.89631 10.1612 5.04546C10.0021 5.19461 9.86293 5.36364 9.74361 5.55256C9.6243 5.73154 9.52486 5.91052 9.44532 6.08949L9.14702 6.93964C9.35583 7.37714 9.55966 7.79475 9.75853 8.19248C9.92756 8.53055 10.1016 8.87359 10.2805 9.2216C10.4595 9.55966 10.6087 9.82813 10.728 10.027C10.9169 10.3153 11.1058 10.6136 11.2948 10.9219C11.4837 11.2202 11.6925 11.4936 11.9212 11.7422C12.0206 11.8516 12.1399 11.9261 12.2791 11.9659C12.4283 11.9957 12.5625 12.0107 12.6818 12.0107C12.8707 12.0107 13.0497 11.9858 13.2188 11.9361C13.3878 11.8864 13.5469 11.8217 13.696 11.7422L14.0689 12.2195C13.9297 12.4382 13.7607 12.657 13.5618 12.8757C13.3629 13.0945 13.1442 13.2933 12.9055 13.4723C12.6769 13.6513 12.4283 13.7955 12.1598 13.9048C11.9013 14.0242 11.6378 14.0838 11.3693 14.0838C11.1307 14.0838 10.9169 14.044 10.728 13.9645C10.549 13.8949 10.3849 13.8004 10.2358 13.6811C10.0867 13.5519 9.94745 13.4027 9.81819 13.2337C9.68893 13.0646 9.55469 12.8906 9.41549 12.7117C9.30611 12.5426 9.1868 12.3388 9.05753 12.1001C8.93822 11.8516 8.81393 11.598 8.68466 11.3395C8.5554 11.081 8.42614 10.8324 8.29688 10.5938C8.16762 10.3452 8.04333 10.1364 7.92401 9.96734C7.8743 10.1364 7.82955 10.3054 7.78978 10.4744C7.75001 10.6136 7.70526 10.7578 7.65555 10.907C7.60583 11.0561 7.56109 11.1705 7.52131 11.25C7.37216 11.5682 7.18324 11.8963 6.95455 12.2344C6.72586 12.5724 6.46236 12.8807 6.16407 13.1591C5.87572 13.4276 5.55753 13.6513 5.20952 13.8303C4.86151 13.9993 4.49361 14.0838 4.10583 14.0838C3.75782 14.0838 3.41975 14.0142 3.09162 13.875C2.7635 13.7457 2.46023 13.5717 2.18182 13.353L3.12145 10.8771C3.43964 11.076 3.78765 11.255 4.16549 11.4141C4.54333 11.5632 4.92117 11.6378 5.29901 11.6378C5.41833 11.6378 5.54262 11.6278 5.67188 11.608C5.80114 11.5781 5.92543 11.5384 6.04475 11.4886C6.17401 11.429 6.28836 11.3594 6.38779 11.2798C6.48722 11.1903 6.5618 11.0859 6.61151 10.9666C6.68111 10.8374 6.75569 10.6683 6.83523 10.4595C6.91478 10.2507 6.99432 10.0419 7.07387 9.8331C7.16336 9.59447 7.25285 9.34091 7.34234 9.07245L4.53836 4.5831C4.4091 4.43395 4.25001 4.31464 4.06109 4.22515C3.88211 4.12572 3.69319 4.076 3.49432 4.076C3.32529 4.076 3.1662 4.1108 3.01705 4.1804C2.8679 4.24006 2.72373 4.32458 2.58452 4.43395L2.18182 3.91194C2.32103 3.70313 2.48509 3.4993 2.67401 3.30043C2.87287 3.09162 3.08665 2.90768 3.31535 2.74859C3.54404 2.57955 3.78765 2.44532 4.04617 2.34589C4.30469 2.23651 4.56819 2.18182 4.83665 2.18182C5.16478 2.18182 5.46805 2.26634 5.74645 2.43537C6.02486 2.59447 6.28339 2.79333 6.52202 3.03197C6.76066 3.2706 6.97941 3.52912 7.17827 3.80753C7.37714 4.08594 7.55611 4.34447 7.7152 4.5831C7.79475 4.69248 7.87927 4.83168 7.96876 5.00072C8.06819 5.15981 8.16265 5.3189 8.25214 5.47799C8.36151 5.65697 8.46591 5.85086 8.56535 6.05966C8.66478 5.82103 8.76918 5.58239 8.87856 5.34376C8.96805 5.14489 9.05753 4.94106 9.14702 4.73225C9.24645 4.5135 9.33594 4.32458 9.41549 4.16549C9.56464 3.88708 9.73367 3.62856 9.92259 3.38992C10.1115 3.15128 10.3203 2.94248 10.549 2.7635C10.7876 2.58452 11.0462 2.44532 11.3246 2.34589C11.603 2.23651 11.9063 2.18182 12.2344 2.18182C12.5426 2.18182 12.8409 2.2216 13.1293 2.30114C13.4176 2.38069 13.696 2.49006 13.9645 2.62927Z" fill="white"/>
+</svg>
--- /dev/null
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.366 4.5C11.9811 3.83333 11.0189 3.83333 10.634 4.5L3.27276 17.25C2.88786 17.9167 3.36898 18.75 4.13878 18.75H18.8612C19.631 18.75 20.1121 17.9167 19.7272 17.25L12.366 4.5ZM10.6668 14.3809H12.073L12.2723 8.46875H10.4676L10.6668 14.3809ZM12.0555 15.5586C11.8836 15.3906 11.6551 15.3066 11.3699 15.3066C11.0887 15.3066 10.8602 15.3926 10.6844 15.5645C10.5125 15.7324 10.4266 15.9453 10.4266 16.2031C10.4266 16.4609 10.5125 16.6738 10.6844 16.8418C10.8602 17.0098 11.0887 17.0937 11.3699 17.0937C11.6551 17.0937 11.8836 17.0098 12.0555 16.8418C12.2312 16.6738 12.3191 16.4609 12.3191 16.2031C12.3191 15.9414 12.2312 15.7266 12.0555 15.5586Z" fill="#FCFF73"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.6364 4.36363C11.6364 6.37194 10.0083 7.99999 8 7.99999C5.99169 7.99999 4.36363 6.37194 4.36363 4.36363C4.36363 2.35532 5.99169 0.727264 8 0.727264C10.0083 0.727264 11.6364 2.35532 11.6364 4.36363Z" fill="white"/>
+<path d="M12.3636 13.3904C12.3636 15.2727 10.41 15.2727 8 15.2727C5.59003 15.2727 3.63636 15.2727 3.63636 13.3904C3.63636 10.0117 5.59003 7.27272 8 7.27272C10.41 7.27272 12.3636 10.0117 12.3636 13.3904Z" fill="white"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" version="1.1">
+ <g>
+ <title>Layer 1</title>
+ <g stroke="null" stroke-width="0" id="surface1">
+ <path stroke="null" d="m8,15.08759c3.83344,0 6.94107,-3.17238 6.94107,-7.08759c0,-3.91521 -3.10764,-7.08759 -6.94107,-7.08759c-3.83343,0 -6.94107,3.17238 -6.94107,7.08759c0,3.91521 3.10764,7.08759 6.94107,7.08759zm-4.31729,-8.03829c0.35438,-0.3646 0.83824,-0.56905 1.34255,-0.56905c0.5043,0 0.98476,0.20445 1.34255,0.56905l0.28964,0.29646l0.70195,-0.71558l-0.28964,-0.29986c-0.5418,-0.55201 -1.27782,-0.8621 -2.0445,-0.8621c-0.76669,0 -1.50271,0.31008 -2.0445,0.8621l-0.28964,0.29986l0.70194,0.71558l0.28964,-0.29646zm7.29203,-0.56905c-0.5043,0 -0.98477,0.20445 -1.34255,0.56905l-0.28964,0.29646l-0.70195,-0.71558l0.28964,-0.29986c0.54179,-0.55201 1.27781,-0.8621 2.0445,-0.8621c0.76669,0 1.50271,0.31008 2.0445,0.8621l0.28964,0.29986l-0.70195,0.71558l-0.28964,-0.29646c-0.35438,-0.3646 -0.83824,-0.56905 -1.34255,-0.56905zm-2.97475,6.57987c-1.98316,0 -3.47223,-2.02405 -3.96632,-3.03608l7.93265,0c-0.49409,1.01203 -1.98316,3.03608 -3.96633,3.03608zm0,0" fill-rule="evenodd" fill="rgb(100%,100%,100%)" id="svg_1"/>
+ </g>
+ </g>
+
+</svg>
\ No newline at end of file
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.0909 9.45455H15.2727V10.9091H13.0909V13.0909H11.6364V10.9091H9.45454V9.45455H11.6364V7.27274H13.0909V9.45455Z" fill="#A5EFAC"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9094 6.54545C11.3659 5.93769 11.6364 5.18225 11.6364 4.36363C11.6364 2.35532 10.0083 0.727264 8 0.727264C5.99169 0.727264 4.36363 2.35532 4.36363 4.36363C4.36363 5.82377 5.22423 7.08291 6.46587 7.66149C4.81274 8.532 3.63636 10.7686 3.63636 13.3904C3.63636 15.2727 5.59003 15.2727 8 15.2727C10.2115 15.2727 12.0388 15.2727 12.3247 13.8182H10.9091V11.6364H8.72727V8.72726H10.8247C10.445 8.2747 10.0092 7.91168 9.53412 7.66149C10.0795 7.40735 10.5514 7.02191 10.9091 6.5458V6.54545H10.9094Z" fill="white"/>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.45454 2.18183C1.45454 1.78017 1.78016 1.45456 2.18182 1.45456H13.8182C14.2198 1.45456 14.5455 1.78017 14.5455 2.18183V2.9091C14.5455 3.31077 14.2198 3.63638 13.8182 3.63638H2.18182C1.78016 3.63638 1.45454 3.31077 1.45454 2.9091V2.18183Z" fill="white"/>
+<path d="M1.45454 5.8182C1.45454 5.41653 1.78016 5.09092 2.18182 5.09092H13.8182C14.2198 5.09092 14.5455 5.41653 14.5455 5.8182V6.54547C14.5455 6.94713 14.2198 7.27274 13.8182 7.27274V6.54547H10.9091V7.27274H2.18182C1.78016 7.27274 1.45454 6.94713 1.45454 6.54547V5.8182Z" fill="white"/>
+<path d="M1.45454 9.45456C1.45454 9.0529 1.78016 8.72729 2.18182 8.72729H8.72727C8.72727 8.72729 8.72727 9.0529 8.72727 9.45456V10.1818C8.72727 10.5835 8.72727 10.9091 8.72727 10.9091H2.18182C1.78016 10.9091 1.45454 10.5835 1.45454 10.1818V9.45456Z" fill="white"/>
+<path d="M1.45454 13.0909C1.45454 12.6893 1.78016 12.3637 2.18182 12.3637H10.9091V13.8182H13.8182V12.3637C14.2198 12.3637 14.5455 12.6893 14.5455 13.0909V13.8182C14.5455 14.2199 14.2198 14.5455 13.8182 14.5455H2.18182C1.78016 14.5455 1.45454 14.2199 1.45454 13.8182V13.0909Z" fill="white"/>
+<path d="M13.0909 9.45455H15.2727V10.9091H13.0909V13.0909H11.6364V10.9091H9.45454V9.45455H11.6364V7.27274H13.0909V9.45455Z" fill="#A5EFAC"/>
+</svg>
--- /dev/null
+<svg width="768" height="195" viewBox="0 0 768 195" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_d_3652_631)">
+<path d="M655.196 55.8306C654.669 55.3031 653.995 55.0393 653.174 55.0393H618.269C617.449 55.0393 616.804 55.3031 616.335 55.8306C615.749 56.3582 615.456 57.0029 615.456 57.7649V157.381C615.456 158.201 615.749 158.875 616.335 159.403C616.804 159.931 617.449 160.194 618.269 160.194H653.174C653.995 160.194 654.669 159.931 655.196 159.403C655.724 158.875 655.988 158.201 655.988 157.381V57.7649C655.988 57.0029 655.724 56.3582 655.196 55.8306ZM600.861 55.8306C600.274 55.3031 599.6 55.0393 598.838 55.0393H563.845C563.083 55.0393 562.439 55.3031 561.911 55.8306C561.383 56.3582 561.12 57.0029 561.12 57.7649V63.304C558.365 61.0767 555.434 59.0838 552.327 57.3253C547.228 54.336 540.663 52.8413 532.633 52.8413C526.361 52.8413 520.47 53.9843 514.961 56.2702C509.451 58.5562 504.615 61.8093 500.453 66.0296C496.174 70.2499 492.804 75.408 490.342 81.5039C487.88 87.6585 486.65 94.4871 486.65 101.99V102.429C486.65 109.874 487.88 116.614 490.342 122.652C492.804 128.689 496.174 133.847 500.453 138.126C504.615 142.287 509.597 145.541 515.4 147.885C521.027 150.23 527.094 151.402 533.6 151.402C541.513 151.402 548.078 149.761 553.295 146.478H553.383C556.255 144.72 558.951 142.727 561.471 140.5C560.944 146.068 559.156 150.464 556.108 153.688C556.05 153.688 556.02 153.717 556.02 153.776C552.034 157.293 546.085 159.051 538.172 159.051C531.724 159.051 525.834 158.289 520.5 156.765C515.224 155.183 509.978 153.014 504.762 150.259C504.293 149.966 503.794 149.849 503.267 149.907C502.739 149.907 502.27 150.083 501.86 150.435C501.45 150.669 501.127 151.021 500.893 151.49L489.639 174.614C489.346 175.258 489.317 175.932 489.551 176.636C489.786 177.339 490.254 177.896 490.958 178.306C498.343 182.058 506.227 184.9 514.609 186.835C522.932 188.945 532.076 190 542.041 190C552.884 190 561.97 188.74 569.297 186.219C576.799 183.757 583.012 180.065 587.936 175.141L588.024 175.053C592.655 170.423 596.054 164.678 598.223 157.82V157.908C600.509 151.226 601.652 143.079 601.652 133.466V57.7649C601.652 57.0029 601.388 56.3582 600.861 55.8306ZM561.559 101.99V102.429C561.559 107.06 559.918 110.929 556.636 114.035V114.123C553.353 117.406 549.162 119.047 544.063 119.047C538.846 119.047 534.597 117.435 531.314 114.211C528.032 111.104 526.39 107.236 526.39 102.605V102.166C526.332 97.4179 527.944 93.5493 531.226 90.5599L531.314 90.472C534.597 87.1896 538.846 85.5483 544.063 85.5483C549.162 85.5483 553.383 87.131 556.724 90.2962C559.947 93.3441 561.559 97.242 561.559 101.99ZM122.915 65.59V65.5021C119.281 57.3546 114.27 50.2915 107.881 44.3128H107.793C101.462 38.4513 93.5786 33.8794 84.1416 30.5969V30.6849C74.9391 27.4024 64.3591 25.7612 52.4016 25.7612H2.72557C1.96358 25.7612 1.31886 26.025 0.791322 26.5525C0.263788 27.0801 0 27.7248 0 28.4868V157.381C0 158.201 0.263788 158.875 0.791322 159.403C1.31886 159.931 1.96358 160.194 2.72557 160.194H51.6104C63.5678 160.194 74.2357 158.495 83.614 155.095C93.1682 151.695 101.198 147.006 107.705 141.027C114.27 134.99 119.252 127.839 122.652 119.574C126.11 111.368 127.839 102.371 127.839 92.5821V92.1425C127.839 82.5297 126.198 73.6788 122.915 65.59ZM85.6363 92.758V93.1976C85.6363 102.635 82.6762 109.991 76.7561 115.266C70.7774 120.366 62.8058 122.915 52.8412 122.915H41.1476V63.0403H52.8412C62.8058 63.0403 70.7774 65.59 76.7561 70.6895C82.6762 75.9649 85.6363 83.321 85.6363 92.758ZM178.922 55.0393H143.929C143.167 55.0393 142.522 55.3031 141.994 55.8306C141.408 56.3582 141.115 57.0029 141.115 57.7649V157.381C141.115 158.201 141.408 158.875 141.994 159.403C142.522 159.931 143.167 160.194 143.929 160.194H178.922C179.684 160.194 180.329 159.931 180.856 159.403C181.384 158.875 181.647 158.201 181.647 157.381V57.7649C181.647 57.0029 181.384 56.3582 180.856 55.8306C180.329 55.3031 179.684 55.0393 178.922 55.0393ZM381.319 66.2934C370.358 77.2544 364.877 90.5013 364.877 106.034C364.877 117.23 367.72 127.253 373.406 136.104L370.416 164.063V164.151C369.889 168.606 371.266 171.566 374.549 173.031C374.607 173.031 374.666 173.06 374.725 173.119C377.011 174.819 380.088 174.702 383.956 172.767V172.855L408.487 160.898C412.473 161.777 416.664 162.217 421.06 162.217C436.593 162.217 449.839 156.736 460.8 145.775C471.761 134.814 477.242 121.567 477.242 106.034C477.242 90.5013 471.761 77.2544 460.8 66.2934C449.839 55.3324 436.593 49.8519 421.06 49.8519C405.527 49.8519 392.28 55.3324 381.319 66.2934ZM253.216 89.5048V89.5928C255.385 91.5271 256.674 94.1354 257.085 97.4178C254.916 96.8317 252.483 96.3042 249.787 95.8353C245.391 94.956 240.79 94.5164 235.983 94.5164C229.712 94.5164 224.084 95.2198 219.102 96.6266C213.944 98.0919 209.548 100.231 205.914 103.045H205.826C202.25 105.917 199.408 109.522 197.298 113.859C195.246 118.021 194.22 122.915 194.22 128.542V128.982C194.22 134.433 195.099 139.21 196.858 143.313V143.401C198.851 147.446 201.488 150.904 204.771 153.776C208.053 156.531 211.805 158.67 216.025 160.194C220.421 161.66 225.169 162.392 230.268 162.392C237.419 162.392 243.574 161.132 248.732 158.612H248.644C251.575 157.264 254.359 155.564 256.997 153.512V157.381C256.997 158.201 257.26 158.875 257.788 159.403C258.316 159.931 258.96 160.194 259.722 160.194H294.276C295.038 160.194 295.712 159.931 296.298 159.403C296.825 158.875 297.089 158.201 297.089 157.381V100.143C297.089 92.8752 296.21 86.5155 294.452 81.0643C292.693 75.4959 289.733 70.6309 285.571 66.4692V66.3813C281.351 62.3369 275.871 59.1424 269.13 56.7978C262.8 54.629 254.945 53.5447 245.567 53.5447C236.716 53.5447 229.096 54.336 222.707 55.9186H222.795C216.289 57.3253 210.105 59.201 204.243 61.5456C203.599 61.8387 203.1 62.3076 202.749 62.9523C202.456 63.5971 202.426 64.2712 202.661 64.9746L209.87 89.5048C209.988 89.9738 210.251 90.3841 210.662 90.7358C211.013 91.0875 211.453 91.2926 211.98 91.3512C212.449 91.4685 212.948 91.4391 213.475 91.2633C217.813 89.6807 222.033 88.3619 226.136 87.3068H226.224C230.444 86.4276 235.016 85.988 239.94 85.988C245.801 85.988 250.227 87.1603 253.216 89.5048ZM257.348 121.421C257.348 125.582 256.088 128.865 253.568 131.268C250.813 133.73 247.267 134.961 242.929 134.961C240.057 134.961 237.654 134.199 235.719 132.675H235.632C234.049 131.268 233.258 129.334 233.258 126.872V126.432C233.258 123.677 234.342 121.421 236.511 119.662V119.75C238.855 117.933 242.226 117.024 246.622 117.024C248.849 117.024 251.106 117.2 253.392 117.552C254.857 117.845 256.176 118.197 257.348 118.607V121.421ZM349.843 21.1014C349.256 20.5738 348.582 20.3101 347.82 20.3101H312.827C312.065 20.3101 311.421 20.5738 310.893 21.1014C310.365 21.6289 310.102 22.2737 310.102 23.0357V157.381C310.102 158.201 310.365 158.875 310.893 159.403C311.421 159.931 312.065 160.194 312.827 160.194H347.82C348.582 160.194 349.256 159.931 349.843 159.403C350.37 158.875 350.634 158.201 350.634 157.381V23.0357C350.634 22.2737 350.37 21.6289 349.843 21.1014ZM749.097 59.5234H749.185C741.917 55.1859 732.832 53.0171 721.93 53.0171C713.665 53.0171 706.104 54.4239 699.246 57.2374C692.329 60.1095 686.438 64.0953 681.573 69.1948L681.485 69.2827C676.62 74.265 672.781 80.0093 669.967 86.5155L669.88 86.6034C667.125 93.3441 665.747 100.378 665.747 107.705V108.056C665.747 115.383 667.125 122.358 669.88 128.982V129.07C672.752 135.811 676.591 141.613 681.397 146.478C686.262 151.285 692.036 155.212 698.718 158.26H698.806C705.547 161.132 712.844 162.568 720.699 162.568C732.246 162.568 741.653 160.341 748.922 155.886V155.974C756.249 151.578 762.344 145.746 767.209 138.478C767.62 137.891 767.766 137.217 767.649 136.455C767.532 135.693 767.18 135.107 766.594 134.697L743.207 117.728C742.621 117.259 741.976 117.083 741.272 117.2C740.51 117.259 739.895 117.581 739.426 118.168C736.906 121.215 734.297 123.531 731.601 125.113C731.542 125.113 731.484 125.143 731.425 125.201C729.022 126.784 726.062 127.575 722.545 127.575C720.025 127.575 717.709 127.077 715.599 126.081C713.489 125.084 711.701 123.765 710.236 122.124C708.829 120.307 707.686 118.109 706.807 115.53C705.81 113.068 705.312 110.46 705.312 107.705V107.265C705.312 104.627 705.781 102.136 706.719 99.7918L706.807 99.7039C707.628 97.4179 708.712 95.3664 710.06 93.5493C711.643 91.7909 713.489 90.3841 715.599 89.329C717.592 88.4498 719.79 88.0102 722.193 88.0102C725.769 88.0102 728.787 88.8015 731.249 90.3841H731.337C733.858 92.0253 736.261 94.3992 738.547 97.5058C738.957 98.0919 739.543 98.4436 740.305 98.5608C741.126 98.6781 741.829 98.5022 742.415 98.0333L766.154 80.273C766.741 79.8627 767.092 79.2766 767.209 78.5146C767.327 77.8112 767.209 77.1664 766.858 76.5803C762.286 69.312 756.366 63.6264 749.097 59.5234Z" fill="white"/>
+<circle cx="161.337" cy="23.739" r="23.739" fill="white"/>
+<circle cx="636.117" cy="23.739" r="23.739" fill="#7DFFF7"/>
+</g>
+<defs>
+<filter id="filter0_d_3652_631" x="0" y="0" width="767.688" height="195" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="5"/>
+<feComposite in2="hardAlpha" operator="out"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.55 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3652_631"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3652_631" result="shape"/>
+</filter>
+</defs>
+</svg>
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 2H13C14.1046 2 15 2.89543 15 4V11.8483C15 12.4946 14.6877 13.101 14.1617 13.4763L12.1391 14.9196C11.5051 15.372 10.9076 14.5797 11.1562 13.8416C11.3705 13.2052 10.7792 12.8782 10.1076 12.8782H3.00005C1.89549 12.8782 1.00006 11.9827 1.00005 10.8782L1 4.00002C0.999992 2.89544 1.89542 2 3 2ZM9.73033 6.07391V6.06934C9.53136 5.64508 9.25694 5.27728 8.90709 4.966H8.90233C8.55565 4.66077 8.12401 4.42273 7.60728 4.25177V4.25635C7.10338 4.08545 6.52403 4 5.86937 4H3.14928C3.10753 4 3.07225 4.01373 3.04332 4.0412C3.01439 4.06866 2.99998 4.10223 2.99998 4.14191V10.8535C2.99998 10.8962 3.01439 10.9313 3.04332 10.9588C3.07225 10.9863 3.10753 11 3.14928 11H5.82603C6.4807 11 7.0648 10.9115 7.57835 10.7345C8.10155 10.5574 8.54124 10.3133 8.89757 10.002C9.25694 9.68762 9.52977 9.31525 9.71593 8.88489C9.90526 8.45758 9.99998 7.98907 9.99998 7.47937V7.45648C9.99998 6.95593 9.91014 6.49512 9.73033 6.07391ZM7.68907 7.48859V7.51147C7.68907 8.00287 7.52696 8.38593 7.20287 8.66058C6.87547 8.92615 6.43895 9.0589 5.8933 9.0589H5.25304V5.94116H5.8933C6.43895 5.94116 6.87547 6.07391 7.20287 6.33948C7.52696 6.61414 7.68907 6.99719 7.68907 7.48859ZM12 11C12.5523 11 13 10.5523 13 10C13 9.44772 12.5523 9 12 9C11.4477 9 11 9.44772 11 10C11 10.5523 11.4477 11 12 11Z" fill="#E0E0E0"/>
+</svg>
--- /dev/null
+@tool
+extends EditorInspectorPlugin
+
+
+func _can_handle(object: Object) -> bool:
+ return true
+
+
+func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
+ if type == TYPE_OBJECT and hint_type == PROPERTY_HINT_RESOURCE_TYPE:
+ if hint_string == "DialogicTimeline":
+ var editor: EditorProperty = load("res://addons/dialogic/Editor/Inspector/timeline_inspector_field.gd").new()
+ add_property_editor(name, editor)
+ return true
+ return false
--- /dev/null
+@tool
+extends EditorProperty
+
+var field: Control = null
+var button: Button = null
+# An internal value of the property.
+var current_value: DialogicTimeline = null
+# A guard against internal changes when the property is updated.
+var updating = false
+
+
+func _init() -> void:
+ var hbox := HBoxContainer.new()
+ add_child(hbox)
+
+ field = load("res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn").instantiate()
+ hbox.add_child(field)
+ field.placeholder_text = "No Timeline"
+ field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ field.size_flags_vertical = Control.SIZE_SHRINK_CENTER
+ field.mode = field.Modes.IDENTIFIER
+ field.fit_text_length = false
+ field.valid_file_drop_extension = ".dtl"
+ field.value_changed.connect(_on_field_value_changed)
+ field.get_suggestions_func = get_timeline_suggestions
+
+ button = Button.new()
+ hbox.add_child(button)
+ button.hide()
+ button.pressed.connect(_on_button_pressed, CONNECT_DEFERRED)
+
+
+func _on_field_value_changed(property:String, value:Variant) -> void:
+ # Ignore the signal if the property is currently being updated.
+ if updating:
+ return
+
+ var new_value: DialogicTimeline = null
+ if value:
+ new_value = DialogicResourceUtil.get_timeline_resource(value)
+
+ if current_value != new_value:
+ current_value = new_value
+ if current_value:
+ button.show()
+ else:
+ button.hide()
+ emit_changed(get_edited_property(), current_value)
+
+
+func _update_property() -> void:
+ field.resource_icon = get_theme_icon("TripleBar", "EditorIcons")
+ button.icon = get_theme_icon("ExternalLink", "EditorIcons")
+
+ # Read the current value from the property.
+ var new_value = get_edited_object()[get_edited_property()]
+ if (new_value == current_value):
+ return
+
+ # Update the control with the new value.
+ updating = true
+ current_value = new_value
+ if current_value:
+ field.set_value(DialogicResourceUtil.get_unique_identifier(current_value.resource_path))
+ button.show()
+ else:
+ button.hide()
+ field.set_value("")
+ updating = false
+
+
+func get_timeline_suggestions(filter:String) -> Dictionary:
+ var suggestions := {}
+ var timeline_directory := DialogicResourceUtil.get_timeline_directory()
+ for identifier in timeline_directory.keys():
+ suggestions[identifier] = {'value': identifier, 'tooltip':timeline_directory[identifier], 'editor_icon': ["TripleBar", "EditorIcons"]}
+ return suggestions
+
+
+func _on_button_pressed() -> void:
+ if current_value:
+ EditorInterface.edit_resource(current_value)
--- /dev/null
+@tool
+extends Label
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+ # don't load the label settings when opening as a scene
+ # prevents HUGE diffs
+ if owner.get_parent() is SubViewport:
+ return
+ label_settings = LabelSettings.new()
+ label_settings.font = get_theme_font("doc_italic", "EditorFonts")
+ label_settings.font_size = get_theme_font_size('font_size', 'Label')
+ label_settings.font_color = get_theme_color("accent_color", "Editor")
--- /dev/null
+class_name DialogicCsvFile
+extends RefCounted
+## Handles translation of a [class DialogicTimeline] to a CSV file.
+
+var lines: Array[PackedStringArray] = []
+## Dictionary of lines from the original file.
+## Key: String, Value: PackedStringArray
+var old_lines: Dictionary = {}
+
+## The amount of columns the CSV file has after loading it.
+## Used to add trailing commas to new lines.
+var column_count := 0
+
+## Whether this CSV file was able to be loaded a defined
+## file path.
+var is_new_file: bool = false
+
+## The underlying file used to read and write the CSV file.
+var file: FileAccess
+
+## File path used to load the CSV file.
+var used_file_path: String
+
+## The amount of events that were updated in the CSV file.
+var updated_rows: int = 0
+
+## The amount of events that were added to the CSV file.
+var new_rows: int = 0
+
+## Whether this CSV handler should add newlines as a separator between sections.
+## A section may be a new character, new timeline, or new glossary item inside
+## a per-project file.
+var add_separator: bool = false
+
+enum PropertyType {
+ String = 0,
+ Array = 1,
+ Other = 2,
+}
+
+## The translation property used for the glossary item translation.
+const TRANSLATION_ID := DialogicGlossary.TRANSLATION_PROPERTY
+
+## Attempts to load the CSV file from [param file_path].
+## If the file does not exist, a single entry is added to the [member lines]
+## array.
+## The [param separator_enabled] enables adding newlines as a separator to
+## per-project files. This is useful for readability.
+func _init(file_path: String, original_locale: String, separator_enabled: bool) -> void:
+ used_file_path = file_path
+ add_separator = separator_enabled
+
+ # The first entry must be the locale row.
+ # [method collect_lines_from_timeline] will add the other locales, if any.
+ var locale_array_line := PackedStringArray(["keys", original_locale])
+ lines.append(locale_array_line)
+
+ if not ResourceLoader.exists(file_path):
+ is_new_file = true
+
+ # The "keys" and original locale are the only columns in a new file.
+ # For example: "keys, en"
+ column_count = 2
+ return
+
+ file = FileAccess.open(file_path, FileAccess.READ)
+
+ var locale_csv_row := file.get_csv_line()
+ column_count = locale_csv_row.size()
+ var locale_key := locale_csv_row[0]
+
+ old_lines[locale_key] = locale_csv_row
+
+ _read_file_into_lines()
+
+
+## Private function to read the CSV file into the [member lines] array.
+## Cannot be called on a new file.
+func _read_file_into_lines() -> void:
+ while not file.eof_reached():
+ var line := file.get_csv_line()
+ var row_key := line[0]
+
+ old_lines[row_key] = line
+
+
+## Collects names from the given [param characters] and adds them to the
+## [member lines].
+##
+## If this is the character name CSV file, use this method to
+## take previously collected characters from other [class DialogicCsvFile]s.
+func collect_lines_from_characters(characters: Dictionary) -> void:
+ for character: DialogicCharacter in characters.values():
+ # Add row for display names.
+ var name_property := DialogicCharacter.TranslatedProperties.NAME
+ var display_name_key: String = character.get_property_translation_key(name_property)
+ var line_value: String = character.display_name
+ var array_line := PackedStringArray([display_name_key, line_value])
+ lines.append(array_line)
+
+ var nicknames: Array = character.nicknames
+
+ if not nicknames.is_empty():
+ var nick_name_property := DialogicCharacter.TranslatedProperties.NICKNAMES
+ var nickname_string: String = ",".join(nicknames)
+ var nickname_name_line_key: String = character.get_property_translation_key(nick_name_property)
+ var nick_array_line := PackedStringArray([nickname_name_line_key, nickname_string])
+ lines.append(nick_array_line)
+
+ # New character item, if needed, add a separator.
+ if add_separator:
+ _append_empty()
+
+
+## Appends an empty line to the [member lines] array.
+func _append_empty() -> void:
+ var empty_line := PackedStringArray(["", ""])
+ lines.append(empty_line)
+
+
+## Returns the property type for the given [param key].
+func _get_key_type(key: String) -> PropertyType:
+ if key.ends_with(DialogicGlossary.NAME_PROPERTY):
+ return PropertyType.String
+
+ if key.ends_with(DialogicGlossary.ALTERNATIVE_PROPERTY):
+ return PropertyType.Array
+
+ return PropertyType.Other
+
+
+func _process_line_into_array(csv_values: PackedStringArray, property_type: PropertyType) -> Array[String]:
+ const KEY_VALUE_INDEX := 0
+ var values_as_array: Array[String] = []
+
+ for i in csv_values.size():
+
+ if i == KEY_VALUE_INDEX:
+ continue
+
+ var csv_value := csv_values[i]
+
+ if csv_value.is_empty():
+ continue
+
+ match property_type:
+ PropertyType.String:
+ values_as_array = [csv_value]
+
+ PropertyType.Array:
+ var split_values := csv_value.split(",")
+
+ for value in split_values:
+ values_as_array.append(value)
+
+ return values_as_array
+
+
+func _add_keys_to_glossary(glossary: DialogicGlossary, names: Array) -> void:
+ var glossary_prefix_key := glossary._get_glossary_translation_id_prefix()
+ var glossary_translation_id_prefix := _get_glossary_translation_key_prefix(glossary)
+
+ for glossary_line: PackedStringArray in names:
+
+ if glossary_line.is_empty():
+ continue
+
+ var csv_key := glossary_line[0]
+
+ # CSV line separators will be empty.
+ if not csv_key.begins_with(glossary_prefix_key):
+ continue
+
+ var value_type := _get_key_type(csv_key)
+
+ # String and Array are the only valid types.
+ if (value_type == PropertyType.Other
+ or not csv_key.begins_with(glossary_translation_id_prefix)):
+ continue
+
+ var new_line_to_add := _process_line_into_array(glossary_line, value_type)
+
+ for name_to_add: String in new_line_to_add:
+ glossary._translation_keys[name_to_add.strip_edges()] = csv_key
+
+
+
+## Reads all [member lines] and adds them to the given [param glossary]'s
+## internal collection of words-to-translation-key mappings.
+##
+## Populate the CSV's lines with the method [method collect_lines_from_glossary]
+## before.
+func add_translation_keys_to_glossary(glossary: DialogicGlossary) -> void:
+ glossary._translation_keys.clear()
+ _add_keys_to_glossary(glossary, lines)
+ _add_keys_to_glossary(glossary, old_lines.values())
+
+
+## Returns the translation key prefix for the given [param glossary_translation_id].
+## The resulting format will look like this: Glossary/a2/
+## You can use this to find entries in [member lines] that to a glossary.
+func _get_glossary_translation_key_prefix(glossary: DialogicGlossary) -> String:
+ return (
+ DialogicGlossary.RESOURCE_NAME
+ .path_join(glossary._translation_id)
+ )
+
+
+## Returns whether [param value_b] is greater than [param value_a].
+##
+## This method helps to sort glossary entry properties by their importance
+## matching the order in the editor.
+##
+## TODO: Allow Dialogic users to define their own order.
+func _sort_glossary_entry_property_keys(property_key_a: String, property_key_b: String) -> bool:
+ const GLOSSARY_CSV_LINE_ORDER := {
+ DialogicGlossary.NAME_PROPERTY: 0,
+ DialogicGlossary.ALTERNATIVE_PROPERTY: 1,
+ DialogicGlossary.TEXT_PROPERTY: 2,
+ DialogicGlossary.EXTRA_PROPERTY: 3,
+ }
+ const UNKNOWN_PROPERTY_ORDER := 100
+
+ var value_a: int = GLOSSARY_CSV_LINE_ORDER.get(property_key_a, UNKNOWN_PROPERTY_ORDER)
+ var value_b: int = GLOSSARY_CSV_LINE_ORDER.get(property_key_b, UNKNOWN_PROPERTY_ORDER)
+
+ return value_a < value_b
+
+
+## Collects properties from glossary entries from the given [param glossary] and
+## adds them to the [member lines].
+func collect_lines_from_glossary(glossary: DialogicGlossary) -> void:
+
+ for glossary_value: Variant in glossary.entries.values():
+
+ if glossary_value is String:
+ continue
+
+ var glossary_entry: Dictionary = glossary_value
+ var glossary_entry_name: String = glossary_entry[DialogicGlossary.NAME_PROPERTY]
+
+ var _glossary_translation_id := glossary.get_set_glossary_translation_id()
+ var entry_translation_id := glossary.get_set_glossary_entry_translation_id(glossary_entry_name)
+
+ var entry_property_keys := glossary_entry.keys().duplicate()
+ entry_property_keys.sort_custom(_sort_glossary_entry_property_keys)
+
+ var entry_name_property: String = glossary_entry[DialogicGlossary.NAME_PROPERTY]
+
+ for entry_key: String in entry_property_keys:
+ # Ignore private keys.
+ if entry_key.begins_with(DialogicGlossary.PRIVATE_PROPERTY_PREFIX):
+ continue
+
+ var item_value: Variant = glossary_entry[entry_key]
+ var item_value_str := ""
+
+ if item_value is Array:
+ var item_array := item_value as Array
+ # We use a space after the comma to make it easier to read.
+ item_value_str = " ,".join(item_array)
+
+ elif not item_value is String or item_value.is_empty():
+ continue
+
+ else:
+ item_value_str = item_value
+
+ var glossary_csv_key := glossary._get_glossary_translation_key(entry_translation_id, entry_key)
+
+ if (entry_key == DialogicGlossary.NAME_PROPERTY
+ or entry_key == DialogicGlossary.ALTERNATIVE_PROPERTY):
+ glossary.entries[glossary_csv_key] = entry_name_property
+
+ var glossary_line := PackedStringArray([glossary_csv_key, item_value_str])
+
+ lines.append(glossary_line)
+
+ # New glossary item, if needed, add a separator.
+ if add_separator:
+ _append_empty()
+
+
+
+## Collects translatable events from the given [param timeline] and adds
+## them to the [member lines].
+func collect_lines_from_timeline(timeline: DialogicTimeline) -> void:
+ for event: DialogicEvent in timeline.events:
+
+ if event.can_be_translated():
+
+ if event._translation_id.is_empty():
+ event.add_translation_id()
+ event.update_text_version()
+
+ var properties: Array = event._get_translatable_properties()
+
+ for property: String in properties:
+ var line_key: String = event.get_property_translation_key(property)
+ var line_value: String = event._get_property_original_translation(property)
+ var array_line := PackedStringArray([line_key, line_value])
+ lines.append(array_line)
+
+ # End of timeline, if needed, add a separator.
+ if add_separator:
+ _append_empty()
+
+
+## Clears the CSV file on disk and writes the current [member lines] array to it.
+## Uses the [member old_lines] dictionary to update existing translations.
+## If a translation row misses a column, a trailing comma will be added to
+## conform to the CSV file format.
+##
+## If the locale CSV line was collected only, a new file won't be created and
+## already existing translations won't be updated.
+func update_csv_file_on_disk() -> void:
+ # None or locale row only.
+ if lines.size() < 2:
+ print_rich("[color=yellow]No lines for the CSV file, skipping: " + used_file_path)
+
+ return
+
+ # Clear the current CSV file.
+ file = FileAccess.open(used_file_path, FileAccess.WRITE)
+
+ for line in lines:
+ var row_key := line[0]
+
+ # In case there might be translations for this line already,
+ # add them at the end again (orig locale text is replaced).
+ if row_key in old_lines:
+ var old_line: PackedStringArray = old_lines[row_key]
+ var updated_line: PackedStringArray = line + old_line.slice(2)
+
+ var line_columns: int = updated_line.size()
+ var line_columns_to_add := column_count - line_columns
+
+ # Add trailing commas to match the amount of columns.
+ for _i in range(line_columns_to_add):
+ updated_line.append("")
+
+ file.store_csv_line(updated_line)
+ updated_rows += 1
+
+ else:
+ var line_columns: int = line.size()
+ var line_columns_to_add := column_count - line_columns
+
+ # Add trailing commas to match the amount of columns.
+ for _i in range(line_columns_to_add):
+ line.append("")
+
+ file.store_csv_line(line)
+ new_rows += 1
+
+ file.close()
--- /dev/null
+@tool
+extends DialogicEditor
+
+## Editor that contains all settings
+
+var button_group := ButtonGroup.new()
+var registered_sections: Array[DialogicSettingsPage] = []
+
+
+func _get_title() -> String:
+ return "Settings"
+
+
+func _get_icon() -> Texture:
+ return get_theme_icon("PluginScript", "EditorIcons")
+
+
+func _register() -> void:
+ editors_manager.register_simple_editor(self)
+ self.alternative_text = "Customize dialogic and it's behaviour"
+
+
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+
+ register_settings_section("res://addons/dialogic/Editor/Settings/settings_general.tscn")
+ register_settings_section("res://addons/dialogic/Editor/Settings/settings_translation.tscn")
+ register_settings_section("res://addons/dialogic/Editor/Settings/settings_modules.tscn")
+
+ for indexer in DialogicUtil.get_indexers():
+ for settings_page in indexer._get_settings_pages():
+ register_settings_section(settings_page)
+
+ add_registered_sections()
+ %SettingsTabs.get_child(0).button_pressed = true
+ %SettingsContent.get_child(0).show()
+
+
+func register_settings_section(path:String) -> void:
+ var section: Control = load(path).instantiate()
+ registered_sections.append(section)
+
+
+func add_registered_sections() -> void:
+ for i in %SettingsTabs.get_children():
+ i.queue_free()
+ for i in %FeatureTabs.get_children():
+ i.queue_free()
+
+ for i in %SettingsContent.get_children():
+ i.queue_free()
+
+
+ registered_sections.sort_custom(section_sort)
+ for section in registered_sections:
+
+ section.name = section._get_title()
+
+ var vbox := VBoxContainer.new()
+ vbox.set_meta('section', section)
+ vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ vbox.name = section.name
+ var hbox := HBoxContainer.new()
+
+ var title := Label.new()
+ title.text = section.name
+ title.theme_type_variation = 'DialogicSectionBig'
+ hbox.add_child(title)
+ vbox.add_child(hbox)
+
+
+ if !section.short_info.is_empty():
+ var tooltip_hint: Control = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
+ tooltip_hint.hint_text = section.short_info
+ hbox.add_child(tooltip_hint)
+
+
+ var scroll := ScrollContainer.new()
+ scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ var inner_vbox := VBoxContainer.new()
+ inner_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ inner_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ scroll.add_child(inner_vbox)
+ var panel := PanelContainer.new()
+ panel.theme_type_variation = "DialogicPanelA"
+ panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ if section.size_flags_vertical == Control.SIZE_EXPAND_FILL:
+ panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ inner_vbox.add_child(panel)
+
+
+ var info_section: Control = section._get_info_section()
+ if info_section != null:
+ inner_vbox.add_child(Control.new())
+ inner_vbox.get_child(-1).custom_minimum_size.y = 50
+
+ inner_vbox.add_child(title.duplicate())
+ inner_vbox.get_child(-1).text = "Information"
+ var info_panel := panel.duplicate()
+ info_panel.theme_type_variation = "DialogicPanelDarkA"
+
+ inner_vbox.add_child(info_panel)
+ info_section.get_parent().remove_child(info_section)
+ info_panel.add_child(info_section)
+
+ panel.add_child(section)
+ vbox.add_child(scroll)
+
+
+ var button := Button.new()
+ button.text = " "+section.name
+ button.tooltip_text = section.name
+ button.toggle_mode = true
+ button.button_group = button_group
+ button.expand_icon = true
+ button.alignment = HORIZONTAL_ALIGNMENT_LEFT
+ button.flat = true
+ button.add_theme_color_override('font_pressed_color', get_theme_color("property_color_z", "Editor"))
+ button.add_theme_color_override('font_hover_color', get_theme_color('warning_color', 'Editor'))
+ button.add_theme_color_override('font_focus_color', get_theme_color('warning_color', 'Editor'))
+ button.add_theme_stylebox_override('focus', StyleBoxEmpty.new())
+ button.pressed.connect(open_tab.bind(vbox))
+ if section._is_feature_tab():
+ %FeatureTabs.add_child(button)
+ else:
+ %SettingsTabs.add_child(button)
+
+ vbox.hide()
+# if section.has_method('_get_icon'):
+# icon.texture = section._get_icon()
+ %SettingsContent.add_child(vbox)
+
+
+func open_tab(tab_to_show:Control) -> void:
+ for tab in %SettingsContent.get_children():
+ tab.hide()
+
+ tab_to_show.show()
+
+
+func section_sort(item1:DialogicSettingsPage, item2:DialogicSettingsPage) -> bool:
+ if !item1._is_feature_tab() and item2._is_feature_tab():
+ return true
+ if item1._get_priority() > item2._get_priority():
+ return true
+ return false
+
+
+
+func _open(extra_information:Variant = null) -> void:
+ refresh()
+ if typeof(extra_information) == TYPE_STRING:
+ if %SettingsContent.has_node(extra_information):
+ open_tab(%SettingsContent.get_node(extra_information))
+
+
+func _close() -> void:
+ for child in %SettingsContent.get_children():
+ if child.get_meta('section').has_method('_about_to_close'):
+ child.get_meta('section')._about_to_close()
+
+
+func refresh() -> void:
+ for child in %SettingsContent.get_children():
+ if child.get_meta('section').has_method('_refresh'):
+ child.get_meta('section')._refresh()
+
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://dganirw26brfb"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Settings/settings_editor.gd" id="1"]
+
+[node name="Settings" type="HSplitContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1")
+
+[node name="TabList" type="ScrollContainer" parent="."]
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="Margin" type="MarginContainer" parent="TabList"]
+layout_mode = 2
+theme_override_constants/margin_top = 3
+
+[node name="VBox" type="VBoxContainer" parent="TabList/Margin"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Title" type="Label" parent="TabList/Margin/VBox"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Settings"
+
+[node name="SettingsTabs" type="VBoxContainer" parent="TabList/Margin/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+
+[node name="Control" type="Control" parent="TabList/Margin/VBox"]
+custom_minimum_size = Vector2(0, 30)
+layout_mode = 2
+
+[node name="Title2" type="Label" parent="TabList/Margin/VBox"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Features"
+
+[node name="FeatureTabs" type="VBoxContainer" parent="TabList/Margin/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+
+[node name="SettingsContent" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+## Settings tab that holds genreal dialogic settings.
+
+
+func _get_title() -> String:
+ return "General"
+
+
+func _get_priority() -> int:
+ return 99
+
+func _ready() -> void:
+ var s := DCSS.inline({
+ 'padding': 5,
+ 'background': Color(0.545098, 0.545098, 0.545098, 0.211765)
+ })
+ %ExtensionsFolderPicker.resource_icon = get_theme_icon("Folder", "EditorIcons")
+
+ # Signals
+ %ExtensionsFolderPicker.value_changed.connect(_on_ExtensionsFolder_value_changed)
+ %PhysicsTimerButton.toggled.connect(_on_physics_timer_button_toggled)
+
+ # Colors
+ %ResetColorsButton.icon = get_theme_icon("Reload", "EditorIcons")
+ %ResetColorsButton.button_up.connect(_on_reset_colors_button)
+
+ # Extension creator
+ %ExtensionCreator.hide()
+
+
+func _refresh() -> void:
+ %PhysicsTimerButton.button_pressed = DialogicUtil.is_physics_timer()
+ %LayoutNodeEndBehaviour.select(ProjectSettings.get_setting('dialogic/layout/end_behaviour', 0))
+ %ExtensionsFolderPicker.set_value(ProjectSettings.get_setting('dialogic/extensions_folder', 'res://addons/dialogic_additions'))
+
+ update_color_palette()
+
+ %SectionList.clear()
+ %SectionList.create_item()
+ var cached_events := DialogicResourceUtil.get_event_cache()
+ var sections := []
+ var section_order: Array = DialogicUtil.get_editor_setting('event_section_order', ['Main', 'Logic', 'Flow', 'Audio', 'Visuals','Other', 'Helper'])
+ for ev in cached_events:
+ if !ev.event_category in sections:
+ sections.append(ev.event_category)
+ var item: TreeItem = %SectionList.create_item(null)
+ item.set_text(0, ev.event_category)
+ item.add_button(0, get_theme_icon("ArrowUp", "EditorIcons"))
+ item.add_button(0, get_theme_icon("ArrowDown", "EditorIcons"))
+ if ev.event_category in section_order:
+
+ item.move_before(item.get_parent().get_child(min(section_order.find(ev.event_category),item.get_parent().get_child_count()-1)))
+
+ %SectionList.get_root().get_child(0).set_button_disabled(0, 0, true)
+ %SectionList.get_root().get_child(-1).set_button_disabled(0, 1, true)
+
+
+func _on_section_list_button_clicked(item:TreeItem, column, id, mouse_button_index):
+ if id == 0:
+ item.move_before(item.get_parent().get_child(item.get_index()-1))
+ else:
+ item.move_after(item.get_parent().get_child(item.get_index()+1))
+
+ for child in %SectionList.get_root().get_children():
+ child.set_button_disabled(0, 0, false)
+ child.set_button_disabled(0, 1, false)
+
+ %SectionList.get_root().get_child(0).set_button_disabled(0, 0, true)
+ %SectionList.get_root().get_child(-1).set_button_disabled(0, 1, true)
+
+ var sections := []
+ for child in %SectionList.get_root().get_children():
+ sections.append(child.get_text(0))
+
+ DialogicUtil.set_editor_setting('event_section_order', sections)
+ force_event_button_list_reload()
+
+
+func force_event_button_list_reload() -> void:
+ find_parent('EditorsManager').editors['Timeline'].node.get_node('%VisualEditor').load_event_buttons()
+
+
+func update_color_palette() -> void:
+ # Color Palette
+ for child in %Colors.get_children():
+ child.queue_free()
+ for color in DialogicUtil.get_color_palette():
+ var button := ColorPickerButton.new()
+ button.custom_minimum_size = Vector2(50 ,50) * DialogicUtil.get_editor_scale()
+ %Colors.add_child(button)
+ button.color = DialogicUtil.get_color(color)
+ button.color_changed.connect(_on_color_change)
+
+
+func _on_color_change(color:Color) -> void:
+ var new_palette := {}
+ for i in %Colors.get_children():
+ new_palette['Color'+str(i.get_index()+1)] = i.color
+ DialogicUtil.set_editor_setting('color_palette', new_palette)
+
+
+
+func _on_reset_colors_button() -> void:
+ DialogicUtil.set_editor_setting('color_palette', null)
+ update_color_palette()
+
+
+func _on_physics_timer_button_toggled(is_toggled: bool) -> void:
+ ProjectSettings.set_setting('dialogic/timer/process_in_physics', is_toggled)
+ ProjectSettings.save()
+
+
+func _on_ExtensionsFolder_value_changed(property:String, value:String) -> void:
+ if value == null or value.is_empty():
+ value = 'res://addons/dialogic_additions'
+ ProjectSettings.set_setting('dialogic/extensions_folder', value)
+ ProjectSettings.save()
+
+
+func _on_layout_node_end_behaviour_item_selected(index:int) -> void:
+ ProjectSettings.set_setting('dialogic/layout/end_behaviour', index)
+ ProjectSettings.save()
+
+
+################################################################################
+## EXTENSION CREATOR
+################################################################################
+
+func _on_create_extension_button_pressed() -> void:
+ %CreateExtensionButton.hide()
+ %ExtensionCreator.show()
+
+ %NameEdit.text = ""
+ %NameEdit.grab_focus()
+
+
+func _on_submit_extension_button_pressed() -> void:
+ if %NameEdit.text.is_empty():
+ return
+
+ var extensions_folder: String = ProjectSettings.get_setting('dialogic/extensions_folder', 'res://addons/dialogic_additions')
+
+ extensions_folder = extensions_folder.path_join(%NameEdit.text.to_pascal_case())
+ DirAccess.make_dir_recursive_absolute(extensions_folder)
+ var mode: int = %ExtensionMode.selected
+
+ var file: FileAccess
+ var indexer_content := "@tool\nextends DialogicIndexer\n\n"
+ if mode != 2: # don't add event in Subsystem Only mode
+ indexer_content += """func _get_events() -> Array:
+ return [this_folder.path_join('event_"""+%NameEdit.text.to_snake_case()+""".gd')]\n\n"""
+ file = FileAccess.open(extensions_folder.path_join('event_'+%NameEdit.text.to_snake_case()+'.gd'), FileAccess.WRITE)
+ file.store_string(
+
+#region EXTENDED EVENT SCRIPT
+"""@tool
+extends DialogicEvent
+class_name Dialogic"""+%NameEdit.text.to_pascal_case()+"""Event
+
+# Define properties of the event here
+
+func _execute() -> void:
+ # This will execute when the event is reached
+ finish() # called to continue with the next event
+
+
+#region INITIALIZE
+################################################################################
+# Set fixed settings of this event
+func _init() -> void:
+ event_name = \""""+%NameEdit.text.capitalize()+"""\"
+ event_category = "Other"
+
+\n
+#endregion
+
+#region SAVING/LOADING
+################################################################################
+func get_shortcode() -> String:
+ return \""""+%NameEdit.text.to_snake_case()+"""\"
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ #"my_parameter" : {"property": "property", "default": "Default"},
+ }
+
+# You can alternatively overwrite these 3 functions: to_text(), from_text(), is_valid_event()
+#endregion
+
+
+#region EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ pass
+
+#endregion
+""")
+
+#endregion
+ if mode != 0: # don't add subsystem in event only mode
+ indexer_content += """func _get_subsystems() -> Array:
+ return [{'name':'"""+%NameEdit.text.to_pascal_case()+"""', 'script':this_folder.path_join('subsystem_"""+%NameEdit.text.to_snake_case()+""".gd')}]"""
+ file = FileAccess.open(extensions_folder.path_join('subsystem_'+%NameEdit.text.to_snake_case()+'.gd'), FileAccess.WRITE)
+ file.store_string(
+
+# region EXTENDED SUBSYSTEM SCRIPT
+"""extends DialogicSubsystem
+
+## Describe the subsystems purpose here.
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(clear_flag:=Dialogic.ClearFlags.FULL_CLEAR) -> void:
+ pass
+
+func load_game_state(load_flag:=LoadFlags.FULL_LOAD) -> void:
+ pass
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+# Add some useful methods here.
+
+#endregion
+""")
+ file = FileAccess.open(extensions_folder.path_join('index.gd'), FileAccess.WRITE)
+ file.store_string(indexer_content)
+
+ %ExtensionCreator.hide()
+ %CreateExtensionButton.show()
+
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
+ force_event_button_list_reload()
+
+
+
+func _on_reload_pressed() -> void:
+ DialogicUtil._update_autoload_subsystem_access()
--- /dev/null
+[gd_scene load_steps=6 format=3 uid="uid://b873ho41sklv8"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Settings/settings_general.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_kqhx5"]
+[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="3_i7rug"]
+
+[sub_resource type="Image" id="Image_e1gle"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_4wgbv"]
+image = SubResource("Image_e1gle")
+
+[node name="General" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2")
+
+[node name="PaletteTitle" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="SectionPaletteTitle" type="Label" parent="PaletteTitle"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Color Palette"
+
+[node name="HintTooltip" parent="PaletteTitle" instance=ExtResource("2_kqhx5")]
+layout_mode = 2
+tooltip_text = "These colors are used for the events."
+texture = SubResource("ImageTexture_4wgbv")
+hint_text = "These colors are used for the events."
+
+[node name="ResetColorsButton" type="Button" parent="PaletteTitle"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+tooltip_text = "Reset Colors to default"
+icon = SubResource("ImageTexture_4wgbv")
+flat = true
+
+[node name="ScrollContainer" type="ScrollContainer" parent="."]
+layout_mode = 2
+horizontal_scroll_mode = 3
+vertical_scroll_mode = 0
+
+[node name="Colors" type="HBoxContainer" parent="ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator" type="HSeparator" parent="."]
+layout_mode = 2
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="SectionBehaviourTitle" type="Label" parent="HBoxContainer2"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Layout Node Behaviour"
+
+[node name="HintTooltip" parent="HBoxContainer2" instance=ExtResource("2_kqhx5")]
+layout_mode = 2
+tooltip_text = "The layout scene configured in the Layout editor is automatically
+instanced when calling Dialogic.start(). Depending on your game,
+you might want it to be deleted after the dialogue, be hidden
+(as reinstancing often is wasting resources) or kept visible. "
+texture = SubResource("ImageTexture_4wgbv")
+hint_text = "The layout scene configured in the Layout editor is automatically
+instanced when calling Dialogic.start(). Depending on your game,
+you might want it to be deleted after the dialogue, be hidden
+(as reinstancing often is wasting resources) or kept visible. "
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer3"]
+layout_mode = 2
+text = "On timeline end"
+
+[node name="LayoutNodeEndBehaviour" type="OptionButton" parent="HBoxContainer3"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 3
+selected = 0
+fit_to_longest_item = false
+popup/item_0/text = "Delete Layout Node"
+popup/item_0/id = 0
+popup/item_1/text = "Hide Layout Node"
+popup/item_1/id = 1
+popup/item_2/text = "Keep Layout Node"
+popup/item_2/id = 2
+
+[node name="HSeparator4" type="HSeparator" parent="."]
+layout_mode = 2
+
+[node name="HBoxContainer6" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="HBoxContainer4" type="VBoxContainer" parent="HBoxContainer6"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBoxContainer5" type="HBoxContainer" parent="HBoxContainer6/HBoxContainer4"]
+layout_mode = 2
+
+[node name="SectionExtensionsTitle" type="Label" parent="HBoxContainer6/HBoxContainer4/HBoxContainer5"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Extensions"
+
+[node name="HintTooltip" parent="HBoxContainer6/HBoxContainer4/HBoxContainer5" instance=ExtResource("2_kqhx5")]
+layout_mode = 2
+tooltip_text = "Configure where dialogic looks for custom modules.
+
+You will have to restart the project to see the change take action."
+texture = SubResource("ImageTexture_4wgbv")
+hint_text = "Configure where dialogic looks for custom modules.
+
+You will have to restart the project to see the change take action."
+
+[node name="Reload" type="Button" parent="HBoxContainer6/HBoxContainer4/HBoxContainer5"]
+layout_mode = 2
+text = "Reload"
+flat = true
+
+[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer6/HBoxContainer4"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer6/HBoxContainer4/HBoxContainer"]
+layout_mode = 2
+text = "Extensions folder"
+
+[node name="ExtensionsFolderPicker" parent="HBoxContainer6/HBoxContainer4/HBoxContainer" instance=ExtResource("3_i7rug")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder = "res://addons/dialogic_additions/Events"
+file_mode = 2
+resource_icon = SubResource("ImageTexture_4wgbv")
+
+[node name="VSeparator" type="VSeparator" parent="HBoxContainer6"]
+layout_mode = 2
+
+[node name="ExtensionsPanel" type="PanelContainer" parent="HBoxContainer6"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_type_variation = &"DialogicPanelA"
+
+[node name="VBox" type="VBoxContainer" parent="HBoxContainer6/ExtensionsPanel"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBoxContainer6" type="HBoxContainer" parent="HBoxContainer6/ExtensionsPanel/VBox"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer6/ExtensionsPanel/VBox/HBoxContainer6"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Extension Creator "
+
+[node name="HintTooltip" parent="HBoxContainer6/ExtensionsPanel/VBox/HBoxContainer6" instance=ExtResource("2_kqhx5")]
+layout_mode = 2
+tooltip_text = "Use the Exension Creator to quickly setup custom modules!"
+texture = SubResource("ImageTexture_4wgbv")
+hint_text = "Use the Exension Creator to quickly setup custom modules!"
+
+[node name="CreateExtensionButton" type="Button" parent="HBoxContainer6/ExtensionsPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Create New Extension"
+
+[node name="ExtensionCreator" type="VBoxContainer" parent="HBoxContainer6/ExtensionsPanel/VBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+
+[node name="ExtensionCreatorOptions" type="GridContainer" parent="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator"]
+layout_mode = 2
+columns = 2
+
+[node name="NameLabel" type="Label" parent="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator/ExtensionCreatorOptions"]
+layout_mode = 2
+text = "Name:"
+
+[node name="NameEdit" type="LineEdit" parent="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator/ExtensionCreatorOptions"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "e.g. \"Print\", \"Item\", \"Door\", \"Quest\""
+
+[node name="ModeLabel" type="Label" parent="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator/ExtensionCreatorOptions"]
+layout_mode = 2
+text = "Setup mode:"
+
+[node name="ExtensionMode" type="OptionButton" parent="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator/ExtensionCreatorOptions"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 4
+selected = 0
+popup/item_0/text = "Event only"
+popup/item_0/id = 0
+popup/item_1/text = "Event+Subsystem"
+popup/item_1/id = 1
+popup/item_2/text = "Subsystem only"
+popup/item_2/id = 2
+popup/item_3/text = "Complex"
+popup/item_3/id = 3
+
+[node name="SubmitExtensionButton" type="Button" parent="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Create"
+
+[node name="HSeparator2" type="HSeparator" parent="."]
+layout_mode = 2
+
+[node name="HBoxContainer7" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="TimerTitle" type="Label" parent="HBoxContainer7"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Timer processing"
+
+[node name="HintTooltip" parent="HBoxContainer7" instance=ExtResource("2_kqhx5")]
+layout_mode = 2
+tooltip_text = "Change whether dialogics timers process in physics_process (frame-rate independent) or process."
+texture = SubResource("ImageTexture_4wgbv")
+hint_text = "Change whether dialogics timers process in physics_process (frame-rate independent) or process."
+
+[node name="HBoxContainer4" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer4"]
+layout_mode = 2
+text = "Process timers in physics_process"
+
+[node name="PhysicsTimerButton" type="CheckBox" parent="HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator5" type="HSeparator" parent="."]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="SectionSections" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Section Order"
+
+[node name="HintTooltip" parent="HBoxContainer" instance=ExtResource("2_kqhx5")]
+layout_mode = 2
+tooltip_text = "You can change the order of the event sections here. "
+texture = SubResource("ImageTexture_4wgbv")
+hint_text = "You can change the order of the event sections here. "
+
+[node name="SectionList" type="Tree" parent="."]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(150, 150)
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/button_margin = 0
+allow_reselect = true
+allow_rmb_select = true
+hide_folding = true
+hide_root = true
+drop_mode_flags = 1
+
+[connection signal="item_selected" from="HBoxContainer3/LayoutNodeEndBehaviour" to="." method="_on_layout_node_end_behaviour_item_selected"]
+[connection signal="pressed" from="HBoxContainer6/HBoxContainer4/HBoxContainer5/Reload" to="." method="_on_reload_pressed"]
+[connection signal="pressed" from="HBoxContainer6/ExtensionsPanel/VBox/CreateExtensionButton" to="." method="_on_create_extension_button_pressed"]
+[connection signal="pressed" from="HBoxContainer6/ExtensionsPanel/VBox/ExtensionCreator/SubmitExtensionButton" to="." method="_on_submit_extension_button_pressed"]
+[connection signal="button_clicked" from="SectionList" to="." method="_on_section_list_button_clicked"]
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+
+func _get_title() -> String:
+ return "Modules"
+
+func _get_priority() -> int:
+ return 0
+
+func _is_feature_tab() -> bool:
+ return true
+
+
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+ %Refresh.icon = get_theme_icon("Loop", "EditorIcons")
+ %Search.right_icon = get_theme_icon("Search", "EditorIcons")
+
+ %Filter_Events.icon = get_theme_icon("Favorites", "EditorIcons")
+ %Filter_Subsystems.icon = get_theme_icon("Callable", "EditorIcons")
+ %Filter_Styles.icon = get_theme_icon("PopupMenu", "EditorIcons")
+ %Filter_EffectsAndModifiers.icon = get_theme_icon("RichTextEffect", "EditorIcons")
+ %Filter_Editors.icon = get_theme_icon("ConfirmationDialog", "EditorIcons")
+ %Filter_Settings.icon = get_theme_icon("PluginScript", "EditorIcons")
+ %Collapse.icon = get_theme_icon("CollapseTree", "EditorIcons")
+
+ %EventDefaultsPanel.add_theme_stylebox_override('panel', get_theme_stylebox("Background", "EditorStyles"))
+
+ %ExternalLink.icon = get_theme_icon("Help", "EditorIcons")
+
+
+func _refresh() -> void:
+ %EventDefaultsPanel.hide()
+ load_modules_tree()
+
+
+func _on_refresh_pressed() -> void:
+ DialogicUtil.get_indexers(true, true)
+ DialogicResourceUtil.update_event_cache()
+ load_modules_tree()
+
+
+func filters_updated(fake_arg:Variant) -> void:
+ load_modules_tree()
+
+
+func _on_collapse_toggled(button_pressed:bool) -> void:
+ for item in %Tree.get_root().get_children():
+ item.collapsed = button_pressed
+
+ if button_pressed:
+ %Collapse.icon = get_theme_icon("ExpandTree", "EditorIcons")
+ %Collapse.tooltip_text = "Expand All"
+ else:
+ %Collapse.icon = get_theme_icon("CollapseTree", "EditorIcons")
+ %Collapse.tooltip_text = "Collapse All"
+
+
+func _on_search_text_changed(new_text:String) -> void:
+ for filter in [%Filter_Events, %Filter_Subsystems, %Filter_Editors, %Filter_EffectsAndModifiers, %Filter_Settings, %Filter_Styles]:
+ filter.text = ""
+ filter.set_meta("counter", 0)
+
+ var hidden_events: Array = DialogicUtil.get_editor_setting('hidden_event_buttons', [])
+
+ for child in %Tree.get_root().get_children():
+ if new_text.to_lower() in child.get_text(0).to_lower() or new_text.is_empty():
+ for sub_child in child.get_children():
+ sub_child.visible = sub_child.get_meta('filter_button').button_pressed
+ sub_child.get_meta('filter_button').set_meta('counter', sub_child.get_meta('filter_button').get_meta('counter')+1)
+ sub_child.get_meta('filter_button').text = str(sub_child.get_meta('filter_button').get_meta('counter'))
+ child.visible = true
+ else:
+ for sub_child in child.get_children():
+ sub_child.visible = sub_child.get_meta('filter_button').button_pressed and new_text.to_lower() in sub_child.get_text(0).to_lower()
+
+ if new_text.to_lower() in sub_child.get_text(0).to_lower():
+ sub_child.get_meta('filter_button').set_meta('counter', sub_child.get_meta('filter_button').get_meta('counter')+1)
+ sub_child.get_meta('filter_button').text = str(sub_child.get_meta('filter_button').get_meta('counter'))
+
+ for i in range(child.get_button_count(0)):
+ child.erase_button(0, child.get_button_count(0)-1)
+ var any_visible := false
+ var counter := 0
+ for sub_child in child.get_children():
+ if sub_child.visible:
+ child.add_button(0, sub_child.get_icon(0), counter, false, sub_child.get_text(0))
+ if sub_child.get_metadata(0) and sub_child.get_metadata(0)['type'] == 'Event' and sub_child.get_metadata(0)['hidden']:
+ var color: Color = sub_child.get_icon_modulate(0)
+ color.a = 0.5
+ child.set_button_color(0, counter, color)
+ else:
+ child.set_button_color(0, counter, sub_child.get_icon_modulate(0))
+ counter += 1
+ any_visible = true
+ child.visible = any_visible
+
+
+
+func load_modules_tree() -> void:
+ %Tree.clear()
+ var root: TreeItem = %Tree.create_item()
+ var cached_events := DialogicResourceUtil.get_event_cache()
+ var hidden_events: Array = DialogicUtil.get_editor_setting('hidden_event_buttons', [])
+ var indexers := DialogicUtil.get_indexers()
+ for i in indexers:
+ var module_item: TreeItem = %Tree.create_item(root)
+ module_item.set_text(0, i.get_script().resource_path.trim_suffix('/index.gd').get_file())
+ module_item.set_metadata(0, {'type':'Module'})
+
+ # Events
+ for ev in i._get_events():
+ if not ResourceLoader.exists(ev):
+ continue
+ var event_item: TreeItem = %Tree.create_item(module_item)
+ event_item.set_icon(0, get_theme_icon("Favorites", "EditorIcons"))
+ for cached_event in cached_events:
+ if cached_event.get_script().resource_path == ev:
+ event_item.set_text(0, cached_event.event_name + " Event")
+ event_item.set_icon_modulate(0, cached_event.event_color)
+ var hidden: bool = cached_event.event_name in hidden_events
+ event_item.set_metadata(0, {'type':'Event', 'event':cached_event, 'hidden':hidden})
+ event_item.add_button(0, get_theme_icon("GuiVisibilityVisible", "EditorIcons"), 0, false, "Toggle Event Button Visibility")
+ if hidden:
+ event_item.set_button(0, 0, get_theme_icon("GuiVisibilityHidden", "EditorIcons"))
+ event_item.set_meta('filter_button', %Filter_Events)
+ event_item.visible = %Filter_Events.button_pressed
+
+ # Subsystems
+ for subsys in i._get_subsystems():
+ var subsys_item: TreeItem = %Tree.create_item(module_item)
+ subsys_item.set_icon(0, get_theme_icon("Callable", "EditorIcons"))
+ subsys_item.set_text(0, subsys.name + " Subsystem")
+ subsys_item.set_icon_modulate(0, get_theme_color("readonly_color", "Editor"))
+ subsys_item.set_metadata(0, {'type':'Subsystem', 'info':subsys})
+ subsys_item.set_meta('filter_button', %Filter_Subsystems)
+ subsys_item.visible = %Filter_Subsystems.button_pressed
+
+ # Style scenes
+ for style in i._get_layout_parts():
+ var style_item: TreeItem = %Tree.create_item(module_item)
+ style_item.set_icon(0, get_theme_icon("PopupMenu", "EditorIcons"))
+ style_item.set_text(0, style.name)
+ style_item.set_icon_modulate(0, get_theme_color("property_color_x", "Editor"))
+ style_item.set_metadata(0, {'type':'Style', 'info':style})
+ style_item.set_meta('filter_button', %Filter_Styles)
+ style_item.visible = %Filter_Styles.button_pressed
+
+ # Text Effects
+ for effect in i._get_text_effects():
+ var effect_item: TreeItem = %Tree.create_item(module_item)
+ effect_item.set_icon(0, get_theme_icon("RichTextEffect", "EditorIcons"))
+ effect_item.set_text(0, "Text effect ["+effect.command+"]")
+ effect_item.set_icon_modulate(0, get_theme_color("property_color_z", "Editor"))
+ effect_item.set_metadata(0, {'type':'Effect', 'info':effect})
+ effect_item.set_meta('filter_button', %Filter_EffectsAndModifiers)
+ effect_item.visible = %Filter_EffectsAndModifiers.button_pressed
+
+ # Text Modifiers
+ for mod in i._get_text_modifiers():
+ var mod_item: TreeItem = %Tree.create_item(module_item)
+ mod_item.set_icon(0, get_theme_icon("RichTextEffect", "EditorIcons"))
+ mod_item.set_text(0, mod.method.capitalize())
+ mod_item.set_icon_modulate(0, get_theme_color("property_color_z", "Editor"))
+ mod_item.set_metadata(0, {'type':'Modifier', 'info':mod})
+ mod_item.set_meta('filter_button', %Filter_EffectsAndModifiers)
+ mod_item.visible = %Filter_EffectsAndModifiers.button_pressed
+
+ # Settings
+ for settings in i._get_settings_pages():
+ var settings_item: TreeItem = %Tree.create_item(module_item)
+ settings_item.set_icon(0, get_theme_icon("PluginScript", "EditorIcons"))
+ settings_item.set_text(0, module_item.get_text(0) + " Settings")
+ settings_item.set_icon_modulate(0, get_theme_color("readonly_color", "Editor"))
+ settings_item.set_metadata(0, {'type':'Settings', 'info':settings})
+ settings_item.set_meta('filter_button', %Filter_Settings)
+ settings_item.visible = %Filter_Settings.button_pressed
+
+ # Editors
+ for editor in i._get_editors():
+ var editor_item: TreeItem = %Tree.create_item(module_item)
+ editor_item.set_icon(0, get_theme_icon("ConfirmationDialog", "EditorIcons"))
+ editor_item.set_text(0, editor.get_file().trim_suffix('.tscn').capitalize())
+ editor_item.set_icon_modulate(0, get_theme_color("readonly_color", "Editor"))
+ editor_item.set_metadata(0, {'type':'Editor', 'info':editor})
+ editor_item.set_meta('filter_button', %Filter_Editors)
+ editor_item.visible = %Filter_Editors.button_pressed
+
+ module_item.collapsed = %Collapse.button_pressed
+
+ _on_search_text_changed(%Search.text)
+ if %Tree.get_root().get_child_count(): %Tree.set_selected(%Tree.get_root().get_child(0), 0)
+
+
+func _on_tree_button_clicked(item:TreeItem, column:int, id:int, mouse_button_index:int) -> void:
+ match item.get_metadata(0)['type']:
+ 'Module':
+ item.collapsed = false
+ %Tree.set_selected(item.get_child(id), 0)
+ 'Event':
+ # Visibility item clicked
+ if id == 0:
+ var meta: Dictionary= item.get_metadata(0)
+ if meta['hidden']:
+ item.set_button(0, 0, get_theme_icon("GuiVisibilityVisible", "EditorIcons"))
+ item.get_parent().set_button_color(0, item.get_index(), item.get_icon_modulate(0))
+ if item == %Tree.get_selected():
+ %VisibilityToggle.button_pressed = true
+ else:
+ item.set_button(0, 0, get_theme_icon("GuiVisibilityHidden", "EditorIcons"))
+ var color: Color = item.get_icon_modulate(0)
+ color.a = 0.5
+ item.get_parent().set_button_color(0, item.get_index(), color)
+ if item == %Tree.get_selected():
+ %VisibilityToggle.button_pressed = false
+ meta['hidden'] = !meta['hidden']
+ item.set_metadata(0, meta)
+ change_event_visibility(meta['event'], !meta['hidden'])
+
+
+func _on_tree_item_selected() -> void:
+ var selected_item: TreeItem = %Tree.get_selected()
+
+ var metadata: Variant = selected_item.get_metadata(0)
+
+ %Title.text = selected_item.get_text(0)
+ %EventDefaultsPanel.hide()
+ %Icon.texture = null
+ %ExternalLink.hide()
+ %VisibilityToggle.hide()
+
+ if metadata is Dictionary:
+ match metadata.type:
+ 'Event':
+ %GeneralInfo.text = "Events can be used in timelines and do all kinds of things. They often interact with subsystems and dialogic nodes."
+
+ load_event_settings(metadata.event)
+ if %EventDefaults.get_child_count():
+ %EventDefaultsPanel.show()
+
+ if metadata.event.help_page_path:
+ %ExternalLink.show()
+ %ExternalLink.set_meta('url', metadata.event.help_page_path)
+ %Icon.texture = metadata.event._get_icon()
+ if !metadata.event.disable_editor_button:
+ %VisibilityToggle.show()
+ %VisibilityToggle.button_pressed = !metadata.event.event_name in DialogicUtil.get_editor_setting('hidden_event_buttons', [])
+ if %VisibilityToggle.button_pressed:
+ %VisibilityToggle.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
+ else:
+ %VisibilityToggle.icon = get_theme_icon("GuiVisibilityHidden", "EditorIcons")
+ # -------------------------------------------------
+ 'Subsystem':
+ %GeneralInfo.text = "Subsystems hold specialized functionality. They mostly manage communication between events and dialogic nodes. Often they provide handy methods that can be accessed by the user like this: Dialogic.Subsystem.a_method()."
+ # -------------------------------------------------
+ 'Effect':
+ %GeneralInfo.text = "Text effects can be used in text events. They will be executed once reached and can take a single argument."
+ # -------------------------------------------------
+ 'Modifier':
+ %GeneralInfo.text = "Modifiers can modify text from text events before it is shown."
+ # -------------------------------------------------
+ 'Style':
+ %GeneralInfo.text = "Style presets can be activated and modified in the Styles editor. They provide the design of the dialog interface in your game."
+ # -------------------------------------------------
+ 'Editor':
+ %GeneralInfo.text = "Editors provide a user interface for editing dialogic data."
+ # -------------------------------------------------
+ 'Settings':
+ %GeneralInfo.text = "Settings pages provide settings that are usually used by subsystems, events and dialogic nodes."
+ # -------------------------------------------------
+ '_':
+ %GeneralInfo.text = ""
+
+
+func _on_external_link_pressed() -> void:
+ if %ExternalLink.has_meta('url'):
+ OS.shell_open(%ExternalLink.get_meta('url'))
+
+
+func change_event_visibility(event:DialogicEvent, visibility:bool) -> void:
+ if event:
+ var list: Array= DialogicUtil.get_editor_setting('hidden_event_buttons', [])
+ if visibility:
+ list.erase(event.event_name)
+ else:
+ list.append(event.event_name)
+ DialogicUtil.set_editor_setting('hidden_event_buttons', list)
+ force_event_button_list_update()
+
+
+func _on_visibility_toggle_toggled(button_pressed:bool) -> void:
+ change_event_visibility(%Tree.get_selected().get_metadata(0).event, button_pressed)
+
+ if button_pressed:
+ %VisibilityToggle.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
+ %Tree.get_selected().set_button(0, 0, get_theme_icon("GuiVisibilityVisible", "EditorIcons"))
+ %Tree.get_selected().get_parent().set_button_color(0, %Tree.get_selected().get_index(), %Tree.get_selected().get_icon_modulate(0))
+ else:
+ %VisibilityToggle.icon = get_theme_icon("GuiVisibilityHidden", "EditorIcons")
+ %Tree.get_selected().set_button(0, 0, get_theme_icon("GuiVisibilityHidden", "EditorIcons"))
+ var color: Color = %Tree.get_selected().get_icon_modulate(0)
+ color.a = 0.5
+ %Tree.get_selected().get_parent().set_button_color(0, %Tree.get_selected().get_index(), color)
+
+
+
+func force_event_button_list_update() -> void:
+ find_parent('EditorsManager').editors['Timeline'].node.get_node('%VisualEditor').load_event_buttons()
+
+################################################################################
+## EVENT DEFAULT SETTINGS
+################################################################################
+func load_event_settings(event:DialogicEvent) -> void:
+ for child in %EventDefaults.get_children():
+ child.queue_free()
+
+ var event_default_overrides: Dictionary = ProjectSettings.get_setting('dialogic/event_default_overrides', {})
+
+ var params := event.get_shortcode_parameters()
+ for prop in params:
+ var current_value: Variant = params[prop].default
+ if event_default_overrides.get(event.event_name, {}).has(params[prop].property):
+ current_value = event_default_overrides.get(event.event_name, {}).get(params[prop].property)
+
+ # Label
+ var label := Label.new()
+ label.text = prop.capitalize()
+ %EventDefaults.add_child(label)
+
+ var reset := Button.new()
+ reset.icon = get_theme_icon("Clear", "EditorIcons")
+ reset.flat = true
+
+ %EventDefaults.add_child(reset)
+
+ # Editing field
+ var editor_node: Node = null
+ match typeof(event.get(params[prop].property)):
+ TYPE_STRING:
+ editor_node = LineEdit.new()
+ editor_node.custom_minimum_size.x = 150
+ editor_node.text = str(current_value)
+ editor_node.text_changed.connect(_on_event_default_string_submitted.bind(params[prop].property))
+ TYPE_INT, TYPE_FLOAT:
+ if params[prop].has('suggestions'):
+ editor_node = OptionButton.new()
+ for i in params[prop].suggestions.call():
+ editor_node.add_item(i, int(params[prop].suggestions.call()[i].value))
+ editor_node.select(int(current_value))
+ editor_node.item_selected.connect(_on_event_default_option_selected.bind(editor_node, params[prop].property))
+ else:
+ editor_node = SpinBox.new()
+
+ editor_node.allow_greater = true
+ editor_node.allow_lesser = true
+ if typeof(event.get(params[prop].property)) == TYPE_INT:
+ editor_node.step = 1
+ else:
+ editor_node.step = 0.001
+
+ editor_node.value = float(current_value)
+ editor_node.value_changed.connect(_on_event_default_number_changed.bind(params[prop].property))
+
+ TYPE_VECTOR2:
+ editor_node = load("res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn").instantiate()
+ editor_node.set_value(current_value)
+ editor_node.property_name = params[prop].property
+ editor_node.value_changed.connect(_on_event_default_value_changed)
+
+ TYPE_BOOL:
+ editor_node = CheckBox.new()
+ editor_node.button_pressed = bool(current_value)
+ editor_node.toggled.connect(_on_event_default_bool_toggled.bind(params[prop].property))
+
+ TYPE_ARRAY:
+ editor_node = load("res://addons/dialogic/Editor/Events/Fields/field_array.tscn").instantiate()
+ editor_node.set_value(current_value)
+ editor_node.property_name = params[prop].property
+ editor_node.value_changed.connect(_on_event_default_value_changed)
+
+ TYPE_DICTIONARY:
+ editor_node = load("res://addons/dialogic/Editor/Events/Fields/field_dictionary.tscn").instantiate()
+ editor_node.set_value(current_value)
+ editor_node.property_name = params[prop].property
+ editor_node.value_changed.connect(_on_event_default_value_changed)
+ %EventDefaults.add_child(editor_node)
+ reset.pressed.connect(reset_event_default_override.bind(prop, editor_node, params[prop].default))
+
+
+func set_event_default_override(prop:String, value:Variant) -> void:
+ var event_default_overrides: Dictionary = ProjectSettings.get_setting('dialogic/event_default_overrides', {})
+ var event: DialogicEvent = %Tree.get_selected().get_metadata(0).event
+
+ if not event_default_overrides.has(event.event_name):
+ event_default_overrides[event.event_name] = {}
+
+ event_default_overrides[event.event_name][prop] = value
+
+ ProjectSettings.set_setting('dialogic/event_default_overrides', event_default_overrides)
+
+
+func reset_event_default_override(prop:String, node:Node, default:Variant) -> void:
+ var event_default_overrides: Dictionary = ProjectSettings.get_setting('dialogic/event_default_overrides', {})
+ var event: DialogicEvent = %Tree.get_selected().get_metadata(0).event
+
+ if not event_default_overrides.has(event.event_name):
+ return
+
+ event_default_overrides[event.event_name].erase(prop)
+
+ ProjectSettings.set_setting('dialogic/event_default_overrides', event_default_overrides)
+
+ if node is CheckBox:
+ node.button_pressed = default
+ elif node is LineEdit:
+ node.text = default
+ elif node.has_method('set_value'):
+ node.set_value(default)
+ elif node is ColorPickerButton:
+ node.color = default
+ elif node is OptionButton:
+ node.select(default)
+ elif node is SpinBox:
+ node.value = default
+
+
+func _on_event_default_string_submitted(text:String, prop:String) -> void:
+ set_event_default_override(prop, text)
+
+func _on_event_default_option_selected(index:int, option_button:OptionButton, prop:String) -> void:
+ set_event_default_override(prop, option_button.get_item_id(index))
+
+func _on_event_default_number_changed(value:float, prop:String) -> void:
+ set_event_default_override(prop, value)
+
+func _on_event_default_value_changed(prop:String, value:Variant) -> void:
+ set_event_default_override(prop, value)
+
+func _on_event_default_bool_toggled(value:bool, prop:String) -> void:
+ set_event_default_override(prop, value)
+
--- /dev/null
+[gd_scene load_steps=7 format=3 uid="uid://o7ljiritpgap"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Settings/settings_modules.gd" id="1_l2hk0"]
+
+[sub_resource type="Image" id="Image_570p8"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_lce2m"]
+image = SubResource("Image_570p8")
+
+[sub_resource type="Image" id="Image_ihhvm"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_137g7"]
+image = SubResource("Image_ihhvm")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_315cl"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 0.365, 0.365, 1)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_detail = 1
+
+[node name="ModuleManagement" type="HSplitContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_bottom = -157.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+script = ExtResource("1_l2hk0")
+short_info = "Here you can manage modules:
+- change event defaults
+- hide events from the event list"
+
+[node name="Overview" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Overview"]
+layout_mode = 2
+size_flags_horizontal = 3
+follow_focus = true
+horizontal_scroll_mode = 3
+vertical_scroll_mode = 0
+
+[node name="HBox" type="HBoxContainer" parent="Overview/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 8
+alignment = 2
+
+[node name="Filter_Events" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Include Events"
+toggle_mode = true
+button_pressed = true
+text = "0"
+flat = true
+icon_alignment = 2
+
+[node name="Filter_Subsystems" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Include Subsystems"
+toggle_mode = true
+button_pressed = true
+text = "0"
+flat = true
+icon_alignment = 2
+
+[node name="Filter_EffectsAndModifiers" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Include Text Effects and Modifiers"
+toggle_mode = true
+button_pressed = true
+text = "0"
+flat = true
+icon_alignment = 2
+
+[node name="Filter_Styles" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Include Preset Style Scenes"
+toggle_mode = true
+button_pressed = true
+text = "0"
+flat = true
+icon_alignment = 2
+
+[node name="Filter_Settings" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Include Settings Pages"
+toggle_mode = true
+text = "0"
+flat = true
+icon_alignment = 2
+
+[node name="Filter_Editors" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Include Editors"
+toggle_mode = true
+text = "0"
+flat = true
+icon_alignment = 2
+
+[node name="Search" type="LineEdit" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+placeholder_text = "Search"
+clear_button_enabled = true
+
+[node name="Refresh" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Refresh"
+
+[node name="Collapse" type="Button" parent="Overview/ScrollContainer/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Collapse All"
+toggle_mode = true
+
+[node name="Tree" type="Tree" parent="Overview"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+allow_reselect = true
+hide_root = true
+
+[node name="Scroll" type="ScrollContainer" parent="."]
+show_behind_parent = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+size_flags_stretch_ratio = 0.75
+horizontal_scroll_mode = 3
+vertical_scroll_mode = 0
+
+[node name="Settings" type="VBoxContainer" parent="Scroll"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="HBox" type="HBoxContainer" parent="Scroll/Settings"]
+layout_mode = 2
+
+[node name="Icon" type="TextureRect" parent="Scroll/Settings/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+expand_mode = 3
+
+[node name="Title" type="Label" parent="Scroll/Settings/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+
+[node name="ExternalLink" type="Button" parent="Scroll/Settings/HBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+icon = SubResource("ImageTexture_lce2m")
+flat = true
+
+[node name="VisibilityToggle" type="Button" parent="Scroll/Settings/HBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+toggle_mode = true
+button_pressed = true
+icon = SubResource("ImageTexture_137g7")
+flat = true
+
+[node name="EventDefaultsPanel" type="PanelContainer" parent="Scroll/Settings"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_315cl")
+
+[node name="VBox" type="VBoxContainer" parent="Scroll/Settings/EventDefaultsPanel"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="Scroll/Settings/EventDefaultsPanel/VBox"]
+layout_mode = 2
+text = "Edit event defaults:"
+
+[node name="EventDefaults" type="GridContainer" parent="Scroll/Settings/EventDefaultsPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+columns = 3
+
+[node name="GeneralInfo" type="Label" parent="Scroll/Settings"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicHintText2"
+autowrap_mode = 3
+
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Filter_Events" to="." method="filters_updated"]
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Filter_Subsystems" to="." method="filters_updated"]
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Filter_EffectsAndModifiers" to="." method="filters_updated"]
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Filter_Styles" to="." method="filters_updated"]
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Filter_Settings" to="." method="filters_updated"]
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Filter_Editors" to="." method="filters_updated"]
+[connection signal="text_changed" from="Overview/ScrollContainer/HBox/Search" to="." method="_on_search_text_changed"]
+[connection signal="pressed" from="Overview/ScrollContainer/HBox/Refresh" to="." method="_on_refresh_pressed"]
+[connection signal="toggled" from="Overview/ScrollContainer/HBox/Collapse" to="." method="_on_collapse_toggled"]
+[connection signal="button_clicked" from="Overview/Tree" to="." method="_on_tree_button_clicked"]
+[connection signal="item_selected" from="Overview/Tree" to="." method="_on_tree_item_selected"]
+[connection signal="pressed" from="Scroll/Settings/HBox/ExternalLink" to="." method="_on_external_link_pressed"]
+[connection signal="toggled" from="Scroll/Settings/HBox/VisibilityToggle" to="." method="_on_visibility_toggle_toggled"]
--- /dev/null
+@tool
+extends Control
+class_name DialogicSettingsPage
+
+@export_multiline var short_info := ""
+
+## Called to get the title of the page
+func _get_title() -> String:
+ return name
+
+
+## Called to get the ordering of the page
+func _get_priority() -> int:
+ return 0
+
+
+## Called to know whether to put this in the features section
+func _is_feature_tab() -> bool:
+ return false
+
+
+## Called when the settings editor is opened
+func _refresh() -> void:
+ pass
+
+
+## Called before the settings editor closes (another editor is opened)
+## Can be used to safe stuff
+func _about_to_close() -> void:
+ pass
+
+
+## Return a section with information.
+func _get_info_section() -> Control:
+ return null
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+## Settings tab that allows enabeling and updating translation csv-files.
+
+
+enum TranslationModes {PER_PROJECT, PER_TIMELINE, NONE}
+enum SaveLocationModes {INSIDE_TRANSLATION_FOLDER, NEXT_TO_TIMELINE, NONE}
+
+var loading := false
+@onready var settings_editor: Control = find_parent('Settings')
+
+## The default CSV filename that contains the translations for character
+## properties.
+const DEFAULT_CHARACTER_CSV_NAME := "dialogic_character_translations.csv"
+## The default CSV filename that contains the translations for timelines.
+## Only used when all timelines are supposed to be translated in one file.
+const DEFAULT_TIMELINE_CSV_NAME := "dialogic_timeline_translations.csv"
+
+const DEFAULT_GLOSSARY_CSV_NAME := "dialogic_glossary_translations.csv"
+
+const _USED_LOCALES_SETTING := "dialogic/translation/locales"
+
+## Contains translation changes that were made during the last update.
+
+## Unique locales that will be set after updating the CSV files.
+var _unique_locales := []
+
+func _get_icon() -> Texture2D:
+ return get_theme_icon("Translation", "EditorIcons")
+
+
+func _is_feature_tab() -> bool:
+ return true
+
+
+func _ready() -> void:
+ %TransEnabled.toggled.connect(store_changes)
+ %OrigLocale.get_suggestions_func = get_locales
+ %OrigLocale.resource_icon = get_theme_icon("Translation", "EditorIcons")
+ %OrigLocale.value_changed.connect(store_changes)
+ %TestingLocale.get_suggestions_func = get_locales
+ %TestingLocale.resource_icon = get_theme_icon("Translation", "EditorIcons")
+ %TestingLocale.value_changed.connect(store_changes)
+ %TransFolderPicker.value_changed.connect(store_changes)
+ %AddSeparatorEnabled.toggled.connect(store_changes)
+
+ %SaveLocationMode.item_selected.connect(store_changes)
+ %TransMode.item_selected.connect(store_changes)
+
+ %UpdateCsvFiles.pressed.connect(_on_update_translations_pressed)
+ %UpdateCsvFiles.icon = get_theme_icon("Add", "EditorIcons")
+
+ %CollectTranslations.pressed.connect(collect_translations)
+ %CollectTranslations.icon = get_theme_icon("File", "EditorIcons")
+
+ %TransRemove.pressed.connect(_on_erase_translations_pressed)
+ %TransRemove.icon = get_theme_icon("Remove", "EditorIcons")
+
+ %UpdateConfirmationDialog.add_button("Keep old & Generate new", false, "generate_new")
+
+ %UpdateConfirmationDialog.custom_action.connect(_on_custom_action)
+
+ _verify_translation_file()
+
+
+func _on_custom_action(action: String) -> void:
+ if action == "generate_new":
+ update_csv_files()
+
+
+func _refresh() -> void:
+ loading = true
+
+ %TransEnabled.button_pressed = ProjectSettings.get_setting('dialogic/translation/enabled', false)
+ %TranslationSettings.visible = %TransEnabled.button_pressed
+ %OrigLocale.set_value(ProjectSettings.get_setting('dialogic/translation/original_locale', TranslationServer.get_tool_locale()))
+ %TransMode.select(ProjectSettings.get_setting('dialogic/translation/file_mode', 1))
+ %TransFolderPicker.set_value(ProjectSettings.get_setting('dialogic/translation/translation_folder', ''))
+ %TestingLocale.set_value(ProjectSettings.get_setting('internationalization/locale/test', ''))
+ %AddSeparatorEnabled.button_pressed = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
+
+ _verify_translation_file()
+
+ loading = false
+
+
+func store_changes(_fake_arg: Variant = null, _fake_arg2: Variant = null) -> void:
+ if loading:
+ return
+
+ _verify_translation_file()
+
+ ProjectSettings.set_setting('dialogic/translation/enabled', %TransEnabled.button_pressed)
+ %TranslationSettings.visible = %TransEnabled.button_pressed
+ ProjectSettings.set_setting('dialogic/translation/original_locale', %OrigLocale.current_value)
+ ProjectSettings.set_setting('dialogic/translation/file_mode', %TransMode.selected)
+ ProjectSettings.set_setting('dialogic/translation/translation_folder', %TransFolderPicker.current_value)
+ ProjectSettings.set_setting('internationalization/locale/test', %TestingLocale.current_value)
+ ProjectSettings.set_setting('dialogic/translation/save_mode', %SaveLocationMode.selected)
+ ProjectSettings.set_setting('dialogic/translation/add_separator', %AddSeparatorEnabled.button_pressed)
+ ProjectSettings.save()
+
+
+## Checks whether the translation folder path is required.
+## If it is, disables the "Update CSV files" button and shows a warning.
+##
+## The translation folder path is required when either of the following is true:
+## - The translation mode is set to "Per Project".
+## - The save location mode is set to "Inside Translation Folder".
+func _verify_translation_file() -> void:
+ var translation_folder: String = %TransFolderPicker.current_value
+ var file_mode: TranslationModes = %TransMode.selected
+
+ if file_mode == TranslationModes.PER_PROJECT:
+ %SaveLocationMode.disabled = true
+ else:
+ %SaveLocationMode.disabled = false
+
+ var valid_translation_folder := (!translation_folder.is_empty()
+ and DirAccess.dir_exists_absolute(translation_folder))
+
+ %UpdateCsvFiles.disabled = not valid_translation_folder
+
+ var status_message := ""
+
+ if not valid_translation_folder:
+ status_message += "⛔ Requires valid translation folder to translate character names"
+
+ if file_mode == TranslationModes.PER_PROJECT:
+ status_message += " and the project CSV file."
+ else:
+ status_message += "."
+
+ %StatusMessage.text = status_message
+
+
+func get_locales(_filter: String) -> Dictionary:
+ var suggestions := {}
+ suggestions['Default'] = {'value':'', 'tooltip':"Will use the fallback locale set in the project settings."}
+ suggestions[TranslationServer.get_tool_locale()] = {'value':TranslationServer.get_tool_locale()}
+
+ var used_locales: Array = ProjectSettings.get_setting(_USED_LOCALES_SETTING, TranslationServer.get_all_languages())
+
+ for locale: String in used_locales:
+ var language_name := TranslationServer.get_language_name(locale)
+
+ # Invalid locales return an empty String.
+ if language_name.is_empty():
+ continue
+
+ suggestions[locale] = { 'value': locale, 'tooltip': language_name }
+
+ return suggestions
+
+
+func _on_update_translations_pressed() -> void:
+ var save_mode: SaveLocationModes = %SaveLocationMode.selected
+ var file_mode: TranslationModes = %TransMode.selected
+ var translation_folder: String = %TransFolderPicker.current_value
+
+ var old_save_mode: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/intern/save_mode', save_mode)
+ var old_file_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/intern/file_mode', file_mode)
+ var old_translation_folder: String = ProjectSettings.get_setting('dialogic/translation/intern/translation_folder', translation_folder)
+
+ if (old_save_mode == save_mode
+ and old_file_mode == file_mode
+ and old_translation_folder == translation_folder):
+ update_csv_files()
+ return
+
+ %UpdateConfirmationDialog.popup_centered()
+
+
+## Used by the dialog to inform that the settings were changed.
+func _delete_and_update() -> void:
+ erase_translations()
+ update_csv_files()
+
+
+## Creates or updates the glossary CSV files.
+func _handle_glossary_translation(
+ csv_data: CsvUpdateData,
+ save_location_mode: SaveLocationModes,
+ translation_mode: TranslationModes,
+ translation_folder_path: String,
+ orig_locale: String) -> void:
+
+ var glossary_csv: DialogicCsvFile = null
+ var glossary_paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
+ var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
+
+ for glossary_path: String in glossary_paths:
+
+ if glossary_csv == null:
+ var csv_name := ""
+
+ # Get glossary CSV file name.
+ match translation_mode:
+ TranslationModes.PER_PROJECT:
+ csv_name = DEFAULT_GLOSSARY_CSV_NAME
+
+ TranslationModes.PER_TIMELINE:
+ var glossary_name: String = glossary_path.trim_suffix('.tres')
+ var path_parts := glossary_name.split("/")
+ var file_name := path_parts[-1]
+ csv_name = "dialogic_" + file_name + '_translation.csv'
+
+ var glossary_csv_path := ""
+ # Get glossary CSV file path.
+ match save_location_mode:
+ SaveLocationModes.INSIDE_TRANSLATION_FOLDER:
+ glossary_csv_path = translation_folder_path.path_join(csv_name)
+
+ SaveLocationModes.NEXT_TO_TIMELINE:
+ glossary_csv_path = glossary_path.get_base_dir().path_join(csv_name)
+
+ # Create or update glossary CSV file.
+ glossary_csv = DialogicCsvFile.new(glossary_csv_path, orig_locale, add_separator_lines)
+
+ if (glossary_csv.is_new_file):
+ csv_data.new_glossaries += 1
+ else:
+ csv_data.updated_glossaries += 1
+
+ var glossary: DialogicGlossary = load(glossary_path)
+ glossary_csv.collect_lines_from_glossary(glossary)
+ glossary_csv.add_translation_keys_to_glossary(glossary)
+ ResourceSaver.save(glossary)
+
+ #If per-file mode is used, save this csv and begin a new one
+ if translation_mode == TranslationModes.PER_TIMELINE:
+ glossary_csv.update_csv_file_on_disk()
+ glossary_csv = null
+
+ # If a Per-Project glossary is still open, we need to save it.
+ if glossary_csv != null:
+ glossary_csv.update_csv_file_on_disk()
+ glossary_csv = null
+
+
+## Keeps information about the amount of new and updated CSV rows and what
+## resources were populated with translation IDs.
+## The final data can be used to display a status message.
+class CsvUpdateData:
+ var new_events := 0
+ var updated_events := 0
+
+ var new_timelines := 0
+ var updated_timelines := 0
+
+ var new_names := 0
+ var updated_names := 0
+
+ var new_glossaries := 0
+ var updated_glossaries := 0
+
+ var new_glossary_entries := 0
+ var updated_glossary_entries := 0
+
+
+func update_csv_files() -> void:
+ _unique_locales = []
+ var orig_locale: String = ProjectSettings.get_setting('dialogic/translation/original_locale', '').strip_edges()
+ var save_location_mode: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/save_mode', SaveLocationModes.NEXT_TO_TIMELINE)
+ var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT)
+ var translation_folder_path: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://')
+ var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
+
+ var csv_data := CsvUpdateData.new()
+
+ if orig_locale.is_empty():
+ orig_locale = ProjectSettings.get_setting('internationalization/locale/fallback')
+
+ ProjectSettings.set_setting('dialogic/translation/intern/save_mode', save_location_mode)
+ ProjectSettings.set_setting('dialogic/translation/intern/file_mode', translation_mode)
+ ProjectSettings.set_setting('dialogic/translation/intern/translation_folder', translation_folder_path)
+
+ var current_timeline := _close_active_timeline()
+
+ var csv_per_project: DialogicCsvFile = null
+ var per_project_csv_path := translation_folder_path.path_join(DEFAULT_TIMELINE_CSV_NAME)
+
+ if translation_mode == TranslationModes.PER_PROJECT:
+ csv_per_project = DialogicCsvFile.new(per_project_csv_path, orig_locale, add_separator_lines)
+
+ if (csv_per_project.is_new_file):
+ csv_data.new_timelines += 1
+ else:
+ csv_data.updated_timelines += 1
+
+ # Iterate over all timelines.
+ # Create or update CSV files.
+ # Transform the timeline into translatable lines and collect into the CSV file.
+ for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.dtl'):
+ var csv_file: DialogicCsvFile = csv_per_project
+
+ # Swap the CSV file to the Per Timeline one.
+ if translation_mode == TranslationModes.PER_TIMELINE:
+ var per_timeline_path: String = timeline_path.trim_suffix('.dtl')
+ var path_parts := per_timeline_path.split("/")
+ var timeline_name: String = path_parts[-1]
+
+ # Adjust the file path to the translation location mode.
+ if save_location_mode == SaveLocationModes.INSIDE_TRANSLATION_FOLDER:
+ var prefixed_timeline_name := "dialogic_" + timeline_name
+ per_timeline_path = translation_folder_path.path_join(prefixed_timeline_name)
+
+
+ per_timeline_path += '_translation.csv'
+ csv_file = DialogicCsvFile.new(per_timeline_path, orig_locale, false)
+ csv_data.new_timelines += 1
+
+ # Load and process timeline, turn events into resources.
+ var timeline: DialogicTimeline = load(timeline_path)
+
+ if timeline.events.size() == 0:
+ print_rich("[color=yellow]Empty timeline, skipping: " + timeline_path + "[/color]")
+ continue
+
+ timeline.process()
+
+ # Collect timeline into CSV.
+ csv_file.collect_lines_from_timeline(timeline)
+
+ # in case new translation_id's were added, we save the timeline again
+ timeline.set_meta("timeline_not_saved", true)
+ ResourceSaver.save(timeline, timeline_path)
+
+ if translation_mode == TranslationModes.PER_TIMELINE:
+ csv_file.update_csv_file_on_disk()
+
+ csv_data.new_events += csv_file.new_rows
+ csv_data.updated_events += csv_file.updated_rows
+
+ _handle_glossary_translation(
+ csv_data,
+ save_location_mode,
+ translation_mode,
+ translation_folder_path,
+ orig_locale
+ )
+
+ _handle_character_names(
+ csv_data,
+ orig_locale,
+ translation_folder_path,
+ add_separator_lines
+ )
+
+ if translation_mode == TranslationModes.PER_PROJECT:
+ csv_per_project.update_csv_file_on_disk()
+
+ _silently_open_timeline(current_timeline)
+
+ # Trigger reimport.
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
+
+ var status_message := "Events created {new_events} found {updated_events}
+ Names created {new_names} found {updated_names}
+ CSVs created {new_timelines} found {updated_timelines}
+ Glossary created {new_glossaries} found {updated_glossaries}
+ Entries created {new_glossary_entries} found {updated_glossary_entries}"
+
+ var status_message_args := {
+ 'new_events': csv_data.new_events,
+ 'updated_events': csv_data.updated_events,
+ 'new_timelines': csv_data.new_timelines,
+ 'updated_timelines': csv_data.updated_timelines,
+ 'new_glossaries': csv_data.new_glossaries,
+ 'updated_glossaries': csv_data.updated_glossaries,
+ 'new_names': csv_data.new_names,
+ 'updated_names': csv_data.updated_names,
+ 'new_glossary_entries': csv_data.new_glossary_entries,
+ 'updated_glossary_entries': csv_data.updated_glossary_entries,
+ }
+
+ %StatusMessage.text = status_message.format(status_message_args)
+ ProjectSettings.set_setting(_USED_LOCALES_SETTING, _unique_locales)
+
+
+## Iterates over all character resource files and creates or updates CSV files
+## that contain the translations for character properties.
+## This will save each character resource file to disk.
+func _handle_character_names(
+ csv_data: CsvUpdateData,
+ original_locale: String,
+ translation_folder_path: String,
+ add_separator_lines: bool) -> void:
+ var names_csv_path := translation_folder_path.path_join(DEFAULT_CHARACTER_CSV_NAME)
+ var character_name_csv: DialogicCsvFile = DialogicCsvFile.new(names_csv_path,
+ original_locale,
+ add_separator_lines
+ )
+
+ var all_characters := {}
+
+ for character_path: String in DialogicResourceUtil.list_resources_of_type('.dch'):
+ var character: DialogicCharacter = load(character_path)
+
+ if character._translation_id.is_empty():
+ csv_data.new_names += 1
+
+ else:
+ csv_data.updated_names += 1
+
+ var translation_id := character.get_set_translation_id()
+ all_characters[translation_id] = character
+
+ ResourceSaver.save(character)
+
+ character_name_csv.collect_lines_from_characters(all_characters)
+ character_name_csv.update_csv_file_on_disk()
+
+
+func collect_translations() -> void:
+ var translation_files := []
+ var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT)
+
+ if translation_mode == TranslationModes.PER_TIMELINE:
+
+ for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.translation'):
+
+ for file: String in DialogicUtil.listdir(timeline_path.get_base_dir()):
+ file = timeline_path.get_base_dir().path_join(file)
+
+ if file.ends_with('.translation'):
+
+ if not file in translation_files:
+ translation_files.append(file)
+
+ if translation_mode == TranslationModes.PER_PROJECT:
+ var translation_folder: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://')
+
+ for file: String in DialogicUtil.listdir(translation_folder):
+ file = translation_folder.path_join(file)
+
+ if file.ends_with('.translation'):
+
+ if not file in translation_files:
+ translation_files.append(file)
+
+ var all_translation_files: Array = ProjectSettings.get_setting('internationalization/locale/translations', [])
+ var orig_file_amount := len(all_translation_files)
+
+ # This array keeps track of valid translation file paths.
+ var found_file_paths := []
+ var removed_translation_files := 0
+
+ for file_path: String in translation_files:
+ # If the file path is not valid, we must clean it up.
+ if ResourceLoader.exists(file_path):
+ found_file_paths.append(file_path)
+ else:
+ removed_translation_files += 1
+ continue
+
+ if not file_path in all_translation_files:
+ all_translation_files.append(file_path)
+
+ var path_without_suffix := file_path.trim_suffix('.translation')
+ var locale_part := path_without_suffix.split(".")[-1]
+ _collect_locale(locale_part)
+
+
+ var valid_translation_files := PackedStringArray(all_translation_files)
+ ProjectSettings.set_setting('internationalization/locale/translations', valid_translation_files)
+ ProjectSettings.save()
+
+ %StatusMessage.text = (
+ "Added translation files: " + str(len(all_translation_files)-orig_file_amount)
+ + "\nRemoved translation files: " + str(removed_translation_files)
+ + "\nTotal translation files: " + str(len(all_translation_files)))
+
+
+func _on_erase_translations_pressed() -> void:
+ %EraseConfirmationDialog.popup_centered()
+
+
+## Deletes translation files generated by [param csv_name].
+## The [param csv_name] may not contain the file extension (.csv).
+##
+## Returns a vector, value 1 is amount of deleted translation files.
+## Value
+func delete_translations_files(translation_files: Array, csv_name: String) -> int:
+ var deleted_files := 0
+
+ for file_path: String in DialogicResourceUtil.list_resources_of_type('.translation'):
+ var base_name: String = file_path.get_basename()
+ var path_parts := base_name.split("/")
+ var translation_name: String = path_parts[-1]
+
+ if translation_name.begins_with(csv_name):
+
+ if OK == DirAccess.remove_absolute(file_path):
+ var project_translation_file_index := translation_files.find(file_path)
+
+ if project_translation_file_index > -1:
+ translation_files.remove_at(project_translation_file_index)
+
+ deleted_files += 1
+ print_rich("[color=green]Deleted translation file: " + file_path + "[/color]")
+ else:
+ print_rich("[color=yellow]Failed to delete translation file: " + file_path + "[/color]")
+
+
+ return deleted_files
+
+
+## Iterates over all timelines and deletes their CSVs and timeline
+## translation IDs.
+## Deletes the Per-Project CSV file and the character name CSV file.
+func erase_translations() -> void:
+ var files: PackedStringArray = ProjectSettings.get_setting('internationalization/locale/translations', [])
+ var translation_files := Array(files)
+ ProjectSettings.set_setting(_USED_LOCALES_SETTING, [])
+
+ var deleted_csv_files := 0
+ var deleted_translation_files := 0
+ var cleaned_timelines := 0
+ var cleaned_characters := 0
+ var cleaned_events := 0
+ var cleaned_glossaries := 0
+
+ var current_timeline := _close_active_timeline()
+
+ # Delete all Dialogic CSV files and their translation files.
+ for csv_path: String in DialogicResourceUtil.list_resources_of_type(".csv"):
+ var csv_path_parts: PackedStringArray = csv_path.split("/")
+ var csv_name: String = csv_path_parts[-1].trim_suffix(".csv")
+
+ # Handle Dialogic CSVs only.
+ if not csv_name.begins_with("dialogic_"):
+ continue
+
+ # Delete the CSV file.
+ if OK == DirAccess.remove_absolute(csv_path):
+ deleted_csv_files += 1
+ print_rich("[color=green]Deleted CSV file: " + csv_path + "[/color]")
+
+ deleted_translation_files += delete_translations_files(translation_files, csv_name)
+ else:
+ print_rich("[color=yellow]Failed to delete CSV file: " + csv_path + "[/color]")
+
+ # Clean timelines.
+ for timeline_path: String in DialogicResourceUtil.list_resources_of_type(".dtl"):
+
+ # Process the timeline.
+ var timeline: DialogicTimeline = load(timeline_path)
+ timeline.process()
+ cleaned_timelines += 1
+
+ # Remove event translation IDs.
+ for event: DialogicEvent in timeline.events:
+
+ if event._translation_id and not event._translation_id.is_empty():
+ event.remove_translation_id()
+ event.update_text_version()
+ cleaned_events += 1
+
+ if "character" in event:
+ # Remove character translation IDs.
+ var character: DialogicCharacter = event.character
+
+ if character != null and not character._translation_id.is_empty():
+ character.remove_translation_id()
+ cleaned_characters += 1
+
+ timeline.set_meta("timeline_not_saved", true)
+ ResourceSaver.save(timeline, timeline_path)
+
+ _erase_glossary_translation_ids()
+ _erase_character_name_translation_ids()
+
+ ProjectSettings.set_setting('dialogic/translation/id_counter', 16)
+ ProjectSettings.set_setting('internationalization/locale/translations', PackedStringArray(translation_files))
+ ProjectSettings.save()
+
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
+
+ var status_message := "Timelines cleaned {cleaned_timelines}
+ Events cleaned {cleaned_events}
+ Characters cleaned {cleaned_characters}
+ Glossaries cleaned {cleaned_glossaries}
+
+ CSVs erased {erased_csv_files}
+ Translations erased {erased_translation_files}"
+
+ var status_message_args := {
+ 'cleaned_timelines': cleaned_timelines,
+ 'cleaned_characters': cleaned_characters,
+ 'cleaned_events': cleaned_events,
+ 'cleaned_glossaries': cleaned_glossaries,
+ 'erased_csv_files': deleted_csv_files,
+ 'erased_translation_files': deleted_translation_files,
+ }
+
+ _silently_open_timeline(current_timeline)
+
+ # Trigger reimport.
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
+
+ # Clear the internal settings.
+ ProjectSettings.clear('dialogic/translation/intern/save_mode')
+ ProjectSettings.clear('dialogic/translation/intern/file_mode')
+ ProjectSettings.clear('dialogic/translation/intern/translation_folder')
+
+ _verify_translation_file()
+ %StatusMessage.text = status_message.format(status_message_args)
+
+
+func _erase_glossary_translation_ids() -> void:
+ # Clean glossary.
+ var glossary_paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
+
+ for glossary_path: String in glossary_paths:
+ var glossary: DialogicGlossary = load(glossary_path)
+ glossary.remove_translation_id()
+ glossary.remove_entry_translation_ids()
+ glossary.clear_translation_keys()
+ ResourceSaver.save(glossary, glossary_path)
+ print_rich("[color=green]Cleaned up glossary file: " + glossary_path + "[/color]")
+
+
+func _erase_character_name_translation_ids() -> void:
+ for character_path: String in DialogicResourceUtil.list_resources_of_type('.dch'):
+ var character: DialogicCharacter = load(character_path)
+
+ character.remove_translation_id()
+ ResourceSaver.save(character)
+
+
+## Closes the current timeline in the Dialogic Editor and returns the timeline
+## as a resource.
+## If no timeline has been opened, returns null.
+func _close_active_timeline() -> Resource:
+ var timeline_node: DialogicEditor = settings_editor.editors_manager.editors['Timeline']['node']
+ # We will close this timeline to ensure it will properly update.
+ # By saving this reference, we can open it again.
+ var current_timeline := timeline_node.current_resource
+ # Clean the current editor, this will also close the timeline.
+ settings_editor.editors_manager.clear_editor(timeline_node)
+
+ return current_timeline
+
+
+## Opens the timeline resource into the Dialogic Editor.
+## If the timeline is null, does nothing.
+func _silently_open_timeline(timeline_to_open: Resource) -> void:
+ if timeline_to_open != null:
+ settings_editor.editors_manager.edit_resource(timeline_to_open, true, true)
+
+
+## Checks [param locale] for unique locales that have not been added
+## to the [_unique_locales] array yet.
+func _collect_locale(locale: String) -> void:
+ if _unique_locales.has(locale):
+ return
+
+ _unique_locales.append(locale)
--- /dev/null
+[gd_scene load_steps=7 format=3 uid="uid://chpb1mj03xjxv"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Settings/settings_translation.gd" id="1_dvmyi"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_k2lou"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3_dq4j2"]
+[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="4_kvsma"]
+
+[sub_resource type="Image" id="Image_g2hic"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_xbph7"]
+image = SubResource("Image_g2hic")
+
+[node name="Translations" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = -101.0
+offset_bottom = 102.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_dvmyi")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Basics" type="VBoxContainer" parent="HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Title" type="Label" parent="HBox/Basics"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Basics"
+
+[node name="VBox4" type="HBoxContainer" parent="HBox/Basics"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBox/Basics/VBox4"]
+layout_mode = 2
+text = "Enable translations"
+
+[node name="TransEnabled" type="CheckBox" parent="HBox/Basics/VBox4"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator5" type="VSeparator" parent="HBox"]
+layout_mode = 2
+
+[node name="Testing" type="VBoxContainer" parent="HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Title2" type="Label" parent="HBox/Testing"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Testing"
+
+[node name="VBox3" type="HBoxContainer" parent="HBox/Testing"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="HBox/Testing/VBox3"]
+layout_mode = 2
+text = "Testing locale"
+
+[node name="HintTooltip8" parent="HBox/Testing/VBox3" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Change this locale to test your game in a different language (only in-editor).
+Equivalent of the testing local project setting. "
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Change this locale to test your game in a different language (only in-editor).
+Equivalent of the testing local project setting.
+
+Update dropdown list via \"Collect Translation\"."
+
+[node name="TestingLocale" parent="HBox/Testing/VBox3" instance=ExtResource("3_dq4j2")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator4" type="HSeparator" parent="."]
+layout_mode = 2
+
+[node name="TranslationSettings" type="HBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="TranslationSettings"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="SettingsTitle" type="Label" parent="TranslationSettings/VBoxContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Settings"
+
+[node name="Grid" type="GridContainer" parent="TranslationSettings/VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="VBox" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="TranslationSettings/VBoxContainer/Grid/VBox"]
+layout_mode = 2
+text = "Default locale"
+
+[node name="HintTooltip" parent="TranslationSettings/VBoxContainer/Grid/VBox" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "The locale of the language your timelines are written in."
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "The locale of the language your timelines are written in."
+
+[node name="OrigLocale" parent="TranslationSettings/VBoxContainer/Grid" instance=ExtResource("3_dq4j2")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="TransFile" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="TranslationSettings/VBoxContainer/Grid/TransFile"]
+layout_mode = 2
+text = "Translation folder"
+
+[node name="HintTooltip3" parent="TranslationSettings/VBoxContainer/Grid/TransFile" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Choose a folder to let Dialogic save CSV files in.
+Also used when saving \"Inside Translation Folder\""
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Choose a folder to let Dialogic save CSV files in.
+Also used when saving \"Inside Translation Folder\""
+
+[node name="TransFolderPicker" parent="TranslationSettings/VBoxContainer/Grid" instance=ExtResource("4_kvsma")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+file_mode = 2
+
+[node name="VBox2" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"]
+layout_mode = 2
+
+[node name="OutputModeLabel" type="Label" parent="TranslationSettings/VBoxContainer/Grid/VBox2"]
+layout_mode = 2
+text = "Output mode"
+
+[node name="OutputModeTooltip" parent="TranslationSettings/VBoxContainer/Grid/VBox2" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Decides how many CSV files will be created.
+
+• \"Per Type\": Uses one CSV file for each type of resource: Timelines, characters, and glossaries.
+For example, 10 timelines will be combined into 1 CSV file.
+
+• \"Per File\": Uses one CSV file for each resource file.
+For example, 10 timelines will result in 10 CSV files.
+
+The \"Per File\" option utilises \"Output location\", in contrast, the \"Per Type\" will always use the Translation folder."
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Decides how many CSV files will be created.
+
+• \"Per Type\": Uses one CSV file for each type of resource: Timelines, characters, and glossaries.
+For example, 10 timelines will be combined into 1 CSV file.
+
+• \"Per File\": Uses one CSV file for each resource file.
+For example, 10 timelines will result in 10 CSV files.
+
+The \"Per File\" option utilises \"Output location\", in contrast, the \"Per Type\" will always use the Translation folder."
+
+[node name="TransMode" type="OptionButton" parent="TranslationSettings/VBoxContainer/Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 2
+selected = 0
+popup/item_0/text = "Per Type"
+popup/item_0/id = 0
+popup/item_1/text = "Per File"
+popup/item_1/id = 1
+
+[node name="OutputLocation" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"]
+layout_mode = 2
+
+[node name="OutputLocationLabel" type="Label" parent="TranslationSettings/VBoxContainer/Grid/OutputLocation"]
+layout_mode = 2
+text = "Output location"
+
+[node name="OutputLocationTooltip" parent="TranslationSettings/VBoxContainer/Grid/OutputLocation" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Decides where to save the generated CSV files.
+
+• \"Inside Translation Folder\": Uses the \"Translation folder\".
+
+• \"Next To Timeline\": Places them in the resource type's folder.
+
+This button requires the \"Per File\" Output mode.
+A resource type can be: Timelines, characters, and glossaries."
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Decides where to save the generated CSV files.
+
+• \"Inside Translation Folder\": Uses the \"Translation folder\".
+
+• \"Next To Timeline\": Places them in the resource type's folder.
+
+This button requires the \"Per File\" Output mode.
+A resource type can be: Timelines, characters, and glossaries."
+
+[node name="SaveLocationMode" type="OptionButton" parent="TranslationSettings/VBoxContainer/Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+disabled = true
+item_count = 2
+selected = 0
+popup/item_0/text = "Inside Translation Folder"
+popup/item_0/id = 0
+popup/item_1/text = "Next to File"
+popup/item_1/id = 1
+
+[node name="Control" type="Control" parent="TranslationSettings/VBoxContainer/Grid"]
+visible = false
+layout_mode = 2
+
+[node name="AddSeparatorHBox" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"]
+layout_mode = 2
+
+[node name="AddSeparatorLabel" type="Label" parent="TranslationSettings/VBoxContainer/Grid/AddSeparatorHBox"]
+layout_mode = 2
+text = "Add Separator Lines"
+
+[node name="HintAddSeparatorEnabled" parent="TranslationSettings/VBoxContainer/Grid/AddSeparatorHBox" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Adds an empty line into per-project CSVs to differentiate between sections.
+
+For example, when a new glossary item or timeline starts, an empty line will be added."
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Adds an empty line into per-project CSVs to differentiate between sections.
+
+For example, when a new glossary item or timeline starts, an empty line will be added."
+
+[node name="AddSeparatorEnabled" type="CheckBox" parent="TranslationSettings/VBoxContainer/Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator6" type="VSeparator" parent="TranslationSettings"]
+layout_mode = 2
+
+[node name="VBoxContainer2" type="VBoxContainer" parent="TranslationSettings"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="TranslationSettings/VBoxContainer2"]
+layout_mode = 2
+
+[node name="Title3" type="Label" parent="TranslationSettings/VBoxContainer2/HBoxContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Actions"
+
+[node name="Actions" type="GridContainer" parent="TranslationSettings/VBoxContainer2"]
+layout_mode = 2
+columns = 2
+
+[node name="UpdateCsvFiles" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"]
+unique_name_in_owner = true
+layout_mode = 2
+disabled = true
+text = "Update CSV files"
+icon = SubResource("ImageTexture_xbph7")
+
+[node name="HintTooltip5" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "This button will scan all timelines and generate or update their CSV files.
+
+A Dialogic CSV file will be prefixed with \"dialogic_\".
+
+This action will be disabled if the \"Translation folder\" is missing or has an invalid path."
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "This button will scan all timelines and generate or update their CSV files.
+
+A Dialogic CSV file will be prefixed with \"dialogic_\".
+
+This action will be disabled if the \"Translation folder\" is missing or has an invalid path."
+
+[node name="CollectTranslations" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Collect translations"
+icon = SubResource("ImageTexture_xbph7")
+
+[node name="HintTooltip6" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Godot imports CSV files as \".translation\" files.
+This buttons adds them to \"Project Settings -> Localization\".
+"
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Godot imports CSV files as \".translation\" files.
+This buttons adds them to \"Project Settings -> Localization\".
+"
+
+[node name="AspectRatioContainer2" type="AspectRatioContainer" parent="TranslationSettings/VBoxContainer2/Actions"]
+custom_minimum_size = Vector2(0, 31)
+layout_mode = 2
+
+[node name="AspectRatioContainer" type="AspectRatioContainer" parent="TranslationSettings/VBoxContainer2/Actions"]
+custom_minimum_size = Vector2(0, 31)
+layout_mode = 2
+
+[node name="TransRemove" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Remove translations"
+icon = SubResource("ImageTexture_xbph7")
+
+[node name="HintTooltip7" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")]
+layout_mode = 2
+tooltip_text = "Be very careful with this button!
+
+It will try to delete any \".csv\" and \".translation\" files that are related to Dialogic.
+CSV and translation files prefixed with \"dialogic_\" are treated as Dialogic-related.
+
+Removes translation IDs (eg. #id:33) from timelines and characters."
+texture = SubResource("ImageTexture_xbph7")
+hint_text = "Be very careful with this button!
+
+It will try to delete any \".csv\" and \".translation\" files that are related to Dialogic.
+CSV and translation files prefixed with \"dialogic_\" are treated as Dialogic-related.
+
+Removes translation IDs (eg. #id:33) from timelines and characters."
+
+[node name="StatusMessage" type="Label" parent="TranslationSettings/VBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "⛔ Requires valid translation folder to translate character names and the project CSV file."
+autowrap_mode = 3
+
+[node name="UpdateConfirmationDialog" type="ConfirmationDialog" parent="."]
+unique_name_in_owner = true
+title = "Please Decide..."
+size = Vector2i(490, 200)
+ok_button_text = "Delete old & Generate new"
+dialog_text = "You have previously generated CSVs and translation files with different Translation Settings!
+
+Please consider to delete the old CSVs and then generate new changes."
+dialog_autowrap = true
+
+[node name="EraseConfirmationDialog" type="ConfirmationDialog" parent="."]
+unique_name_in_owner = true
+position = Vector2i(0, 36)
+size = Vector2i(500, 280)
+min_size = Vector2i(300, 70)
+ok_button_text = "DELETE ALL"
+dialog_text = "You are about to:
+- Delete all CSVs prefixed with \"dialogic_\".
+- Delete the related CSV import files.
+- Delete the related translation files.
+- Remove translation IDs from timelines and characters.
+- Remove all \"dialogic\" prefixed translations from \"Project Settings -> Localization\".
+- Remove the \"_translation_keys\" and \"entries\" starting with \"Glossary/\"."
+dialog_autowrap = true
+
+[node name="AspectRatioContainer" type="AspectRatioContainer" parent="."]
+custom_minimum_size = Vector2(0, 31)
+layout_mode = 2
+
+[connection signal="confirmed" from="UpdateConfirmationDialog" to="." method="_delete_and_update"]
+[connection signal="confirmed" from="EraseConfirmationDialog" to="." method="erase_translations"]
--- /dev/null
+[gd_resource type="Theme" format=3 uid="uid://cqst728xxipcw"]
+
+[resource]
--- /dev/null
+[gd_resource type="Theme" format=2]
+
+[resource]
+Button/colors/font_color = Color( 1, 1, 1, 1 )
+Button/colors/font_color_disabled = Color( 0.901961, 0.901961, 0.901961, 0.2 )
+Button/colors/font_color_hover = Color( 0.870588, 0.870588, 0.870588, 1 )
+Button/colors/font_color_pressed = Color( 1, 1, 1, 1 )
--- /dev/null
+@tool
+extends Node
+
+enum Modes {TEXT_EVENT_ONLY, FULL_HIGHLIGHTING}
+
+var syntax_highlighter: SyntaxHighlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
+var text_syntax_highlighter: SyntaxHighlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
+
+
+# These RegEx's are used to deduce information from the current line for auto-completion
+
+# To find the currently typed word and the symbol before
+var completion_word_regex := RegEx.new()
+# To find the shortcode of the current shortcode event (basically the type)
+var completion_shortcode_getter_regex := RegEx.new()
+# To find the parameter name of the current if typing a value
+var completion_shortcode_param_getter_regex := RegEx.new()
+# To find the value of a paramater that is being typed
+var completion_shortcode_value_regex := RegEx.new()
+
+# Stores references to all shortcode events for parameter and value suggestions
+var shortcode_events := {}
+var custom_syntax_events := []
+var text_event: DialogicTextEvent = null
+
+func _ready() -> void:
+ # Compile RegEx's
+ completion_word_regex.compile("(?<s>(\\W)|^)(?<word>\\w*)\\x{FFFF}")
+ completion_shortcode_getter_regex.compile("\\[(?<code>\\w*)")
+ completion_shortcode_param_getter_regex.compile("(?<param>\\w*)\\W*=\\s*\"?(\\w|\\s)*"+String.chr(0xFFFF))
+ completion_shortcode_value_regex.compile(r'(\[|\s)[^\[\s=]*="(?<value>[^"$]*)'+String.chr(0xFFFF))
+
+ text_syntax_highlighter.mode = text_syntax_highlighter.Modes.TEXT_EVENT_ONLY
+
+#region AUTO COMPLETION
+################################################################################
+
+# Helper that gets the current line with a special character where the caret is
+func get_code_completion_line(text:CodeEdit) -> String:
+ return text.get_line(text.get_caret_line()).insert(text.get_caret_column(), String.chr(0xFFFF)).strip_edges()
+
+
+# Helper that gets the currently typed word
+func get_code_completion_word(text:CodeEdit) -> String:
+ var result := completion_word_regex.search(get_code_completion_line(text))
+ return result.get_string('word') if result else ""
+
+# Helper that gets the currently typed parameter
+func get_code_completion_parameter_value(text:CodeEdit) -> String:
+ var result := completion_shortcode_value_regex.search(get_code_completion_line(text))
+ return result.get_string('value') if result else ""
+
+
+# Helper that gets the symbol before the current word
+func get_code_completion_prev_symbol(text:CodeEdit) -> String:
+ var result := completion_word_regex.search(get_code_completion_line(text))
+ return result.get_string('s') if result else ""
+
+
+func get_line_untill_caret(line:String) -> String:
+ return line.substr(0, line.find(String.chr(0xFFFF)))
+
+
+# Called if something was typed
+# Adds all kinds of options depending on the
+# content of the current line, the last word and the symbol that came before
+# Triggers opening of the popup
+func request_code_completion(force:bool, text:CodeEdit, mode:=Modes.FULL_HIGHLIGHTING) -> void:
+ ## TODO remove this once https://github.com/godotengine/godot/issues/38560 is fixed
+ if mode != Modes.FULL_HIGHLIGHTING:
+ return
+
+ # make sure shortcode event references are loaded
+ if mode == Modes.FULL_HIGHLIGHTING:
+ var hidden_events: Array = DialogicUtil.get_editor_setting('hidden_event_buttons', [])
+ if shortcode_events.is_empty():
+ for event in DialogicResourceUtil.get_event_cache():
+ if event.get_shortcode() != 'default_shortcode':
+ shortcode_events[event.get_shortcode()] = event
+
+ else:
+ custom_syntax_events.append(event)
+ if event.event_name in hidden_events:
+ event.set_meta('hidden', true)
+ if event is DialogicTextEvent:
+ text_event = event
+ # this is done to force-load the text effects regex which is used below
+ event.load_text_effects()
+
+ # fill helpers
+ var line := get_code_completion_line(text)
+ var word := get_code_completion_word(text)
+ var symbol := get_code_completion_prev_symbol(text)
+ var line_part := get_line_untill_caret(line)
+
+ ## Note on use of KIND types for options.
+ # These types are mostly useless for us.
+ # However I decidede to assign some special cases for them:
+ # - KIND_PLAIN_TEXT is only shown if the beginnging of the option is already typed
+ # !word.is_empty() and option.begins_with(word)
+ # - KIND_CLASS is only shown if anything from the options is already typed
+ # !word.is_empty() and word in option
+ # - KIND_CONSTANT is shown and checked against the beginning
+ # option.begins_with(word)
+ # - KIND_MEMBER is shown and searched completely
+ # word in option
+
+ ## Note on VALUE key
+ # The value key is used to store a potential closing string for the completion.
+ # The completion will check if the string is already present and add it otherwise.
+
+ # Shortcode event suggestions
+ if mode == Modes.FULL_HIGHLIGHTING and syntax_highlighter.line_is_shortcode_event(text.get_caret_line()):
+ if symbol == '[':
+ # suggest shortcodes if a shortcode event has just begun
+ var shortcodes := shortcode_events.keys()
+ shortcodes.sort()
+ for shortcode in shortcodes:
+ if shortcode_events[shortcode].get_meta('hidden', false):
+ continue
+ if shortcode_events[shortcode].get_shortcode_parameters().is_empty():
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode, shortcode_events[shortcode].event_color.lerp(syntax_highlighter.normal_color, 0.3), shortcode_events[shortcode]._get_icon())
+ else:
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode+" ", shortcode_events[shortcode].event_color.lerp(syntax_highlighter.normal_color, 0.3), shortcode_events[shortcode]._get_icon())
+ else:
+ var full_event_text: String = syntax_highlighter.get_full_event(text.get_caret_line())
+ var current_shortcode := completion_shortcode_getter_regex.search(full_event_text)
+ if !current_shortcode:
+ text.update_code_completion_options(false)
+ return
+
+ var code := current_shortcode.get_string('code')
+ if !code in shortcode_events.keys():
+ text.update_code_completion_options(false)
+ return
+
+ # suggest parameters
+ if symbol == ' ' and line.count('"')%2 == 0:
+ var parameters: Array = shortcode_events[code].get_shortcode_parameters().keys()
+ for param in parameters:
+ if !param+'=' in full_event_text:
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, param, param+'="' , shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("MemberProperty", "EditorIcons"))
+
+ # suggest values
+ elif symbol == '=' or symbol == '"':
+ var current_parameter_gex := completion_shortcode_param_getter_regex.search(line)
+ if !current_parameter_gex:
+ text.update_code_completion_options(false)
+ return
+
+ var current_parameter := current_parameter_gex.get_string('param')
+ if !shortcode_events[code].get_shortcode_parameters().has(current_parameter):
+ text.update_code_completion_options(false)
+ return
+ if !shortcode_events[code].get_shortcode_parameters()[current_parameter].has('suggestions'):
+ if typeof(shortcode_events[code].get_shortcode_parameters()[current_parameter].default) == TYPE_BOOL:
+ suggest_bool(text, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3))
+ elif len(word) > 0:
+ text.add_code_completion_option(CodeEdit.KIND_VARIABLE, word, word, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("GuiScrollArrowRight", "EditorIcons"), '" ')
+ text.update_code_completion_options(true)
+ return
+
+ var suggestions: Dictionary = shortcode_events[code].get_shortcode_parameters()[current_parameter]['suggestions'].call()
+ suggest_custom_suggestions(suggestions, text, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3))
+
+ # Force update and showing of the popup
+ text.update_code_completion_options(true)
+ return
+
+
+ for event in custom_syntax_events:
+ if mode == Modes.TEXT_EVENT_ONLY and !event is DialogicTextEvent:
+ continue
+
+ if ! ' ' in line_part:
+ event._get_start_code_completion(self, text)
+
+ if event.is_valid_event(line):
+ event._get_code_completion(self, text, line, word, symbol)
+ break
+
+ # Force update and showing of the popup
+ text.update_code_completion_options(true)
+
+
+
+# Helper that adds all characters as options
+func suggest_characters(text:CodeEdit, type := CodeEdit.KIND_MEMBER, text_event_start:=false) -> void:
+ for character in DialogicResourceUtil.get_character_directory():
+ var result: String = character
+ if " " in character:
+ result = '"'+character+'"'
+ if text_event_start and load(DialogicResourceUtil.get_character_directory()[character]).portraits.is_empty():
+ result += ':'
+ text.add_code_completion_option(type, character, result, syntax_highlighter.character_name_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"))
+
+
+# Helper that adds all timelines as options
+func suggest_timelines(text:CodeEdit, type := CodeEdit.KIND_MEMBER, color:=Color()) -> void:
+ for timeline in DialogicResourceUtil.get_timeline_directory():
+ text.add_code_completion_option(type, timeline, timeline+'/', color, text.get_theme_icon("TripleBar", "EditorIcons"))
+
+
+func suggest_labels(text:CodeEdit, timeline:String='', end:='', color:=Color()) -> void:
+ if timeline in DialogicResourceUtil.get_label_cache():
+ for i in DialogicResourceUtil.get_label_cache()[timeline]:
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+end, color, load("res://addons/dialogic/Modules/Jump/icon_label.png"))
+
+
+# Helper that adds all portraits of a given character as options
+func suggest_portraits(text:CodeEdit, character_name:String, end_check:=')') -> void:
+ if !character_name in DialogicResourceUtil.get_character_directory():
+ return
+ var character_resource: DialogicCharacter = load(DialogicResourceUtil.get_character_directory()[character_name])
+ for portrait in character_resource.portraits:
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, portrait, portrait, syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"), end_check)
+ if character_resource.portraits.is_empty():
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, 'Has no portraits!', '', syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Pieces/warning.svg"))
+
+
+# Helper that adds all variable paths as options
+func suggest_variables(text:CodeEdit):
+ for variable in DialogicUtil.list_variables(ProjectSettings.get_setting('dialogic/variables')):
+ text.add_code_completion_option(CodeEdit.KIND_MEMBER, variable, variable, syntax_highlighter.variable_color, text.get_theme_icon("MemberProperty", "EditorIcons"), '}')
+
+
+# Helper that adds true and false as options
+func suggest_bool(text:CodeEdit, color:Color):
+ text.add_code_completion_option(CodeEdit.KIND_VARIABLE, 'true', 'true', color, text.get_theme_icon("GuiChecked", "EditorIcons"), '" ')
+ text.add_code_completion_option(CodeEdit.KIND_VARIABLE, 'false', 'false', color, text.get_theme_icon("GuiUnchecked", "EditorIcons"), '" ')
+
+
+func suggest_custom_suggestions(suggestions:Dictionary, text:CodeEdit, color:Color) -> void:
+ for key in suggestions.keys():
+ if suggestions[key].has('text_alt'):
+ text.add_code_completion_option(CodeEdit.KIND_VARIABLE, key, suggestions[key].text_alt[0], color, suggestions[key].get('icon', null), '" ')
+ else:
+ text.add_code_completion_option(CodeEdit.KIND_VARIABLE, key, str(suggestions[key].value), color, suggestions[key].get('icon', null), '" ')
+
+
+# Filters the list of all possible options, depending on what was typed
+# Purpose of the different Kinds is explained in [_request_code_completion]
+func filter_code_completion_candidates(candidates:Array, text:CodeEdit) -> Array:
+ var valid_candidates := []
+ var current_word := get_code_completion_word(text)
+ for candidate in candidates:
+ if candidate.kind == text.KIND_PLAIN_TEXT:
+ if !current_word.is_empty() and candidate.insert_text.begins_with(current_word):
+ valid_candidates.append(candidate)
+ elif candidate.kind == text.KIND_MEMBER:
+ if current_word.is_empty() or current_word.to_lower() in candidate.insert_text.to_lower():
+ valid_candidates.append(candidate)
+ elif candidate.kind == text.KIND_VARIABLE:
+ var current_param_value := get_code_completion_parameter_value(text)
+ if current_param_value.is_empty() or current_param_value.to_lower() in candidate.insert_text.to_lower():
+ valid_candidates.append(candidate)
+ elif candidate.kind == text.KIND_CONSTANT:
+ if current_word.is_empty() or candidate.insert_text.begins_with(current_word):
+ valid_candidates.append(candidate)
+ elif candidate.kind == text.KIND_CLASS:
+ if !current_word.is_empty() and current_word.to_lower() in candidate.insert_text.to_lower():
+ valid_candidates.append(candidate)
+ return valid_candidates
+
+
+# Called when code completion was activated
+# Inserts the selected item
+func confirm_code_completion(replace:bool, text:CodeEdit) -> void:
+ # Note: I decided to ALWAYS use replace mode, as dialogic is supposed to be beginner friendly
+
+ var code_completion := text.get_code_completion_option(text.get_code_completion_selected_index())
+
+ var word := get_code_completion_word(text)
+ if code_completion.kind == CodeEdit.KIND_VARIABLE:
+ word = get_code_completion_parameter_value(text)
+
+ text.remove_text(text.get_caret_line(), text.get_caret_column()-len(word), text.get_caret_line(), text.get_caret_column())
+
+ # Something has changed between 4.2 and 4.3
+ # Probably about how carets are reset when text is removed or idk.
+ # To keep compatibility with 4.2 for at least a while this should do the trick:
+ # TODO: Remove once compatibility for 4.2 is dropped.
+ if Engine.get_version_info().hex >= 0x040300:
+ text.set_caret_column(text.get_caret_column())
+ else:
+ text.set_caret_column(text.get_caret_column()-len(word))
+
+ text.insert_text_at_caret(code_completion.insert_text)
+
+ if code_completion.has('default_value') and typeof(code_completion['default_value']) == TYPE_STRING:
+ var next_letter := text.get_line(text.get_caret_line()).substr(text.get_caret_column(), len(code_completion['default_value']))
+ if next_letter == code_completion['default_value'] or next_letter[0] == code_completion['default_value'][0]:
+ text.set_caret_column(text.get_caret_column()+1)
+ else:
+ text.insert_text_at_caret(code_completion['default_value'])
+
+
+#endregion
+
+#region SYMBOL CLICKING
+################################################################################
+
+# Performs an action (like opening a link) when a valid symbol was clicked
+func symbol_lookup(symbol:String, line:int, column:int) -> void:
+ if symbol in shortcode_events.keys():
+ if !shortcode_events[symbol].help_page_path.is_empty():
+ OS.shell_open(shortcode_events[symbol].help_page_path)
+ if symbol in DialogicResourceUtil.get_character_directory():
+ EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(symbol, 'dch'))
+ if symbol in DialogicResourceUtil.get_timeline_directory():
+ EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(symbol, 'dtl'))
+
+
+# Called to test if a symbol can be clicked
+func symbol_validate(symbol:String, text:CodeEdit) -> void:
+ if symbol in shortcode_events.keys():
+ if !shortcode_events[symbol].help_page_path.is_empty():
+ text.set_symbol_lookup_word_as_valid(true)
+ if symbol in DialogicResourceUtil.get_character_directory():
+ text.set_symbol_lookup_word_as_valid(true)
+ if symbol in DialogicResourceUtil.get_timeline_directory():
+ text.set_symbol_lookup_word_as_valid(true)
+
+#endregion
--- /dev/null
+@tool
+extends SyntaxHighlighter
+
+## Syntax highlighter for the dialogic text timeline editor and text events in the visual editor.
+
+enum Modes {TEXT_EVENT_ONLY, FULL_HIGHLIGHTING}
+var mode := Modes.FULL_HIGHLIGHTING
+
+
+## RegEx's
+var word_regex := RegEx.new()
+var region_regex := RegEx.new()
+var number_regex := RegEx.create_from_string(r"(\d|\.)+")
+var shortcode_regex := RegEx.create_from_string(r"\W*\[(?<id>\w*)(?<args>[^\]]*)?")
+var shortcode_param_regex := RegEx.create_from_string(r'((?<parameter>[^\s=]*)\s*=\s*"(?<value>([^=]|\\=)*)(?<!\\)")')
+
+## Colors
+var normal_color: Color
+var translation_id_color: Color
+
+var code_flow_color: Color
+var boolean_operator_color: Color
+var variable_color: Color
+var string_color: Color
+var character_name_color: Color
+var character_portrait_color: Color
+
+var shortcode_events := {}
+var custom_syntax_events := []
+var text_event: DialogicTextEvent = null
+
+
+func _init() -> void:
+ update_colors()
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().get_base_control().theme_changed.connect(update_colors)
+
+
+func update_colors() -> void:
+ if not DialogicUtil.get_dialogic_plugin():
+ return
+ var editor_settings: EditorSettings = DialogicUtil.get_dialogic_plugin().get_editor_interface().get_editor_settings()
+ normal_color = editor_settings.get('text_editor/theme/highlighting/text_color')
+ translation_id_color = editor_settings.get('text_editor/theme/highlighting/comment_color')
+
+ code_flow_color = editor_settings.get("text_editor/theme/highlighting/control_flow_keyword_color")
+ boolean_operator_color = code_flow_color.lightened(0.5)
+ variable_color = editor_settings.get('text_editor/theme/highlighting/engine_type_color')
+ string_color = editor_settings.get('text_editor/theme/highlighting/string_color')
+ character_name_color = editor_settings.get('text_editor/theme/highlighting/symbol_color').lerp(normal_color, 0.3)
+ character_portrait_color = character_name_color.lerp(normal_color, 0.5)
+
+
+func _get_line_syntax_highlighting(line:int) -> Dictionary:
+ var str_line := get_text_edit().get_line(line)
+
+ if shortcode_events.is_empty():
+ for event in DialogicResourceUtil.get_event_cache():
+ if event.get_shortcode() != 'default_shortcode':
+ shortcode_events[event.get_shortcode()] = event
+ else:
+ custom_syntax_events.append(event)
+ if event is DialogicTextEvent:
+ text_event = event
+ text_event.load_text_effects()
+
+ var dict := {}
+ dict[0] = {'color':normal_color}
+
+ dict = color_translation_id(dict, str_line)
+
+ if mode == Modes.FULL_HIGHLIGHTING:
+ if line_is_shortcode_event(line):
+ var full_event := get_full_event(line)
+ var result := shortcode_regex.search(full_event)
+ if result:
+ if result.get_string('id') in shortcode_events:
+ if full_event.begins_with(str_line):
+ dict[result.get_start('id')] = {"color":shortcode_events[result.get_string('id')].event_color.lerp(normal_color, 0.4)}
+ dict[result.get_end('id')] = {"color":normal_color}
+
+ if result.get_string('args'):
+ color_shortcode_content(dict, str_line, result.get_start('args'), result.get_end('args'), shortcode_events[result.get_string('id')].event_color)
+ else:
+ color_shortcode_content(dict, str_line, 0, 0, shortcode_events[result.get_string('id')].event_color)
+ return fix_dict(dict)
+
+ else:
+ for event in custom_syntax_events:
+ if event.is_valid_event(str_line.strip_edges()):
+ dict = event._get_syntax_highlighting(self, dict, str_line)
+ return fix_dict(dict)
+
+ else:
+ dict = text_event._get_syntax_highlighting(self, dict, str_line)
+ return fix_dict(dict)
+
+
+func line_is_shortcode_event(line_idx:int) -> bool:
+ var str_line := get_text_edit().get_line(line_idx)
+ if text_event.text_effects_regex.search(str_line.get_slice(' ', 0)):
+ return false
+
+ if str_line.strip_edges().begins_with("["):
+ return true
+
+ if line_idx > 0 and get_text_edit().get_line(line_idx-1).ends_with('\\'):
+ return line_is_shortcode_event(line_idx-1)
+
+ return false
+
+
+func get_full_event(line_idx:int) -> String:
+ var str_line := get_text_edit().get_line(line_idx)
+ var offset := 1
+ # Add previous lines
+ while get_text_edit().get_line(line_idx-offset).ends_with('\\'):
+ str_line = get_text_edit().get_line(line_idx-offset).trim_suffix('\\')+"\n"+str_line
+ offset += 1
+
+ # This is commented out, as it is not needed right now.
+ # However without it, this isn't actually the full event.
+ # Might need to be included some day.
+ #offset = 0
+ ## Add following lines
+ #while get_text_edit().get_line(line_idx+offset).ends_with('\\'):
+ #str_line = str_line.trim_suffix('\\')+"\n"+get_text_edit().get_line(line_idx+offset)
+ #offset += 1
+
+ return str_line
+
+func fix_dict(dict:Dictionary) -> Dictionary:
+ var d := {}
+ var k := dict.keys()
+ k.sort()
+ for i in k:
+ d[i] = dict[i]
+ return d
+
+
+func color_condition(dict:Dictionary, line:String, from:int = 0, to:int = 0) -> Dictionary:
+ dict = color_word(dict, code_flow_color, line, 'or', from, to)
+ dict = color_word(dict, code_flow_color, line, 'and', from, to)
+ dict = color_word(dict, code_flow_color, line, '==', from, to)
+ dict = color_word(dict, code_flow_color, line, '!=', from, to)
+ if !">=" in line:
+ dict = color_word(dict, code_flow_color, line, '>', from, to)
+ else:
+ dict = color_word(dict, code_flow_color, line, '>=', from, to)
+ if !"<=" in line:
+ dict = color_word(dict, code_flow_color, line, '<', from, to)
+ else:
+ dict = color_word(dict, code_flow_color, line, '<=', from, to)
+ dict = color_region(dict, variable_color, line, '{', '}', from, to)
+ dict = color_region(dict, string_color, line, '"', '"', from, to)
+
+
+ return dict
+
+
+func color_translation_id(dict:Dictionary, line:String) -> Dictionary:
+ dict = color_region(dict, translation_id_color, line, '#id:', '')
+ return dict
+
+
+func color_word(dict:Dictionary, color:Color, line:String, word:String, from:int= 0, to:int = 0) -> Dictionary:
+ word_regex.compile("\\W(?<word>"+word+")\\W")
+ if to <= from:
+ to = len(line)-1
+ for i in word_regex.search_all(line.substr(from, to-from+2)):
+ dict[i.get_start('word')+from] = {'color':color}
+ dict[i.get_end('word')+from] = {'color':normal_color}
+ return dict
+
+
+func color_region(dict:Dictionary, color:Color, line:String, start:String, end:String, from:int = 0, to:int = 0, base_color:Color=normal_color) -> Dictionary:
+ if start in "()[].":
+ start = "\\"+start
+ if end in "()[].":
+ end = "\\"+end
+
+ if end.is_empty():
+ region_regex.compile("(?<!\\\\)"+start+".*")
+ else:
+ region_regex.compile("(?<!\\\\)"+start+"((?!"+end+").)*"+end)
+ if to <= from:
+ to = len(line)-1
+ for region in region_regex.search_all(line.substr(from, to-from+2)):
+ dict[region.get_start()+from] = {'color':color}
+ dict[region.get_end()+from] = {'color':base_color}
+ return dict
+
+
+func color_shortcode_content(dict:Dictionary, line:String, from:int = 0, to:int = 0, base_color:=normal_color) -> Dictionary:
+ if to <= from:
+ to = len(line)-1
+ var args_result := shortcode_param_regex.search_all(line.substr(from, to-from+2))
+ for x in args_result:
+ dict[x.get_start()+from] = {"color":base_color.lerp(normal_color, 0.5)}
+ dict[x.get_start('value')+from-1] = {"color":base_color.lerp(normal_color, 0.7)}
+ dict[x.get_end()+from] = {"color":normal_color}
+ return dict
--- /dev/null
+@tool
+extends CodeEdit
+
+## Sub-Editor that allows editing timelines in a text format.
+
+@onready var timeline_editor := get_parent().get_parent()
+@onready var code_completion_helper: Node= find_parent('EditorsManager').get_node('CodeCompletionHelper')
+
+var label_regex := RegEx.create_from_string('label +(?<name>[^\n]+)')
+
+func _ready() -> void:
+ await find_parent('EditorView').ready
+ syntax_highlighter = code_completion_helper.syntax_highlighter
+ timeline_editor.editors_manager.sidebar.content_item_activated.connect(_on_content_item_clicked)
+
+
+func _on_text_editor_text_changed() -> void:
+ timeline_editor.current_resource_state = DialogicEditor.ResourceStates.UNSAVED
+ request_code_completion(true)
+ $UpdateTimer.start()
+
+
+func clear_timeline() -> void:
+ text = ''
+ update_content_list()
+
+
+func load_timeline(timeline:DialogicTimeline) -> void:
+ clear_timeline()
+
+ text = timeline.as_text()
+
+ timeline_editor.current_resource.set_meta("timeline_not_saved", false)
+ clear_undo_history()
+
+ await get_tree().process_frame
+ update_content_list()
+
+
+func save_timeline() -> void:
+ if !timeline_editor.current_resource:
+ return
+
+ var text_array: Array = text_timeline_to_array(text)
+
+ timeline_editor.current_resource.events = text_array
+ timeline_editor.current_resource.events_processed = false
+ ResourceSaver.save(timeline_editor.current_resource, timeline_editor.current_resource.resource_path)
+
+ timeline_editor.current_resource.set_meta("timeline_not_saved", false)
+ timeline_editor.current_resource_state = DialogicEditor.ResourceStates.SAVED
+ DialogicResourceUtil.update_directory('dtl')
+
+
+func text_timeline_to_array(text:String) -> Array:
+ # Parse the lines down into an array
+ var events := []
+
+ var lines := text.split('\n', true)
+ var idx := -1
+
+ while idx < len(lines)-1:
+ idx += 1
+ var line: String = lines[idx]
+ var line_stripped: String = line.strip_edges(true, true)
+ events.append(line)
+
+ return events
+
+
+################################################################################
+## HELPFUL EDITOR FUNCTIONALITY
+################################################################################
+
+func _gui_input(event):
+ if not event is InputEventKey: return
+ if not event.is_pressed(): return
+ match event.as_text():
+ "Ctrl+K":
+ toggle_comment()
+ "Alt+Up":
+ move_line(-1)
+ "Alt+Down":
+ move_line(1)
+ "Ctrl+Shift+D":
+ duplicate_line()
+ _:
+ return
+ get_viewport().set_input_as_handled()
+
+# Toggle the selected lines as comments
+func toggle_comment() -> void:
+ var cursor: Vector2 = Vector2(get_caret_column(), get_caret_line())
+ var selection := Rect2i(
+ Vector2i(get_selection_line(), get_selection_column()),
+ # TODO When ditching godot 4.2, switch to this, the above methods have been deprecated in 4.3
+ #Vector2i(get_selection_origin_line(), get_selection_origin_column()),
+ Vector2i(get_caret_line(), get_caret_column()))
+ var from: int = cursor.y
+ var to: int = cursor.y
+ if has_selection():
+ from = get_selection_from_line()
+ to = get_selection_to_line()
+
+ var lines: PackedStringArray = text.split("\n")
+ var will_comment: bool = false
+ for i in range(from, to+1):
+ if not lines[i].begins_with("#"):
+ will_comment = true
+
+ for i in range(from, to + 1):
+ if will_comment:
+ lines[i] = "#" + lines[i]
+ else:
+ lines[i] = lines[i].trim_prefix("#")
+
+ text = "\n".join(lines)
+ if will_comment:
+ cursor.x += 1
+ selection.position.y += 1
+ selection.size.y += 1
+ else:
+ cursor.x -= 1
+ selection.position.y -= 1
+ selection.size.y -= 1
+ select(selection.position.x, selection.position.y, selection.size.x, selection.size.y)
+ text_changed.emit()
+
+
+# Move the selected lines up or down
+func move_line(offset: int) -> void:
+ offset = clamp(offset, -1, 1)
+
+ var cursor: Vector2 = Vector2(get_caret_column(), get_caret_line())
+ var reselect: bool = false
+ var from: int = cursor.y
+ var to: int = cursor.y
+ if has_selection():
+ reselect = true
+ from = get_selection_from_line()
+ to = get_selection_to_line()
+
+ var lines := text.split("\n")
+
+ if from + offset < 0 or to + offset >= lines.size(): return
+
+ var target_from_index: int = from - 1 if offset == -1 else to + 1
+ var target_to_index: int = to if offset == -1 else from
+ var line_to_move: String = lines[target_from_index]
+ lines.remove_at(target_from_index)
+ lines.insert(target_to_index, line_to_move)
+
+ text = "\n".join(lines)
+
+ cursor.y += offset
+ from += offset
+ to += offset
+ if reselect:
+ select(from, 0, to, get_line_width(to))
+ set_caret_line(cursor.y)
+ set_caret_column(cursor.x)
+ text_changed.emit()
+
+
+func duplicate_line() -> void:
+ var cursor: Vector2 = Vector2(get_caret_column(), get_caret_line())
+ var from: int = cursor.y
+ var to: int = cursor.y+1
+ if has_selection():
+ from = get_selection_from_line()
+ to = get_selection_to_line()+1
+
+ var lines := text.split("\n")
+ var lines_to_dupl: PackedStringArray = lines.slice(from, to)
+
+ text = "\n".join(lines.slice(0, from)+lines_to_dupl+lines.slice(from))
+
+ set_caret_line(cursor.y+to-from)
+ set_caret_column(cursor.x)
+ text_changed.emit()
+
+
+# Allows dragging files into the editor
+func _can_drop_data(at_position:Vector2, data:Variant) -> bool:
+ if typeof(data) == TYPE_DICTIONARY and 'files' in data.keys() and len(data.files) == 1:
+ return true
+ return false
+
+
+# Allows dragging files into the editor
+func _drop_data(at_position:Vector2, data:Variant) -> void:
+ if typeof(data) == TYPE_DICTIONARY and 'files' in data.keys() and len(data.files) == 1:
+ set_caret_column(get_line_column_at_pos(at_position).x)
+ set_caret_line(get_line_column_at_pos(at_position).y)
+ var result: String = data.files[0]
+ if get_line(get_caret_line())[get_caret_column()-1] != '"':
+ result = '"'+result
+ if get_line(get_caret_line())[get_caret_column()] != '"':
+ result = result+'"'
+
+ insert_text_at_caret(result)
+
+
+func _on_update_timer_timeout() -> void:
+ update_content_list()
+
+
+func update_content_list() -> void:
+ var labels: PackedStringArray = []
+ for i in label_regex.search_all(text):
+ labels.append(i.get_string('name'))
+ timeline_editor.editors_manager.sidebar.update_content_list(labels)
+
+
+func _on_content_item_clicked(label:String) -> void:
+ if label == "~ Top":
+ set_caret_line(0)
+ set_caret_column(0)
+ adjust_viewport_to_caret()
+ return
+
+ for i in label_regex.search_all(text):
+ if i.get_string('name') == label:
+ set_caret_column(0)
+ set_caret_line(text.count('\n', 0, i.get_start()+1))
+ center_viewport_to_caret()
+ return
+
+
+func _search_timeline(search_text:String) -> bool:
+ set_search_text(search_text)
+ queue_redraw()
+ set_meta("current_search", search_text)
+
+ return search(search_text, 0, 0, 0).y != -1
+
+
+func _search_navigate_down() -> void:
+ search_navigate(false)
+
+
+func _search_navigate_up() -> void:
+ search_navigate(true)
+
+
+func search_navigate(navigate_up := false) -> void:
+ if not has_meta("current_search"):
+ return
+ var pos: Vector2i
+ var search_from_line := 0
+ var search_from_column := 0
+ if has_selection():
+ if navigate_up:
+ search_from_line = get_selection_from_line()
+ search_from_column = get_selection_from_column()-1
+ if search_from_column == -1:
+ if search_from_line == 0:
+ search_from_line = get_line_count()
+ else:
+ search_from_line -= 1
+ search_from_column = max(get_line(search_from_line).length()-1,0)
+ else:
+ search_from_line = get_selection_to_line()
+ search_from_column = get_selection_to_column()
+ else:
+ search_from_line = get_caret_line()
+ search_from_column = get_caret_column()
+
+ pos = search(get_meta("current_search"), 4 if navigate_up else 0, search_from_line, search_from_column)
+ select(pos.y, pos.x, pos.y, pos.x+len(get_meta("current_search")))
+ set_caret_line(pos.y)
+ center_viewport_to_caret()
+ queue_redraw()
+
+
+################################################################################
+## AUTO COMPLETION
+################################################################################
+
+# Called if something was typed
+func _request_code_completion(force:bool):
+ code_completion_helper.request_code_completion(force, self)
+
+
+# Filters the list of all possible options, depending on what was typed
+# Purpose of the different Kinds is explained in [_request_code_completion]
+func _filter_code_completion_candidates(candidates:Array) -> Array:
+ return code_completion_helper.filter_code_completion_candidates(candidates, self)
+
+
+# Called when code completion was activated
+# Inserts the selected item
+func _confirm_code_completion(replace:bool) -> void:
+ code_completion_helper.confirm_code_completion(replace, self)
+
+
+################################################################################
+## SYMBOL CLICKING
+################################################################################
+
+# Performs an action (like opening a link) when a valid symbol was clicked
+func _on_symbol_lookup(symbol, line, column):
+ code_completion_helper.symbol_lookup(symbol, line, column)
+
+
+# Called to test if a symbol can be clicked
+func _on_symbol_validate(symbol:String) -> void:
+ code_completion_helper.symbol_validate(symbol, self)
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://defdeav8rli6o"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.gd" id="1_1kbx2"]
+
+[node name="TimelineTextEditor" type="CodeEdit"]
+offset_top = 592.0
+offset_right = 1024.0
+offset_bottom = 600.0
+theme_override_constants/line_spacing = 10
+wrap_mode = 1
+highlight_current_line = true
+draw_tabs = true
+minimap_draw = true
+caret_blink = true
+line_folding = true
+gutters_draw_line_numbers = true
+gutters_draw_fold_gutter = true
+code_completion_enabled = true
+code_completion_prefixes = Array[String](["[", "{"])
+indent_automatic = true
+auto_brace_completion_enabled = true
+auto_brace_completion_highlight_matching = true
+script = ExtResource("1_1kbx2")
+
+[node name="UpdateTimer" type="Timer" parent="."]
+one_shot = true
+
+[connection signal="code_completion_requested" from="." to="." method="_on_code_completion_requested"]
+[connection signal="symbol_lookup" from="." to="." method="_on_symbol_lookup"]
+[connection signal="symbol_validate" from="." to="." method="_on_symbol_validate"]
+[connection signal="text_changed" from="." to="." method="_on_text_editor_text_changed"]
+[connection signal="timeout" from="UpdateTimer" to="." method="_on_update_timer_timeout"]
--- /dev/null
+@tool
+extends Button
+
+@export var visible_name := ""
+@export var event_id := ""
+@export var event_icon: Texture:
+ get:
+ return event_icon
+ set(texture):
+ event_icon = texture
+ icon = event_icon
+@export var event_sorting_index: int = 0
+@export var resource: DialogicEvent
+@export var dialogic_color_name := ""
+
+
+func _ready() -> void:
+ tooltip_text = visible_name
+
+ custom_minimum_size = Vector2(get_theme_font("font", "Label").get_string_size(text).x+35,30) * DialogicUtil.get_editor_scale()
+
+ add_theme_color_override("font_color", get_theme_color("font_color", "Editor"))
+ add_theme_color_override("font_color_hover", get_theme_color("accent_color", "Editor"))
+ apply_base_button_style()
+
+
+func apply_base_button_style() -> void:
+ var nstyle: StyleBoxFlat = get_parent().get_theme_stylebox('normal', 'Button').duplicate()
+ nstyle.border_width_left = 5 * DialogicUtil.get_editor_scale()
+ add_theme_stylebox_override('normal', nstyle)
+ var hstyle: StyleBoxFlat = get_parent().get_theme_stylebox('hover', 'Button').duplicate()
+ hstyle.border_width_left = 5 * DialogicUtil.get_editor_scale()
+ add_theme_stylebox_override('hover', hstyle)
+ set_color(resource.event_color)
+
+
+func set_color(color:Color) -> void:
+ var style := get_theme_stylebox('normal', 'Button')
+ style.border_color = color
+ add_theme_stylebox_override('normal', style)
+ style = get_theme_stylebox('hover', 'Button')
+ style.border_color = color
+ add_theme_stylebox_override('hover', style)
+
+
+func toggle_name(on:= false) -> void:
+ if !on:
+ text = ""
+ custom_minimum_size = Vector2(40, 40) * DialogicUtil.get_editor_scale()
+ var style := get_theme_stylebox('normal', 'Button')
+ style.bg_color = style.border_color.darkened(0.2)
+ add_theme_stylebox_override('normal', style)
+ style = get_theme_stylebox('hover', 'Button')
+ style.bg_color = style.border_color
+ add_theme_stylebox_override('hover', style)
+ else:
+ text = visible_name
+ custom_minimum_size = Vector2(get_theme_font("font", 'Label').get_string_size(text).x+35,30) * DialogicUtil.get_editor_scale()
+ apply_base_button_style()
+
+
+func _on_button_down() -> void:
+ find_parent('VisualEditor').get_node('%TimelineArea').start_dragging(1, resource)
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://depcrpeh3f4rv"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/AddEventButton.gd" id="1_s43sc"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qx31r"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.1, 0.1, 0.1, 0.6)
+border_width_left = 3
+border_color = Color(0.231373, 0.545098, 0.94902, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_n1o16"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.225, 0.225, 0.225, 0.6)
+border_width_left = 3
+border_color = Color(0.231373, 0.545098, 0.94902, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 5
+
+[node name="AddEventButton" type="Button"]
+custom_minimum_size = Vector2(44, 30)
+offset_right = 97.0
+offset_bottom = 42.0
+tooltip_text = "S"
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_styles/normal = SubResource("StyleBoxFlat_qx31r")
+theme_override_styles/hover = SubResource("StyleBoxFlat_n1o16")
+alignment = 0
+expand_icon = true
+script = ExtResource("1_s43sc")
+visible_name = "S"
+
+[connection signal="button_down" from="." to="." method="_on_button_down"]
--- /dev/null
+@tool
+extends ScrollContainer
+
+# Script of the TimelineArea (that contains the event blocks).
+# Manages the drawing of the event lines and event dragging.
+
+
+enum DragTypes {NOTHING, NEW_EVENT, EXISTING_EVENTS}
+
+var drag_type: DragTypes = DragTypes.NOTHING
+var drag_data: Variant
+var drag_to_position := 0:
+ set(value):
+ drag_to_position = value
+ drag_to_position_updated = true
+var dragging := false
+var drag_to_position_updated := false
+
+
+signal drag_completed(type, index, data)
+signal drag_canceled()
+
+
+func _ready() -> void:
+ resized.connect(add_extra_scroll_area_to_timeline)
+ %Timeline.child_entered_tree.connect(add_extra_scroll_area_to_timeline)
+
+ # This prevents the view to turn black if you are editing this scene in Godot
+ if find_parent('EditorView'):
+ %TimelineArea.get_theme_color("background_color", "CodeEdit")
+
+
+#region EVENT DRAGGING
+################################################################################
+
+func start_dragging(type:DragTypes, data:Variant) -> void:
+ dragging = true
+ drag_type = type
+ drag_data = data
+ drag_to_position_updated = false
+
+
+func _input(event:InputEvent) -> void:
+ if !dragging:
+ return
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
+ if !event.is_pressed():
+ finish_dragging()
+
+
+func _process(delta:float) -> void:
+ if !dragging:
+ return
+
+ for child in %Timeline.get_children():
+ if (child.global_position.y < get_global_mouse_position().y) and \
+ (child.global_position.y+child.size.y > get_global_mouse_position().y):
+
+ if get_global_mouse_position().y > child.global_position.y+(child.size.y/2.0):
+ drag_to_position = child.get_index()+1
+ queue_redraw()
+ else:
+ drag_to_position = child.get_index()
+ queue_redraw()
+
+
+func finish_dragging() -> void:
+ dragging = false
+ if drag_to_position_updated and get_global_rect().has_point(get_global_mouse_position()):
+ drag_completed.emit(drag_type, drag_to_position, drag_data)
+ else:
+ drag_canceled.emit()
+ queue_redraw()
+
+#endregion
+
+
+#region LINE DRAWING
+################################################################################
+
+func _draw() -> void:
+ var line_width := 5 * DialogicUtil.get_editor_scale()
+ var horizontal_line_length := 100 * DialogicUtil.get_editor_scale()
+ var color_multiplier := Color(1,1,1,0.25)
+ var selected_color_multiplier := Color(1,1,1,1)
+
+
+ ## Draw Event Lines
+ for idx in range($Timeline.get_child_count()):
+ var block: Control = $Timeline.get_child(idx)
+
+ if not "resource" in block:
+ continue
+
+ if not block.visible:
+ continue
+
+ if block.resource is DialogicEndBranchEvent:
+ continue
+
+ if not (block.has_any_enabled_body_content or block.resource.can_contain_events):
+ continue
+
+ var icon_panel_height: int = block.get_node('%IconPanel').size.y
+ var rect_position: Vector2 = block.get_node('%IconPanel').global_position+Vector2(0,1)*block.get_node('%IconPanel').size+Vector2(0,-4)
+ var color: Color = block.resource.event_color
+
+ if block.is_selected() or block.end_node and block.end_node.is_selected():
+ color *= selected_color_multiplier
+ else:
+ color *= color_multiplier
+
+ if block.expanded and not block.resource.can_contain_events:
+ draw_rect(Rect2(rect_position-global_position+Vector2(line_width, 0), Vector2(line_width, block.size.y-block.get_node('%IconPanel').size.y)), color)
+
+ ## If the indentation has not changed, nothing else happens
+ if idx >= $Timeline.get_child_count()-1 or block.current_indent_level >= $Timeline.get_child(idx+1).current_indent_level:
+ continue
+
+ ## Draw connection between opening and end branch events
+ if block.resource.can_contain_events:
+ var end_node: Node = block.end_node
+
+ if end_node != null:
+ var v_length: float = end_node.global_position.y+end_node.size.y/2-rect_position.y
+ #var rect_size := Vector2(line_width, )
+ var offset := Vector2(line_width, 0)
+
+ # Draw vertical line
+ draw_rect(Rect2(rect_position-global_position+offset, Vector2(line_width, v_length)), color)
+ # Draw horizonal line (on END BRANCH event)
+ draw_rect(Rect2(
+ rect_position.x+line_width-global_position.x+offset.x,
+ rect_position.y+v_length-line_width-global_position.y,
+ horizontal_line_length-offset.x,
+ line_width),
+ color)
+
+ if block.resource.wants_to_group:
+ var group_color: Color = block.resource.event_color*color_multiplier
+ var group_starter := true
+ if idx != 0:
+ var block_above := $Timeline.get_child(idx-1)
+ if block_above.resource.event_name == block.resource.event_name:
+ group_starter = false
+ if block_above.resource is DialogicEndBranchEvent and block_above.parent_node.resource.event_name == block.resource.event_name:
+ group_starter = false
+
+ ## Draw small horizontal line on any event in group
+ draw_rect(Rect2(
+ rect_position.x-global_position.x-line_width,
+ rect_position.y-global_position.y-icon_panel_height/2,
+ line_width,
+ line_width),
+ group_color)
+
+ if group_starter:
+ ## Find the last event in the group (or that events END BRANCH)
+ var sub_idx := idx
+ var group_end_idx := idx
+ while sub_idx < $Timeline.get_child_count()-1:
+ sub_idx += 1
+ if $Timeline.get_child(sub_idx).current_indent_level == block.current_indent_level-1:
+ group_end_idx = sub_idx-1
+ break
+
+ var end_node := $Timeline.get_child(group_end_idx)
+
+ var offset := Vector2(-2*line_width, -icon_panel_height/2)
+ var v_length: float = end_node.global_position.y - rect_position.y + icon_panel_height
+
+ ## Draw vertical line
+ draw_rect(Rect2(
+ rect_position.x - global_position.x + offset.x,
+ rect_position.y - global_position.y + offset.y,
+ line_width,
+ v_length),
+ group_color)
+
+
+ ## Draw line that indicates the dragging position
+ if dragging and get_global_rect().has_point(get_global_mouse_position()):
+ var height: int = 0
+ if drag_to_position == %Timeline.get_child_count():
+ height = %Timeline.get_child(-1).global_position.y+%Timeline.get_child(-1).size.y-global_position.y-(line_width/2.0)
+ else:
+ height = %Timeline.get_child(drag_to_position).global_position.y-global_position.y-(line_width/2.0)
+
+ draw_line(Vector2(0, height), Vector2(size.x*0.9, height), get_theme_color("accent_color", "Editor"), line_width*.3)
+
+#endregion
+
+
+#region SPACE BELOW
+################################################################################
+
+func add_extra_scroll_area_to_timeline(fake_arg:Variant=null) -> void:
+ if %Timeline.get_children().size() > 4:
+ %Timeline.custom_minimum_size.y = 0
+ %Timeline.size.y = 0
+ if %Timeline.size.y + 200 > %TimelineArea.size.y:
+ %Timeline.custom_minimum_size = Vector2(0, %Timeline.size.y + 200)
+
+#endregion
--- /dev/null
+@tool
+extends Container
+
+## Visual mode of the timeline editor.
+
+
+################## EDITOR NODES ################################################
+################################################################################
+var TimelineUndoRedo := UndoRedo.new()
+@onready var timeline_editor := get_parent().get_parent()
+var event_node
+var sidebar_collapsed := false
+
+################## SIGNALS #####################################################
+################################################################################
+signal selection_updated
+signal batch_loaded
+signal timeline_loaded
+
+
+################## TIMELINE LOADING ############################################
+################################################################################
+var _batches := []
+var _building_timeline := false
+var _timeline_changed_while_loading := false
+var _initialized := false
+
+################## TIMELINE EVENT MANAGEMENT ###################################
+################################################################################
+var selected_items: Array = []
+var drag_allowed := false
+
+
+#region CREATE/SAVE/LOAD
+################################################################################
+
+func something_changed() -> void:
+ timeline_editor.current_resource_state = DialogicEditor.ResourceStates.UNSAVED
+
+
+func save_timeline() -> void:
+ if !is_inside_tree():
+ return
+
+ # return if resource is unchanged
+ if timeline_editor.current_resource_state != DialogicEditor.ResourceStates.UNSAVED:
+ return
+
+ # create a list of text versions of all the events with the right indent
+ var new_events := []
+ var indent := 0
+ for event in %Timeline.get_children():
+ if 'event_name' in event.resource:
+ event.resource.update_text_version()
+ new_events.append(event.resource)
+
+ if !timeline_editor.current_resource:
+ return
+
+ timeline_editor.current_resource.events = new_events
+ timeline_editor.current_resource.events_processed = true
+ var error: int = ResourceSaver.save(timeline_editor.current_resource, timeline_editor.current_resource.resource_path)
+
+ if error != OK:
+ print('[Dialogic] Saving error: ', error)
+
+ timeline_editor.current_resource.set_meta("unsaved", false)
+ timeline_editor.current_resource_state = DialogicEditor.ResourceStates.SAVED
+ DialogicResourceUtil.update_directory('dtl')
+
+
+func _notification(what:int) -> void:
+ if what == NOTIFICATION_WM_CLOSE_REQUEST:
+ save_timeline()
+
+
+func load_timeline(resource:DialogicTimeline) -> void:
+ if _building_timeline:
+ _timeline_changed_while_loading = true
+ await batch_loaded
+ _timeline_changed_while_loading = false
+ _building_timeline = false
+
+ clear_timeline_nodes()
+
+ if timeline_editor.current_resource.events.size() == 0:
+ pass
+ else:
+ await timeline_editor.current_resource.process()
+
+ if timeline_editor.current_resource.events.size() == 0:
+ return
+
+ var data := resource.events
+ var page := 1
+ var batch_size := 10
+ _batches = []
+ _building_timeline = true
+ while batch_events(data, batch_size, page).size() != 0:
+ _batches.append(batch_events(data, batch_size, page))
+ page += 1
+ batch_loaded.emit()
+ # Reset the scroll position
+ %TimelineArea.scroll_vertical = 0
+
+
+func batch_events(array: Array, size: int, batch_number: int) -> Array:
+ return array.slice((batch_number - 1) * size, batch_number * size)
+
+
+# a list of all events like choice and condition events (so they get connected to their end events)
+var opener_events_stack := []
+
+func load_batch(data:Array) -> void:
+ # Don't try to cast it to Array immedietly, as the item may have become null and will throw a useless error
+ var current_batch = _batches.pop_front()
+ if current_batch:
+ var current_batch_items: Array = current_batch
+ for i in current_batch_items:
+ if i is DialogicEndBranchEvent:
+ create_end_branch_event(%Timeline.get_child_count(), opener_events_stack.pop_back())
+ else:
+ var piece := add_event_node(i, %Timeline.get_child_count())
+ if i.can_contain_events:
+ opener_events_stack.push_back(piece)
+ batch_loaded.emit()
+
+
+func _on_batch_loaded() -> void:
+ if _timeline_changed_while_loading:
+ return
+ if _batches.size() > 0:
+ indent_events()
+ await get_tree().process_frame
+ load_batch(_batches)
+ return
+
+ if opener_events_stack:
+ for ev in opener_events_stack:
+ if is_instance_valid(ev):
+ create_end_branch_event(%Timeline.get_child_count(), ev)
+
+ opener_events_stack = []
+ indent_events()
+ update_content_list()
+ _building_timeline = false
+
+
+func clear_timeline_nodes() -> void:
+ deselect_all_items()
+ for event in %Timeline.get_children():
+ event.free()
+#endregion
+
+
+#region SETUP
+################################################################################
+
+func _ready() -> void:
+ event_node = load("res://addons/dialogic/Editor/Events/EventBlock/event_block.tscn")
+
+ batch_loaded.connect(_on_batch_loaded)
+
+ await find_parent('EditorView').ready
+ timeline_editor.editors_manager.sidebar.content_item_activated.connect(_on_content_item_clicked)
+ %Timeline.child_order_changed.connect(update_content_list)
+
+ var editor_scale := DialogicUtil.get_editor_scale()
+ %RightSidebar.size.x = DialogicUtil.get_editor_setting("dialogic/editor/right_sidebar_width", 200 * editor_scale)
+ $View.split_offset = -DialogicUtil.get_editor_setting("dialogic/editor/right_sidebar_width", 200 * editor_scale)
+ sidebar_collapsed = DialogicUtil.get_editor_setting("dialogic/editor/right_sidebar_collapsed", false)
+
+ load_event_buttons()
+ _on_right_sidebar_resized()
+ _initialized = true
+
+
+func load_event_buttons() -> void:
+ sidebar_collapsed = DialogicUtil.get_editor_setting("dialogic/editor/right_sidebar_collapsed", false)
+
+ # Clear previous event buttons
+ for child in %RightSidebar.get_child(0).get_children():
+
+ if child is FlowContainer:
+
+ for button in child.get_children():
+ button.queue_free()
+
+
+ for child in %RightSidebar.get_child(0).get_children():
+ child.get_parent().remove_child(child)
+ child.queue_free()
+
+ # Event buttons
+ var button_scene := load("res://addons/dialogic/Editor/TimelineEditor/VisualEditor/AddEventButton.tscn")
+
+ var scripts := DialogicResourceUtil.get_event_cache()
+ var hidden_buttons :Array = DialogicUtil.get_editor_setting('hidden_event_buttons', [])
+ var sections := {}
+
+ for event_script in scripts:
+ var event_resource: Variant
+
+ if typeof(event_script) == TYPE_STRING:
+ event_resource = load(event_script).new()
+ else:
+ event_resource = event_script
+
+ if event_resource.disable_editor_button == true:
+ continue
+
+ if event_resource.event_name in hidden_buttons:
+ continue
+
+ var button: Button = button_scene.instantiate()
+ button.resource = event_resource
+ button.visible_name = event_resource.event_name
+ button.event_icon = event_resource._get_icon()
+ button.set_color(event_resource.event_color)
+ button.dialogic_color_name = event_resource.dialogic_color_name
+ button.event_sorting_index = event_resource.event_sorting_index
+
+ button.button_up.connect(_add_event_button_pressed.bind(event_resource))
+
+ if !event_resource.event_category in sections:
+ var section := VBoxContainer.new()
+ section.name = event_resource.event_category
+
+ var section_header := HBoxContainer.new()
+ section_header.add_child(Label.new())
+ section_header.get_child(0).text = event_resource.event_category
+ section_header.get_child(0).size_flags_horizontal = SIZE_SHRINK_BEGIN
+ section_header.get_child(0).theme_type_variation = "DialogicSection"
+ section_header.add_child(HSeparator.new())
+ section_header.get_child(1).size_flags_horizontal = SIZE_EXPAND_FILL
+ section.add_child(section_header)
+
+ var button_container := FlowContainer.new()
+ section.add_child(button_container)
+
+ sections[event_resource.event_category] = button_container
+ %RightSidebar.get_child(0).add_child(section, true)
+
+ sections[event_resource.event_category].add_child(button)
+ button.toggle_name(!sidebar_collapsed)
+
+ # Sort event button
+ while event_resource.event_sorting_index < sections[event_resource.event_category].get_child(max(0, button.get_index()-1)).resource.event_sorting_index:
+ sections[event_resource.event_category].move_child(button, button.get_index()-1)
+
+ # Sort event sections
+ var sections_order: Array = DialogicUtil.get_editor_setting('event_section_order',
+ ['Main', 'Flow', 'Logic', 'Audio', 'Visual','Other', 'Helper'])
+
+ sections_order.reverse()
+ for section_name in sections_order:
+ if %RightSidebar.get_child(0).has_node(section_name):
+ %RightSidebar.get_child(0).move_child(%RightSidebar.get_child(0).get_node(section_name), 0)
+
+ # Resize RightSidebar
+ %RightSidebar.custom_minimum_size.x = 50 * DialogicUtil.get_editor_scale()
+
+ _on_right_sidebar_resized()
+#endregion
+
+
+#region CONTENT LIST
+################################################################################
+
+func _on_content_item_clicked(label:String) -> void:
+ if label == "~ Top":
+ %TimelineArea.scroll_vertical = 0
+ return
+
+ for event in %Timeline.get_children():
+ if 'event_name' in event.resource and event.resource is DialogicLabelEvent:
+ if event.resource.name == label:
+ scroll_to_piece(event.get_index())
+ return
+
+
+func update_content_list() -> void:
+ if not is_inside_tree():
+ return
+
+ var labels: PackedStringArray = []
+
+ for event in %Timeline.get_children():
+
+ if 'event_name' in event.resource and event.resource is DialogicLabelEvent:
+ labels.append(event.resource.name)
+
+ timeline_editor.editors_manager.sidebar.update_content_list(labels)
+
+
+#endregion
+
+
+#region DRAG & DROP + DRAGGING EVENTS
+#################################################################################
+
+# SIGNAL handles input on the events mainly for selection and moving events
+func _on_event_block_gui_input(event: InputEvent, item: Node) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
+ if event.is_pressed():
+ if len(selected_items) > 1 and item in selected_items and !Input.is_key_pressed(KEY_CTRL):
+ pass
+ elif not _is_item_selected(item) and not len(selected_items) > 1:
+ select_item(item)
+ elif len(selected_items) > 1 or Input.is_key_pressed(KEY_CTRL):
+ select_item(item)
+
+ drag_allowed = true
+
+ if event.is_released() and not %TimelineArea.dragging and not Input.is_key_pressed(KEY_SHIFT):
+ if len(selected_items) > 1 and item in selected_items and not Input.is_key_pressed(KEY_CTRL):
+ deselect_all_items()
+ select_item(item)
+
+ if len(selected_items) > 0 and event is InputEventMouseMotion:
+ if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
+ if !%TimelineArea.dragging and !get_viewport().gui_is_dragging() and drag_allowed:
+ sort_selection()
+ %TimelineArea.start_dragging(%TimelineArea.DragTypes.EXISTING_EVENTS, selected_items)
+
+
+## Activated by TimelineArea drag_completed
+func _on_timeline_area_drag_completed(type:int, index:int, data:Variant) -> void:
+ if type == %TimelineArea.DragTypes.NEW_EVENT:
+ var resource: DialogicEvent = data.duplicate()
+ resource._load_custom_defaults()
+
+ add_event_undoable(resource, index)
+
+ elif type == %TimelineArea.DragTypes.EXISTING_EVENTS:
+ if not (len(data) == 1 and data[0].get_index()+1 == index):
+ move_blocks_to_index(data, index)
+
+ await get_tree().process_frame
+ something_changed()
+ scroll_to_piece(index)
+ indent_events()
+#endregion
+
+
+#region CREATING THE TIMELINE
+################################################################################
+# Adding an event to the timeline
+func add_event_node(event_resource:DialogicEvent, at_index:int = -1, auto_select: bool = false, indent: bool = false) -> Control:
+ if event_resource is DialogicEndBranchEvent:
+ return create_end_branch_event(at_index, %Timeline.get_child(0))
+
+ if event_resource['event_node_ready'] == false:
+ if event_resource['event_node_as_text'] != "":
+ event_resource._load_from_string(event_resource['event_node_as_text'])
+
+ var block: Control = event_node.instantiate()
+ block.resource = event_resource
+ event_resource.editor_node = block
+ event_resource._enter_visual_editor(timeline_editor)
+ block.content_changed.connect(something_changed)
+
+ if event_resource.event_name == "Label":
+ block.content_changed.connect(update_content_list)
+ if at_index == -1:
+ if len(selected_items) != 0:
+ selected_items[0].add_sibling(block)
+ else:
+ %Timeline.add_child(block)
+ else:
+ %Timeline.add_child(block)
+ %Timeline.move_child(block, at_index)
+
+ block.gui_input.connect(_on_event_block_gui_input.bind(block))
+
+ # Building editing part
+ block.build_editor(true, event_resource.expand_by_default)
+
+ if auto_select:
+ select_item(block, false)
+
+ # Indent on create
+ if indent:
+ indent_events()
+
+ return block
+
+
+func create_end_branch_event(at_index:int, parent_node:Node) -> Node:
+ var end_branch_event: Control = load("res://addons/dialogic/Editor/Events/BranchEnd.tscn").instantiate()
+ end_branch_event.resource = DialogicEndBranchEvent.new()
+ end_branch_event.gui_input.connect(_on_event_block_gui_input.bind(end_branch_event))
+ parent_node.end_node = end_branch_event
+ end_branch_event.parent_node = parent_node
+ end_branch_event.add_end_control(parent_node.resource.get_end_branch_control())
+ %Timeline.add_child(end_branch_event)
+ %Timeline.move_child(end_branch_event, at_index)
+ return end_branch_event
+
+
+# combination of the above that establishes the correct connection between the event and it's end branch
+func add_event_with_end_branch(resource, at_index:int=-1, auto_select:bool = false, indent:bool = false) -> void:
+ var event := add_event_node(resource, at_index, auto_select, indent)
+ create_end_branch_event(at_index+1, event)
+
+
+## Adds an event (either single nodes or with end branches) to the timeline with UndoRedo support
+func add_event_undoable(event_resource: DialogicEvent, at_index: int = -1) -> void:
+ TimelineUndoRedo.create_action("[D] Add "+event_resource.event_name+" event.")
+ if event_resource.can_contain_events:
+ TimelineUndoRedo.add_do_method(add_event_with_end_branch.bind(event_resource, at_index, true, true))
+ TimelineUndoRedo.add_undo_method(delete_events_at_index.bind(at_index, 2))
+ else:
+ TimelineUndoRedo.add_do_method(add_event_node.bind(event_resource, at_index, true, true))
+ TimelineUndoRedo.add_undo_method(delete_events_at_index.bind(at_index, 1))
+ TimelineUndoRedo.commit_action()
+#endregion
+
+
+#region DELETING, COPY, PASTE
+################################################################################
+
+## Lists the given events (as text) based on their indexes.
+## This is used to store info for undo/redo.
+## Based on the action you might want to include END_BRANCHES or not (see EndBranchMode)
+func get_events_indexed(events:Array) -> Dictionary:
+ var indexed_dict := {}
+ for event in events:
+ # do not collect selected end branches (e.g. on delete, copy, etc.)
+ if event.resource is DialogicEndBranchEvent:
+ continue
+
+ indexed_dict[event.get_index()] = event.resource._store_as_string()
+
+ # store an end branch if it is selected or connected to a selected event
+ if 'end_node' in event and event.end_node:
+ event = event.end_node
+ indexed_dict[event.get_index()] = event.resource._store_as_string()
+ elif event.resource is DialogicEndBranchEvent:
+ if event.parent_node in events: # add local index
+ indexed_dict[event.get_index()] += str(events.find(event.parent_node))
+ else: # add global index
+ indexed_dict[event.get_index()] += '#'+str(event.parent_node.get_index())
+ return indexed_dict
+
+
+## Returns an indexed dictionary of [amount] events at [index]
+func get_events_at_index_indexed(index:int, amount:int) -> Dictionary:
+ var events := []
+
+ for i in range(amount):
+ events.append(%Timeline.get_child(index+i))
+
+ return get_events_indexed(events)
+
+
+## Selects events based on an indexed dictionary
+func select_events_indexed(indexed_events:Dictionary) -> void:
+ selected_items = []
+ for event_index in indexed_events.keys():
+ selected_items.append(%Timeline.get_child(event_index))
+
+
+## Adds events based on an indexed dictionary
+func add_events_indexed(indexed_events:Dictionary) -> void:
+ # sort the dictionaries indexes just in case
+ var indexes := indexed_events.keys()
+ indexes.sort()
+
+ var events := []
+ for event_idx in indexes:
+ # first get a new resource from the text version
+ var event_resource :DialogicEvent
+ for i in DialogicResourceUtil.get_event_cache():
+ if i._test_event_string(indexed_events[event_idx]):
+ event_resource = i.duplicate()
+ break
+
+ event_resource._load_from_string(indexed_events[event_idx])
+
+ # now create the visual block.
+ deselect_all_items()
+ if event_resource is DialogicEndBranchEvent:
+ var idx: String = indexed_events[event_idx].trim_prefix('<<END BRANCH>>')
+ if idx.begins_with('#'): # a global index
+ events.append(create_end_branch_event(%Timeline.get_child_count(), %Timeline.get_child(int(idx.trim_prefix('#')))))
+ else: # a local index (index in the added events list)
+ events.append(create_end_branch_event(%Timeline.get_child_count(), events[int(idx)]))
+ %Timeline.move_child(events[-1], event_idx)
+ else:
+ events.append(add_event_node(event_resource))
+ %Timeline.move_child(events[-1], event_idx)
+
+ selected_items = events
+ visual_update_selection()
+ indent_events()
+ something_changed()
+
+
+## Deletes events based on an indexed dictionary
+func delete_events_indexed(indexed_events:Dictionary) -> void:
+ if indexed_events.is_empty():
+ return
+
+ var idx_shift := 0
+ for idx in indexed_events:
+ if 'end_node' in %Timeline.get_child(idx-idx_shift) and %Timeline.get_child(idx-idx_shift).end_node != null and is_instance_valid(%Timeline.get_child(idx-idx_shift).end_node):
+ %Timeline.get_child(idx-idx_shift).end_node.parent_node = null
+ if %Timeline.get_child(idx-idx_shift) != null and is_instance_valid(%Timeline.get_child(idx-idx_shift)):
+ if %Timeline.get_child(idx-idx_shift) in selected_items:
+ selected_items.erase(%Timeline.get_child(idx-idx_shift))
+ %Timeline.get_child(idx-idx_shift).queue_free()
+ %Timeline.get_child(idx-idx_shift).get_parent().remove_child(%Timeline.get_child(idx-idx_shift))
+ idx_shift += 1
+
+ indent_events()
+ something_changed()
+
+
+func delete_selected_events() -> void:
+ # try to find which item to select afterwards
+ var next_node := %Timeline.get_child(mini(%Timeline.get_child_count() - 1, selected_items[-1].get_index() + 1))
+ if _is_item_selected(next_node):
+ next_node = null
+
+ delete_events_indexed(get_events_indexed(selected_items))
+
+ # select next
+ if next_node != null:
+ select_item(next_node, false)
+ elif %Timeline.get_child_count() > 0:
+ next_node = %Timeline.get_child(max(0, %Timeline.get_child_count() - 1))
+ select_item(next_node, false)
+ else:
+ deselect_all_items()
+
+
+func cut_events_indexed(indexed_events:Dictionary) -> void:
+ select_events_indexed(indexed_events)
+ copy_selected_events()
+ delete_events_indexed(indexed_events)
+
+
+func copy_selected_events() -> void:
+ if len(selected_items) == 0:
+ return
+
+ var event_copy_array := []
+ for item in selected_items:
+ event_copy_array.append(item.resource._store_as_string())
+ if item.resource is DialogicEndBranchEvent:
+ if item.parent_node in selected_items: # add local index
+ event_copy_array[-1] += str(selected_items.find(item.parent_node))
+ else: # add global index
+ event_copy_array[-1] += '#'+str(item.parent_node.get_index())
+ DisplayServer.clipboard_set(var_to_str({
+ "events":event_copy_array,
+ "project_name": ProjectSettings.get_setting("application/config/name")
+ }))
+
+
+func get_clipboard_data() -> Array:
+ var clipboard_parse: Variant = str_to_var(DisplayServer.clipboard_get())
+
+ if clipboard_parse is Dictionary:
+ if clipboard_parse.has("project_name"):
+ if clipboard_parse.project_name != ProjectSettings.get_setting("application/config/name"):
+ print("[Dialogic] Be careful when copying from another project!")
+ if clipboard_parse.has('events'):
+ return clipboard_parse.events
+ return []
+
+
+func add_events_at_index(event_list:Array, at_index:int) -> void:
+ var new_indexed_events := {}
+
+ for i in range(len(event_list)):
+ new_indexed_events[at_index+i] = event_list[i]
+
+ add_events_indexed(new_indexed_events)
+
+
+func delete_events_at_index(at_index:int, amount:int = 1)-> void:
+ var new_indexed_events := {}
+ # delete_events_indexed actually only needs the keys, so we give trash as values
+ for i in range(amount):
+ new_indexed_events[at_index+i] = ""
+ delete_events_indexed(new_indexed_events)
+ indent_events()
+
+#endregion
+
+
+#region BLOCK SELECTION
+################################################################################
+
+func _is_item_selected(item: Node) -> bool:
+ return item in selected_items
+
+
+func select_item(item: Node, multi_possible:bool = true) -> void:
+ if item == null:
+ return
+
+ if Input.is_key_pressed(KEY_CTRL) and multi_possible:
+ # deselect the item if it is selected
+ if _is_item_selected(item):
+ selected_items.erase(item)
+ else:
+ selected_items.append(item)
+ elif Input.is_key_pressed(KEY_SHIFT) and multi_possible:
+ if len(selected_items) == 0:
+ selected_items = [item]
+ else:
+ var index: int = selected_items[-1].get_index()
+ var goal_idx := item.get_index()
+ while true:
+ if index < goal_idx: index += 1
+ else: index -= 1
+ if not %Timeline.get_child(index) in selected_items:
+ selected_items.append(%Timeline.get_child(index))
+
+ if index == goal_idx:
+ break
+ else:
+ if len(selected_items) == 1:
+ if _is_item_selected(item):
+ selected_items.erase(item)
+ else:
+ selected_items = [item]
+ else:
+ selected_items = [item]
+
+ sort_selection()
+ visual_update_selection()
+
+
+# checks all the events and sets their styles (selected/deselected)
+func visual_update_selection() -> void:
+ for item in %Timeline.get_children():
+ item.visual_deselect()
+ if 'end_node' in item and item.end_node != null:
+ item.end_node.unhighlight()
+ for item in selected_items:
+ item.visual_select()
+ if 'end_node' in item and item.end_node != null:
+ item.end_node.highlight()
+ %TimelineArea.queue_redraw()
+
+
+## Sorts the selection using 'custom_sort_selection'
+func sort_selection() -> void:
+ selected_items.sort_custom(custom_sort_selection)
+
+
+## Compares two event blocks based on their position in the timeline
+func custom_sort_selection(item1, item2) -> bool:
+ return item1.get_index() < item2.get_index()
+
+
+func select_all_items() -> void:
+ selected_items = []
+ for event in %Timeline.get_children():
+ selected_items.append(event)
+ visual_update_selection()
+
+
+func deselect_all_items() -> void:
+ selected_items = []
+ visual_update_selection()
+#endregion
+
+
+#region CREATING NEW EVENTS USING THE BUTTONS
+################################################################################
+
+# Event Creation signal for buttons
+# If force_resource is true, the event will be added with the actual resource
+func _add_event_button_pressed(event_resource:DialogicEvent, force_resource := false):
+ if %TimelineArea.get_global_rect().has_point(get_global_mouse_position()) and !force_resource:
+ return
+
+ var at_index := -1
+ if selected_items:
+ at_index = selected_items[-1].get_index()+1
+ else:
+ at_index = %Timeline.get_child_count()
+
+ var resource: DialogicEvent = null
+ if force_resource:
+ resource = event_resource
+ else:
+ resource = event_resource.duplicate()
+ resource._load_custom_defaults()
+
+ resource.created_by_button = true
+
+ add_event_undoable(resource, at_index)
+
+ resource.created_by_button = false
+
+ something_changed()
+ scroll_to_piece(at_index)
+ indent_events()
+#endregion
+
+
+#region BLOCK GETTERS
+################################################################################
+
+func get_block_above(block:Node) -> Node:
+ if block.get_index() > 0:
+ return %Timeline.get_child(block.get_index() - 1)
+ return null
+
+
+func get_block_below(block:Node) -> Node:
+ if block.get_index() < %Timeline.get_child_count() - 1:
+ return %Timeline.get_child(block.get_index() + 1)
+ return null
+#endregion
+
+
+#region BLOCK MOVEMENT
+################################################################################
+
+
+func move_blocks_to_index(blocks:Array, index:int):
+ # the amount of events that were BEFORE the new index (thus shifting the index)
+ var index_shift := 0
+ for event in blocks:
+ if event.resource is DialogicEndBranchEvent:
+ if !event.parent_node in blocks:
+ if index <= event.parent_node.get_index():
+ return
+ if "end_node" in event and event.end_node:
+ if !event.end_node in blocks:
+ if event.end_node.get_index() == event.get_index()+1:
+ blocks.append(event.end_node)
+ else:
+ return
+ index_shift += int(event.get_index() < index)
+
+ var do_indexes := {}
+ var undo_indexes := {}
+
+ var event_count := 0
+ for event in blocks:
+ do_indexes[event.get_index()] = index + event_count
+ undo_indexes[index -index_shift+event_count] = event.get_index()+index_shift*int(index < event.get_index())#+int((index -index_shift+event_count) < event.get_index())
+ event_count += 1
+
+ # complex check to avoid tangling conditions & choices
+ for idx in do_indexes:
+ var event := %Timeline.get_child(idx)
+ if !event.resource is DialogicEndBranchEvent and !event.resource.can_contain_events:
+ continue
+
+ if event.resource is DialogicEndBranchEvent:
+ if !event.parent_node or event.parent_node.get_index() in do_indexes:
+ continue
+ elif event.resource.can_contain_events:
+ if !event.end_node or event.end_node.get_index() in do_indexes:
+ continue
+
+ var check_from := 0
+ var check_to := 0
+
+ if event.resource is DialogicEndBranchEvent:
+ check_from = event.parent_node.get_index()+1
+ check_to = index
+ else:
+ check_from = index
+ check_to = event.end_node.get_index()
+
+ for c_idx in range(check_from, check_to):
+ if c_idx in do_indexes:
+ continue
+ var c_event := %Timeline.get_child(c_idx)
+ if c_event.resource is DialogicEndBranchEvent and c_event.parent_node.get_index() < check_from:
+ return
+ if c_event.resource.can_contain_events and c_event.end_node.get_index() > check_to:
+ return
+
+ TimelineUndoRedo.create_action('[D] Move events.')
+ TimelineUndoRedo.add_do_method(move_events_by_indexes.bind(do_indexes))
+ TimelineUndoRedo.add_undo_method(move_events_by_indexes.bind(undo_indexes))
+ TimelineUndoRedo.commit_action()
+
+
+func move_events_by_indexes(index_dict:Dictionary) -> void:
+ var sorted_indexes := index_dict.keys()
+ sorted_indexes.sort()
+
+ var evts := {}
+ var count := 0
+ for idx in sorted_indexes:
+ evts[idx] =%Timeline.get_child(idx-count)
+ %Timeline.remove_child(%Timeline.get_child(idx-count))
+ count += 1
+ if idx < index_dict[idx]:
+ index_dict[idx] -= len(sorted_indexes.filter(func(x):return x<=index_dict[idx]-count-1))
+
+ for idx in sorted_indexes:
+ %Timeline.add_child(evts[idx])
+ %Timeline.move_child(evts[idx], index_dict[idx])
+
+ indent_events()
+ visual_update_selection()
+ something_changed()
+
+
+func offset_blocks_by_index(blocks:Array, offset:int):
+ var do_indexes := {}
+ var undo_indexes := {}
+
+ for event in blocks:
+ if event.resource is DialogicEndBranchEvent:
+ if !event.parent_node in blocks:
+ if event.get_index()+offset+int(offset>0) <= event.parent_node.get_index():
+ continue
+ if "end_node" in event and event.end_node:
+ if !event.end_node in blocks:
+ if event.get_index()+offset+int(offset>0) > event.end_node.get_index():
+ if event.end_node.get_index() == event.get_index()+1:
+ blocks.append(event.end_node)
+ else:
+ return
+ do_indexes[event.get_index()] = event.get_index()+offset+int(offset>0)
+ undo_indexes[event.get_index()+offset] = event.get_index()+int(offset<0)
+
+
+ TimelineUndoRedo.create_action("[D] Move events.")
+ TimelineUndoRedo.add_do_method(move_events_by_indexes.bind(do_indexes))
+ TimelineUndoRedo.add_undo_method(move_events_by_indexes.bind(undo_indexes))
+
+ TimelineUndoRedo.commit_action()
+#endregion
+
+
+#region VISIBILITY/VISUALS
+################################################################################
+
+func scroll_to_piece(piece_index:int) -> void:
+ await get_tree().process_frame
+ var height: float = %Timeline.get_child(min(piece_index, %Timeline.get_child_count()-1)).position.y
+ if height < %TimelineArea.scroll_vertical or height > %TimelineArea.scroll_vertical+%TimelineArea.size.y:
+ %TimelineArea.scroll_vertical = height
+
+
+func indent_events() -> void:
+ var indent: int = 0
+ var event_list: Array = %Timeline.get_children()
+
+ if event_list.size() < 2:
+ return
+
+ var currently_hidden := false
+ var hidden_count := 0
+ var hidden_until: Control = null
+
+ # will be applied to the indent after the current event
+ var delayed_indent: int = 0
+
+ for block in event_list:
+ if (not "resource" in block):
+ continue
+
+ if (not currently_hidden) and block.resource.can_contain_events and block.end_node and block.collapsed:
+ currently_hidden = true
+ hidden_until = block.end_node
+ hidden_count = 0
+ elif currently_hidden and block == hidden_until:
+ block.update_hidden_events_indicator(hidden_count)
+ currently_hidden = false
+ hidden_until = null
+ elif currently_hidden:
+ block.hide()
+ hidden_count += 1
+ else:
+ block.show()
+ if block.resource is DialogicEndBranchEvent:
+ block.update_hidden_events_indicator(0)
+
+ delayed_indent = 0
+
+ if block.resource.can_contain_events:
+ delayed_indent = 1
+
+ if block.resource.wants_to_group:
+ indent += 1
+
+ elif block.resource is DialogicEndBranchEvent:
+ block.parent_node_changed()
+ delayed_indent -= 1
+ if block.parent_node.resource.wants_to_group:
+ delayed_indent -= 1
+
+ if indent >= 0:
+ block.set_indent(indent)
+ else:
+ block.set_indent(0)
+ indent += delayed_indent
+
+ await get_tree().process_frame
+ await get_tree().process_frame
+ %TimelineArea.queue_redraw()
+
+
+#region SPECIAL BLOCK OPERATIONS
+################################################################################
+
+func _on_event_popup_menu_index_pressed(index:int) -> void:
+ var item: Control = %EventPopupMenu.current_event
+ if index == 0:
+ if not item in selected_items:
+ selected_items = [item]
+ duplicate_selected()
+ elif index == 2:
+ if not item.resource.help_page_path.is_empty():
+ OS.shell_open(item.resource.help_page_path)
+ elif index == 3:
+ find_parent('EditorView').plugin_reference.get_editor_interface().set_main_screen_editor('Script')
+ find_parent('EditorView').plugin_reference.get_editor_interface().edit_script(item.resource.get_script(), 1, 1)
+ elif index == 5 or index == 6:
+ if index == 5:
+ offset_blocks_by_index(selected_items, -1)
+ else:
+ offset_blocks_by_index(selected_items, +1)
+
+ elif index == 8:
+ var events_indexed : Dictionary
+ if item in selected_items:
+ events_indexed = get_events_indexed(selected_items)
+ else:
+ events_indexed = get_events_indexed([item])
+ TimelineUndoRedo.create_action("[D] Deleting 1 event.")
+ TimelineUndoRedo.add_do_method(delete_events_indexed.bind(events_indexed))
+ TimelineUndoRedo.add_undo_method(add_events_indexed.bind(events_indexed))
+ TimelineUndoRedo.commit_action()
+ indent_events()
+
+
+func _on_right_sidebar_resized() -> void:
+ var _scale := DialogicUtil.get_editor_scale()
+
+ if %RightSidebar.size.x < 160 * _scale and (not sidebar_collapsed or not _initialized):
+ sidebar_collapsed = true
+
+ for section in %RightSidebar.get_node('EventContainer').get_children():
+
+ for con in section.get_children():
+
+ if con.get_child_count() == 0:
+ continue
+
+ if con.get_child(0) is Label:
+ con.get_child(0).hide()
+
+ elif con.get_child(0) is Button:
+
+ for button in con.get_children():
+ button.toggle_name(false)
+
+
+ elif %RightSidebar.size.x > 160 * _scale and (sidebar_collapsed or not _initialized):
+ sidebar_collapsed = false
+
+ for section in %RightSidebar.get_node('EventContainer').get_children():
+
+ for con in section.get_children():
+
+ if con.get_child_count() == 0:
+ continue
+
+ if con.get_child(0) is Label:
+ con.get_child(0).show()
+
+ elif con.get_child(0) is Button:
+ for button in con.get_children():
+ button.toggle_name(true)
+
+ if _initialized:
+ DialogicUtil.set_editor_setting("dialogic/editor/right_sidebar_width", %RightSidebar.size.x)
+ DialogicUtil.set_editor_setting("dialogic/editor/right_sidebar_collapsed", sidebar_collapsed)
+
+#endregion
+
+
+#region SHORTCUTS
+################################################################################
+
+func duplicate_selected() -> void:
+ if len(selected_items) > 0:
+ var events := get_events_indexed(selected_items).values()
+ var at_index: int = selected_items[-1].get_index()+1
+ TimelineUndoRedo.create_action("[D] Duplicate "+str(len(events))+" event(s).")
+ TimelineUndoRedo.add_do_method(add_events_at_index.bind(events, at_index))
+ TimelineUndoRedo.add_undo_method(delete_events_at_index.bind(at_index, len(events)))
+ TimelineUndoRedo.commit_action()
+
+
+func _input(event:InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed == false:
+ drag_allowed = false
+
+ # we protect this with is_visible_in_tree to not
+ # invoke a shortcut by accident
+ if !((event is InputEventKey or !event is InputEventWithModifiers) and is_visible_in_tree()):
+ return
+
+
+ if "pressed" in event:
+ if !event.pressed:
+ return
+
+
+ ## Some shortcuts should always work
+ match event.as_text():
+ "Ctrl+T": # Add text event
+ _add_event_button_pressed(DialogicTextEvent.new(), true)
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+Shift+T", "Ctrl+Alt+T", "Ctrl+Option+T": # Add text event with current or previous character
+ get_viewport().set_input_as_handled()
+ var ev := DialogicTextEvent.new()
+ ev.character = get_previous_character(event.as_text() == "Ctrl+Alt+T" or event.as_text() == "Ctrl+Option+T")
+ _add_event_button_pressed(ev, true)
+
+ "Ctrl+E": # Add character join event
+ _add_event_button_pressed(DialogicCharacterEvent.new(), true)
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+Shift+E": # Add character update event
+ var ev := DialogicCharacterEvent.new()
+ ev.action = DialogicCharacterEvent.Actions.UPDATE
+ _add_event_button_pressed(ev, true)
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+Alt+E", "Ctrl+Option+E": # Add character leave event
+ var ev := DialogicCharacterEvent.new()
+ ev.action = DialogicCharacterEvent.Actions.LEAVE
+ _add_event_button_pressed(ev, true)
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+J": # Add jump event
+ _add_event_button_pressed(DialogicJumpEvent.new(), true)
+ get_viewport().set_input_as_handled()
+ "Ctrl+L": # Add label event
+ _add_event_button_pressed(DialogicLabelEvent.new(), true)
+ get_viewport().set_input_as_handled()
+
+ ## Some shortcuts should be disabled when writing text.
+ var focus_owner: Control = get_viewport().gui_get_focus_owner()
+ if focus_owner is TextEdit or focus_owner is LineEdit or (focus_owner is Button and focus_owner.get_parent_control().name == "Spin"):
+ return
+
+ match event.as_text():
+ "Ctrl+Z": # UNDO
+ TimelineUndoRedo.undo()
+ indent_events()
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+Shift+Z", "Ctrl+Y": # REDO
+ TimelineUndoRedo.redo()
+ indent_events()
+ get_viewport().set_input_as_handled()
+
+ "Up": #select previous
+ if (len(selected_items) == 1):
+ var prev := maxi(0, selected_items[0].get_index() - 1)
+ var prev_node := %Timeline.get_child(prev)
+ if (prev_node != selected_items[0]):
+ selected_items = []
+ select_item(prev_node)
+ get_viewport().set_input_as_handled()
+
+ "Down": #select next
+ if (len(selected_items) == 1):
+ var next := mini(%Timeline.get_child_count() - 1, selected_items[0].get_index() + 1)
+ var next_node := %Timeline.get_child(next)
+ if (next_node != selected_items[0]):
+ selected_items = []
+ select_item(next_node)
+ get_viewport().set_input_as_handled()
+
+ "Delete":
+ if (len(selected_items) != 0):
+ var events_indexed := get_events_indexed(selected_items)
+ TimelineUndoRedo.create_action("[D] Deleting "+str(len(selected_items))+" event(s).")
+ TimelineUndoRedo.add_do_method(delete_events_indexed.bind(events_indexed))
+ TimelineUndoRedo.add_undo_method(add_events_indexed.bind(events_indexed))
+ TimelineUndoRedo.commit_action()
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+A": # select all
+ if (len(selected_items) != 0):
+ select_all_items()
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+Shift+A": # deselect all
+ if (len(selected_items) != 0):
+ deselect_all_items()
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+C":
+ select_events_indexed(get_events_indexed(selected_items))
+ copy_selected_events()
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+V":
+ var events_list := get_clipboard_data()
+ var paste_position := 0
+ if selected_items:
+ paste_position = selected_items[-1].get_index()+1
+ else:
+ paste_position = %Timeline.get_child_count()
+ if events_list:
+ TimelineUndoRedo.create_action("[D] Pasting "+str(len(events_list))+" event(s).")
+ TimelineUndoRedo.add_do_method(add_events_at_index.bind(events_list, paste_position))
+ TimelineUndoRedo.add_undo_method(delete_events_at_index.bind(paste_position, len(events_list)))
+ TimelineUndoRedo.commit_action()
+ get_viewport().set_input_as_handled()
+
+
+ "Ctrl+X":
+ var events_indexed := get_events_indexed(selected_items)
+ TimelineUndoRedo.create_action("[D] Cut "+str(len(selected_items))+" event(s).")
+ TimelineUndoRedo.add_do_method(cut_events_indexed.bind(events_indexed))
+ TimelineUndoRedo.add_undo_method(add_events_indexed.bind(events_indexed))
+ TimelineUndoRedo.commit_action()
+ get_viewport().set_input_as_handled()
+
+ "Ctrl+D":
+ duplicate_selected()
+ get_viewport().set_input_as_handled()
+
+ "Alt+Up", "Option+Up":
+ if len(selected_items) > 0:
+ offset_blocks_by_index(selected_items, -1)
+
+ get_viewport().set_input_as_handled()
+
+ "Alt+Down", "Option+Down":
+ if len(selected_items) > 0:
+ offset_blocks_by_index(selected_items, +1)
+
+ get_viewport().set_input_as_handled()
+
+
+func get_previous_character(double_previous := false) -> DialogicCharacter:
+ var character: DialogicCharacter = null
+ var idx: int = %Timeline.get_child_count()
+ if idx == 0:
+ return null
+ if len(selected_items):
+ idx = selected_items[0].get_index()
+ var one_skipped := false
+ idx += 1
+ for i in range(selected_items[0].get_index()+1):
+ idx -= 1
+ if !('resource' in %Timeline.get_child(idx) and 'character' in %Timeline.get_child(idx).resource):
+ continue
+ if %Timeline.get_child(idx).resource.character == null:
+ continue
+ if double_previous:
+ if %Timeline.get_child(idx).resource.character == character:
+ continue
+ if character != null:
+ if one_skipped:
+ one_skipped = false
+ else:
+ character = %Timeline.get_child(idx).resource.character
+ break
+ character = %Timeline.get_child(idx).resource.character
+ else:
+ character = %Timeline.get_child(idx).resource.character
+ break
+ return character
+
+#endregion
+
+#region SEARCH
+################################################################################
+
+var search_results := {}
+func _search_timeline(search_text:String) -> bool:
+ for event in search_results:
+ if is_instance_valid(search_results[event]):
+ search_results[event].set_search_text("")
+ search_results[event].deselect()
+ search_results[event].queue_redraw()
+ search_results.clear()
+
+ for block in %Timeline.get_children():
+ if block.resource is DialogicTextEvent:
+ var text_field: TextEdit = block.get_node("%BodyContent").find_child("Field_Text_Multiline", true, false)
+ text_field.set_search_text(search_text)
+ if text_field.search(search_text, 0, 0, 0).x != -1:
+ search_results[block] = text_field
+ text_field.queue_redraw()
+ set_meta("current_search", search_text)
+ search_navigate(false)
+ return not search_results.is_empty()
+
+
+func _search_navigate_down() -> void:
+ search_navigate(false)
+
+
+func _search_navigate_up() -> void:
+ search_navigate(true)
+
+
+func search_navigate(navigate_up := false) -> void:
+ var search_text: String = get_meta("current_search", "")
+
+ if search_results.is_empty() or %Timeline.get_child_count() == 0:
+ return
+ if selected_items.is_empty():
+ select_item(%Timeline.get_child(0), false)
+
+ while not selected_items[0] in search_results:
+ select_item(%Timeline.get_child(wrapi(selected_items[0].get_index()+1, 0, %Timeline.get_child_count()-1)), false)
+
+ var event: Node = selected_items[0]
+ var counter := 0
+ while true:
+ counter += 1
+ var field: TextEdit = search_results[event]
+ field.queue_redraw()
+ var result := search_text_field(field, search_text, navigate_up)
+ var current_line := field.get_selection_from_line() if field.has_selection() else -1
+ var current_column := field.get_selection_from_column() if field.has_selection() else -1
+ var next_is_in_this_event := false
+ if result.y == -1:
+ next_is_in_this_event = false
+ elif navigate_up:
+ if current_line == -1:
+ current_line = field.get_line_count()-1
+ current_column = field.get_line(current_line).length()
+ next_is_in_this_event = result.x < current_column or result.y < current_line
+ else:
+ next_is_in_this_event = result.x > current_column or result.y > current_line
+
+ if next_is_in_this_event:
+ if not event in selected_items:
+ select_item(event, false)
+ %TimelineArea.ensure_control_visible(event)
+ event._on_ToggleBodyVisibility_toggled(true)
+ field.select(result.y, result.x, result.y, result.x+len(search_text))
+ break
+
+ else:
+ field.deselect()
+ var index := search_results.keys().find(event)
+ event = search_results.keys()[wrapi(index+(-1 if navigate_up else 1), 0, search_results.size())]
+
+ if counter > 5:
+ print("[Dialogic] Search failed.")
+ break
+
+
+func search_text_field(field:TextEdit, search_text := "", navigate_up:= false) -> Vector2i:
+ var search_from_line: int = 0
+ var search_from_column: int = 0
+ if field.has_selection():
+ if navigate_up:
+ search_from_line = field.get_selection_from_line()
+ search_from_column = field.get_selection_from_column()-1
+ if search_from_column == -1:
+ search_from_line -= 1
+ if search_from_line == -1:
+ return Vector2i(-1, -1)
+ search_from_column = field.get_line(search_from_line).length()-1
+ else:
+ search_from_line = field.get_selection_to_line()
+ search_from_column = field.get_selection_to_column()
+ else:
+ if navigate_up:
+ search_from_line = field.get_line_count()-1
+ search_from_column = field.get_line(search_from_line).length()-1
+
+ var search := field.search(search_text, 4 if navigate_up else 0, search_from_line, search_from_column)
+ return search
+
+#endregion
--- /dev/null
+[gd_scene load_steps=10 format=3 uid="uid://ysqbusmy0qma"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/timeline_editor_visual.gd" id="1_8smxc"]
+[ext_resource type="Theme" uid="uid://cqst728xxipcw" path="res://addons/dialogic/Editor/Theme/MainTheme.tres" id="2_x0fhp"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/TimelineArea.gd" id="3_sap1x"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Events/EventBlock/event_right_click_menu.gd" id="4_ugiq6"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_phyjj"]
+content_margin_top = 10.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_plab4"]
+bg_color = Color(0, 0, 0, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dov6v"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 0.365, 0.365, 1)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_detail = 1
+
+[sub_resource type="Image" id="Image_y3447"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_vg181"]
+image = SubResource("Image_y3447")
+
+[node name="TimelineVisualEditor" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+script = ExtResource("1_8smxc")
+
+[node name="View" type="HSplitContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme = ExtResource("2_x0fhp")
+
+[node name="TimelineArea" type="ScrollContainer" parent="View"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxEmpty_phyjj")
+script = ExtResource("3_sap1x")
+
+[node name="Timeline" type="VBoxContainer" parent="View/TimelineArea"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="EventPopupMenu" type="PopupMenu" parent="View/TimelineArea"]
+unique_name_in_owner = true
+size = Vector2i(165, 124)
+theme_override_styles/panel = SubResource("StyleBoxFlat_plab4")
+theme_override_styles/hover = SubResource("StyleBoxFlat_dov6v")
+item_count = 6
+item_0/text = "Documentation"
+item_0/icon = SubResource("ImageTexture_vg181")
+item_0/id = 0
+item_1/text = ""
+item_1/id = -1
+item_1/separator = true
+item_2/text = "Move up"
+item_2/icon = SubResource("ImageTexture_vg181")
+item_2/id = 2
+item_3/text = "Move down"
+item_3/icon = SubResource("ImageTexture_vg181")
+item_3/id = 3
+item_4/text = ""
+item_4/id = -1
+item_4/separator = true
+item_5/text = "Delete"
+item_5/icon = SubResource("ImageTexture_vg181")
+item_5/id = 5
+script = ExtResource("4_ugiq6")
+
+[node name="RightSidebar" type="ScrollContainer" parent="View"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+size_flags_stretch_ratio = 0.2
+horizontal_scroll_mode = 0
+
+[node name="EventContainer" type="VBoxContainer" parent="View/RightSidebar"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.2
+
+[connection signal="drag_completed" from="View/TimelineArea" to="." method="_on_timeline_area_drag_completed"]
+[connection signal="index_pressed" from="View/TimelineArea/EventPopupMenu" to="." method="_on_event_popup_menu_index_pressed"]
+[connection signal="resized" from="View/RightSidebar" to="." method="_on_right_sidebar_resized"]
--- /dev/null
+extends Control
+
+func _ready() -> void:
+ print("[Dialogic] Testing scene was started.")
+ if not ProjectSettings.get_setting('internationalization/locale/test', "").is_empty():
+ print("Testing locale is: ", ProjectSettings.get_setting('internationalization/locale/test'))
+ $PauseIndictator.hide()
+
+ var scene: Node = DialogicUtil.autoload().Styles.load_style(DialogicUtil.get_editor_setting('current_test_style', ''))
+ if not scene is CanvasLayer:
+ if scene is Control:
+ scene.position = get_viewport_rect().size/2.0
+ if scene is Node2D:
+ scene.position = get_viewport_rect().size/2.0
+
+ randomize()
+ var current_timeline: String = DialogicUtil.get_editor_setting("current_timeline_path", "")
+ if not current_timeline:
+ get_tree().quit()
+ DialogicUtil.autoload().start(current_timeline)
+ DialogicUtil.autoload().timeline_ended.connect(get_tree().quit)
+ DialogicUtil.autoload().signal_event.connect(receive_event_signal)
+ DialogicUtil.autoload().text_signal.connect(receive_text_signal)
+
+func receive_event_signal(argument:Variant) -> void:
+ print("[Dialogic] Encountered a signal event: ", argument)
+
+func receive_text_signal(argument:String) -> void:
+ print("[Dialogic] Encountered a signal in text: ", argument)
+
+func _input(event:InputEvent) -> void:
+ if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
+ DialogicUtil.autoload().paused = !DialogicUtil.autoload().paused
+ $PauseIndictator.visible = DialogicUtil.autoload().paused
+
+ if (event is InputEventMouseButton
+ and event.is_pressed()
+ and event.button_index == MOUSE_BUTTON_MIDDLE):
+ var auto_skip: DialogicAutoSkip = DialogicUtil.autoload().Inputs.auto_skip
+ var is_auto_skip_enabled := auto_skip.enabled
+
+ auto_skip.disable_on_unread_text = false
+ auto_skip.enabled = not is_auto_skip_enabled
+
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://ud18ke1g2nw4"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/test_timeline_scene.gd" id="1_bamud"]
+
+[node name="TestTimelineScene" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_bamud")
+
+[node name="PauseIndictator" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -65.0
+offset_top = 7.0
+offset_right = -8.0
+offset_bottom = 33.0
+grow_horizontal = 0
+text = "Paused"
+metadata/_edit_layout_mode = 1
--- /dev/null
+@tool
+extends DialogicEditor
+
+## Editor that holds both the visual and the text timeline editors.
+
+# references
+enum EditorMode {VISUAL, TEXT}
+
+var current_editor_mode := EditorMode.VISUAL
+var play_timeline_button: Button = null
+
+
+## Overwrite. Register to the editor manager in here.
+func _register() -> void:
+ resource_unsaved.connect(_on_resource_unsaved)
+ resource_saved.connect(_on_resource_saved)
+
+ # register editor
+ editors_manager.register_resource_editor('dtl', self)
+ # add timeline button
+ var add_timeline_button: Button = editors_manager.add_icon_button(
+ load("res://addons/dialogic/Editor/Images/Toolbar/add-timeline.svg"),
+ "Add Timeline",
+ self)
+ add_timeline_button.pressed.connect(_on_create_timeline_button_pressed)
+ add_timeline_button.shortcut = Shortcut.new()
+ add_timeline_button.shortcut.events.append(InputEventKey.new())
+ add_timeline_button.shortcut.events[0].keycode = KEY_1
+ add_timeline_button.shortcut.events[0].ctrl_pressed = true
+ # play timeline button
+ play_timeline_button = editors_manager.add_custom_button(
+ "Play Timeline",
+ get_theme_icon("PlayScene", "EditorIcons"),
+ self)
+ play_timeline_button.pressed.connect(play_timeline)
+ play_timeline_button.tooltip_text = "Play the current timeline (CTRL+F5)"
+ if OS.get_name() == "macOS":
+ play_timeline_button.tooltip_text = "Play the current timeline (CTRL+B)"
+
+ %VisualEditor.load_event_buttons()
+
+ current_editor_mode = DialogicUtil.get_editor_setting('timeline_editor_mode', 0)
+
+ match current_editor_mode:
+ EditorMode.VISUAL:
+ %VisualEditor.show()
+ %TextEditor.hide()
+ %SwitchEditorMode.text = "Text Editor"
+ EditorMode.TEXT:
+ %VisualEditor.hide()
+ %TextEditor.show()
+ %SwitchEditorMode.text = "Visual Editor"
+
+ $NoTimelineScreen.show()
+ play_timeline_button.disabled = true
+
+
+func _get_title() -> String:
+ return "Timeline"
+
+
+func _get_icon() -> Texture:
+ return get_theme_icon("TripleBar", "EditorIcons")
+
+
+## If this editor supports editing resources, load them here (overwrite in subclass)
+func _open_resource(resource:Resource) -> void:
+ current_resource = resource
+ current_resource_state = ResourceStates.SAVED
+ match current_editor_mode:
+ EditorMode.VISUAL:
+ %VisualEditor.load_timeline(current_resource)
+ EditorMode.TEXT:
+ %TextEditor.load_timeline(current_resource)
+ $NoTimelineScreen.hide()
+ %TimelineName.text = DialogicResourceUtil.get_unique_identifier(current_resource.resource_path)
+ play_timeline_button.disabled = false
+
+
+## If this editor supports editing resources, save them here (overwrite in subclass)
+func _save() -> void:
+ match current_editor_mode:
+ EditorMode.VISUAL:
+ %VisualEditor.save_timeline()
+ EditorMode.TEXT:
+ %TextEditor.save_timeline()
+
+
+func _input(event: InputEvent) -> void:
+ if event is InputEventKey:
+ var keycode := KEY_F5
+ if OS.get_name() == "macOS":
+ keycode = KEY_B
+ if event.keycode == keycode and event.pressed:
+ if Input.is_key_pressed(KEY_CTRL):
+ play_timeline()
+
+ if event.keycode == KEY_F and event.pressed:
+ if Input.is_key_pressed(KEY_CTRL):
+ if is_ancestor_of(get_viewport().gui_get_focus_owner()):
+ search_timeline()
+
+
+## Method to play the current timeline. Connected to the button in the sidebar.
+func play_timeline() -> void:
+ _save()
+
+ var dialogic_plugin := DialogicUtil.get_dialogic_plugin()
+
+ # Save the current opened timeline
+ DialogicUtil.set_editor_setting('current_timeline_path', current_resource.resource_path)
+
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().play_custom_scene("res://addons/dialogic/Editor/TimelineEditor/test_timeline_scene.tscn")
+
+
+## Method to switch from visual to text editor (and vice versa). Connected to the button in the sidebar.
+func toggle_editor_mode() -> void:
+ match current_editor_mode:
+ EditorMode.VISUAL:
+ current_editor_mode = EditorMode.TEXT
+ %VisualEditor.save_timeline()
+ %VisualEditor.hide()
+ %TextEditor.show()
+ %TextEditor.load_timeline(current_resource)
+ %SwitchEditorMode.text = "Visual Editor"
+ EditorMode.TEXT:
+ current_editor_mode = EditorMode.VISUAL
+ %TextEditor.save_timeline()
+ %TextEditor.hide()
+ %VisualEditor.load_timeline(current_resource)
+ %VisualEditor.show()
+ %SwitchEditorMode.text = "Text Editor"
+ _on_search_text_changed(%Search.text)
+ DialogicUtil.set_editor_setting('timeline_editor_mode', current_editor_mode)
+
+
+func _on_resource_unsaved() -> void:
+ if current_resource:
+ current_resource.set_meta("timeline_not_saved", true)
+
+
+func _on_resource_saved() -> void:
+ if current_resource:
+ current_resource.set_meta("timeline_not_saved", false)
+
+
+func new_timeline(path:String) -> void:
+ _save()
+ var new_timeline := DialogicTimeline.new()
+ new_timeline.resource_path = path
+ new_timeline.set_meta('timeline_not_saved', true)
+ var err := ResourceSaver.save(new_timeline)
+ EditorInterface.get_resource_filesystem().update_file(new_timeline.resource_path)
+ DialogicResourceUtil.update_directory('dtl')
+ editors_manager.edit_resource(new_timeline)
+
+
+func _ready() -> void:
+ $NoTimelineScreen.add_theme_stylebox_override("panel", get_theme_stylebox("Background", "EditorStyles"))
+
+ # switch editor mode button
+ %SwitchEditorMode.text = "Text editor"
+ %SwitchEditorMode.icon = get_theme_icon("ArrowRight", "EditorIcons")
+ %SwitchEditorMode.pressed.connect(toggle_editor_mode)
+ %SwitchEditorMode.custom_minimum_size.x = 200 * DialogicUtil.get_editor_scale()
+
+ %SearchClose.icon = get_theme_icon("Close", "EditorIcons")
+ %SearchUp.icon = get_theme_icon("MoveUp", "EditorIcons")
+ %SearchDown.icon = get_theme_icon("MoveDown", "EditorIcons")
+
+
+
+func _on_create_timeline_button_pressed() -> void:
+ editors_manager.show_add_resource_dialog(
+ new_timeline,
+ '*.dtl; DialogicTimeline',
+ 'Create new timeline',
+ 'timeline',
+ )
+
+
+func _clear() -> void:
+ current_resource = null
+ current_resource_state = ResourceStates.SAVED
+ match current_editor_mode:
+ EditorMode.VISUAL:
+ %VisualEditor.clear_timeline_nodes()
+ EditorMode.TEXT:
+ %TextEditor.clear_timeline()
+ $NoTimelineScreen.show()
+ play_timeline_button.disabled = true
+
+
+func get_current_editor() -> Node:
+ if current_editor_mode == 1:
+ return %TextEditor
+ return %VisualEditor
+
+#region SEARCH
+
+func search_timeline() -> void:
+ %SearchSection.show()
+ if get_viewport().gui_get_focus_owner() is TextEdit:
+ %Search.text = get_viewport().gui_get_focus_owner().get_selected_text()
+ _on_search_text_changed(%Search.text)
+ else:
+ %Search.text = ""
+ %Search.grab_focus()
+
+
+func _on_close_search_pressed() -> void:
+ %SearchSection.hide()
+ %Search.text = ""
+ _on_search_text_changed('')
+
+
+func _on_search_text_changed(new_text: String) -> void:
+ var editor: Node = null
+ var anything_found: bool = get_current_editor()._search_timeline(new_text)
+ if anything_found or new_text.is_empty():
+ %SearchLabel.hide()
+ %Search.add_theme_color_override("font_color", get_theme_color("font_color", "Editor"))
+ else:
+ %SearchLabel.show()
+ %SearchLabel.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
+ %Search.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
+ %SearchLabel.text = "No Match"
+
+
+func _on_search_down_pressed() -> void:
+ get_current_editor()._search_navigate_down()
+
+
+func _on_search_up_pressed() -> void:
+ get_current_editor()._search_navigate_up()
+
+#endregion
+
+
--- /dev/null
+[gd_scene load_steps=10 format=3 uid="uid://crce0na84rhfd"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/timeline_editor.gd" id="1_4aceh"]
+[ext_resource type="PackedScene" uid="uid://ysqbusmy0qma" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/timeline_editor_visual.tscn" id="2_qs7vc"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_yqd26"]
+[ext_resource type="PackedScene" uid="uid://defdeav8rli6o" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.tscn" id="3_up2bn"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd" id="4_1t6bf"]
+
+[sub_resource type="Image" id="Image_43fqw"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_lvr8x"]
+image = SubResource("Image_43fqw")
+
+[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_7lpql"]
+script = ExtResource("4_1t6bf")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lpeon"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 0.365, 0.365, 1)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_detail = 1
+
+[node name="Timeline" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_4aceh")
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="HBox" type="HBoxContainer" parent="VBox"]
+layout_mode = 2
+
+[node name="TimelineName" type="Label" parent="VBox/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicTitle"
+text = "Cool Name"
+
+[node name="NameTooltip" parent="VBox/HBox" instance=ExtResource("2_yqd26")]
+layout_mode = 2
+tooltip_text = "This unique identifier is based on the file name. You can change it in the Reference Manager.
+This is what you should use in a jump event to reference this timeline.
+
+You can also use this name in Dialogic.start()."
+texture = SubResource("ImageTexture_lvr8x")
+hint_text = "This unique identifier is based on the file name. You can change it in the Reference Manager.
+This is what you should use in a jump event to reference this timeline.
+
+You can also use this name in Dialogic.start()."
+
+[node name="SwitchEditorMode" type="Button" parent="VBox/HBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Switch between Text Editor and Visual Editor"
+text = "Text editor"
+icon = SubResource("ImageTexture_lvr8x")
+
+[node name="VisualEditor" parent="VBox" instance=ExtResource("2_qs7vc")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/margin_left = 0
+theme_override_constants/margin_top = 0
+theme_override_constants/margin_right = 0
+theme_override_constants/margin_bottom = 0
+
+[node name="TextEditor" parent="VBox" instance=ExtResource("3_up2bn")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+syntax_highlighter = SubResource("SyntaxHighlighter_7lpql")
+symbol_lookup_on_click = true
+line_folding = false
+gutters_draw_fold_gutter = false
+
+[node name="SearchSection" type="HBoxContainer" parent="VBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+
+[node name="Search" type="LineEdit" parent="VBox/SearchSection"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Search"
+
+[node name="SearchLabel" type="Label" parent="VBox/SearchSection"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+
+[node name="SearchUp" type="Button" parent="VBox/SearchSection"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="SearchDown" type="Button" parent="VBox/SearchSection"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="SearchClose" type="Button" parent="VBox/SearchSection"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="NoTimelineScreen" type="PanelContainer" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_lpeon")
+
+[node name="CenterContainer" type="CenterContainer" parent="NoTimelineScreen"]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="NoTimelineScreen/CenterContainer"]
+custom_minimum_size = Vector2(250, 0)
+layout_mode = 2
+
+[node name="Label" type="Label" parent="NoTimelineScreen/CenterContainer/VBoxContainer"]
+layout_mode = 2
+text = "No timeline opened.
+Create a timeline or double-click one in the file system dock."
+horizontal_alignment = 1
+autowrap_mode = 3
+
+[node name="CreateTimelineButton" type="Button" parent="NoTimelineScreen/CenterContainer/VBoxContainer"]
+layout_mode = 2
+text = "Create New Timeline"
+
+[connection signal="text_changed" from="VBox/SearchSection/Search" to="." method="_on_search_text_changed"]
+[connection signal="pressed" from="VBox/SearchSection/SearchUp" to="." method="_on_search_up_pressed"]
+[connection signal="pressed" from="VBox/SearchSection/SearchDown" to="." method="_on_search_down_pressed"]
+[connection signal="pressed" from="VBox/SearchSection/SearchClose" to="." method="_on_close_search_pressed"]
+[connection signal="pressed" from="NoTimelineScreen/CenterContainer/VBoxContainer/CreateTimelineButton" to="." method="_on_create_timeline_button_pressed"]
--- /dev/null
+@tool
+class_name DialogicEditor
+extends Control
+
+## Base class for all dialogic editors.
+
+# These signals will automatically be emitted if current_resource_state is changed.
+signal resource_saved()
+signal resource_unsaved()
+
+signal opened
+
+var current_resource: Resource
+
+## State of the current resource
+enum ResourceStates {SAVED, UNSAVED}
+var current_resource_state: ResourceStates:
+ set(value):
+ current_resource_state = value
+ if value == ResourceStates.SAVED:
+ resource_saved.emit()
+ else:
+ resource_unsaved.emit()
+
+var editors_manager: Control
+# text displayed on the current resource label on non-resource editors
+var alternative_text: String = ""
+
+## Overwrite. Register to the editor manager in here.
+func _register() -> void:
+ pass
+
+
+## Used on the tab
+func _get_icon() -> Texture:
+ return null
+
+## Used on the tab
+func _get_title() -> String:
+ return ""
+
+
+## If this editor supports editing resources, load them here (overwrite in subclass)
+func _open_resource(_resource:Resource) -> void:
+ pass
+
+
+## If this editor supports editing resources, save them here (overwrite in subclass)
+func _save() -> void:
+ pass
+
+
+## Overwrite. Called when this editor is shown. (show() doesn't have to be called)
+func _open(_extra_info:Variant = null) -> void:
+ pass
+
+
+## Overwrite. Called when another editor is opened. (hide() doesn't have to be called)
+func _close() -> void:
+ pass
+
+
+## Overwrite. Called to clear all current state and resource from the editor.
+## Although rarely used, sometimes you just want NO timeline to be open.
+func _clear() -> void:
+ pass
--- /dev/null
+@tool
+extends Control
+
+## Editor root node. Most editor functionality is handled by EditorsManager node!
+
+var plugin_reference: EditorPlugin = null
+var editors_manager: Control = null
+
+var editor_file_dialog: EditorFileDialog
+
+@onready var sidebar := %Sidebar as DialogicSidebar
+
+func _ready() -> void:
+ if get_parent() is SubViewport:
+ return
+
+ ## CONNECTIONS
+ sidebar.show_sidebar.connect(_on_sidebar_toggled)
+
+ ## REFERENCES
+ editors_manager = $EditorsManager
+ var button: Button = editors_manager.add_icon_button(
+ get_theme_icon("MakeFloating", "EditorIcons"), "Make floating"
+ )
+ button.pressed.connect(toggle_floating_window)
+
+ # File dialog
+ editor_file_dialog = EditorFileDialog.new()
+ add_child(editor_file_dialog)
+
+ var info_message := Label.new()
+ info_message.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
+ editor_file_dialog.get_line_edit().get_parent().add_sibling(info_message)
+ info_message.get_parent().move_child(info_message, info_message.get_index() - 1)
+ editor_file_dialog.set_meta("info_message_label", info_message)
+
+ $SaveConfirmationDialog.add_button("No Saving Please!", true, "nosave")
+ $SaveConfirmationDialog.hide()
+ update_theme_additions()
+ EditorInterface.get_base_control().theme_changed.connect(update_theme_additions)
+
+
+func _on_sidebar_toggled(sidebar_shown: bool) -> void:
+ var h_split := (%HSplit as HSplitContainer)
+ if sidebar_shown:
+ h_split.dragger_visibility = SplitContainer.DRAGGER_VISIBLE
+ h_split.split_offset = 150
+ h_split.collapsed = false
+ else:
+ h_split.dragger_visibility = SplitContainer.DRAGGER_HIDDEN_COLLAPSED
+ h_split.split_offset = 0
+ h_split.collapsed = true
+
+
+func update_theme_additions() -> void:
+ add_theme_stylebox_override(
+ "panel",
+ (
+ DCSS
+ . inline(
+ {
+ "background": get_theme_color("base_color", "Editor"),
+ "padding":
+ [5 * DialogicUtil.get_editor_scale(), 5 * DialogicUtil.get_editor_scale()],
+ }
+ )
+ )
+ )
+ var holder_panel := (
+ DCSS
+ . inline(
+ {
+ "border-radius": 5,
+ #'border': 2,
+ #'border-color': get_theme_color("base_color", "Editor"),
+ "background": get_theme_color("dark_color_2", "Editor"),
+ "padding":
+ [5 * DialogicUtil.get_editor_scale(), 5 * DialogicUtil.get_editor_scale()],
+ }
+ )
+ )
+ holder_panel.border_width_top = 0
+ holder_panel.corner_radius_top_left = 0
+ editors_manager.editors_holder.add_theme_stylebox_override("panel", holder_panel)
+
+ if theme == null:
+ theme = Theme.new()
+ theme.clear()
+
+ theme.set_type_variation("DialogicTitle", "Label")
+ theme.set_font("font", "DialogicTitle", get_theme_font("title", "EditorFonts"))
+ theme.set_color("font_color", "DialogicTitle", get_theme_color("warning_color", "Editor"))
+ theme.set_color(
+ "font_uneditable_color", "DialogicTitle", get_theme_color("warning_color", "Editor")
+ )
+ theme.set_color(
+ "font_selected_color", "DialogicTitle", get_theme_color("warning_color", "Editor")
+ )
+ theme.set_font_size(
+ "font_size", "DialogicTitle", get_theme_font_size("doc_size", "EditorFonts")
+ )
+
+ theme.set_type_variation("DialogicSubTitle", "Label")
+ theme.set_font("font", "DialogicSubTitle", get_theme_font("title", "EditorFonts"))
+ theme.set_font_size(
+ "font_size", "DialogicSubTitle", get_theme_font_size("doc_size", "EditorFonts")
+ )
+ theme.set_color("font_color", "DialogicSubTitle", get_theme_color("accent_color", "Editor"))
+
+ theme.set_type_variation("DialogicPanelA", "PanelContainer")
+ var panel_style := (
+ DCSS
+ . inline(
+ {
+ "border-radius": 10,
+ "background": get_theme_color("base_color", "Editor"),
+ "padding": [5, 5],
+ }
+ )
+ )
+ theme.set_stylebox("panel", "DialogicPanelA", panel_style)
+ theme.set_stylebox("normal", "DialogicPanelA", panel_style)
+
+ var dark_panel := panel_style.duplicate()
+ dark_panel.bg_color = get_theme_color("dark_color_3", "Editor")
+ theme.set_stylebox("panel", "DialogicPanelDarkA", dark_panel)
+
+ var cornerless_panel := panel_style.duplicate()
+ cornerless_panel.corner_radius_top_left = 0
+ theme.set_stylebox("panel", "DialogicPanelA_cornerless", cornerless_panel)
+
+ # panel used for example for portrait previews in character editor
+ theme.set_type_variation("DialogicPanelB", "PanelContainer")
+ var side_panel: StyleBoxFlat = panel_style.duplicate()
+ side_panel.corner_radius_top_left = 0
+ side_panel.corner_radius_bottom_left = 0
+ side_panel.expand_margin_left = get_theme_constant("separation", "SplitContainer")
+ side_panel.bg_color = get_theme_color("dark_color_2", "Editor")
+ side_panel.set_border_width_all(1)
+ side_panel.border_width_left = 0
+ side_panel.border_color = get_theme_color("contrast_color_2", "Editor")
+ theme.set_stylebox("panel", "DialogicPanelB", side_panel)
+
+ theme.set_type_variation("DialogicEventEdit", "Control")
+ var edit_panel := StyleBoxFlat.new()
+ edit_panel.draw_center = true
+ edit_panel.bg_color = get_theme_color("accent_color", "Editor")
+ edit_panel.bg_color.a = 0.05
+ edit_panel.border_width_bottom = 2
+ edit_panel.border_color = get_theme_color("accent_color", "Editor").lerp(
+ get_theme_color("dark_color_2", "Editor"), 0.4
+ )
+ edit_panel.content_margin_left = 5
+ edit_panel.content_margin_right = 5
+ edit_panel.set_corner_radius_all(1)
+ theme.set_stylebox("panel", "DialogicEventEdit", edit_panel)
+ theme.set_stylebox("normal", "DialogicEventEdit", edit_panel)
+
+ var focus_edit := edit_panel.duplicate()
+ focus_edit.border_color = get_theme_color("property_color_z", "Editor")
+ focus_edit.draw_center = false
+ theme.set_stylebox("focus", "DialogicEventEdit", focus_edit)
+
+ var hover_edit := edit_panel.duplicate()
+ hover_edit.border_color = get_theme_color("warning_color", "Editor")
+
+ theme.set_stylebox("hover", "DialogicEventEdit", hover_edit)
+ var disabled_edit := edit_panel.duplicate()
+ disabled_edit.border_color = get_theme_color("property_color", "Editor")
+ theme.set_stylebox("disabled", "DialogicEventEdit", disabled_edit)
+
+ theme.set_type_variation("DialogicHintText", "Label")
+ theme.set_color("font_color", "DialogicHintText", get_theme_color("readonly_color", "Editor"))
+ theme.set_font("font", "DialogicHintText", get_theme_font("doc_italic", "EditorFonts"))
+
+ theme.set_type_variation("DialogicHintText2", "Label")
+ theme.set_color(
+ "font_color", "DialogicHintText2", get_theme_color("property_color_w", "Editor")
+ )
+ theme.set_font("font", "DialogicHintText2", get_theme_font("doc_italic", "EditorFonts"))
+
+ theme.set_type_variation("DialogicSection", "Label")
+ theme.set_font("font", "DialogicSection", get_theme_font("main_msdf", "EditorFonts"))
+ theme.set_color("font_color", "DialogicSection", get_theme_color("property_color_z", "Editor"))
+ theme.set_font_size(
+ "font_size", "DialogicSection", get_theme_font_size("doc_size", "EditorFonts")
+ )
+
+ theme.set_type_variation("DialogicSettingsSection", "DialogicSection")
+ theme.set_font("font", "DialogicSettingsSection", get_theme_font("main_msdf", "EditorFonts"))
+ theme.set_color(
+ "font_color", "DialogicSettingsSection", get_theme_color("property_color_z", "Editor")
+ )
+ theme.set_font_size(
+ "font_size", "DialogicSettingsSection", get_theme_font_size("doc_size", "EditorFonts")
+ )
+
+ theme.set_type_variation("DialogicSectionBig", "DialogicSection")
+ theme.set_color("font_color", "DialogicSectionBig", get_theme_color("accent_color", "Editor"))
+ theme.set_font_size(
+ "font_size", "DialogicSectionBig", get_theme_font_size("doc_title_size", "EditorFonts")
+ )
+
+ theme.set_type_variation("DialogicLink", "LinkButton")
+ theme.set_color("font_hover_color", "DialogicLink", get_theme_color("warning_color", "Editor"))
+
+ theme.set_type_variation("DialogicMegaSeparator", "HSeparator")
+ (
+ theme
+ . set_stylebox(
+ "separator",
+ "DialogicMegaSeparator",
+ (
+ DCSS
+ . inline(
+ {
+ "border-radius": 10,
+ "border": 0,
+ "background": get_theme_color("accent_color", "Editor"),
+ "padding": [5, 5],
+ }
+ )
+ )
+ )
+ )
+ theme.set_constant("separation", "DialogicMegaSeparator", 50)
+
+ theme.set_type_variation("DialogicTextEventTextEdit", "CodeEdit")
+ var editor_settings := plugin_reference.get_editor_interface().get_editor_settings()
+ var text_panel := (
+ DCSS
+ . inline(
+ {
+ "border-radius": 8,
+ "background":
+ editor_settings.get_setting("text_editor/theme/highlighting/background_color").lerp(
+ editor_settings.get_setting("text_editor/theme/highlighting/text_color"), 0.05
+ ),
+ "padding": [8, 8],
+ }
+ )
+ )
+ text_panel.content_margin_bottom = 5
+ text_panel.content_margin_left = 13
+ theme.set_stylebox("normal", "DialogicTextEventTextEdit", text_panel)
+
+ var event_field_group_panel := DCSS.inline({
+ 'border-radius': 8,
+ "border":1,
+ "padding":2,
+ "boder-color": get_theme_color("property_color", "Editor"),
+ "background":"none"})
+ theme.set_type_variation("DialogicEventEditGroup", "PanelContainer")
+ theme.set_stylebox("panel", "DialogicEventEditGroup", event_field_group_panel)
+
+ theme.set_icon('Plugin', 'Dialogic', load("res://addons/dialogic/Editor/Images/plugin-icon.svg"))
+
+
+## Switches from floating window mode to embedded mode based on current mode
+func toggle_floating_window() -> void:
+ if get_parent() is Window:
+ swap_to_embedded_editor()
+ else:
+ swap_to_floating_window()
+
+
+## Removes the main control from it's parent and adds it to a new Window node
+func swap_to_floating_window() -> void:
+ if get_parent() is Window:
+ return
+
+ var parent := get_parent()
+ get_parent().remove_child(self)
+ var window := Window.new()
+ parent.add_child(window)
+ window.add_child(self)
+ window.title = "Dialogic"
+ window.close_requested.connect(swap_to_embedded_editor)
+ window.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
+ window.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
+ window.size = size
+ window.min_size = Vector2(500, 500)
+ set_anchors_preset(Control.PRESET_FULL_RECT)
+ window.disable_3d = true
+ window.wrap_controls = true
+ window.popup_centered()
+ plugin_reference.get_editor_interface().set_main_screen_editor("2D")
+
+
+## Removes the main control from the window node and adds it to it's grandparent
+## which is the original owner.
+func swap_to_embedded_editor() -> void:
+ if not get_parent() is Window:
+ return
+
+ var window := get_parent()
+ get_parent().remove_child(self)
+ plugin_reference.get_editor_interface().set_main_screen_editor("Dialogic")
+ window.get_parent().add_child(self)
+ window.queue_free()
+
+
+func godot_file_dialog(
+ callable: Callable,
+ filter: String,
+ mode := EditorFileDialog.FILE_MODE_OPEN_FILE,
+ window_title := "Save",
+ current_file_name := "New_File",
+ saving_something := false,
+ extra_message: String = ""
+) -> EditorFileDialog:
+ for connection in editor_file_dialog.file_selected.get_connections():
+ editor_file_dialog.file_selected.disconnect(connection.callable)
+ for connection in editor_file_dialog.dir_selected.get_connections():
+ editor_file_dialog.dir_selected.disconnect(connection.callable)
+ editor_file_dialog.file_mode = mode
+ editor_file_dialog.clear_filters()
+ editor_file_dialog.popup_centered_ratio(0.6)
+ editor_file_dialog.add_filter(filter)
+ editor_file_dialog.title = window_title
+ editor_file_dialog.current_file = current_file_name
+ editor_file_dialog.disable_overwrite_warning = !saving_something
+ if extra_message:
+ editor_file_dialog.get_meta("info_message_label").show()
+ editor_file_dialog.get_meta("info_message_label").text = extra_message
+ else:
+ editor_file_dialog.get_meta("info_message_label").hide()
+
+ if mode == EditorFileDialog.FILE_MODE_OPEN_FILE or mode == EditorFileDialog.FILE_MODE_SAVE_FILE:
+ editor_file_dialog.file_selected.connect(callable)
+ elif mode == EditorFileDialog.FILE_MODE_OPEN_DIR:
+ editor_file_dialog.dir_selected.connect(callable)
+ elif mode == EditorFileDialog.FILE_MODE_OPEN_ANY:
+ editor_file_dialog.dir_selected.connect(callable)
+ editor_file_dialog.file_selected.connect(callable)
+ return editor_file_dialog
--- /dev/null
+[gd_scene load_steps=18 format=3 uid="uid://de6yhw4r8jqb3"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Editor/editor_main.gd" id="1_x88ov"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/editors_manager.gd" id="2_pe2tl"]
+[ext_resource type="Texture2D" uid="uid://dybg3l5pwetne" path="res://addons/dialogic/Editor/Images/plugin-icon.svg" id="2_scwcl"]
+[ext_resource type="PackedScene" uid="uid://cwe3r2tbh2og1" path="res://addons/dialogic/Editor/Common/side_bar.tscn" id="3_lp6hj"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/toolbar.gd" id="4_6cx8s"]
+[ext_resource type="Texture2D" uid="uid://bbea0efx0ybu7" path="res://addons/dialogic/Editor/Images/Resources/character.svg" id="6_8yp76"]
+[ext_resource type="Texture2D" uid="uid://b5xwnxdb7064n" path="res://addons/dialogic/Modules/Glossary/icon.svg" id="7_45ytg"]
+[ext_resource type="Texture2D" uid="uid://1mccycya6eua" path="res://addons/dialogic/Modules/StyleEditor/styles_icon.svg" id="8_jj1i6"]
+[ext_resource type="Texture2D" uid="uid://ckilxvwc34s84" path="res://addons/dialogic/Modules/Variable/variable.svg" id="9_k4reh"]
+[ext_resource type="PackedScene" uid="uid://c7lmt5cp7bxcm" path="res://addons/dialogic/Editor/Common/reference_manager.tscn" id="10_l1rf8"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/reference_manager_window.gd" id="10_xbkrt"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/CodeCompletionHelper.gd" id="11_fyce4"]
+[ext_resource type="Script" path="res://addons/dialogic/Editor/Common/update_manager.gd" id="14_l6b1p"]
+[ext_resource type="PackedScene" uid="uid://vv3m5m68fwg7" path="res://addons/dialogic/Editor/Common/update_install_window.tscn" id="15_cu4xj"]
+
+[sub_resource type="Image" id="Image_uqxml"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_drcn6"]
+image = SubResource("Image_uqxml")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5bs7k"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.1155, 0.132, 0.1595, 1)
+corner_detail = 1
+anti_aliasing = false
+
+[node name="EditorView" type="ScrollContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1_x88ov")
+
+[node name="EditorsManager" type="Control" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("2_pe2tl")
+
+[node name="HSplit" type="HSplitContainer" parent="EditorsManager"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+split_offset = 150
+
+[node name="Sidebar" parent="EditorsManager/HSplit" instance=ExtResource("3_lp6hj")]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(20, 0)
+layout_mode = 2
+split_offset = 0
+
+[node name="VBox" type="VBoxContainer" parent="EditorsManager/HSplit"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Toolbar" type="HBoxContainer" parent="EditorsManager/HSplit/VBox"]
+layout_mode = 2
+size_flags_vertical = 0
+mouse_filter = 2
+alignment = 2
+script = ExtResource("4_6cx8s")
+
+[node name="EditorTabBar" type="TabBar" parent="EditorsManager/HSplit/VBox/Toolbar"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 8
+tab_count = 7
+tab_0/icon = ExtResource("2_scwcl")
+tab_1/title = "Timeline"
+tab_1/icon = SubResource("ImageTexture_drcn6")
+tab_2/title = "Character"
+tab_2/icon = ExtResource("6_8yp76")
+tab_3/title = "Glossary"
+tab_3/icon = ExtResource("7_45ytg")
+tab_4/title = "Layouts"
+tab_4/icon = ExtResource("8_jj1i6")
+tab_5/title = "Variables"
+tab_5/icon = ExtResource("9_k4reh")
+tab_6/title = "Settings"
+tab_6/icon = SubResource("ImageTexture_drcn6")
+
+[node name="CustomButtons" type="HBoxContainer" parent="EditorsManager/HSplit/VBox/Toolbar"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Editors" type="PanelContainer" parent="EditorsManager/HSplit/VBox"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="CodeCompletionHelper" type="Node" parent="EditorsManager"]
+script = ExtResource("11_fyce4")
+
+[node name="SaveConfirmationDialog" type="AcceptDialog" parent="."]
+size = Vector2i(207, 100)
+
+[node name="ResourceRenameWarning" type="AcceptDialog" parent="."]
+title = "Dialogic resource renamed!"
+initial_position = 5
+size = Vector2i(494, 135)
+ok_button_text = "Show Unique Identifiers"
+dialog_text = "You renamed a dialogic resource. This does NOT automatically rename the unique identifier for this resource. Consider checking in the Reference Manager if the identifiers are still the way you want them."
+dialog_autowrap = true
+
+[node name="ReferenceManager" type="Window" parent="."]
+disable_3d = true
+title = "Reference Manager"
+initial_position = 2
+size = Vector2i(858, 442)
+visible = false
+wrap_controls = true
+content_scale_mode = 1
+content_scale_aspect = 4
+script = ExtResource("10_xbkrt")
+
+[node name="Manager" parent="ReferenceManager" instance=ExtResource("10_l1rf8")]
+theme_override_styles/panel = SubResource("StyleBoxFlat_5bs7k")
+
+[node name="UpdateManager" type="Node" parent="."]
+script = ExtResource("14_l6b1p")
+
+[node name="Window" type="Window" parent="UpdateManager"]
+title = "Dialogic Update Checker"
+initial_position = 2
+size = Vector2i(600, 400)
+visible = false
+wrap_controls = true
+
+[node name="UpdateInstallWindow" parent="UpdateManager/Window" instance=ExtResource("15_cu4xj")]
+
+[node name="UpdateCheckRequest" type="HTTPRequest" parent="UpdateManager"]
+timeout = 5.0
+
+[node name="DownloadRequest" type="HTTPRequest" parent="UpdateManager"]
+
+[connection signal="close_requested" from="ReferenceManager" to="ReferenceManager" method="_on_close_requested"]
+[connection signal="downdload_completed" from="UpdateManager" to="UpdateManager/Window/UpdateInstallWindow" method="_on_update_manager_downdload_completed"]
+[connection signal="update_check_completed" from="UpdateManager" to="UpdateManager" method="_on_update_check_completed"]
+[connection signal="close_requested" from="UpdateManager/Window" to="UpdateManager/Window/UpdateInstallWindow" method="_on_window_close_requested"]
+[connection signal="request_completed" from="UpdateManager/UpdateCheckRequest" to="UpdateManager" method="_on_UpdateCheck_request_completed"]
+[connection signal="request_completed" from="UpdateManager/DownloadRequest" to="UpdateManager" method="_on_DownloadRequest_completed"]
--- /dev/null
+@tool
+extends Control
+
+## Node that manages editors, the toolbar and the sidebar.
+
+signal resource_opened(resource)
+signal editor_changed(previous, current)
+
+### References
+@onready var hsplit := $HSplit
+@onready var sidebar := $HSplit/Sidebar
+@onready var editors_holder := $HSplit/VBox/Editors
+@onready var toolbar := $HSplit/VBox/Toolbar
+@onready var tabbar := $HSplit/VBox/Toolbar/EditorTabBar
+
+var reference_manager: Node:
+ get:
+ return get_node("../ReferenceManager")
+
+## Information on supported resource extensions and registered editors
+var current_editor: DialogicEditor = null
+var previous_editor: DialogicEditor = null
+var editors := {}
+var supported_file_extensions := []
+var used_resources_cache: Array = []
+
+
+################################################################################
+## REGISTERING EDITORS
+################################################################################
+
+## Asks all childs of the editor holder to register
+func _ready() -> void:
+ if owner.get_parent() is SubViewport:
+ return
+
+ tabbar.clear_tabs()
+
+ # Load base editors
+ _add_editor("res://addons/dialogic/Editor/HomePage/home_page.tscn")
+ _add_editor("res://addons/dialogic/Editor/TimelineEditor/timeline_editor.tscn")
+ _add_editor("res://addons/dialogic/Editor/CharacterEditor/character_editor.tscn")
+
+ # Load custom editors
+ for indexer in DialogicUtil.get_indexers():
+ for editor_path in indexer._get_editors():
+ _add_editor(editor_path)
+ _add_editor("res://addons/dialogic/Editor/Settings/settings_editor.tscn")
+
+ tabbar.tab_clicked.connect(_on_editors_tab_changed)
+
+ # Needs to be done here to make sure this node is ready when doing the register calls
+ for editor in editors_holder.get_children():
+ editor.editors_manager = self
+ editor._register()
+
+ DialogicResourceUtil.update()
+
+ await get_parent().ready
+ await get_tree().process_frame
+
+ load_saved_state()
+ used_resources_cache = DialogicUtil.get_editor_setting('last_resources', [])
+ sidebar.update_resource_list(used_resources_cache)
+
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_file_system_dock().files_moved.connect(_on_file_moved)
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed)
+
+ hsplit.set("theme_override_constants/separation", get_theme_constant("base_margin", "Editor") * DialogicUtil.get_editor_scale())
+
+
+func _add_editor(path:String) -> void:
+ var editor: DialogicEditor = load(path).instantiate()
+ editors_holder.add_child(editor)
+ editor.hide()
+ tabbar.add_tab(editor._get_title(), editor._get_icon())
+
+
+## Call to register an editor/tab that edits a resource with a custom ending.
+func register_resource_editor(resource_extension:String, editor:DialogicEditor) -> void:
+ editors[editor.name] = {'node':editor, 'buttons':[], 'extension': resource_extension}
+ supported_file_extensions.append(resource_extension)
+ editor.resource_saved.connect(_on_resource_saved.bind(editor))
+ editor.resource_unsaved.connect(_on_resource_unsaved.bind(editor))
+
+
+## Call to register an editor/tab that doesn't edit a resource
+func register_simple_editor(editor:DialogicEditor) -> void:
+ editors[editor.name] = {'node': editor, 'buttons':[]}
+
+
+## Call to add an icon button. These buttons are always visible.
+func add_icon_button(icon:Texture, tooltip:String, editor:DialogicEditor=null) -> Node:
+ var button: Button = toolbar.add_icon_button(icon, tooltip)
+ if editor != null:
+ editors[editor.name]['buttons'].append(button)
+ return button
+
+
+## Call to add a custom action button. Only visible if editor is visible.
+func add_custom_button(label:String, icon:Texture, editor:DialogicEditor) -> Node:
+ var button: Button = toolbar.add_custom_button(label, icon)
+ editors[editor.name]['buttons'].append(button)
+ return button
+
+
+func can_edit_resource(resource:Resource) -> bool:
+ return resource.resource_path.get_extension() in supported_file_extensions
+
+
+################################################################################
+## OPENING/CLOSING
+################################################################################
+
+
+func _on_editors_tab_changed(tab:int) -> void:
+ open_editor(editors_holder.get_child(tab))
+
+
+func edit_resource(resource:Resource, save_previous:bool = true, silent:= false) -> void:
+ if not resource:
+ # The resource doesn't exists, show an error
+ print("[Dialogic] The resource you are trying to edit doesn't exist any more.")
+ return
+
+ if current_editor and save_previous:
+ current_editor._save()
+
+ if !resource.resource_path in used_resources_cache:
+ used_resources_cache.append(resource.resource_path)
+ sidebar.update_resource_list(used_resources_cache)
+
+ ## Open the correct editor
+ var extension: String = resource.resource_path.get_extension()
+ for editor in editors.values():
+ if editor.get('extension', '') == extension:
+ editor['node']._open_resource(resource)
+ if !silent:
+ open_editor(editor['node'], false)
+ if !silent:
+ resource_opened.emit(resource)
+
+
+
+## Only works if there was a different editor opened previously
+func toggle_editor(editor) -> void:
+ if editor.visible:
+ open_editor(previous_editor, true)
+ else:
+ open_editor(editor, true)
+
+
+## Shows the given editor
+func open_editor(editor:DialogicEditor, save_previous: bool = true, extra_info:Variant = null) -> void:
+ if current_editor and save_previous:
+ current_editor._save()
+
+ if current_editor:
+ current_editor._close()
+ current_editor.hide()
+
+ if current_editor != previous_editor:
+ previous_editor = current_editor
+
+ editor._open(extra_info)
+ editor.opened.emit()
+ current_editor = editor
+ editor.show()
+ tabbar.current_tab = editor.get_index()
+
+ if editor.current_resource:
+ var text: String = editor.current_resource.resource_path.get_file()
+ if editor.current_resource_state == DialogicEditor.ResourceStates.UNSAVED:
+ text += "(*)"
+
+ ## This makes custom button editor-specific
+ ## I think it's better without.
+
+ save_current_state()
+ editor_changed.emit(previous_editor, current_editor)
+
+
+## Rarely used to completely clear an editor.
+func clear_editor(editor:DialogicEditor, save:bool = false) -> void:
+ if save:
+ editor._save()
+
+ editor._clear()
+
+## Shows a file selector. Calls [accept_callable] once accepted
+func show_add_resource_dialog(accept_callable:Callable, filter:String = "*", title = "New resource", default_name = "new_character", mode = EditorFileDialog.FILE_MODE_SAVE_FILE) -> void:
+ find_parent('EditorView').godot_file_dialog(
+ _on_add_resource_dialog_accepted.bind(accept_callable),
+ filter,
+ mode,
+ title,
+ default_name,
+ true,
+ "Do not use \"'()!;:/\\*# in character or timeline names!"
+ )
+
+
+func _on_add_resource_dialog_accepted(path:String, callable:Callable) -> void:
+ var file_name: String = path.get_file().trim_suffix('.'+path.get_extension())
+ for i in ['#','&','+',';','(',')','!','*','*','"',"'",'%', '$', ':','.',',']:
+ file_name = file_name.replace(i, '')
+ callable.call(path.trim_suffix(path.get_file()).path_join(file_name)+'.'+path.get_extension())
+
+
+## Called by the plugin.gd script on CTRL+S or Debug Game start
+func save_current_resource() -> void:
+ if current_editor:
+ current_editor._save()
+
+
+## Change the resource state
+func _on_resource_saved(editor:DialogicEditor):
+ sidebar.set_unsaved_indicator(true)
+
+
+## Change the resource state
+func _on_resource_unsaved(editor:DialogicEditor):
+ sidebar.set_unsaved_indicator(false)
+
+
+## Tries opening the last resource
+func load_saved_state() -> void:
+ var current_resources: Dictionary = DialogicUtil.get_editor_setting('current_resources', {})
+ for editor in current_resources.keys():
+ editors[editor]['node']._open_resource(load(current_resources[editor]))
+
+ var current_editor: String = DialogicUtil.get_editor_setting('current_editor', 'HomePage')
+ open_editor(editors[current_editor]['node'])
+
+
+func save_current_state() -> void:
+ DialogicUtil.set_editor_setting('current_editor', current_editor.name)
+ var current_resources: Dictionary = {}
+ for editor in editors.values():
+ if editor['node'].current_resource != null:
+ current_resources[editor['node'].name] = editor['node'].current_resource.resource_path
+ DialogicUtil.set_editor_setting('current_resources', current_resources)
+
+
+func _on_file_moved(old_name:String, new_name:String) -> void:
+ if !old_name.get_extension() in supported_file_extensions:
+ return
+
+ used_resources_cache = DialogicUtil.get_editor_setting('last_resources', [])
+ if old_name in used_resources_cache:
+ used_resources_cache.insert(used_resources_cache.find(old_name), new_name)
+ used_resources_cache.erase(old_name)
+
+ sidebar.update_resource_list(used_resources_cache)
+
+ for editor in editors:
+ if editors[editor].node.current_resource != null and editors[editor].node.current_resource.resource_path == old_name:
+ editors[editor].node.current_resource.take_over_path(new_name)
+ edit_resource(load(new_name), true, true)
+
+ save_current_state()
+
+
+func _on_file_removed(file_name:String) -> void:
+ var current_resources: Dictionary = DialogicUtil.get_editor_setting('current_resources', {})
+ for editor_name in current_resources:
+ if current_resources[editor_name] == file_name:
+ clear_editor(editors[editor_name].node, false)
+ sidebar.update_resource_list()
+ save_current_state()
+
+
+
+################################################################################
+## HELPERS
+################################################################################
+
+
+func get_current_editor() -> DialogicEditor:
+ return current_editor
+
+
+func _exit_tree() -> void:
+ DialogicUtil.set_editor_setting('last_resources', used_resources_cache)
--- /dev/null
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
--- /dev/null
+extends Control
+
+func _ready() -> void:
+ if DialogicUtil.autoload().has_subsystem('History'):
+ DialogicUtil.autoload().History.visited_event.connect(_on_visited_event)
+ DialogicUtil.autoload().History.unvisited_event.connect(_on_not_read_event)
+
+func _on_visited_event() -> void:
+ show()
+
+func _on_not_read_event() -> void:
+ hide()
--- /dev/null
+@tool
+extends DialogicEvent
+
+# DEFINE ALL PROPERTIES OF THE EVENT
+# var MySetting: String = ""
+
+func _execute() -> void:
+ # I have no idea how this event works ;)
+ finish()
+
+
+#region INITIALIZE
+################################################################################
+
+# SET ALL VALUES THAT SHOULD NEVER CHANGE HERE
+func _init() -> void:
+ event_name = "Default"
+ event_color = Color("#ffffff")
+ event_category = "Main"
+ event_sorting_index = 0
+
+#endregion
+
+
+#region SAVING/LOADING
+################################################################################
+func get_shortcode() -> String:
+ return "default_shortcode"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_name
+ #"arg_name" : "NameOfProperty",
+ }
+
+# You can alternatively overwrite these 3 functions:
+# - to_text(),
+# - from_text(),
+# - is_valid_event()
+
+#endregion
+
+
+#region EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ pass
+
+#endregion
--- /dev/null
+@tool
+extends DialogicPortrait
+
+# If the custom portrait accepts a change, then accept it here
+func _update_portrait(passed_character:DialogicCharacter, passed_portrait:String) -> void:
+ if passed_portrait == "":
+ passed_portrait = passed_character['default_portrait']
+
+ if $Sprite.sprite_frames.has_animation(passed_portrait):
+ $Sprite.play(passed_portrait)
+
+func _on_animated_sprite_2d_animation_finished() -> void:
+ $Sprite.frame = randi()%$Sprite.sprite_frames.get_frame_count($Sprite.animation)
+ $Sprite.play()
+
+
+func _get_covered_rect() -> Rect2:
+ return Rect2($Sprite.position, $Sprite.sprite_frames.get_frame_texture($Sprite.animation, 0).get_size()*$Sprite.scale)
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://cyns86lydp1tl"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Example Assets/portraits/CustomPortrait_AnimatedSprite.gd" id="1_63c5k"]
+[ext_resource type="Texture2D" uid="uid://bfkpn7mrd786b" path="res://addons/dialogic/Example Assets/portraits/Antonio/pl5.png" id="2_15o4t"]
+[ext_resource type="Texture2D" uid="uid://s2jsr1aqiu84" path="res://addons/dialogic/Example Assets/portraits/Antonio/pl5 blink.png" id="3_qen6e"]
+
+[sub_resource type="SpriteFrames" id="SpriteFrames_yaycq"]
+animations = [{
+"frames": [{
+"duration": 10.0,
+"texture": ExtResource("2_15o4t")
+}, {
+"duration": 1.0,
+"texture": ExtResource("3_qen6e")
+}, {
+"duration": 5.0,
+"texture": ExtResource("2_15o4t")
+}, {
+"duration": 4.0,
+"texture": ExtResource("2_15o4t")
+}, {
+"duration": 1.0,
+"texture": ExtResource("3_qen6e")
+}, {
+"duration": 1.0,
+"texture": ExtResource("2_15o4t")
+}, {
+"duration": 1.0,
+"texture": ExtResource("3_qen6e")
+}, {
+"duration": 5.0,
+"texture": ExtResource("2_15o4t")
+}, {
+"duration": 1.0,
+"texture": ExtResource("3_qen6e")
+}, {
+"duration": 10.0,
+"texture": ExtResource("2_15o4t")
+}],
+"loop": false,
+"name": &"default",
+"speed": 10.0
+}]
+
+[node name="CustomCharacterScene" type="Node2D"]
+position = Vector2(160, 580)
+script = ExtResource("1_63c5k")
+
+[node name="Sprite" type="AnimatedSprite2D" parent="."]
+position = Vector2(-161, -580)
+scale = Vector2(0.751953, 0.751953)
+sprite_frames = SubResource("SpriteFrames_yaycq")
+autoplay = "default"
+centered = false
+
+[connection signal="animation_finished" from="Sprite" to="." method="_on_animated_sprite_2d_animation_finished"]
--- /dev/null
+@tool
+extends DialogicPortrait
+
+enum Faces {BASED_ON_PORTRAIT_NAME, NEUTRAL, HAPPY, SAD, JOY, SHOCK, ANGRY}
+
+@export var emotion: Faces = Faces.BASED_ON_PORTRAIT_NAME
+@export var portrait_width: int
+@export var portrait_height: int
+@export var alien := true
+
+var does_custom_portrait_change := true
+
+func _ready() -> void:
+ $Alien.hide()
+
+
+# Function to accept and use the extra data, if the custom portrait wants to accept it
+func _set_extra_data(data: String) -> void:
+ if data == "alien":
+ $Alien.show()
+ elif data == "no_alien":
+ $Alien.hide()
+
+
+# This function can be overridden. Defaults to true, if not overridden!
+func _should_do_portrait_update(_character: DialogicCharacter, _portrait:String) -> bool:
+ return true
+
+
+# If the custom portrait accepts a change, then accept it here
+func _update_portrait(_passed_character: DialogicCharacter, passed_portrait: String) -> void:
+ for face in $Faces.get_children():
+ face.hide()
+
+ if emotion == Faces.BASED_ON_PORTRAIT_NAME:
+ if 'happy' in passed_portrait.to_lower(): $Faces/Smile.show()
+ elif 'sad' in passed_portrait.to_lower(): $Faces/Frown.show()
+ elif 'joy' in passed_portrait.to_lower(): $Faces/Joy.show()
+ elif 'shock' in passed_portrait.to_lower(): $Faces/Shock.show()
+ elif 'angry' in passed_portrait.to_lower(): $Faces/Anger.show()
+ else: $Faces/Neutral.show()
+
+ else:
+ if emotion == Faces.HAPPY: $Faces/Smile.show()
+ elif emotion == Faces.SAD: $Faces/Frown.show()
+ elif emotion == Faces.JOY: $Faces/Joy.show()
+ elif emotion == Faces.SHOCK: $Faces/Shock.show()
+ elif emotion == Faces.ANGRY: $Faces/Anger.show()
+ else: $Faces/Neutral.show()
+
+ $Alien.visible = alien
+
+
+func _set_mirror(is_mirrored: bool) -> void:
+ if is_mirrored:
+ self.scale.x = -1
+
+ else:
+ self.scale.x = 1
+
+
+## If implemented, this is used by the editor for the "full view" mode
+func _get_covered_rect() -> Rect2:
+ # This will focus on the face.
+ # return Rect2($Faces/Anger.position+$Faces.position, $Faces/Anger.get_rect().size*$Faces/Anger.scale*$Faces.scale)
+ var size: Vector2 = $Body.get_rect().size
+ var scaled_size: Vector2 = size * $Body.scale
+ var position: Vector2 = $Body.position
+
+ return Rect2(position, scaled_size)
--- /dev/null
+[gd_scene load_steps=10 format=3 uid="uid://bgshjju5v2q0i"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Example Assets/portraits/CustomPortrait_FaceAtlas.gd" id="1_fc12l"]
+[ext_resource type="Texture2D" uid="uid://djqit26f4be4f" path="res://addons/dialogic/Example Assets/portraits/Princess/princess_blank.png" id="2_igcyp"]
+[ext_resource type="Texture2D" uid="uid://ndmjrpk41eo4" path="res://addons/dialogic/Example Assets/portraits/Portrait1.png" id="3_6xy1t"]
+[ext_resource type="Texture2D" uid="uid://dokv225cp85ja" path="res://addons/dialogic/Example Assets/portraits/Princess/anger.png" id="3_wdpjk"]
+[ext_resource type="Texture2D" uid="uid://5bruuhj5cqu4" path="res://addons/dialogic/Example Assets/portraits/Princess/frown.png" id="4_pimb3"]
+[ext_resource type="Texture2D" uid="uid://dg7c4umbfsyvs" path="res://addons/dialogic/Example Assets/portraits/Princess/joy.png" id="5_2ekfy"]
+[ext_resource type="Texture2D" uid="uid://bu3631ymfqxi3" path="res://addons/dialogic/Example Assets/portraits/Princess/neutral.png" id="6_5hpoa"]
+[ext_resource type="Texture2D" uid="uid://c5aku2g01k6c6" path="res://addons/dialogic/Example Assets/portraits/Princess/shock.png" id="7_5xil3"]
+[ext_resource type="Texture2D" uid="uid://dsid4ye0q74nl" path="res://addons/dialogic/Example Assets/portraits/Princess/smile.png" id="8_7s6tq"]
+
+[node name="CustomPortraitFaceAtlas" type="Node2D"]
+position = Vector2(301, 598)
+script = ExtResource("1_fc12l")
+
+[node name="Body" type="Sprite2D" parent="."]
+position = Vector2(-182, -465)
+scale = Vector2(0.287561, 0.287561)
+texture = ExtResource("2_igcyp")
+centered = false
+
+[node name="Alien" type="Sprite2D" parent="."]
+visible = false
+position = Vector2(-58, -378)
+rotation = -0.523598
+scale = Vector2(0.84236, 0.875348)
+texture = ExtResource("3_6xy1t")
+
+[node name="Faces" type="Node2D" parent="."]
+position = Vector2(2, -397)
+
+[node name="Anger" type="Sprite2D" parent="Faces"]
+position = Vector2(-38, -41)
+scale = Vector2(0.290393, 0.288066)
+texture = ExtResource("3_wdpjk")
+centered = false
+
+[node name="Frown" type="Sprite2D" parent="Faces"]
+position = Vector2(-38, -41)
+scale = Vector2(0.290393, 0.288066)
+texture = ExtResource("4_pimb3")
+centered = false
+
+[node name="Joy" type="Sprite2D" parent="Faces"]
+position = Vector2(-38, -41)
+scale = Vector2(0.290393, 0.288066)
+texture = ExtResource("5_2ekfy")
+centered = false
+
+[node name="Neutral" type="Sprite2D" parent="Faces"]
+position = Vector2(-38, -41)
+scale = Vector2(0.290393, 0.288066)
+texture = ExtResource("6_5hpoa")
+centered = false
+
+[node name="Shock" type="Sprite2D" parent="Faces"]
+position = Vector2(-38, -41)
+scale = Vector2(0.290393, 0.288066)
+texture = ExtResource("7_5xil3")
+centered = false
+
+[node name="Smile" type="Sprite2D" parent="Faces"]
+position = Vector2(-38, -41)
+scale = Vector2(0.290393, 0.288066)
+texture = ExtResource("8_7s6tq")
+centered = false
--- /dev/null
+Copyright (c) 2020 Tim Krief.
+
+Typing sound effects by Tim Krief are licensed under a Creative
+Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) License.
--- /dev/null
+@tool
+## Event that can change the currently playing background music.
+## This event won't play new music if it's already playing.
+class_name DialogicMusicEvent
+extends DialogicEvent
+
+
+### Settings
+
+## The file to play. If empty, the previous music will be faded out.
+var file_path := "":
+ set(value):
+ if file_path != value:
+ file_path = value
+ ui_update_needed.emit()
+## The channel to use.
+var channel_id: int = 0
+## The length of the fade. If 0 (by default) it's an instant change.
+var fade_length: float = 0
+## The volume the music will be played at.
+var volume: float = 0
+## The audio bus the music will be played at.
+var audio_bus := ""
+## If true, the audio will loop, otherwise only play once.
+var loop := true
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ if not dialogic.Audio.is_music_playing_resource(file_path, channel_id):
+ dialogic.Audio.update_music(file_path, volume, audio_bus, fade_length, loop, channel_id)
+
+ finish()
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Music"
+ set_default_color('Color7')
+ event_category = "Audio"
+ event_sorting_index = 2
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon_music.png'))
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "music"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "path" : {"property": "file_path", "default": ""},
+ "channel" : {"property": "channel_id", "default": 0},
+ "fade" : {"property": "fade_length", "default": 0},
+ "volume" : {"property": "volume", "default": 0},
+ "bus" : {"property": "audio_bus", "default": "",
+ "suggestions": get_bus_suggestions},
+ "loop" : {"property": "loop", "default": true},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('file_path', ValueType.FILE, {
+ 'left_text' : 'Play',
+ 'file_filter' : "*.mp3, *.ogg, *.wav; Supported Audio Files",
+ 'placeholder' : "No music",
+ 'editor_icon' : ["AudioStreamPlayer", "EditorIcons"]})
+ add_header_edit('channel_id', ValueType.FIXED_OPTIONS, {'left_text':'on:', 'options': get_channel_list()})
+ add_header_edit('file_path', ValueType.AUDIO_PREVIEW)
+ add_body_edit('fade_length', ValueType.NUMBER, {'left_text':'Fade Time:'})
+ add_body_edit('volume', ValueType.NUMBER, {'left_text':'Volume:', 'mode':2}, '!file_path.is_empty()')
+ add_body_edit('audio_bus', ValueType.SINGLELINE_TEXT, {'left_text':'Audio Bus:'}, '!file_path.is_empty()')
+ add_body_edit('loop', ValueType.BOOL, {'left_text':'Loop:'}, '!file_path.is_empty() and not file_path.to_lower().ends_with(".wav")')
+
+
+func get_bus_suggestions() -> Dictionary:
+ var bus_name_list := {}
+ for i in range(AudioServer.bus_count):
+ bus_name_list[AudioServer.get_bus_name(i)] = {'value':AudioServer.get_bus_name(i)}
+ return bus_name_list
+
+
+func get_channel_list() -> Array:
+ var channel_name_list := []
+ for i in ProjectSettings.get_setting('dialogic/audio/max_channels', 4):
+ channel_name_list.append({
+ 'label': 'Channel %s' % (i + 1),
+ 'value': i,
+ })
+ return channel_name_list
--- /dev/null
+@tool
+class_name DialogicSoundEvent
+extends DialogicEvent
+
+## Event that allows to play a sound effect. Requires the Audio subsystem!
+
+
+### Settings
+
+## The path to the file to play.
+var file_path := "":
+ set(value):
+ if file_path != value:
+ file_path = value
+ ui_update_needed.emit()
+## The volume to play the sound at.
+var volume: float = 0
+## The bus to play the sound on.
+var audio_bus := ""
+## If true, the sound will loop infinitely. Not recommended (as there is no way to stop it).
+var loop := false
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ dialogic.Audio.play_sound(file_path, volume, audio_bus, loop)
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Sound"
+ set_default_color('Color7')
+ event_category = "Audio"
+ event_sorting_index = 3
+ help_page_path = "https://dialogic.coppolaemilio.com"
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon_sound.png'))
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "sound"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_name
+ "path" : {"property": "file_path", "default": "",},
+ "volume" : {"property": "volume", "default": 0},
+ "bus" : {"property": "audio_bus", "default": "",
+ "suggestions": get_bus_suggestions},
+ "loop" : {"property": "loop", "default": false},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('file_path', ValueType.FILE,
+ {'left_text' : 'Play',
+ 'file_filter' : '*.mp3, *.ogg, *.wav; Supported Audio Files',
+ 'placeholder' : "Select file",
+ 'editor_icon' : ["AudioStreamPlayer", "EditorIcons"]})
+ add_header_edit('file_path', ValueType.AUDIO_PREVIEW)
+ add_body_edit('volume', ValueType.NUMBER, {'left_text':'Volume:', 'mode':2}, '!file_path.is_empty()')
+ add_body_edit('audio_bus', ValueType.SINGLELINE_TEXT, {'left_text':'Audio Bus:'}, '!file_path.is_empty()')
+
+
+func get_bus_suggestions() -> Dictionary:
+ var bus_name_list := {}
+ for i in range(AudioServer.bus_count):
+ bus_name_list[AudioServer.get_bus_name(i)] = {'value':AudioServer.get_bus_name(i)}
+ return bus_name_list
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_music.gd'), this_folder.path_join('event_sound.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Audio', 'script':this_folder.path_join('subsystem_audio.gd')}]
+
+
+func _get_settings_pages() -> Array:
+ return [this_folder.path_join('settings_audio.tscn')]
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+## Settings page that contains settings for the audio subsystem
+
+const MUSIC_MAX_CHANNELS := "dialogic/audio/max_channels"
+const TYPE_SOUND_AUDIO_BUS := "dialogic/audio/type_sound_bus"
+
+func _ready() -> void:
+ %MusicChannelCount.value_changed.connect(_on_music_channel_count_value_changed)
+ %TypeSoundBus.item_selected.connect(_on_type_sound_bus_item_selected)
+
+
+func _refresh() -> void:
+ %MusicChannelCount.value = ProjectSettings.get_setting(MUSIC_MAX_CHANNELS, 4)
+ %TypeSoundBus.clear()
+ var idx := 0
+ for i in range(AudioServer.bus_count):
+ %TypeSoundBus.add_item(AudioServer.get_bus_name(i))
+ if AudioServer.get_bus_name(i) == ProjectSettings.get_setting(TYPE_SOUND_AUDIO_BUS, ""):
+ idx = i
+ %TypeSoundBus.select(idx)
+
+
+func _on_music_channel_count_value_changed(value:float) -> void:
+ ProjectSettings.set_setting(MUSIC_MAX_CHANNELS, value)
+ ProjectSettings.save()
+
+
+func _on_type_sound_bus_item_selected(index:int) -> void:
+ ProjectSettings.set_setting(TYPE_SOUND_AUDIO_BUS, %TypeSoundBus.get_item_text(index))
+ ProjectSettings.save()
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://c2qgetjc3mfo3"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Audio/settings_audio.gd" id="1_2iyyr"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_o1ban"]
+
+[node name="Audio" type="VBoxContainer"]
+offset_right = 121.0
+offset_bottom = 58.0
+script = ExtResource("1_2iyyr")
+
+[node name="Label" type="Label" parent="."]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Music Channels"
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+text = "Max music channels"
+
+[node name="HintTooltip" parent="HBoxContainer" instance=ExtResource("2_o1ban")]
+layout_mode = 2
+texture = null
+hint_text = "Lowering this value may invalidate existing music events!"
+
+[node name="MusicChannelCount" type="SpinBox" parent="HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+min_value = 1.0
+value = 1.0
+
+[node name="TypingSoundsTitle" type="Label" parent="."]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Typing Sounds"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer2"]
+layout_mode = 2
+text = "Audio Bus"
+
+[node name="HintTooltip" parent="HBoxContainer2" instance=ExtResource("2_o1ban")]
+layout_mode = 2
+tooltip_text = "Lowering this value may invalidate existing music events!"
+texture = null
+hint_text = "The default audio bus used by TypeSound nodes."
+
+[node name="TypeSoundBus" type="OptionButton" parent="HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
--- /dev/null
+extends DialogicSubsystem
+## Subsystem for managing background music and one-shot sound effects.
+##
+## This subsystem has many different helper methods for managing audio
+## in your timeline.
+## For instance, you can listen to music changes via [signal music_started].
+
+
+## Whenever a new background music is started, this signal is emitted and
+## contains a dictionary with the following keys: [br]
+## [br]
+## Key | Value Type | Value [br]
+## ----------- | ------------- | ----- [br]
+## `path` | [type String] | The path to the audio resource file. [br]
+## `volume` | [type float] | The volume of the audio resource that will be set to the [member base_music_player]. [br]
+## `audio_bus` | [type String] | The audio bus name that the [member base_music_player] will use. [br]
+## `loop` | [type bool] | Whether the audio resource will loop or not once it finishes playing. [br]
+## `channel` | [type int] | The channel ID to play the audio on. [br]
+signal music_started(info: Dictionary)
+
+
+## Whenever a new sound effect is set, this signal is emitted and contains a
+## dictionary with the following keys: [br]
+## [br]
+## Key | Value Type | Value [br]
+## ----------- | ------------- | ----- [br]
+## `path` | [type String] | The path to the audio resource file. [br]
+## `volume` | [type float] | The volume of the audio resource that will be set to [member base_sound_player]. [br]
+## `audio_bus` | [type String] | The audio bus name that the [member base_sound_player] will use. [br]
+## `loop` | [type bool] | Whether the audio resource will loop or not once it finishes playing. [br]
+signal sound_started(info: Dictionary)
+
+
+var max_channels: int:
+ set(value):
+ if max_channels != value:
+ max_channels = value
+ ProjectSettings.set_setting('dialogic/audio/max_channels', value)
+ ProjectSettings.save()
+ current_music_player.resize(value)
+ get:
+ return ProjectSettings.get_setting('dialogic/audio/max_channels', 4)
+
+## Audio player base duplicated to play background music.
+##
+## Background music is long audio.
+var base_music_player := AudioStreamPlayer.new()
+## Reference to the last used music player.
+var current_music_player: Array[AudioStreamPlayer] = []
+## Audio player base, that will be duplicated to play sound effects.
+##
+## Sound effects are short audio.
+var base_sound_player := AudioStreamPlayer.new()
+
+
+#region STATE
+####################################################################################################
+
+## Clears the state on this subsystem and stops all audio.
+##
+## If you want to stop sounds only, use [method stop_all_sounds].
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ for idx in max_channels:
+ update_music('', 0.0, '', 0.0, true, idx)
+ stop_all_sounds()
+
+
+## Loads the state on this subsystem from the current state info.
+func load_game_state(load_flag:=LoadFlags.FULL_LOAD) -> void:
+ if load_flag == LoadFlags.ONLY_DNODES:
+ return
+ var info: Dictionary = dialogic.current_state_info.get("music", {})
+ if not info.is_empty() and info.has('path'):
+ update_music(info.path, info.volume, info.audio_bus, 0, info.loop, 0)
+ else:
+ for channel_id in info.keys():
+ if info[channel_id].is_empty() or info[channel_id].path.is_empty():
+ update_music('', 0.0, '', 0.0, true, channel_id)
+ else:
+ update_music(info[channel_id].path, info[channel_id].volume, info[channel_id].audio_bus, 0, info[channel_id].loop, channel_id)
+
+
+## Pauses playing audio.
+func pause() -> void:
+ for child in get_children():
+ child.stream_paused = true
+
+
+## Resumes playing audio.
+func resume() -> void:
+ for child in get_children():
+ child.stream_paused = false
+
+
+func _on_dialogic_timeline_ended() -> void:
+ if not dialogic.Styles.get_layout_node():
+ clear_game_state()
+ pass
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+func _ready() -> void:
+ dialogic.timeline_ended.connect(_on_dialogic_timeline_ended)
+
+ base_music_player.name = "Music"
+ add_child(base_music_player)
+
+ base_sound_player.name = "Sound"
+ add_child(base_sound_player)
+
+ current_music_player.resize(max_channels)
+
+
+## Updates the background music. Will fade out previous music.
+func update_music(path := "", volume := 0.0, audio_bus := "", fade_time := 0.0, loop := true, channel_id := 0) -> void:
+
+ if channel_id > max_channels:
+ printerr("\tChannel ID (%s) higher than Max Music Channels (%s)" % [channel_id, max_channels])
+ dialogic.print_debug_moment()
+ return
+
+ if not dialogic.current_state_info.has('music'):
+ dialogic.current_state_info['music'] = {}
+
+ dialogic.current_state_info['music'][channel_id] = {'path':path, 'volume':volume, 'audio_bus':audio_bus, 'loop':loop, 'channel':channel_id}
+ music_started.emit(dialogic.current_state_info['music'][channel_id])
+
+ var fader: Tween = null
+ if is_instance_valid(current_music_player[channel_id]) and current_music_player[channel_id].playing or !path.is_empty():
+ fader = create_tween()
+
+ var prev_node: Node = null
+ if is_instance_valid(current_music_player[channel_id]) and current_music_player[channel_id].playing:
+ prev_node = current_music_player[channel_id]
+ fader.tween_method(interpolate_volume_linearly.bind(prev_node), db_to_linear(prev_node.volume_db),0.0,fade_time)
+
+ if path:
+ current_music_player[channel_id] = base_music_player.duplicate()
+ add_child(current_music_player[channel_id])
+ current_music_player[channel_id].stream = load(path)
+ current_music_player[channel_id].volume_db = volume
+ if audio_bus:
+ current_music_player[channel_id].bus = audio_bus
+ if not current_music_player[channel_id].stream is AudioStreamWAV:
+ if "loop" in current_music_player[channel_id].stream:
+ current_music_player[channel_id].stream.loop = loop
+ elif "loop_mode" in current_music_player[channel_id].stream:
+ if loop:
+ current_music_player[channel_id].stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
+ else:
+ current_music_player[channel_id].stream.loop_mode = AudioStreamWAV.LOOP_DISABLED
+
+ current_music_player[channel_id].play(0)
+ fader.parallel().tween_method(interpolate_volume_linearly.bind(current_music_player[channel_id]), 0.0, db_to_linear(volume),fade_time)
+
+ if prev_node:
+ fader.tween_callback(prev_node.queue_free)
+
+
+## Whether music is playing.
+func has_music(channel_id := 0) -> bool:
+ return !dialogic.current_state_info.get('music', {}).get(channel_id, {}).get('path', '').is_empty()
+
+
+## Plays a given sound file.
+func play_sound(path: String, volume := 0.0, audio_bus := "", loop := false) -> void:
+ if base_sound_player != null and !path.is_empty():
+ sound_started.emit({'path':path, 'volume':volume, 'audio_bus':audio_bus, 'loop':loop})
+
+ var new_sound_node := base_sound_player.duplicate()
+ new_sound_node.name += "Sound"
+ new_sound_node.stream = load(path)
+
+ if "loop" in new_sound_node.stream:
+ new_sound_node.stream.loop = loop
+ elif "loop_mode" in new_sound_node.stream:
+ if loop:
+ new_sound_node.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
+ else:
+ new_sound_node.stream.loop_mode = AudioStreamWAV.LOOP_DISABLED
+
+ new_sound_node.volume_db = volume
+ if audio_bus:
+ new_sound_node.bus = audio_bus
+
+ add_child(new_sound_node)
+ new_sound_node.play()
+ new_sound_node.finished.connect(new_sound_node.queue_free)
+
+
+## Stops all audio.
+func stop_all_sounds() -> void:
+ for node in get_children():
+ if node == base_sound_player:
+ continue
+ if "Sound" in node.name:
+ node.queue_free()
+
+
+## Converts a linear loudness value to decibel and sets that volume to
+## the given [param node].
+func interpolate_volume_linearly(value: float, node: Node) -> void:
+ node.volume_db = linear_to_db(value)
+
+
+## Returns whether the currently playing audio resource is the same as this
+## event's [param resource_path], for [param channel_id].
+func is_music_playing_resource(resource_path: String, channel_id := 0) -> bool:
+ var is_playing_resource: bool = (current_music_player.size() > channel_id
+ and is_instance_valid(current_music_player[channel_id])
+ and current_music_player[channel_id].is_playing()
+ and current_music_player[channel_id].stream.resource_path == resource_path)
+
+ return is_playing_resource
+
+#endregion
--- /dev/null
+extends DialogicBackground
+
+## The default background scene.
+## Extend the DialogicBackground class to create your own background scene.
+
+@onready var image_node: TextureRect = $Image
+@onready var color_node: ColorRect = $ColorRect
+
+
+func _ready() -> void:
+ image_node.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
+ image_node.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
+
+ image_node.anchor_right = 1
+ image_node.anchor_bottom = 1
+
+
+func _update_background(argument:String, _time:float) -> void:
+ if argument.begins_with('res://'):
+ image_node.texture = load(argument)
+ color_node.color = Color.TRANSPARENT
+ elif argument.is_valid_html_color():
+ image_node.texture = null
+ color_node.color = Color(argument, 1)
+ else:
+ image_node.texture = null
+ color_node.color = Color.from_string(argument, Color.TRANSPARENT)
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://cl6g6ymkhjven"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Background/DefaultBackgroundScene/default_background.gd" id="1_nkdrp"]
+
+[node name="DefaultBackground" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_nkdrp")
+
+[node name="ColorRect" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Image" type="TextureRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 0
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_push_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_push_shader()
+ shader.set_shader_parameter('final_offset', Vector2.DOWN)
+ tween_shader_progress().set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
+
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_push_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_push_shader()
+ shader.set_shader_parameter('final_offset', Vector2.LEFT)
+ tween_shader_progress().set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
+
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_push_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_push_shader()
+ shader.set_shader_parameter('final_offset', Vector2.RIGHT)
+ tween_shader_progress().set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
+
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_push_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_push_shader()
+ shader.set_shader_parameter('final_offset', Vector2.UP)
+ tween_shader_progress().set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
+
--- /dev/null
+extends DialogicBackgroundTransition
+
+
+func _fade() -> void:
+ var shader := set_shader()
+ shader.set_shader_parameter("wipe_texture", load(this_folder.path_join("simple_fade.tres")))
+
+ shader.set_shader_parameter("feather", 1)
+
+ shader.set_shader_parameter("previous_background", prev_texture)
+ shader.set_shader_parameter("next_background", next_texture)
+
+ tween_shader_progress()
--- /dev/null
+[gd_resource type="GradientTexture2D" load_steps=2 format=3 uid="uid://qak7mr560k0i"]
+
+[sub_resource type="Gradient" id="Gradient_skd6w"]
+offsets = PackedFloat32Array(1)
+colors = PackedColorArray(0.423651, 0.423651, 0.423651, 1)
+
+[resource]
+gradient = SubResource("Gradient_skd6w")
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_swipe_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_swipe_shader()
+ var texture: GradientTexture2D = shader.get_shader_parameter('wipe_texture')
+ texture.fill_from = Vector2.DOWN
+ texture.fill_to = Vector2.RIGHT
+ tween_shader_progress()
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_swipe_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_swipe_shader()
+ var texture: GradientTexture2D = shader.get_shader_parameter('wipe_texture')
+
+ texture.fill_from = Vector2.ZERO
+ texture.fill_to = Vector2.RIGHT
+
+ tween_shader_progress()
--- /dev/null
+extends "res://addons/dialogic/Modules/Background/Transitions/simple_swipe_transitions.gd"
+
+func _fade() -> void:
+ var shader := setup_swipe_shader()
+ var texture: GradientTexture2D = shader.get_shader_parameter('wipe_texture')
+ texture.fill_from = Vector2.RIGHT
+ texture.fill_to = Vector2.ZERO
+ tween_shader_progress()
--- /dev/null
+class_name DialogicBackgroundTransition
+extends Node
+
+## Helper
+var this_folder: String = get_script().resource_path.get_base_dir()
+
+
+## Set before _fade() is called, will be the root node of the previous bg scene.
+var prev_scene: Node
+## Set before _fade() is called, will be the viewport texture of the previous bg scene.
+var prev_texture: ViewportTexture
+
+## Set before _fade() is called, will be the root node of the upcoming bg scene.
+var next_scene: Node
+## Set before _fade() is called, will be the viewport texture of the upcoming bg scene.
+var next_texture: ViewportTexture
+
+## Set before _fade() is called, will be the requested time for the fade
+var time: float
+
+## Set before _fade() is called, will be the background holder (TextureRect)
+var bg_holder: DialogicNode_BackgroundHolder
+
+
+signal transition_finished
+
+
+## To be overridden by transitions
+func _fade() -> void:
+ pass
+
+
+func set_shader(path_to_shader:String=DialogicUtil.get_module_path('Background').path_join("Transitions/default_transition_shader.gdshader")) -> ShaderMaterial:
+ if bg_holder:
+ if path_to_shader.is_empty():
+ bg_holder.material = null
+ bg_holder.color = Color.TRANSPARENT
+ return null
+ bg_holder.material = ShaderMaterial.new()
+ bg_holder.material.shader = load(path_to_shader)
+ return bg_holder.material
+ return null
+
+
+func tween_shader_progress(_progress_parameter:="progress") -> PropertyTweener:
+ if !bg_holder:
+ return
+
+ if !bg_holder.material is ShaderMaterial:
+ return
+
+ bg_holder.material.set_shader_parameter("progress", 0.0)
+ var tween := create_tween()
+ var tweener := tween.tween_property(bg_holder, "material:shader_parameter/progress", 1.0, time).from(0.0)
+ tween.tween_callback(emit_signal.bind('transition_finished'))
+ return tweener
--- /dev/null
+shader_type canvas_item;
+
+// Indicates how far the transition is (0 start, 1 end).
+uniform float progress : hint_range(0.0, 1.0);
+// The previous background, transparent if there was none.
+uniform sampler2D previous_background : source_color, hint_default_transparent;
+// The next background, transparent if there is none.
+uniform sampler2D next_background : source_color, hint_default_transparent;
+
+// The texture used to determine how far along the progress has to be for bending in the new background.
+uniform sampler2D wipe_texture : source_color;
+// The size of the trailing smear of the transition.
+uniform float feather : hint_range(0.0, 1.0, 0.0001) = 0.1;
+// Determines if the wipe texture should keep it's aspect ratio when scaled to the screen's size.
+uniform bool keep_aspect_ratio = false;
+
+void fragment() {
+ vec2 frag_coord = UV;
+ if(keep_aspect_ratio) {
+ vec2 ratio = (SCREEN_PIXEL_SIZE.x > SCREEN_PIXEL_SIZE.y) // determine how to scale
+ ? vec2(SCREEN_PIXEL_SIZE.y / SCREEN_PIXEL_SIZE.x, 1) // fit to width
+ : vec2(1, SCREEN_PIXEL_SIZE.x / SCREEN_PIXEL_SIZE.y); // fit to height
+
+ frag_coord *= ratio;
+ frag_coord += ((vec2(1,1) - ratio) / 2.0);
+ }
+
+ // get the blend factor between the previous and next background.
+ float alpha = (texture(wipe_texture, frag_coord).r) - progress;
+ float blend_factor = 1. - smoothstep(0., feather, alpha + (feather * (1. -progress)));
+
+ vec4 old_frag = texture(previous_background, UV);
+ vec4 new_frag = texture(next_background, UV);
+
+ COLOR = mix(old_frag, new_frag, blend_factor);
+}
--- /dev/null
+shader_type canvas_item;
+
+uniform vec2 final_offset = vec2(0,-1);
+uniform float progress: hint_range(0.0, 1.0);
+uniform sampler2D previous_background: source_color, hint_default_transparent;
+uniform sampler2D next_background: source_color, hint_default_transparent;
+
+
+void fragment() {
+ vec2 uv = UV + final_offset * progress*vec2(-1, -1);
+
+ if (uv.x < 1.0 && uv.x > 0.0 && uv.y < 1.0 && uv.y > 0.0){
+ COLOR = texture(previous_background, uv, 1);
+ } else {
+ COLOR = texture(next_background, uv-final_offset*vec2(-1,-1));
+ }
+}
--- /dev/null
+extends DialogicBackgroundTransition
+
+func setup_push_shader() -> ShaderMaterial:
+ var shader := set_shader(DialogicUtil.get_module_path('Background').path_join("Transitions/push_transition_shader.gdshader"))
+
+ shader.set_shader_parameter("previous_background", prev_texture)
+ shader.set_shader_parameter("next_background", next_texture)
+
+ return shader
--- /dev/null
+[gd_resource type="GradientTexture2D" load_steps=2 format=3 uid="uid://cweb3y3xc4uw0"]
+
+[sub_resource type="Gradient" id="Gradient_skd6w"]
+colors = PackedColorArray(0, 0, 0, 1, 0.991164, 0.991164, 0.991164, 1)
+
+[resource]
+gradient = SubResource("Gradient_skd6w")
--- /dev/null
+extends DialogicBackgroundTransition
+
+func setup_swipe_shader() -> ShaderMaterial:
+ var shader := set_shader()
+ shader.set_shader_parameter("wipe_texture", load(
+ DialogicUtil.get_module_path('Background').path_join("Transitions/simple_swipe_gradient.tres")
+ ))
+
+ shader.set_shader_parameter("feather", 0.3)
+
+ shader.set_shader_parameter("previous_background", prev_texture)
+ shader.set_shader_parameter("next_background", next_texture)
+
+ return shader
--- /dev/null
+extends Node
+class_name DialogicBackground
+
+## This is the base class for dialogic backgrounds.
+## Extend it and override it's methods when you create a custom background.
+## You can take a look at the default background to get an idea of how it's working.
+
+
+## The subviewport container that holds this background. Set when instanced.
+var viewport_container: SubViewportContainer
+## The viewport that holds this background. Set when instanced.
+var viewport: SubViewport
+
+
+## Load the new background in here.
+## The time argument is given for when [_should_do_background_update] returns true
+## (then you have to do a transition in here)
+func _update_background(_argument:String, _time:float) -> void:
+ pass
+
+
+## If a background event with this scene is encountered while this background is used,
+## this decides whether to create a new instance and call fade_out or just call [_update_background] # on this scene. Default is false
+func _should_do_background_update(_argument:String) -> bool:
+ return false
+
+
+## Called by dialogic when first created.
+## If you return false (by default) it will attempt to animate the "modulate" property.
+func _custom_fade_in(_time:float) -> bool:
+ return false
+
+
+## Called by dialogic before removing (done by dialogic).
+## If you return false (by default) it will attempt to animate the "modulate" property.
+func _custom_fade_out(_time:float) -> bool:
+ return false
+
--- /dev/null
+@tool
+class_name DialogicBackgroundEvent
+extends DialogicEvent
+
+## Event to show scenes in the background and switch between them.
+
+### Settings
+
+## The scene to use. If empty, this will default to the DefaultBackground.gd scene.
+## This scene supports images and fading.
+## If you set it to a scene path, then that scene will be instanced.
+## Learn more about custom backgrounds in the Subsystem_Background.gd docs.
+var scene := ""
+## The argument that is passed to the background scene.
+## For the default scene it's the path to the image to show.
+var argument := ""
+## The time the fade animation will take. Leave at 0 for instant change.
+var fade: float = 0.0
+## Name of the transition to use.
+var transition := ""
+
+## Helpers for visual editor
+enum ArgumentTypes {IMAGE, CUSTOM}
+var _arg_type := ArgumentTypes.IMAGE :
+ get:
+ if argument.begins_with("res://"):
+ return ArgumentTypes.IMAGE
+ else:
+ return _arg_type
+ set(value):
+ if value == ArgumentTypes.CUSTOM:
+ if argument.begins_with("res://"):
+ argument = " "+argument
+ _arg_type = value
+
+enum SceneTypes {DEFAULT, CUSTOM}
+var _scene_type := SceneTypes.DEFAULT :
+ get:
+ if scene.is_empty():
+ return _scene_type
+ else:
+ return SceneTypes.CUSTOM
+ set(value):
+ if value == SceneTypes.DEFAULT:
+ scene = ""
+ _scene_type = value
+
+#region EXECUTION
+################################################################################
+
+func _execute() -> void:
+ var final_fade_duration := fade
+
+ if dialogic.Inputs.auto_skip.enabled:
+ var time_per_event: float = dialogic.Inputs.auto_skip.time_per_event
+ final_fade_duration = min(fade, time_per_event)
+
+ dialogic.Backgrounds.update_background(scene, argument, final_fade_duration, transition)
+
+ finish()
+
+#endregion
+
+#region INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Background"
+ set_default_color('Color8')
+ event_category = "Visuals"
+ event_sorting_index = 0
+
+#endregion
+
+#region SAVE & LOAD
+################################################################################
+
+func get_shortcode() -> String:
+ return "background"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "scene" : {"property": "scene", "default": ""},
+ "arg" : {"property": "argument", "default": ""},
+ "fade" : {"property": "fade", "default": 0},
+ "transition" : {"property": "transition", "default": "",
+ "suggestions": get_transition_suggestions},
+ }
+
+
+#endregion
+
+#region EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('_scene_type', ValueType.FIXED_OPTIONS, {
+ 'left_text' :'Show',
+ 'options': [
+ {
+ 'label': 'Background',
+ 'value': SceneTypes.DEFAULT,
+ 'icon': ["GuiRadioUnchecked", "EditorIcons"]
+ },
+ {
+ 'label': 'Custom Scene',
+ 'value': SceneTypes.CUSTOM,
+ 'icon': ["PackedScene", "EditorIcons"]
+ }
+ ]})
+ add_header_label("with image", "_scene_type == SceneTypes.DEFAULT")
+ add_header_edit("scene", ValueType.FILE,
+ {'file_filter':'*.tscn, *.scn; Scene Files',
+ 'placeholder': "Custom scene",
+ 'editor_icon': ["PackedScene", "EditorIcons"],
+ }, '_scene_type == SceneTypes.CUSTOM')
+ add_header_edit('_arg_type', ValueType.FIXED_OPTIONS, {
+ 'left_text' : 'with',
+ 'options': [
+ {
+ 'label': 'Image',
+ 'value': ArgumentTypes.IMAGE,
+ 'icon': ["Image", "EditorIcons"]
+ },
+ {
+ 'label': 'Custom Argument',
+ 'value': ArgumentTypes.CUSTOM,
+ 'icon': ["String", "EditorIcons"]
+ }
+ ], "symbol_only": true}, "_scene_type == SceneTypes.CUSTOM")
+ add_header_edit('argument', ValueType.FILE,
+ {'file_filter':'*.jpg, *.jpeg, *.png, *.webp, *.tga, *svg, *.bmp, *.dds, *.exr, *.hdr; Supported Image Files',
+ 'placeholder': "No Image",
+ 'editor_icon': ["Image", "EditorIcons"],
+ },
+ '_arg_type == ArgumentTypes.IMAGE or _scene_type == SceneTypes.DEFAULT')
+ add_header_edit('argument', ValueType.SINGLELINE_TEXT, {}, '_arg_type == ArgumentTypes.CUSTOM')
+
+ add_body_edit("transition", ValueType.DYNAMIC_OPTIONS,
+ {'left_text':'Transition:',
+ 'empty_text':'Simple Fade',
+ 'suggestions_func':get_transition_suggestions,
+ 'editor_icon':["PopupMenu", "EditorIcons"]})
+ add_body_edit("fade", ValueType.NUMBER, {'left_text':'Fade time:'})
+
+
+func get_transition_suggestions(_filter:String="") -> Dictionary:
+ var transitions := DialogicResourceUtil.list_special_resources("BackgroundTransition")
+ var suggestions := {}
+ for i in transitions:
+ suggestions[DialogicUtil.pretty_name(i)] = {'value': DialogicUtil.pretty_name(i), 'editor_icon': ["PopupMenu", "EditorIcons"]}
+ return suggestions
+
+#endregion
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_background.gd')]
+
+func _get_subsystems() -> Array:
+ return [{'name':'Backgrounds', 'script':this_folder.path_join('subsystem_backgrounds.gd')}]
+
+
+func _get_special_resources() -> Dictionary:
+ return {&"BackgroundTransition":list_special_resources("Transitions/Defaults", ".gd")}
--- /dev/null
+class_name DialogicNode_BackgroundHolder
+extends ColorRect
+
+
+func _ready() -> void:
+ add_to_group('dialogic_background_holders')
--- /dev/null
+extends DialogicSubsystem
+## Subsystem for managing backgrounds.
+##
+## This subsystem has many different helper methods for managing backgrounds.
+## For instance, you can listen to background changes via
+## [signal background_changed].
+
+
+## Whenever a new background is set, this signal is emitted and contains a
+## dictionary with the following keys: [br]
+## [br]
+## Key | Value Type | Value [br]
+## ----------- | ------------- | ----- [br]
+## `scene` | [type String] | The scene path of the new background. [br]
+## `argument` | [type String] | Information given to the background on its update routine. [br]
+## `fade_time` | [type float] | The time the background may take to transition in. [br]
+## `same_scene`| [type bool] | If the new background uses the same Godot scene. [br]
+signal background_changed(info: Dictionary)
+
+## The default background scene Dialogic will use.
+var default_background_scene: PackedScene = load(get_script().resource_path.get_base_dir().path_join('DefaultBackgroundScene/default_background.tscn'))
+## The default transition Dialogic will use.
+var default_transition: String = get_script().resource_path.get_base_dir().path_join("Transitions/Defaults/simple_fade.gd")
+
+
+#region STATE
+####################################################################################################
+
+## Empties the current background state.
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ update_background()
+
+## Loads the background state from the current state info.
+func load_game_state(_load_flag := LoadFlags.FULL_LOAD) -> void:
+ update_background(dialogic.current_state_info.get('background_scene', ''), dialogic.current_state_info.get('background_argument', ''), 0.0, default_transition, true)
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## Method that adds a given scene as child of the DialogicNode_BackgroundHolder.
+## It will call [_update_background()] on that scene with the given argument [argument].
+## It will call [_fade_in()] on that scene with the given fade time.
+## Will call fade_out on previous backgrounds scene.
+##
+## If the scene is the same as the last background you can bypass another instantiating
+## and use the same scene.
+## To do so implement [_should_do_background_update()] on the custom background scene.
+## Then [_update_background()] will be called directly on that previous scene.
+func update_background(scene := "", argument := "", fade_time := 0.0, transition_path:=default_transition, force := false) -> void:
+ var background_holder: DialogicNode_BackgroundHolder
+ if dialogic.has_subsystem('Styles'):
+ background_holder = dialogic.Styles.get_first_node_in_layout('dialogic_background_holders')
+ else:
+ background_holder = get_tree().get_first_node_in_group('dialogic_background_holders')
+
+ var info := {'scene':scene, 'argument':argument, 'fade_time':fade_time, 'same_scene':false}
+ if background_holder == null:
+ background_changed.emit(info)
+ return
+
+
+ var bg_set := false
+
+ # First try just updating the existing scene.
+ if scene == dialogic.current_state_info.get('background_scene', ''):
+
+ if not force and argument == dialogic.current_state_info.get('background_argument', ''):
+ return
+
+ for old_bg in background_holder.get_children():
+ if !old_bg.has_meta('node') or not old_bg.get_meta('node') is DialogicBackground:
+ continue
+
+ var prev_bg_node: DialogicBackground = old_bg.get_meta('node')
+ if prev_bg_node._should_do_background_update(argument):
+ prev_bg_node._update_background(argument, fade_time)
+ bg_set = true
+ info['same_scene'] = true
+
+ dialogic.current_state_info['background_scene'] = scene
+ dialogic.current_state_info['background_argument'] = argument
+
+ if bg_set:
+ background_changed.emit(info)
+ return
+
+ var old_viewport: SubViewportContainer = null
+ if background_holder.has_meta('current_viewport'):
+ old_viewport = background_holder.get_meta('current_viewport', null)
+
+ var new_viewport: SubViewportContainer
+ if scene.ends_with('.tscn') and ResourceLoader.exists(scene):
+ new_viewport = add_background_node(load(scene), background_holder)
+ elif argument:
+ new_viewport = add_background_node(default_background_scene, background_holder)
+ else:
+ new_viewport = null
+
+ var trans_script: Script = load(DialogicResourceUtil.guess_special_resource("BackgroundTransition", transition_path, {"path":default_transition}).path)
+ var trans_node := Node.new()
+ trans_node.set_script(trans_script)
+ trans_node = (trans_node as DialogicBackgroundTransition)
+ trans_node.bg_holder = background_holder
+ trans_node.time = fade_time
+
+ if old_viewport:
+ trans_node.prev_scene = old_viewport.get_meta('node', null)
+ trans_node.prev_texture = old_viewport.get_child(0).get_texture()
+ old_viewport.get_meta('node')._custom_fade_out(fade_time)
+ old_viewport.hide()
+ # TODO We have to call this again here because of https://github.com/godotengine/godot/issues/23729
+ old_viewport.get_child(0).render_target_update_mode = SubViewport.UPDATE_ALWAYS
+ trans_node.transition_finished.connect(old_viewport.queue_free)
+ if new_viewport:
+ trans_node.next_scene = new_viewport.get_meta('node', null)
+ trans_node.next_texture = new_viewport.get_child(0).get_texture()
+ new_viewport.get_meta('node')._update_background(argument, fade_time)
+ new_viewport.get_meta('node')._custom_fade_in(fade_time)
+ else:
+ background_holder.remove_meta('current_viewport')
+
+ add_child(trans_node)
+ if fade_time == 0:
+ trans_node.transition_finished.emit()
+ _on_transition_finished(background_holder, trans_node)
+ else:
+ trans_node.transition_finished.connect(_on_transition_finished.bind(background_holder, trans_node))
+ # We need to break this connection if the background_holder get's removed during the transition
+ background_holder.tree_exited.connect(trans_node.disconnect.bind("transition_finished", _on_transition_finished))
+ trans_node._fade()
+
+ background_changed.emit(info)
+
+
+func _on_transition_finished(background_node:DialogicNode_BackgroundHolder, transition_node:DialogicBackgroundTransition) -> void:
+ if background_node.has_meta("current_viewport"):
+ if background_node.get_meta("current_viewport").get_meta("node", null) == transition_node.next_scene:
+ background_node.get_meta("current_viewport").show()
+ background_node.material = null
+ background_node.color = Color.TRANSPARENT
+ transition_node.queue_free()
+
+
+## Adds sub-viewport with the given background scene as child to
+## Dialogic scene.
+func add_background_node(scene:PackedScene, parent:DialogicNode_BackgroundHolder) -> SubViewportContainer:
+ var v_con := SubViewportContainer.new()
+ var viewport := SubViewport.new()
+ var b_scene := scene.instantiate()
+ if not b_scene is DialogicBackground:
+ printerr("[Dialogic] Given background scene was not of type DialogicBackground! Make sure the scene has a script that extends DialogicBackground.")
+ v_con.queue_free()
+ viewport.queue_free()
+ b_scene.queue_free()
+ return null
+
+ parent.add_child(v_con)
+ v_con.hide()
+ v_con.stretch = true
+ v_con.size = parent.size
+ v_con.set_anchors_preset(Control.PRESET_FULL_RECT)
+
+ v_con.add_child(viewport)
+ viewport.transparent_bg = true
+ viewport.disable_3d = true
+ viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
+ viewport.canvas_item_default_texture_filter = ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter")
+
+ viewport.add_child(b_scene)
+ b_scene.viewport = viewport
+ b_scene.viewport_container = v_con
+
+ parent.set_meta('current_viewport', v_con)
+ v_con.set_meta('node', b_scene)
+
+ return v_con
+
+
+## Whether a background is set.
+func has_background() -> bool:
+ return !dialogic.current_state_info.get('background_scene', '').is_empty() or !dialogic.current_state_info.get('background_argument','').is_empty()
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicCallEvent
+extends DialogicEvent
+
+## Event that allows calling a method in a node or autoload.
+
+### Settings
+
+## The name of the autoload to call the method on.
+var autoload_name := ""
+## The name of the method to call on the given autoload.
+var method := "":
+ set(value):
+ method = value
+ if Engine.is_editor_hint():
+ update_argument_info()
+ check_arguments_and_update_warning()
+## A list of arguments to give to the call.
+var arguments := []:
+ set(value):
+ arguments = value
+ if Engine.is_editor_hint():
+ check_arguments_and_update_warning()
+
+var _current_method_arg_hints := {'a':null, 'm':null, 'info':{}}
+
+################################################################################
+## EXECUTION
+################################################################################
+
+func _execute() -> void:
+ var object: Object = null
+ var obj_path := autoload_name
+ var autoload: Node = dialogic.get_node('/root/'+obj_path.get_slice('.', 0))
+ obj_path = obj_path.trim_prefix(obj_path.get_slice('.', 0)+'.')
+ object = autoload
+ if object:
+ while obj_path:
+ if obj_path.get_slice(".", 0) in object and object.get(obj_path.get_slice(".", 0)) is Object:
+ object = object.get(obj_path.get_slice(".", 0))
+ else:
+ break
+ obj_path = obj_path.trim_prefix(obj_path.get_slice('.', 0)+'.')
+
+ if object == null:
+ printerr("[Dialogic] Call event failed: Unable to find autoload '",autoload_name,"'")
+ finish()
+ return
+
+ if object.has_method(method):
+ var args := []
+ for arg in arguments:
+ if arg is String and arg.begins_with('@'):
+ args.append(dialogic.Expressions.execute_string(arg.trim_prefix('@')))
+ else:
+ args.append(arg)
+ dialogic.current_state = dialogic.States.WAITING
+ await object.callv(method, args)
+ dialogic.current_state = dialogic.States.IDLE
+ else:
+ printerr("[Dialogic] Call event failed: Autoload doesn't have the method '", method,"'.")
+
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Call"
+ set_default_color('Color6')
+ event_category = "Logic"
+ event_sorting_index = 10
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ var result := "do "
+ if autoload_name:
+ result += autoload_name
+ if method:
+ result += '.'+method
+ if arguments.is_empty():
+ result += '()'
+ else:
+ result += '('
+ for i in arguments:
+ if i is String and i.begins_with('@'):
+ result += i.trim_prefix('@')
+ else:
+ result += var_to_str(i)
+ result += ', '
+ result = result.trim_suffix(', ')+')'
+ return result
+
+
+func from_text(string:String) -> void:
+ var result := RegEx.create_from_string(r"do (?<autoload>[^\(]*)\.((?<method>[^.(]*)(\((?<arguments>.*)\))?)?").search(string.strip_edges())
+ if result:
+ autoload_name = result.get_string('autoload')
+ method = result.get_string('method')
+ if result.get_string('arguments').is_empty():
+ arguments = []
+ else:
+ var arr := []
+ for i in result.get_string('arguments').split(','):
+ i = i.strip_edges()
+ if str_to_var(i) != null:
+ arr.append(str_to_var(i))
+ else:
+ # Mark this as a complex expression
+ arr.append("@"+i)
+ arguments = arr
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges().begins_with("do"):
+ return true
+ return false
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "autoload" : {"property": "autoload_name", "default": ""},
+ "method" : {"property": "method", "default": ""},
+ "args" : {"property": "arguments", "default": []},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('autoload_name', ValueType.DYNAMIC_OPTIONS, {'left_text':'On autoload',
+ 'empty_text':'Autoload',
+ 'suggestions_func':get_autoload_suggestions,
+ 'editor_icon':["Node", "EditorIcons"]})
+ add_header_edit('method', ValueType.DYNAMIC_OPTIONS, {'left_text':'call',
+ 'empty_text':'Method',
+ 'suggestions_func':get_method_suggestions,
+ 'editor_icon':["Callable", "EditorIcons"]}, 'autoload_name')
+ add_body_edit('arguments', ValueType.ARRAY, {'left_text':'Arguments:'}, 'not autoload_name.is_empty() and not method.is_empty()')
+
+
+
+func get_autoload_suggestions(filter:String="") -> Dictionary:
+ var suggestions := {}
+
+ for prop in ProjectSettings.get_property_list():
+ if prop.name.begins_with('autoload/'):
+ var autoload: String = prop.name.trim_prefix('autoload/')
+ suggestions[autoload] = {'value': autoload, 'tooltip':autoload, 'editor_icon': ["Node", "EditorIcons"]}
+ if filter.begins_with(autoload):
+ suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
+ return suggestions
+
+
+func get_method_suggestions(filter:String="", temp_autoload:String = "") -> Dictionary:
+ var suggestions := {}
+
+ var script: Script
+ if temp_autoload and ProjectSettings.has_setting('autoload/'+temp_autoload):
+ script = load(ProjectSettings.get_setting('autoload/'+temp_autoload).trim_prefix('*'))
+
+ elif autoload_name and ProjectSettings.has_setting('autoload/'+autoload_name):
+ var loaded_autoload := load(ProjectSettings.get_setting('autoload/'+autoload_name).trim_prefix('*'))
+
+ if loaded_autoload is PackedScene:
+ var packed_scene: PackedScene = loaded_autoload
+ script = packed_scene.instantiate().get_script()
+
+ else:
+ script = loaded_autoload
+
+ if script:
+ for script_method in script.get_script_method_list():
+ if script_method.name.begins_with('@') or script_method.name.begins_with('_'):
+ continue
+ suggestions[script_method.name] = {'value': script_method.name, 'tooltip':script_method.name, 'editor_icon': ["Callable", "EditorIcons"]}
+ if !filter.is_empty():
+ suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
+ return suggestions
+
+
+func update_argument_info() -> void:
+ if autoload_name and method and not _current_method_arg_hints.is_empty() and (_current_method_arg_hints.a == autoload_name and _current_method_arg_hints.m == method):
+ if !ResourceLoader.exists(ProjectSettings.get_setting('autoload/'+autoload_name, '').trim_prefix('*')):
+ _current_method_arg_hints = {}
+ return
+ var script: Script = load(ProjectSettings.get_setting('autoload/'+autoload_name, '').trim_prefix('*'))
+ for m in script.get_script_method_list():
+ if m.name == method:
+ _current_method_arg_hints = {'a':autoload_name, 'm':method, 'info':m}
+ break
+
+
+func check_arguments_and_update_warning() -> void:
+ if not _current_method_arg_hints.has("info") or _current_method_arg_hints.info.is_empty():
+ ui_update_warning.emit()
+ return
+
+ var idx := -1
+ for arg in arguments:
+ idx += 1
+ if len(_current_method_arg_hints.info.args) <= idx:
+ continue
+ if _current_method_arg_hints.info.args[idx].type != 0:
+ if _current_method_arg_hints.info.args[idx].type != typeof(arg):
+ if arg is String and arg.begins_with('@'):
+ continue
+ var expected_type: String = ""
+ match _current_method_arg_hints.info.args[idx].type:
+ TYPE_BOOL: expected_type = "bool"
+ TYPE_STRING: expected_type = "string"
+ TYPE_FLOAT: expected_type = "float"
+ TYPE_INT: expected_type = "int"
+ _: expected_type = "something else"
+
+ ui_update_warning.emit('Argument '+ str(idx+1)+ ' ('+_current_method_arg_hints.info.args[idx].name+') has the wrong type (method expects '+expected_type+')!')
+ return
+
+ if len(arguments) < len(_current_method_arg_hints.info.args)-len(_current_method_arg_hints.info.default_args):
+ ui_update_warning.emit("The method is expecting at least "+str(len(_current_method_arg_hints.info.args)-len(_current_method_arg_hints.info.default_args))+ " arguments, but is given only "+str(len(arguments))+".")
+ return
+ elif len(arguments) > len(_current_method_arg_hints.info.args):
+ ui_update_warning.emit("The method is expecting at most "+str(len(_current_method_arg_hints.info.args))+ " arguments, but is given "+str(len(arguments))+".")
+ return
+ ui_update_warning.emit()
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ if line.count(' ') == 1 and not '.' in line:
+ for i in get_autoload_suggestions():
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+'.', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3), TextNode.get_theme_icon("Node", "EditorIcons"))
+ elif symbol == '.' and not '(' in line:
+ for i in get_method_suggestions('', line.get_slice('.', 0).trim_prefix('do ')):
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+'(', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3), TextNode.get_theme_icon("Callable", "EditorIcons"))
+
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'do', 'do ', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3), _get_icon())
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ dict[line.find('do')] = {"color":event_color.lerp(Highlighter.normal_color, 0.3)}
+ dict[line.find('do')+2] = {"color":event_color.lerp(Highlighter.normal_color, 0.5)}
+
+ Highlighter.color_region(dict, Highlighter.normal_color, line, '(', ')')
+ Highlighter.color_region(dict, Highlighter.string_color, line, '"', '"')
+ Highlighter.color_word(dict, Highlighter.boolean_operator_color, line, 'true')
+ Highlighter.color_word(dict, Highlighter.boolean_operator_color, line, 'false')
+ return dict
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_call.gd')]
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_OUT)
+ tween.tween_property(node, 'position:y', base_position.y-node.get_viewport().size.y/10, time*0.4).set_trans(Tween.TRANS_EXPO)
+ tween.parallel().tween_property(node, 'scale:y', base_scale.y*1.05, time*0.4).set_trans(Tween.TRANS_EXPO)
+ tween.tween_property(node, 'position:y', base_position.y, time*0.6).set_trans(Tween.TRANS_BOUNCE)
+ tween.parallel().tween_property(node, 'scale:y', base_scale.y, time*0.6).set_trans(Tween.TRANS_BOUNCE)
+ tween.finished.connect(emit_signal.bind('finished_once'))
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "bounce": {"type": AnimationType.ACTION},
+ }
--- /dev/null
+extends DialogicAnimation
+
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+
+ var end_scale: Vector2 = node.scale
+ var end_modulate_alpha := 1.0
+ var modulation_property := get_modulation_property()
+
+ if is_reversed:
+ end_scale = Vector2(0, 0)
+ end_modulate_alpha = 0.0
+
+ else:
+ node.scale = Vector2(0, 0)
+ var original_modulation: Color = node.get(modulation_property)
+ original_modulation.a = 0.0
+ node.set(modulation_property, original_modulation)
+
+
+ tween.set_ease(Tween.EASE_IN_OUT)
+ tween.set_trans(Tween.TRANS_SINE)
+ tween.set_parallel()
+
+ (tween.tween_property(node, "scale", end_scale, time)
+ .set_trans(Tween.TRANS_SPRING)
+ .set_ease(Tween.EASE_OUT))
+ tween.tween_property(node, modulation_property + ":a", end_modulate_alpha, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "bounce in": {"reversed": false, "type": AnimationType.IN},
+ "bounce out": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+
+ var start_height: float = base_position.y - node.get_viewport().size.y / 5
+ var end_height := base_position.y
+
+ var start_modulation := 0.0
+ var end_modulation := 1.0
+
+ if is_reversed:
+ end_height = start_height
+ start_height = base_position.y
+ end_modulation = 0.0
+ start_modulation = 1.0
+
+ node.position.y = start_height
+
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_SINE)
+ tween.set_parallel()
+
+ var end_postion := Vector2(base_position.x, end_height)
+ tween.tween_property(node, "position", end_postion, time)
+
+ var property := get_modulation_property()
+
+ var original_modulation: Color = node.get(property)
+ original_modulation.a = start_modulation
+ node.set(property, original_modulation)
+ var modulation_alpha := property + ":a"
+
+ tween.tween_property(node, modulation_alpha, end_modulation, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "fade in down": {"reversed": false, "type": AnimationType.IN},
+ "fade out up": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+
+ var modulation_property := get_modulation_property()
+ var end_modulation_alpha := 1.0
+
+ if is_reversed:
+ end_modulation_alpha = 0.0
+
+ else:
+ var original_modulation: Color = node.get(modulation_property)
+ original_modulation.a = 0.0
+ node.set(modulation_property, original_modulation)
+
+ var tween := (node.create_tween() as Tween)
+ if is_reversed:
+ tween.set_ease(Tween.EASE_IN)
+ else:
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_SINE)
+ tween.tween_property(node, modulation_property + ":a", end_modulation_alpha, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "fade in": {"reversed": false, "type": AnimationType.IN},
+ "fade out": {"reversed": true, "type": AnimationType.OUT},
+ "fade cross": {"type": AnimationType.CROSSFADE},
+ }
+
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+
+ var start_height: float = base_position.y + node.get_viewport().size.y / 5
+ var end_height := base_position.y
+
+ var start_modulation := 0.0
+ var end_modulation := 1.0
+
+ if is_reversed:
+ end_height = start_height
+ start_height = base_position.y
+ end_modulation = 0.0
+ start_modulation = 1.0
+
+ node.position.y = start_height
+
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_SINE)
+ tween.set_parallel()
+
+ var end_postion := Vector2(base_position.x, end_height)
+ tween.tween_property(node, "position", end_postion, time)
+
+ var property := get_modulation_property()
+
+ var original_modulation: Color = node.get(property)
+ original_modulation.a = start_modulation
+ node.set(property, original_modulation)
+ var modulation_alpha := property + ":a"
+
+ tween.tween_property(node, modulation_alpha, end_modulation, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "fade in up": {"reversed": false, "type": AnimationType.IN},
+ "fade out down": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.tween_property(node, 'scale', Vector2(1,1)*1.2, time*0.5).set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
+ tween.tween_property(node, 'scale', Vector2(1,1), time*0.5).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT)
+ tween.finished.connect(emit_signal.bind('finished_once'))
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "heartbeat": {"type": AnimationType.ACTION},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ await node.get_tree().process_frame
+ finished.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "instant in": {"reversed": false, "type": AnimationType.IN},
+ "instant out": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
+ var strength: float = node.get_viewport().size.x/60
+ var bound_multitween := DialogicUtil.multitween.bind(node, "position", "animation_shake_x")
+ tween.tween_method(bound_multitween, Vector2(), Vector2(1, 0)*strength, time*0.2)
+ tween.tween_method(bound_multitween, Vector2(), Vector2(-1,0)*strength, time*0.1)
+ tween.tween_method(bound_multitween, Vector2(), Vector2(1, 0)*strength, time*0.1)
+ tween.tween_method(bound_multitween, Vector2(), Vector2(-1,0)*strength, time*0.1)
+ tween.tween_method(bound_multitween, Vector2(), Vector2(1, 0)*strength, time*0.1)
+ tween.tween_method(bound_multitween, Vector2(), Vector2(-1,0)*strength, time*0.1)
+ tween.tween_method(bound_multitween, Vector2(), Vector2(1, 0)*strength, time*0.2)
+ tween.finished.connect(emit_signal.bind('finished_once'))
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "shake x": {"type": AnimationType.ACTION},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
+
+ var strength: float = node.get_viewport().size.y/40
+ tween.tween_property(node, 'position:y', base_position.y + strength, time * 0.2)
+ tween.tween_property(node, 'position:y', base_position.y - strength, time * 0.1)
+ tween.tween_property(node, 'position:y', base_position.y + strength, time * 0.1)
+ tween.tween_property(node, 'position:y', base_position.y - strength, time * 0.1)
+ tween.tween_property(node, 'position:y', base_position.y + strength, time * 0.1)
+ tween.tween_property(node, 'position:y', base_position.y - strength, time * 0.1)
+ tween.tween_property(node, 'position:y', base_position.y + strength, time * 0.1)
+ tween.tween_property(node, 'position:y', base_position.y, time * 0.2)
+
+ tween.finished.connect(emit_signal.bind('finished_once'))
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "shake y": {"type": AnimationType.ACTION},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
+
+ var target_position := base_position.y
+ var start_position: float = -node.get_viewport().size.y
+
+ if is_reversed:
+ target_position = -node.get_viewport().size.y
+ start_position = base_position.y
+
+ node.position.y = start_position
+
+ tween.tween_property(node, 'position:y', target_position, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "slide in down": {"reversed": false, "type": AnimationType.IN},
+ "slide out up": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
+
+ var end_position_x: float = base_position.x
+
+ if is_reversed:
+ end_position_x = -node.get_viewport().size.x / 2
+
+ else:
+ node.position.x = -node.get_viewport().size.x / 5
+
+ tween.tween_property(node, 'position:x', end_position_x, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "slide in left": {"reversed": false, "type": AnimationType.IN},
+ "slide out right": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
+
+ var viewport_x: float = node.get_viewport().size.x
+
+ var start_position_x: float = viewport_x + viewport_x / 5
+ var end_position_x := base_position.x
+
+ if is_reversed:
+ start_position_x = base_position.x
+ end_position_x = viewport_x + node.get_viewport().size.x / 5
+
+
+ node.position.x = start_position_x
+ tween.tween_property(node, 'position:x', end_position_x, time)
+
+ tween.finished.connect(emit_signal.bind('finished_once'))
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "slide in right": {"reversed": false, "type": AnimationType.IN},
+ "slide out left": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
+
+ var start_position_y: float = node.get_viewport().size.y * 2
+ var end_position_y := base_position.y
+
+ if is_reversed:
+ start_position_y = base_position.y
+ end_position_y = node.get_viewport().size.y * 2
+
+ node.position.y = start_position_y
+ tween.tween_property(node, 'position:y', end_position_y, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "slide in up": {"reversed": false, "type": AnimationType.IN},
+ "slide out down": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var tween := (node.create_tween() as Tween)
+ tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
+
+ var strength: float = 0.01
+
+ tween.set_parallel(true)
+ tween.tween_property(node, 'scale', Vector2(1,1)*(1+strength), time*0.3)
+ tween.tween_property(node, 'rotation', -strength, time*0.1).set_delay(time*0.2)
+ tween.tween_property(node, 'rotation', strength, time*0.1).set_delay(time*0.3)
+ tween.tween_property(node, 'rotation', -strength, time*0.1).set_delay(time*0.4)
+ tween.tween_property(node, 'rotation', strength, time*0.1).set_delay(time*0.5)
+ tween.tween_property(node, 'rotation', -strength, time*0.1).set_delay(time*0.6)
+ tween.chain().tween_property(node, 'scale', Vector2(1,1), time*0.3)
+ tween.parallel().tween_property(node, 'rotation', 0.0, time*0.3)
+
+ tween.finished.connect(emit_signal.bind('finished_once'))
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "tada": {"type": AnimationType.ACTION},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var modulate_property := get_modulation_property()
+ var modulate_alpha_property := modulate_property + ":a"
+
+ var end_scale: Vector2 = node.scale
+ var end_modulation_alpha := 1.0
+
+ if is_reversed:
+ end_modulation_alpha = 0.0
+
+ else:
+ node.scale = Vector2(0, 0)
+ node.position.y = base_position.y - node.get_viewport().size.y * 0.5
+
+ var original_modulation: Color = node.get(modulate_property)
+ original_modulation.a = 0.0
+ node.set(modulate_property, original_modulation)
+
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_EXPO)
+ tween.set_parallel(true)
+ tween.tween_property(node, "scale", end_scale, time)
+ tween.tween_property(node, "position", base_position, time)
+ tween.tween_property(node, modulate_alpha_property, end_modulation_alpha, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "zoom center in": {"reversed": false, "type": AnimationType.IN},
+ "zoom center out": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+extends DialogicAnimation
+
+func animate() -> void:
+ var modulate_property := get_modulation_property()
+ var modulate_alpha_property := modulate_property + ":a"
+
+ var end_scale: Vector2 = node.scale
+ var end_modulation_alpha := 1.0
+
+ if is_reversed:
+ end_scale = Vector2(0, 0)
+ end_modulation_alpha = 0.0
+
+ else:
+ node.scale = Vector2(0,0)
+
+ var original_modulation: Color = node.get(modulate_property)
+ original_modulation.a = 0.0
+ node.set(modulate_property, original_modulation)
+
+ var tween := (node.create_tween() as Tween)
+ tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
+ tween.set_parallel(true)
+ tween.tween_property(node, "scale", end_scale, time)
+ tween.tween_property(node, modulate_alpha_property, end_modulation_alpha, time)
+
+ await tween.finished
+ finished_once.emit()
+
+
+func _get_named_variations() -> Dictionary:
+ return {
+ "zoom in": {"reversed": false, "type": AnimationType.IN},
+ "zoom out": {"reversed": true, "type": AnimationType.OUT},
+ }
--- /dev/null
+@tool
+class_name DialogicPortraitAnimationUtil
+
+enum AnimationType {ALL=-1, IN=1, OUT=2, ACTION=3, CROSSFADE=4}
+
+
+static func guess_animation(string:String, type := AnimationType.ALL) -> String:
+ var default := {}
+ var filter := {}
+ var ignores := []
+ match type:
+ AnimationType.ALL:
+ pass
+ AnimationType.IN:
+ filter = {"type":AnimationType.IN}
+ ignores = ["in"]
+ AnimationType.OUT:
+ filter = {"type":AnimationType.OUT}
+ ignores = ["out"]
+ AnimationType.ACTION:
+ filter = {"type":AnimationType.ACTION}
+ AnimationType.CROSSFADE:
+ filter = {"type":AnimationType.CROSSFADE}
+ ignores = ["cross"]
+ return DialogicResourceUtil.guess_special_resource(&"PortraitAnimation", string, default, filter, ignores).get("path", "")
+
+
+static func get_portrait_animations_filtered(type := AnimationType.ALL) -> Dictionary:
+ var filter := {"type":type}
+ if type == AnimationType.ALL:
+ filter["type"] = [AnimationType.IN, AnimationType.OUT, AnimationType.ACTION]
+ return DialogicResourceUtil.list_special_resources("PortraitAnimation", filter)
+
+
+static func get_suggestions(_search_text := "", current_value:= "", empty_text := "Default", action := AnimationType.ALL) -> Dictionary:
+ var suggestions := {}
+
+ if empty_text and current_value:
+ suggestions[empty_text] = {'value':"", 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
+
+ for anim_name in get_portrait_animations_filtered(action):
+ suggestions[DialogicUtil.pretty_name(anim_name)] = {
+ 'value' : DialogicUtil.pretty_name(anim_name),
+ 'editor_icon' : ["Animation", "EditorIcons"]
+ }
+
+ return suggestions
--- /dev/null
+class_name DialogicAnimation
+extends Node
+
+## Class that can be used to animate portraits. Can be extended to create animations.
+
+enum AnimationType {IN=1, OUT=2, ACTION=3, CROSSFADE=4}
+
+signal finished_once
+signal finished
+
+## Set at runtime, will be the node to animate.
+var node: Node
+
+## Set at runtime, will be the length of the animation.
+var time: float
+
+## Set at runtime, will be the base position of the node.
+## Depending on the animation, this might be the start, end or both.
+var base_position: Vector2
+## Set at runtime, will be the base scale of the node.
+var base_scale: Vector2
+
+## Used to repeate the animation for a number of times.
+var repeats: int
+
+## If `true`, the animation will be reversed.
+## This must be implemented by each animation or it will have no effect.
+var is_reversed: bool = false
+
+
+func _ready() -> void:
+ finished_once.connect(finished_one_loop)
+
+
+## To be overridden. Do the actual animating/tweening in here.
+## Use the properties [member node], [member time], [member base_position], etc.
+func animate() -> void:
+ pass
+
+
+## This method controls whether to repeat the animation or not.
+## Animations must call this once they finished an animation.
+func finished_one_loop() -> void:
+ repeats -= 1
+
+ if repeats > 0:
+ animate()
+
+ else:
+ finished.emit()
+
+
+func pause() -> void:
+ if node:
+ node.process_mode = Node.PROCESS_MODE_DISABLED
+
+
+func resume() -> void:
+ if node:
+ node.process_mode = Node.PROCESS_MODE_INHERIT
+
+
+func _get_named_variations() -> Dictionary:
+ return {}
+
+
+## If the animation wants to change the modulation, this method
+## will return the property to change.
+##
+## The [class CanvasGroup] can use `self_modulate` instead of `modulate`
+## to uniformly change the modulation of all children without additively
+## overlaying the modulations.
+func get_modulation_property() -> String:
+ if node is CanvasGroup:
+ return "self_modulate"
+ else:
+ return "modulate"
--- /dev/null
+@tool
+extends DialogicPortrait
+
+## Default portrait scene.
+## The parent class has a character and portrait variable.
+
+@export_group('Main')
+@export_file var image := ""
+
+
+## Load anything related to the given character and portrait
+func _update_portrait(passed_character:DialogicCharacter, passed_portrait:String) -> void:
+ apply_character_and_portrait(passed_character, passed_portrait)
+
+ apply_texture($Portrait, image)
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://b32paf0ll6um8"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Character/default_portrait.gd" id="1_wn77n"]
+
+[node name="DefaultPortrait" type="Node2D"]
+script = ExtResource("1_wn77n")
+
+[node name="Portrait" type="Sprite2D" parent="."]
+centered = false
--- /dev/null
+class_name DialogicPortrait
+extends Node
+
+## Default portrait class. Should be extended by custom portraits.
+
+## Stores the character that this scene displays.
+var character: DialogicCharacter
+## Stores the name of the current portrait.
+var portrait: String
+
+#region MAIN OVERRIDES
+################################################################################
+
+## This function can be overridden.
+## If this returns true, it won't instance a new scene, but call
+## [method _update_portrait] on this one.
+## This is only relevant if the next portrait uses the same scene.
+## This allows implementing transitions between portraits that use the same scene.
+func _should_do_portrait_update(_character: DialogicCharacter, _portrait: String) -> bool:
+ return false
+
+
+## If the custom portrait accepts a change, then accept it here
+## You should position your portrait so that the root node is at the pivot point*.
+## For example for a simple sprite this code would work:
+## >>> $Sprite.position = $Sprite.get_rect().size * Vector2(-0.5, -1)
+##
+## * this depends on the portrait containers, but it will most likely be the bottom center (99% of cases)
+func _update_portrait(_passed_character: DialogicCharacter, _passed_portrait: String) -> void:
+ pass
+
+
+## This should be implemented. It is used for sizing in the
+## character editor preview and in portrait containers.
+## Scale and offset will be applied by Dialogic.
+## For example, a simple sprite:
+## >>> return Rect2($Sprite.position, $Sprite.get_rect().size)
+##
+## This will only work as expected if the portrait is positioned so that the
+## root is at the pivot point.
+##
+## If you've used apply_texture this should work automatically.
+func _get_covered_rect() -> Rect2:
+ if has_meta('texture_holder_node') and get_meta('texture_holder_node', null) != null and is_instance_valid(get_meta('texture_holder_node')):
+ var node: Node = get_meta('texture_holder_node')
+ if node is Sprite2D or node is TextureRect:
+ return Rect2(node.position, node.get_rect().size)
+ return Rect2()
+
+
+## If implemented, this is called when the mirror changes
+func _set_mirror(mirror:bool) -> void:
+ if has_meta('texture_holder_node') and get_meta('texture_holder_node', null) != null and is_instance_valid(get_meta('texture_holder_node')):
+ var node: Node = get_meta('texture_holder_node')
+ if node is Sprite2D or node is TextureRect:
+ node.flip_h = mirror
+
+
+## Function to accept and use the extra data, if the custom portrait wants to accept it
+func _set_extra_data(_data: String) -> void:
+ pass
+
+#endregion
+
+#region HIGHLIGHT OVERRIDES
+################################################################################
+
+## Called when this becomes the active speaker
+func _highlight() -> void:
+ pass
+
+
+## Called when this stops being the active speaker
+func _unhighlight() -> void:
+ pass
+#endregion
+
+
+#region HELPERS
+################################################################################
+
+## Helper that quickly setups and checks the character and portrait.
+func apply_character_and_portrait(passed_character:DialogicCharacter, passed_portrait:String) -> void:
+ if passed_portrait == "" or not passed_portrait in passed_character.portraits.keys():
+ passed_portrait = passed_character.default_portrait
+
+ portrait = passed_portrait
+ character = passed_character
+
+
+func apply_texture(node:Node, texture_path:String) -> void:
+ if not character or not character.portraits.has(portrait):
+ return
+
+ if not "texture" in node:
+ return
+
+ node.texture = null
+
+ if not ResourceLoader.exists(texture_path):
+ # This is a leftover from alpha.
+ # Removing this will break any portraits made before alpha-10
+ if ResourceLoader.exists(character.portraits[portrait].get('image', '')):
+ texture_path = character.portraits[portrait].get('image', '')
+ else:
+ return
+
+ node.texture = load(texture_path)
+
+ if node is Sprite2D or node is TextureRect:
+ if node is Sprite2D:
+ node.centered = false
+ node.scale = Vector2.ONE
+ if node is TextureRect:
+ if !is_inside_tree():
+ await ready
+ node.position = node.get_rect().size * Vector2(-0.5, -1)
+
+ set_meta('texture_holder_node', node)
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicCharacterEvent
+extends DialogicEvent
+## Event that allows to manipulate character portraits.
+
+enum Actions {JOIN, LEAVE, UPDATE}
+
+### Settings
+
+## The type of action of this event (JOIN/LEAVE/UPDATE). See [Actions].
+var action := Actions.JOIN
+## The character that will join/leave/update.
+var character: DialogicCharacter = null
+## For Join/Update, this will be the portrait of the character that is shown.
+## Not used on Leave.
+## If empty, the default portrait will be used.
+var portrait := ""
+## The index of the position this character should move to
+var transform := "center"
+
+## Name of the animation script (extending DialogicAnimation).
+## On Join/Leave empty (default) will fallback to the animations set in the settings.
+## On Update empty will mean no animation.
+var animation_name := ""
+## Length of the animation.
+var animation_length: float = 0.5
+## How often the animation is repeated. Only for Update events.
+var animation_repeats: int = 1
+## If true, the events waits for the animation to finish before the next event starts.
+var animation_wait := false
+
+## The fade animation to use. If left empty, the default cross-fade animation AND time will be used.
+var fade_animation := ""
+var fade_length := 0.5
+
+## For Update only. If bigger then 0, the portrait will tween to the
+## new position (if changed) in this time (in seconds).
+var transform_time: float = 0.0
+var transform_ease := Tween.EaseType.EASE_IN_OUT
+var transform_trans := Tween.TransitionType.TRANS_SINE
+
+var ease_options := [
+ {'label': 'In', 'value': Tween.EASE_IN},
+ {'label': 'Out', 'value': Tween.EASE_OUT},
+ {'label': 'In_Out', 'value': Tween.EASE_IN_OUT},
+ {'label': 'Out_In', 'value': Tween.EASE_OUT_IN},
+ ]
+
+var trans_options := [
+ {'label': 'Linear', 'value': Tween.TRANS_LINEAR},
+ {'label': 'Sine', 'value': Tween.TRANS_SINE},
+ {'label': 'Quint', 'value': Tween.TRANS_QUINT},
+ {'label': 'Quart', 'value': Tween.TRANS_QUART},
+ {'label': 'Quad', 'value': Tween.TRANS_QUAD},
+ {'label': 'Expo', 'value': Tween.TRANS_EXPO},
+ {'label': 'Elastic', 'value': Tween.TRANS_ELASTIC},
+ {'label': 'Cubic', 'value': Tween.TRANS_CUBIC},
+ {'label': 'Circ', 'value': Tween.TRANS_CIRC},
+ {'label': 'Bounce', 'value': Tween.TRANS_BOUNCE},
+ {'label': 'Back', 'value': Tween.TRANS_BACK},
+ {'label': 'Spring', 'value': Tween.TRANS_SPRING}
+ ]
+
+## The z_index that the portrait should have.
+var z_index: int = 0
+## If true, the portrait will be set to mirrored.
+var mirrored := false
+## If set, will be passed to the portrait scene.
+var extra_data := ""
+
+
+### Helpers
+
+## Indicators for whether something should be updated (UPDATE mode only)
+var set_portrait := false
+var set_transform := false
+var set_z_index := false
+var set_mirrored := false
+## Used to set the character resource from the unique name identifier and vice versa
+var character_identifier: String:
+ get:
+ if character_identifier == '--All--':
+ return '--All--'
+ if character:
+ var identifier := DialogicResourceUtil.get_unique_identifier(character.resource_path)
+ if not identifier.is_empty():
+ return identifier
+ return character_identifier
+ set(value):
+ character_identifier = value
+ character = DialogicResourceUtil.get_character_resource(value)
+ if character and not character.portraits.has(portrait):
+ portrait = ""
+ ui_update_needed.emit()
+
+var regex := RegEx.create_from_string(r'(?<type>join|update|leave)\s*(")?(?<name>(?(2)[^"\n]*|[^(: \n]*))(?(2)"|)(\W*\((?<portrait>.*)\))?(\s*(?<transform>[^\[]*))?(\s*\[(?<shortcode>.*)\])?')
+
+################################################################################
+## EXECUTION
+################################################################################
+
+func _execute() -> void:
+ if not character and not character_identifier == "--All--":
+ finish()
+ return
+
+ # Calculate animation time (can be shortened during skipping)
+ var final_animation_length: float = animation_length
+ var final_position_move_time: float = transform_time
+ if dialogic.Inputs.auto_skip.enabled:
+ var max_time: float = dialogic.Inputs.auto_skip.time_per_event
+ final_animation_length = min(max_time, animation_length)
+ final_position_move_time = min(max_time, transform_time)
+
+
+ # JOIN -------------------------------------
+ if action == Actions.JOIN:
+ if dialogic.has_subsystem('History') and !dialogic.Portraits.is_character_joined(character):
+ var character_name_text := dialogic.Text.get_character_name_parsed(character)
+ dialogic.History.store_simple_history_entry(character_name_text + " joined", event_name, {'character': character_name_text, 'mode':'Join'})
+
+ await dialogic.Portraits.join_character(
+ character, portrait, transform,
+ mirrored, z_index, extra_data,
+ animation_name, final_animation_length, animation_wait)
+
+ # LEAVE -------------------------------------
+ elif action == Actions.LEAVE:
+ if character_identifier == '--All--':
+ if dialogic.has_subsystem('History') and len(dialogic.Portraits.get_joined_characters()):
+ dialogic.History.store_simple_history_entry("Everyone left", event_name, {'character': "All", 'mode':'Leave'})
+
+ await dialogic.Portraits.leave_all_characters(
+ animation_name,
+ final_animation_length,
+ animation_wait
+ )
+
+ elif character:
+ if dialogic.has_subsystem('History') and dialogic.Portraits.is_character_joined(character):
+ var character_name_text := dialogic.Text.get_character_name_parsed(character)
+ dialogic.History.store_simple_history_entry(character_name_text+" left", event_name, {'character': character_name_text, 'mode':'Leave'})
+
+ await dialogic.Portraits.leave_character(
+ character,
+ animation_name,
+ final_animation_length,
+ animation_wait
+ )
+
+ # UPDATE -------------------------------------
+ elif action == Actions.UPDATE:
+ if not character or not dialogic.Portraits.is_character_joined(character):
+ finish()
+ return
+
+ if set_portrait:
+ dialogic.Portraits.change_character_portrait(character, portrait, fade_animation, fade_length)
+
+ dialogic.Portraits.change_character_extradata(character, extra_data)
+
+ if set_mirrored:
+ dialogic.Portraits.change_character_mirror(character, mirrored)
+
+ if set_z_index:
+ dialogic.Portraits.change_character_z_index(character, z_index)
+
+ if set_transform:
+ dialogic.Portraits.move_character(character, transform, final_position_move_time, transform_ease, transform_trans)
+
+ if animation_name:
+ var final_animation_repetitions: int = animation_repeats
+
+ if dialogic.Inputs.auto_skip.enabled:
+ var time_per_event: float = dialogic.Inputs.auto_skip.time_per_event
+ var time_for_repetitions: float = time_per_event / animation_repeats
+ final_animation_length = time_for_repetitions
+
+ var animation := dialogic.Portraits.animate_character(
+ character,
+ animation_name,
+ final_animation_length,
+ final_animation_repetitions,
+ )
+
+ if animation_wait:
+ dialogic.current_state = DialogicGameHandler.States.ANIMATING
+ await animation.finished
+ dialogic.current_state = DialogicGameHandler.States.IDLE
+
+
+ finish()
+
+
+#region INITIALIZE
+###############################################################################
+
+func _init() -> void:
+ event_name = "Character"
+ set_default_color('Color2')
+ event_category = "Main"
+ event_sorting_index = 2
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon.svg'))
+
+#endregion
+
+#region SAVING, LOADING, DEFAULTS
+################################################################################
+
+func to_text() -> String:
+ var result_string := ""
+
+ # ACTIONS
+ match action:
+ Actions.JOIN: result_string += "join "
+ Actions.LEAVE: result_string += "leave "
+ Actions.UPDATE: result_string += "update "
+
+ var default_values := DialogicUtil.get_custom_event_defaults(event_name)
+
+ # CHARACTER IDENTIFIER
+ if action == Actions.LEAVE and character_identifier == '--All--':
+ result_string += "--All--"
+ elif character:
+ var name := DialogicResourceUtil.get_unique_identifier(character.resource_path)
+
+ if name.count(" ") > 0:
+ name = '"' + name + '"'
+
+ result_string += name
+
+ # PORTRAIT
+ if portrait.strip_edges() != default_values.get('portrait', ''):
+ if action != Actions.LEAVE and (action != Actions.UPDATE or set_portrait):
+ result_string += " (" + portrait + ")"
+
+ # TRANSFORM
+ if action == Actions.JOIN or (action == Actions.UPDATE and set_transform):
+ result_string += " " + str(transform)
+
+ # SETS:
+ if action == Actions.JOIN or action == Actions.LEAVE:
+ set_mirrored = mirrored != default_values.get("mirrored", false)
+ set_z_index = z_index != default_values.get("z_index", 0)
+
+ var shortcode := store_to_shortcode_parameters()
+
+ if shortcode != "":
+ result_string += " [" + shortcode + "]"
+
+ return result_string
+
+
+func from_text(string:String) -> void:
+ # Load default character
+ character = DialogicResourceUtil.get_character_resource(character_identifier)
+
+ var result := regex.search(string)
+
+ # ACTION
+ match result.get_string('type'):
+ "join": action = Actions.JOIN
+ "leave": action = Actions.LEAVE
+ "update": action = Actions.UPDATE
+
+ # CHARACTER
+ var given_name := result.get_string('name').strip_edges()
+ var given_portrait := result.get_string('portrait').strip_edges()
+ var given_transform := result.get_string('transform').strip_edges()
+
+ if given_name:
+ if action == Actions.LEAVE and given_name == "--All--":
+ character_identifier = '--All--'
+ else:
+ character = DialogicResourceUtil.get_character_resource(given_name)
+
+ # PORTRAIT
+ if given_portrait:
+ portrait = given_portrait.trim_prefix('(').trim_suffix(')')
+ set_portrait = true
+
+ # TRANSFORM
+ if given_transform:
+ transform = given_transform
+ set_transform = true
+
+ # SHORTCODE
+ if not result.get_string('shortcode'):
+ return
+
+ load_from_shortcode_parameters(result.get_string('shortcode'))
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "action" : {"property": "action", "default": 0, "custom_stored":true,
+ "suggestions": func(): return {'Join':
+ {'value':Actions.JOIN},
+ 'Leave':{'value':Actions.LEAVE},
+ 'Update':{'value':Actions.UPDATE}}},
+ "character" : {"property": "character_identifier", "default": "", "custom_stored":true,},
+ "portrait" : {"property": "portrait", "default": "", "custom_stored":true,},
+ "transform" : {"property": "transform", "default": "center", "custom_stored":true,},
+
+ "animation" : {"property": "animation_name", "default": ""},
+ "length" : {"property": "animation_length", "default": 0.5},
+ "wait" : {"property": "animation_wait", "default": false},
+ "repeat" : {"property": "animation_repeats", "default": 1},
+
+ "z_index" : {"property": "z_index", "default": 0},
+ "mirrored" : {"property": "mirrored", "default": false},
+ "fade" : {"property": "fade_animation", "default":""},
+ "fade_length" : {"property": "fade_length", "default":0.5},
+ "move_time" : {"property": "transform_time", "default": 0.0},
+ "move_ease" : {"property": "transform_ease", "default": Tween.EaseType.EASE_IN_OUT,
+ "suggestions": func(): return list_to_suggestions(ease_options)},
+ "move_trans" : {"property": "transform_trans", "default": Tween.TransitionType.TRANS_SINE,
+ "suggestions": func(): return list_to_suggestions(trans_options)},
+ "extra_data" : {"property": "extra_data", "default": ""},
+ }
+
+
+func is_valid_event(string:String) -> bool:
+ if string.begins_with("join") or string.begins_with("leave") or string.begins_with("update"):
+ return true
+ return false
+
+#endregion
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('action', ValueType.FIXED_OPTIONS, {
+ 'options': [
+ {
+ 'label': 'Join',
+ 'value': Actions.JOIN,
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/join.svg")
+ },
+ {
+ 'label': 'Leave',
+ 'value': Actions.LEAVE,
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/leave.svg")
+ },
+ {
+ 'label': 'Update',
+ 'value': Actions.UPDATE,
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/update.svg")
+ }
+ ]
+ })
+ add_header_edit('character_identifier', ValueType.DYNAMIC_OPTIONS,
+ {'placeholder' : 'Character',
+ 'file_extension' : '.dch',
+ 'mode' : 2,
+ 'suggestions_func' : get_character_suggestions,
+ 'icon' : load("res://addons/dialogic/Editor/Images/Resources/character.svg"),
+ 'autofocus' : true})
+
+ add_header_edit('set_portrait', ValueType.BOOL_BUTTON,
+ {'icon':load("res://addons/dialogic/Modules/Character/update_portrait.svg"),
+ 'tooltip':'Change Portrait'}, "should_show_portrait_selector() and action == Actions.UPDATE")
+ add_header_edit('portrait', ValueType.DYNAMIC_OPTIONS,
+ {'placeholder' : 'Default',
+ 'collapse_when_empty':true,
+ 'suggestions_func' : get_portrait_suggestions,
+ 'icon' : load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")},
+ 'should_show_portrait_selector() and (action != Actions.UPDATE or set_portrait)')
+ add_header_edit('set_transform', ValueType.BOOL_BUTTON,
+ {'icon': load("res://addons/dialogic/Modules/Character/update_position.svg"), 'tooltip':'Change Position'}, "character != null and !has_no_portraits() and action == Actions.UPDATE")
+ add_header_label('at position', 'character != null and !has_no_portraits() and action == Actions.JOIN')
+ add_header_label('to position', 'character != null and !has_no_portraits() and action == Actions.UPDATE and set_transform')
+ add_header_edit('transform', ValueType.DYNAMIC_OPTIONS,
+ {'placeholder' : 'center',
+ 'mode' : 0,
+ 'suggestions_func' : get_position_suggestions,
+ 'tooltip' : "You can use a predefined position or a custom transform like 'pos=x0.5y1 size=x0.5y1 rot=10'.\nLearn more about this in the documentation."},
+ 'character != null and !has_no_portraits() and action != %s and (action != Actions.UPDATE or set_transform)' %Actions.LEAVE)
+
+ # Body
+ add_body_edit('fade_animation', ValueType.DYNAMIC_OPTIONS,
+ {'left_text' : 'Fade:',
+ 'suggestions_func' : get_fade_suggestions,
+ 'editor_icon' : ["Animation", "EditorIcons"],
+ 'placeholder' : 'Default',
+ 'enable_pretty_name' : true},
+ 'should_show_fade_options()')
+ add_body_edit('fade_length', ValueType.NUMBER, {'left_text':'Length:', 'suffix':'s', "min":0},
+ 'should_show_fade_options() and !fade_animation.is_empty()')
+ add_body_line_break("should_show_fade_options()")
+ add_body_edit('animation_name', ValueType.DYNAMIC_OPTIONS,
+ {'left_text' : 'Animation:',
+ 'suggestions_func' : get_animation_suggestions,
+ 'editor_icon' : ["Animation", "EditorIcons"],
+ 'placeholder' : 'Default',
+ 'enable_pretty_name' : true},
+ 'should_show_animation_options()')
+ add_body_edit('animation_length', ValueType.NUMBER, {'left_text':'Length:', 'suffix':'s', "min":0},
+ 'should_show_animation_options() and !animation_name.is_empty()')
+ add_body_edit('animation_wait', ValueType.BOOL, {'left_text':'Await end:'},
+ 'should_show_animation_options() and !animation_name.is_empty()')
+ add_body_edit('animation_repeats', ValueType.NUMBER, {'left_text':'Repeat:', 'mode':1, "min":1},
+ 'should_show_animation_options() and !animation_name.is_empty() and action == %s)' %Actions.UPDATE)
+ add_body_line_break()
+ add_body_edit('transform_time', ValueType.NUMBER, {'left_text':'Movement duration:', "min":0},
+ "should_show_transform_options()")
+ add_body_edit("transform_trans", ValueType.FIXED_OPTIONS, {'options':trans_options, 'left_text':"Trans:"}, 'should_show_transform_options() and transform_time > 0')
+ add_body_edit("transform_ease", ValueType.FIXED_OPTIONS, {'options':ease_options, 'left_text':"Ease:"}, 'should_show_transform_options() and transform_time > 0')
+
+ add_body_edit('set_z_index', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Character/update_z_index.svg"), 'tooltip':'Change Z-Index'}, "character != null and action == Actions.UPDATE")
+ add_body_edit('z_index', ValueType.NUMBER, {'left_text':'Z-index:', 'mode':1},
+ 'action != %s and (action != Actions.UPDATE or set_z_index)' %Actions.LEAVE)
+ add_body_edit('set_mirrored', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Character/update_mirror.svg"), 'tooltip':'Change Mirroring'}, "character != null and action == Actions.UPDATE")
+ add_body_edit('mirrored', ValueType.BOOL, {'left_text':'Mirrored:'},
+ 'action != %s and (action != Actions.UPDATE or set_mirrored)' %Actions.LEAVE)
+ add_body_edit('extra_data', ValueType.SINGLELINE_TEXT, {'left_text':'Extra Data:'}, 'action != Actions.LEAVE')
+
+
+func should_show_transform_options() -> bool:
+ return action == Actions.UPDATE and set_transform
+
+
+func should_show_animation_options() -> bool:
+ return (character and !character.portraits.is_empty()) or character_identifier == '--All--'
+
+
+func should_show_fade_options() -> bool:
+ return action == Actions.UPDATE and set_portrait and character and not character.portraits.is_empty()
+
+
+func should_show_portrait_selector() -> bool:
+ return character and len(character.portraits) > 1 and action != Actions.LEAVE
+
+
+func has_no_portraits() -> bool:
+ return character and character.portraits.is_empty()
+
+
+func get_character_suggestions(search_text:String) -> Dictionary:
+ return DialogicUtil.get_character_suggestions(search_text, character, false, action == Actions.LEAVE, editor_node)
+
+
+func get_portrait_suggestions(search_text:String) -> Dictionary:
+ var empty_text := "Don't Change"
+ if action == Actions.JOIN:
+ empty_text = "Default portrait"
+ return DialogicUtil.get_portrait_suggestions(search_text, character, true, empty_text)
+
+
+func get_position_suggestions(search_text:String='') -> Dictionary:
+ return DialogicUtil.get_portrait_position_suggestions(search_text)
+
+
+func get_animation_suggestions(search_text:String='') -> Dictionary:
+ var DPAU := DialogicPortraitAnimationUtil
+ match action:
+ Actions.JOIN:
+ return DPAU.get_suggestions(search_text, animation_name, "Default", DPAU.AnimationType.IN)
+ Actions.LEAVE:
+ return DPAU.get_suggestions(search_text, animation_name, "Default", DPAU.AnimationType.OUT)
+ Actions.UPDATE:
+ return DPAU.get_suggestions(search_text, animation_name, "None", DPAU.AnimationType.ACTION)
+ return {}
+
+
+func get_fade_suggestions(search_text:String='') -> Dictionary:
+ return DialogicPortraitAnimationUtil.get_suggestions(search_text, fade_animation, "Default", DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ var line_until_caret: String = CodeCompletionHelper.get_line_untill_caret(line)
+ if symbol == ' ' and line_until_caret.count(' ') == 1:
+ CodeCompletionHelper.suggest_characters(TextNode, CodeEdit.KIND_MEMBER)
+ if line.begins_with('leave'):
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'All', '--All-- ', event_color, TextNode.get_theme_icon("GuiEllipsis", "EditorIcons"))
+
+ if symbol == '(':
+ var completion_character := regex.search(line).get_string('name')
+ CodeCompletionHelper.suggest_portraits(TextNode, completion_character)
+
+ elif not '[' in line_until_caret and symbol == ' ':
+ if not line.begins_with("leave"):
+ for position in get_position_suggestions():
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, position, position+' ', TextNode.syntax_highlighter.normal_color)
+
+ # Shortcode Part
+ if '[' in line_until_caret:
+ # Suggest Parameters
+ if symbol == '[' or symbol == ' ' and line_until_caret.count('"')%2 == 0:# and (symbol == "[" or (symbol == " " and line_until_caret.rfind('="') < line_until_caret.rfind('"')-1)):
+ suggest_parameter("animation", line, TextNode)
+
+ if "animation=" in line:
+ for param in ["length", "wait"]:
+ suggest_parameter(param, line, TextNode)
+ if line.begins_with('update'):
+ suggest_parameter("repeat", line, TextNode)
+ if line.begins_with("update"):
+ for param in ["move_time", "move_trans", "move_ease"]:
+ suggest_parameter(param, line, TextNode)
+ if not line.begins_with('leave'):
+ for param in ["mirrored", "z_index", "extra_data"]:
+ suggest_parameter(param, line, TextNode)
+
+ # Suggest Values
+ else:
+ var current_param: RegExMatch = CodeCompletionHelper.completion_shortcode_param_getter_regex.search(line)
+ if not current_param:
+ return
+
+ match current_param.get_string("param"):
+ "animation":
+ var animations := {}
+ if line.begins_with('join'):
+ animations = DialogicPortraitAnimationUtil.get_portrait_animations_filtered(DialogicPortraitAnimationUtil.AnimationType.IN)
+ elif line.begins_with('update'):
+ animations = DialogicPortraitAnimationUtil.get_portrait_animations_filtered(DialogicPortraitAnimationUtil.AnimationType.ACTION)
+ elif line.begins_with('leave'):
+ animations = DialogicPortraitAnimationUtil.get_portrait_animations_filtered(DialogicPortraitAnimationUtil.AnimationType.OUT)
+
+ for script: String in animations:
+ TextNode.add_code_completion_option(CodeEdit.KIND_VARIABLE, DialogicUtil.pretty_name(script), DialogicUtil.pretty_name(script), TextNode.syntax_highlighter.normal_color, null, '" ')
+
+ "wait", "mirrored":
+ CodeCompletionHelper.suggest_bool(TextNode, TextNode.syntax_highlighter.normal_color)
+ "move_trans":
+ CodeCompletionHelper.suggest_custom_suggestions(list_to_suggestions(trans_options), TextNode, TextNode.syntax_highlighter.normal_color)
+ "move_ease":
+ CodeCompletionHelper.suggest_custom_suggestions(list_to_suggestions(ease_options), TextNode, TextNode.syntax_highlighter.normal_color)
+
+
+func suggest_parameter(parameter:String, line:String, TextNode:TextEdit) -> void:
+ if not parameter + "=" in line:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, parameter, parameter + '="', TextNode.syntax_highlighter.normal_color)
+
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'join', 'join ', event_color, load('res://addons/dialogic/Editor/Images/Dropdown/join.svg'))
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'leave', 'leave ', event_color, load('res://addons/dialogic/Editor/Images/Dropdown/leave.svg'))
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'update', 'update ', event_color, load('res://addons/dialogic/Editor/Images/Dropdown/update.svg'))
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ var word := line.get_slice(' ', 0)
+
+ dict[line.find(word)] = {"color":event_color}
+ dict[line.find(word)+len(word)] = {"color":Highlighter.normal_color}
+ var result := regex.search(line)
+ if result.get_string('name'):
+ dict[result.get_start('name')] = {"color":event_color.lerp(Highlighter.normal_color, 0.5)}
+ dict[result.get_end('name')] = {"color":Highlighter.normal_color}
+ if result.get_string('portrait'):
+ dict[result.get_start('portrait')] = {"color":event_color.lerp(Highlighter.normal_color, 0.6)}
+ dict[result.get_end('portrait')] = {"color":Highlighter.normal_color}
+ if result.get_string('shortcode'):
+ dict = Highlighter.color_shortcode_content(dict, line, result.get_start('shortcode'), result.get_end('shortcode'), event_color)
+ return dict
+
+
+## HELPER
+func list_to_suggestions(list:Array) -> Dictionary:
+ return list.reduce(
+ func(accum, value):
+ accum[value.label] = value
+ accum[value.label]["text_alt"] = [value.label.to_lower()]
+ return accum,
+ {})
--- /dev/null
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M21.0625 10.483C21.0625 13.5111 18.6077 15.9659 15.5795 15.9659C12.5514 15.9659 10.0966 13.5111 10.0966 10.483C10.0966 7.4548 12.5514 5 15.5795 5C18.6077 5 21.0625 7.4548 21.0625 10.483Z" fill="white"/>
+<path d="M22.3158 24.1393C22.3158 27 19.3349 27 15.6579 27C11.9808 27 9 27 9 24.1393C9 19.0046 11.9808 14.8421 15.6579 14.8421C19.3349 14.8421 22.3158 19.0046 22.3158 24.1393Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_character.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Portraits', 'script':this_folder.path_join('subsystem_portraits.gd')}, {'name':'PortraitContainers', 'script':this_folder.path_join('subsystem_containers.gd')}]
+
+func _get_settings_pages() -> Array:
+ return [this_folder.path_join('settings_portraits.tscn')]
+
+func _get_text_effects() -> Array[Dictionary]:
+ return [
+ {'command':'portrait', 'subsystem':'Portraits', 'method':'text_effect_portrait', 'arg':true},
+ {'command':'extra_data', 'subsystem':'Portraits', 'method':'text_effect_extradata', 'arg':true},
+ ]
+
+
+func _get_special_resources() -> Dictionary:
+ return {&'PortraitAnimation': list_animations("DefaultAnimations")}
+
+
+func _get_portrait_scene_presets() -> Array[Dictionary]:
+ return [
+ {
+ "path": "",
+ "name": "Default Scene",
+ "description": "The default scene defined in Settings>Portraits.",
+ "author":"Dialogic",
+ "type": "Default",
+ "icon":"",
+ "preview_image":[this_folder.path_join("default_portrait_thumbnail.png")],
+ "documentation":"",
+ },
+ {
+ "path": "CUSTOM",
+ "name": "Custom Scene",
+ "description": "A custom scene. Should extend DialogicPortrait and be in @tool mode.",
+ "author":"Dialogic",
+ "type": "Custom",
+ "icon":"",
+ "preview_image":[this_folder.path_join("custom_portrait_thumbnail.png")],
+ "documentation":"https://docs.dialogic.pro/custom-portraits.html",
+ },
+ {
+ "path": this_folder.path_join("default_portrait.tscn"),
+ "name": "Simple Image Portrait",
+ "description": "Can display images as portraits. Does nothing else.",
+ "author":"Dialogic",
+ "type": "General",
+ "icon":"",
+ "preview_image":[this_folder.path_join("simple_image_portrait_thumbnail.png")],
+ "documentation":"",
+ }
+ ]
--- /dev/null
+@tool
+class_name DialogicNode_PortraitContainer
+extends Control
+
+## Node that defines a position for dialogic portraits and how to display portraits at that position.
+
+enum PositionModes {
+ POSITION, ## This container can be joined/moved to with the Character Event
+ SPEAKER, ## This container is joined/left automatically based on the speaker.
+ }
+
+@export var mode := PositionModes.POSITION
+
+@export_subgroup('Mode: Position')
+## The position this node corresponds to.
+@export var container_ids: PackedStringArray = ["1"]
+
+
+@export_subgroup('Mode: Speaker')
+## Can be used to use a different portrait.
+## E.g. "Faces/" would mean instead of "happy" it will use portrait "Faces/happy"
+@export var portrait_prefix := ''
+
+@export_subgroup('Portrait Placement')
+enum SizeModes {
+ KEEP, ## The height and width of the container have no effect, only the origin.
+ FIT_STRETCH, ## The portrait will be fitted into the container, ignoring it's aspect ratio and the character/portrait scale.
+ FIT_IGNORE_SCALE, ## The portrait will be fitted into the container, ignoring the character/portrait scale, but preserving the aspect ratio.
+ FIT_SCALE_HEIGHT ## Recommended. The portrait will be scaled to fit the container height. A character/portrait scale of 100% means 100% container height. Aspect ratio will be preserved.
+ }
+## Defines how to affect the scale of the portrait
+@export var size_mode: SizeModes = SizeModes.FIT_SCALE_HEIGHT :
+ set(mode):
+ size_mode = mode
+ _update_debug_portrait_transform()
+
+## If true, portraits will be mirrored in this position.
+@export var mirrored := false :
+ set(mirror):
+ mirrored = mirror
+ _update_debug_portrait_scene()
+
+
+@export_group('Origin', 'origin')
+enum OriginAnchors {TOP_LEFT, TOP_CENTER, TOP_RIGHT, LEFT_MIDDLE, CENTER, RIGHT_MIDDLE, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT}
+## The portrait will be placed relative to this point in the container.
+@export var origin_anchor: OriginAnchors = OriginAnchors.BOTTOM_CENTER :
+ set(anchor):
+ origin_anchor = anchor
+ _update_debug_origin()
+
+## An offset to apply to the origin. Rarely useful.
+@export var origin_offset := Vector2() :
+ set(offset):
+ origin_offset = offset
+ _update_debug_origin()
+
+enum PivotModes {AT_ORIGIN, PERCENTAGE, PIXELS}
+## Usually you want to rotate or scale around the portrait origin.
+## For the moments where that is not the case, set the mode to PERCENTAGE or PIXELS and use [member pivot_value].
+@export var pivot_mode: PivotModes = PivotModes.AT_ORIGIN
+## Only has an effect when [member pivot_mode] is not AT_ORIGIN. Meaning depends on whether [member pivot_mode] is PERCENTAGE or PIXELS.
+@export var pivot_value := Vector2()
+
+@export_group('Debug', 'debug')
+## A character that will be displayed in the editor, useful for getting the right size.
+@export var debug_character: DialogicCharacter = null:
+ set(character):
+ debug_character = character
+ _update_debug_portrait_scene()
+@export var debug_character_portrait := "":
+ set(portrait):
+ debug_character_portrait = portrait
+ _update_debug_portrait_scene()
+
+var debug_character_holder_node: Node2D = null
+var debug_character_scene_node: Node = null
+var debug_origin: Sprite2D = null
+var default_portrait_scene: String = DialogicUtil.get_module_path('Character').path_join("default_portrait.tscn")
+# Used if no debug character is specified
+var default_debug_character := load(DialogicUtil.get_module_path('Character').path_join("preview_character.tres"))
+
+var ignore_resize := false
+
+
+func _ready() -> void:
+ match mode:
+ PositionModes.POSITION:
+ add_to_group('dialogic_portrait_con_position')
+ PositionModes.SPEAKER:
+ add_to_group('dialogic_portrait_con_speaker')
+
+ if Engine.is_editor_hint():
+ resized.connect(_update_debug_origin)
+
+ if !ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
+ default_portrait_scene = ProjectSettings.get_setting('dialogic/portraits/default_portrait', '')
+
+ debug_origin = Sprite2D.new()
+ add_child(debug_origin)
+ debug_origin.texture = load("res://addons/dialogic/Editor/Images/Dropdown/default.svg")
+
+ _update_debug_origin()
+ _update_debug_portrait_scene()
+ else:
+ resized.connect(update_portrait_transforms)
+
+
+################################################################################
+## MAIN METHODS
+################################################################################
+
+func update_portrait_transforms() -> void:
+ if ignore_resize:
+ return
+
+ match pivot_mode:
+ PivotModes.AT_ORIGIN:
+ pivot_offset = _get_origin_position()
+ PivotModes.PERCENTAGE:
+ pivot_offset = size*pivot_value
+ PivotModes.PIXELS:
+ pivot_offset = pivot_value
+
+ for child in get_children():
+ DialogicUtil.autoload().Portraits._update_character_transform(child)
+
+
+## Returns a Rect2 with the position as the position and the scale as the size.
+func get_local_portrait_transform(portrait_rect:Rect2, character_scale:=1.0) -> Rect2:
+ var transform := Rect2()
+ transform.position = _get_origin_position()
+
+ # Mode that ignores the containers size
+ if size_mode == SizeModes.KEEP:
+ transform.size = Vector2(1,1) * character_scale
+
+ # Mode that makes sure neither height nor width go out of container
+ elif size_mode == SizeModes.FIT_IGNORE_SCALE:
+ if size.x/size.y < portrait_rect.size.x/portrait_rect.size.y:
+ transform.size = Vector2(1,1) * size.x/portrait_rect.size.x
+ else:
+ transform.size = Vector2(1,1) * size.y/portrait_rect.size.y
+
+ # Mode that stretches the portrait to fill the whole container
+ elif size_mode == SizeModes.FIT_STRETCH:
+ transform.size = size/portrait_rect.size
+
+ # Mode that size the character so 100% size fills the height
+ elif size_mode == SizeModes.FIT_SCALE_HEIGHT:
+ transform.size = Vector2(1,1) * size.y / portrait_rect.size.y*character_scale
+
+ return transform
+
+
+## Returns the current origin position
+func _get_origin_position(rect_size = null) -> Vector2:
+ if rect_size == null:
+ rect_size = size
+ return rect_size * Vector2(origin_anchor%3 / 2.0, floor(origin_anchor/3.0) / 2.0) + origin_offset
+
+
+func is_container(id:Variant) -> bool:
+ return str(id) in container_ids
+
+#region DEBUG METHODS
+################################################################################
+### USE THIS TO DEBUG THE POSITIONS
+#func _draw():
+ #draw_rect(Rect2(Vector2(), size), Color(1, 0.3098039329052, 1), false, 2)
+ #draw_string(get_theme_default_font(),get_theme_default_font().get_string_size(container_ids[0], HORIZONTAL_ALIGNMENT_LEFT, 1, get_theme_default_font_size()) , container_ids[0], HORIZONTAL_ALIGNMENT_CENTER)
+#
+#func _process(delta:float) -> void:
+ #queue_redraw()
+
+
+## Loads the debug_character with the debug_character_portrait
+## Creates a holder node and applies mirror
+func _update_debug_portrait_scene() -> void:
+ if !Engine.is_editor_hint():
+ return
+ if is_instance_valid(debug_character_holder_node):
+ for child in get_children():
+ if child != debug_origin:
+ child.free()
+
+ # Get character
+ var character := _get_debug_character()
+ if not character is DialogicCharacter or character.portraits.is_empty():
+ return
+
+ # Determine portrait
+ var debug_portrait := debug_character_portrait
+ if debug_portrait.is_empty():
+ debug_portrait = character.default_portrait
+ if mode == PositionModes.SPEAKER and !portrait_prefix.is_empty():
+ if portrait_prefix+debug_portrait in character.portraits:
+ debug_portrait = portrait_prefix+debug_portrait
+ if not debug_portrait in character.portraits:
+ debug_portrait = character.default_portrait
+
+ var portrait_info: Dictionary = character.get_portrait_info(debug_portrait)
+
+ # Determine scene
+ var portrait_scene_path: String = portrait_info.get('scene', default_portrait_scene)
+ if portrait_scene_path.is_empty():
+ portrait_scene_path = default_portrait_scene
+
+ debug_character_scene_node = load(portrait_scene_path).instantiate()
+
+ if !is_instance_valid(debug_character_scene_node):
+ return
+
+ # Load portrait
+ DialogicUtil.apply_scene_export_overrides(debug_character_scene_node, character.portraits[debug_portrait].get('export_overrides', {}))
+ debug_character_scene_node._update_portrait(character, debug_portrait)
+
+ # Add character node
+ if !is_instance_valid(debug_character_holder_node):
+ debug_character_holder_node = Node2D.new()
+ add_child(debug_character_holder_node)
+
+ # Add portrait node
+ debug_character_holder_node.add_child(debug_character_scene_node)
+ move_child(debug_character_holder_node, 0)
+ debug_character_scene_node._set_mirror(character.mirror != mirrored != portrait_info.get('mirror', false))
+
+ _update_debug_portrait_transform()
+
+
+## Set's the size and position of the holder and scene node
+## according to the size_mode
+func _update_debug_portrait_transform() -> void:
+ if !Engine.is_editor_hint() or !is_instance_valid(debug_character_scene_node) or !is_instance_valid(debug_origin):
+ return
+ var character := _get_debug_character()
+ var portrait_info := character.get_portrait_info(debug_character_portrait)
+ var transform := get_local_portrait_transform(debug_character_scene_node._get_covered_rect(), character.scale*portrait_info.get('scale', 1))
+ debug_character_holder_node.position = transform.position
+ debug_character_scene_node.position = portrait_info.get('offset', Vector2())+character.offset
+
+ debug_character_holder_node.scale = transform.size
+
+
+## Updates the debug origins position. Also calls _update_debug_portrait_transform()
+func _update_debug_origin() -> void:
+ if !Engine.is_editor_hint() or !is_instance_valid(debug_origin):
+ return
+ debug_origin.position = _get_origin_position()
+ _update_debug_portrait_transform()
+
+
+
+## Returns the debug character or the default debug character
+func _get_debug_character() -> DialogicCharacter:
+ return debug_character if debug_character != null else default_debug_character
+
+#endregion
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="64" height="64" viewBox="0 0 64 64" fill="none" version="1.1" id="svg13818" sodipodi:docname="event_portrait_position.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs13822" />
+ <sodipodi:namedview id="namedview13820" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="false" inkscape:zoom="6.5390625" inkscape:cx="-20.109916" inkscape:cy="11.622461" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg13818" />
+ <path d="m 41.97648,22.585683 c 0,5.509774 -4.466676,9.976311 -9.976424,9.976311 -5.509775,0 -9.976338,-4.466537 -9.976338,-9.976311 0,-5.509775 4.466563,-9.976328 9.976338,-9.976328 5.509748,0 9.976424,4.466553 9.976424,9.976328 z" fill="#ffffff" id="path13814" style="stroke-width:2.74349" />
+ <path d="m 43.971538,47.35052 c 0,5.164069 -5.359673,5.164069 -11.971482,5.164069 -6.611724,0 -11.971595,0 -11.971595,-5.164069 0,-9.269422 5.359871,-16.783783 11.971595,-16.783783 6.611808,0 11.971482,7.514361 11.971482,16.783783 z" fill="#ffffff" id="path13816" style="stroke-width:2.74349" />
+ <rect style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:4.081;stroke-linecap:round;stroke-miterlimit:3.2;stroke-dasharray:4.081,8.162;stroke-dashoffset:0" id="rect14304" width="35.873577" height="49.97176" x="14.06331" y="6.9472136" ry="0.19193064" />
+</svg>
--- /dev/null
+[gd_resource type="Resource" script_class="DialogicCharacter" load_steps=2 format=3 uid="uid://dykf1j17ct5mo"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Resources/character.gd" id="1_qsljv"]
+
+[resource]
+script = ExtResource("1_qsljv")
+display_name = "Preview"
+nicknames = [""]
+color = Color(1, 1, 1, 1)
+description = ""
+scale = 1.0
+offset = Vector2(0, 0)
+mirror = false
+default_portrait = "character"
+portraits = {
+"character": {
+"image": "res://addons/dialogic/Editor/Images/preview_character.png",
+"offset": Vector2(0, 0),
+"scene": "res://addons/dialogic/Modules/Character/default_portrait.tscn"
+},
+"speaker": {
+"image": "res://addons/dialogic/Editor/Images/preview_character_speaker.png",
+"offset": Vector2(0, 0),
+"scene": "res://addons/dialogic/Modules/Character/default_portrait.tscn"
+}
+}
+custom_info = {
+"sound_mood_default": "",
+"sound_moods": {},
+"style": ""
+}
+metadata/timeline_not_saved = true
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+
+const POSITION_SUGGESTION_KEY := 'dialogic/portraits/position_suggestion_names'
+
+const DEFAULT_PORTRAIT_SCENE_KEY := 'dialogic/portraits/default_portrait'
+
+const ANIMATION_JOIN_DEFAULT_KEY := 'dialogic/animations/join_default'
+const ANIMATION_JOIN_DEFAULT_LENGTH_KEY := 'dialogic/animations/join_default_length'
+const ANIMATION_JOIN_DEFAULT_WAIT_KEY := 'dialogic/animations/join_default_wait'
+const ANIMATION_LEAVE_DEFAULT_KEY := 'dialogic/animations/leave_default'
+const ANIMATION_LEAVE_DEFAULT_LENGTH_KEY := 'dialogic/animations/leave_default_length'
+const ANIMATION_LEAVE_DEFAULT_WAIT_KEY := 'dialogic/animations/leave_default_wait'
+const ANIMATION_CROSSFADE_DEFAULT_KEY := 'dialogic/animations/cross_fade_default'
+const ANIMATION_CROSSFADE_DEFAULT_LENGTH_KEY:= 'dialogic/animations/cross_fade_default_length'
+
+
+func _ready():
+ %JoinDefault.get_suggestions_func = get_join_animation_suggestions
+ %JoinDefault.mode = 1
+ %LeaveDefault.get_suggestions_func = get_leave_animation_suggestions
+ %LeaveDefault.mode = 1
+ %CrossFadeDefault.get_suggestions_func = get_crossfade_animation_suggestions
+ %CrossFadeDefault.mode = 1
+
+ %PositionSuggestions.text_submitted.connect(save_setting.bind(POSITION_SUGGESTION_KEY))
+ %CustomPortraitScene.value_changed.connect(save_setting_with_name.bind(DEFAULT_PORTRAIT_SCENE_KEY))
+
+ %JoinDefault.value_changed.connect(
+ save_setting_with_name.bind(ANIMATION_JOIN_DEFAULT_KEY))
+ %JoinDefaultLength.value_changed.connect(
+ save_setting.bind(ANIMATION_JOIN_DEFAULT_LENGTH_KEY))
+ %JoinDefaultWait.toggled.connect(
+ save_setting.bind(ANIMATION_JOIN_DEFAULT_WAIT_KEY))
+
+ %LeaveDefault.value_changed.connect(
+ save_setting_with_name.bind(ANIMATION_LEAVE_DEFAULT_KEY))
+ %LeaveDefaultLength.value_changed.connect(
+ save_setting.bind(ANIMATION_LEAVE_DEFAULT_LENGTH_KEY))
+ %LeaveDefaultWait.toggled.connect(
+ save_setting.bind(ANIMATION_LEAVE_DEFAULT_WAIT_KEY))
+
+ %CrossFadeDefault.value_changed.connect(
+ save_setting_with_name.bind(ANIMATION_CROSSFADE_DEFAULT_KEY))
+ %CrossFadeDefaultLength.value_changed.connect(
+ save_setting.bind(ANIMATION_CROSSFADE_DEFAULT_LENGTH_KEY))
+
+
+func _refresh():
+ %PositionSuggestions.text = ProjectSettings.get_setting(POSITION_SUGGESTION_KEY, 'leftmost, left, center, right, rightmost')
+
+ %CustomPortraitScene.resource_icon = get_theme_icon(&"PackedScene", &"EditorIcons")
+ %CustomPortraitScene.set_value(ProjectSettings.get_setting(DEFAULT_PORTRAIT_SCENE_KEY, ''))
+
+ # JOIN
+ %JoinDefault.resource_icon = get_theme_icon(&"Animation", &"EditorIcons")
+ %JoinDefault.set_value(ProjectSettings.get_setting(ANIMATION_JOIN_DEFAULT_KEY, "Fade In Up"))
+ %JoinDefaultLength.set_value(ProjectSettings.get_setting(ANIMATION_JOIN_DEFAULT_LENGTH_KEY, 0.5))
+ %JoinDefaultWait.button_pressed = ProjectSettings.get_setting(ANIMATION_JOIN_DEFAULT_WAIT_KEY, true)
+
+ # LEAVE
+ %LeaveDefault.resource_icon = get_theme_icon(&"Animation", &"EditorIcons")
+ %LeaveDefault.set_value(ProjectSettings.get_setting(ANIMATION_LEAVE_DEFAULT_KEY, "Fade Out Down"))
+ %LeaveDefaultLength.set_value(ProjectSettings.get_setting(ANIMATION_LEAVE_DEFAULT_LENGTH_KEY, 0.5))
+ %LeaveDefaultWait.button_pressed = ProjectSettings.get_setting(ANIMATION_LEAVE_DEFAULT_WAIT_KEY, true)
+
+ # CROSS FADE
+ %CrossFadeDefault.resource_icon = get_theme_icon(&"Animation", &"EditorIcons")
+ %CrossFadeDefault.set_value(ProjectSettings.get_setting(ANIMATION_CROSSFADE_DEFAULT_KEY, "Fade Cross"))
+ %CrossFadeDefaultLength.set_value(ProjectSettings.get_setting(ANIMATION_CROSSFADE_DEFAULT_LENGTH_KEY, 0.5))
+
+
+func save_setting_with_name(property_name:String, value:Variant, settings_key:String) -> void:
+ save_setting(value, settings_key)
+
+
+func save_setting(value:Variant, settings_key:String) -> void:
+ ProjectSettings.set_setting(settings_key, value)
+ ProjectSettings.save()
+
+
+func get_join_animation_suggestions(search_text:String) -> Dictionary:
+ return DialogicPortraitAnimationUtil.get_suggestions(search_text, %JoinDefault.current_value, "", DialogicPortraitAnimationUtil.AnimationType.IN)
+
+
+func get_leave_animation_suggestions(search_text:String) -> Dictionary:
+ return DialogicPortraitAnimationUtil.get_suggestions(search_text, %LeaveDefault.current_value, "", DialogicPortraitAnimationUtil.AnimationType.OUT)
+
+
+func get_crossfade_animation_suggestions(search_text:String) -> Dictionary:
+ return DialogicPortraitAnimationUtil.get_suggestions(search_text, %CrossFadeDefault.current_value, "", DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://cp463rpri5j8a"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Character/settings_portraits.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_dce78"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3"]
+[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="3_m06d8"]
+
+[node name="Portraits" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("2")
+
+[node name="PositionsTitle" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.5
+
+[node name="Title2" type="Label" parent="PositionsTitle"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Position Suggestions
+"
+
+[node name="HintTooltip" parent="PositionsTitle" instance=ExtResource("2_dce78")]
+layout_mode = 2
+tooltip_text = "You can change the position names that will be suggested in the timeline editor here."
+texture = null
+hint_text = "You can change the position names that will be suggested in the timeline editor here."
+
+[node name="PositionSuggestions" type="LineEdit" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="DefaultSceneTitle" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.5
+
+[node name="Title2" type="Label" parent="DefaultSceneTitle"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Default Portrait Scene
+"
+
+[node name="HintTooltip" parent="DefaultSceneTitle" instance=ExtResource("2_dce78")]
+layout_mode = 2
+tooltip_text = "If this is set, this scene will be what is used by default for any portrait that has no scene specified"
+texture = null
+hint_text = "If this is set, this scene will be what is used by default for any portrait that has no scene specified"
+
+[node name="DefaultScene" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="DefaultScene"]
+layout_mode = 2
+text = "Scene"
+
+[node name="CustomPortraitScene" parent="DefaultScene" instance=ExtResource("3_m06d8")]
+unique_name_in_owner = true
+layout_mode = 2
+file_filter = "*.tscn, *.scn; PortraitScene"
+placeholder = "Default Scene"
+
+[node name="Animations2" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.5
+
+[node name="Title2" type="Label" parent="Animations2"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Default Animations
+"
+
+[node name="HintTooltip" parent="Animations2" instance=ExtResource("2_dce78")]
+layout_mode = 2
+tooltip_text = "These settings are used for Leave and Join events if no animation is selected.
+
+The Cross-Fade will play if the portrait of a character changes and
+no animation is set."
+texture = null
+hint_text = "These settings are used for Leave and Join events if no animation is selected.
+
+The Cross-Fade will play if the portrait of a character changes and
+no animation is set."
+
+[node name="GridContainer" type="GridContainer" parent="."]
+layout_mode = 2
+columns = 2
+
+[node name="DefaultJoinLabel" type="Label" parent="GridContainer"]
+layout_mode = 2
+text = "Join"
+
+[node name="DefaultIn" type="HBoxContainer" parent="GridContainer"]
+layout_mode = 2
+
+[node name="JoinDefault" parent="GridContainer/DefaultIn" instance=ExtResource("3")]
+unique_name_in_owner = true
+layout_mode = 2
+mode = 1
+
+[node name="JoinDefaultLength" type="SpinBox" parent="GridContainer/DefaultIn"]
+unique_name_in_owner = true
+layout_mode = 2
+step = 0.1
+
+[node name="JoinDefaultWait" type="CheckButton" parent="GridContainer/DefaultIn"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Wait:"
+
+[node name="DefaultOutLabel" type="Label" parent="GridContainer"]
+layout_mode = 2
+text = "Leave"
+
+[node name="DefaultOut" type="HBoxContainer" parent="GridContainer"]
+layout_mode = 2
+
+[node name="LeaveDefault" parent="GridContainer/DefaultOut" instance=ExtResource("3")]
+unique_name_in_owner = true
+layout_mode = 2
+mode = 1
+
+[node name="LeaveDefaultLength" type="SpinBox" parent="GridContainer/DefaultOut"]
+unique_name_in_owner = true
+layout_mode = 2
+step = 0.1
+
+[node name="LeaveDefaultWait" type="CheckButton" parent="GridContainer/DefaultOut"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Wait:"
+
+[node name="CrossFadeLabel" type="Label" parent="GridContainer"]
+layout_mode = 2
+text = "Cross-Fade"
+
+[node name="DefaultCrossFade" type="HBoxContainer" parent="GridContainer"]
+layout_mode = 2
+
+[node name="CrossFadeDefault" parent="GridContainer/DefaultCrossFade" instance=ExtResource("3")]
+unique_name_in_owner = true
+layout_mode = 2
+mode = 1
+
+[node name="CrossFadeDefaultLength" type="SpinBox" parent="GridContainer/DefaultCrossFade"]
+unique_name_in_owner = true
+layout_mode = 2
+step = 0.1
+
+[connection signal="value_changed" from="GridContainer/DefaultIn/JoinDefault" to="." method="_on_JoinDefault_value_changed"]
+[connection signal="value_changed" from="GridContainer/DefaultIn/JoinDefaultLength" to="." method="_on_JoinDefaultLength_value_changed"]
+[connection signal="toggled" from="GridContainer/DefaultIn/JoinDefaultWait" to="." method="_on_JoinDefaultWait_toggled"]
+[connection signal="value_changed" from="GridContainer/DefaultOut/LeaveDefault" to="." method="_on_LeaveDefault_value_changed"]
+[connection signal="value_changed" from="GridContainer/DefaultOut/LeaveDefaultLength" to="." method="_on_LeaveDefaultLength_value_changed"]
+[connection signal="toggled" from="GridContainer/DefaultOut/LeaveDefaultWait" to="." method="_on_LeaveDefaultWait_toggled"]
+[connection signal="value_changed" from="GridContainer/DefaultCrossFade/CrossFadeDefault" to="." method="_on_LeaveDefault_value_changed"]
+[connection signal="value_changed" from="GridContainer/DefaultCrossFade/CrossFadeDefaultLength" to="." method="_on_LeaveDefaultLength_value_changed"]
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that manages portrait positions.
+
+signal position_changed(info: Dictionary)
+
+
+var transform_regex := r"(?<part>position|pos|size|siz|rotation|rot)\W*=(?<value>((?!(pos|siz|rot)).)*)"
+
+#region STATE
+####################################################################################################
+
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+func get_container(position_id: String) -> DialogicNode_PortraitContainer:
+ for portrait_position:DialogicNode_PortraitContainer in get_tree().get_nodes_in_group(&'dialogic_portrait_con_position'):
+ if portrait_position.is_visible_in_tree() and portrait_position.is_container(position_id):
+ return portrait_position
+ return null
+
+
+func get_containers(position_id: String) -> Array[DialogicNode_PortraitContainer]:
+ return get_tree().get_nodes_in_group(&'dialogic_portrait_con_position').filter(
+ func(node:DialogicNode_PortraitContainer):
+ return node.is_visible_in_tree() and node.is_container(position_id))
+
+
+func get_container_container() -> CanvasItem:
+ var any_portrait_container := get_tree().get_first_node_in_group(&'dialogic_portrait_con_position')
+ if any_portrait_container:
+ return any_portrait_container.get_parent()
+ return null
+
+
+## Creates a new portrait container node.
+## It will copy it's size and most settings from the first p_container in the tree.
+## It will be added as a sibling of the first p_container in the tree.
+func add_container(position_id: String) -> DialogicNode_PortraitContainer:
+ var example_position := get_tree().get_first_node_in_group(&'dialogic_portrait_con_position')
+ if example_position:
+ var new_position := DialogicNode_PortraitContainer.new()
+ example_position.get_parent().add_child(new_position)
+ new_position.name = "Portrait_"+position_id.validate_node_name()
+ copy_container_setup(example_position, new_position)
+ new_position.container_ids = [position_id]
+ position_changed.emit({&'change':'added', &'container_node':new_position, &'position_id':position_id})
+ return new_position
+ return null
+
+
+## Moves the [container] to the [destionation] (using [tween] and [time]).
+## The destination can be a position_id (e.g. "center") or translation, roataion and scale.
+## When moving to a preset container, then some more will be "copied" (e.g. anchors, etc.)
+func move_container(container:DialogicNode_PortraitContainer, destination:String, tween:Tween = null, time:float=1.0) -> void:
+ var target_position: Vector2 = container.position + container._get_origin_position()
+ var target_rotation: float = container.rotation
+ var target_size: Vector2 = container.size
+
+ var destination_container := get_container(destination)
+ if destination_container:
+ container.set_meta("target_container", destination_container)
+ target_position = destination_container.position + destination_container._get_origin_position()
+ target_rotation = destination_container.rotation_degrees
+ target_size = destination_container.size
+ else:
+ var regex := RegEx.create_from_string(transform_regex)
+ for found in regex.search_all(destination):
+ match found.get_string('part'):
+ 'pos', 'position':
+ target_position = str_to_vector(found.get_string("value"), target_position)
+ 'rot', 'rotation':
+ target_rotation = float(found.get_string("value"))
+ 'siz', 'size':
+ target_size = str_to_vector(found.get_string("value"), target_size)
+
+ translate_container(container, target_position, false, tween, time)
+ rotate_container(container, target_rotation, false, tween, time)
+ resize_container(container, target_size, false, tween, time)
+
+ if destination_container:
+ if time:
+ tween.finished.connect(func():
+ if container.has_meta("target_container"):
+ if container.get_meta("target_container") == destination_container:
+ copy_container_setup(destination_container, container)
+ )
+ else:
+ copy_container_setup(destination_container, container)
+
+
+func copy_container_setup(from:DialogicNode_PortraitContainer, to:DialogicNode_PortraitContainer) -> void:
+ to.ignore_resize = true
+ to.layout_mode = from.layout_mode
+ to.anchors_preset = from.anchors_preset
+ to.anchor_bottom = from.anchor_bottom
+ to.anchor_left = from.anchor_left
+ to.anchor_right = from.anchor_right
+ to.anchor_top = from.anchor_top
+ to.offset_bottom = from.offset_bottom
+ to.offset_top = from.offset_top
+ to.offset_right = from.offset_right
+ to.offset_left = from.offset_left
+ to.size_mode = from.size_mode
+ to.origin_anchor = from.origin_anchor
+ to.ignore_resize = false
+ to.update_portrait_transforms()
+
+
+## Translates the given container.
+## The given translation should be the target position of the ORIGIN point, not the container!
+func translate_container(container:DialogicNode_PortraitContainer, translation:Variant, relative := false, tween:Tween=null, time:float=1.0) -> void:
+ if !container.has_meta(&'default_translation'):
+ container.set_meta(&'default_translation', container.position + container._get_origin_position())
+
+ var final_translation: Vector2
+ if typeof(translation) == TYPE_STRING:
+ final_translation = str_to_vector(translation, container.position+container._get_origin_position())
+ elif typeof(translation) == TYPE_VECTOR2:
+ final_translation = translation
+
+ if relative:
+ final_translation += container.position
+ else:
+ final_translation -= container._get_origin_position()
+
+ if tween:
+ tween.tween_method(DialogicUtil.multitween.bind(container, "position", "base"), container.position, final_translation, time)
+ if not tween.finished.is_connected(save_position_container):
+ tween.finished.connect(save_position_container.bind(container))
+ else:
+ container.position = final_translation
+ save_position_container(container)
+ position_changed.emit({&'change':'moved', &'container_node':container})
+
+
+func rotate_container(container:DialogicNode_PortraitContainer, rotation:float, relative := false, tween:Tween=null, time:float=1.0) -> void:
+ if !container.has_meta(&'default_rotation'):
+ container.set_meta(&'default_rotation', container.rotation_degrees)
+
+ var final_rotation := rotation
+
+ if relative:
+ final_rotation += container.rotation_degrees
+
+ container.pivot_offset = container._get_origin_position()
+
+ if tween:
+ tween.tween_property(container, 'rotation_degrees', final_rotation, time)
+ if not tween.finished.is_connected(save_position_container):
+ tween.finished.connect(save_position_container.bind(container))
+ else:
+ container.rotation_degrees = final_rotation
+ save_position_container(container)
+
+ position_changed.emit({&'change':'rotated', &'container_node':container})
+
+
+func resize_container(container: DialogicNode_PortraitContainer, rect_size: Variant, relative := false, tween:Tween=null, time:float=1.0) -> void:
+ if !container.has_meta(&'default_size'):
+ container.set_meta(&'default_size', container.size)
+
+ var final_rect_resize: Vector2
+ if typeof(rect_size) == TYPE_STRING:
+ final_rect_resize = str_to_vector(rect_size, container.size)
+ elif typeof(rect_size) == TYPE_VECTOR2:
+ final_rect_resize = rect_size
+
+ if relative:
+ final_rect_resize += container.rect_size
+
+ var relative_position_change := container._get_origin_position()-container._get_origin_position(final_rect_resize)
+
+ if tween:
+ tween.tween_method(DialogicUtil.multitween.bind(container, "position", "resize_move"), Vector2(), relative_position_change, time)
+ tween.tween_property(container, 'size', final_rect_resize, time)
+ if not tween.finished.is_connected(save_position_container):
+ tween.finished.connect(save_position_container.bind(container))
+ else:
+ container.position = container.position + relative_position_change
+ container.size = final_rect_resize
+ save_position_container(container)
+
+ position_changed.emit({&'change':'resized', &'container_node':container})
+
+
+func save_position_container(container: DialogicNode_PortraitContainer) -> void:
+ if not dialogic.current_state_info.has('portrait_containers'):
+ dialogic.current_state_info['portrait_containers'] = {}
+
+ var info := {
+ "container_ids" : container.container_ids,
+ "position" : container.position,
+ "rotation" : container.rotation_degrees,
+ "size" : container.size,
+ "pivot_mode" : container.pivot_mode,
+ "pivot_value" : container.pivot_value,
+ "origin_anchor" : container.origin_anchor,
+ "anchor_left" : container.anchor_left,
+ "anchor_right" : container.anchor_right,
+ "anchor_top" : container.anchor_top,
+ "anchor_bottom" : container.anchor_bottom,
+ "offset_left" : container.offset_left,
+ "offset_right" : container.offset_right,
+ "offset_top" : container.offset_top,
+ "offset_bottom" : container.offset_bottom,
+ }
+
+ dialogic.current_state_info.portrait_containers[container.container_ids[0]] = info
+
+
+func load_position_container(position_id: String) -> DialogicNode_PortraitContainer:
+ # First check whether the container already exists:
+ var container := get_container(position_id)
+ if container:
+ return container
+
+ if not dialogic.current_state_info.has('portrait_containers') or not dialogic.current_state_info.portrait_containers.has(position_id):
+ return null
+
+ var info: Dictionary = dialogic.current_state_info.portrait_containers[position_id]
+ container = add_container(position_id)
+
+ if not container:
+ return null
+
+ container.container_ids = info.container_ids
+ container.position = info.position
+ container.rotation = info.rotation
+ container.size = info.size
+ container.pivot_mode = info.pivot_mode
+ container.pivot_value = info.pivot_value
+ container.origin_anchor = info.origin_anchor
+ container.anchor_left = info.anchor_left
+ container.anchor_right = info.anchor_right
+ container.anchor_top = info.anchor_top
+ container.anchor_bottom = info.anchor_bottom
+ container.offset_left = info.offset_left
+ container.offset_right = info.offset_right
+ container.offset_top = info.offset_top
+ container.offset_bottom = info.offset_bottom
+
+ return container
+
+
+func str_to_vector(input: String, base_vector:=Vector2()) -> Vector2:
+ var vector_regex := RegEx.create_from_string(r"(?<part>x|y)\s*(?<number>(-|\+)?(\d|\.|)*)(\s*(?<type>%|px))?")
+ var vec := base_vector
+ for i in vector_regex.search_all(input):
+ var value := float(i.get_string(&'number'))
+ match i.get_string(&'type'):
+ 'px':
+ pass # Keep values as they are
+ '%', _:
+ match i.get_string(&'part'):
+ 'x': value *= get_viewport().get_visible_rect().size.x
+ 'y': value *= get_viewport().get_visible_rect().size.y
+
+ match i.get_string(&'part'):
+ 'x': vec.x = value
+ 'y': vec.y = value
+ return vec
+
+
+func vector_to_str(vec:Vector2) -> String:
+ return "x" + str(vec.x) + "px y" + str(vec.y) + "px"
+
+
+func reset_all_containers(time:= 0.0, tween:Tween = null) -> void:
+ for container in get_tree().get_nodes_in_group(&'dialogic_portrait_con_position'):
+ reset_container(container, time, tween)
+
+
+func reset_container(container:DialogicNode_PortraitContainer, time := 0.0, tween: Tween = null ) -> void:
+ if container.has_meta(&'default_translation'):
+ translate_container(container, vector_to_str(container.get_meta(&'default_translation')), false, tween, time)
+ if container.has_meta(&'default_rotation'):
+ rotate_container(container, container.get_meta(&'default_rotation'), false, tween, time)
+ if container.has_meta(&'default_size'):
+ resize_container(container, vector_to_str(container.get_meta(&'default_size')), false, tween, time)
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that manages portraits and portrait positions.
+
+signal character_joined(info:Dictionary)
+signal character_left(info:Dictionary)
+signal character_portrait_changed(info:Dictionary)
+signal character_moved(info:Dictionary)
+
+## Emitted when a portrait starts animating.
+#signal portrait_animating(character_node: Node, portrait_node: Node, animation_name: String, animation_length: float)
+
+
+## The default portrait scene.
+var default_portrait_scene: PackedScene = load(get_script().resource_path.get_base_dir().path_join('default_portrait.tscn'))
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ for character in dialogic.current_state_info.get('portraits', {}).keys():
+ remove_character(load(character))
+ dialogic.current_state_info['portraits'] = {}
+
+
+func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
+ if not "portraits" in dialogic.current_state_info:
+ dialogic.current_state_info["portraits"] = {}
+
+ # Load Position Portraits
+ var portraits_info: Dictionary = dialogic.current_state_info.portraits.duplicate()
+ dialogic.current_state_info.portraits = {}
+ for character_path in portraits_info:
+ var character_info: Dictionary = portraits_info[character_path]
+ var character: DialogicCharacter = load(character_path)
+ var container := dialogic.PortraitContainers.load_position_container(character.get_character_name())
+ add_character(character, container, character_info.portrait, character_info.position_id)
+ change_character_mirror(character, character_info.get('custom_mirror', false))
+ change_character_z_index(character, character_info.get('z_index', 0))
+ change_character_extradata(character, character_info.get('extra_data', ""))
+
+ # Load Speaker Portrait
+ var speaker: Variant = dialogic.current_state_info.get('speaker', "")
+ if speaker:
+ dialogic.current_state_info['speaker'] = ""
+ change_speaker(load(speaker))
+ dialogic.current_state_info['speaker'] = speaker
+
+
+func pause() -> void:
+ for portrait in dialogic.current_state_info['portraits'].values():
+ if portrait.node.has_meta('animation_node'):
+ portrait.node.get_meta('animation_node').pause()
+
+
+func resume() -> void:
+ for portrait in dialogic.current_state_info['portraits'].values():
+ if portrait.node.has_meta('animation_node'):
+ portrait.node.get_meta('animation_node').resume()
+
+
+func _ready() -> void:
+ if !ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
+ default_portrait_scene = load(ProjectSettings.get_setting('dialogic/portraits/default_portrait', ''))
+
+
+#region MAIN METHODS
+####################################################################################################
+## The following methods allow manipulating portraits.
+## A portrait is made up of a character node [Node2D] that instances the portrait scene as it's child.
+## The character node is then always the child of a portrait container.
+## - Position (PortraitContainer)
+## ---- character_node (Node2D)
+## --------- portrait_node (e.g. default_portrait.tscn, or a custom portrait)
+##
+## Using these main methods a character can be present multiple times.
+## For a VN style, the "character" methods (next section) provide access based on the character.
+## - (That is what the character event uses)
+
+
+## Creates a new [character node] for the given [character], and add it to the given [portrait container].
+func _create_character_node(character:DialogicCharacter, container:DialogicNode_PortraitContainer) -> Node:
+ if container == null:
+ return null
+ var character_node := Node2D.new()
+ character_node.name = character.get_character_name()
+ character_node.set_meta('character', character)
+ container.add_child(character_node)
+ return character_node
+
+
+## Changes the portrait of a specific [character node].
+func _change_portrait(character_node: Node2D, portrait: String, fade_animation:="", fade_length := 0.5) -> Dictionary:
+ var character: DialogicCharacter = character_node.get_meta('character')
+
+ if portrait.is_empty():
+ portrait = character.default_portrait
+
+ var info := {'character':character, 'portrait':portrait, 'same_scene':false}
+
+ if not portrait in character.portraits.keys():
+ print_debug('[Dialogic] Change to not-existing portrait will be ignored!')
+ return info
+
+ # Path to the scene to use.
+ var scene_path: String = character.portraits[portrait].get('scene', '')
+
+ var portrait_node: Node = null
+ var previous_portrait: Node = null
+ var portrait_count := character_node.get_child_count()
+
+ if portrait_count > 0:
+ previous_portrait = character_node.get_child(-1)
+
+ # Check if the scene is the same as the currently loaded scene.
+ if (not previous_portrait == null and
+ previous_portrait.get_meta('scene', '') == scene_path and
+ # Also check if the scene supports changing to the given portrait.
+ previous_portrait._should_do_portrait_update(character, portrait)):
+ portrait_node = previous_portrait
+ info['same_scene'] = true
+
+ else:
+
+ if ResourceLoader.exists(scene_path):
+ var packed_scene: PackedScene = load(scene_path)
+
+ if packed_scene:
+ portrait_node = packed_scene.instantiate()
+ else:
+ push_error('[Dialogic] Portrait node "' + str(scene_path) + '" for character [' + character.display_name + '] could not be loaded. Your portrait might not show up on the screen. Confirm the path is correct.')
+
+ if !portrait_node:
+ portrait_node = default_portrait_scene.instantiate()
+
+ portrait_node.set_meta('scene', scene_path)
+
+
+ if portrait_node:
+ portrait_node.set_meta('portrait', portrait)
+ character_node.set_meta('portrait', portrait)
+
+ DialogicUtil.apply_scene_export_overrides(portrait_node, character.portraits[portrait].get('export_overrides', {}))
+
+ if portrait_node.has_method('_update_portrait'):
+ portrait_node._update_portrait(character, portrait)
+
+ if not portrait_node.is_inside_tree():
+ character_node.add_child(portrait_node)
+
+ _update_portrait_transform(portrait_node)
+
+ ## Handle Cross-Animating
+ if previous_portrait and previous_portrait != portrait_node:
+ if not fade_animation.is_empty() and fade_length > 0:
+ var fade_out := _animate_node(previous_portrait, fade_animation, fade_length, 1, true)
+ var _fade_in := _animate_node(portrait_node, fade_animation, fade_length, 1, false)
+ fade_out.finished.connect(previous_portrait.queue_free)
+ else:
+ previous_portrait.queue_free()
+
+ return info
+
+
+## Changes the mirroring of the given portrait.
+## Unless @force is false, this will take into consideration the character mirror,
+## portrait mirror and portrait position mirror settings.
+func _change_portrait_mirror(character_node: Node2D, mirrored := false, force := false) -> void:
+ var latest_portrait := character_node.get_child(-1)
+
+ if latest_portrait.has_method('_set_mirror'):
+ var character: DialogicCharacter = character_node.get_meta('character')
+ var current_portrait_info := character.get_portrait_info(character_node.get_meta('portrait'))
+ latest_portrait._set_mirror(force or (mirrored != character.mirror != character_node.get_parent().mirrored != current_portrait_info.get('mirror', false)))
+
+
+func _change_portrait_extradata(character_node: Node2D, extra_data := "") -> void:
+ var latest_portrait := character_node.get_child(-1)
+
+ if latest_portrait.has_method('_set_extra_data'):
+ latest_portrait._set_extra_data(extra_data)
+
+
+func _update_character_transform(character_node:Node, time := 0.0) -> void:
+ for child in character_node.get_children():
+ _update_portrait_transform(child, time)
+
+
+func _update_portrait_transform(portrait_node: Node, time:float = 0.0) -> void:
+ var character_node: Node = portrait_node.get_parent()
+
+ var character: DialogicCharacter = character_node.get_meta('character')
+ var portrait_info: Dictionary = character.portraits.get(portrait_node.get_meta('portrait'), {})
+
+ # ignore the character scale on custom portraits that have 'ignore_char_scale' set to true
+ var apply_character_scale: bool = not portrait_info.get('ignore_char_scale', false)
+
+ var transform: Rect2 = character_node.get_parent().get_local_portrait_transform(
+ portrait_node._get_covered_rect(),
+ (character.scale * portrait_info.get('scale', 1))*int(apply_character_scale)+portrait_info.get('scale', 1)*int(!apply_character_scale))
+
+ var tween: Tween
+
+ if character_node.has_meta('move_tween'):
+ if character_node.get_meta('move_tween').is_running():
+ time = character_node.get_meta('move_time')-character_node.get_meta('move_tween').get_total_elapsed_time()
+ tween = character_node.get_meta('move_tween')
+ tween.stop()
+ if time == 0:
+ character_node.position = transform.position
+ portrait_node.position = character.offset + portrait_info.get('offset', Vector2())
+ portrait_node.scale = transform.size
+ else:
+ if not tween:
+ tween = character_node.create_tween().set_parallel().set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
+ character_node.set_meta('move_tween', tween)
+ character_node.set_meta('move_time', time)
+ tween.tween_method(DialogicUtil.multitween.bind(character_node, "position", "base"), character_node.position, transform.position, time)
+ tween.tween_property(portrait_node, 'position',character.offset + portrait_info.get('offset', Vector2()), time)
+ tween.tween_property(portrait_node, 'scale', transform.size, time)
+
+
+## Animates the node with the given animation.
+## Is used both on the character node (most animations) and the portrait nodes (cross-fade animations)
+func _animate_node(node: Node, animation_path: String, length: float, repeats := 1, is_reversed := false) -> DialogicAnimation:
+ if node.has_meta('animation_node') and is_instance_valid(node.get_meta('animation_node')):
+ node.get_meta('animation_node').queue_free()
+
+ var anim_script: Script = load(animation_path)
+ var anim_node := Node.new()
+ anim_node.set_script(anim_script)
+ anim_node = (anim_node as DialogicAnimation)
+ anim_node.node = node
+ anim_node.base_position = node.position
+ anim_node.base_scale = node.scale
+ anim_node.time = length
+ anim_node.repeats = repeats
+ anim_node.is_reversed = is_reversed
+
+ add_child(anim_node)
+ anim_node.animate()
+
+ node.set_meta("animation_path", animation_path)
+ node.set_meta("animation_length", length)
+ node.set_meta("animation_node", anim_node)
+
+ #if not is_silent:
+ #portrait_animating.emit(portrait_node.get_parent(), portrait_node, animation_path, length)
+
+ return anim_node
+
+
+## Moves the given portrait to the given container.
+func _move_character(character_node: Node2D, transform:="", time := 0.0, easing:= Tween.EASE_IN_OUT, trans:= Tween.TRANS_SINE) -> void:
+ var tween := character_node.create_tween().set_ease(easing).set_trans(trans).set_parallel()
+ if time == 0:
+ tween.kill()
+ tween = null
+ var container: DialogicNode_PortraitContainer = character_node.get_parent()
+ dialogic.PortraitContainers.move_container(container, transform, tween, time)
+
+ for portrait_node in character_node.get_children():
+ _update_portrait_transform(portrait_node, time)
+
+
+## Changes the given portraits z_index.
+func _change_portrait_z_index(character_node: Node, z_index:int, update_zindex:= true) -> void:
+ if update_zindex:
+ character_node.get_parent().set_meta('z_index', z_index)
+
+ var sorted_children := character_node.get_parent().get_parent().get_children()
+ sorted_children.sort_custom(z_sort_portrait_containers)
+ var idx := 0
+ for con in sorted_children:
+ con.get_parent().move_child(con, idx)
+ idx += 1
+
+
+## Checks if [para, character] has joined the scene, if so, returns its
+## active [DialogicPortrait] node.
+##
+## The difference between an active and inactive nodes is whether the node is
+## the latest node. [br]
+## If a portrait is fading/animating from portrait A and B, both will exist
+## in the scene, but only the new portrait is active, even if it is not
+## fully visible yet.
+func get_character_portrait(character: DialogicCharacter) -> DialogicPortrait:
+ if is_character_joined(character):
+ var portrait_node: DialogicPortrait = dialogic.current_state_info['portraits'][character.resource_path].node.get_child(-1)
+ return portrait_node
+
+ return null
+
+
+func z_sort_portrait_containers(con1: DialogicNode_PortraitContainer, con2: DialogicNode_PortraitContainer) -> bool:
+ if con1.get_meta('z_index', 0) < con2.get_meta('z_index', 0):
+ return true
+
+ return false
+
+
+## Private method to remove a [param portrait_node].
+func _remove_portrait(portrait_node: Node) -> void:
+ portrait_node.get_parent().remove_child(portrait_node)
+ portrait_node.queue_free()
+
+
+## Gets the default animation length for joining characters
+## If Auto-Skip is enabled, limits the time.
+func _get_join_default_length() -> float:
+ var default_time: float = ProjectSettings.get_setting('dialogic/animations/join_default_length', 0.5)
+
+ if dialogic.Inputs.auto_skip.enabled:
+ default_time = min(default_time, dialogic.Inputs.auto_skip.time_per_event)
+
+ return default_time
+
+
+## Gets the default animation length for leaving characters
+## If Auto-Skip is enabled, limits the time.
+func _get_leave_default_length() -> float:
+ var default_time: float = ProjectSettings.get_setting('dialogic/animations/leave_default_length', 0.5)
+
+ if dialogic.Inputs.auto_skip.enabled:
+ default_time = min(default_time, dialogic.Inputs.auto_skip.time_per_event)
+
+ return default_time
+
+
+## Checks multiple cases to return a valid portrait to use.
+func get_valid_portrait(character:DialogicCharacter, portrait:String) -> String:
+ if character == null:
+ printerr('[Dialogic] Tried to use portrait "', portrait, '" on <null> character.')
+ dialogic.print_debug_moment()
+ return ""
+
+ if "{" in portrait and dialogic.has_subsystem("Expressions"):
+ var test: Variant = dialogic.Expressions.execute_string(portrait)
+ if test:
+ portrait = str(test)
+
+ if not portrait in character.portraits:
+ if not portrait.is_empty():
+ printerr('[Dialogic] Tried to use invalid portrait "', portrait, '" on character "', DialogicResourceUtil.get_unique_identifier(character.resource_path), '". Using default portrait instead.')
+ dialogic.print_debug_moment()
+ portrait = character.default_portrait
+
+ if portrait.is_empty():
+ portrait = character.default_portrait
+
+ return portrait
+
+#endregion
+
+
+#region Character Methods
+####################################################################################################
+## The following methods are used to manage character portraits with the following rules:
+## - a character can only be present once with these methods.
+## Most of them will fail silently if the character isn't joined yet.
+
+
+## Adds a character at a position and sets it's portrait.
+## If the character is already joined it will only update, portrait, position, etc.
+func join_character(character:DialogicCharacter, portrait:String, position_id:String, mirrored:= false, z_index:= 0, extra_data:= "", animation_name:= "", animation_length:= 0.0, animation_wait := false) -> Node:
+ if is_character_joined(character):
+ change_character_portrait(character, portrait)
+
+ if animation_name.is_empty():
+ animation_length = _get_join_default_length()
+
+ if animation_wait:
+ dialogic.current_state = DialogicGameHandler.States.ANIMATING
+ await get_tree().create_timer(animation_length).timeout
+ dialogic.current_state = DialogicGameHandler.States.IDLE
+ move_character(character, position_id, animation_length)
+ change_character_mirror(character, mirrored)
+ return
+
+ var container := dialogic.PortraitContainers.add_container(character.get_character_name())
+ var character_node := add_character(character, container, portrait, position_id)
+ if character_node == null:
+ return null
+
+ dialogic.current_state_info['portraits'][character.resource_path] = {'portrait':portrait, 'node':character_node, 'position_id':position_id, 'custom_mirror':mirrored}
+
+ _change_portrait_mirror(character_node, mirrored)
+ _change_portrait_extradata(character_node, extra_data)
+ _change_portrait_z_index(character_node, z_index)
+
+ var info := {'character':character}
+ info.merge(dialogic.current_state_info['portraits'][character.resource_path])
+ character_joined.emit(info)
+
+ if animation_name.is_empty():
+ animation_name = ProjectSettings.get_setting('dialogic/animations/join_default', "Fade In Up")
+ animation_length = _get_join_default_length()
+ animation_wait = ProjectSettings.get_setting('dialogic/animations/join_default_wait', true)
+
+ animation_name = DialogicPortraitAnimationUtil.guess_animation(animation_name, DialogicPortraitAnimationUtil.AnimationType.IN)
+
+ if animation_name and animation_length > 0:
+ var anim: DialogicAnimation = _animate_node(character_node, animation_name, animation_length)
+ if animation_wait:
+ dialogic.current_state = DialogicGameHandler.States.ANIMATING
+ await anim.finished
+ dialogic.current_state = DialogicGameHandler.States.IDLE
+
+ return character_node
+
+
+func add_character(character:DialogicCharacter, container: DialogicNode_PortraitContainer, portrait:String, position_id:String) -> Node:
+ if is_character_joined(character):
+ printerr('[DialogicError] Cannot add a already joined character. If this is intended call _create_character_node manually.')
+ return null
+
+ portrait = get_valid_portrait(character, portrait)
+
+ if portrait.is_empty():
+ return null
+
+ if not character:
+ printerr('[DialogicError] Cannot call add_portrait() with null character.')
+ return null
+
+ var character_node := _create_character_node(character, container)
+
+ if character_node == null:
+ printerr('[Dialogic] Failed to join character to position ', position_id, ". Could not find position container.")
+ return null
+
+
+ dialogic.current_state_info['portraits'][character.resource_path] = {'portrait':portrait, 'node':character_node, 'position_id':position_id}
+
+ _move_character(character_node, position_id)
+ _change_portrait(character_node, portrait)
+
+ return character_node
+
+
+## Changes the portrait of a character. Only works with joined characters.
+func change_character_portrait(character: DialogicCharacter, portrait: String, fade_animation:="DEFAULT", fade_length := -1.0) -> void:
+ if not is_character_joined(character):
+ return
+
+ portrait = get_valid_portrait(character, portrait)
+
+ if dialogic.current_state_info.portraits[character.resource_path].portrait == portrait:
+ return
+
+ if fade_animation == "DEFAULT":
+ fade_animation = ProjectSettings.get_setting('dialogic/animations/cross_fade_default', "Fade Cross")
+ fade_length = ProjectSettings.get_setting('dialogic/animations/cross_fade_default_length', 0.5)
+
+ fade_animation = DialogicPortraitAnimationUtil.guess_animation(fade_animation, DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
+
+ var info := _change_portrait(dialogic.current_state_info.portraits[character.resource_path].node, portrait, fade_animation, fade_length)
+ dialogic.current_state_info.portraits[character.resource_path].portrait = info.portrait
+ _change_portrait_mirror(
+ dialogic.current_state_info.portraits[character.resource_path].node,
+ dialogic.current_state_info.portraits[character.resource_path].get('custom_mirror', false)
+ )
+ character_portrait_changed.emit(info)
+
+
+## Changes the mirror of the given character. Only works with joined characters
+func change_character_mirror(character:DialogicCharacter, mirrored:= false, force:= false) -> void:
+ if !is_character_joined(character):
+ return
+
+ _change_portrait_mirror(dialogic.current_state_info.portraits[character.resource_path].node, mirrored, force)
+ dialogic.current_state_info.portraits[character.resource_path]['custom_mirror'] = mirrored
+
+
+## Changes the z_index of a character. Only works with joined characters
+func change_character_z_index(character:DialogicCharacter, z_index:int, update_zindex:= true) -> void:
+ if !is_character_joined(character):
+ return
+
+ _change_portrait_z_index(dialogic.current_state_info.portraits[character.resource_path].node, z_index, update_zindex)
+ if update_zindex:
+ dialogic.current_state_info.portraits[character.resource_path]['z_index'] = z_index
+
+
+## Changes the extra data on the given character. Only works with joined characters
+func change_character_extradata(character:DialogicCharacter, extra_data:="") -> void:
+ if !is_character_joined(character):
+ return
+ _change_portrait_extradata(dialogic.current_state_info.portraits[character.resource_path].node, extra_data)
+ dialogic.current_state_info.portraits[character.resource_path]['extra_data'] = extra_data
+
+
+## Starts the given animation on the given character. Only works with joined characters
+func animate_character(character: DialogicCharacter, animation_path: String, length: float, repeats := 1, is_reversed := false) -> DialogicAnimation:
+ if not is_character_joined(character):
+ return null
+
+ animation_path = DialogicPortraitAnimationUtil.guess_animation(animation_path)
+
+ var character_node: Node = dialogic.current_state_info.portraits[character.resource_path].node
+
+ return _animate_node(character_node, animation_path, length, repeats, is_reversed)
+
+
+## Moves the given character to the given position. Only works with joined characters
+func move_character(character:DialogicCharacter, position_id:String, time:= 0.0, easing:=Tween.EASE_IN_OUT, trans:=Tween.TRANS_SINE) -> void:
+ if !is_character_joined(character):
+ return
+
+ if dialogic.current_state_info.portraits[character.resource_path].position_id == position_id:
+ return
+
+ _move_character(dialogic.current_state_info.portraits[character.resource_path].node, position_id, time, easing, trans)
+ dialogic.current_state_info.portraits[character.resource_path].position_id = position_id
+ character_moved.emit({'character':character, 'position_id':position_id, 'time':time})
+
+
+## Removes a character with a given animation or the default animation.
+func leave_character(character: DialogicCharacter, animation_name:= "", animation_length:= 0.0, animation_wait := false) -> void:
+ if not is_character_joined(character):
+ return
+
+ if animation_name.is_empty():
+ animation_name = ProjectSettings.get_setting('dialogic/animations/leave_default', "Fade Out Down")
+ animation_length = _get_leave_default_length()
+ animation_wait = ProjectSettings.get_setting('dialogic/animations/leave_default_wait', true)
+
+ animation_name = DialogicPortraitAnimationUtil.guess_animation(animation_name, DialogicPortraitAnimationUtil.AnimationType.OUT)
+
+ if not animation_name.is_empty():
+ var character_node := get_character_node(character)
+
+ var animation := _animate_node(character_node, animation_name, animation_length, 1, true)
+ if animation_length > 0:
+ if animation_wait:
+ dialogic.current_state = DialogicGameHandler.States.ANIMATING
+ await animation.finished
+ dialogic.current_state = DialogicGameHandler.States.IDLE
+ remove_character(character)
+ else:
+ animation.finished.connect(func(): remove_character(character))
+ else:
+ remove_character(character)
+
+
+## Removes all joined characters with a given animation or the default animation.
+func leave_all_characters(animation_name:="", animation_length:=0.0, animation_wait := false) -> void:
+ for character in get_joined_characters():
+ await leave_character(character, animation_name, animation_length, animation_wait)
+
+
+## Finds the character node for a [param character].
+## Return `null` if the [param character] is not part of the scene.
+func get_character_node(character: DialogicCharacter) -> Node:
+ if is_character_joined(character):
+ if is_instance_valid(dialogic.current_state_info['portraits'][character.resource_path].node):
+ return dialogic.current_state_info['portraits'][character.resource_path].node
+ return null
+
+
+## Removes the given characters portrait.
+## Only works with joined characters.
+func remove_character(character: DialogicCharacter) -> void:
+ var character_node := get_character_node(character)
+
+ if is_instance_valid(character_node) and character_node is Node:
+ var container := character_node.get_parent()
+ container.get_parent().remove_child(container)
+ container.queue_free()
+ character_node.queue_free()
+ character_left.emit({'character': character})
+
+ dialogic.current_state_info['portraits'].erase(character.resource_path)
+
+
+func get_current_character() -> DialogicCharacter:
+ if dialogic.current_state_info.get('speaker', null):
+ return load(dialogic.current_state_info.speaker)
+ return null
+
+
+
+## Returns true if the given character is currently joined.
+func is_character_joined(character: DialogicCharacter) -> bool:
+ if character == null or not character.resource_path in dialogic.current_state_info['portraits']:
+ return false
+
+ return true
+
+
+## Returns a list of the joined charcters (as resources)
+func get_joined_characters() -> Array[DialogicCharacter]:
+ var chars: Array[DialogicCharacter] = []
+
+ for char_path: String in dialogic.current_state_info.get('portraits', {}).keys():
+ chars.append(load(char_path))
+
+ return chars
+
+
+## Returns a dictionary with info on a given character.
+## Keys can be [joined, character, node (for the portrait node), position_id]
+## Only joined is included (and false) for not joined characters
+func get_character_info(character:DialogicCharacter) -> Dictionary:
+ if is_character_joined(character):
+ var info: Dictionary = dialogic.current_state_info['portraits'][character.resource_path]
+ info['joined'] = true
+ return info
+ else:
+ return {'joined':false}
+
+#endregion
+
+
+#region SPEAKER PORTRAIT CONTAINERS
+####################################################################################################
+
+## Updates all portrait containers set to SPEAKER.
+func change_speaker(speaker: DialogicCharacter = null, portrait := "") -> void:
+ for container: Node in get_tree().get_nodes_in_group('dialogic_portrait_con_speaker'):
+
+ var just_joined := true
+ for character_node: Node in container.get_children():
+ if not character_node.get_meta('character') == speaker:
+ var leave_animation: String = ProjectSettings.get_setting('dialogic/animations/leave_default', "Fade Out")
+ leave_animation = DialogicPortraitAnimationUtil.guess_animation(leave_animation, DialogicPortraitAnimationUtil.AnimationType.OUT)
+ var leave_animation_length := _get_leave_default_length()
+
+ if leave_animation and leave_animation_length:
+ var animate_out := _animate_node(character_node, leave_animation, leave_animation_length, 1, true)
+ animate_out.finished.connect(character_node.queue_free)
+ else:
+ character_node.get_parent().remove_child(character_node)
+ character_node.queue_free()
+ else:
+ just_joined = false
+
+ if speaker == null or speaker.portraits.is_empty():
+ continue
+
+ if just_joined:
+ _create_character_node(speaker, container)
+
+ elif portrait.is_empty():
+ continue
+
+ if portrait.is_empty():
+ portrait = speaker.default_portrait
+
+ var character_node := container.get_child(-1)
+
+ var fade_animation: String = ProjectSettings.get_setting('dialogic/animations/cross_fade_default', "Fade Cross")
+ var fade_length: float = ProjectSettings.get_setting('dialogic/animations/cross_fade_default_length', 0.5)
+
+ fade_animation = DialogicPortraitAnimationUtil.guess_animation(fade_animation, DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
+
+ if container.portrait_prefix+portrait in speaker.portraits:
+ portrait = container.portrait_prefix+portrait
+
+ _change_portrait(character_node, portrait, fade_animation, fade_length)
+
+ # if the character has no portraits _change_portrait won't actually add a child node
+ if character_node.get_child_count() == 0:
+ continue
+
+ if just_joined:
+ # Change speaker is called before the text is changed.
+ # In styles where the speaker is IN the textbox,
+ # this can mean the portrait container isn't sized correctly yet.
+ character_node.hide()
+ if not container.is_visible_in_tree():
+ await get_tree().process_frame
+ character_node.show()
+ var join_animation: String = ProjectSettings.get_setting('dialogic/animations/join_default', "Fade In Up")
+ join_animation = DialogicPortraitAnimationUtil.guess_animation(join_animation, DialogicPortraitAnimationUtil.AnimationType.IN)
+ var join_animation_length := _get_join_default_length()
+
+ if join_animation and join_animation_length:
+ _animate_node(character_node, join_animation, join_animation_length)
+
+ _change_portrait_mirror(character_node)
+
+ if speaker:
+ if speaker.resource_path != dialogic.current_state_info['speaker']:
+ if dialogic.current_state_info['speaker'] and is_character_joined(load(dialogic.current_state_info['speaker'])):
+ dialogic.current_state_info['portraits'][dialogic.current_state_info['speaker']].node.get_child(-1)._unhighlight()
+
+ if speaker and is_character_joined(speaker):
+ dialogic.current_state_info['portraits'][speaker.resource_path].node.get_child(-1)._highlight()
+
+ elif dialogic.current_state_info['speaker'] and is_character_joined(load(dialogic.current_state_info['speaker'])):
+ dialogic.current_state_info['portraits'][dialogic.current_state_info['speaker']].node.get_child(-1)._unhighlight()
+
+#endregion
+
+
+#region TEXT EFFECTS
+####################################################################################################
+
+## Called from the [portrait=something] text effect.
+func text_effect_portrait(_text_node:Control, _skipped:bool, argument:String) -> void:
+ if argument:
+ if dialogic.current_state_info.get('speaker', null):
+ change_character_portrait(load(dialogic.current_state_info.speaker), argument)
+ change_speaker(load(dialogic.current_state_info.speaker), argument)
+
+
+## Called from the [extra_data=something] text effect.
+func text_effect_extradata(_text_node:Control, _skipped:bool, argument:String) -> void:
+ if argument:
+ if dialogic.current_state_info.get('speaker', null):
+ change_character_extradata(load(dialogic.current_state_info.speaker), argument)
+#endregion
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="31" height="22" viewBox="0 0 31 22" fill="none" version="1.1" id="svg4" sodipodi:docname="update_mirror.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs8" />
+ <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="16" inkscape:cx="21.84375" inkscape:cy="14.0625" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4">
+ <inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" />
+ </sodipodi:namedview>
+ <g inkscape:groupmode="layer" id="layer2" inkscape:label="Mirror" style="display:inline">
+ <path id="path8742" style="fill:#ffffff;fill-opacity:1;stroke-width:0.759831" d="m 10.967882,3.1351682 c 0.863708,-0.038595 2.312143,1.467492 2.312143,3.2198562 v 0 c 0,1.7523639 -1.491571,3.2198509 -2.312143,3.2198509" sodipodi:nodetypes="cssc" class="UnoptimicedTransforms" transform="matrix(1.1995187,0,0,1.1995187,-2.6822942,-1.4596729)" />
+ <path id="path16944" style="fill:#ffffff;fill-opacity:1;stroke-width:0.914608" d="m 7.5210109,10.015357 c -0.023367,7.6e-4 -0.046802,8.86e-4 -0.070302,0.0012 -0.9139878,0.009 -2.8630987,-1.7296781 -2.8630987,-3.8633837 0,-2.1337061 1.9491109,-3.8466572 2.8630988,-3.8634194 0.023497,-4.31e-4 0.046938,3.82e-4 0.070302,0.00113" sodipodi:nodetypes="csssc" />
+ <path id="path8744" style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.866873" d="m 10.468766,9.4004107 c 0.0017,-5.3e-6 0.0035,-8.8e-6 0.0051,-8.8e-6 1.704613,0 3.086415,2.9100071 3.086415,6.4996661 v 0 c 0,1.999827 -1.381802,1.999829 -3.086415,1.999829 h -0.0052" />
+ <path id="path16885" style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.874663" d="m 7.5210109,17.899897 c -1.7328976,-2e-6 -3.1367969,-0.002 -3.1367969,-1.999829 0,-3.586045 1.4039606,-6.4938047 3.1369101,-6.4996573" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="M 4.3862586,18.660184 H 13.902208" id="path11459" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 5.0553488,16.987459 -1.3381805,1.635554 1.3010089,1.970099" id="path11461" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 13.567664,16.875944 0.854949,1.821412 -0.817777,1.9701" id="path11463" />
+ <path style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.37801;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.981752" d="m 9.0131471,1.539982 -0.03659,16.903546" id="path16998" />
+ </g>
+ <path id="path26928" style="display:inline;fill:#ff4596;fill-opacity:1;stroke-width:0.613366" inkscape:label="path26928" d="m 17.19844,8.281361 c -0.272575,-8.2e-6 -0.500185,9.5e-6 -0.687018,0.012976 -0.194489,0.013249 -0.37819,0.041877 -0.556302,0.1156474 -0.4133,0.1711984 -0.741702,0.4995528 -0.912902,0.9128597 -0.08914,0.2151709 -0.113203,0.4419435 -0.122463,0.6884219 -0.0075,0.198126 -0.107439,0.363433 -0.25499,0.44863 -0.147562,0.08519 -0.340728,0.08912 -0.516018,-0.0037 -0.218061,-0.115251 -0.426533,-0.207727 -0.657444,-0.238134 -0.443521,-0.05839 -0.892077,0.06181 -1.246989,0.33413 -0.152944,0.117382 -0.269558,0.262171 -0.378298,0.423977 -0.104461,0.155439 -0.218268,0.352519 -0.354578,0.588618 l -0.01552,0.02687 c -0.136286,0.236087 -0.250093,0.433158 -0.332475,0.601337 -0.08575,0.17508 -0.152839,0.348492 -0.178013,0.53963 -0.05839,0.443529 0.06182,0.892086 0.334128,1.247025 0.141774,0.184685 0.326035,0.319018 0.534849,0.450213 0.167922,0.105499 0.261112,0.274802 0.2611,0.445256 -4e-6,0.170395 -0.09319,0.339684 -0.2611,0.445179 -0.208839,0.131218 -0.393112,0.265542 -0.534887,0.450286 -0.272334,0.354894 -0.39255,0.803431 -0.334165,1.246956 0.02519,0.191135 0.09222,0.364576 0.178014,0.53963 0.08239,0.168183 0.196157,0.365293 0.332477,0.601374 l 0.01552,0.02687 c 0.136299,0.236083 0.250113,0.433152 0.354577,0.588576 0.108764,0.161802 0.225385,0.306641 0.378336,0.423982 0.35491,0.272332 0.803465,0.392553 1.246989,0.334165 0.230896,-0.03043 0.439318,-0.122887 0.657371,-0.238137 0.175301,-0.09269 0.368508,-0.08873 0.516089,-0.0034 0.147577,0.08519 0.247617,0.250486 0.255026,0.448666 0.0092,0.246453 0.03333,0.473216 0.122463,0.688386 0.171199,0.413282 0.4996,0.741708 0.912902,0.912899 0.178115,0.0738 0.361807,0.102372 0.556303,0.115607 0.186836,0.01298 0.414443,0.01298 0.687018,0.01298 h 0.03098 c 0.272641,0 0.500197,0 0.687087,-0.01298 0.194489,-0.01325 0.378223,-0.04182 0.556344,-0.11562 0.413285,-0.171184 0.741671,-0.499617 0.912862,-0.912901 0.08914,-0.21517 0.113203,-0.441908 0.122409,-0.68842 0.0075,-0.198111 0.107425,-0.36345 0.254987,-0.448709 0.147577,-0.08522 0.340795,-0.08914 0.516092,0.0034 0.218051,0.115251 0.426474,0.207726 0.657404,0.2381 0.443522,0.05839 0.892099,-0.06174 1.246993,-0.334094 0.152961,-0.117441 0.269546,-0.262206 0.378297,-0.424009 0.104448,-0.155439 0.218252,-0.352497 0.354541,-0.588579 l 0.01569,-0.02687 c 0.136272,-0.236049 0.2501,-0.433192 0.332472,-0.601376 0.08573,-0.17506 0.152807,-0.348467 0.178015,-0.539596 0.05839,-0.443585 -0.06185,-0.89213 -0.334163,-1.247026 C 22.966705,15.585721 22.782388,15.451451 22.573601,15.320252 22.405679,15.214753 22.3125,15.045453 22.3125,14.875 c 0,-0.170395 0.09314,-0.339612 0.261028,-0.445112 0.208903,-0.131191 0.393243,-0.265477 0.534992,-0.450283 0.272333,-0.354903 0.392559,-0.803461 0.334166,-1.246992 -0.0252,-0.191132 -0.09228,-0.364516 -0.178014,-0.539595 -0.08236,-0.168164 -0.196157,-0.365264 -0.332511,-0.601337 l -0.01552,-0.02685 c -0.136362,-0.236031 -0.25012,-0.433148 -0.354576,-0.588582 -0.108763,-0.161803 -0.225359,-0.306611 -0.378332,-0.423976 -0.354895,-0.27233 -0.80344,-0.392521 -1.247026,-0.33413 -0.230874,0.03042 -0.439283,0.122886 -0.657331,0.238135 -0.17532,0.09266 -0.368517,0.08873 -0.516097,0.0034 -0.147577,-0.0852 -0.247635,-0.250542 -0.25506,-0.448696 C 19.499068,9.7645223 19.474933,9.5377607 19.385811,9.3226061 19.21463,8.9093853 18.886236,8.5810255 18.472951,8.4098311 18.294838,8.336061 18.111106,8.3074462 17.916607,8.2941837 17.729734,8.281208 17.502159,8.281208 17.229518,8.281208 Z m 0.01569,4.293575 c 1.270347,0 2.300127,1.029802 2.300127,2.300124 0,1.270344 -1.02978,2.300124 -2.300127,2.300124 -1.270314,0 -2.300123,-1.02978 -2.300123,-2.300124 0,-1.270322 1.029809,-2.300124 2.300123,-2.300124 z" />
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="31" height="22" viewBox="0 0 31 22" fill="none" version="1.1" id="svg4" sodipodi:docname="update_portrait.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs8" />
+ <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="16" inkscape:cx="23.75" inkscape:cy="14.0625" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4">
+ <inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" />
+ </sodipodi:namedview>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="m 9.9375,15.875 c 3.576743,0 6.476256,-2.960429 6.476256,-6.612317 0,-3.6518789 -2.899513,-6.612317 -6.476256,-6.612317 -3.5767353,0 -6.4762575,2.9604381 -6.4762575,6.612317 0,3.651888 2.8995222,6.612317 6.4762575,6.612317 z M 5.9096842,8.375405 c 0.332121,-0.339136 0.7825817,-0.5296473 1.2522769,-0.5296473 0.4696953,0 0.9201559,0.1905113 1.2522768,0.5296473 L 8.6852232,8.652084 9.339464,7.984097 9.0684787,7.7074187 C 8.5628126,7.1911667 7.8770324,6.9011417 7.1619611,6.9011417 c -0.7150713,0 -1.4008514,0.290025 -1.9064807,0.806277 L 4.9845045,7.984097 5.6386991,8.652084 Z M 12.713037,7.8457577 c -0.469713,0 -0.920183,0.1905113 -1.252322,0.5296473 L 11.189823,8.652084 10.535537,7.984097 10.806521,7.7074187 c 0.505703,-0.516252 1.191445,-0.806277 1.906516,-0.806277 0.715072,0 1.400815,0.290025 1.906519,0.806277 L 14.890541,7.984097 14.236254,8.652084 13.965361,8.375405 C 13.633222,8.036269 13.182751,7.8457577 12.713037,7.8457577 Z M 9.9375,13.985766 c -1.8503592,0 -3.2381288,-1.889233 -3.7007186,-2.833849 h 7.4014356 c -0.46259,0.944616 -1.850358,2.833849 -3.700717,2.833849 z" fill="#ffffff" id="path2" style="display:inline;stroke-width:0.934854" inkscape:label="Portrait" />
+ <path id="path26928" style="display:inline;fill:#ff4596;fill-opacity:1;stroke-width:0.613366" inkscape:label="path26928" d="m 19.13594,6.968861 c -0.272575,-8.2e-6 -0.500185,9.5e-6 -0.687018,0.012976 -0.194489,0.013249 -0.37819,0.041877 -0.556302,0.1156474 -0.4133,0.1711984 -0.741702,0.4995528 -0.912902,0.9128597 -0.08914,0.2151709 -0.113203,0.4419435 -0.122463,0.6884214 -0.0075,0.1981263 -0.107439,0.3634338 -0.25499,0.4486307 -0.147562,0.085189 -0.340728,0.089123 -0.516018,-0.00369 -0.218061,-0.1152514 -0.426533,-0.207727 -0.657444,-0.2381338 -0.443521,-0.058391 -0.892077,0.061805 -1.246989,0.3341293 -0.152944,0.1173821 -0.269558,0.2621716 -0.378298,0.4239769 -0.104461,0.1554392 -0.218268,0.3525194 -0.354578,0.5886184 l -0.01552,0.02687 c -0.136286,0.236087 -0.250093,0.433158 -0.332475,0.601337 -0.08575,0.17508 -0.152839,0.348492 -0.178013,0.53963 -0.05839,0.443529 0.06182,0.892086 0.334128,1.247025 0.141774,0.184685 0.326035,0.319018 0.534849,0.450213 0.167922,0.105499 0.261112,0.274802 0.2611,0.445256 -4e-6,0.170395 -0.09319,0.339684 -0.2611,0.445179 -0.208839,0.131218 -0.393112,0.265542 -0.534887,0.450286 -0.272334,0.354894 -0.39255,0.803431 -0.334165,1.246956 0.02519,0.191135 0.09222,0.364576 0.178014,0.53963 0.08239,0.168183 0.196157,0.365293 0.332477,0.601374 l 0.01552,0.02687 c 0.136299,0.236083 0.250113,0.433152 0.354577,0.588576 0.108764,0.161802 0.225385,0.306641 0.378336,0.423982 0.35491,0.272332 0.803465,0.392553 1.246989,0.334165 0.230896,-0.03043 0.439318,-0.122887 0.657371,-0.238137 0.175301,-0.09269 0.368508,-0.08873 0.516089,-0.0034 0.147577,0.08519 0.247617,0.250486 0.255026,0.448666 0.0092,0.246453 0.03333,0.473216 0.122463,0.688386 0.171199,0.413282 0.4996,0.741708 0.912902,0.912899 0.178115,0.0738 0.361807,0.102372 0.556303,0.115607 0.186836,0.01298 0.414443,0.01298 0.687018,0.01298 h 0.03098 c 0.272641,0 0.500197,0 0.687087,-0.01298 0.194489,-0.01325 0.378223,-0.04182 0.556344,-0.11562 0.413285,-0.171184 0.741671,-0.499617 0.912862,-0.912901 0.08914,-0.21517 0.113203,-0.441908 0.122409,-0.68842 0.0075,-0.198111 0.107425,-0.36345 0.254987,-0.448709 0.147577,-0.08522 0.340795,-0.08914 0.516092,0.0034 0.218051,0.115251 0.426474,0.207726 0.657404,0.2381 0.443522,0.05839 0.892099,-0.06174 1.246993,-0.334094 0.152961,-0.117441 0.269546,-0.262206 0.378297,-0.424009 0.104448,-0.155439 0.218252,-0.352497 0.354541,-0.588579 l 0.01569,-0.02687 c 0.136272,-0.236049 0.2501,-0.433192 0.332472,-0.601376 0.08573,-0.17506 0.152807,-0.348467 0.178015,-0.539596 0.05839,-0.443585 -0.06185,-0.89213 -0.334163,-1.247026 C 24.904205,14.273221 24.719888,14.138951 24.511101,14.007752 24.343179,13.902253 24.25,13.732953 24.25,13.5625 c 0,-0.170395 0.09314,-0.339612 0.261028,-0.445112 0.208903,-0.131191 0.393243,-0.265477 0.534992,-0.450283 0.272333,-0.354903 0.392559,-0.803461 0.334166,-1.246992 -0.0252,-0.191132 -0.09228,-0.364516 -0.178014,-0.539595 -0.08236,-0.168164 -0.196157,-0.365264 -0.332511,-0.601337 l -0.01552,-0.02685 C 24.717779,10.016297 24.604021,9.8191797 24.499565,9.6637461 24.390802,9.5019435 24.274206,9.3571349 24.121233,9.2397705 23.766338,8.9674396 23.317793,8.8472493 22.874207,8.9056399 22.643333,8.9360576 22.434924,9.0285264 22.216876,9.143775 22.041556,9.236435 21.848359,9.232501 21.700779,9.147185 21.553202,9.0619873 21.453144,8.8966429 21.445719,8.6984893 21.436568,8.4520223 21.412433,8.2252607 21.323311,8.0101061 21.15213,7.5968853 20.823736,7.2685255 20.410451,7.0973311 20.232338,7.023561 20.048606,6.9949462 19.854107,6.9816837 19.667234,6.968708 19.439659,6.968708 19.167018,6.968708 Z m 0.01569,4.293575 c 1.270347,0 2.300127,1.029802 2.300127,2.300124 0,1.270344 -1.02978,2.300124 -2.300127,2.300124 -1.270314,0 -2.300123,-1.02978 -2.300123,-2.300124 0,-1.270322 1.029809,-2.300124 2.300123,-2.300124 z" />
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="31" height="22" viewBox="0 0 31 22" fill="none" version="1.1" id="svg4" sodipodi:docname="update_position.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs8" />
+ <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="16" inkscape:cx="23.84375" inkscape:cy="14.0625" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4">
+ <inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" />
+ </sodipodi:namedview>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="m 9.6329576,18.73815 c 4.1893454,0 7.5854694,-3.467473 7.5854694,-7.744834 0,-4.2773499 -3.396124,-7.7448337 -7.5854694,-7.7448337 -4.1893365,0 -7.5854711,3.4674838 -7.5854711,7.7448337 0,4.277361 3.3961346,7.744834 7.5854711,7.744834 z M 4.9152823,9.9540703 c 0.3890046,-0.397221 0.9166174,-0.6203616 1.4667592,-0.6203616 0.5501418,0 1.0777545,0.2231406 1.4667591,0.6203616 L 8.1661985,10.278137 8.9324937,9.4957421 8.6150957,9.1716755 C 8.0228222,8.5670037 7.2195858,8.2273042 6.3820415,8.2273042 c -0.8375444,0 -1.6407807,0.3396995 -2.233011,0.9443713 l -0.317387,0.3240666 0.766241,0.7823949 z M 12.883872,9.3337087 c -0.550163,0 -1.077787,0.2231406 -1.466813,0.6203616 L 11.099771,10.278137 10.333423,9.4957421 10.650819,9.1716755 c 0.592317,-0.6046718 1.395509,-0.9443713 2.233053,-0.9443713 0.837545,0 1.640738,0.3396995 2.233056,0.9443713 l 0.317397,0.3240666 -0.766349,0.7823949 -0.31729,-0.3240667 C 13.96166,9.5568493 13.434035,9.3337087 12.883872,9.3337087 Z M 9.6329576,16.52534 c -2.1672773,0 -3.7927356,-2.21281 -4.3345549,-3.319213 h 8.6691083 c -0.54182,1.106403 -2.167276,3.319213 -4.3345534,3.319213 z" fill="#ffffff" id="path2" style="display:none;stroke-width:1.09497" inkscape:label="Portrait" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 17.531776,3.4508426 1.936447,3.0983145 3.063553,4.9016859 -5,8" id="path4576" sodipodi:nodetypes="cccc" />
+ <g inkscape:groupmode="layer" id="layer3" inkscape:label="Position" style="display:inline">
+ <path d="m 15.435727,5.7204944 c 0,2.118883 -1.717741,3.8365686 -3.836615,3.8365686 -2.1188833,0 -3.8365806,-1.7176856 -3.8365806,-3.8365686 0,-2.1188838 1.7176973,-3.8365775 3.8365806,-3.8365775 2.118874,0 3.836615,1.7176937 3.836615,3.8365775 z" fill="#ffffff" id="path19723" style="stroke-width:1.05506" />
+ <path d="m 16.202965,15.244259 c 0,1.985936 -2.061163,1.985936 -4.603853,1.985936 -2.542658,0 -4.6038923,0 -4.6038923,-1.985936 0,-3.564724 2.0612343,-6.454507 4.6038923,-6.454507 2.54269,0 4.603853,2.889783 4.603853,6.454507 z" fill="#ffffff" id="path19725" style="stroke-width:1.05506" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.744;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="m 7.2613947,9.27472 h -4" id="path22754" sodipodi:nodetypes="cc" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="m 5.2613947,6.2747209 -2,2.9999991 2,3" id="path22756" sodipodi:nodetypes="ccc" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.64208;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="m 15.767666,9.21222 h 3.487458" id="path25041" sodipodi:nodetypes="cc" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.60066;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 17.511395,6.1617262 19.255124,9.21222 17.511395,12.262715" id="path25043" sodipodi:nodetypes="ccc" />
+ </g>
+ <path id="path26928" style="display:inline;fill:#ff4596;fill-opacity:1;stroke-width:0.613366" inkscape:label="path26928" d="m 20.13594,8.406361 c -0.272575,-8.2e-6 -0.500185,9.5e-6 -0.687018,0.012976 -0.194489,0.013249 -0.37819,0.041877 -0.556302,0.1156474 -0.4133,0.1711984 -0.741702,0.4995528 -0.912902,0.9128597 -0.08914,0.2151709 -0.113203,0.4419435 -0.122463,0.6884219 -0.0075,0.198126 -0.107439,0.363433 -0.25499,0.44863 -0.147562,0.08519 -0.340728,0.08912 -0.516018,-0.0037 -0.218061,-0.115251 -0.426533,-0.207727 -0.657444,-0.238134 -0.443521,-0.05839 -0.892077,0.06181 -1.246989,0.33413 -0.152944,0.117382 -0.269558,0.262171 -0.378298,0.423977 -0.104461,0.155439 -0.218268,0.352519 -0.354578,0.588618 l -0.01552,0.02687 c -0.136286,0.236087 -0.250093,0.433158 -0.332475,0.601337 -0.08575,0.17508 -0.152839,0.348492 -0.178013,0.53963 -0.05839,0.443529 0.06182,0.892086 0.334128,1.247025 0.141774,0.184685 0.326035,0.319018 0.534849,0.450213 0.167922,0.105499 0.261112,0.274802 0.2611,0.445256 -4e-6,0.170395 -0.09319,0.339684 -0.2611,0.445179 -0.208839,0.131218 -0.393112,0.265542 -0.534887,0.450286 -0.272334,0.354894 -0.39255,0.803431 -0.334165,1.246956 0.02519,0.191135 0.09222,0.364576 0.178014,0.53963 0.08239,0.168183 0.196157,0.365293 0.332477,0.601374 l 0.01552,0.02687 c 0.136299,0.236083 0.250113,0.433152 0.354577,0.588576 0.108764,0.161802 0.225385,0.306641 0.378336,0.423982 0.35491,0.272332 0.803465,0.392553 1.246989,0.334165 0.230896,-0.03043 0.439318,-0.122887 0.657371,-0.238137 0.175301,-0.09269 0.368508,-0.08873 0.516089,-0.0034 0.147577,0.08519 0.247617,0.250486 0.255026,0.448666 0.0092,0.246453 0.03333,0.473216 0.122463,0.688386 0.171199,0.413282 0.4996,0.741708 0.912902,0.912899 0.178115,0.0738 0.361807,0.102372 0.556303,0.115607 0.186836,0.01298 0.414443,0.01298 0.687018,0.01298 h 0.03098 c 0.272641,0 0.500197,0 0.687087,-0.01298 0.194489,-0.01325 0.378223,-0.04182 0.556344,-0.11562 0.413285,-0.171184 0.741671,-0.499617 0.912862,-0.912901 0.08914,-0.21517 0.113203,-0.441908 0.122409,-0.68842 0.0075,-0.198111 0.107425,-0.36345 0.254987,-0.448709 0.147577,-0.08522 0.340795,-0.08914 0.516092,0.0034 0.218051,0.115251 0.426474,0.207726 0.657404,0.2381 0.443522,0.05839 0.892099,-0.06174 1.246993,-0.334094 0.152961,-0.117441 0.269546,-0.262206 0.378297,-0.424009 0.104448,-0.155439 0.218252,-0.352497 0.354541,-0.588579 l 0.01569,-0.02687 c 0.136272,-0.236049 0.2501,-0.433192 0.332472,-0.601376 0.08573,-0.17506 0.152807,-0.348467 0.178015,-0.539596 0.05839,-0.443585 -0.06185,-0.89213 -0.334163,-1.247026 C 25.904205,15.710721 25.719888,15.576451 25.511101,15.445252 25.343179,15.339753 25.25,15.170453 25.25,15 c 0,-0.170395 0.09314,-0.339612 0.261028,-0.445112 0.208903,-0.131191 0.393243,-0.265477 0.534992,-0.450283 0.272333,-0.354903 0.392559,-0.803461 0.334166,-1.246992 -0.0252,-0.191132 -0.09228,-0.364516 -0.178014,-0.539595 -0.08236,-0.168164 -0.196157,-0.365264 -0.332511,-0.601337 l -0.01552,-0.02685 c -0.136362,-0.236031 -0.25012,-0.433148 -0.354576,-0.588582 -0.108763,-0.161803 -0.225359,-0.306611 -0.378332,-0.423976 -0.354895,-0.27233 -0.80344,-0.392521 -1.247026,-0.33413 -0.230874,0.03042 -0.439283,0.122886 -0.657331,0.238135 -0.17532,0.09266 -0.368517,0.08873 -0.516097,0.0034 -0.147577,-0.0852 -0.247635,-0.250542 -0.25506,-0.448696 C 22.436568,9.8895223 22.412433,9.6627607 22.323311,9.4476061 22.15213,9.0343853 21.823736,8.7060255 21.410451,8.5348311 21.232338,8.461061 21.048606,8.4324462 20.854107,8.4191837 20.667234,8.406208 20.439659,8.406208 20.167018,8.406208 Z m 0.01569,4.293575 c 1.270347,0 2.300127,1.029802 2.300127,2.300124 0,1.270344 -1.02978,2.300124 -2.300127,2.300124 -1.270314,0 -2.300123,-1.02978 -2.300123,-2.300124 0,-1.270322 1.029809,-2.300124 2.300123,-2.300124 z" />
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="31" height="22" viewBox="0 0 31 22" fill="none" version="1.1" id="svg4" sodipodi:docname="update_position.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs8" />
+ <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="16" inkscape:cx="23.84375" inkscape:cy="14.0625" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4">
+ <inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" />
+ </sodipodi:namedview>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="m 9.6329576,18.73815 c 4.1893454,0 7.5854694,-3.467473 7.5854694,-7.744834 0,-4.2773499 -3.396124,-7.7448337 -7.5854694,-7.7448337 -4.1893365,0 -7.5854711,3.4674838 -7.5854711,7.7448337 0,4.277361 3.3961346,7.744834 7.5854711,7.744834 z M 4.9152823,9.9540703 c 0.3890046,-0.397221 0.9166174,-0.6203616 1.4667592,-0.6203616 0.5501418,0 1.0777545,0.2231406 1.4667591,0.6203616 L 8.1661985,10.278137 8.9324937,9.4957421 8.6150957,9.1716755 C 8.0228222,8.5670037 7.2195858,8.2273042 6.3820415,8.2273042 c -0.8375444,0 -1.6407807,0.3396995 -2.233011,0.9443713 l -0.317387,0.3240666 0.766241,0.7823949 z M 12.883872,9.3337087 c -0.550163,0 -1.077787,0.2231406 -1.466813,0.6203616 L 11.099771,10.278137 10.333423,9.4957421 10.650819,9.1716755 c 0.592317,-0.6046718 1.395509,-0.9443713 2.233053,-0.9443713 0.837545,0 1.640738,0.3396995 2.233056,0.9443713 l 0.317397,0.3240666 -0.766349,0.7823949 -0.31729,-0.3240667 C 13.96166,9.5568493 13.434035,9.3337087 12.883872,9.3337087 Z M 9.6329576,16.52534 c -2.1672773,0 -3.7927356,-2.21281 -4.3345549,-3.319213 h 8.6691083 c -0.54182,1.106403 -2.167276,3.319213 -4.3345534,3.319213 z" fill="#ffffff" id="path2" style="display:none;stroke-width:1.09497" inkscape:label="Portrait" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 17.531776,3.4508426 1.936447,3.0983145 3.063553,4.9016859 -5,8" id="path4576" sodipodi:nodetypes="cccc" />
+ <g inkscape:groupmode="layer" id="layer3" inkscape:label="Position" style="display:inline">
+ <path d="m 15.435727,5.7204944 c 0,2.118883 -1.717741,3.8365686 -3.836615,3.8365686 -2.1188833,0 -3.8365806,-1.7176856 -3.8365806,-3.8365686 0,-2.1188838 1.7176973,-3.8365775 3.8365806,-3.8365775 2.118874,0 3.836615,1.7176937 3.836615,3.8365775 z" fill="#ffffff" id="path19723" style="stroke-width:1.05506" />
+ <path d="m 16.202965,15.244259 c 0,1.985936 -2.061163,1.985936 -4.603853,1.985936 -2.542658,0 -4.6038923,0 -4.6038923,-1.985936 0,-3.564724 2.0612343,-6.454507 4.6038923,-6.454507 2.54269,0 4.603853,2.889783 4.603853,6.454507 z" fill="#ffffff" id="path19725" style="stroke-width:1.05506" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.744;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="m 7.2613947,9.27472 h -4" id="path22754" sodipodi:nodetypes="cc" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="m 5.2613947,6.2747209 -2,2.9999991 2,3" id="path22756" sodipodi:nodetypes="ccc" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.64208;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="m 15.767666,9.21222 h 3.487458" id="path25041" sodipodi:nodetypes="cc" />
+ <path style="fill:none;stroke:#ffffff;stroke-width:1.60066;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 17.511395,6.1617262 19.255124,9.21222 17.511395,12.262715" id="path25043" sodipodi:nodetypes="ccc" />
+ </g>
+ <path id="path26928" style="display:inline;fill:#ff4596;fill-opacity:1;stroke-width:0.613366" inkscape:label="path26928" d="m 20.13594,8.406361 c -0.272575,-8.2e-6 -0.500185,9.5e-6 -0.687018,0.012976 -0.194489,0.013249 -0.37819,0.041877 -0.556302,0.1156474 -0.4133,0.1711984 -0.741702,0.4995528 -0.912902,0.9128597 -0.08914,0.2151709 -0.113203,0.4419435 -0.122463,0.6884219 -0.0075,0.198126 -0.107439,0.363433 -0.25499,0.44863 -0.147562,0.08519 -0.340728,0.08912 -0.516018,-0.0037 -0.218061,-0.115251 -0.426533,-0.207727 -0.657444,-0.238134 -0.443521,-0.05839 -0.892077,0.06181 -1.246989,0.33413 -0.152944,0.117382 -0.269558,0.262171 -0.378298,0.423977 -0.104461,0.155439 -0.218268,0.352519 -0.354578,0.588618 l -0.01552,0.02687 c -0.136286,0.236087 -0.250093,0.433158 -0.332475,0.601337 -0.08575,0.17508 -0.152839,0.348492 -0.178013,0.53963 -0.05839,0.443529 0.06182,0.892086 0.334128,1.247025 0.141774,0.184685 0.326035,0.319018 0.534849,0.450213 0.167922,0.105499 0.261112,0.274802 0.2611,0.445256 -4e-6,0.170395 -0.09319,0.339684 -0.2611,0.445179 -0.208839,0.131218 -0.393112,0.265542 -0.534887,0.450286 -0.272334,0.354894 -0.39255,0.803431 -0.334165,1.246956 0.02519,0.191135 0.09222,0.364576 0.178014,0.53963 0.08239,0.168183 0.196157,0.365293 0.332477,0.601374 l 0.01552,0.02687 c 0.136299,0.236083 0.250113,0.433152 0.354577,0.588576 0.108764,0.161802 0.225385,0.306641 0.378336,0.423982 0.35491,0.272332 0.803465,0.392553 1.246989,0.334165 0.230896,-0.03043 0.439318,-0.122887 0.657371,-0.238137 0.175301,-0.09269 0.368508,-0.08873 0.516089,-0.0034 0.147577,0.08519 0.247617,0.250486 0.255026,0.448666 0.0092,0.246453 0.03333,0.473216 0.122463,0.688386 0.171199,0.413282 0.4996,0.741708 0.912902,0.912899 0.178115,0.0738 0.361807,0.102372 0.556303,0.115607 0.186836,0.01298 0.414443,0.01298 0.687018,0.01298 h 0.03098 c 0.272641,0 0.500197,0 0.687087,-0.01298 0.194489,-0.01325 0.378223,-0.04182 0.556344,-0.11562 0.413285,-0.171184 0.741671,-0.499617 0.912862,-0.912901 0.08914,-0.21517 0.113203,-0.441908 0.122409,-0.68842 0.0075,-0.198111 0.107425,-0.36345 0.254987,-0.448709 0.147577,-0.08522 0.340795,-0.08914 0.516092,0.0034 0.218051,0.115251 0.426474,0.207726 0.657404,0.2381 0.443522,0.05839 0.892099,-0.06174 1.246993,-0.334094 0.152961,-0.117441 0.269546,-0.262206 0.378297,-0.424009 0.104448,-0.155439 0.218252,-0.352497 0.354541,-0.588579 l 0.01569,-0.02687 c 0.136272,-0.236049 0.2501,-0.433192 0.332472,-0.601376 0.08573,-0.17506 0.152807,-0.348467 0.178015,-0.539596 0.05839,-0.443585 -0.06185,-0.89213 -0.334163,-1.247026 C 25.904205,15.710721 25.719888,15.576451 25.511101,15.445252 25.343179,15.339753 25.25,15.170453 25.25,15 c 0,-0.170395 0.09314,-0.339612 0.261028,-0.445112 0.208903,-0.131191 0.393243,-0.265477 0.534992,-0.450283 0.272333,-0.354903 0.392559,-0.803461 0.334166,-1.246992 -0.0252,-0.191132 -0.09228,-0.364516 -0.178014,-0.539595 -0.08236,-0.168164 -0.196157,-0.365264 -0.332511,-0.601337 l -0.01552,-0.02685 c -0.136362,-0.236031 -0.25012,-0.433148 -0.354576,-0.588582 -0.108763,-0.161803 -0.225359,-0.306611 -0.378332,-0.423976 -0.354895,-0.27233 -0.80344,-0.392521 -1.247026,-0.33413 -0.230874,0.03042 -0.439283,0.122886 -0.657331,0.238135 -0.17532,0.09266 -0.368517,0.08873 -0.516097,0.0034 -0.147577,-0.0852 -0.247635,-0.250542 -0.25506,-0.448696 C 22.436568,9.8895223 22.412433,9.6627607 22.323311,9.4476061 22.15213,9.0343853 21.823736,8.7060255 21.410451,8.5348311 21.232338,8.461061 21.048606,8.4324462 20.854107,8.4191837 20.667234,8.406208 20.439659,8.406208 20.167018,8.406208 Z m 0.01569,4.293575 c 1.270347,0 2.300127,1.029802 2.300127,2.300124 0,1.270344 -1.02978,2.300124 -2.300127,2.300124 -1.270314,0 -2.300123,-1.02978 -2.300123,-2.300124 0,-1.270322 1.029809,-2.300124 2.300123,-2.300124 z" />
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="31" height="22" viewBox="0 0 31 22" fill="none" version="1.1" id="svg4" sodipodi:docname="update_z_index.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs8" />
+ <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="16" inkscape:cx="23.84375" inkscape:cy="14.0625" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4">
+ <inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" />
+ </sodipodi:namedview>
+ <g inkscape:groupmode="layer" id="layer1" inkscape:label="Z_Index" style="display:inline" transform="matrix(0.90945359,0,0,0.90945359,0.03431233,1.6441321)">
+ <path style="fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="M 4.0841125,6.8565774 V 16.856578 H 12.084113 V 6.8565774 Z" id="path6916" sodipodi:nodetypes="ccccc" />
+ <path style="fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 12.084113,13.856578 h 3 V 3.8565774 H 7.0841125 v 3" id="path6920" sodipodi:nodetypes="ccccc" />
+ <path style="fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 15.084113,10.856578 h 3 V 0.8565774 h -8.000001 v 3" id="path6922" sodipodi:nodetypes="ccccc" />
+ <path style="fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 6.5841125,9.8565775 h 3 l -3,4.0000005 h 3" id="path6924" sodipodi:nodetypes="cccc" />
+ </g>
+ <path id="path26928" style="display:inline;fill:#ff4596;fill-opacity:1;stroke-width:0.613366" inkscape:label="path26928" d="m 19.07344,7.281361 c -0.272575,-8.2e-6 -0.500185,9.5e-6 -0.687018,0.012976 -0.194489,0.013249 -0.37819,0.041877 -0.556302,0.1156474 -0.4133,0.1711984 -0.741702,0.4995528 -0.912902,0.9128597 -0.08914,0.2151709 -0.113203,0.4419435 -0.122463,0.6884214 -0.0075,0.1981263 -0.107439,0.3634338 -0.25499,0.4486307 -0.147562,0.085189 -0.340728,0.089123 -0.516018,-0.00369 -0.218061,-0.1152514 -0.426533,-0.207727 -0.657444,-0.2381338 -0.443521,-0.058391 -0.892077,0.061805 -1.246989,0.3341293 -0.152944,0.1173821 -0.269558,0.2621716 -0.378298,0.4239769 -0.104461,0.1554394 -0.218268,0.3525194 -0.354578,0.5886184 l -0.01552,0.02687 c -0.136286,0.236087 -0.250093,0.433158 -0.332475,0.601337 -0.08575,0.17508 -0.152839,0.348492 -0.178013,0.53963 -0.05839,0.443529 0.06182,0.892086 0.334128,1.247025 0.141774,0.184685 0.326035,0.319018 0.534849,0.450213 0.167922,0.105499 0.261112,0.274802 0.2611,0.445256 -4e-6,0.170395 -0.09319,0.339684 -0.2611,0.445179 -0.208839,0.131218 -0.393112,0.265542 -0.534887,0.450286 -0.272334,0.354894 -0.39255,0.803431 -0.334165,1.246956 0.02519,0.191135 0.09222,0.364576 0.178014,0.53963 0.08239,0.168183 0.196157,0.365293 0.332477,0.601374 l 0.01552,0.02687 c 0.136299,0.236083 0.250113,0.433152 0.354577,0.588576 0.108764,0.161802 0.225385,0.306641 0.378336,0.423982 0.35491,0.272332 0.803465,0.392553 1.246989,0.334165 0.230896,-0.03043 0.439318,-0.122887 0.657371,-0.238137 0.175301,-0.09269 0.368508,-0.08873 0.516089,-0.0034 0.147577,0.08519 0.247617,0.250486 0.255026,0.448666 0.0092,0.246453 0.03333,0.473216 0.122463,0.688386 0.171199,0.413282 0.4996,0.741708 0.912902,0.912899 0.178115,0.0738 0.361807,0.102372 0.556303,0.115607 0.186836,0.01298 0.414443,0.01298 0.687018,0.01298 h 0.03098 c 0.272641,0 0.500197,0 0.687087,-0.01298 0.194489,-0.01325 0.378223,-0.04182 0.556344,-0.11562 0.413285,-0.171184 0.741671,-0.499617 0.912862,-0.912901 0.08914,-0.21517 0.113203,-0.441908 0.122409,-0.68842 0.0075,-0.198111 0.107425,-0.36345 0.254987,-0.448709 0.147577,-0.08522 0.340795,-0.08914 0.516092,0.0034 0.218051,0.115251 0.426474,0.207726 0.657404,0.2381 0.443522,0.05839 0.892099,-0.06174 1.246993,-0.334094 0.152961,-0.117441 0.269546,-0.262206 0.378297,-0.424009 0.104448,-0.155439 0.218252,-0.352497 0.354541,-0.588579 l 0.01569,-0.02687 c 0.136272,-0.236049 0.2501,-0.433192 0.332472,-0.601376 0.08573,-0.17506 0.152807,-0.348467 0.178015,-0.539596 0.05839,-0.443585 -0.06185,-0.89213 -0.334163,-1.247026 C 24.841705,14.585721 24.657388,14.451451 24.448601,14.320252 24.280679,14.214753 24.1875,14.045453 24.1875,13.875 c 0,-0.170395 0.09314,-0.339612 0.261028,-0.445112 0.208903,-0.131191 0.393243,-0.265477 0.534992,-0.450283 0.272333,-0.354903 0.392559,-0.803461 0.334166,-1.246992 -0.0252,-0.191132 -0.09228,-0.364516 -0.178014,-0.539595 -0.08236,-0.168164 -0.196157,-0.365264 -0.332511,-0.601337 l -0.01552,-0.02685 C 24.655279,10.328797 24.541521,10.13168 24.437065,9.9762461 24.328302,9.8144435 24.211706,9.6696349 24.058733,9.5522705 23.703838,9.2799396 23.255293,9.1597493 22.811707,9.2181399 22.580833,9.2485576 22.372424,9.3410264 22.154376,9.456275 21.979056,9.548935 21.785859,9.545001 21.638279,9.459685 21.490702,9.3744873 21.390644,9.2091429 21.383219,9.0109893 21.374068,8.7645223 21.349933,8.5377607 21.260811,8.3226061 21.08963,7.9093853 20.761236,7.5810255 20.347951,7.4098311 20.169838,7.336061 19.986106,7.3074462 19.791607,7.2941837 19.604734,7.281208 19.377159,7.281208 19.104518,7.281208 Z m 0.01569,4.293575 c 1.270347,0 2.300127,1.029802 2.300127,2.300124 0,1.270344 -1.02978,2.300124 -2.300127,2.300124 -1.270314,0 -2.300123,-1.02978 -2.300123,-2.300124 0,-1.270322 1.029809,-2.300124 2.300123,-2.300124 z" />
+</svg>
--- /dev/null
+@tool
+class_name DialogicChoiceEvent
+extends DialogicEvent
+
+## Event that allows adding choices. Needs to go after a text event (or another choices EndBranch).
+
+enum ElseActions {HIDE=0, DISABLE=1, DEFAULT=2}
+
+
+### Settings
+## The text that is displayed on the choice button.
+var text := ""
+## If not empty this condition will determine if this choice is active.
+var condition := ""
+## Determines what happens if [condition] is false. Default will use the action set in the settings.
+var else_action := ElseActions.DEFAULT
+## The text that is displayed if [condition] is false and [else_action] is Disable.
+## If empty [text] will be used for disabled button as well.
+var disabled_text := ""
+## A dictionary that can be filled with arbitrary information
+## This can then be interpreted by a custom choice layer
+var extra_data := {}
+
+
+## UI helper
+var _has_condition := false
+
+#endregion
+
+var regex := RegEx.create_from_string(r'- (?<text>(?>\\\||(?(?=.*\|)[^|]|(?!\[if)[^|]))*)\|?\s*(\[if(?<condition>([^\]\[]|\[[^\]]*\])+)\])?\s*(\[(?<shortcode>[^]]*)\])?')
+
+#region EXECUTION
+################################################################################
+
+func _execute() -> void:
+ if dialogic.Choices.is_question(dialogic.current_event_idx):
+ dialogic.Choices.show_current_question(false)
+ dialogic.current_state = dialogic.States.AWAITING_CHOICE
+
+#endregion
+
+
+#region INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Choice"
+ set_default_color('Color3')
+ event_category = "Flow"
+ event_sorting_index = 0
+ can_contain_events = true
+ wants_to_group = true
+
+
+# return a control node that should show on the END BRANCH node
+func get_end_branch_control() -> Control:
+ return load(get_script().resource_path.get_base_dir().path_join('ui_choice_end.tscn')).instantiate()
+#endregion
+
+
+#region SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ var result_string := ""
+
+ result_string = "- "+text.strip_edges()
+ var shortcode := store_to_shortcode_parameters()
+ if (condition and _has_condition) or shortcode or extra_data:
+ result_string += " |"
+ if condition and _has_condition:
+ result_string += " [if " + condition + "]"
+
+ if shortcode or extra_data:
+ result_string += " [" + shortcode
+ if extra_data:
+ var extra_data_string := ""
+ for i in extra_data:
+ extra_data_string += " " + i + '="' + value_to_string(extra_data[i]) + '"'
+ if shortcode:
+ result_string += " "
+ result_string += extra_data_string.strip_edges()
+ result_string += "]"
+
+ return result_string
+
+
+func from_text(string:String) -> void:
+ var result := regex.search(string.strip_edges())
+ if result == null:
+ return
+ text = result.get_string('text').strip_edges()
+ condition = result.get_string('condition').strip_edges()
+ _has_condition = not condition.is_empty()
+ if result.get_string('shortcode'):
+ load_from_shortcode_parameters(result.get_string("shortcode"))
+ var shortcode := parse_shortcode_parameters(result.get_string('shortcode'))
+ shortcode.erase("else")
+ shortcode.erase("alt_text")
+ extra_data = shortcode.duplicate()
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ "else" : {"property": "else_action", "default": ElseActions.DEFAULT,
+ "suggestions": func(): return {
+ "Default" :{'value':ElseActions.DEFAULT, 'text_alt':['default']},
+ "Hide" :{'value':ElseActions.HIDE,'text_alt':['hide'] },
+ "Disable" :{'value':ElseActions.DISABLE,'text_alt':['disable']}}},
+ "alt_text" : {"property": "disabled_text", "default": ""},
+ "extra_data" : {"property": "extra_data", "default": {}, "custom_stored":true},
+ }
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges().begins_with("-"):
+ return true
+ return false
+
+#endregion
+
+#region TRANSLATIONS
+################################################################################
+
+func _get_translatable_properties() -> Array:
+ return ['text', 'disabled_text']
+
+
+func _get_property_original_translation(property:String) -> String:
+ match property:
+ 'text':
+ return text
+ 'disabled_text':
+ return disabled_text
+ return ''
+#endregion
+
+
+#region EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit("text", ValueType.SINGLELINE_TEXT, {'autofocus':true})
+ add_body_edit("", ValueType.LABEL, {"text":"Condition:"})
+ add_body_edit("_has_condition", ValueType.BOOL_BUTTON, {"editor_icon":["Add", "EditorIcons"], "tooltip":"Add Condition"}, "not _has_condition")
+ add_body_edit("condition", ValueType.CONDITION, {}, "_has_condition")
+ add_body_edit("_has_condition", ValueType.BOOL_BUTTON, {"editor_icon":["Remove", "EditorIcons"], "tooltip":"Remove Condition"}, "_has_condition")
+ add_body_edit("else_action", ValueType.FIXED_OPTIONS, {'left_text':'Else:',
+ 'options': [
+ {
+ 'label': 'Default',
+ 'value': ElseActions.DEFAULT,
+ },
+ {
+ 'label': 'Hide',
+ 'value': ElseActions.HIDE,
+ },
+ {
+ 'label': 'Disable',
+ 'value': ElseActions.DISABLE,
+ }
+ ]}, '_has_condition')
+ add_body_edit("disabled_text", ValueType.SINGLELINE_TEXT, {
+ 'left_text':'Disabled text:',
+ 'placeholder':'(Empty for same)'}, 'allow_alt_text()')
+ add_body_line_break()
+ add_body_edit("extra_data", ValueType.DICTIONARY, {"left_text":"Extra Data:"})
+
+
+func allow_alt_text() -> bool:
+ return _has_condition and (
+ else_action == ElseActions.DISABLE or
+ (else_action == ElseActions.DEFAULT and
+ ProjectSettings.get_setting("dialogic/choices/def_false_behaviour", 0) == 1))
+#endregion
+
+
+#region CODE COMPLETION
+################################################################################
+
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ line = CodeCompletionHelper.get_line_untill_caret(line)
+
+ if !'[if' in line:
+ if symbol == '{':
+ CodeCompletionHelper.suggest_variables(TextNode)
+ return
+
+ if symbol == '[':
+ if !'[if' in line and line.count('[') - line.count(']') == 1:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'if', 'if ', TextNode.syntax_highlighter.code_flow_color)
+ elif '[if' in line:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'else', 'else="', TextNode.syntax_highlighter.code_flow_color)
+ if symbol == ' ' and '[else' in line:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'alt_text', 'alt_text="', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5))
+ elif symbol == '{':
+ CodeCompletionHelper.suggest_variables(TextNode)
+ if (symbol == '=' or symbol == '"') and line.count('[') > 1 and !'" ' in line:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'default', "default", event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), null, '"')
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'hide', "hide", event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), null, '"')
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'disable', "disable", event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), null, '"')
+#endregion
+
+
+#region SYNTAX HIGHLIGHTING
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ var result := regex.search(line)
+
+ dict[0] = {'color':event_color}
+
+ if not result:
+ return dict
+
+ var condition_begin := result.get_start("condition")
+ var condition_end := result.get_end("condition")
+
+ var shortcode_begin := result.get_start("shortcode")
+
+ dict = Highlighter.color_region(dict, event_color.lerp(Highlighter.variable_color, 0.5), line, '{','}', 0, condition_begin, event_color)
+
+ if condition_begin > 0:
+ var from := line.find('[if')
+ dict[from] = {"color":Highlighter.normal_color}
+ dict[from+1] = {"color":Highlighter.code_flow_color}
+ dict[condition_begin] = {"color":Highlighter.normal_color}
+ dict = Highlighter.color_condition(dict, line, condition_begin, condition_end)
+ if shortcode_begin:
+ dict = Highlighter.color_shortcode_content(dict, line, shortcode_begin, 0, event_color)
+ return dict
+#endregion
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="choice-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#464646" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="1.0544998" inkscape:cx="-184.44765" inkscape:cy="-1.8966339" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
+ <defs id="defs2" />
+ <rect style="stroke-width:3.57045;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1" id="rect26013" width="58.831448" height="24.065563" x="-167.86046" y="-31.618748" ry="12.032781" transform="matrix(0.15519359,0,0,0.15519359,31.232208,8.9372117)" />
+ <path sodipodi:type="star" style="stroke-width:8.16565;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1" id="path26015" inkscape:flatsided="false" sodipodi:sides="3" sodipodi:cx="-138.47092" sodipodi:cy="-7.3761749" sodipodi:r1="20.010386" sodipodi:r2="10.457433" sodipodi:arg1="0.52359878" sodipodi:arg2="1.5707963" inkscape:rounded="0.02640344" inkscape:randomized="0" d="m -121.14141,2.6290179 c -0.22886,0.3963922 -16.87179,0.4522399 -17.32951,0.4522399 -0.45771,0 -17.10064,-0.055848 -17.3295,-0.45224 -0.22886,-0.3963922 8.04424,-14.8375168 8.2731,-15.2339088 0.22886,-0.396392 8.59869,-14.781669 9.0564,-14.781669 0.45772,0 8.82755,14.385276 9.05641,14.781668 0.22885,0.396393 8.50195,14.8375177 8.2731,15.2339099 z" transform="matrix(0,-0.07693147,-0.06541836,0,2.2708052,-4.7551753)" inkscape:transform-center-x="-0.73902511" />
+ <path id="path28608" style="stroke-width:3.3313;stroke-dasharray:none;fill:#ffffff;color:#000000;fill-opacity:0.45;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none" d="m -155.82823,2.6553184 c -7.56011,0 -13.69881,6.1387113 -13.69881,13.6988156 0,7.560104 6.1387,13.698816 13.69881,13.698816 h 34.76649 c 7.56011,0 13.69882,-6.138712 13.69882,-13.698816 0,-7.5601043 -6.13871,-13.6988156 -13.69882,-13.6988156 z" transform="matrix(0.15519359,0,0,0.15519359,31.232208,8.9372117)" />
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_choice.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Choices', 'script':this_folder.path_join('subsystem_choices.gd')}]
+
+
+func _get_settings_pages() -> Array:
+ return [this_folder.path_join('settings_choices.tscn')]
--- /dev/null
+class_name DialogicNode_ButtonSound
+extends AudioStreamPlayer
+
+## Node that is used for playing sound effects on hover/focus/press of sibling DialogicNode_ChoiceButtons.
+
+## Sound to be played if one of the sibling ChoiceButtons is pressed.
+## If sibling ChoiceButton has a sound_pressed set, that is prioritized.
+@export var sound_pressed: AudioStream
+## Sound to be played on hover. See [sound_pressed] for more.
+@export var sound_hover: AudioStream
+## Sound to be played on focus. See [sound_pressed] for more.
+@export var sound_focus: AudioStream
+
+func _ready() -> void:
+ add_to_group('dialogic_button_sound')
+ _connect_all_buttons()
+
+#basic play sound
+func play_sound(sound) -> void:
+ if sound != null:
+ stream = sound
+ play()
+
+func _connect_all_buttons() -> void:
+ for child in get_parent().get_children():
+ if child is DialogicNode_ChoiceButton:
+ child.button_up.connect(_on_pressed.bind(child.sound_pressed))
+ child.mouse_entered.connect(_on_hover.bind(child.sound_hover))
+ child.focus_entered.connect(_on_focus.bind(child.sound_focus))
+
+
+#the custom_sound argument comes from the specifec button and get used
+#if none are found, it uses the above sounds
+
+func _on_pressed(custom_sound) -> void:
+ if custom_sound != null:
+ play_sound(custom_sound)
+ else:
+ play_sound(sound_pressed)
+
+func _on_hover(custom_sound) -> void:
+ if custom_sound != null:
+ play_sound(custom_sound)
+ else:
+ play_sound(sound_hover)
+
+func _on_focus(custom_sound) -> void:
+ if custom_sound != null:
+ play_sound(custom_sound)
+ else:
+ play_sound(sound_focus)
+
--- /dev/null
+class_name DialogicNode_ChoiceButton
+extends Button
+## The button allows the player to make a choice in the Dialogic system.
+##
+## This class is used in the Choice Layer. [br]
+## You may change the [member text_node] to any [class Node] that has a
+## `text` property. [br]
+## If you don't set the [member text_node], the text will be set on this
+## button instead.
+##
+## Using a different node may allow using rich text effects; they are
+## not supported on buttons at this point.
+
+
+## Used to identify what choices to put on. If you leave it at -1, choices will be distributed automatically.
+@export var choice_index: int = -1
+
+## Can be set to play this sound when pressed. Requires a sibling DialogicNode_ButtonSound node.
+@export var sound_pressed: AudioStream
+## Can be set to play this sound when hovered. Requires a sibling DialogicNode_ButtonSound node.
+@export var sound_hover: AudioStream
+## Can be set to play this sound when focused. Requires a sibling DialogicNode_ButtonSound node.
+@export var sound_focus: AudioStream
+## If set, the text will be set on this node's `text` property instead.
+@export var text_node: Node
+
+
+func _ready() -> void:
+ add_to_group('dialogic_choice_button')
+ shortcut_in_tooltip = false
+ hide()
+
+
+func _load_info(choice_info: Dictionary) -> void:
+ set_choice_text(choice_info.text)
+ visible = choice_info.visible
+ disabled = choice_info.disabled
+
+
+## Called when the text changes.
+func set_choice_text(new_text: String) -> void:
+ if text_node:
+ text_node.text = new_text
+ else:
+ text = new_text
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+func _refresh() -> void:
+ %Autofocus.button_pressed = ProjectSettings.get_setting('dialogic/choices/autofocus_first', true)
+ %Delay.value = ProjectSettings.get_setting('dialogic/choices/delay', 0.2)
+ %FalseBehaviour.select(ProjectSettings.get_setting('dialogic/choices/def_false_behaviour', 0))
+ %HotkeyType.select(ProjectSettings.get_setting('dialogic/choices/hotkey_behaviour', 0))
+
+ var reveal_delay: float = ProjectSettings.get_setting('dialogic/choices/reveal_delay', 0)
+ var reveal_by_input: bool = ProjectSettings.get_setting('dialogic/choices/reveal_by_input', false)
+ if not reveal_by_input and reveal_delay == 0:
+ _on_appear_mode_item_selected(0)
+ if not reveal_by_input and reveal_delay != 0:
+ _on_appear_mode_item_selected(1)
+ if reveal_by_input and reveal_delay == 0:
+ _on_appear_mode_item_selected(2)
+ if reveal_by_input and reveal_delay != 0:
+ _on_appear_mode_item_selected(3)
+
+ %RevealDelay.value = reveal_delay
+
+func _on_Autofocus_toggled(button_pressed: bool) -> void:
+ ProjectSettings.set_setting('dialogic/choices/autofocus_first', button_pressed)
+ ProjectSettings.save()
+
+
+func _on_FalseBehaviour_item_selected(index) -> void:
+ ProjectSettings.set_setting('dialogic/choices/def_false_behaviour', index)
+ ProjectSettings.save()
+
+
+func _on_HotkeyType_item_selected(index) -> void:
+ ProjectSettings.set_setting('dialogic/choices/hotkey_behaviour', index)
+ ProjectSettings.save()
+
+
+func _on_Delay_value_changed(value) -> void:
+ ProjectSettings.set_setting('dialogic/choices/delay', value)
+ ProjectSettings.save()
+
+
+func _on_reveal_delay_value_changed(value) -> void:
+ ProjectSettings.set_setting('dialogic/choices/reveal_delay', value)
+ ProjectSettings.save()
+
+
+func _on_appear_mode_item_selected(index:int) -> void:
+ %AppearMode.selected = index
+ match index:
+ 0:
+ ProjectSettings.set_setting('dialogic/choices/reveal_delay', 0)
+ ProjectSettings.set_setting('dialogic/choices/reveal_by_input', false)
+ %RevealDelay.hide()
+ 1:
+ ProjectSettings.set_setting('dialogic/choices/reveal_delay', %RevealDelay.value)
+ ProjectSettings.set_setting('dialogic/choices/reveal_by_input', false)
+ %RevealDelay.show()
+ 2:
+ ProjectSettings.set_setting('dialogic/choices/reveal_delay', 0)
+ ProjectSettings.set_setting('dialogic/choices/reveal_by_input', true)
+ %RevealDelay.hide()
+ 3:
+ ProjectSettings.set_setting('dialogic/choices/reveal_delay', %RevealDelay.value)
+ ProjectSettings.set_setting('dialogic/choices/reveal_by_input', true)
+ %RevealDelay.show()
+ ProjectSettings.save()
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://uarvgnbrcltm"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Choice/settings_choices.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_nxutt"]
+
+[sub_resource type="Image" id="Image_2imc3"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_udy8i"]
+image = SubResource("Image_2imc3")
+
+[node name="Choices" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_bottom = -227.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2")
+
+[node name="VBoxContainer2" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="VBoxContainer2"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Behaviour"
+
+[node name="VBoxContainer" type="GridContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+columns = 2
+
+[node name="AutofocusLabel" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/AutofocusLabel"]
+layout_mode = 2
+text = "Autofocus first choice"
+
+[node name="Autofocus" type="CheckBox" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="AppearModeLabel" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="VBoxContainer/AppearModeLabel"]
+layout_mode = 2
+text = "Choices appear"
+
+[node name="HintTooltip" parent="VBoxContainer/AppearModeLabel" instance=ExtResource("2_nxutt")]
+layout_mode = 2
+tooltip_text = "Choices can appear either instantly when the text finished, after a delay, a click or either."
+texture = SubResource("ImageTexture_udy8i")
+hint_text = "Choices can appear either instantly when the text finished, after a delay, a click or either."
+
+[node name="RevealDelayLabel" type="HBoxContainer" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="AppearMode" type="OptionButton" parent="VBoxContainer/RevealDelayLabel"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+item_count = 4
+selected = 0
+fit_to_longest_item = false
+popup/item_0/text = "Instantly"
+popup/item_0/id = 0
+popup/item_1/text = "After delay"
+popup/item_1/id = 1
+popup/item_2/text = "After another click"
+popup/item_2/id = 2
+popup/item_3/text = "After delay or click"
+popup/item_3/id = 3
+
+[node name="RevealDelay" type="SpinBox" parent="VBoxContainer/RevealDelayLabel"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Delay after which choices will appear (in seconds)."
+step = 0.01
+
+[node name="DelayLabel" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="VBoxContainer/DelayLabel"]
+layout_mode = 2
+text = "Delay before choices can be pressed"
+
+[node name="HintTooltip2" parent="VBoxContainer/DelayLabel" instance=ExtResource("2_nxutt")]
+layout_mode = 2
+tooltip_text = "Adding a small delay before choices can be activated can prevent accidentally choosing an option."
+texture = SubResource("ImageTexture_udy8i")
+hint_text = "Adding a small delay before choices can be activated can prevent accidentally choosing an option."
+
+[node name="Delay" type="SpinBox" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+step = 0.01
+
+[node name="DefaultFalseBehaviourLabel" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="VBoxContainer/DefaultFalseBehaviourLabel"]
+layout_mode = 2
+text = "Default behaviour for false choices"
+
+[node name="HintTooltip3" parent="VBoxContainer/DefaultFalseBehaviourLabel" instance=ExtResource("2_nxutt")]
+layout_mode = 2
+tooltip_text = "Define the default behaviour (hide or disable) for choices that have a condition that isn't met.
+
+Choices can overwrite this setting individually."
+texture = SubResource("ImageTexture_udy8i")
+hint_text = "Define the default behaviour (hide or disable) for choices that have a condition that isn't met.
+
+Choices can overwrite this setting individually."
+
+[node name="FalseBehaviour" type="OptionButton" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 2
+selected = 0
+popup/item_0/text = "Hide"
+popup/item_0/id = 0
+popup/item_1/text = "Disable"
+popup/item_1/id = 1
+
+[node name="HSeparator" type="HSeparator" parent="."]
+layout_mode = 2
+
+[node name="HotkeySelection" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Title2" type="Label" parent="HotkeySelection"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Choice Hotkeys"
+
+[node name="HintTooltip4" parent="HotkeySelection" instance=ExtResource("2_nxutt")]
+layout_mode = 2
+tooltip_text = "You can add more complex hotkeys (or individual ones) by editing the choice buttons of your layout scene."
+texture = SubResource("ImageTexture_udy8i")
+hint_text = "You can add more complex hotkeys (or individual ones) by editing the choice buttons of your layout scene."
+
+[node name="VBoxContainer3" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Label4" type="Label" parent="VBoxContainer3"]
+layout_mode = 2
+text = "Hotkey type"
+
+[node name="HotkeyType" type="OptionButton" parent="VBoxContainer3"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+item_count = 2
+selected = 0
+popup/item_0/text = "No Hotkeys"
+popup/item_0/id = 0
+popup/item_1/text = "Default (1-9)"
+popup/item_1/id = 1
+
+[connection signal="toggled" from="VBoxContainer/Autofocus" to="." method="_on_Autofocus_toggled"]
+[connection signal="item_selected" from="VBoxContainer/RevealDelayLabel/AppearMode" to="." method="_on_appear_mode_item_selected"]
+[connection signal="value_changed" from="VBoxContainer/RevealDelayLabel/RevealDelay" to="." method="_on_reveal_delay_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/Delay" to="." method="_on_Delay_value_changed"]
+[connection signal="item_selected" from="VBoxContainer/FalseBehaviour" to="." method="_on_FalseBehaviour_item_selected"]
+[connection signal="item_selected" from="VBoxContainer3/HotkeyType" to="." method="_on_HotkeyType_item_selected"]
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that manages showing and activating of choices.
+
+## Emitted when a choice button was pressed. Info includes the keys 'button_index', 'text', 'event_index'.
+signal choice_selected(info:Dictionary)
+## Emitted when a set of choices is reached and shown.
+## Info includes the keys 'choices' (an array of dictionaries with infos on all the choices).
+signal question_shown(info:Dictionary)
+
+## Contains information on the latest question.
+var last_question_info := {}
+
+## The delay between the text finishing revealing and the choices appearing
+var reveal_delay := 0.0
+## If true the player has to click to reveal choices when they are reached
+var reveal_by_input := false
+## The delay between the choices becoming visible and being clickable. Can prevent accidental selection.
+var block_delay := 0.2
+## If true, the first (top-most) choice will be focused
+var autofocus_first_choice := true
+## If true the dialogic input action is used to trigger choices.
+## However mouse events will be ignored no matter what.
+var use_input_action := false
+
+enum FalseBehaviour {HIDE=0, DISABLE=1}
+## The behaviour of choices with a false condition and else_action set to DEFAULT.
+var default_false_behaviour := FalseBehaviour.HIDE
+
+enum HotkeyBehaviour {NONE, NUMBERS}
+## Will add some hotkeys to the choices if different then HotkeyBehaviour.NONE.
+var hotkey_behaviour := HotkeyBehaviour.NONE
+
+
+### INTERNALS
+
+## Used to block choices from being clicked for a couple of seconds (if delay is set in settings).
+var _choice_blocker := Timer.new()
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ hide_all_choices()
+
+
+func _ready() -> void:
+ _choice_blocker.one_shot = true
+ DialogicUtil.update_timer_process_callback(_choice_blocker)
+ add_child(_choice_blocker)
+
+ reveal_delay = float(ProjectSettings.get_setting('dialogic/choices/reveal_delay', reveal_delay))
+ reveal_by_input = ProjectSettings.get_setting('dialogic/choices/reveal_by_input', reveal_by_input)
+ block_delay = ProjectSettings.get_setting('dialogic/choices/delay', block_delay)
+ autofocus_first_choice = ProjectSettings.get_setting('dialogic/choices/autofocus_first', autofocus_first_choice)
+ hotkey_behaviour = ProjectSettings.get_setting('dialogic/choices/hotkey_behaviour', hotkey_behaviour)
+ default_false_behaviour = ProjectSettings.get_setting('dialogic/choices/def_false_behaviour', default_false_behaviour)
+
+
+func post_install() -> void:
+ dialogic.Inputs.dialogic_action.connect(_on_dialogic_action)
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## Hides all choice buttons.
+func hide_all_choices() -> void:
+ for node in get_tree().get_nodes_in_group('dialogic_choice_button'):
+ node.hide()
+ if node.is_connected('button_up', _on_choice_selected):
+ node.disconnect('button_up', _on_choice_selected)
+
+
+## Collects information on all the choices of the current question.
+## The result is a dictionary like this:
+## {'choices':
+## [
+## {'event_index':10, 'button_index':1, 'disabled':false, 'text':"My Choice", 'visible':true},
+## {'event_index':15, 'button_index':2, 'disabled':false, 'text':"My Choice2", 'visible':true},
+## ]
+func get_current_question_info() -> Dictionary:
+ var question_info := {'choices':[]}
+
+ var button_idx := 1
+ last_question_info = {'choices':[]}
+
+ for choice_index in get_current_choice_indexes():
+ var event: DialogicEvent = dialogic.current_timeline_events[choice_index]
+
+ if not event is DialogicChoiceEvent:
+ continue
+
+ var choice_event: DialogicChoiceEvent = event
+ var choice_info := {}
+ choice_info['event_index'] = choice_index
+ choice_info['button_index'] = button_idx
+
+ # Check Condition
+ var condition: String = choice_event.condition
+
+ if condition.is_empty() or dialogic.Expressions.execute_condition(choice_event.condition):
+ choice_info['disabled'] = false
+ choice_info['text'] = choice_event.get_property_translated('text')
+ choice_info['visible'] = true
+ button_idx += 1
+ else:
+ choice_info['disabled'] = true
+ if not choice_event.disabled_text.is_empty():
+ choice_info['text'] = choice_event.get_property_translated('disabled_text')
+ else:
+ choice_info['text'] = choice_event.get_property_translated('text')
+
+ var hide := choice_event.else_action == DialogicChoiceEvent.ElseActions.HIDE
+ hide = hide or choice_event.else_action == DialogicChoiceEvent.ElseActions.DEFAULT and default_false_behaviour == DialogicChoiceEvent.ElseActions.HIDE
+ choice_info['visible'] = not hide
+
+ if not hide:
+ button_idx += 1
+
+ choice_info.text = dialogic.Text.parse_text(choice_info.text, true, true, false, true, false, false)
+
+ choice_info.merge(choice_event.extra_data)
+
+ if dialogic.has_subsystem('History'):
+ choice_info['visited_before'] = dialogic.History.has_event_been_visited(choice_index)
+
+ question_info['choices'].append(choice_info)
+
+ return question_info
+
+
+## Lists all current choices and shows buttons.
+func show_current_question(instant:=true) -> void:
+ hide_all_choices()
+ _choice_blocker.stop()
+
+ if !instant and (reveal_delay != 0 or reveal_by_input):
+ if reveal_delay != 0:
+ _choice_blocker.start(reveal_delay)
+ _choice_blocker.timeout.connect(show_current_question)
+ if reveal_by_input:
+ dialogic.Inputs.dialogic_action.connect(show_current_question)
+ return
+
+ if _choice_blocker.timeout.is_connected(show_current_question):
+ _choice_blocker.timeout.disconnect(show_current_question)
+ if dialogic.Inputs.dialogic_action.is_connected(show_current_question):
+ dialogic.Inputs.dialogic_action.disconnect(show_current_question)
+
+ var missing_button := false
+
+ var question_info := get_current_question_info()
+
+ for choice in question_info.choices:
+ var node: DialogicNode_ChoiceButton = get_choice_button_node(choice.button_index)
+
+ if not node:
+ missing_button = true
+ continue
+
+ node._load_info(choice)
+
+ if choice.button_index == 1 and autofocus_first_choice:
+ node.grab_focus()
+
+ match hotkey_behaviour:
+ ## Add 1 to 9 as shortcuts if it's enabled
+ HotkeyBehaviour.NUMBERS:
+ if choice.button_index > 0 or choice.button_index < 10:
+ var shortcut: Shortcut
+ if node.shortcut != null:
+ shortcut = node.shortcut
+ else:
+ shortcut = Shortcut.new()
+
+ var input_key := InputEventKey.new()
+ input_key.keycode = OS.find_keycode_from_string(str(choice.button_index))
+ shortcut.events.append(input_key)
+ node.shortcut = shortcut
+
+ if node.pressed.is_connected(_on_choice_selected):
+ node.pressed.disconnect(_on_choice_selected)
+ node.pressed.connect(_on_choice_selected.bind(choice))
+
+ _choice_blocker.start(block_delay)
+ question_shown.emit(question_info)
+
+ if missing_button:
+ printerr("[Dialogic] The layout you are using doesn't have enough Choice Buttons for the choices you are trying to display.")
+
+
+
+func get_choice_button_node(button_index:int) -> DialogicNode_ChoiceButton:
+ var idx := 1
+ for node: DialogicNode_ChoiceButton in get_tree().get_nodes_in_group('dialogic_choice_button'):
+ if !node.get_parent().is_visible_in_tree():
+ continue
+ if node.choice_index == button_index or (node.choice_index == -1 and idx == button_index):
+ return node
+
+ if node.choice_index > 0:
+ idx = node.choice_index
+ idx += 1
+
+ return null
+
+
+func _on_choice_selected(choice_info := {}) -> void:
+ if dialogic.paused or not _choice_blocker.is_stopped():
+ return
+
+ if dialogic.has_subsystem('History'):
+ var all_choices: Array = dialogic.Choices.last_question_info['choices']
+ if dialogic.has_subsystem('VAR'):
+ dialogic.History.store_simple_history_entry(dialogic.VAR.parse_variables(choice_info.text), "Choice", {'all_choices': all_choices})
+ else:
+ dialogic.History.store_simple_history_entry(choice_info.text, "Choice", {'all_choices': all_choices})
+ if dialogic.has_subsystem("History"):
+ dialogic.History.mark_event_as_visited(choice_info.event_index)
+
+ choice_selected.emit(choice_info)
+ hide_all_choices()
+ dialogic.current_state = dialogic.States.IDLE
+ dialogic.handle_event(choice_info.event_index + 1)
+
+
+
+func get_current_choice_indexes() -> Array:
+ var choices := []
+ var evt_idx := dialogic.current_event_idx
+ var ignore := 0
+ while true:
+ if evt_idx >= len(dialogic.current_timeline_events):
+ break
+ if dialogic.current_timeline_events[evt_idx] is DialogicChoiceEvent:
+ if ignore == 0:
+ choices.append(evt_idx)
+ ignore += 1
+ elif dialogic.current_timeline_events[evt_idx].can_contain_events:
+ ignore += 1
+ else:
+ if ignore == 0:
+ break
+
+ if dialogic.current_timeline_events[evt_idx] is DialogicEndBranchEvent:
+ ignore -= 1
+ evt_idx += 1
+ return choices
+
+
+func _on_dialogic_action() -> void:
+ if get_viewport().gui_get_focus_owner() is DialogicNode_ChoiceButton and use_input_action and not dialogic.Inputs.input_was_mouse_input:
+ get_viewport().gui_get_focus_owner().pressed.emit()
+
+
+#endregion
+
+
+#region HELPERS
+####################################################################################################
+
+func is_question(index:int) -> bool:
+ if dialogic.current_timeline_events[index] is DialogicTextEvent:
+ if len(dialogic.current_timeline_events)-1 != index:
+ if dialogic.current_timeline_events[index+1] is DialogicChoiceEvent:
+ return true
+
+ if dialogic.current_timeline_events[index] is DialogicChoiceEvent:
+ if index != 0 and dialogic.current_timeline_events[index-1] is DialogicEndBranchEvent:
+ if dialogic.current_timeline_events[dialogic.current_timeline_events[index-1].find_opening_index(index-1)] is DialogicChoiceEvent:
+ return false
+ else:
+ return true
+ else:
+ return true
+ return false
+
+#endregion
--- /dev/null
+@tool
+extends HBoxContainer
+
+var parent_resource: DialogicChoiceEvent = null
+
+func refresh() -> void:
+ $AddChoice.icon = get_theme_icon("Add", "EditorIcons")
+
+ if parent_resource is DialogicChoiceEvent:
+ show()
+ if len(parent_resource.text) > 12:
+ $Label.text = "End of choice ("+parent_resource.text.substr(0,12)+"...)"
+ else:
+ $Label.text = "End of choice ("+parent_resource.text+")"
+ else:
+ hide()
+
+
+func _on_add_choice_pressed() -> void:
+ var timeline := find_parent('VisualEditor')
+ if timeline:
+ var resource := DialogicChoiceEvent.new()
+ resource.created_by_button = true
+ timeline.add_event_undoable(resource, get_parent().get_index()+1)
+ timeline.indent_events()
+ timeline.something_changed()
+ # Prevent focusing on future redos
+ resource.created_by_button = false
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://cn0wbb2lk0s22"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Choice/ui_choice_end.gd" id="1_7qd85"]
+
+[node name="Choice_End" type="HBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_bottom = -625.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_7qd85")
+
+[node name="AddChoice" type="Button" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="."]
+layout_mode = 2
+
+[connection signal="pressed" from="AddChoice" to="." method="_on_add_choice_pressed"]
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" version="1.1" id="svg4" sodipodi:docname="clear_background.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs8" /><sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="5.6568543" inkscape:cx="19.533825" inkscape:cy="-6.1871843" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4"><inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" /></sodipodi:namedview><g inkscape:groupmode="layer" id="layer2" inkscape:label="Mirror" style="display:inline"><path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="M 4.3862586,18.660184 H 13.902208" id="path11459" /><path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 5.0553488,16.987459 -1.3381805,1.635554 1.3010089,1.970099" id="path11461" /><path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 13.567664,16.875944 0.854949,1.821412 -0.817777,1.9701" id="path11463" /></g><rect style="fill:none;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785" id="rect3829" width="15" height="12" x="3" y="4" ry="0.052568696" /><rect style="fill:none;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785" id="rect3883" width="15" height="2" x="3" y="16" ry="0.052568696" /><path style="fill:none;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785" d="m 7,12.5 h 7 l -2,-3 -2,2 z" id="path3986" /><path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="path2789" sodipodi:type="arc" sodipodi:cx="13.037281" sodipodi:cy="14.981825" sodipodi:rx="4.7729707" sodipodi:ry="4.7729707" sodipodi:start="3.5600983" sodipodi:end="2.1454842" sodipodi:arc-type="arc" d="m 8.6762308,13.042111 a 4.7729707,4.7729707 0 0 1 4.6703702,-2.823223 4.7729707,4.7729707 0 0 1 4.265983,3.403578 4.7729707,4.7729707 0 0 1 -1.715517,5.180729 4.7729707,4.7729707 0 0 1 -5.454243,0.18488" sodipodi:open="true" /><path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 8.3526989,8.9714173 V 13.214058 L 12.28598,12.993087" id="path2845" sodipodi:nodetypes="ccc" /></svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" version="1.1" id="svg4" sodipodi:docname="clear_characters.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs8" />
+ <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="22.627417" inkscape:cx="14.915534" inkscape:cy="9.8994949" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4">
+ <inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" />
+ </sodipodi:namedview>
+ <g inkscape:groupmode="layer" id="layer2" inkscape:label="Mirror" style="display:inline">
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="M 4.3862586,18.660184 H 13.902208" id="path11459" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 5.0553488,16.987459 -1.3381805,1.635554 1.3010089,1.970099" id="path11461" />
+ <path style="display:none;fill:none;fill-opacity:0.980952;stroke:#ffffff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="m 13.567664,16.875944 0.854949,1.821412 -0.817777,1.9701" id="path11463" />
+ </g>
+ <path d="m 13.691936,6.8584286 c 0,2.118883 -1.717741,3.8365674 -3.8366152,3.8365674 -2.118884,0 -3.836581,-1.7176844 -3.836581,-3.8365674 0,-2.118884 1.717697,-3.8365777 3.836581,-3.8365777 2.1188742,0 3.8366152,1.7176937 3.8366152,3.8365777 z" fill="#ffffff" id="path19723" style="display:inline;stroke-width:1.05506" />
+ <path d="m 14.459174,16.382192 c 0,1.985936 -2.061163,1.985936 -4.6038532,1.985936 -2.542658,0 -4.603893,0 -4.603893,-1.985936 0,-3.564724 2.061235,-6.4545064 4.603893,-6.4545064 2.5426902,0 4.6038532,2.8897824 4.6038532,6.4545064 z" fill="#ffffff" id="path19725" style="display:inline;stroke-width:1.05506" />
+ <path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="path2789" sodipodi:type="arc" sodipodi:cx="13.037281" sodipodi:cy="14.981825" sodipodi:rx="4.7729707" sodipodi:ry="4.7729707" sodipodi:start="3.5600983" sodipodi:end="2.1454842" sodipodi:arc-type="arc" d="m 8.6762308,13.042111 a 4.7729707,4.7729707 0 0 1 4.6703702,-2.823223 4.7729707,4.7729707 0 0 1 4.265983,3.403578 4.7729707,4.7729707 0 0 1 -1.715517,5.180729 4.7729707,4.7729707 0 0 1 -5.454243,0.18488" sodipodi:open="true" />
+ <path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 8.3526989,8.9714173 V 13.214058 L 12.28598,12.993087" id="path2845" sodipodi:nodetypes="ccc" />
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" version="1.1" id="svg4" sodipodi:docname="clear_music.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs8" /><sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="16" inkscape:cx="5.40625" inkscape:cy="9.5" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4"><inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" /></sodipodi:namedview><circle style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785" id="path6256" cx="5.28125" cy="15.09375" r="1.5" /><circle style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785" id="circle6466" cx="13.28125" cy="14.09375" r="1.5" /><path style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785;stroke-dasharray:none" d="M 7,15 V 7 l 8,-3 v 10" id="path6571" sodipodi:nodetypes="cccc" /><path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="path2789" sodipodi:type="arc" sodipodi:cx="13.037281" sodipodi:cy="14.981825" sodipodi:rx="4.7729707" sodipodi:ry="4.7729707" sodipodi:start="3.5600983" sodipodi:end="2.1454842" sodipodi:arc-type="arc" d="m 8.6762308,13.042111 a 4.7729707,4.7729707 0 0 1 4.6703702,-2.823223 4.7729707,4.7729707 0 0 1 4.265983,3.403578 4.7729707,4.7729707 0 0 1 -1.715517,5.180729 4.7729707,4.7729707 0 0 1 -5.454243,0.18488" sodipodi:open="true" /><path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 8.3526989,8.9714173 V 13.214058 L 12.28598,12.993087" id="path2845" sodipodi:nodetypes="ccc" /></svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" version="1.1" id="svg4" sodipodi:docname="clear_positions.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs8" /><sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="11.313709" inkscape:cx="2.6958446" inkscape:cy="10.429825" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4"><inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" /></sodipodi:namedview><path d="m 13.240377,8.240346 c 0,1.789594 -1.450792,3.240341 -3.240377,3.240341 -1.789595,0 -3.24035,-1.450747 -3.24035,-3.240341 C 6.75965,6.450752 8.210405,5 10,5 c 1.789585,0 3.240377,1.450752 3.240377,3.240346 z" fill="#ffffff" id="path13814" style="stroke-width:0.891095" /><path d="m 13.888379,16.284052 c 0,1.677308 -1.740841,1.677308 -3.888379,1.677308 -2.147512,0 -3.888416,0 -3.888416,-1.677308 0,-3.010741 1.740904,-5.451432 3.888416,-5.451432 2.147538,0 3.888379,2.440691 3.888379,5.451432 z" fill="#ffffff" id="path13816" style="stroke-width:0.891095" /><rect style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.32552;stroke-linecap:round;stroke-miterlimit:3.2;stroke-dasharray:1.32552, 2.65105;stroke-dashoffset:0" id="rect14304" width="11.651864" height="16.231003" x="4.1740818" y="3.1609161" ry="0.062339745" /><path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="path2789" sodipodi:type="arc" sodipodi:cx="13.037281" sodipodi:cy="14.981825" sodipodi:rx="4.7729707" sodipodi:ry="4.7729707" sodipodi:start="3.5600983" sodipodi:end="2.1454842" sodipodi:arc-type="arc" d="m 8.6762308,13.042111 a 4.7729707,4.7729707 0 0 1 4.6703702,-2.823223 4.7729707,4.7729707 0 0 1 4.265983,3.403578 4.7729707,4.7729707 0 0 1 -1.715517,5.180729 4.7729707,4.7729707 0 0 1 -5.454243,0.18488" sodipodi:open="true" /><path style="fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 8.3526989,8.9714173 V 13.214058 L 12.28598,12.993087" id="path2845" sodipodi:nodetypes="ccc" /></svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" version="1.1" id="svg4" sodipodi:docname="clear_style.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs8" /><sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="22.627417" inkscape:cx="-4.574097" inkscape:cy="10.960155" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4"><inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" /></sodipodi:namedview><path id="rect9566" style="fill:#ffffff;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785;fill-rule:evenodd;fill-opacity:1" d="m 3.460939,4.1191406 c -0.02912,0 -0.05273,0.023611 -0.05273,0.052734 V 15.066407 c 0,0.02912 0.02361,0.05273 0.05273,0.05273 h 14.894532 c 0.02912,0 0.05273,-0.02361 0.05273,-0.05273 V 4.171875 c 0,-0.029123 -0.02361,-0.052734 -0.05273,-0.052734 z m 7.6875,1.6035156 c 0.784,0 1.364235,0.1716254 1.740235,0.515625 0.375999,0.3439997 0.564453,0.892532 0.564453,1.6445313 v 4.3808595 h -0.769531 l -0.203125,-0.912109 h -0.04883 c -0.28,0.352 -0.57472,0.611297 -0.886719,0.779297 -0.312,0.168 -0.737438,0.251953 -1.273438,0.251953 -0.583999,0 -1.067172,-0.151079 -1.451172,-0.455078 C 8.436313,11.615735 8.24414,11.130609 8.24414,10.47461 8.24414,9.83461 8.496,9.3439996 9,9 9.503999,8.6480003 10.280126,8.4558281 11.328125,8.4238281 l 1.091799,-0.037109 V 8.0039062 c 0,-0.5359994 -0.115657,-0.9091877 -0.347657,-1.1171875 -0.231999,-0.2079998 -0.560375,-0.3125 -0.984374,-0.3125 -0.336,0 -0.654985,0.05225 -0.958985,0.15625 -0.304,0.096 -0.589516,0.2079376 -0.853516,0.3359375 L 8.953127,6.2753906 c 0.28,-0.1519998 0.610141,-0.2807657 0.99414,-0.3847656 0.384,-0.1119999 0.785173,-0.1679688 1.201172,-0.1679688 z m 1.259766,3.3964844 -0.947266,0.035156 c -0.799999,0.032 -1.355969,0.1607659 -1.667968,0.3847657 -0.304,0.2239998 -0.457032,0.5392665 -0.457032,0.9472665 0,0.359999 0.108219,0.624968 0.324219,0.792968 0.224,0.168 0.507563,0.251953 0.851563,0.251953 0.535999,0 0.98375,-0.147359 1.34375,-0.443359 0.367999,-0.304 0.552734,-0.768579 0.552734,-1.3925785 z" /><path style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3.2;stroke-dasharray:none;stroke-dashoffset:0.236785" d="M 19.297391,15.49935 10,20 13,15 Z" id="path9626" sodipodi:nodetypes="cccc" /><rect style="display:inline;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.2;stroke-linecap:round;stroke-miterlimit:3.2;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="rect999" width="16" height="11" x="3" y="4" ry="0.052568696" /><path style="display:inline;fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="path2789" sodipodi:type="arc" sodipodi:cx="13.037281" sodipodi:cy="14.981825" sodipodi:rx="4.7729707" sodipodi:ry="4.7729707" sodipodi:start="3.5600983" sodipodi:end="2.1454842" sodipodi:arc-type="arc" d="m 8.6762308,13.042111 a 4.7729707,4.7729707 0 0 1 4.6703702,-2.823223 4.7729707,4.7729707 0 0 1 4.265983,3.403578 4.7729707,4.7729707 0 0 1 -1.715517,5.180729 4.7729707,4.7729707 0 0 1 -5.454243,0.18488" sodipodi:open="true" /><path style="display:inline;fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 8.3526989,8.9714173 V 13.214058 L 12.28598,12.993087" id="path2845" sodipodi:nodetypes="ccc" /></svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" version="1.1" id="svg4" sodipodi:docname="clear_style.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs8" /><sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="true" inkscape:zoom="22.627417" inkscape:cx="-4.574097" inkscape:cy="10.960155" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg4"><inkscape:grid type="xygrid" id="grid6918" originx="0" originy="0" /></sodipodi:namedview><path id="rect9566" style="fill:#ffffff;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.236785;fill-rule:evenodd;fill-opacity:1" d="m 3.460939,4.1191406 c -0.02912,0 -0.05273,0.023611 -0.05273,0.052734 V 15.066407 c 0,0.02912 0.02361,0.05273 0.05273,0.05273 h 14.894532 c 0.02912,0 0.05273,-0.02361 0.05273,-0.05273 V 4.171875 c 0,-0.029123 -0.02361,-0.052734 -0.05273,-0.052734 z" /><path style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3.2;stroke-dasharray:none;stroke-dashoffset:0.236785" d="M 19.297391,15.49935 10,20 13,15 Z" id="path9626" sodipodi:nodetypes="cccc" /><rect style="display:inline;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.2;stroke-linecap:round;stroke-miterlimit:3.2;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="rect999" width="16" height="11" x="3" y="4" ry="0.052568696" /><path style="display:inline;fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" id="path2789" sodipodi:type="arc" sodipodi:cx="13.037281" sodipodi:cy="14.981825" sodipodi:rx="4.7729707" sodipodi:ry="4.7729707" sodipodi:start="3.5600983" sodipodi:end="2.1454842" sodipodi:arc-type="arc" d="m 8.6762308,13.042111 a 4.7729707,4.7729707 0 0 1 4.6703702,-2.823223 4.7729707,4.7729707 0 0 1 4.265983,3.403578 4.7729707,4.7729707 0 0 1 -1.715517,5.180729 4.7729707,4.7729707 0 0 1 -5.454243,0.18488" sodipodi:open="true" /><path style="display:inline;fill:none;stroke:#ff4596;stroke-width:2.12032;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.236785;stroke-opacity:1" d="M 8.3526989,8.9714173 V 13.214058 L 12.28598,12.993087" id="path2845" sodipodi:nodetypes="ccc" /></svg>
--- /dev/null
+@tool
+class_name DialogicClearEvent
+extends DialogicEvent
+
+## Event that clears audio & visuals (not variables).
+## Useful to make sure the scene is clear for a completely new thing.
+
+var time := 1.0
+var step_by_step := true
+
+var clear_textbox := true
+var clear_portraits := true
+var clear_style := true
+var clear_music := true
+var clear_portrait_positions := true
+var clear_background := true
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ var final_time := time
+
+ if dialogic.Inputs.auto_skip.enabled:
+ var time_per_event: float = dialogic.Inputs.auto_skip.time_per_event
+ final_time = min(time, time_per_event)
+
+ if clear_textbox and dialogic.has_subsystem("Text"):
+ dialogic.Text.update_dialog_text('')
+ dialogic.Text.hide_textbox()
+ dialogic.current_state = dialogic.States.IDLE
+ if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
+
+ if clear_portraits and dialogic.has_subsystem('Portraits') and len(dialogic.Portraits.get_joined_characters()) != 0:
+ if final_time == 0:
+ dialogic.Portraits.leave_all_characters("Instant", final_time, step_by_step)
+ else:
+ dialogic.Portraits.leave_all_characters("", final_time, step_by_step)
+ if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
+
+ if clear_background and dialogic.has_subsystem('Backgrounds') and dialogic.Backgrounds.has_background():
+ dialogic.Backgrounds.update_background('', '', final_time)
+ if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
+
+ if clear_music and dialogic.has_subsystem('Audio'):
+ for channel_id in dialogic.Audio.max_channels:
+ if dialogic.Audio.has_music(channel_id):
+ dialogic.Audio.update_music('', 0.0, "", final_time, channel_id)
+ if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
+
+ if clear_style and dialogic.has_subsystem('Styles'):
+ dialogic.Styles.change_style()
+
+ if clear_portrait_positions and dialogic.has_subsystem('Portraits'):
+ dialogic.PortraitContainers.reset_all_containers()
+
+ if not step_by_step:
+ await dialogic.get_tree().create_timer(final_time).timeout
+
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Clear"
+ set_default_color('Color9')
+ event_category = "Other"
+ event_sorting_index = 2
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "clear"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "time" : {"property": "time", "default": ""},
+ "step" : {"property": "step_by_step", "default": true},
+ "text" : {"property": "clear_textbox", "default": true},
+ "portraits" : {"property": "clear_portraits", "default": true},
+ "music" : {"property": "clear_music", "default": true},
+ "background": {"property": "clear_background", "default": true},
+ "positions" : {"property": "clear_portrait_positions", "default": true},
+ "style" : {"property": "clear_style", "default": true},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_label('Clear')
+
+ add_body_edit('time', ValueType.NUMBER, {'left_text':'Time:'})
+
+ add_body_edit('step_by_step', ValueType.BOOL, {'left_text':'Step by Step:'}, 'time > 0')
+ add_body_line_break()
+
+ add_body_edit('clear_textbox', ValueType.BOOL_BUTTON, {'left_text':'Clear:', 'icon':load("res://addons/dialogic/Modules/Clear/clear_textbox.svg"), 'tooltip':'Clear Textbox'})
+ add_body_edit('clear_portraits', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_characters.svg"), 'tooltip':'Clear Portraits'})
+ add_body_edit('clear_background', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_background.svg"), 'tooltip':'Clear Background'})
+ add_body_edit('clear_music', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_music.svg"), 'tooltip':'Clear Music'})
+ add_body_edit('clear_style', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_style.svg"), 'tooltip':'Clear Style'})
+ add_body_edit('clear_portrait_positions', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_positions.svg"), 'tooltip':'Clear Portrait Positions'})
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_clear.gd')]
--- /dev/null
+@tool
+class_name DialogicCommentEvent
+extends DialogicEvent
+
+## Event that does nothing but store a comment string. Will print the comment in debug builds.
+
+
+### Settings
+
+## Content of the comment.
+var text := ""
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ print("[Dialogic Comment] #", text)
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Comment"
+ set_default_color('Color9')
+ event_category = "Helpers"
+ event_sorting_index = 0
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ return "# "+text
+
+
+func from_text(string:String) -> void:
+ text = string.trim_prefix("# ")
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges().begins_with('#'):
+ return true
+ return false
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('text', ValueType.SINGLELINE_TEXT, {'left_text':'#', 'autofocus':true})
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, _line:String) -> Dictionary:
+ dict[0] = {'color':event_color.lerp(Highlighter.normal_color, 0.3)}
+ return dict
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_comment.gd')]
--- /dev/null
+@tool
+class_name DialogicConditionEvent
+extends DialogicEvent
+
+## Event that allows branching a timeline based on a condition.
+
+enum ConditionTypes {IF, ELIF, ELSE}
+
+### Settings
+## condition type (see [ConditionTypes]). Defaults to if.
+var condition_type := ConditionTypes.IF
+## The condition as a string. Will be executed as an Expression.
+var condition := ""
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ if condition_type == ConditionTypes.ELSE:
+ finish()
+ return
+
+ if condition.is_empty(): condition = "true"
+
+ var result: bool = dialogic.Expressions.execute_condition(condition)
+ if not result:
+ var idx: int = dialogic.current_event_idx
+ var ignore := 1
+ while true:
+ idx += 1
+ if not dialogic.current_timeline.get_event(idx) or ignore == 0:
+ break
+ elif dialogic.current_timeline.get_event(idx).can_contain_events:
+ ignore += 1
+ elif dialogic.current_timeline.get_event(idx) is DialogicEndBranchEvent:
+ ignore -= 1
+
+ dialogic.current_event_idx = idx-1
+ finish()
+
+
+## only called if the previous event was an end-branch event
+## return true if this event should be executed if the previous event was an end-branch event
+func should_execute_this_branch() -> bool:
+ return condition_type == ConditionTypes.IF
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Condition"
+ set_default_color('Color3')
+ event_category = "Flow"
+ event_sorting_index = 1
+ can_contain_events = true
+
+
+# return a control node that should show on the END BRANCH node
+func get_end_branch_control() -> Control:
+ return load(get_script().resource_path.get_base_dir().path_join('ui_condition_end.tscn')).instantiate()
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ var result_string := ""
+
+ match condition_type:
+ ConditionTypes.IF:
+ result_string = 'if '+condition+':'
+ ConditionTypes.ELIF:
+ result_string = 'elif '+condition+':'
+ ConditionTypes.ELSE:
+ result_string = 'else:'
+
+ return result_string
+
+
+func from_text(string:String) -> void:
+ if string.strip_edges().begins_with('if'):
+ condition = string.strip_edges().trim_prefix('if ').trim_suffix(':').strip_edges()
+ condition_type = ConditionTypes.IF
+ elif string.strip_edges().begins_with('elif'):
+ condition = string.strip_edges().trim_prefix('elif ').trim_suffix(':').strip_edges()
+ condition_type = ConditionTypes.ELIF
+ elif string.strip_edges().begins_with('else'):
+ condition = ""
+ condition_type = ConditionTypes.ELSE
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges() in ['if', 'elif', 'else'] or (string.strip_edges().begins_with('if ') or string.strip_edges().begins_with('elif ') or string.strip_edges().begins_with('else')):
+ return true
+ return false
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('condition_type', ValueType.FIXED_OPTIONS, {
+ 'options': [
+ {
+ 'label': 'IF',
+ 'value': ConditionTypes.IF,
+ },
+ {
+ 'label': 'ELIF',
+ 'value': ConditionTypes.ELIF,
+ },
+ {
+ 'label': 'ELSE',
+ 'value': ConditionTypes.ELSE,
+ }
+ ], 'disabled':true})
+ add_header_edit('condition', ValueType.CONDITION, {}, 'condition_type != %s'%ConditionTypes.ELSE)
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ if (line.begins_with('if') or line.begins_with('elif')) and symbol == '{':
+ CodeCompletionHelper.suggest_variables(TextNode)
+
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'if', 'if ', TextNode.syntax_highlighter.code_flow_color)
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'elif', 'elif ', TextNode.syntax_highlighter.code_flow_color)
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'else', 'else:\n ', TextNode.syntax_highlighter.code_flow_color)
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ var word := line.get_slice(' ', 0)
+ dict[line.find(word)] = {"color":Highlighter.code_flow_color}
+ dict[line.find(word)+len(word)] = {"color":Highlighter.normal_color}
+ dict = Highlighter.color_condition(dict, line)
+ return dict
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="condition-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="1.6347657" inkscape:cx="-20.492233" inkscape:cy="-96.038226" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
+ <defs id="defs2" />
+ <path id="path22732" style="stroke-width:1.28593;stroke-dasharray:none;fill:#ffffff;color:#000000;fill-opacity:0.45;stroke-linecap:round;stroke-linejoin:round" d="m -71.974852,-11.59036 c -0.483439,0.03047 -0.990446,0.05427 -1.422078,0.09835 -4.072307,0.415892 -7.64202,1.269927 -10.422371,3.8196754 -2.780352,2.5497501 -3.858991,6.1377302 -4.31408,9.9545471 -0.255536,2.1431512 -0.348662,4.5365819 -0.388081,7.2300049 -1.4e-5,9.111e-4 1.3e-5,0.00175 0,0.00266 a 8.4415045,8.4415045 0 0 0 -3.710694,6.9509056 8.4415045,8.4415045 0 0 0 8.442094,8.442094 8.4415045,8.4415045 0 0 0 8.439436,-8.442094 8.4415045,8.4415045 0 0 0 -3.633609,-6.9349571 c 0.04202,-2.4939081 0.12555,-4.566242 0.310996,-6.1215813 0.335629,-2.81487882 0.851715,-3.6528496 1.29449,-4.05890348 0.44278,-0.40605392 1.724439,-1.03690872 4.949364,-1.36625832 1.306091,-0.1333859 2.944876,-0.2069313 4.779246,-0.2578347 -2.274077,-1.8226451 -7.703145,-6.1839349 -7.703145,-6.405997 0,-0.1168195 1.570967,-1.4331011 3.378432,-2.9106081 z m -11.81521,19.6167041 a 8.4415045,8.4415045 0 0 0 -1.305122,0.1142979 8.4415045,8.4415045 0 0 1 1.305122,-0.1142979 z m -2.190266,0.3216289 a 8.4415045,8.4415045 0 0 0 -0.358842,0.095691 8.4415045,8.4415045 0 0 1 0.358842,-0.095691 z m -1.35031,0.4917466 a 8.4415045,8.4415045 0 0 0 -0.164801,0.074427 8.4415045,8.4415045 0 0 1 0.164801,-0.074427 z" transform="matrix(0.1944119,0,0,0.1944119,19.699396,10.154114)" />
+ <circle style="stroke-width:1.31524;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" id="path22484" cx="-57.670414" cy="-33.437908" r="8.4415045" transform="matrix(0.1944119,0,0,0.1944119,19.699396,10.154114)" />
+ <path id="path21748" style="stroke-width:1.28345;stroke-dasharray:none;fill:#ffffff;color:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none" d="m -57.93482,-33.437202 a 4.7632976,4.7632976 0 0 0 -4.712794,4.811143 l 0.09037,9.186359 c 2.239956,-1.935188 4.604724,-3.944606 4.779246,-3.944606 0.174522,0 2.53929,2.009418 4.779246,3.944606 l 0.09038,-9.186359 a 4.7632976,4.7632976 0 0 0 -4.715452,-4.811143 4.7632976,4.7632976 0 0 0 -0.154169,0.02392 4.7632976,4.7632976 0 0 0 -0.156827,-0.02392 z m 14.351028,21.844184 c 1.808699,1.478479 3.38109,2.796384 3.38109,2.9132662 0,0.2219874 -5.424909,4.5821407 -7.700486,6.405997 1.797768,0.050169 3.408936,0.1219386 4.696845,0.2525186 3.217204,0.32619 4.51554,0.9531571 4.973286,1.36891633 0.457749,0.41575927 0.975761,1.25405428 1.323729,4.06421967 0.347967,2.8101653 0.382765,7.1584092 0.382765,13.0538802 a 4.7632976,4.7632976 0 0 0 4.760639,4.765955 4.7632976,4.7632976 0 0 0 4.763297,-4.765955 c 0,-5.900707 0.02453,-10.3975383 -0.449217,-14.2234398 -0.473738,-3.8259015 -1.587699,-7.4123327 -4.377874,-9.9465729 -2.790176,-2.5342393 -6.355607,-3.3839673 -10.417055,-3.7957523 -0.406605,-0.04123 -0.884609,-0.06397 -1.337019,-0.09303 z" transform="matrix(0.1944119,0,0,0.1944119,19.699396,10.154114)" />
+ <circle style="stroke-width:1.31524;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" id="circle22488" cx="-31.835474" cy="16.466833" r="8.4415045" transform="matrix(0.1944119,0,0,0.1944119,19.699396,10.154114)" />
+ <path sodipodi:type="star" style="stroke-width:9.5266;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" id="path22546" inkscape:flatsided="false" sodipodi:sides="4" sodipodi:cx="6.6831412" sodipodi:cy="16.177292" sodipodi:r1="6.2749648" sodipodi:r2="8.9210739" sodipodi:arg1="0.78539816" sodipodi:arg2="1.5707963" inkscape:rounded="0.02640344" inkscape:randomized="0" d="m 11.120211,20.614362 c -0.117775,0.117775 -4.2705103,4.484004 -4.4370695,4.484004 -0.1665592,0 -4.3192953,-4.366229 -4.4370704,-4.484004 -0.1177752,-0.117775 -4.4840038,-4.270511 -4.4840038,-4.43707 0,-0.166559 4.3662286,-4.319295 4.4840037,-4.43707 0.1177752,-0.117775 4.2705108,-4.484004 4.43707,-4.484004 0.1665592,0 4.319295,4.366229 4.43707,4.484004 0.117776,0.117775 4.484004,4.27051 4.484004,4.43707 0,0.166559 -4.366228,4.319295 -4.484004,4.43707 z" transform="matrix(0.26708005,0,0,0.22344459,6.6817327,4.8519382)" />
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_condition.gd')]
--- /dev/null
+@tool
+extends HBoxContainer
+
+var parent_resource: DialogicEvent = null
+
+
+func _ready() -> void:
+ $AddElif.button_up.connect(add_elif)
+ $AddElse.button_up.connect(add_else)
+
+
+func refresh() -> void:
+ if parent_resource is DialogicConditionEvent:
+ # hide add elif and add else button on ELSE event
+ $AddElif.visible = parent_resource.condition_type != DialogicConditionEvent.ConditionTypes.ELSE
+ $AddElse.visible = parent_resource.condition_type != DialogicConditionEvent.ConditionTypes.ELSE
+ $Label.text = "End of "+["IF", "ELIF", "ELSE"][parent_resource.condition_type]+" ("+parent_resource.condition+")"
+
+ # hide add add else button if followed by ELIF or ELSE event
+ var timeline_editor := find_parent('VisualEditor')
+ if timeline_editor:
+ var next_event: DialogicEvent = null
+ if timeline_editor.get_block_below(get_parent()):
+ next_event = timeline_editor.get_block_below(get_parent()).resource
+ if next_event is DialogicConditionEvent:
+ if next_event.condition_type != DialogicConditionEvent.ConditionTypes.IF:
+ $AddElse.hide()
+ if parent_resource.condition_type == DialogicConditionEvent.ConditionTypes.ELSE:
+ $Label.text = "End of ELSE"
+ else:
+ hide()
+
+
+func add_elif() -> void:
+ var timeline := find_parent('VisualEditor')
+ if timeline:
+ var resource := DialogicConditionEvent.new()
+ resource.condition_type = DialogicConditionEvent.ConditionTypes.ELIF
+ timeline.add_event_undoable(resource, get_parent().get_index()+1)
+ timeline.indent_events()
+ timeline.something_changed()
+
+
+func add_else() -> void:
+ var timeline := find_parent('VisualEditor')
+ if timeline:
+ var resource := DialogicConditionEvent.new()
+ resource.condition_type = DialogicConditionEvent.ConditionTypes.ELSE
+ timeline.add_event_undoable(resource, get_parent().get_index()+1)
+ timeline.indent_events()
+ timeline.something_changed()
--- /dev/null
+[gd_scene load_steps=2 format=3]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Condition/ui_condition_end.gd" id="1_sh52m"]
+
+[node name="Condition_End" type="HBoxContainer"]
+offset_right = 90.0
+offset_bottom = 23.0
+script = ExtResource("1_sh52m")
+
+[node name="Label" type="Label" parent="."]
+offset_top = 2.0
+offset_right = 141.0
+offset_bottom = 28.0
+text = "End of condition X"
+
+[node name="AddElif" type="Button" parent="."]
+offset_left = 145.0
+offset_right = 212.0
+offset_bottom = 31.0
+text = "Add Elif"
+
+[node name="AddElse" type="Button" parent="."]
+offset_left = 216.0
+offset_right = 290.0
+offset_bottom = 31.0
+text = "Add Else"
--- /dev/null
+@tool
+class_name DialogicEndBranchEvent
+extends DialogicEvent
+
+## Event that indicates the end of a condition or choice (or custom branch).
+## In text this is not stored (only as a change in indentation).
+
+
+#region EXECUTE
+################################################################################
+
+func _execute() -> void:
+ dialogic.current_event_idx = find_next_index()-1
+ finish()
+
+
+func find_next_index() -> int:
+ var idx: int = dialogic.current_event_idx
+
+ var ignore: int = 1
+ while true:
+ idx += 1
+ var event: DialogicEvent = dialogic.current_timeline.get_event(idx)
+ if not event:
+ return idx
+ if event is DialogicEndBranchEvent:
+ if ignore > 1:
+ ignore -= 1
+ elif event.can_contain_events and not event.should_execute_this_branch():
+ ignore += 1
+ elif ignore <= 1:
+ return idx
+
+ return idx
+
+
+func find_opening_index(at_index:int) -> int:
+ var idx: int = at_index
+
+ var ignore: int = 1
+ while true:
+ idx -= 1
+ var event: DialogicEvent = dialogic.current_timeline.get_event(idx)
+ if not event:
+ return idx
+ if event is DialogicEndBranchEvent:
+ ignore += 1
+ elif event.can_contain_events:
+ ignore -= 1
+ if ignore == 0:
+ return idx
+
+ return idx
+#endregion
+
+#region INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "End Branch"
+ disable_editor_button = true
+
+#endregion
+
+#region SAVING/LOADING
+################################################################################
+
+## NOTE: This event is very special. It is rarely stored at all, as it is usually
+## just a placeholder for removing an indentation level.
+## When copying events however, some representation of this is necessary. That's why this is half-implemented.
+func to_text() -> String:
+ return "<<END BRANCH>>"
+
+
+func from_text(_string:String) -> void:
+ pass
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges().begins_with("<<END BRANCH>>"):
+ return true
+ return false
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_end_branch.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [
+ {'name':'Expressions', 'script':this_folder.path_join('subsystem_expression.gd')},
+ {'name':'Animations', 'script':this_folder.path_join('subsystem_animation.gd')},
+ {'name':'Inputs', 'script':this_folder.path_join('subsystem_input.gd')},
+ ]
+
+
+func _get_text_effects() -> Array[Dictionary]:
+ return [
+ {'command':'aa', 'subsystem':'Inputs', 'method':'effect_autoadvance'},
+ {'command':'ns', 'subsystem':'Inputs', 'method':'effect_noskip'},
+ {'command':'input', 'subsystem':'Inputs', 'method':'effect_input'},
+ ]
+
+func _get_text_modifiers() -> Array[Dictionary]:
+ return [
+ {'subsystem':'Expressions', 'method':"modifier_condition", 'command':'if', 'mode':-1},
+ ]
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that allows entering and leaving an animation state.
+
+signal finished
+signal animation_interrupted
+
+var prev_state: DialogicGameHandler.States = DialogicGameHandler.States.IDLE
+
+var _is_animating := false
+
+#region MAIN METHODS
+####################################################################################################
+
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ stop_animation()
+
+
+func is_animating() -> bool:
+ return _is_animating
+
+
+func start_animating() -> void:
+ prev_state = dialogic.current_state
+ dialogic.current_state = dialogic.States.ANIMATING
+ _is_animating = true
+
+
+func animation_finished(_arg := "") -> void:
+ # It can happen that the animation state has already been stopped
+ if not is_animating():
+ return
+ _is_animating = false
+ dialogic.current_state = prev_state as DialogicGameHandler.States
+ finished.emit()
+
+
+func stop_animation() -> void:
+ animation_finished()
+ animation_interrupted.emit()
+
+#endregion
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that allows executing strings (with the Expression class).
+## This is used by conditions and to allow expresions as variables.
+
+
+#region MAIN METHODS
+####################################################################################################
+
+func execute_string(string:String, default: Variant = null, no_warning := false) -> Variant:
+ # Some methods are not supported by the expression class, but very useful.
+ # Thus they are recreated below and secretly added.
+ string = string.replace('range(', 'd_range(')
+ string = string.replace('len(', 'd_len(')
+ string = string.replace('regex(', 'd_regex(')
+
+
+ var regex: RegEx = RegEx.create_from_string('{([^{}]*)}')
+
+ for res in regex.search_all(string):
+ var value: Variant = dialogic.VAR.get_variable(res.get_string())
+ string = string.replace(res.get_string(), var_to_str(value))
+
+ if string.begins_with("{") and string.ends_with('}') and string.count("{") == 1:
+ string = string.trim_prefix("{").trim_suffix("}")
+
+ var expr := Expression.new()
+
+ var autoloads := []
+ var autoload_names := []
+ for c in get_tree().root.get_children():
+ autoloads.append(c)
+ autoload_names.append(c.name)
+
+ if expr.parse(string, autoload_names) != OK:
+ if not no_warning:
+ printerr('[Dialogic] Expression "', string, '" failed to parse.')
+ printerr(' ', expr.get_error_text())
+ dialogic.print_debug_moment()
+ return default
+
+ var result: Variant = expr.execute(autoloads, self)
+ if expr.has_execute_failed():
+ if not no_warning:
+ printerr('[Dialogic] Expression "', string, '" failed to parse.')
+ printerr(' ', expr.get_error_text())
+ dialogic.print_debug_moment()
+ return default
+ return result
+
+
+func execute_condition(condition:String) -> bool:
+ if execute_string(condition, false):
+ return true
+ return false
+
+
+var condition_modifier_regex := RegEx.create_from_string(r"(?(DEFINE)(?<nobraces>([^{}]|\{(?P>nobraces)\})*))\[if *(?<condition>\{(?P>nobraces)\})(?<truetext>(\\\]|\\\/|[^\]\/])*)(\/(?<falsetext>(\\\]|[^\]])*))?\]")
+func modifier_condition(text:String) -> String:
+ for find in condition_modifier_regex.search_all(text):
+ if execute_condition(find.get_string("condition")):
+ text = text.replace(find.get_string(), find.get_string("truetext").strip_edges())
+ else:
+ text = text.replace(find.get_string(), find.get_string("falsetext").strip_edges())
+ return text
+#endregion
+
+
+#region HELPERS
+####################################################################################################
+func d_range(a1, a2=null,a3=null,a4=null) -> Array:
+ if !a2:
+ return range(a1)
+ elif !a3:
+ return range(a1, a2)
+ elif !a4:
+ return range(a1, a2, a3)
+ else:
+ return range(a1, a2, a3, a4)
+
+func d_len(arg:Variant) -> int:
+ return len(arg)
+
+
+# Checks if there is a match in a string based on a regex pattern string.
+func d_regex(input: String, pattern: String, offset: int = 0, end: int = -1) -> bool:
+ var regex: RegEx = RegEx.create_from_string(pattern)
+ regex.compile(pattern)
+ var match := regex.search(input, offset, end)
+ if match:
+ return true
+ else:
+ return false
+
+#endregion
--- /dev/null
+extends DialogicSubsystem
+## Subsystem that handles input, Auto-Advance, and skipping.
+##
+## This subsystem can be accessed via GDScript: `Dialogic.Inputs`.
+
+
+signal dialogic_action_priority
+signal dialogic_action
+
+## Whenever the Auto-Skip timer finishes, this signal is emitted.
+## Configure Auto-Skip settings via [member auto_skip].
+signal autoskip_timer_finished
+
+
+const _SETTING_INPUT_ACTION := "dialogic/text/input_action"
+const _SETTING_INPUT_ACTION_DEFAULT := "dialogic_default_action"
+
+var input_block_timer := Timer.new()
+var _auto_skip_timer_left: float = 0.0
+var action_was_consumed := false
+var input_was_mouse_input := false
+
+var auto_skip: DialogicAutoSkip = null
+var auto_advance: DialogicAutoAdvance = null
+var manual_advance: DialogicManualAdvance = null
+
+
+#region SUBSYSTEM METHODS
+################################################################################
+
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ if not is_node_ready():
+ await ready
+
+ manual_advance.disabled_until_next_event = false
+ manual_advance.system_enabled = true
+
+
+func pause() -> void:
+ auto_advance.autoadvance_timer.paused = true
+ input_block_timer.paused = true
+ set_process(false)
+
+
+func resume() -> void:
+ auto_advance.autoadvance_timer.paused = false
+ input_block_timer.paused = false
+ var is_autoskip_timer_done := _auto_skip_timer_left > 0.0
+ set_process(!is_autoskip_timer_done)
+
+
+func post_install() -> void:
+ dialogic.Settings.connect_to_change('autoadvance_delay_modifier', auto_advance._update_autoadvance_delay_modifier)
+ auto_skip.toggled.connect(_on_autoskip_toggled)
+ auto_skip._init()
+ add_child(input_block_timer)
+ input_block_timer.one_shot = true
+
+
+#endregion
+
+
+#region MAIN METHODS
+################################################################################
+
+func handle_input() -> void:
+ if dialogic.paused or is_input_blocked():
+ return
+
+ if not action_was_consumed:
+ # We want to stop auto-advancing that cancels on user inputs.
+ if (auto_advance.is_enabled()
+ and auto_advance.enabled_until_user_input):
+ auto_advance.enabled_until_user_input = false
+ action_was_consumed = true
+
+ # We want to stop auto-skipping if it's enabled, we are listening
+ # to user inputs, and it's not instant skipping.
+ if (auto_skip.disable_on_user_input
+ and auto_skip.enabled):
+ auto_skip.enabled = false
+ action_was_consumed = true
+
+
+ dialogic_action_priority.emit()
+
+ if action_was_consumed:
+ action_was_consumed = false
+ return
+
+ dialogic_action.emit()
+ input_was_mouse_input = false
+
+
+## Unhandled Input is used for all NON-Mouse based inputs.
+func _unhandled_input(event:InputEvent) -> void:
+ if is_input_pressed(event, true):
+ if event is InputEventMouse or event is InputEventScreenTouch:
+ return
+ input_was_mouse_input = false
+ handle_input()
+
+
+## Input is used for all mouse based inputs.
+## If any DialogicInputNode is present this won't do anything (because that node handles MouseInput then).
+func _input(event:InputEvent) -> void:
+ if is_input_pressed(event):
+ if not event is InputEventMouse:
+ return
+ if get_tree().get_nodes_in_group('dialogic_input').any(func(node):return node.is_visible_in_tree()):
+ return
+ input_was_mouse_input = true
+ handle_input()
+
+
+func is_input_pressed(event: InputEvent, exact := false) -> bool:
+ var action: String = ProjectSettings.get_setting(_SETTING_INPUT_ACTION, _SETTING_INPUT_ACTION_DEFAULT)
+ return (event is InputEventAction and event.action == action) or Input.is_action_just_pressed(action, exact)
+
+
+## This is called from the gui_input of the InputCatcher and DialogText nodes
+func handle_node_gui_input(event:InputEvent) -> void:
+ if Input.is_action_just_pressed(ProjectSettings.get_setting(_SETTING_INPUT_ACTION, _SETTING_INPUT_ACTION_DEFAULT)):
+ if event is InputEventMouseButton and event.pressed:
+ input_was_mouse_input = true
+ handle_input()
+
+
+func is_input_blocked() -> bool:
+ return input_block_timer.time_left > 0.0
+
+
+func block_input(time:=0.1) -> void:
+ if time > 0:
+ input_block_timer.wait_time = max(time, input_block_timer.time_left)
+ input_block_timer.start()
+
+
+func _ready() -> void:
+ auto_skip = DialogicAutoSkip.new()
+ auto_advance = DialogicAutoAdvance.new()
+ manual_advance = DialogicManualAdvance.new()
+
+ # We use the process method to count down the auto-start_autoskip_timer timer.
+ set_process(false)
+
+
+func stop_timers() -> void:
+ auto_advance.autoadvance_timer.stop()
+ input_block_timer.stop()
+ _auto_skip_timer_left = 0.0
+
+#endregion
+
+
+#region AUTO-SKIP
+################################################################################
+
+## This method will advance the timeline based on Auto-Skip settings.
+## The state, whether Auto-Skip is enabled, is ignored.
+func start_autoskip_timer() -> void:
+ _auto_skip_timer_left = auto_skip.time_per_event
+ set_process(true)
+ await autoskip_timer_finished
+
+
+## If Auto-Skip disables, we want to stop the timer.
+func _on_autoskip_toggled(enabled: bool) -> void:
+ if not enabled:
+ _auto_skip_timer_left = 0.0
+
+
+## Handles fine-grained Auto-Skip logic.
+## The [method _process] method allows for a more precise timer than the
+## [Timer] class.
+func _process(delta: float) -> void:
+ if _auto_skip_timer_left > 0:
+ _auto_skip_timer_left -= delta
+
+ if _auto_skip_timer_left <= 0:
+ autoskip_timer_finished.emit()
+
+ else:
+ autoskip_timer_finished.emit()
+ set_process(false)
+
+#endregion
+
+#region TEXT EFFECTS
+################################################################################
+
+
+func effect_input(_text_node:Control, skipped:bool, _argument:String) -> void:
+ if skipped:
+ return
+ dialogic.Text.show_next_indicators()
+ await dialogic.Inputs.dialogic_action_priority
+ dialogic.Text.hide_next_indicators()
+ dialogic.Inputs.action_was_consumed = true
+
+
+func effect_noskip(text_node:Control, skipped:bool, argument:String) -> void:
+ dialogic.Text.set_text_reveal_skippable(false, true)
+ manual_advance.disabled_until_next_event = true
+ effect_autoadvance(text_node, skipped, argument)
+
+
+func effect_autoadvance(_text_node: Control, _skipped:bool, argument:String) -> void:
+ if argument.ends_with('?'):
+ argument = argument.trim_suffix('?')
+ else:
+ auto_advance.enabled_until_next_event = true
+
+ if argument.is_valid_float():
+ auto_advance.override_delay_for_current_event = float(argument)
+
+#endregion
--- /dev/null
+@tool
+extends DialogicLayoutBase
+
+## The default layout base scene.
+
+@export var canvas_layer: int = 1
+@export var follow_viewport: bool = false
+
+@export_subgroup("Global")
+@export var global_bg_color: Color = Color(0, 0, 0, 0.9)
+@export var global_font_color: Color = Color("white")
+@export_file('*.ttf', '*.tres') var global_font: String = ""
+@export var global_font_size: int = 18
+
+
+func _apply_export_overrides() -> void:
+ # apply layer
+ set(&'layer', canvas_layer)
+ set(&'follow_viewport_enabled', follow_viewport)
+
+
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://cqpb3ie51rwl5"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Base_Default/default_layout_base.gd" id="1_ifsho"]
+
+[node name="DefaultLayoutBase" type="CanvasLayer"]
+script = ExtResource("1_ifsho")
--- /dev/null
+[style]
+type = "Layout Base"
+name = "Default Layout Base"
+author = "Dialogic"
+description = "A very simple base for layouts."
+scene = "default_layout_base.tscn"
--- /dev/null
+[style]
+type = "Layout Base"
+name = "Textbubble Base"
+author = "Dialogic"
+description = "A base scene for the textbubble style. Expects a textbubble layer."
+scene = "text_bubble_base.tscn"
--- /dev/null
+@tool
+extends DialogicLayoutBase
+
+## This layout won't do anything on its own
+
+var bubbles: Array = []
+var registered_characters: Dictionary = {}
+
+@export_group("Main")
+@export_range(1, 25, 1) var bubble_count: int = 2
+
+
+func _ready() -> void:
+ if Engine.is_editor_hint():
+ return
+
+ DialogicUtil.autoload().Text.about_to_show_text.connect(_on_dialogic_text_event)
+ $Example/CRT.position = $Example.get_viewport_rect().size/2
+
+ if not has_node('TextBubbleLayer'):
+ return
+
+ if len(bubbles) < bubble_count:
+ add_bubble()
+
+
+func register_character(character:Variant, node:Node):
+ if typeof(character) == TYPE_STRING:
+ var character_string: String = character
+ if character.begins_with("res://"):
+ character = load(character)
+ else:
+ character = DialogicResourceUtil.get_character_resource(character)
+ if not character:
+ printerr("[Dialogic] Textbubble: Tried registering character from invalid string '", character_string, "'.")
+
+ registered_characters[character] = node
+ if len(registered_characters) > len(bubbles) and len(bubbles) < bubble_count:
+ add_bubble()
+
+
+func _get_persistent_info() -> Dictionary:
+ return {"textbubble_registers": registered_characters}
+
+
+func _load_persistent_info(info: Dictionary) -> void:
+ var register_info: Dictionary = info.get("textbubble_registers", {})
+ for character in register_info:
+ if is_instance_valid(register_info[character]):
+ register_character(character, register_info[character])
+
+
+func add_bubble() -> void:
+ if not has_node('TextBubbleLayer'):
+ return
+
+ var new_bubble: Control = get_node("TextBubbleLayer").add_bubble()
+ bubbles.append(new_bubble)
+
+
+func _on_dialogic_text_event(info:Dictionary):
+ var bubble_to_use: Node
+ for bubble in bubbles:
+ if bubble.current_character == info.character:
+ bubble_to_use = bubble
+
+ if bubble_to_use == null:
+ for bubble in bubbles:
+ if bubble.current_character == null:
+ bubble_to_use = bubble
+
+ if bubble_to_use == null:
+ bubble_to_use = bubbles[0]
+
+ var node_to_point_at: Node
+ if info.character in registered_characters:
+ node_to_point_at = registered_characters[info.character]
+ $Example.hide()
+ else:
+ node_to_point_at = $Example/CRT/Marker
+ $Example.show()
+
+ bubble_to_use.current_character = info.character
+ bubble_to_use.node_to_point_at = node_to_point_at
+ bubble_to_use.reset()
+ if has_node('TextBubbleLayer'):
+ get_node("TextBubbleLayer").bubble_apply_overrides(bubble_to_use)
+ bubble_to_use.open()
+
+ ## Now close other bubbles
+ for bubble in bubbles:
+ if bubble != bubble_to_use:
+ bubble.close()
+ bubble.current_character = null
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://syki6k0e6aac"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Base_TextBubble/text_bubble_base.gd" id="1_urqwc"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_70ljh"]
+content_margin_left = 5.0
+content_margin_top = 5.0
+content_margin_right = 5.0
+content_margin_bottom = 5.0
+bg_color = Color(0, 0, 0, 0.654902)
+
+[node name="TextBubbleHolder" type="CanvasLayer"]
+script = ExtResource("1_urqwc")
+
+[node name="Example" type="Control" parent="."]
+visible = false
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Label" type="RichTextLabel" parent="Example"]
+layout_mode = 1
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_left = 12.0
+offset_top = -235.0
+offset_right = 835.0
+offset_bottom = -14.0
+grow_vertical = 0
+theme_override_styles/normal = SubResource("StyleBoxFlat_70ljh")
+bbcode_enabled = true
+text = "This is a fallback bubble, that is not actually connected to any character. In game use the following code to add speech bubbles to a character:
+[color=darkgray]
+var layout = Dialogic.start(timeline_path)
+layout.register_character(character_resource, node)
+[/color]
+- [color=lightblue]character_resource[/color] should be a loaded DialogicCharacter (a .dch file).
+- [color=lightblue]node[/color] should be the 2D or 3D node the bubble should point at.
+ -> E.g. [color=darkgray]layout.register_character(load(\"res://path/to/my/character.dch\"), $BubbleMarker)"
+
+[node name="CRT" type="ColorRect" parent="Example"]
+layout_mode = 0
+offset_left = 504.0
+offset_top = 290.0
+offset_right = 540.0
+offset_bottom = 324.0
+rotation = 0.785397
+color = Color(1, 0.313726, 1, 1)
+
+[node name="Marker" type="Marker2D" parent="Example/CRT"]
+position = Vector2(10.6066, 9.1924)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" version="1.1" id="svg32487" sodipodi:docname="picture.svg" inkscape:export-filename="..\dialogic\dialogic\addons\dialogic\Modules\LayoutStuff\Layer_FullBackground\background_layer_icon.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs32491" />
+ <sodipodi:namedview id="namedview32489" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" showgrid="false" />
+ <path d="M 7.191,6 C 6.87508,6 6.57215,6.11708 6.34885,6.32546 6.12546,6.53385 6,6.81646 6,7.11108 V 24.8889 c 0,0.2946 0.12546,0.5773 0.34885,0.7856 C 6.57215,25.8829 6.87508,26 7.191,26 h 17.618 c 0,0 0.6188,-0.1171 0.8422,-0.3255 C 25.8745,25.4662 26,25.1835 26,24.8889 26,24.5942 26,7.11108 26,7.11108 26,6.81646 25.8745,6.53385 25.6512,6.32546 25.4278,6.11708 25.1249,6 24.809,6 Z M 8.22223,8.22223 H 23.7778 V 21.5555 H 8.22223 Z" fill="#ffffff" id="path32483" />
+ <path d="M 20.4326,19.7336 H 11.749 c -0.7571,0 -1.3172,-0.6658 -1.146,-1.3624 l 0.6072,-2.47 c 0.134,-0.5452 0.6764,-0.9111 1.266,-0.8541 l 1.8929,0.1834 c 0.3794,0.0368 0.7542,-0.1025 1.0054,-0.3736 l 2.335,-2.5203 c 0.6051,-0.6532 1.7341,-0.4282 2.0083,0.4002 l 1.8377,5.5543 c 0.2368,0.7154 -0.3292,1.4425 -1.1229,1.4425 z" fill="#ffffff" id="path32485" />
+</svg>
--- /dev/null
+@tool
+extends DialogicLayoutLayer
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://c1k5m0w3r40xf"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_FullBackground/full_background_layer.gd" id="1_tu40u"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Background/node_background_holder.gd" id="2_ghan2"]
+
+[node name="BackgroundLayer" type="Control"]
+layout_direction = 2
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_tu40u")
+
+[node name="DialogicNode_BackgroundHolder" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+color = Color(1, 1, 1, 0)
+script = ExtResource("2_ghan2")
--- /dev/null
+[style]
+type = "Layer"
+name = "Full Background"
+author = "Dialogic"
+description = "A simple layer displaying backgrounds."
+scene = "full_background_layer.tscn"
+icon = "background_layer_icon.svg"
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## Layer that provides a popup with glossary info,
+## when hovering a glossary entry on a text node.
+
+
+@export_group('Text')
+enum Alignment {LEFT, CENTER, RIGHT}
+@export var title_alignment: Alignment = Alignment.LEFT
+@export var text_alignment: Alignment = Alignment.LEFT
+@export var extra_alignment: Alignment = Alignment.RIGHT
+
+@export_subgroup("Colors")
+enum TextColorModes {GLOBAL, ENTRY, CUSTOM}
+@export var title_color_mode: TextColorModes = TextColorModes.ENTRY
+@export var title_custom_color: Color = Color.WHITE
+@export var text_color_mode: TextColorModes = TextColorModes.ENTRY
+@export var text_custom_color: Color = Color.WHITE
+@export var extra_color_mode: TextColorModes = TextColorModes.ENTRY
+@export var extra_custom_color: Color = Color.WHITE
+
+
+@export_group("Font")
+@export var font_use_global: bool = true
+@export_file('*.ttf', '*.tres') var font_custom: String = ""
+
+@export_subgroup('Sizes')
+@export var font_title_size: int = 18
+@export var font_text_size: int = 17
+@export var font_extra_size: int = 15
+
+
+@export_group("Box")
+@export_subgroup("Color")
+enum ModulateModes {BASE_COLOR_ONLY, ENTRY_COLOR_ON_BOX, GLOBAL_BG_COLOR}
+@export var box_modulate_mode: ModulateModes = ModulateModes.ENTRY_COLOR_ON_BOX
+@export var box_base_modulate: Color = Color.WHITE
+@export_subgroup("Size")
+@export var box_width: int = 200
+
+const MISSING_INDEX := -1
+func get_pointer() -> Control:
+ return $Pointer
+
+
+func get_title() -> Label:
+ return %Title
+
+
+func get_text() -> RichTextLabel:
+ return %Text
+
+
+func get_extra() -> RichTextLabel:
+ return %Extra
+
+
+func get_panel() -> PanelContainer:
+ return %Panel
+
+
+func get_panel_point() -> PanelContainer:
+ return %PanelPoint
+
+
+func _ready() -> void:
+ if Engine.is_editor_hint():
+ return
+
+ get_pointer().hide()
+ var text_system: Node = DialogicUtil.autoload().get(&'Text')
+ var _error: int = 0
+ _error = text_system.connect(&'animation_textbox_hide', get_pointer().hide)
+ _error = text_system.connect(&'meta_hover_started', _on_dialogic_display_dialog_text_meta_hover_started)
+ _error = text_system.connect(&'meta_hover_ended', _on_dialogic_display_dialog_text_meta_hover_ended)
+
+
+## Method that shows the bubble and fills in the info
+func _on_dialogic_display_dialog_text_meta_hover_started(meta: String) -> void:
+ var entry_info := DialogicUtil.autoload().Glossary.get_entry(meta)
+
+ if entry_info.is_empty():
+ return
+
+ get_pointer().show()
+ get_title().text = entry_info.title
+ get_text().text = entry_info.text
+ get_text().text = ['', '[center]', '[right]'][text_alignment] + get_text().text
+ get_extra().text = entry_info.extra
+ get_extra().text = ['', '[center]', '[right]'][extra_alignment] + get_extra().text
+ get_pointer().global_position = get_pointer().get_global_mouse_position()
+
+ if title_color_mode == TextColorModes.ENTRY:
+ get_title().add_theme_color_override(&"font_color", entry_info.color)
+ if text_color_mode == TextColorModes.ENTRY:
+ get_text().add_theme_color_override(&"default_color", entry_info.color)
+ if extra_color_mode == TextColorModes.ENTRY:
+ get_extra().add_theme_color_override(&"default_color", entry_info.color)
+
+ match box_modulate_mode:
+ ModulateModes.ENTRY_COLOR_ON_BOX:
+ get_panel().self_modulate = entry_info.color
+ get_panel_point().self_modulate = entry_info.color
+
+
+## Method that keeps the bubble at mouse position when visible
+func _process(_delta: float) -> void:
+ if Engine.is_editor_hint():
+ return
+
+ var pointer: Control = get_pointer()
+ if pointer.visible:
+ pointer.global_position = pointer.get_global_mouse_position()
+
+
+## Method that hides the bubble
+func _on_dialogic_display_dialog_text_meta_hover_ended(_meta:String) -> void:
+ get_pointer().hide()
+
+
+
+func _apply_export_overrides() -> void:
+ # Apply fonts
+ var font: FontFile
+ var global_font_setting: String = get_global_setting(&"font", '')
+ if font_use_global and ResourceLoader.exists(global_font_setting):
+ font = load(global_font_setting)
+ elif ResourceLoader.exists(font_custom):
+ font = load(font_custom)
+
+ var title: Label = get_title()
+ if font:
+ title.add_theme_font_override(&"font", font)
+ title.horizontal_alignment = title_alignment as HorizontalAlignment
+
+ # Apply font & sizes
+ title.add_theme_font_size_override(&"font_size", font_title_size)
+ var labels: Array[RichTextLabel] = [get_text(), get_extra()]
+ var sizes: PackedInt32Array = [font_text_size, font_extra_size]
+ for i : int in len(labels):
+ if font:
+ labels[i].add_theme_font_override(&'normal_font', font)
+
+ labels[i].add_theme_font_size_override(&"normal_font_size", sizes[i])
+ labels[i].add_theme_font_size_override(&"bold_font_size", sizes[i])
+ labels[i].add_theme_font_size_override(&"italics_font_size", sizes[i])
+ labels[i].add_theme_font_size_override(&"bold_italics_font_size", sizes[i])
+ labels[i].add_theme_font_size_override(&"mono_font_size", sizes[i])
+
+
+ # Apply text colors
+ # this applies Global or Custom colors, entry colors are applied on hover
+ var controls: Array[Control] = [get_title(), get_text(), get_extra()]
+ var settings: Array[StringName] = [&'font_color', &'default_color', &'default_color']
+ var color_modes: Array[TextColorModes] = [title_color_mode, text_color_mode, extra_color_mode]
+ var custom_colors: PackedColorArray = [title_custom_color, text_custom_color, extra_custom_color]
+ for i : int in len(controls):
+ match color_modes[i]:
+ TextColorModes.GLOBAL:
+ controls[i].add_theme_color_override(settings[i], get_global_setting(&'font_color', custom_colors[i]) as Color)
+ TextColorModes.CUSTOM:
+ controls[i].add_theme_color_override(settings[i], custom_colors[i])
+
+ # Apply box size
+ var panel: PanelContainer = get_panel()
+ panel.size.x = box_width
+ panel.position.x = -box_width/2.0
+
+ # Apply box coloring
+ match box_modulate_mode:
+ ModulateModes.BASE_COLOR_ONLY:
+ panel.self_modulate = box_base_modulate
+ get_panel_point().self_modulate = box_base_modulate
+ ModulateModes.GLOBAL_BG_COLOR:
+ panel.self_modulate = get_global_setting(&'bg_color', box_base_modulate)
+ get_panel_point().self_modulate = get_global_setting(&'bg_color', box_base_modulate)
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://dsbwnp5hegnu3"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Glossary/glossary_popup_layer.gd" id="1_3nmfj"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a3cyk"]
+bg_color = Color(0.12549, 0.12549, 0.12549, 1)
+border_width_left = 2
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 2
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+expand_margin_left = 5.0
+expand_margin_top = 5.0
+expand_margin_right = 5.0
+expand_margin_bottom = 5.0
+
+[node name="Glossary" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+mouse_filter = 2
+script = ExtResource("1_3nmfj")
+
+[node name="Pointer" type="Control" parent="."]
+anchors_preset = 0
+
+[node name="Panel" type="PanelContainer" parent="Pointer"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -81.0
+offset_top = -113.0
+offset_right = 86.0
+offset_bottom = -35.0
+grow_horizontal = 2
+grow_vertical = 0
+theme_override_styles/panel = SubResource("StyleBoxFlat_a3cyk")
+metadata/_edit_use_custom_anchors = true
+metadata/_edit_layout_mode = 1
+
+[node name="VBox" type="VBoxContainer" parent="Pointer/Panel"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Title" type="Label" parent="Pointer/Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator" type="HSeparator" parent="Pointer/Panel/VBox"]
+layout_mode = 2
+
+[node name="Text" type="RichTextLabel" parent="Pointer/Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+bbcode_enabled = true
+fit_content = true
+
+[node name="Extra" type="RichTextLabel" parent="Pointer/Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_font_sizes/normal_font_size = 15
+bbcode_enabled = true
+fit_content = true
+
+[node name="Control" type="Control" parent="Pointer/Panel"]
+show_behind_parent = true
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 8
+
+[node name="PanelPoint" type="PanelContainer" parent="Pointer/Panel/Control"]
+unique_name_in_owner = true
+layout_mode = 0
+offset_left = -0.999999
+offset_top = -14.0
+offset_right = 19.0
+offset_bottom = 6.0
+rotation = 0.75799
+size_flags_horizontal = 4
+size_flags_vertical = 8
+theme_override_styles/panel = SubResource("StyleBoxFlat_a3cyk")
--- /dev/null
+[style]
+type = "Layer"
+name = "Popup Glossary"
+author = "Dialogic"
+description = "A popup that that appears when hovering glossary entries."
+scene = "glossary_popup_layer.tscn"
+icon = "popup_glossary_layer_icon.svg"
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="100" height="100" viewBox="0 0 26.458333 26.458333" version="1.1" id="svg13076" inkscape:export-filename="text_input_layer_icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview13078" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="mm" showgrid="true" inkscape:zoom="4.185" inkscape:cx="70.728793" inkscape:cy="64.755078" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
+ <inkscape:grid type="xygrid" id="grid14286" />
+ </sodipodi:namedview>
+ <defs id="defs13073" />
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
+ <path style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 3.9687497,20.505208 v -9.260417 l 9.2604173,2.645834 9.260416,-2.645834 v 9.260417 l -9.260416,2.645833 -9.2604173,-2.645833" id="path35151" sodipodi:nodetypes="ccccccc" />
+ <path style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 13.229167,13.890625 v 9.260416" id="path35153" sodipodi:nodetypes="cc" />
+ <rect style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" id="rect35155" width="10.583333" height="3.96875" x="7.9375" y="3.96875" ry="0.050781649" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 11.90625,7.9374999 c 1.322917,2.6458331 1.322917,2.6458331 1.322917,2.6458331 l 1.322916,-2.6458331 z" id="path35313" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 6.6145829,15.213541 3.9687501,1.322917" id="path35315" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 6.6145829,17.859374 3.9687501,1.322917" id="path35317" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 15.875,16.536458 3.96875,-1.322917" id="path35319" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 15.875,19.182291 3.96875,-1.322916" id="path35321" sodipodi:nodetypes="cc" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 15.875,19.182291 3.96875,-1.322916" id="path35323" sodipodi:nodetypes="cc" />
+ </g>
+</svg>
--- /dev/null
+extends Container
+
+func get_text_box() -> RichTextLabel:
+ return %TextBox
+
+
+func get_name_label() -> Label:
+ return %NameLabel
+
+
+func get_icon() -> TextureRect:
+ return %Icon
+
+
+func load_info(text:String, character:String = "", character_color: Color =Color(), icon:Texture= null) -> void:
+ get_text_box().text = text
+ var name_label: Label = get_name_label()
+ if character:
+ name_label.text = character
+ name_label.add_theme_color_override('font_color', character_color)
+ name_label.show()
+ else:
+ name_label.hide()
+
+ var icon_node: TextureRect = get_icon()
+ if icon == null:
+ icon_node.hide()
+ else:
+ icon_node.show()
+ icon_node.texture = icon
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://cuoywrmvda1yg"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_History/example_history_item.gd" id="1_dgoja"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_upgjp"]
+content_margin_left = 5.0
+content_margin_top = 5.0
+content_margin_right = 5.0
+content_margin_bottom = 5.0
+bg_color = Color(0.780392, 0.780392, 0.780392, 0.156863)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="HistoryItem" type="PanelContainer"]
+offset_left = -37.0
+offset_top = 510.0
+offset_right = 1085.0
+offset_bottom = 555.0
+theme_override_styles/panel = SubResource("StyleBoxFlat_upgjp")
+script = ExtResource("1_dgoja")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Icon" type="TextureRect" parent="HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+expand_mode = 1
+stretch_mode = 4
+
+[node name="NameLabel" type="Label" parent="HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="TextBox" type="RichTextLabel" parent="HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+bbcode_enabled = true
+text = "Some tex"
+fit_content = true
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1"
+ id="svg5" inkscape:export-filename="history-icon.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" sodipodi:docname="history_icon.svg" inkscape:export-ydpi="96" inkscape:export-xdpi="96" inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px"
+ viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
+<sodipodi:namedview id="namedview7" inkscape:pagecheckerboard="0" inkscape:cy="23.572542" inkscape:zoom="2.2483786" inkscape:cx="-42.25267" pagecolor="#464646" showgrid="false" borderopacity="0.25" bordercolor="#000000" inkscape:document-units="mm" inkscape:deskcolor="#d1d1d1" inkscape:pageopacity="0" inkscape:showpageshadow="2" inkscape:current-layer="svg5" inkscape:window-y="-8" inkscape:window-x="-8" inkscape:window-height="1017" inkscape:window-width="1920" showguides="true" inkscape:window-maximized="1">
+ </sodipodi:namedview>
+<g>
+ <path fill="#FFFFFF" d="M50.037,53.276l17.386,5.477c0.4,0.126,0.805,0.186,1.203,0.186c1.7,0,3.276-1.092,3.814-2.799
+ c0.663-2.107-0.507-4.354-2.613-5.018l-14.968-4.714l-1.078-7.916c-0.299-2.189-2.311-3.721-4.504-3.423
+ c-2.189,0.298-3.722,2.314-3.423,4.503l1.421,10.429C47.484,51.535,48.56,52.811,50.037,53.276z"/>
+ <path fill="#FFFFFF" d="M52.908,7.826c-20.465,0-37.303,15.785-39.001,35.818l-2.034,0.034c-1.447,0.024-2.769,0.829-3.455,2.104
+ c-0.687,1.274-0.63,2.821,0.146,4.042l5.943,9.347c0.735,1.155,2.009,1.854,3.375,1.854c0.021,0,0.042,0,0.063-0.001
+ c1.391-0.021,2.669-0.765,3.378-1.961l5.65-9.541c0.738-1.247,0.745-2.795,0.017-4.048s-2.067-2.012-3.525-1.99l-1.513,0.025
+ c1.731-15.55,14.95-27.684,30.956-27.684c17.178,0,31.152,13.975,31.152,31.152c0,17.178-13.975,31.152-31.152,31.152
+ c-2.438,0-4.866-0.282-7.213-0.838c-0.543-0.129-1.086-0.138-1.604-0.048c-0.385-0.001-0.775,0.035-1.164,0.152l-17.479,5.268
+ l1.693-10.559c0.35-2.182-1.135-4.233-3.316-4.583c-2.182-0.357-4.233,1.135-4.583,3.315l-2.705,16.865
+ c-0.218,1.361,0.279,2.738,1.316,3.645c0.739,0.646,1.678,0.989,2.634,0.989c0.386,0,0.774-0.056,1.154-0.17l22.993-6.929
+ c2.707,0.582,5.484,0.892,8.275,0.892c21.589,0,39.152-17.563,39.152-39.152S74.497,7.826,52.908,7.826z"/>
+</g>
+</svg>
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## Example scene for viewing the History
+## Implements most of the visual options from 1.x History mode
+
+@export_group('Look')
+@export_subgroup('Font')
+@export var font_use_global_size: bool = true
+@export var font_custom_size: int = 15
+@export var font_use_global_fonts: bool = true
+@export_file('*.ttf', '*.tres') var font_custom_normal: String = ""
+@export_file('*.ttf', '*.tres') var font_custom_bold: String = ""
+@export_file('*.ttf', '*.tres') var font_custom_italics: String = ""
+
+@export_subgroup('Buttons')
+@export var show_open_button: bool = true
+@export var show_close_button: bool = true
+
+@export_group('Settings')
+@export_subgroup('Events')
+@export var show_all_choices: bool = true
+@export var show_join_and_leave: bool = true
+
+@export_subgroup('Behaviour')
+@export var scroll_to_bottom: bool = true
+@export var show_name_colors: bool = true
+@export var name_delimeter: String = ": "
+
+var scroll_to_bottom_flag: bool = false
+
+@export_group('Private')
+@export var HistoryItem: PackedScene = null
+
+var history_item_theme: Theme = null
+
+func get_show_history_button() -> Button:
+ return $ShowHistory
+
+
+func get_hide_history_button() -> Button:
+ return $HideHistory
+
+
+func get_history_box() -> ScrollContainer:
+ return %HistoryBox
+
+
+func get_history_log() -> VBoxContainer:
+ return %HistoryLog
+
+
+func _ready() -> void:
+ if Engine.is_editor_hint():
+ return
+ DialogicUtil.autoload().History.open_requested.connect(_on_show_history_pressed)
+ DialogicUtil.autoload().History.close_requested.connect(_on_hide_history_pressed)
+
+
+func _apply_export_overrides() -> void:
+ var history_subsystem: Node = DialogicUtil.autoload().get(&'History')
+ if history_subsystem != null:
+ get_show_history_button().visible = show_open_button and history_subsystem.get(&'simple_history_enabled')
+ else:
+ set(&'visible', false)
+
+ history_item_theme = Theme.new()
+
+ if font_use_global_size:
+ history_item_theme.default_font_size = get_global_setting(&'font_size', font_custom_size)
+ else:
+ history_item_theme.default_font_size = font_custom_size
+
+ if font_use_global_fonts and ResourceLoader.exists(get_global_setting(&'font', '') as String):
+ history_item_theme.default_font = load(get_global_setting(&'font', '') as String) as Font
+ elif ResourceLoader.exists(font_custom_normal):
+ history_item_theme.default_font = load(font_custom_normal)
+
+ if ResourceLoader.exists(font_custom_bold):
+ history_item_theme.set_font(&'RichtTextLabel', &'bold_font', load(font_custom_bold) as Font)
+ if ResourceLoader.exists(font_custom_italics):
+ history_item_theme.set_font(&'RichtTextLabel', &'italics_font', load(font_custom_italics) as Font)
+
+
+
+func _process(_delta : float) -> void:
+ if Engine.is_editor_hint():
+ return
+ if scroll_to_bottom_flag and get_history_box().visible and get_history_log().get_child_count():
+ await get_tree().process_frame
+ get_history_box().ensure_control_visible(get_history_log().get_children()[-1] as Control)
+ scroll_to_bottom_flag = false
+
+
+func _on_show_history_pressed() -> void:
+ DialogicUtil.autoload().paused = true
+ show_history()
+
+
+func show_history() -> void:
+ for child: Node in get_history_log().get_children():
+ child.queue_free()
+
+ var history_subsystem: Node = DialogicUtil.autoload().get(&'History')
+ for info: Dictionary in history_subsystem.call(&'get_simple_history'):
+ var history_item : Node = HistoryItem.instantiate()
+ history_item.set(&'theme', history_item_theme)
+ match info.event_type:
+ "Text":
+ if info.has('character') and info['character']:
+ if show_name_colors:
+ history_item.call(&'load_info', info['text'], info['character']+name_delimeter, info['character_color'])
+ else:
+ history_item.call(&'load_info', info['text'], info['character']+name_delimeter)
+ else:
+ history_item.call(&'load_info', info['text'])
+ "Character":
+ if !show_join_and_leave:
+ history_item.queue_free()
+ continue
+ history_item.call(&'load_info', '[i]'+info['text'])
+ "Choice":
+ var choices_text: String = ""
+ if show_all_choices:
+ for i : String in info['all_choices']:
+ if i.ends_with('#disabled'):
+ choices_text += "- [i]("+i.trim_suffix('#disabled')+")[/i]\n"
+ elif i == info['text']:
+ choices_text += "-> [b]"+i+"[/b]\n"
+ else:
+ choices_text += "-> "+i+"\n"
+ else:
+ choices_text += "- [b]"+info['text']+"[/b]\n"
+ history_item.call(&'load_info', choices_text)
+
+ get_history_log().add_child(history_item)
+
+ if scroll_to_bottom:
+ scroll_to_bottom_flag = true
+
+ get_show_history_button().hide()
+ get_hide_history_button().visible = show_close_button
+ get_history_box().show()
+
+
+func _on_hide_history_pressed() -> void:
+ DialogicUtil.autoload().paused = false
+ get_history_box().hide()
+ get_hide_history_button().hide()
+ var history_subsystem: Node = DialogicUtil.autoload().get(&'History')
+ get_show_history_button().visible = show_open_button and history_subsystem.get(&'simple_history_enabled')
--- /dev/null
+[gd_scene load_steps=4 format=3 uid="uid://lx24i8fl6uo"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_History/history_layer.gd" id="1_4mqm3"]
+[ext_resource type="PackedScene" uid="uid://cuoywrmvda1yg" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_History/example_history_item.tscn" id="2_x1xgk"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1hdvb"]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(0, 0, 0, 0.776471)
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_left = 20
+corner_radius_top_right = 20
+corner_radius_bottom_right = 20
+corner_radius_bottom_left = 20
+
+[node name="ExampleHistoryScene" type="Control"]
+layout_direction = 1
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+mouse_filter = 2
+script = ExtResource("1_4mqm3")
+font_use_global_size = null
+font_custom_size = null
+font_use_global_fonts = null
+font_custom_normal = null
+font_custom_bold = null
+font_custom_italics = null
+HistoryItem = ExtResource("2_x1xgk")
+disabled = null
+
+[node name="HistoryBox" type="ScrollContainer" parent="."]
+unique_name_in_owner = true
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 74.0
+offset_top = 65.0
+offset_right = -74.0
+offset_bottom = -57.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_1hdvb")
+horizontal_scroll_mode = 0
+
+[node name="HistoryLog" type="VBoxContainer" parent="HistoryBox"]
+unique_name_in_owner = true
+layout_direction = 1
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="ShowHistory" type="Button" parent="."]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -73.0
+offset_top = 7.0
+offset_right = -9.0
+offset_bottom = 38.0
+grow_horizontal = 0
+size_flags_horizontal = 4
+size_flags_vertical = 4
+text = "History"
+
+[node name="HideHistory" type="Button" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -123.0
+offset_top = 58.0
+offset_right = -62.0
+offset_bottom = 89.0
+grow_horizontal = 0
+size_flags_horizontal = 4
+size_flags_vertical = 4
+text = "Return"
+
+[connection signal="pressed" from="ShowHistory" to="." method="_on_show_history_pressed"]
+[connection signal="pressed" from="HideHistory" to="." method="_on_hide_history_pressed"]
--- /dev/null
+[style]
+type = "Layer"
+name = "Overlay History"
+author = "Dialogic"
+description = "Provides a history button and overlay."
+scene = "history_layer.tscn"
+icon = "history_icon.svg"
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## A layer that holds a full-screen input catcher.
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://cn674foxwedqu"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Input/full_advance_input_layer.gd" id="1_3cmha"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_input.gd" id="2_dxpjw"]
+
+[node name="FullAdvanceInputLayer" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_3cmha")
+
+[node name="DialogicNode_Input" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 1
+script = ExtResource("2_dxpjw")
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="100" height="100" viewBox="0 0 26.458333 26.458333" version="1.1" id="svg13076" inkscape:export-filename="portrait_layer.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview13078" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="mm" showgrid="true" inkscape:zoom="4.185" inkscape:cx="70.728793" inkscape:cy="64.755078" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
+ <inkscape:grid type="xygrid" id="grid14286" />
+ </sodipodi:namedview>
+ <defs id="defs13073" />
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
+ <path id="rect32348" style="fill:#ffffff;fill-opacity:0.75;stroke:none;stroke-width:0.724233;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" d="m 6.234761,11.189706 v 5.629112 c 0,3.874621 3.1197871,6.993682 6.994406,6.993682 3.87462,0 6.993682,-3.119061 6.993682,-6.993682 v -5.629112 z" />
+ <path id="path35376" style="fill:#ffffff;fill-opacity:0.75;stroke:none;stroke-width:0.724233;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" d="M 13.806123,2.8560687 V 5.619952 c 0.38293,0.2052666 0.642107,0.6084819 0.642107,1.0750043 v 1.2197859 c 0,0.4665295 -0.259168,0.8697412 -0.642107,1.0750052 v 1.0445996 h 6.416726 V 9.8244141 c 0,-3.6799558 -2.814523,-6.6763287 -6.416726,-6.9683454 z" />
+ <path id="path35374" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.724233;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" d="M 12.652213,2.8560687 C 9.0496644,3.1477449 6.234761,6.144214 6.234761,9.8244141 v 0.2099329 h 6.417452 V 8.9904712 C 12.268805,8.785348 12.009381,8.3816175 12.009381,7.9147422 V 6.6949563 c 0,-0.4668682 0.259433,-0.8706032 0.642832,-1.0757281 z" />
+ </g>
+</svg>
--- /dev/null
+[style]
+type = "Layer"
+name = "Input Catcher"
+author = "Dialogic"
+description = "A full screen mouse input catcher for advancing dialog."
+scene = "full_advance_input_layer.tscn"
+icon = "input_layer_icon.svg"
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://cmpf1qxjh5tuw"]
+
+[resource]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(1, 1, 1, 1)
+skew = Vector2(0.073, 0)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
--- /dev/null
+[style]
+type = "Layer"
+name = "Textbox with Portrait "
+author = "Dialogic"
+description = "A layer with a textbox that also contains a speaker portrait."
+scene = "textbox_with_speaker_portrait.tscn"
+icon = "speaker-textbox-icon.svg"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1"
+ id="svg13076" inkscape:export-filename="bitmap.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" sodipodi:docname="textbox_layer_icon.svg" inkscape:export-ydpi="96" inkscape:export-xdpi="96"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px"
+ viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
+<g>
+ <circle id="path6939_1_" fill="#FFFFFF" cx="30.504" cy="50.494" r="10.29"/>
+ <path id="path6995_1_" fill="#FFFFFF" d="M39.762,75.19h-19.55l4.117-17.494h11.318L39.762,75.19z"/>
+</g>
+<path fill="#FFFFFF" d="M87,20H13c-4.418,0-8,3.582-8,8v47.875c0,4.418,3.582,8,8,8h74c4.418,0,8-3.582,8-8V28
+ C95,23.582,91.418,20,87,20z M58.585,36.733h12.364c1.888,0,3.42,1.531,3.42,3.419c0,1.888-1.532,3.419-3.42,3.419H58.585
+ c-1.888,0-3.419-1.531-3.419-3.419C55.166,38.264,56.697,36.733,58.585,36.733z M47.546,69.842c0,2.721-2.207,4.928-4.927,4.928
+ h-24.23c-2.72,0-4.927-2.207-4.927-4.928v-35.77c0-2.72,2.207-4.927,4.927-4.927h24.23c2.721,0,4.927,2.208,4.927,4.927V69.842z
+ M85.08,67.142H58.585c-1.889,0-3.419-1.531-3.419-3.42s1.53-3.42,3.419-3.42H85.08c1.887,0,3.419,1.531,3.419,3.42
+ S86.967,67.142,85.08,67.142z M85.08,55.355H58.585c-1.888,0-3.419-1.529-3.419-3.418s1.531-3.419,3.419-3.419H85.08
+ c1.888,0,3.419,1.53,3.419,3.419S86.968,55.355,85.08,55.355z"/>
+</svg>
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+enum Alignments {LEFT, CENTER, RIGHT}
+enum LimitedAlignments {LEFT=0, RIGHT=1}
+
+@export_group('Text')
+@export_subgroup("Text")
+@export var text_alignment: Alignments = Alignments.LEFT
+@export_subgroup('Size')
+@export var text_use_global_size: bool = true
+@export var text_custom_size: int = 15
+@export_subgroup('Color')
+@export var text_use_global_color: bool = true
+@export var text_custom_color: Color = Color.WHITE
+@export_subgroup('Fonts')
+@export var use_global_fonts: bool = true
+@export_file('*.ttf', '*.tres') var custom_normal_font: String = ""
+@export_file('*.ttf', '*.tres') var custom_bold_font: String = ""
+@export_file('*.ttf', '*.tres') var custom_italic_font: String = ""
+@export_file('*.ttf', '*.tres') var custom_bold_italic_font: String = ""
+
+@export_group('Name Label')
+@export_subgroup("Color")
+enum NameLabelColorModes {GLOBAL_COLOR, CHARACTER_COLOR, CUSTOM_COLOR}
+@export var name_label_color_mode: NameLabelColorModes = NameLabelColorModes.GLOBAL_COLOR
+@export var name_label_custom_color: Color = Color.WHITE
+@export_subgroup("Behaviour")
+@export var name_label_alignment: Alignments = Alignments.LEFT
+@export var name_label_hide_when_no_character: bool = false
+@export_subgroup("Font & Size")
+@export var name_label_use_global_size: bool = true
+@export var name_label_custom_size: int = 15
+@export var name_label_use_global_font: bool = true
+@export_file('*.ttf', '*.tres') var name_label_customfont: String = ""
+
+@export_group('Box')
+@export_subgroup("Box")
+@export_file('*.tres') var box_panel: String = this_folder.path_join("default_stylebox.tres")
+@export var box_modulate_global_color: bool = true
+@export var box_modulate_custom_color: Color = Color(0.47247135639191, 0.31728461384773, 0.16592600941658)
+@export var box_size: Vector2 = Vector2(600, 160)
+@export var box_distance: int = 25
+
+@export_group('Portrait')
+@export_subgroup('Portrait')
+@export var portrait_stretch_factor: float = 0.3
+@export var portrait_position: LimitedAlignments = LimitedAlignments.LEFT
+@export var portrait_bg_modulate: Color = Color(0, 0, 0, 0.5137255191803)
+
+
+## Called by dialogic whenever export overrides might change
+func _apply_export_overrides() -> void:
+ ## FONT SETTINGS
+ var dialog_text: DialogicNode_DialogText = %DialogicNode_DialogText
+ dialog_text.alignment = text_alignment as DialogicNode_DialogText.Alignment
+
+ var text_size: int = text_custom_size
+ if text_use_global_size:
+ text_size = get_global_setting(&'font_size', text_custom_size)
+
+ dialog_text.add_theme_font_size_override(&"normal_font_size", text_size)
+ dialog_text.add_theme_font_size_override(&"bold_font_size", text_size)
+ dialog_text.add_theme_font_size_override(&"italics_font_size", text_size)
+ dialog_text.add_theme_font_size_override(&"bold_italics_font_size", text_size)
+
+
+ var text_color: Color = text_custom_color
+ if text_use_global_color:
+ text_color = get_global_setting(&'font_color', text_custom_color)
+ dialog_text.add_theme_color_override(&"default_color", text_color)
+
+ var normal_font: String = custom_normal_font
+ if use_global_fonts and ResourceLoader.exists(get_global_setting(&'font', '') as String):
+ normal_font = get_global_setting(&'font', '')
+
+ if !normal_font.is_empty():
+ dialog_text.add_theme_font_override(&"normal_font", load(normal_font) as Font)
+ if !custom_bold_font.is_empty():
+ dialog_text.add_theme_font_override(&"bold_font", load(custom_bold_font) as Font)
+ if !custom_italic_font.is_empty():
+ dialog_text.add_theme_font_override(&"italics_font", load(custom_italic_font) as Font)
+ if !custom_bold_italic_font.is_empty():
+ dialog_text.add_theme_font_override(&"bold_italics_font", load(custom_bold_italic_font) as Font)
+
+ ## BOX SETTINGS
+ var panel: PanelContainer = %Panel
+ var portrait_panel: Panel = %PortraitPanel
+ if box_modulate_global_color:
+ panel.self_modulate = get_global_setting(&'bg_color', box_modulate_custom_color)
+ else:
+ panel.self_modulate = box_modulate_custom_color
+ panel.size = box_size
+ panel.position = Vector2(-box_size.x/2, -box_size.y-box_distance)
+ portrait_panel.size_flags_stretch_ratio = portrait_stretch_factor
+
+ var stylebox: StyleBox = load(box_panel)
+ panel.add_theme_stylebox_override(&'panel', stylebox)
+
+ ## PORTRAIT SETTINGS
+ var portrait_background_color: ColorRect = %PortraitBackgroundColor
+ portrait_background_color.color = portrait_bg_modulate
+
+ portrait_panel.get_parent().move_child(portrait_panel, portrait_position)
+
+ ## NAME LABEL SETTINGS
+ var name_label: DialogicNode_NameLabel = %DialogicNode_NameLabel
+ if name_label_use_global_size:
+ name_label.add_theme_font_size_override(&"font_size", get_global_setting(&'font_size', name_label_custom_size) as int)
+ else:
+ name_label.add_theme_font_size_override(&"font_size", name_label_custom_size)
+
+ var name_label_font: String = name_label_customfont
+ if name_label_use_global_font and ResourceLoader.exists(get_global_setting(&'font', '') as String):
+ name_label_font = get_global_setting(&'font', '')
+ if !name_label_font.is_empty():
+ name_label.add_theme_font_override(&'font', load(name_label_font) as Font)
+
+ name_label.use_character_color = false
+ match name_label_color_mode:
+ NameLabelColorModes.GLOBAL_COLOR:
+ name_label.add_theme_color_override(&"font_color", get_global_setting(&'font_color', name_label_custom_color) as Color)
+ NameLabelColorModes.CUSTOM_COLOR:
+ name_label.add_theme_color_override(&"font_color", name_label_custom_color)
+ NameLabelColorModes.CHARACTER_COLOR:
+ name_label.use_character_color = true
+
+ name_label.horizontal_alignment = name_label_alignment as HorizontalAlignment
+ name_label.hide_when_empty = name_label_hide_when_no_character
--- /dev/null
+[gd_scene load_steps=7 format=3 uid="uid://by6waso0mjpjp"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Character/node_portrait_container.gd" id="1_4jxq7"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_SpeakerPortraitTextbox/speaker_portrait_textbox_layer.gd" id="1_7jt4d"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_name_label.gd" id="2_y0h34"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_dialog_text.gd" id="3_11puy"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_type_sound.gd" id="5_sr2qw"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dmg1w"]
+bg_color = Color(0.254902, 0.254902, 0.254902, 1)
+skew = Vector2(0.073, 0)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[node name="TextboxWithSpeakerPortrait" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_7jt4d")
+box_panel = "res://addons/dialogic/Modules/DefaultLayoutParts/Layer_SpeakerPortraitTextbox/default_stylebox.tres"
+
+[node name="Anchor" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 0
+mouse_filter = 2
+
+[node name="Panel" type="PanelContainer" parent="Anchor"]
+unique_name_in_owner = true
+self_modulate = Color(0.533333, 0.376471, 0.176471, 1)
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -250.0
+offset_top = -200.0
+offset_right = 250.0
+offset_bottom = -50.0
+grow_horizontal = 2
+grow_vertical = 0
+mouse_filter = 2
+
+[node name="HBox" type="HBoxContainer" parent="Anchor/Panel"]
+layout_mode = 2
+mouse_filter = 2
+theme_override_constants/separation = 15
+
+[node name="PortraitPanel" type="Panel" parent="Anchor/Panel/HBox"]
+unique_name_in_owner = true
+clip_children = 1
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.3
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_dmg1w")
+
+[node name="PortraitBackgroundColor" type="ColorRect" parent="Anchor/Panel/HBox/PortraitPanel"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -7.0
+offset_top = -3.0
+offset_right = 7.0
+offset_bottom = 3.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+color = Color(0, 0, 0, 0.231373)
+
+[node name="DialogicNode_PortraitContainer" type="Control" parent="Anchor/Panel/HBox/PortraitPanel/PortraitBackgroundColor"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = 4.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_4jxq7")
+mode = 1
+container_ids = PackedStringArray("1")
+debug_character_portrait = "speaker"
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Anchor/Panel/HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+mouse_filter = 2
+
+[node name="DialogicNode_NameLabel" type="Label" parent="Anchor/Panel/HBox/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_font_sizes/font_size = 8
+text = "Name"
+script = ExtResource("2_y0h34")
+
+[node name="DialogicNode_DialogText" type="RichTextLabel" parent="Anchor/Panel/HBox/VBoxContainer" node_paths=PackedStringArray("textbox_root")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_font_sizes/normal_font_size = 6
+bbcode_enabled = true
+text = "Some text"
+scroll_following = true
+visible_characters_behavior = 1
+script = ExtResource("3_11puy")
+textbox_root = NodePath("../../..")
+
+[node name="DialogicNode_TypeSounds" type="AudioStreamPlayer" parent="Anchor/Panel/HBox/VBoxContainer/DialogicNode_DialogText"]
+script = ExtResource("5_sr2qw")
--- /dev/null
+[style]
+type = "Layer"
+name = "Simple Text Input Box"
+author = "Dialogic"
+description = "A layer with a simple text input box."
+scene = "text_input_layer.tscn"
+icon = "text_input_layer_icon.svg"
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## A layer that contains a text-input node.
+
+
+func _apply_export_overrides() -> void:
+ var layer_theme: Theme = get(&'theme')
+ if layer_theme == null:
+ layer_theme = Theme.new()
+
+ if get_global_setting(&'font', ''):
+ layer_theme.default_font = load(get_global_setting(&'font', '') as String)
+ layer_theme.default_font_size = get_global_setting(&'font_size', 0)
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://cvgf4c6gg0tsy"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_TextInput/text_input_layer.gd" id="1_7ahrn"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/TextInput/node_text_input.gd" id="1_mxdep"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3dpjm"]
+content_margin_left = 15.0
+content_margin_top = 15.0
+content_margin_right = 15.0
+content_margin_bottom = 15.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="Theme" id="Theme_8xwp1"]
+
+[node name="TextInputLayer" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_7ahrn")
+
+[node name="DialogicNode_TextInput" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -210.0
+offset_top = -50.0
+offset_right = 210.0
+offset_bottom = 50.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_mxdep")
+input_line_edit = NodePath("TextInputPanel/VBoxContainer/InputField")
+text_label = NodePath("TextInputPanel/VBoxContainer/TextLabel")
+confirmation_button = NodePath("TextInputPanel/VBoxContainer/ConfirmationButton")
+metadata/_edit_layout_mode = 1
+
+[node name="TextInputPanel" type="PanelContainer" parent="DialogicNode_TextInput"]
+unique_name_in_owner = true
+self_modulate = Color(0, 0, 0, 0.780392)
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_3dpjm")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="DialogicNode_TextInput/TextInputPanel"]
+layout_mode = 2
+
+[node name="TextLabel" type="Label" parent="DialogicNode_TextInput/TextInputPanel/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme = SubResource("Theme_8xwp1")
+text = "Please enter some text:"
+autowrap_mode = 3
+
+[node name="InputField" type="LineEdit" parent="DialogicNode_TextInput/TextInputPanel/VBoxContainer"]
+layout_mode = 2
+
+[node name="ConfirmationButton" type="Button" parent="DialogicNode_TextInput/TextInputPanel/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 8
+text = "Confirm"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1"
+ id="svg13076" inkscape:export-filename="input_layer_icon.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" inkscape:export-ydpi="96" inkscape:export-xdpi="96" inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px"
+ viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
+<sodipodi:namedview id="namedview13078" inkscape:pagecheckerboard="0" inkscape:cy="64.755078" inkscape:zoom="4.185" inkscape:cx="70.728793" pagecolor="#505050" showgrid="true" borderopacity="1" bordercolor="#eeeeee" inkscape:document-units="mm" inkscape:deskcolor="#505050" inkscape:pageopacity="0" inkscape:showpageshadow="0" inkscape:current-layer="layer1" inkscape:window-y="-8" inkscape:window-x="-8" inkscape:window-height="1017" inkscape:window-width="1920" inkscape:window-maximized="1">
+ <inkscape:grid id="grid14286" type="xygrid"></inkscape:grid>
+</sodipodi:namedview>
+<g id="layer1" inkscape:label="Layer 1" inkscape:groupmode="layer">
+ <g id="path30124">
+ <polygon fill="#FFFFFF" points="40.03,68.723 24.778,68.723 24.778,60.723 28.403,60.723 28.403,39.191 24.778,39.191
+ 24.778,31.191 40.03,31.191 40.03,39.191 36.403,39.191 36.403,60.723 40.03,60.723 "/>
+ </g>
+ <g id="rect30126">
+ <path fill="#FFFFFF" d="M81.973,83.529H17.151c-6.41,0-11.625-5.216-11.625-11.626V28.097c0-6.411,5.215-11.626,11.625-11.626
+ h64.821c6.411,0,11.627,5.215,11.627,11.626v43.807C93.6,78.313,88.384,83.529,81.973,83.529z M17.151,24.471
+ c-1.999,0-3.625,1.626-3.625,3.626v43.807c0,1.999,1.626,3.626,3.625,3.626h64.821c2,0,3.627-1.627,3.627-3.626V28.097
+ c0-2-1.627-3.626-3.627-3.626H17.151z"/>
+ </g>
+</g>
+</svg>
--- /dev/null
+[style]
+type = "Layer"
+name = "Textbubble Layer"
+author = "Dialogic"
+description = "A simple textbubble layer. Expects a textbubble base. Each textbubble provides a name label, dialog text and choices."
+scene = "text_bubble_layer.tscn"
+icon = "text_bubble_layer_icon.svg"
--- /dev/null
+shader_type canvas_item;
+
+uniform sampler2D deformation_sampler : filter_linear, repeat_enable;
+uniform float radius :hint_range(1.0, 200, 0.01)= 25;
+uniform vec2 box_size = vec2(100, 100);
+uniform float box_padding = 15;
+uniform float wobble_amount : hint_range(0.0, 1.0, 0.01) = 0.2;
+uniform float wobble_speed : hint_range(0.0, 10.0, 0.01) = 1;
+uniform float wobble_detail : hint_range(0.01, 1, 0.01) = 0.5;
+
+void fragment() {
+ float adjusted_radius = min(min(radius, box_size.x/2.0), box_size.y/2.0);
+ vec2 deformation_sample = texture(deformation_sampler, UV*wobble_detail+TIME*wobble_speed*0.05).xy*(vec2(box_padding)/box_size)*0.9;
+ vec2 deformed_UV = UV+((deformation_sample)-vec2(0.5)*vec2(box_padding)/box_size)*wobble_amount;
+ float rounded_box = length(max(abs(deformed_UV*(box_size+vec2(box_padding))-vec2(0.5)*(box_size+vec2(box_padding)))+adjusted_radius-vec2(0.5)*box_size,0))-adjusted_radius;
+ COLOR.a = min(smoothstep(0.0, -1, rounded_box), COLOR.a);
+}
--- /dev/null
+extends Control
+
+@onready var tail: Line2D = ($Group/Tail as Line2D)
+@onready var bubble: Control = ($Group/Background as Control)
+@onready var text: DialogicNode_DialogText = (%DialogText as DialogicNode_DialogText)
+# The choice container is added by the TextBubble layer
+@onready var choice_container: Container = null
+@onready var name_label: Label = (%NameLabel as Label)
+@onready var name_label_box: PanelContainer = (%NameLabelPanel as PanelContainer)
+@onready var name_label_holder: HBoxContainer = $DialogText/NameLabelPositioner
+
+var node_to_point_at: Node = null
+var current_character: DialogicCharacter = null
+
+var max_width := 300
+
+var bubble_rect: Rect2 = Rect2(0.0, 0.0, 2.0, 2.0)
+var base_position := Vector2.ZERO
+
+var base_direction := Vector2(1.0, -1.0).normalized()
+var safe_zone := 50.0
+var padding := Vector2()
+
+var name_label_alignment := HBoxContainer.ALIGNMENT_BEGIN
+var name_label_offset := Vector2()
+var force_choices_on_separate_lines := false
+
+# Sets the padding shader paramter.
+# It's the amount of spacing around the background to allow some wobbeling.
+var bg_padding := 30
+
+
+func _ready() -> void:
+ reset()
+ DialogicUtil.autoload().Choices.question_shown.connect(_on_question_shown)
+
+
+func reset() -> void:
+ scale = Vector2.ZERO
+ modulate.a = 0.0
+
+ tail.points = []
+ bubble_rect = Rect2(0,0,2,2)
+
+ base_position = get_speaker_canvas_position()
+ position = base_position
+
+
+func _process(delta:float) -> void:
+ base_position = get_speaker_canvas_position()
+
+ var center := get_viewport_rect().size / 2.0
+
+ var dist_x := absf(base_position.x - center.x)
+ var dist_y := absf(base_position.y - center.y)
+ var x_e := center.x - bubble_rect.size.x
+ var y_e := center.y - bubble_rect.size.y
+ var influence_x := remap(clamp(dist_x, x_e, center.x), x_e, center.x * 0.8, 0.0, 1.0)
+ var influence_y := remap(clamp(dist_y, y_e, center.y), y_e, center.y * 0.8, 0.0, 1.0)
+ if base_position.x > center.x: influence_x = -influence_x
+ if base_position.y > center.y: influence_y = -influence_y
+ var edge_influence := Vector2(influence_x, influence_y)
+
+ var direction := (base_direction + edge_influence).normalized()
+
+ var p: Vector2 = base_position + direction * (
+ safe_zone + lerp(bubble_rect.size.y, bubble_rect.size.x, abs(direction.x)) * 0.4
+ )
+ p = p.clamp(bubble_rect.size / 2.0, get_viewport_rect().size - bubble_rect.size / 2.0)
+
+ position = position.lerp(p, 5 * delta)
+
+ var point_a: Vector2 = Vector2.ZERO
+ var point_b: Vector2 = (base_position - position) * 0.75
+
+ var offset: Vector2 = Vector2.from_angle(point_a.angle_to_point(point_b)) * bubble_rect.size * abs(direction.x) * 0.4
+
+ point_a += offset
+ point_b += offset * 0.5
+
+ var curve := Curve2D.new()
+ var direction_point := Vector2(0, (point_b.y - point_a.y))
+ curve.add_point(point_a, Vector2.ZERO, direction_point * 0.5)
+ curve.add_point(point_b)
+ tail.points = curve.tessellate(5)
+ tail.width = bubble_rect.size.x * 0.15
+
+
+func open() -> void:
+ set_process(true)
+ show()
+ text.enabled = true
+ var open_tween := create_tween().set_parallel(true)
+ open_tween.tween_property(self, "scale", Vector2.ONE, 0.1).from(Vector2.ZERO)
+ open_tween.tween_property(self, "modulate:a", 1.0, 0.1).from(0.0)
+
+
+func close() -> void:
+ text.enabled = false
+ var close_tween := create_tween().set_parallel(true)
+ close_tween.tween_property(self, "scale", Vector2.ONE * 0.8, 0.2)
+ close_tween.tween_property(self, "modulate:a", 0.0, 0.2)
+ await close_tween.finished
+ hide()
+ set_process(false)
+
+
+func _on_dialog_text_started_revealing_text() -> void:
+ _resize_bubble(get_base_content_size(), true)
+
+
+func _resize_bubble(content_size:Vector2, popup:=false) -> void:
+ var bubble_size: Vector2 = content_size+(padding*2)+Vector2.ONE*bg_padding
+ var half_size: Vector2= (bubble_size / 2.0)
+ bubble.pivot_offset = half_size
+ bubble_rect = Rect2(position, bubble_size * Vector2(1.1, 1.1))
+ bubble.position = -half_size
+ bubble.size = bubble_size
+
+ text.size = content_size
+ text.position = -(content_size/2.0)
+
+ if popup:
+ var t := create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
+ t.tween_property(bubble, "scale", Vector2.ONE, 0.2).from(Vector2.ZERO)
+ else:
+ bubble.scale = Vector2.ONE
+
+ bubble.material.set(&"shader_parameter/box_size", bubble_size)
+ name_label_holder.position = Vector2(0, bubble.position.y - text.position.y - name_label_holder.size.y/2.0)
+ name_label_holder.position += name_label_offset
+ name_label_holder.alignment = name_label_alignment
+ name_label_holder.size.x = text.size.x
+
+
+func _on_question_shown(info:Dictionary) -> void:
+ if !is_visible_in_tree():
+ return
+
+ await get_tree().process_frame
+
+ var content_size := get_base_content_size()
+ content_size.y += choice_container.size.y
+ content_size.x = max(content_size.x, choice_container.size.x)
+ _resize_bubble(content_size)
+
+
+func get_base_content_size() -> Vector2:
+ var font: Font = text.get_theme_font(&"normal_font")
+ return font.get_multiline_string_size(
+ text.get_parsed_text(),
+ HORIZONTAL_ALIGNMENT_LEFT,
+ max_width,
+ text.get_theme_font_size(&"normal_font_size")
+ )
+
+
+func add_choice_container(node:Container, alignment:=FlowContainer.ALIGNMENT_BEGIN) -> void:
+ if choice_container:
+ choice_container.get_parent().remove_child(choice_container)
+ choice_container.queue_free()
+
+ node.name = "ChoiceContainer"
+ choice_container = node
+ node.set_anchors_preset(LayoutPreset.PRESET_BOTTOM_WIDE)
+ node.grow_vertical = Control.GROW_DIRECTION_BEGIN
+ text.add_child(node)
+
+ if node is HFlowContainer:
+ (node as HFlowContainer).alignment = alignment
+
+ for i:int in range(5):
+ choice_container.add_child(DialogicNode_ChoiceButton.new())
+ if node is HFlowContainer:
+ continue
+ match alignment:
+ HBoxContainer.ALIGNMENT_BEGIN:
+ (choice_container.get_child(-1) as Control).size_flags_horizontal = SIZE_SHRINK_BEGIN
+ HBoxContainer.ALIGNMENT_CENTER:
+ (choice_container.get_child(-1) as Control).size_flags_horizontal = SIZE_SHRINK_CENTER
+ HBoxContainer.ALIGNMENT_END:
+ (choice_container.get_child(-1) as Control).size_flags_horizontal = SIZE_SHRINK_END
+
+ for child:Button in choice_container.get_children():
+ var prev := child.get_parent().get_child(wrap(child.get_index()-1, 0, choice_container.get_child_count()-1)).get_path()
+ var next := child.get_parent().get_child(wrap(child.get_index()+1, 0, choice_container.get_child_count()-1)).get_path()
+ child.focus_next = next
+ child.focus_previous = prev
+ child.focus_neighbor_left = prev
+ child.focus_neighbor_top = prev
+ child.focus_neighbor_right = next
+ child.focus_neighbor_bottom = next
+
+
+func get_speaker_canvas_position() -> Vector2:
+ if is_instance_valid(node_to_point_at):
+ if node_to_point_at is Node3D:
+ base_position = get_viewport().get_camera_3d().unproject_position(
+ (node_to_point_at as Node3D).global_position)
+ if node_to_point_at is CanvasItem:
+ base_position = (node_to_point_at as CanvasItem).get_global_transform_with_canvas().origin
+ return base_position
--- /dev/null
+shader_type canvas_item;
+
+uniform sampler2D deformation_sampler : filter_linear, repeat_enable;
+uniform float radius : hint_range(1.0, 200, 0.01) = 25;
+uniform vec2 box_size = vec2(100, 100);
+uniform float box_padding = 15;
+uniform float wobble_amount : hint_range(0.0, 1.0, 0.01) = 0.2;
+uniform float wobble_speed : hint_range(0.0, 10.0, 0.01) = 1;
+uniform float wobble_detail : hint_range(0.01, 1, 0.01) = 0.5;
+
+void fragment() {
+ float adjusted_radius = min(min(radius, box_size.x/2.0), box_size.y/2.0);
+ vec2 deformation_sample = texture(deformation_sampler, UV*wobble_detail+TIME*wobble_speed*0.05).xy*(vec2(box_padding)/box_size)*0.9;
+ vec2 deformed_UV = UV+((deformation_sample)-vec2(0.5)*vec2(box_padding)/box_size)*wobble_amount;
+ float rounded_box = length(max(abs(deformed_UV*(box_size+vec2(box_padding))-vec2(0.5)*(box_size+vec2(box_padding)))+adjusted_radius-vec2(0.5)*box_size,0))-adjusted_radius;
+ COLOR.a = min(smoothstep(0.0, -1, rounded_box), COLOR.a);
+}
--- /dev/null
+[gd_scene load_steps=11 format=3 uid="uid://dlx7jcvm52tyw"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Textbubble/text_bubble.gd" id="1_jdhpk"]
+[ext_resource type="Shader" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Textbubble/text_bubble.gdshader" id="2_1mhvf"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_dialog_text.gd" id="3_syv35"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_type_sound.gd" id="4_7bm4b"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_name_label.gd" id="6_5gd03"]
+
+[sub_resource type="Curve" id="Curve_0j8nu"]
+_data = [Vector2(0, 1), 0.0, -1.0, 0, 1, Vector2(1, 0), -1.0, 0.0, 1, 0]
+point_count = 2
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_lsfnp"]
+noise_type = 0
+fractal_type = 0
+cellular_jitter = 0.15
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_kr7hw"]
+seamless = true
+noise = SubResource("FastNoiseLite_lsfnp")
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_60xbe"]
+resource_local_to_scene = true
+shader = ExtResource("2_1mhvf")
+shader_parameter/radius = 200.0
+shader_parameter/box_size = Vector2(100, 100)
+shader_parameter/box_padding = 10.0
+shader_parameter/wobble_amount = 0.75
+shader_parameter/wobble_speed = 10.0
+shader_parameter/wobble_detail = 0.51
+shader_parameter/deformation_sampler = SubResource("NoiseTexture2D_kr7hw")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h6ls0"]
+content_margin_left = 5.0
+content_margin_right = 5.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+shadow_color = Color(0.152941, 0.152941, 0.152941, 0.12549)
+shadow_size = 5
+
+[node name="TextBubble" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+script = ExtResource("1_jdhpk")
+
+[node name="Group" type="CanvasGroup" parent="."]
+
+[node name="Tail" type="Line2D" parent="Group"]
+unique_name_in_owner = true
+points = PackedVector2Array(-9, 7, -29, 118, -95, 174, -193, 195)
+width = 96.0
+width_curve = SubResource("Curve_0j8nu")
+
+[node name="Background" type="ColorRect" parent="Group"]
+unique_name_in_owner = true
+material = SubResource("ShaderMaterial_60xbe")
+offset_left = -115.0
+offset_top = -69.0
+offset_right = 108.0
+offset_bottom = 83.0
+mouse_filter = 2
+
+[node name="DialogText" type="RichTextLabel" parent="." node_paths=PackedStringArray("textbox_root")]
+unique_name_in_owner = true
+clip_contents = false
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -53.0
+offset_top = -13.0
+offset_right = 53.0
+offset_bottom = 12.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_colors/default_color = Color(0, 0, 0, 1)
+scroll_active = false
+visible_characters_behavior = 1
+script = ExtResource("3_syv35")
+textbox_root = NodePath("..")
+
+[node name="DialogicNode_TypeSounds" type="AudioStreamPlayer" parent="DialogText"]
+script = ExtResource("4_7bm4b")
+
+[node name="NameLabelPositioner" type="HBoxContainer" parent="DialogText"]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_bottom = 23.0
+grow_horizontal = 2
+alignment = 1
+
+[node name="NameLabelPanel" type="PanelContainer" parent="DialogText/NameLabelPositioner"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_h6ls0")
+
+[node name="NameLabel" type="Label" parent="DialogText/NameLabelPositioner/NameLabelPanel" node_paths=PackedStringArray("name_label_root")]
+unique_name_in_owner = true
+layout_mode = 2
+horizontal_alignment = 1
+script = ExtResource("6_5gd03")
+name_label_root = NodePath("..")
+use_character_color = false
+
+[connection signal="started_revealing_text" from="DialogText" to="." method="_on_dialog_text_started_revealing_text"]
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## This layout won't do anything on its own
+
+@export_group("Main")
+@export_subgroup("Text")
+@export var text_size: int = 15
+@export var text_color: Color = Color.BLACK
+@export_file('*.ttf') var normal_font: String = ""
+@export_file('*.ttf') var bold_font: String = ""
+@export_file('*.ttf') var italic_font: String = ""
+@export_file('*.ttf') var bold_italic_font: String = ""
+@export var text_max_width: int = 300
+
+@export_subgroup('Box')
+@export var box_modulate: Color = Color.WHITE
+@export var box_modulate_by_character_color: bool = false
+@export var box_padding: Vector2 = Vector2(10,10)
+@export_range(1, 999) var box_corner_radius: int = 25
+@export_range(0.1, 5) var box_wobble_speed: float = 1
+@export_range(0, 1) var box_wobble_amount: float = 0.5
+@export_range(0, 1) var box_wobble_detail: float = 0.2
+
+@export_subgroup('Behaviour')
+@export var behaviour_distance: int = 50
+@export var behaviour_direction: Vector2 = Vector2(1, -1)
+
+@export_group('Name Label')
+@export_subgroup("Name Label")
+@export var name_label_enabled: bool = true
+@export var name_label_font_size: int = 15
+@export_file('*.ttf') var name_label_font: String = ""
+@export var name_label_use_character_color: bool = true
+@export var name_label_color: Color = Color.BLACK
+@export_subgroup("Name Label Box")
+@export var name_label_box_modulate: Color = Color.WHITE
+@export var name_label_box_modulate_use_character_color: bool = false
+@export var name_label_padding: Vector2 = Vector2(5,0)
+@export var name_label_offset: Vector2 = Vector2(0,0)
+@export var name_label_alignment := HBoxContainer.ALIGNMENT_BEGIN
+
+
+@export_group('Choices')
+@export_subgroup('Choices Text')
+@export var choices_text_size: int = 15
+@export_file('*.ttf') var choices_text_font: String = ""
+@export var choices_text_color: Color = Color.DARK_SLATE_GRAY
+@export var choices_text_color_hover: Color = Color.DARK_MAGENTA
+@export var choices_text_color_focus: Color = Color.DARK_MAGENTA
+@export var choices_text_color_disabled: Color = Color.DARK_GRAY
+
+@export_subgroup('Choices Layout')
+@export var choices_layout_alignment := FlowContainer.ALIGNMENT_END
+@export var choices_layout_force_lines: bool = false
+@export_file('*.tres', "*.res") var choices_base_theme: String = ""
+
+const TextBubble := preload("res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Textbubble/text_bubble.gd")
+
+var bubbles: Array[TextBubble] = []
+var fallback_bubble: TextBubble = null
+
+const textbubble_scene: PackedScene = preload("res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Textbubble/text_bubble.tscn")
+
+
+func add_bubble() -> TextBubble:
+ var new_bubble: TextBubble = textbubble_scene.instantiate()
+ add_child(new_bubble)
+ bubbles.append(new_bubble)
+ return new_bubble
+
+
+## Called by dialogic whenever export overrides might change
+func _apply_export_overrides() -> void:
+ pass
+
+
+
+## Called by the base layer before opening the bubble
+func bubble_apply_overrides(bubble:TextBubble) -> void:
+ ## TEXT FONT AND COLOR
+ var rtl: RichTextLabel = bubble.text
+ rtl.add_theme_font_size_override(&'normal_font', text_size)
+ rtl.add_theme_font_size_override(&"normal_font_size", text_size)
+ rtl.add_theme_font_size_override(&"bold_font_size", text_size)
+ rtl.add_theme_font_size_override(&"italics_font_size", text_size)
+ rtl.add_theme_font_size_override(&"bold_italics_font_size", text_size)
+
+ rtl.add_theme_color_override(&"default_color", text_color)
+
+ if !normal_font.is_empty():
+ rtl.add_theme_font_override(&"normal_font", load(normal_font) as Font)
+ if !bold_font.is_empty():
+ rtl.add_theme_font_override(&"bold_font", load(bold_font) as Font)
+ if !italic_font.is_empty():
+ rtl.add_theme_font_override(&"italitc_font", load(italic_font) as Font)
+ if !bold_italic_font.is_empty():
+ rtl.add_theme_font_override(&"bold_italics_font", load(bold_italic_font) as Font)
+ bubble.set(&'max_width', text_max_width)
+
+
+ ## BOX & TAIL COLOR
+ var tail_and_bg_group := (bubble.get_node("Group") as CanvasGroup)
+ tail_and_bg_group.self_modulate = box_modulate
+ if box_modulate_by_character_color and bubble.current_character != null:
+ tail_and_bg_group.self_modulate = bubble.current_character.color
+
+ var background := (bubble.get_node('%Background') as ColorRect)
+ var bg_material: ShaderMaterial = (background.material as ShaderMaterial)
+ bg_material.set_shader_parameter(&'radius', box_corner_radius)
+ bg_material.set_shader_parameter(&'wobble_amount', box_wobble_amount)
+ bg_material.set_shader_parameter(&'wobble_speed', box_wobble_speed)
+ bg_material.set_shader_parameter(&'wobble_detail', box_wobble_detail)
+
+ bubble.padding = box_padding
+
+
+ ## BEHAVIOUR
+ bubble.safe_zone = behaviour_distance
+ bubble.base_direction = behaviour_direction
+
+
+ ## NAME LABEL SETTINGS
+ var nl: DialogicNode_NameLabel = bubble.name_label
+ nl.add_theme_font_size_override(&"font_size", name_label_font_size)
+
+ if !name_label_font.is_empty():
+ nl.add_theme_font_override(&'font', load(name_label_font) as Font)
+
+
+ if name_label_use_character_color and bubble.current_character:
+ nl.add_theme_color_override(&"font_color", bubble.current_character.color)
+ else:
+ nl.add_theme_color_override(&"font_color", name_label_color)
+
+ var nlp: PanelContainer = bubble.name_label_box
+ nlp.self_modulate = name_label_box_modulate
+ if name_label_box_modulate_use_character_color and bubble.current_character:
+ nlp.self_modulate = bubble.current_character.color
+ nlp.get_theme_stylebox(&'panel').content_margin_left = name_label_padding.x
+ nlp.get_theme_stylebox(&'panel').content_margin_right = name_label_padding.x
+ nlp.get_theme_stylebox(&'panel').content_margin_top = name_label_padding.y
+ nlp.get_theme_stylebox(&'panel').content_margin_bottom = name_label_padding.y
+ bubble.name_label_offset = name_label_offset
+ bubble.name_label_alignment = name_label_alignment
+
+ nlp.get_parent().visible = name_label_enabled
+
+ ## CHOICE SETTINGS
+ if choices_layout_force_lines:
+ bubble.add_choice_container(VBoxContainer.new(), choices_layout_alignment)
+ else:
+ bubble.add_choice_container(HFlowContainer.new(), choices_layout_alignment)
+
+ var choice_theme: Theme = null
+ if choices_base_theme.is_empty() or not ResourceLoader.exists(choices_base_theme):
+ choice_theme = Theme.new()
+ var base_style := StyleBoxFlat.new()
+ base_style.draw_center = false
+ base_style.border_width_bottom = 2
+ base_style.border_color = choices_text_color
+ choice_theme.set_stylebox(&'normal', &'Button', base_style)
+ var focus_style := (base_style.duplicate() as StyleBoxFlat)
+ focus_style.border_color = choices_text_color_focus
+ choice_theme.set_stylebox(&'focus', &'Button', focus_style)
+ var hover_style := (base_style.duplicate() as StyleBoxFlat)
+ hover_style.border_color = choices_text_color_hover
+ choice_theme.set_stylebox(&'hover', &'Button', hover_style)
+ var disabled_style := (base_style.duplicate() as StyleBoxFlat)
+ disabled_style.border_color = choices_text_color_disabled
+ choice_theme.set_stylebox(&'disabled', &'Button', disabled_style)
+ choice_theme.set_stylebox(&'pressed', &'Button', base_style)
+ else:
+ choice_theme = (load(choices_base_theme) as Theme)
+
+ if !choices_text_font.is_empty():
+ choice_theme.default_font = (load(choices_text_font) as Font)
+
+ choice_theme.set_font_size(&'font_size', &'Button', choices_text_size)
+ choice_theme.set_color(&'font_color', &'Button', choices_text_color)
+ choice_theme.set_color(&'font_pressed_color', &'Button', choices_text_color)
+ choice_theme.set_color(&'font_hover_color', &'Button', choices_text_color_hover)
+ choice_theme.set_color(&'font_focus_color', &'Button', choices_text_color_focus)
+ choice_theme.set_color(&'font_disabled_color', &'Button', choices_text_color_disabled)
+ bubble.choice_container.theme = choice_theme
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://d2it0xiap3gnt"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Textbubble/text_bubble_layer.gd" id="1_b37je"]
+
+[node name="TextBubbleLayer" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+mouse_filter = 2
+script = ExtResource("1_b37je")
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="100" height="100" viewBox="0 0 26.458333 26.458333" version="1.1" id="svg13076" inkscape:export-filename="input_layer_icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" sodipodi:docname="text_bubble_layer_icon.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview13078" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="mm" showgrid="true" inkscape:zoom="5.9184838" inkscape:cx="61.502239" inkscape:cy="68.598651" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
+ <inkscape:grid type="xygrid" id="grid14286" />
+ </sodipodi:namedview>
+ <defs id="defs13073" />
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
+ <rect x="4.0595255" y="7.3477197" width="18.477333" height="10.384831" rx="1.759746" stroke="#ffffff" stroke-width="1.46627" id="rect30126" style="fill:#ffffff;fill-opacity:1" />
+ <rect x="4.4799924" y="3.1119337" width="6.9150147" height="2.1797333" rx="0.65857279" stroke="#ffffff" stroke-width="0.410954" id="rect1570" style="fill:#ffffff;fill-opacity:1" />
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="m 5.8208333,22.489583 c 2.507191,-1.706008 3.833717,-3.283474 4.7624997,-5.291666 l 5.820833,-1e-6 c 0.106809,2.563666 -4.892406,4.003771 -10.5833327,5.291667" id="path1626" sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://bu0tsjabpj4rd"]
+
+[resource]
+content_margin_left = 10.0
+content_margin_top = 5.0
+content_margin_right = 10.0
+content_margin_bottom = 5.0
+bg_color = Color(0, 0, 0, 0.956863)
+draw_center = false
+border_width_left = 5
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+expand_margin_left = 5.0
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://xs2s6euq5stw"]
+
+[resource]
+content_margin_top = 5.0
+content_margin_bottom = 5.0
+bg_color = Color(0, 0, 0, 0.956863)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+expand_margin_left = 1.0
+expand_margin_top = 1.0
+expand_margin_right = 1.0
+expand_margin_bottom = 1.0
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://wrp8f7ard3uu"]
+
+[resource]
+content_margin_left = 10.0
+content_margin_top = 5.0
+content_margin_right = 10.0
+content_margin_bottom = 5.0
+bg_color = Color(0, 0, 0, 0.941176)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="100" height="100" viewBox="0 0 26.458333 26.458333" version="1.1" id="svg13076" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs13073" />
+ <g id="layer1">
+ <rect style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" id="rect19792" width="13.229166" height="2.6458333" x="6.614583" y="6.614583" ry="0.050781649" />
+ <rect style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" id="rect19794" width="13.229166" height="2.6458333" x="6.614583" y="11.90625" ry="0.050781649" />
+ <rect style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" id="rect19796" width="13.229166" height="2.6458333" x="6.614583" y="17.197916" ry="0.050781649" />
+ </g>
+</svg>
--- /dev/null
+[style]
+type = "Layer"
+name = "Centered Choices"
+author = "Dialogic"
+description = "A layer containing simple centered choices."
+scene = "vn_choice_layer.tscn"
+icon = "choices_layer_icon.svg"
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## A layer that allows showing up to 10 choices.
+## Choices are positioned in the center of the screen.
+
+@export_group("Text")
+@export_subgroup('Font')
+@export var font_use_global: bool = true
+@export_file('*.ttf', '*.tres') var font_custom: String = ""
+@export_subgroup('Size')
+@export var font_size_use_global: bool = true
+@export var font_size_custom: int = 16
+@export_subgroup('Color')
+@export var text_color_use_global: bool = true
+@export var text_color_custom: Color = Color.WHITE
+@export var text_color_pressed: Color = Color.WHITE
+@export var text_color_hovered: Color = Color.GRAY
+@export var text_color_disabled: Color = Color.DARK_GRAY
+@export var text_color_focused: Color = Color.WHITE
+
+@export_group('Boxes')
+@export_subgroup('Panels')
+@export_file('*.tres') var boxes_stylebox_normal: String = "res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Choices/choice_panel_normal.tres"
+@export_file('*.tres') var boxes_stylebox_hovered: String = "res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Choices/choice_panel_hover.tres"
+@export_file('*.tres') var boxes_stylebox_pressed: String = ""
+@export_file('*.tres') var boxes_stylebox_disabled: String = ""
+@export_file('*.tres') var boxes_stylebox_focused: String = "res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Choices/choice_panel_focus.tres"
+@export_subgroup('Modulate')
+@export_subgroup('Size & Position')
+@export var boxes_v_separation: int = 10
+@export var boxes_fill_width: bool = true
+@export var boxes_min_size: Vector2 = Vector2()
+@export var boxes_offset: Vector2 = Vector2()
+
+@export_group('Sounds')
+@export_range(-80, 24, 0.01) var sounds_volume: float = -10
+@export_file("*.wav", "*.ogg", "*.mp3") var sounds_pressed: String = "res://addons/dialogic/Example Assets/sound-effects/typing1.wav"
+@export_file("*.wav", "*.ogg", "*.mp3") var sounds_hover: String = "res://addons/dialogic/Example Assets/sound-effects/typing2.wav"
+@export_file("*.wav", "*.ogg", "*.mp3") var sounds_focus: String = "res://addons/dialogic/Example Assets/sound-effects/typing4.wav"
+
+func get_choices() -> VBoxContainer:
+ return $Choices
+
+
+func get_button_sound() -> DialogicNode_ButtonSound:
+ return %DialogicNode_ButtonSound
+
+
+## Method that applies all exported settings
+func _apply_export_overrides() -> void:
+ # apply text settings
+ var layer_theme: Theme = Theme.new()
+
+ # font
+ if font_use_global and get_global_setting(&'font', false):
+ layer_theme.set_font(&'font', &'Button', load(get_global_setting(&'font', '') as String) as Font)
+ elif ResourceLoader.exists(font_custom):
+ layer_theme.set_font(&'font', &'Button', load(font_custom) as Font)
+
+ # font size
+ if font_size_use_global:
+ layer_theme.set_font_size(&'font_size', &'Button', get_global_setting(&'font_size', font_size_custom) as int)
+ else:
+ layer_theme.set_font_size(&'font_size', &'Button', font_size_custom)
+
+ # font color
+ if text_color_use_global:
+ layer_theme.set_color(&'font_color', &'Button', get_global_setting(&'font_color', text_color_custom) as Color)
+ else:
+ layer_theme.set_color(&'font_color', &'Button', text_color_custom)
+
+ layer_theme.set_color(&'font_pressed_color', &'Button', text_color_pressed)
+ layer_theme.set_color(&'font_hover_color', &'Button', text_color_hovered)
+ layer_theme.set_color(&'font_disabled_color', &'Button', text_color_disabled)
+ layer_theme.set_color(&'font_pressed_color', &'Button', text_color_pressed)
+ layer_theme.set_color(&'font_focus_color', &'Button', text_color_focused)
+
+
+ # apply box settings
+ if ResourceLoader.exists(boxes_stylebox_normal):
+ var style_box: StyleBox = load(boxes_stylebox_normal)
+ layer_theme.set_stylebox(&'normal', &'Button', style_box)
+ layer_theme.set_stylebox(&'hover', &'Button', style_box)
+ layer_theme.set_stylebox(&'pressed', &'Button', style_box)
+ layer_theme.set_stylebox(&'disabled', &'Button', style_box)
+ layer_theme.set_stylebox(&'focus', &'Button', style_box)
+
+ if ResourceLoader.exists(boxes_stylebox_hovered):
+ layer_theme.set_stylebox(&'hover', &'Button', load(boxes_stylebox_hovered) as StyleBox)
+
+ if ResourceLoader.exists(boxes_stylebox_pressed):
+ layer_theme.set_stylebox(&'pressed', &'Button', load(boxes_stylebox_pressed) as StyleBox)
+ if ResourceLoader.exists(boxes_stylebox_disabled):
+ layer_theme.set_stylebox(&'disabled', &'Button', load(boxes_stylebox_disabled) as StyleBox)
+ if ResourceLoader.exists(boxes_stylebox_focused):
+ layer_theme.set_stylebox(&'focus', &'Button', load(boxes_stylebox_focused) as StyleBox)
+
+ get_choices().add_theme_constant_override(&"separation", boxes_v_separation)
+ self.position = boxes_offset
+
+ for child: Node in get_choices().get_children():
+ if not child is DialogicNode_ChoiceButton:
+ continue
+ var choice: DialogicNode_ChoiceButton = child as DialogicNode_ChoiceButton
+
+ if boxes_fill_width:
+ choice.size_flags_horizontal = Control.SIZE_FILL
+ else:
+ choice.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
+
+ choice.custom_minimum_size = boxes_min_size
+
+
+ set(&'theme', layer_theme)
+
+ # apply sound settings
+ var button_sound: DialogicNode_ButtonSound = get_button_sound()
+ button_sound.volume_db = sounds_volume
+ button_sound.sound_pressed = load(sounds_pressed)
+ button_sound.sound_hover = load(sounds_hover)
+ button_sound.sound_focus = load(sounds_focus)
--- /dev/null
+[gd_scene load_steps=7 format=3 uid="uid://dhk6j6eb6e3q"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Choices/vn_choice_layer.gd" id="1_kurgw"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Choice/node_choice_button.gd" id="1_w632k"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Choice/node_button_sound.gd" id="2_mgko6"]
+[ext_resource type="AudioStream" uid="uid://b6c1p14bc20p1" path="res://addons/dialogic/Example Assets/sound-effects/typing1.wav" id="3_mql8i"]
+[ext_resource type="AudioStream" uid="uid://c2viukvbub6v6" path="res://addons/dialogic/Example Assets/sound-effects/typing4.wav" id="4_420fr"]
+
+[sub_resource type="AudioStream" id="AudioStream_pe27w"]
+
+[node name="VN_ChoiceLayer" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_kurgw")
+
+[node name="Choices" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -41.0
+offset_top = -47.0
+offset_right = 42.0
+offset_bottom = 47.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+alignment = 1
+metadata/_edit_layout_mode = 1
+
+[node name="DialogicNode_ChoiceButton1" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton2" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton3" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton4" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton5" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton6" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton7" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton8" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton9" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton10" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ChoiceButton11" type="Button" parent="Choices"]
+layout_mode = 2
+text = "Some text"
+script = ExtResource("1_w632k")
+
+[node name="DialogicNode_ButtonSound" type="AudioStreamPlayer" parent="Choices"]
+unique_name_in_owner = true
+script = ExtResource("2_mgko6")
+sound_pressed = ExtResource("3_mql8i")
+sound_hover = ExtResource("4_420fr")
+sound_focus = SubResource("AudioStream_pe27w")
--- /dev/null
+[style]
+type = "Layer"
+name = "5 Portraits"
+author = "Dialogic"
+description = "A layer with 5 portrait position containers."
+scene = "vn_portrait_layer.tscn"
+icon = "portrait_layer_icon.svg"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1"
+ id="svg13076" inkscape:export-filename="portrait_layre.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" inkscape:export-ydpi="96" inkscape:export-xdpi="96" inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px"
+ viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
+<sodipodi:namedview id="namedview13078" inkscape:pagecheckerboard="0" inkscape:cy="64.755078" inkscape:zoom="4.185" inkscape:cx="70.728793" pagecolor="#505050" showgrid="true" borderopacity="1" bordercolor="#eeeeee" inkscape:document-units="mm" inkscape:deskcolor="#505050" inkscape:pageopacity="0" inkscape:showpageshadow="0" inkscape:current-layer="layer1" inkscape:window-y="-8" inkscape:window-x="-8" inkscape:window-height="1017" inkscape:window-width="1920" inkscape:window-maximized="1">
+ <inkscape:grid id="grid14286" type="xygrid"></inkscape:grid>
+</sodipodi:namedview>
+<g>
+ <ellipse id="path23638" fill="#FFFFFF" cx="18.793" cy="33.473" rx="13.682" ry="14.261"/>
+ <path id="path23636" sodipodi:nodetypes="ccccc" fill="#FFFFFF" d="M6.903,80.789c0-11.847,0-23.693,4.756-35.539h14.268
+ c4.756,11.846,4.756,23.692,4.756,35.539H6.903"/>
+ <ellipse id="ellipse23642" fill="#FFFFFF" cx="49.999" cy="33.473" rx="13.683" ry="14.261"/>
+ <path id="path23640" sodipodi:nodetypes="ccccc" fill="#FFFFFF" d="M38.108,80.789c0-11.847,0-23.693,4.756-35.539h14.268
+ c4.756,11.846,4.756,23.692,4.756,35.539H38.108"/>
+ <path id="path23644" sodipodi:nodetypes="ccccc" fill="#FFFFFF" d="M69.316,80.789c0-11.847,0-23.693,4.756-35.539H88.34
+ c4.757,11.846,4.757,23.692,4.757,35.539H69.316"/>
+ <ellipse id="ellipse23646" fill="#FFFFFF" cx="81.206" cy="33.473" rx="13.683" ry="14.261"/>
+</g>
+</svg>
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+
+## A layer that allows showing 5 portraits, like in a visual novel.
+
+## The canvas layer that the portraits are on.
+@export var portrait_size_mode: DialogicNode_PortraitContainer.SizeModes = DialogicNode_PortraitContainer.SizeModes.FIT_SCALE_HEIGHT
+
+
+func _apply_export_overrides() -> void:
+ # apply portrait size
+ for child: DialogicNode_PortraitContainer in %Portraits.get_children():
+ child.size_mode = portrait_size_mode
+ child.update_portrait_transforms()
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://cy1y14inwkplb"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Portraits/vn_portrait_layer.gd" id="1_1i7em"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Character/node_portrait_container.gd" id="1_rxdcc"]
+
+[node name="VN_PortraitLayer" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_1i7em")
+
+[node name="Portraits" type="Control" parent="."]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+
+[node name="DialogicNode_PortraitContainer1" type="Control" parent="Portraits"]
+layout_mode = 1
+anchor_right = 0.2
+anchor_bottom = 1.0
+offset_right = -1.52588e-05
+grow_vertical = 2
+pivot_offset = Vector2(115.2, 648)
+mouse_filter = 2
+script = ExtResource("1_rxdcc")
+container_ids = PackedStringArray("leftmost", "0")
+metadata/_edit_use_anchors_ = true
+
+[node name="DialogicNode_PortraitContainer2" type="Control" parent="Portraits"]
+layout_mode = 1
+anchor_left = 0.2
+anchor_right = 0.4
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_rxdcc")
+container_ids = PackedStringArray("left", "1")
+metadata/_edit_use_anchors_ = true
+
+[node name="DialogicNode_PortraitContainer3" type="Control" parent="Portraits"]
+layout_mode = 1
+anchor_left = 0.4
+anchor_right = 0.6
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_rxdcc")
+container_ids = PackedStringArray("center", "middle", "2")
+metadata/_edit_use_anchors_ = true
+
+[node name="DialogicNode_PortraitContainer4" type="Control" parent="Portraits"]
+layout_mode = 1
+anchor_left = 0.6
+anchor_right = 0.8
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_rxdcc")
+container_ids = PackedStringArray("right", "3")
+metadata/_edit_use_anchors_ = true
+
+[node name="DialogicNode_PortraitContainer5" type="Control" parent="Portraits"]
+layout_mode = 1
+anchor_left = 0.8
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_rxdcc")
+container_ids = PackedStringArray("rightmost", "4")
+metadata/_edit_use_anchors_ = true
--- /dev/null
+extends AnimationPlayer
+
+## A custom script/node that adds some animations to the textbox.
+
+# Careful: Sync these with the ones in the root script!
+enum AnimationsIn {NONE, POP_IN, FADE_UP}
+enum AnimationsOut {NONE, POP_OUT, FADE_DOWN}
+enum AnimationsNewText {NONE, WIGGLE}
+
+var animation_in: AnimationsIn
+var animation_out: AnimationsOut
+var animation_new_text: AnimationsNewText
+
+var full_clear := true
+
+
+func get_text_panel() -> PanelContainer:
+ return %DialogTextPanel
+
+
+func get_dialog() -> DialogicNode_DialogText:
+ return %DialogicNode_DialogText
+
+
+func _ready() -> void:
+ var text_system: Node = DialogicUtil.autoload().get(&'Text')
+ text_system.connect(&'animation_textbox_hide', _on_textbox_hide)
+ text_system.connect(&'animation_textbox_show', _on_textbox_show)
+ text_system.connect(&'animation_textbox_new_text', _on_textbox_new_text)
+ text_system.connect(&'about_to_show_text', _on_about_to_show_text)
+ var animation_system: Node = DialogicUtil.autoload().get(&'Animations')
+ animation_system.connect(&'animation_interrupted', _on_animation_interrupted)
+
+
+func _on_textbox_show() -> void:
+ if animation_in == AnimationsIn.NONE:
+ return
+ play('RESET')
+ var animation_system: Node = DialogicUtil.autoload().get(&'Animations')
+ animation_system.call(&'start_animating')
+ get_text_panel().get_parent().get_parent().set(&'modulate', Color.TRANSPARENT)
+ get_dialog().text = ""
+ match animation_in:
+ AnimationsIn.POP_IN:
+ play("textbox_pop")
+ AnimationsIn.FADE_UP:
+ play("textbox_fade_up")
+ if not animation_finished.is_connected(Callable(animation_system, &'animation_finished')):
+ animation_finished.connect(Callable(animation_system, &'animation_finished'), CONNECT_ONE_SHOT)
+
+
+func _on_textbox_hide() -> void:
+ if animation_out == AnimationsOut.NONE:
+ return
+ play('RESET')
+ var animation_system: Node = DialogicUtil.autoload().get(&'Animations')
+ animation_system.call(&'start_animating')
+ match animation_out:
+ AnimationsOut.POP_OUT:
+ play_backwards("textbox_pop")
+ AnimationsOut.FADE_DOWN:
+ play_backwards("textbox_fade_up")
+
+ if not animation_finished.is_connected(Callable(animation_system, &'animation_finished')):
+ animation_finished.connect(Callable(animation_system, &'animation_finished'), CONNECT_ONE_SHOT)
+
+
+func _on_about_to_show_text(info:Dictionary) -> void:
+ full_clear = !info.append
+
+
+func _on_textbox_new_text() -> void:
+ if DialogicUtil.autoload().Inputs.auto_skip.enabled:
+ return
+
+ if animation_new_text == AnimationsNewText.NONE:
+ return
+
+ var animation_system: Node = DialogicUtil.autoload().get(&'Animations')
+ animation_system.call(&'start_animating')
+ if full_clear:
+ get_dialog().text = ""
+ match animation_new_text:
+ AnimationsNewText.WIGGLE:
+ play("new_text")
+
+ if not animation_finished.is_connected(Callable(animation_system, &'animation_finished')):
+ animation_finished.connect(Callable(animation_system, &'animation_finished'), CONNECT_ONE_SHOT)
+
+
+func _on_animation_interrupted() -> void:
+ if is_playing():
+ stop()
--- /dev/null
+extends Range
+
+var enabled: bool = true
+
+func _process(_delta : float) -> void:
+ if !enabled:
+ hide()
+ return
+ if DialogicUtil.autoload().Inputs.auto_advance.get_progress() < 0:
+ hide()
+ else:
+ show()
+ value = DialogicUtil.autoload().Inputs.auto_advance.get_progress()
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="6.4715624mm" height="6.4715624mm" viewBox="0 0 6.4715624 6.4715622" version="1.1" id="svg5" inkscape:export-filename="next.svg" inkscape:export-xdpi="17.054285" inkscape:export-ydpi="17.054285" sodipodi:docname="next.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="mm" showgrid="true" inkscape:zoom="8.4359982" inkscape:cx="-7.0531072" inkscape:cy="10.312947" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
+ <inkscape:grid type="xygrid" id="grid2291" originx="-1.8058334" originy="-1.8059061" />
+ </sodipodi:namedview>
+ <defs id="defs2" />
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-3.3788024,-4.701698)">
+ <path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.18;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2;stroke-dasharray:none;stroke-dashoffset:0" d="M 4.4833857,5.2770419 6.6000523,10.568709 8.716719,5.2770419 c -2.6603643,0.2499583 -1.6020309,0.2499583 -4.2333333,0 z" id="path2289" sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
--- /dev/null
+[style]
+type = "Layer"
+name = "Visual Novel Textbox"
+author = "Dialogic"
+description = "A textbox in a VN style."
+scene = "vn_textbox_layer.tscn"
+icon = "textbox_layer_icon.svg"
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="100" height="100" viewBox="0 0 26.458333 26.458333" version="1.1" id="svg13076" inkscape:export-filename="bitmap.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview13078" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="mm" showgrid="true">
+ <inkscape:grid type="xygrid" id="grid14286" />
+ </sodipodi:namedview>
+ <defs id="defs13073" />
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
+ <path id="rect14391" style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2" d="M 2.6966149,11.90625 H 23.761718 c 0.02813,0 0.05078,0.02265 0.05078,0.05078 v 10.481769 c 0,0.02813 -0.02265,0.05078 -0.05078,0.05078 H 2.6966149 c -0.028133,0 -0.050782,-0.02265 -0.050782,-0.05078 V 11.957032 c 0,-0.02813 0.022649,-0.05078 0.050782,-0.05078 z" />
+ <rect style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.88;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2;stroke-dasharray:none" id="rect14393" width="5.2916665" height="1.3229166" x="2.6458333" y="7.9375" ry="0.050781649" />
+ </g>
+</svg>
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://dkv1pl1c1dq6"]
+
+[resource]
+content_margin_left = 15.0
+content_margin_top = 15.0
+content_margin_right = 15.0
+content_margin_bottom = 15.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
--- /dev/null
+@tool
+extends DialogicLayoutLayer
+## This layer's scene file contains following nodes:
+## - a dialog_text node
+## - a name_label node
+## - a next_indicator node
+## - a type_sound node
+##
+## As well as custom:
+## - animations
+## - auto-advance progress indicator
+##
+## If you want to customize this layer, here is a little rundown of this layer:
+## The Layer Settings are divided into the `@export_group`s below.
+## They get applied in [method _apply_export_overrides].
+## Each `@export_group` has its own method to apply the settings to the scene.
+## If you want to change a specific part inside the scene, you can simply
+## remove or add # (commenting) to the method line.
+
+
+
+enum Alignments {LEFT, CENTER, RIGHT}
+
+enum AnimationsIn {NONE, POP_IN, FADE_UP}
+enum AnimationsOut {NONE, POP_OUT, FADE_DOWN}
+enum AnimationsNewText {NONE, WIGGLE}
+
+@export_group("Text")
+
+@export_subgroup("Alignment & Size")
+@export var text_alignment: Alignments= Alignments.LEFT
+@export var text_use_global_size: bool = true
+@export var text_size: int = 15
+
+@export_subgroup("Color")
+@export var text_use_global_color: bool = true
+@export var text_custom_color: Color = Color.WHITE
+
+@export_subgroup('Font')
+@export var text_use_global_font: bool = true
+@export_file('*.ttf', '*.tres') var normal_font: String = ""
+@export_file('*.ttf', '*.tres') var bold_font: String = ""
+@export_file('*.ttf', '*.tres') var italics_font: String = ""
+@export_file('*.ttf', '*.tres') var bold_italics_font: String = ""
+
+
+@export_group("Box")
+
+@export_subgroup("Panel")
+@export_file("*.tres") var box_panel: String = this_folder.path_join("vn_textbox_default_panel.tres")
+
+@export_subgroup("Color")
+@export var box_color_use_global: bool = true
+@export var box_color_custom: Color = Color.BLACK
+
+@export_subgroup("Size & Position")
+@export var box_size: Vector2 = Vector2(550, 110)
+@export var box_margin_bottom: int = 15
+
+@export_subgroup("Animation")
+@export var box_animation_in: AnimationsIn = AnimationsIn.FADE_UP
+@export var box_animation_out: AnimationsOut = AnimationsOut.FADE_DOWN
+@export var box_animation_new_text: AnimationsNewText = AnimationsNewText.NONE
+
+
+@export_group("Name Label")
+
+@export_subgroup('Color')
+@export var name_label_use_global_color: bool= true
+@export var name_label_use_character_color: bool = true
+@export var name_label_custom_color: Color = Color.WHITE
+
+@export_subgroup('Font')
+@export var name_label_use_global_font: bool = true
+@export_file('*.ttf', '*.tres') var name_label_font: String = ""
+@export var name_label_use_global_font_size: bool = true
+@export var name_label_custom_font_size: int = 15
+
+@export_subgroup('Box')
+@export_file("*.tres") var name_label_box_panel: String = this_folder.path_join("vn_textbox_name_label_panel.tres")
+@export var name_label_box_use_global_color: bool = true
+@export var name_label_box_modulate: Color = box_color_custom
+
+@export_subgroup('Alignment')
+@export var name_label_alignment: Alignments = Alignments.LEFT
+@export var name_label_box_offset: Vector2 = Vector2.ZERO
+
+
+@export_group("Indicators")
+
+@export_subgroup("Next Indicator")
+@export var next_indicator_enabled: bool = true
+@export var next_indicator_show_on_questions: bool = true
+@export var next_indicator_show_on_autoadvance: bool = false
+@export_enum('bounce', 'blink', 'none') var next_indicator_animation: int = 0
+@export_file("*.png","*.svg","*.tres") var next_indicator_texture: String = ''
+@export var next_indicator_size: Vector2 = Vector2(25,25)
+
+@export_subgroup("Autoadvance")
+@export var autoadvance_progressbar: bool = true
+
+
+@export_group('Sounds')
+
+@export_subgroup('Typing Sounds')
+@export var typing_sounds_enabled: bool = true
+@export var typing_sounds_mode: DialogicNode_TypeSounds.Modes = DialogicNode_TypeSounds.Modes.INTERRUPT
+@export_dir var typing_sounds_sounds_folder: String = "res://addons/dialogic/Example Assets/sound-effects/"
+@export_file("*.wav", "*.ogg", "*.mp3") var typing_sounds_end_sound: String = ""
+@export_range(1, 999, 1) var typing_sounds_every_nths_character: int = 1
+@export_range(0.01, 4, 0.01) var typing_sounds_pitch: float = 1.0
+@export_range(0.0, 3.0) var typing_sounds_pitch_variance: float = 0.0
+@export_range(-80, 24, 0.01) var typing_sounds_volume: float = -10
+@export_range(0.0, 10) var typing_sounds_volume_variance: float = 0.0
+@export var typing_sounds_ignore_characters: String = " .,!?"
+
+
+func _apply_export_overrides() -> void:
+ if !is_inside_tree():
+ await ready
+
+ ## FONT SETTINGS
+ _apply_text_settings()
+
+
+ ## BOX SETTINGS
+ _apply_box_settings()
+
+ ## BOX ANIMATIONS
+ _apply_box_animations_settings()
+
+ ## NAME LABEL SETTINGS
+ _apply_name_label_settings()
+
+ ## NEXT INDICATOR SETTINGS
+ _apply_indicator_settings()
+
+ ## OTHER
+ var progress_bar: ProgressBar = %AutoAdvanceProgressbar
+ progress_bar.set(&'enabled', autoadvance_progressbar)
+
+ #### SOUNDS
+
+ ## TYPING SOUNDS
+ _apply_sounds_settings()
+
+
+## Applies all text box settings to the scene.
+## Except the box animations.
+func _apply_box_settings() -> void:
+ var dialog_text_panel: PanelContainer = %DialogTextPanel
+ if ResourceLoader.exists(box_panel):
+ dialog_text_panel.add_theme_stylebox_override(&'panel', load(box_panel) as StyleBox)
+
+ if box_color_use_global:
+ dialog_text_panel.self_modulate = get_global_setting(&'bg_color', box_color_custom)
+ else:
+ dialog_text_panel.self_modulate = box_color_custom
+
+ var sizer: Control = %Sizer
+ sizer.size = box_size
+ sizer.position = box_size * Vector2(-0.5, -1)+Vector2(0, -box_margin_bottom)
+
+
+## Applies box animations settings to the scene.
+func _apply_box_animations_settings() -> void:
+ var animations: AnimationPlayer = %Animations
+ animations.set(&'animation_in', box_animation_in)
+ animations.set(&'animation_out', box_animation_out)
+ animations.set(&'animation_new_text', box_animation_new_text)
+
+
+## Applies all name label settings to the scene.
+func _apply_name_label_settings() -> void:
+ var name_label: DialogicNode_NameLabel = %DialogicNode_NameLabel
+
+ if name_label_use_global_font_size:
+ name_label.add_theme_font_size_override(&"font_size", get_global_setting(&'font_size', name_label_custom_font_size) as int)
+ else:
+ name_label.add_theme_font_size_override(&"font_size", name_label_custom_font_size)
+
+ if name_label_use_global_font and get_global_setting(&'font', false):
+ name_label.add_theme_font_override(&'font', load(get_global_setting(&'font', '') as String) as Font)
+ elif not name_label_font.is_empty():
+ name_label.add_theme_font_override(&'font', load(name_label_font) as Font)
+
+ if name_label_use_global_color:
+ name_label.add_theme_color_override(&"font_color", get_global_setting(&'font_color', name_label_custom_color) as Color)
+ else:
+ name_label.add_theme_color_override(&"font_color", name_label_custom_color)
+
+ name_label.use_character_color = name_label_use_character_color
+
+ var name_label_panel: PanelContainer = %NameLabelPanel
+ if ResourceLoader.exists(name_label_box_panel):
+ name_label_panel.add_theme_stylebox_override(&'panel', load(name_label_box_panel) as StyleBox)
+ else:
+ name_label_panel.add_theme_stylebox_override(&'panel', load(this_folder.path_join("vn_textbox_name_label_panel.tres")) as StyleBox)
+
+ if name_label_box_use_global_color:
+ name_label_panel.self_modulate = get_global_setting(&'bg_color', name_label_box_modulate)
+ else:
+ name_label_panel.self_modulate = name_label_box_modulate
+ var dialog_text_panel: PanelContainer = %DialogTextPanel
+ name_label_panel.position = name_label_box_offset+Vector2(0, -40)
+ name_label_panel.position -= Vector2(
+ dialog_text_panel.get_theme_stylebox(&'panel', &'PanelContainer').content_margin_left,
+ dialog_text_panel.get_theme_stylebox(&'panel', &'PanelContainer').content_margin_top)
+ name_label_panel.anchor_left = name_label_alignment/2.0
+ name_label_panel.anchor_right = name_label_alignment/2.0
+ name_label_panel.grow_horizontal = [1, 2, 0][name_label_alignment]
+
+
+## Applies all text settings to the scene.
+func _apply_text_settings() -> void:
+ var dialog_text: DialogicNode_DialogText = %DialogicNode_DialogText
+ dialog_text.alignment = text_alignment as DialogicNode_DialogText.Alignment
+
+ if text_use_global_size:
+ text_size = get_global_setting(&'font_size', text_size)
+ dialog_text.add_theme_font_size_override(&"normal_font_size", text_size)
+ dialog_text.add_theme_font_size_override(&"bold_font_size", text_size)
+ dialog_text.add_theme_font_size_override(&"italics_font_size", text_size)
+ dialog_text.add_theme_font_size_override(&"bold_italics_font_size", text_size)
+
+ if text_use_global_color:
+ dialog_text.add_theme_color_override(&"default_color", get_global_setting(&'font_color', text_custom_color) as Color)
+ else:
+ dialog_text.add_theme_color_override(&"default_color", text_custom_color)
+
+ if text_use_global_font and get_global_setting(&'font', false):
+ dialog_text.add_theme_font_override(&"normal_font", load(get_global_setting(&'font', '') as String) as Font)
+ elif !normal_font.is_empty():
+ dialog_text.add_theme_font_override(&"normal_font", load(normal_font) as Font)
+ if !bold_font.is_empty():
+ dialog_text.add_theme_font_override(&"bold_font", load(bold_font) as Font)
+ if !italics_font.is_empty():
+ dialog_text.add_theme_font_override(&"italics_font", load(italics_font) as Font)
+ if !bold_italics_font.is_empty():
+ dialog_text.add_theme_font_override(&"bold_italics_font", load(bold_italics_font) as Font)
+
+
+## Applies all indicator settings to the scene.
+func _apply_indicator_settings() -> void:
+ var next_indicator: DialogicNode_NextIndicator = %NextIndicator
+ next_indicator.enabled = next_indicator_enabled
+
+ if next_indicator_enabled:
+ next_indicator.animation = next_indicator_animation as DialogicNode_NextIndicator.Animations
+ if ResourceLoader.exists(next_indicator_texture):
+ next_indicator.texture = load(next_indicator_texture)
+ next_indicator.show_on_questions = next_indicator_show_on_questions
+ next_indicator.show_on_autoadvance = next_indicator_show_on_autoadvance
+ next_indicator.texture_size = next_indicator_size
+
+
+## Applies all sound settings to the scene.
+func _apply_sounds_settings() -> void:
+ var type_sounds: DialogicNode_TypeSounds = %DialogicNode_TypeSounds
+ type_sounds.enabled = typing_sounds_enabled
+ type_sounds.mode = typing_sounds_mode
+
+ if not typing_sounds_sounds_folder.is_empty():
+ type_sounds.sounds = DialogicNode_TypeSounds.load_sounds_from_path(typing_sounds_sounds_folder)
+ else:
+ type_sounds.sounds.clear()
+
+ if not typing_sounds_end_sound.is_empty():
+ type_sounds.end_sound = load(typing_sounds_end_sound)
+ else:
+ type_sounds.end_sound = null
+
+ type_sounds.play_every_character = typing_sounds_every_nths_character
+ type_sounds.base_pitch = typing_sounds_pitch
+ type_sounds.base_volume = typing_sounds_volume
+ type_sounds.pitch_variance = typing_sounds_pitch_variance
+ type_sounds.volume_variance = typing_sounds_volume_variance
+ type_sounds.ignore_characters = typing_sounds_ignore_characters
--- /dev/null
+[gd_scene load_steps=17 format=3 uid="uid://bquja8jyk8kbr"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/vn_textbox_layer.gd" id="1_bpydr"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/animations.gd" id="2_xy7a2"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_dialog_text.gd" id="3_4634k"]
+[ext_resource type="StyleBox" uid="uid://dkv1pl1c1dq6" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/vn_textbox_default_panel.tres" id="3_ssa84"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_type_sound.gd" id="4_ma5mw"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_next_indicator.gd" id="5_40a50"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/autoadvance_indicator.gd" id="6_07xym"]
+[ext_resource type="Texture2D" uid="uid://b0rpqfg4fhebk" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/next.svg" id="6_uch03"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_name_label.gd" id="7_bi7sh"]
+[ext_resource type="StyleBox" uid="uid://m7gyepkysu83" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/vn_textbox_name_label_panel.tres" id="9_yg8ig"]
+
+[sub_resource type="Animation" id="Animation_au0a2"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Anchor/AnimationParent:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(0, 0)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Anchor/AnimationParent:rotation")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [0.0]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Anchor/AnimationParent:scale")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(1, 1)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Anchor/AnimationParent:modulate")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Color(1, 1, 1, 1)]
+}
+tracks/4/type = "bezier"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("Anchor/AnimationParent/Sizer/DialogTextPanel:rotation")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+
+[sub_resource type="Animation" id="Animation_6kbwc"]
+resource_name = "new_text"
+length = 0.4
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Anchor/AnimationParent/Sizer/DialogTextPanel:rotation")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(3, 3, 3, 3, 3),
+"points": PackedFloat32Array(0, -0.025, 0, 0.025, 0, 0.005, -0.025, 0, 0.025, 0, -0.005, -0.025, 0, 0.025, 0, 0.005, -0.025, 0, 0.025, 0, 0, -0.025, 0, 0.025, 0),
+"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4)
+}
+
+[sub_resource type="Animation" id="Animation_g6k55"]
+resource_name = "textbox_fade_up"
+length = 0.7
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Anchor/AnimationParent:position")
+tracks/0/interp = 2
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.3, 0.7),
+"transitions": PackedFloat32Array(1, 1, 1),
+"update": 0,
+"values": [Vector2(0, 50), Vector2(0, 19.6793), Vector2(0, 0)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Anchor/AnimationParent:modulate")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0.1, 0.6),
+"transitions": PackedFloat32Array(1, 1),
+"update": 0,
+"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Anchor/AnimationParent:rotation")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [0.0]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Anchor/AnimationParent:scale")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(1, 1)]
+}
+
+[sub_resource type="Animation" id="Animation_htbgc"]
+resource_name = "textbox_pop"
+length = 0.3
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Anchor/AnimationParent:position")
+tracks/0/interp = 2
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(0, 0)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Anchor/AnimationParent:rotation")
+tracks/1/interp = 2
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0, 0.2, 0.3),
+"transitions": PackedFloat32Array(1, 1, 1),
+"update": 0,
+"values": [-0.0899883, 0.0258223, 0.0]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Anchor/AnimationParent:scale")
+tracks/2/interp = 2
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0, 0.2, 0.3),
+"transitions": PackedFloat32Array(1, 1, 1),
+"update": 0,
+"values": [Vector2(0.793957, 0.778082), Vector2(0.937299, 1.14248), Vector2(1, 1)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Anchor/AnimationParent:modulate")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0, 0.3),
+"transitions": PackedFloat32Array(1, 1),
+"update": 0,
+"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_c14kh"]
+_data = {
+"RESET": SubResource("Animation_au0a2"),
+"new_text": SubResource("Animation_6kbwc"),
+"textbox_fade_up": SubResource("Animation_g6k55"),
+"textbox_pop": SubResource("Animation_htbgc")
+}
+
+[sub_resource type="FontVariation" id="FontVariation_v8y64"]
+
+[node name="VN_TextboxLayer" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_bpydr")
+box_panel = "res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/vn_textbox_default_panel.tres"
+box_size = Vector2(550, 150)
+name_label_box_panel = "res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/vn_textbox_name_label_panel.tres"
+name_label_box_modulate = Color(0, 0, 0, 1)
+
+[node name="Animations" type="AnimationPlayer" parent="."]
+unique_name_in_owner = true
+libraries = {
+"": SubResource("AnimationLibrary_c14kh")
+}
+autoplay = "RESET"
+script = ExtResource("2_xy7a2")
+
+[node name="Anchor" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 0
+
+[node name="AnimationParent" type="Control" parent="Anchor"]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 0
+mouse_filter = 2
+
+[node name="Sizer" type="Control" parent="Anchor/AnimationParent"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -150.0
+offset_top = -50.0
+offset_right = 150.0
+grow_horizontal = 2
+grow_vertical = 0
+mouse_filter = 2
+
+[node name="DialogTextPanel" type="PanelContainer" parent="Anchor/AnimationParent/Sizer"]
+unique_name_in_owner = true
+self_modulate = Color(0.00784314, 0.00784314, 0.00784314, 0.843137)
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+theme_override_styles/panel = ExtResource("3_ssa84")
+metadata/_edit_layout_mode = 1
+
+[node name="DialogicNode_DialogText" type="RichTextLabel" parent="Anchor/AnimationParent/Sizer/DialogTextPanel" node_paths=PackedStringArray("textbox_root")]
+unique_name_in_owner = true
+layout_mode = 2
+mouse_filter = 1
+theme_override_colors/default_color = Color(1, 1, 1, 1)
+theme_override_font_sizes/normal_font_size = 15
+theme_override_font_sizes/bold_font_size = 15
+theme_override_font_sizes/italics_font_size = 15
+theme_override_font_sizes/bold_italics_font_size = 15
+bbcode_enabled = true
+text = "Some default text"
+visible_characters_behavior = 1
+script = ExtResource("3_4634k")
+textbox_root = NodePath("..")
+
+[node name="DialogicNode_TypeSounds" type="AudioStreamPlayer" parent="Anchor/AnimationParent/Sizer/DialogTextPanel/DialogicNode_DialogText"]
+unique_name_in_owner = true
+script = ExtResource("4_ma5mw")
+play_every_character = 0
+
+[node name="NextIndicator" type="Control" parent="Anchor/AnimationParent/Sizer/DialogTextPanel"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 8
+mouse_filter = 2
+script = ExtResource("5_40a50")
+show_on_questions = true
+texture = ExtResource("6_uch03")
+metadata/_edit_layout_mode = 1
+
+[node name="AutoAdvanceProgressbar" type="ProgressBar" parent="Anchor/AnimationParent/Sizer/DialogTextPanel"]
+unique_name_in_owner = true
+modulate = Color(1, 1, 1, 0.188235)
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+size_flags_vertical = 8
+mouse_filter = 2
+max_value = 1.0
+step = 0.001
+value = 0.5
+show_percentage = false
+script = ExtResource("6_07xym")
+
+[node name="NameLabelHolder" type="Control" parent="Anchor/AnimationParent/Sizer/DialogTextPanel"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="NameLabelPanel" type="PanelContainer" parent="Anchor/AnimationParent/Sizer/DialogTextPanel/NameLabelHolder"]
+unique_name_in_owner = true
+self_modulate = Color(0.00784314, 0.00784314, 0.00784314, 0.843137)
+layout_mode = 1
+offset_top = -50.0
+offset_right = 9.0
+offset_bottom = -25.0
+mouse_filter = 2
+theme_override_styles/panel = ExtResource("9_yg8ig")
+metadata/_edit_layout_mode = 1
+metadata/_edit_use_custom_anchors = true
+metadata/_edit_group_ = true
+
+[node name="DialogicNode_NameLabel" type="Label" parent="Anchor/AnimationParent/Sizer/DialogTextPanel/NameLabelHolder/NameLabelPanel" node_paths=PackedStringArray("name_label_root")]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 1, 1, 1)
+theme_override_fonts/font = SubResource("FontVariation_v8y64")
+theme_override_font_sizes/font_size = 15
+text = "S"
+script = ExtResource("7_bi7sh")
+name_label_root = NodePath("..")
--- /dev/null
+[gd_resource type="StyleBoxFlat" format=3 uid="uid://m7gyepkysu83"]
+
+[resource]
+content_margin_left = 10.0
+content_margin_top = 5.0
+content_margin_right = 10.0
+content_margin_bottom = 5.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
--- /dev/null
+[style]
+type = "Style"
+name = "Speaker Textbox Style"
+author = "Dialogic"
+description = "A style with a textbox that has a speaker portrait inside of it."
+style_path = "speaker_textbox_style.tres"
--- /dev/null
+[gd_resource type="Resource" script_class="DialogicStyle" load_steps=18 format=3 uid="uid://dgkmuyvy5qc35"]
+
+[ext_resource type="PackedScene" uid="uid://c1k5m0w3r40xf" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_FullBackground/full_background_layer.tscn" id="1_sde84"]
+[ext_resource type="Script" path="res://addons/dialogic/Resources/dialogic_style_layer.gd" id="2_i34tx"]
+[ext_resource type="PackedScene" uid="uid://by6waso0mjpjp" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_SpeakerPortraitTextbox/textbox_with_speaker_portrait.tscn" id="3_epko4"]
+[ext_resource type="PackedScene" uid="uid://cn674foxwedqu" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Input/full_advance_input_layer.tscn" id="4_8y2vo"]
+[ext_resource type="PackedScene" uid="uid://dsbwnp5hegnu3" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Glossary/glossary_popup_layer.tscn" id="5_ll78j"]
+[ext_resource type="PackedScene" uid="uid://dhk6j6eb6e3q" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Choices/vn_choice_layer.tscn" id="6_36eid"]
+[ext_resource type="PackedScene" uid="uid://cvgf4c6gg0tsy" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_TextInput/text_input_layer.tscn" id="7_hx5el"]
+[ext_resource type="PackedScene" uid="uid://lx24i8fl6uo" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_History/history_layer.tscn" id="8_00chv"]
+[ext_resource type="Script" path="res://addons/dialogic/Resources/dialogic_style.gd" id="9_sdr6x"]
+
+[sub_resource type="Resource" id="Resource_1cyj6"]
+script = ExtResource("2_i34tx")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_jk75o"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("1_sde84")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_l2hgc"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("4_8y2vo")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_iqsmu"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("3_epko4")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_axty6"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("5_ll78j")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_xh5pc"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("6_36eid")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_ytmk0"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("7_hx5el")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_yjxtw"]
+script = ExtResource("2_i34tx")
+scene = ExtResource("8_00chv")
+overrides = {}
+
+[resource]
+script = ExtResource("9_sdr6x")
+name = "Speaker Textbox Style"
+layer_list = Array[String](["10", "11", "12", "13", "14", "15", "16"])
+layer_info = {
+"": SubResource("Resource_1cyj6"),
+"10": SubResource("Resource_jk75o"),
+"11": SubResource("Resource_l2hgc"),
+"12": SubResource("Resource_iqsmu"),
+"13": SubResource("Resource_axty6"),
+"14": SubResource("Resource_xh5pc"),
+"15": SubResource("Resource_ytmk0"),
+"16": SubResource("Resource_yjxtw")
+}
+base_overrides = {}
+layers = Array[ExtResource("2_i34tx")]([])
+metadata/_latest_layer = ""
--- /dev/null
+[style]
+type = "Style"
+name = "Textbubble Style"
+author = "Dialogic"
+description = "A simple text bubble style."
+style_path = "textbubble_style.tres"
--- /dev/null
+[gd_resource type="Resource" script_class="DialogicStyle" load_steps=9 format=3 uid="uid://b0sbwssin2kuk"]
+
+[ext_resource type="PackedScene" uid="uid://syki6k0e6aac" path="res://addons/dialogic/Modules/DefaultLayoutParts/Base_TextBubble/text_bubble_base.tscn" id="1_a7s28"]
+[ext_resource type="Script" path="res://addons/dialogic/Resources/dialogic_style.gd" id="1_q3xp1"]
+[ext_resource type="PackedScene" uid="uid://d2it0xiap3gnt" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Textbubble/text_bubble_layer.tscn" id="2_ctkoo"]
+[ext_resource type="Script" path="res://addons/dialogic/Resources/dialogic_style_layer.gd" id="3_3a5cc"]
+[ext_resource type="PackedScene" uid="uid://cn674foxwedqu" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Input/full_advance_input_layer.tscn" id="4_rr4hm"]
+
+[sub_resource type="Resource" id="Resource_u2jf8"]
+script = ExtResource("3_3a5cc")
+scene = ExtResource("1_a7s28")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_ajt4g"]
+script = ExtResource("3_3a5cc")
+scene = ExtResource("4_rr4hm")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_phvdv"]
+script = ExtResource("3_3a5cc")
+scene = ExtResource("2_ctkoo")
+overrides = {}
+
+[resource]
+script = ExtResource("1_q3xp1")
+name = "Textbubble Style"
+layer_list = Array[String](["10", "11"])
+layer_info = {
+"": SubResource("Resource_u2jf8"),
+"10": SubResource("Resource_ajt4g"),
+"11": SubResource("Resource_phvdv")
+}
+base_overrides = {}
+layers = Array[ExtResource("3_3a5cc")]([])
+metadata/_latest_layer = ""
--- /dev/null
+[gd_resource type="Resource" script_class="DialogicStyle" load_steps=20 format=3 uid="uid://8t1mr302tmqs"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Resources/dialogic_style.gd" id="1_mvpc0"]
+[ext_resource type="Script" path="res://addons/dialogic/Resources/dialogic_style_layer.gd" id="2_3b8ue"]
+[ext_resource type="PackedScene" uid="uid://c1k5m0w3r40xf" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_FullBackground/full_background_layer.tscn" id="2_dtgi6"]
+[ext_resource type="PackedScene" uid="uid://cy1y14inwkplb" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Portraits/vn_portrait_layer.tscn" id="4_q1t5h"]
+[ext_resource type="PackedScene" uid="uid://bquja8jyk8kbr" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Textbox/vn_textbox_layer.tscn" id="5_o6sv8"]
+[ext_resource type="PackedScene" uid="uid://cn674foxwedqu" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Input/full_advance_input_layer.tscn" id="6_j6olx"]
+[ext_resource type="PackedScene" uid="uid://dsbwnp5hegnu3" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_Glossary/glossary_popup_layer.tscn" id="7_vw5f4"]
+[ext_resource type="PackedScene" uid="uid://dhk6j6eb6e3q" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_VN_Choices/vn_choice_layer.tscn" id="8_tc6v1"]
+[ext_resource type="PackedScene" uid="uid://cvgf4c6gg0tsy" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_TextInput/text_input_layer.tscn" id="9_tufw5"]
+[ext_resource type="PackedScene" uid="uid://lx24i8fl6uo" path="res://addons/dialogic/Modules/DefaultLayoutParts/Layer_History/history_layer.tscn" id="10_8v8jj"]
+
+[sub_resource type="Resource" id="Resource_3dixn"]
+script = ExtResource("2_3b8ue")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_gen8e"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("2_dtgi6")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_nit0s"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("4_q1t5h")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_1ak3n"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("6_j6olx")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_05bhv"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("5_o6sv8")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_pvwog"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("7_vw5f4")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_spe5r"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("8_tc6v1")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_jf1ol"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("9_tufw5")
+overrides = {}
+
+[sub_resource type="Resource" id="Resource_gs5pl"]
+script = ExtResource("2_3b8ue")
+scene = ExtResource("10_8v8jj")
+overrides = {}
+
+[resource]
+script = ExtResource("1_mvpc0")
+name = "Visual Novel Style"
+layer_list = Array[String](["10", "11", "12", "13", "14", "15", "16", "17"])
+layer_info = {
+"": SubResource("Resource_3dixn"),
+"10": SubResource("Resource_gen8e"),
+"11": SubResource("Resource_nit0s"),
+"12": SubResource("Resource_1ak3n"),
+"13": SubResource("Resource_05bhv"),
+"14": SubResource("Resource_pvwog"),
+"15": SubResource("Resource_spe5r"),
+"16": SubResource("Resource_jf1ol"),
+"17": SubResource("Resource_gs5pl")
+}
+base_overrides = {}
+layers = Array[ExtResource("2_3b8ue")]([])
+metadata/_latest_layer = "17"
--- /dev/null
+[style]
+type = "Style"
+name = "Visual Novel Style"
+author = "Dialogic"
+description = "A full visual novel style."
+style_path = "default_vn_style.tres"
--- /dev/null
+extends DialogicIndexer
+
+
+func _get_layout_parts() -> Array[Dictionary]:
+ return scan_for_layout_parts()
--- /dev/null
+@tool
+class_name DialogicEndTimelineEvent
+extends DialogicEvent
+
+## Event that ends a timeline (even if more events come after).
+
+
+#region EXECUTE
+################################################################################
+
+func _execute() -> void:
+ dialogic.end_timeline()
+
+#endregion
+
+
+#region INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "End"
+ set_default_color('Color4')
+ event_category = "Flow"
+ event_sorting_index = 10
+
+#endregion
+
+
+#region SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "end_timeline"
+
+#endregion
+
+
+#region EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_label('End Timeline')
+
+#endregion
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_end.gd')]
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3636 2.90912H3.63635V13H7V11.6364H5.0909V10.1818H7V9.5V9H10V8.7273H5.0909V7.27275H10V6H12.3636V2.90912ZM10.9091 4.36366H5.0909V5.81821H10.9091V4.36366Z" fill="white"/>
+<path d="M13 10H16V12H13V15H11V12H8V10H11V7H13V10Z" fill="#A5EFAC"/>
+</svg>
--- /dev/null
+@tool
+class_name DialogicGlossaryEvent
+extends DialogicEvent
+
+## Event that does nothing right now.
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ pass
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Glossary"
+ set_default_color('Color6')
+ event_category = "Other"
+ event_sorting_index = 0
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+func get_shortcode() -> String:
+ return "glossary"
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ }
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ pass
--- /dev/null
+@tool
+extends DialogicEditor
+
+var current_glossary: DialogicGlossary = null
+var current_entry_name := ""
+var current_entry := {}
+
+################################################################################
+## BASICS
+################################################################################
+
+func _get_title() -> String:
+ return "Glossary"
+
+
+func _get_icon() -> Texture:
+ var base_directory: String = self.get_script().get_path().get_base_dir()
+ var icon_path := base_directory + "/icon.svg"
+ return load(icon_path)
+
+
+func _register() -> void:
+ editors_manager.register_simple_editor(self)
+ alternative_text = "Create and edit glossaries."
+
+
+func _ready() -> void:
+ var add_glossary_icon_path: String = self.get_script().get_path().get_base_dir() + "/add-glossary.svg"
+ var add_glossary_icon := load(add_glossary_icon_path)
+ %AddGlossaryFile.icon = add_glossary_icon
+
+ %LoadGlossaryFile.icon = get_theme_icon('Folder', 'EditorIcons')
+ %DeleteGlossaryFile.icon = get_theme_icon('Remove', 'EditorIcons')
+ %DeleteGlossaryEntry.icon = get_theme_icon('Remove', 'EditorIcons')
+
+ %DeleteGlossaryFile.pressed.connect(_on_delete_glossary_file_pressed)
+
+ %AddGlossaryEntry.icon = get_theme_icon('Add', 'EditorIcons')
+ %EntrySearch.right_icon = get_theme_icon('Search', 'EditorIcons')
+
+ %GlossaryList.item_selected.connect(_on_GlossaryList_item_selected)
+ %EntryList.item_selected.connect(_on_EntryList_item_selected)
+
+ %DefaultColor.color_changed.connect(set_setting.bind('dialogic/glossary/default_color'))
+ %DefaultCaseSensitive.toggled.connect(set_setting.bind('dialogic/glossary/default_case_sensitive'))
+
+ %EntryCaseSensitive.icon = get_theme_icon("MatchCase", "EditorIcons")
+
+ %EntryAlternatives.text_changed.connect(_on_entry_alternatives_text_changed)
+
+
+func set_setting(value: Variant, setting: String) -> void:
+ ProjectSettings.set_setting(setting, value)
+ ProjectSettings.save()
+
+
+func _open(_argument: Variant = null) -> void:
+ %DefaultColor.color = ProjectSettings.get_setting('dialogic/glossary/default_color', Color.POWDER_BLUE)
+ %DefaultCaseSensitive.button_pressed = ProjectSettings.get_setting('dialogic/glossary/default_case_sensitive', true)
+
+ %GlossaryList.clear()
+ var idx := 0
+ for file: String in ProjectSettings.get_setting('dialogic/glossary/glossary_files', []):
+
+ if ResourceLoader.exists(file):
+ %GlossaryList.add_item(DialogicUtil.pretty_name(file), get_theme_icon('FileList', 'EditorIcons'))
+ else:
+ %GlossaryList.add_item(DialogicUtil.pretty_name(file), get_theme_icon('FileDead', 'EditorIcons'))
+
+ %GlossaryList.set_item_tooltip(idx, file)
+ idx += 1
+
+ %EntryList.clear()
+
+ if %GlossaryList.item_count != 0:
+ %GlossaryList.select(0)
+ _on_GlossaryList_item_selected(0)
+ else:
+ current_glossary = null
+ hide_entry_editor()
+
+################################################################################
+## GLOSSARY LIST
+################################################################################
+func _on_GlossaryList_item_selected(idx: int) -> void:
+ %EntryList.clear()
+ var tooltip_item: String = %GlossaryList.get_item_tooltip(idx)
+
+ if ResourceLoader.exists(tooltip_item):
+ var glossary_item := load(tooltip_item)
+
+ if not glossary_item is DialogicGlossary:
+ return
+
+ current_glossary = load(tooltip_item)
+
+ if not current_glossary is DialogicGlossary:
+ return
+
+ var entry_idx := 0
+
+ for entry_key: String in current_glossary.entries.keys():
+ var entry: Variant = current_glossary.entries.get(entry_key)
+
+ if entry is String:
+ continue
+
+ # Older glossary entries may not have the name property and the
+ # alternatives may not be set up as alias entries.
+ if not entry.has(DialogicGlossary.NAME_PROPERTY):
+ entry[DialogicGlossary.NAME_PROPERTY] = entry_key
+ var alternatives_array: Array = entry.get(DialogicGlossary.ALTERNATIVE_PROPERTY, [])
+ var alternatives := ",".join(alternatives_array)
+ _on_entry_alternatives_text_changed(alternatives)
+ ResourceSaver.save(current_glossary)
+
+ %EntryList.add_item(entry.get(DialogicGlossary.NAME_PROPERTY, str(DialogicGlossary.NAME_PROPERTY)), get_theme_icon("Breakpoint", "EditorIcons"))
+ var modulate_color: Color = entry.get('color', %DefaultColor.color)
+ %EntryList.set_item_metadata(entry_idx, entry)
+ %EntryList.set_item_icon_modulate(entry_idx, modulate_color)
+
+ entry_idx += 1
+
+ if %EntryList.item_count != 0:
+ %EntryList.select(0)
+ _on_EntryList_item_selected(0)
+ else:
+ hide_entry_editor()
+
+
+func _on_add_glossary_file_pressed() -> void:
+ find_parent('EditorView').godot_file_dialog(create_new_glossary_file, '*.tres', EditorFileDialog.FILE_MODE_SAVE_FILE, 'Create new glossary resource')
+
+
+func create_new_glossary_file(path:String) -> void:
+ var glossary := DialogicGlossary.new()
+ glossary.resource_path = path
+ ResourceSaver.save(glossary, path)
+ load_glossary_file(path)
+
+
+func _on_load_glossary_file_pressed() -> void:
+ find_parent('EditorView').godot_file_dialog(load_glossary_file, '*.tres', EditorFileDialog.FILE_MODE_OPEN_FILE, 'Select glossary resource')
+
+
+func load_glossary_file(path:String) -> void:
+ var list: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
+
+ if not path in list:
+ list.append(path)
+ ProjectSettings.set_setting('dialogic/glossary/glossary_files', list)
+ ProjectSettings.save()
+ %GlossaryList.add_item(DialogicUtil.pretty_name(path), get_theme_icon('FileList', 'EditorIcons'))
+
+ var selected_item_index: int = %GlossaryList.item_count - 1
+
+ %GlossaryList.set_item_tooltip(selected_item_index, path)
+ %GlossaryList.select(selected_item_index)
+ _on_GlossaryList_item_selected(selected_item_index)
+
+
+func _on_delete_glossary_file_pressed() -> void:
+ var selected_items: PackedInt32Array = %GlossaryList.get_selected_items()
+
+ if not selected_items.is_empty():
+ var list: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
+ var selected_item_index := selected_items[0]
+ list.remove_at(selected_item_index)
+
+ ProjectSettings.set_setting('dialogic/glossary/glossary_files', list)
+ ProjectSettings.save()
+
+ _open()
+
+
+################################################################################
+## ENTRY LIST
+################################################################################
+func _on_EntryList_item_selected(idx: int) -> void:
+ current_entry_name = %EntryList.get_item_text(idx)
+
+ var entry_info: Dictionary = current_glossary.get_entry(current_entry_name)
+ current_entry = entry_info
+
+ %EntrySettings.show()
+ %EntryName.text = current_entry_name
+ %EntryCaseSensitive.button_pressed = entry_info.get('case_sensitive', %DefaultCaseSensitive.button_pressed)
+
+ var alternative_property: Array = entry_info.get(DialogicGlossary.ALTERNATIVE_PROPERTY, [])
+ var alternatives := ", ".join(alternative_property)
+ %EntryAlternatives.text = alternatives
+
+ %EntryTitle.text = entry_info.get('title', '')
+ %EntryText.text = entry_info.get('text', '')
+ %EntryExtra.text = entry_info.get('extra', '')
+ %EntryEnabled.button_pressed = entry_info.get('enabled', true)
+
+ %EntryColor.color = entry_info.get('color', %DefaultColor.color)
+ %EntryCustomColor.button_pressed = entry_info.has('color')
+ %EntryColor.disabled = !entry_info.has('color')
+
+ _check_entry_alternatives(alternatives)
+ _check_entry_name(current_entry_name, current_entry)
+
+func _on_add_glossary_entry_pressed() -> void:
+ if !current_glossary:
+ return
+
+ var entry_count := current_glossary.entries.size() + 1
+ var new_name := "New Entry " + str(entry_count)
+
+ if new_name in current_glossary.entries.keys():
+ var random_hex_number := str(randi() % 0xFFFFFF)
+ new_name = new_name + " " + str(random_hex_number)
+
+ var new_glossary := {}
+ new_glossary[DialogicGlossary.NAME_PROPERTY] = new_name
+
+ if not current_glossary.try_add_entry(new_glossary):
+ print_rich("[color=red]Failed adding '" + new_name + "', exists already.[/color]")
+ return
+
+ ResourceSaver.save(current_glossary)
+
+ %EntryList.add_item(new_name, get_theme_icon("Breakpoint", "EditorIcons"))
+ var item_count: int = %EntryList.item_count - 1
+
+ %EntryList.set_item_metadata(item_count, new_name)
+ %EntryList.set_item_icon_modulate(item_count, %DefaultColor.color)
+ %EntryList.select(item_count)
+
+ _on_EntryList_item_selected(item_count)
+
+ %EntryList.ensure_current_is_visible()
+ %EntryName.grab_focus()
+
+
+func _on_delete_glossary_entry_pressed() -> void:
+ var selected_items: Array = %EntryList.get_selected_items()
+
+ if not selected_items.is_empty():
+ var selected_item_index: int = selected_items[0]
+
+ if not current_glossary == null:
+ current_glossary.remove_entry(current_entry_name)
+ ResourceSaver.save(current_glossary)
+
+ %EntryList.remove_item(selected_item_index)
+ var entries_count: int = %EntryList.item_count
+
+ if entries_count > 0:
+ var previous_item_index := selected_item_index - 1
+ %EntryList.select(previous_item_index)
+
+
+
+func _on_entry_search_text_changed(new_text: String) -> void:
+ if new_text.is_empty() or new_text.to_lower() in %EntryList.get_item_text(%EntryList.get_selected_items()[0]).to_lower():
+ return
+
+ for i: int in %EntryList.item_count:
+
+ if new_text.is_empty() or new_text.to_lower() in %EntryList.get_item_text(i).to_lower():
+ %EntryList.select(i)
+ _on_EntryList_item_selected(i)
+ %EntryList.ensure_current_is_visible()
+
+
+################################################################################
+## ENTRY EDITOR
+################################################################################
+func hide_entry_editor() -> void:
+ %EntrySettings.hide()
+
+
+func _update_alias_entries(old_alias_value_key: String, new_alias_value_key: String) -> void:
+ for entry_key: String in current_glossary.entries.keys():
+
+ var entry_value: Variant = current_glossary.entries.get(entry_key)
+
+ if not entry_value is String:
+ continue
+
+ if not entry_value == old_alias_value_key:
+ continue
+
+ current_glossary.entries[entry_key] = new_alias_value_key
+
+
+## Checks if the [param entry_name] is already used as a key for another entry
+## and returns true if it doesn't.
+## The [param entry] will be used to check if found entry uses the same
+## reference in memory.
+func _check_entry_name(entry_name: String, entry: Dictionary) -> bool:
+ var selected_item: int = %EntryList.get_selected_items()[0]
+ var raised_error: bool = false
+
+ var entry_assigned: Variant = current_glossary.entries.get(entry_name, {})
+
+ # Alternative entry uses the entry name already.
+ if entry_assigned is String:
+ raised_error = true
+
+ if entry_assigned is Dictionary and not entry_assigned.is_empty():
+ var entry_name_assigned: String = entry_assigned.get(DialogicGlossary.NAME_PROPERTY, "")
+
+ # Another entry uses the entry name already.
+ if not entry_name_assigned == entry_name:
+ raised_error = true
+
+ # Not the same memory reference.
+ if not entry == entry_assigned:
+ raised_error = true
+
+ if raised_error:
+ %EntryList.set_item_custom_bg_color(selected_item,
+ get_theme_color("warning_color", "Editor").darkened(0.8))
+ %EntryName.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
+ %EntryName.right_icon = get_theme_icon("StatusError", "EditorIcons")
+
+ return false
+
+ else:
+ %EntryName.add_theme_color_override("font_color", get_theme_color("font_color", "Editor"))
+ %EntryName.add_theme_color_override("caret_color", get_theme_color("font_color", "Editor"))
+ %EntryName.right_icon = null
+ %EntryList.set_item_custom_bg_color(
+ selected_item,
+ Color.TRANSPARENT
+ )
+
+ return true
+
+
+func _on_entry_name_text_changed(new_name: String) -> void:
+ new_name = new_name.strip_edges()
+
+ if current_entry_name != new_name:
+ var selected_item: int = %EntryList.get_selected_items()[0]
+
+ if not _check_entry_name(new_name, current_entry):
+ return
+
+ print_rich("[color=green]Renaming entry '" + current_entry_name + "'' to '" + new_name + "'[/color]")
+
+ _update_alias_entries(current_entry_name, new_name)
+
+ current_glossary.replace_entry_key(current_entry_name, new_name)
+
+ %EntryList.set_item_text(selected_item, new_name)
+ %EntryList.set_item_metadata(selected_item, new_name)
+ ResourceSaver.save(current_glossary)
+ current_entry_name = new_name
+
+
+func _on_entry_case_sensitive_toggled(button_pressed: bool) -> void:
+ current_glossary.get_entry(current_entry_name)['case_sensitive'] = button_pressed
+ ResourceSaver.save(current_glossary)
+
+
+## Checks if the [param new_alternatives] has any alternatives that are already
+## used as a key for another entry and returns true if it doesn't.
+func _can_change_alternative(new_alternatives: String) -> bool:
+ for alternative: String in new_alternatives.split(',', false):
+ var stripped_alternative := alternative.strip_edges()
+
+ var value: Variant = current_glossary.entries.get(stripped_alternative, null)
+
+ if value == null:
+ continue
+
+ if value is String:
+ value = current_glossary.entries.get(value, null)
+
+ var value_name: String = value[DialogicGlossary.NAME_PROPERTY]
+
+ if not current_entry_name == value_name:
+ return false
+
+ return true
+
+
+## Checks if [entry_alternatives] has any alternatives that are already
+## used by any entry and returns true if it doesn't.
+## If false, it will set the alternatives text field to a warning color and
+## set an icon.
+## If true, the alternatives text field will be set to the default color and
+## the icon will be removed.
+func _check_entry_alternatives(entry_alternatives: String) -> bool:
+
+ if not _can_change_alternative(entry_alternatives):
+ %EntryAlternatives.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
+ %EntryAlternatives.right_icon = get_theme_icon("StatusError", "EditorIcons")
+ return false
+
+ else:
+ %EntryAlternatives.add_theme_color_override("font_color", get_theme_color("font_color", "Editor"))
+ %EntryAlternatives.right_icon = null
+
+ return true
+
+
+## The [param new_alternatives] is a passed as a string of comma separated
+## values form the Dialogic editor.
+##
+## Saves the glossary resource file.
+func _on_entry_alternatives_text_changed(new_alternatives: String) -> void:
+ var current_alternatives: Array = current_glossary.get_entry(current_entry_name).get(DialogicGlossary.ALTERNATIVE_PROPERTY, [])
+
+ if not _check_entry_alternatives(new_alternatives):
+ return
+
+ for current_alternative: String in current_alternatives:
+ current_glossary._remove_entry_alias(current_alternative)
+
+ var alternatives := []
+
+ for new_alternative: String in new_alternatives.split(',', false):
+ var stripped_alternative := new_alternative.strip_edges()
+ alternatives.append(stripped_alternative)
+ current_glossary._add_entry_key_alias(current_entry_name, stripped_alternative)
+
+ current_glossary.get_entry(current_entry_name)[DialogicGlossary.ALTERNATIVE_PROPERTY] = alternatives
+ ResourceSaver.save(current_glossary)
+
+
+func _on_entry_title_text_changed(new_text:String) -> void:
+ current_glossary.get_entry(current_entry_name)['title'] = new_text
+ ResourceSaver.save(current_glossary)
+
+
+func _on_entry_text_text_changed() -> void:
+ current_glossary.get_entry(current_entry_name)['text'] = %EntryText.text
+ ResourceSaver.save(current_glossary)
+
+
+func _on_entry_extra_text_changed() -> void:
+ current_glossary.get_entry(current_entry_name)['extra'] = %EntryExtra.text
+ ResourceSaver.save(current_glossary)
+
+
+func _on_entry_enabled_toggled(button_pressed:bool) -> void:
+ current_glossary.get_entry(current_entry_name)['enabled'] = button_pressed
+ ResourceSaver.save(current_glossary)
+
+
+func _on_entry_custom_color_toggled(button_pressed:bool) -> void:
+ %EntryColor.disabled = !button_pressed
+
+ if !button_pressed:
+ current_glossary.get_entry(current_entry_name).erase('color')
+ %EntryList.set_item_icon_modulate(%EntryList.get_selected_items()[0], %DefaultColor.color)
+ else:
+ current_glossary.get_entry(current_entry_name)['color'] = %EntryColor.color
+ %EntryList.set_item_icon_modulate(%EntryList.get_selected_items()[0], %EntryColor.color)
+
+
+func _on_entry_color_color_changed(color:Color) -> void:
+ current_glossary.get_entry(current_entry_name)['color'] = color
+ %EntryList.set_item_icon_modulate(%EntryList.get_selected_items()[0], color)
+ ResourceSaver.save(current_glossary)
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://due48ce7jiudt"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Glossary/glossary_editor.gd" id="1_tf3p1"]
+[ext_resource type="Texture2D" uid="uid://cenut3sc5cul0" path="res://addons/dialogic/Modules/Glossary/add-glossary.svg" id="2_0elx7"]
+
+[sub_resource type="Image" id="Image_puu06"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_dfvxn"]
+image = SubResource("Image_puu06")
+
+[node name="GlossaryEditor" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource("1_tf3p1")
+
+[node name="Entries" type="HSplitContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+split_offset = -200
+
+[node name="Settings" type="VBoxContainer" parent="Entries"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.3
+
+[node name="Label" type="Label" parent="Entries/Settings"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Glossaries"
+
+[node name="Glossaries" type="PanelContainer" parent="Entries/Settings"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelA"
+
+[node name="Glossaries" type="VBoxContainer" parent="Entries/Settings/Glossaries"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.69
+
+[node name="HBox" type="HBoxContainer" parent="Entries/Settings/Glossaries/Glossaries"]
+layout_mode = 2
+
+[node name="AddGlossaryFile" type="Button" parent="Entries/Settings/Glossaries/Glossaries/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "New Glossary"
+icon = ExtResource("2_0elx7")
+
+[node name="LoadGlossaryFile" type="Button" parent="Entries/Settings/Glossaries/Glossaries/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "Import Glossary File"
+icon = SubResource("ImageTexture_dfvxn")
+
+[node name="DeleteGlossaryFile" type="Button" parent="Entries/Settings/Glossaries/Glossaries/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "Delete Glossary"
+icon = SubResource("ImageTexture_dfvxn")
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Entries/Settings/Glossaries/Glossaries"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="GlossaryList" type="ItemList" parent="Entries/Settings/Glossaries/Glossaries/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Label2" type="Label" parent="Entries/Settings"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Defaults"
+
+[node name="Defaults" type="VBoxContainer" parent="Entries/Settings"]
+layout_mode = 2
+
+[node name="DefaultsColor" type="HBoxContainer" parent="Entries/Settings/Defaults"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Entries/Settings/Defaults/DefaultsColor"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Color"
+
+[node name="DefaultColor" type="ColorPickerButton" parent="Entries/Settings/Defaults/DefaultsColor"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+size_flags_horizontal = 8
+
+[node name="DefCaseSensitive" type="HBoxContainer" parent="Entries/Settings/Defaults"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Entries/Settings/Defaults/DefCaseSensitive"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Case sensitive"
+
+[node name="DefaultCaseSensitive" type="CheckBox" parent="Entries/Settings/Defaults/DefCaseSensitive"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSplit" type="HSplitContainer" parent="Entries"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Entries/HSplit"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Label2" type="Label" parent="Entries/HSplit/VBoxContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Entries"
+
+[node name="Tabs" type="PanelContainer" parent="Entries/HSplit/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelA"
+
+[node name="Entries" type="VBoxContainer" parent="Entries/HSplit/VBoxContainer/Tabs"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.69
+
+[node name="HBox" type="HBoxContainer" parent="Entries/HSplit/VBoxContainer/Tabs/Entries"]
+layout_mode = 2
+
+[node name="AddGlossaryEntry" type="Button" parent="Entries/HSplit/VBoxContainer/Tabs/Entries/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "New Glossary Entry"
+icon = SubResource("ImageTexture_dfvxn")
+
+[node name="DeleteGlossaryEntry" type="Button" parent="Entries/HSplit/VBoxContainer/Tabs/Entries/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 4
+tooltip_text = "Delete Glossary Entry"
+icon = SubResource("ImageTexture_dfvxn")
+
+[node name="EntrySearch" type="LineEdit" parent="Entries/HSplit/VBoxContainer/Tabs/Entries/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Search"
+right_icon = SubResource("ImageTexture_dfvxn")
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Entries/HSplit/VBoxContainer/Tabs/Entries"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="EntryList" type="ItemList" parent="Entries/HSplit/VBoxContainer/Tabs/Entries/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+focus_neighbor_right = NodePath("../../../../EntryEditor/Tabs/Entry Settings/EntrySettings/HBox/EntryName")
+
+[node name="EntryEditor" type="ScrollContainer" parent="Entries/HSplit"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+horizontal_scroll_mode = 0
+metadata/_edit_layout_mode = 1
+
+[node name="VBox" type="VBoxContainer" parent="Entries/HSplit/EntryEditor"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Label" type="Label" parent="Entries/HSplit/EntryEditor/VBox"]
+layout_mode = 2
+theme_type_variation = &"DialogicSection"
+text = "Entry Settings"
+
+[node name="Entry Settings" type="VBoxContainer" parent="Entries/HSplit/EntryEditor/VBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="EntrySettings" type="GridContainer" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/h_separation = 13
+columns = 2
+
+[node name="Label2" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Name"
+
+[node name="HBox2" type="HBoxContainer" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+
+[node name="EntryName" type="LineEdit" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox2"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_left = NodePath("../../../../../../VBoxContainer/Tabs/Entries/ScrollContainer/EntryList")
+theme_override_colors/caret_color = Color(0, 0, 0, 1)
+placeholder_text = "Enter unique name..."
+caret_blink = true
+
+[node name="EntryCaseSensitive" type="Button" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox2"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Case sensitive"
+toggle_mode = true
+icon = SubResource("ImageTexture_dfvxn")
+flat = true
+
+[node name="Label3" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Alternatives"
+
+[node name="EntryAlternatives" type="LineEdit" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+
+[node name="Label4" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Title"
+
+[node name="EntryTitle" type="LineEdit" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+
+[node name="Label5" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Description"
+
+[node name="EntryText" type="TextEdit" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 100)
+layout_mode = 2
+focus_next = NodePath("../EntryExtra")
+wrap_mode = 1
+
+[node name="Label6" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Extra"
+
+[node name="EntryExtra" type="TextEdit" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 50)
+layout_mode = 2
+wrap_mode = 1
+
+[node name="Label8" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Enabled"
+
+[node name="EntryEnabled" type="CheckBox" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+unique_name_in_owner = true
+layout_mode = 2
+button_pressed = true
+
+[node name="Label7" type="Label" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Color"
+
+[node name="HBox" type="HBoxContainer" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings"]
+layout_mode = 2
+
+[node name="EntryCustomColor" type="CheckBox" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="EntryColor" type="ColorPickerButton" parent="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[connection signal="pressed" from="Entries/Settings/Glossaries/Glossaries/HBox/AddGlossaryFile" to="." method="_on_add_glossary_file_pressed"]
+[connection signal="pressed" from="Entries/Settings/Glossaries/Glossaries/HBox/LoadGlossaryFile" to="." method="_on_load_glossary_file_pressed"]
+[connection signal="pressed" from="Entries/HSplit/VBoxContainer/Tabs/Entries/HBox/AddGlossaryEntry" to="." method="_on_add_glossary_entry_pressed"]
+[connection signal="pressed" from="Entries/HSplit/VBoxContainer/Tabs/Entries/HBox/DeleteGlossaryEntry" to="." method="_on_delete_glossary_entry_pressed"]
+[connection signal="text_changed" from="Entries/HSplit/VBoxContainer/Tabs/Entries/HBox/EntrySearch" to="." method="_on_entry_search_text_changed"]
+[connection signal="text_changed" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox2/EntryName" to="." method="_on_entry_name_text_changed"]
+[connection signal="toggled" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox2/EntryCaseSensitive" to="." method="_on_entry_case_sensitive_toggled"]
+[connection signal="text_changed" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/EntryTitle" to="." method="_on_entry_title_text_changed"]
+[connection signal="text_changed" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/EntryText" to="." method="_on_entry_text_text_changed"]
+[connection signal="text_changed" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/EntryExtra" to="." method="_on_entry_extra_text_changed"]
+[connection signal="toggled" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/EntryEnabled" to="." method="_on_entry_enabled_toggled"]
+[connection signal="toggled" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox/EntryCustomColor" to="." method="_on_entry_custom_color_toggled"]
+[connection signal="color_changed" from="Entries/HSplit/EntryEditor/VBox/Entry Settings/EntrySettings/HBox/EntryColor" to="." method="_on_entry_color_color_changed"]
--- /dev/null
+@tool
+## Resource used to store glossary entries. Can be saved to disc and used as a glossary.
+## Add/create glossaries fom the glossaries editor
+class_name DialogicGlossary
+extends Resource
+
+## Stores all entries for the glossary.
+##
+## The value may either be a dictionary, representing an entry, or
+## a string, representing the actual key for the key used.
+## The string key-value pairs are the alias keys, they allow to redirect
+## the actual glossary entry.
+@export var entries := {}
+
+## If false, no entries from this glossary will be shown
+@export var enabled: bool = true
+
+## Refers to the translation type of this resource used for CSV translation files.
+const RESOURCE_NAME := "Glossary"
+## The name of glossary entries, the value is the key in [member entries].
+## This constant is used for CSV translation files.
+const NAME_PROPERTY := "name"
+## Property in a glossary entry. Alternative words for the entry name.
+const ALTERNATIVE_PROPERTY := "alternatives"
+## Property in a glossary entry.
+const TITLE_PROPERTY := "title"
+## Property in a glossary entry.
+const TEXT_PROPERTY := "text"
+## Property in a glossary entry.
+const EXTRA_PROPERTY := "extra"
+## Property in a glossary entry. The translation ID of the entry.
+## May be empty if the entry has not been translated yet.
+const TRANSLATION_PROPERTY := "_translation_id"
+## Property in a glossary entry.
+const REGEX_OPTION_PROPERTY := "regex_options"
+## Prefix used for private properties in entries.
+## Ignored when entries are translated.
+const PRIVATE_PROPERTY_PREFIX := "_"
+
+
+## Private ID assigned when this glossary is translated.
+@export var _translation_id := ""
+
+## Private lookup table used to find the translation ID of a glossary entry.
+## The keys (String) are all translated words that may trigger a glossary entry to
+## be shown.
+## The values (String) are the translation ID.
+@export var _translation_keys := {}
+
+
+
+## Removes an entry and all its aliases (alternative property) from
+## the glossary.
+## [param entry_key] may be an entry name or an alias.
+##
+## Returns true if the entry matching the given [param entry_key] was found.
+func remove_entry(entry_key: String) -> bool:
+ var entry: Dictionary = get_entry(entry_key)
+
+ if entry.is_empty():
+ return false
+
+ var aliases: Array = entry.get(ALTERNATIVE_PROPERTY, [])
+
+ for alias: String in aliases:
+ _remove_entry_alias(alias)
+
+ entries.erase(entry_key)
+
+ return true
+
+
+## This is an internal method.
+## Erases an entry alias key based the given [param entry_key].
+##
+## Returns true if [param entry_key] lead to a value and the value
+## was an alias.
+##
+## This method does not update the entry's alternative property.
+func _remove_entry_alias(entry_key: String) -> bool:
+ var value: Variant = entries.get(entry_key, null)
+
+ if value == null or value is Dictionary:
+ return false
+
+ entries.erase(entry_key)
+
+ return true
+
+
+## Updates the glossary entry's name and related alias keys.
+## The [param old_entry_key] is the old unique name of the entry.
+## The [param new_entry_key] is the new unique name of the entry.
+##
+## This method fails if the [param old_entry_key] does not exist.
+
+## Do not use this to update alternative names.
+## In order to update alternative names, delete all with
+## [method _remove_entry_alias] and then add them again with
+## [method _add_entry_key_alias].
+func replace_entry_key(old_entry_key: String, new_entry_key: String) -> void:
+ var entry := get_entry(old_entry_key)
+
+ if entry == null:
+ return
+
+ entry.name = new_entry_key
+
+ entries.erase(old_entry_key)
+ entries[new_entry_key] = entry
+
+
+## Gets the glossary entry for the given [param entry_key].
+## If there is no matching entry, an empty Dictionary will be returned.
+## Valid glossary entry dictionaries will never be empty.
+func get_entry(entry_key: String) -> Dictionary:
+ var entry: Variant = entries.get(entry_key, {})
+
+ # Handle alias value.
+ if entry is String:
+ entry = entries.get(entry, {})
+
+ return entry
+
+
+## This is an internal method.
+## The [param entry_key] must be valid entry key for an entry.
+## Adds the [param alias] as a valid entry key for that entry.
+##
+## Returns the index of the entry, -1 if the entry does not exist.
+func _add_entry_key_alias(entry_key: String, alias: String) -> bool:
+ var entry := get_entry(entry_key)
+ var alias_entry := get_entry(alias)
+
+ if not entry.is_empty() and alias_entry.is_empty():
+ entries[alias] = entry_key
+ return true
+
+ return false
+
+
+## Adds [param entry] to the glossary if it does not exist.
+## If it does exist, returns false.
+func try_add_entry(entry: Dictionary) -> bool:
+ var entry_key: String = entry[NAME_PROPERTY]
+
+ if entries.has(entry_key):
+ return false
+
+ entries[entry_key] = entry
+
+ for alternative: String in entry.get(ALTERNATIVE_PROPERTY, []):
+ entries[alternative.strip_edges()] = entry_key
+
+ return true
+
+
+## Returns an array of words that can trigger the glossary popup.
+## This method respects whether translation is enabled or not.
+## The words may be: The entry key and the alternative words.
+func _get_word_options(entry_key: String) -> Array:
+ var word_options: Array = []
+
+ var translation_enabled: bool = ProjectSettings.get_setting("dialogic/translation/enabled", false)
+
+ if not translation_enabled:
+ word_options.append(entry_key)
+
+ for alternative: String in get_entry(entry_key).get(ALTERNATIVE_PROPERTY, []):
+ word_options.append(alternative.strip_edges())
+
+ return word_options
+
+ var translation_entry_key_id: String = get_property_translation_key(entry_key, NAME_PROPERTY)
+
+ if translation_entry_key_id.is_empty():
+ return []
+
+ var translated_entry_key := tr(translation_entry_key_id)
+
+ if not translated_entry_key == translation_entry_key_id:
+ word_options.append(translated_entry_key)
+
+ var translation_alternatives_id: String = get_property_translation_key(entry_key, ALTERNATIVE_PROPERTY)
+ var translated_alternatives_str := tr(translation_alternatives_id)
+
+ if not translated_alternatives_str == translation_alternatives_id:
+ var translated_alternatives := translated_alternatives_str.split(",")
+
+ for alternative: String in translated_alternatives:
+ word_options.append(alternative.strip_edges())
+
+ return word_options
+
+
+## Gets the regex option for the given [param entry_key].
+## If the regex option does not exist, it will be generated.
+##
+## A regex option is the accumulation of valid words that can trigger the
+## glossary popup.
+##
+## The [param entry_key] must be valid or an error will occur.
+func get_set_regex_option(entry_key: String) -> String:
+ var entry: Dictionary = get_entry(entry_key)
+
+ var regex_options: Dictionary = entry.get(REGEX_OPTION_PROPERTY, {})
+
+ if regex_options.is_empty():
+ entry[REGEX_OPTION_PROPERTY] = regex_options
+
+ var locale_key: String = TranslationServer.get_locale()
+ var regex_option: String = regex_options.get(locale_key, "")
+
+ if not regex_option.is_empty():
+ return regex_option
+
+ var word_options: Array = _get_word_options(entry_key)
+ regex_option = "|".join(word_options)
+
+ regex_options[locale_key] = regex_option
+
+ return regex_option
+
+
+#region ADD AND CLEAR TRANSLATION KEYS
+
+## This is automatically called, no need to use this.
+func add_translation_id() -> String:
+ _translation_id = DialogicUtil.get_next_translation_id()
+ return _translation_id
+
+
+## Removes the translation ID of this glossary.
+func remove_translation_id() -> void:
+ _translation_id = ""
+
+
+## Removes the translation ID of all glossary entries.
+func remove_entry_translation_ids() -> void:
+ for entry: Variant in entries.values():
+
+ # Ignore aliases.
+ if entry is String:
+ continue
+
+ if entry.has(TRANSLATION_PROPERTY):
+ entry[TRANSLATION_PROPERTY] = ""
+
+
+## Clears the lookup tables using translation keys.
+func clear_translation_keys() -> void:
+ const RESOURCE_NAME_KEY := RESOURCE_NAME + "/"
+
+ for translation_key: String in entries.keys():
+
+ if translation_key.begins_with(RESOURCE_NAME_KEY):
+ entries.erase(translation_key)
+
+ _translation_keys.clear()
+
+#endregion
+
+
+#region GET AND SET TRANSLATION IDS AND KEYS
+
+## Returns a key used to reference this glossary in the translation CSV file.
+##
+## Time complexity: O(1)
+func get_property_translation_key(entry_key: String, property: String) -> String:
+ var entry := get_entry(entry_key)
+
+ if entry == null:
+ return ""
+
+ var entry_translation_key: String = entry.get(TRANSLATION_PROPERTY, "")
+
+ if entry_translation_key.is_empty() or _translation_id.is_empty():
+ return ""
+
+ var glossary_csv_key := (RESOURCE_NAME
+ .path_join(_translation_id)
+ .path_join(entry_translation_key)
+ .path_join(property))
+
+ return glossary_csv_key
+
+
+
+## Returns the translation key prefix for this glossary.
+## The resulting format will look like this: Glossary/a2/
+## This prefix can be used to find translations for this glossary.
+func _get_glossary_translation_id_prefix() -> String:
+ return (
+ DialogicGlossary.RESOURCE_NAME
+ .path_join(_translation_id)
+ )
+
+
+## Returns the translation key for the given [param glossary_translation_id] and
+## [param entry_translation_id].
+##
+## By key, we refer to the uniquely named property per translation entry.
+##
+## The resulting format will look like this: Glossary/a2/b4/name
+func _get_glossary_translation_key(entry_translation_id: String, property: String) -> String:
+ return (
+ DialogicGlossary.RESOURCE_NAME
+ .path_join(_translation_id)
+ .path_join(entry_translation_id)
+ .path_join(property)
+ )
+
+
+## Tries to get the glossary entry's translation ID.
+## If it does not exist, a new one will be generated.
+func get_set_glossary_entry_translation_id(entry_key: String) -> String:
+ var glossary_entry: Dictionary = get_entry(entry_key)
+ var entry_translation_id := ""
+
+ var glossary_translation_id: String = glossary_entry.get(TRANSLATION_PROPERTY, "")
+
+ if glossary_translation_id.is_empty():
+ entry_translation_id = DialogicUtil.get_next_translation_id()
+ glossary_entry[TRANSLATION_PROPERTY] = entry_translation_id
+
+ else:
+ entry_translation_id = glossary_entry[TRANSLATION_PROPERTY]
+
+ return entry_translation_id
+
+
+## Tries to get the glossary's translation ID.
+## If it does not exist, a new one will be generated.
+func get_set_glossary_translation_id() -> String:
+ if _translation_id == null or _translation_id.is_empty():
+ add_translation_id()
+
+ return _translation_id
+
+#endregion
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.90906 2.18182H13.0909V13.8182H2.90906V2.18182ZM4.60603 3.84416H11.3939V5.5065H4.60603V3.84416ZM11.3939 7.16884H4.60603V8.83117H11.3939V7.16884ZM4.60603 10.4935H11.3939V12.1558H4.60603V10.4935Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return []
+# return [this_folder.path_join('event_glossary.gd')]
+
+func _get_editors() -> Array:
+ return [this_folder.path_join('glossary_editor.tscn')]
+
+func _get_subsystems() -> Array:
+ return [{'name':'Glossary', 'script':this_folder.path_join('subsystem_glossary.gd')}]
+
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that handles glossaries.
+
+## List of glossary resources that are used.
+var glossaries := []
+## If false, no parsing will be done.
+var enabled := true
+
+## Any key in this dictionary will overwrite the color for any item with that name.
+var color_overrides := {}
+
+const SETTING_DEFAULT_COLOR := 'dialogic/glossary/default_color'
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ glossaries = []
+
+ for path: String in ProjectSettings.get_setting('dialogic/glossary/glossary_files', []):
+ add_glossary(path)
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+func parse_glossary(text: String) -> String:
+ if not enabled:
+ return text
+
+ var def_case_sensitive: bool = ProjectSettings.get_setting('dialogic/glossary/default_case_sensitive', true)
+ var def_color: Color = ProjectSettings.get_setting(SETTING_DEFAULT_COLOR, Color.POWDER_BLUE)
+ var regex := RegEx.new()
+
+ for glossary: DialogicGlossary in glossaries:
+
+ if !glossary.enabled:
+ continue
+
+ for entry_value: Variant in glossary.entries.values():
+
+ if not entry_value is Dictionary:
+ continue
+
+ var entry: Dictionary = entry_value
+ var entry_key: String = entry.get(DialogicGlossary.NAME_PROPERTY, "")
+
+ # Older versions of the glossary resource do not have a property
+ # for their name, we must skip these.
+ # They can be updated by opening the resource in the glossary
+ # editor.
+ if entry_key.is_empty():
+ continue
+
+ if not entry.get('enabled', true):
+ continue
+
+ var regex_options := glossary.get_set_regex_option(entry_key)
+
+ if regex_options.is_empty():
+ continue
+
+ var pattern: String = '(?<=\\W|^)(?<!\\\\)(?<word>' + regex_options + ')(?!])(?=\\W|$)'
+
+ if entry.get('case_sensitive', def_case_sensitive):
+ regex.compile(pattern)
+
+ else:
+ regex.compile('(?i)'+pattern)
+
+ var color: String = entry.get('color', def_color).to_html()
+
+ if entry_key in color_overrides:
+ color = color_overrides[entry_key].to_html()
+
+ text = regex.sub(text,
+ '[url=' + entry_key + ']' +
+ '[color=' + color + ']${word}[/color]' +
+ '[/url]',
+ true
+ )
+
+ return text
+
+
+func add_glossary(path:String) -> void:
+ if ResourceLoader.exists(path):
+ var resource: DialogicGlossary = load(path)
+
+ if resource is DialogicGlossary:
+ glossaries.append(resource)
+ else:
+ printerr('[Dialogic] The glossary file "' + path + '" is missing. Make sure it exists.')
+
+
+## Iterates over all glossaries and returns the first one that matches the
+## [param entry_key].
+##
+## Runtime complexity:
+## O(n), where n is the number of glossaries.
+func find_glossary(entry_key: String) -> DialogicGlossary:
+ for glossary: DialogicGlossary in glossaries:
+
+ if glossary.entries.has(entry_key):
+ return glossary
+
+ return null
+
+
+## Returns the first match for a given entry key.
+## If translation is available and enabled, it will be translated
+func get_entry(entry_key: String) -> Dictionary:
+ var glossary: DialogicGlossary = dialogic.Glossary.find_glossary(entry_key)
+
+ var result := {
+ "title": "",
+ "text": "",
+ "extra": "",
+ "color": Color.WHITE,
+ }
+
+ if glossary == null:
+ return {}
+
+ var is_translation_enabled: bool = ProjectSettings.get_setting('dialogic/translation/enabled', false)
+
+ var entry := glossary.get_entry(entry_key)
+
+ if entry.is_empty():
+ return {}
+
+ result.color = entry.get("color")
+ if result.color == null:
+ result.color = ProjectSettings.get_setting(SETTING_DEFAULT_COLOR, Color.POWDER_BLUE)
+
+ if is_translation_enabled and not glossary._translation_id.is_empty():
+ var translation_key: String = glossary._translation_keys.get(entry_key)
+ var last_slash := translation_key.rfind('/')
+
+ if last_slash == -1:
+ return {}
+
+ var tr_base := translation_key.substr(0, last_slash)
+
+ result.title = translate(tr_base, "title", entry)
+ result.text = translate(tr_base, "text", entry)
+ result.extra = translate(tr_base, "extra", entry)
+ else:
+ result.title = entry.get("title", "")
+ result.text = entry.get("text", "")
+ result.extra = entry.get("extra", "")
+
+ ## PARSE TEXTS FOR VARIABLES
+ result.title = dialogic.VAR.parse_variables(result.title)
+ result.text = dialogic.VAR.parse_variables(result.text)
+ result.extra = dialogic.VAR.parse_variables(result.extra)
+
+ return result
+
+
+
+## Tries to translate the property with the given
+func translate(tr_base: String, property: StringName, fallback_entry: Dictionary) -> String:
+ var tr_key := tr_base.path_join(property)
+ var tr_value := tr(tr_key)
+
+ if tr_key == tr_value:
+ tr_value = fallback_entry.get(property, "")
+
+ return tr_value
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_portrait_scene_presets() -> Array[Dictionary]:
+ return [
+ {
+ "path": this_folder.path_join("simple_highlight_portrait.tscn"),
+ "name": "Simple Highlight Portrait",
+ "description": "A portrait scene that displays a simple image, but changes color and moves to the front when this character is speaking.",
+ "author":"Dialogic",
+ "type": "General",
+ "icon":"",
+ "preview_image":[this_folder.path_join("highlight_portrait_thumbnail.png")],
+ "documentation":"",
+ },
+ ]
--- /dev/null
+@tool
+extends DialogicPortrait
+
+@export_group('Main')
+@export_file var image := ""
+
+var unhighlighted_color := Color.DARK_GRAY
+var _prev_z_index := 0
+
+## Load anything related to the given character and portrait
+func _update_portrait(passed_character:DialogicCharacter, passed_portrait:String) -> void:
+ apply_character_and_portrait(passed_character, passed_portrait)
+
+ apply_texture($Portrait, image)
+
+
+func _ready() -> void:
+ if not Engine.is_editor_hint():
+ self.modulate = unhighlighted_color
+
+
+func _should_do_portrait_update(_character: DialogicCharacter, _portrait: String) -> bool:
+ return true
+
+
+func _highlight() -> void:
+ create_tween().tween_property(self, 'modulate', Color.WHITE, 0.15)
+ _prev_z_index = DialogicUtil.autoload().Portraits.get_character_info(character).get('z_index', 0)
+ DialogicUtil.autoload().Portraits.change_character_z_index(character, 99)
+
+
+func _unhighlight() -> void:
+ create_tween().tween_property(self, 'modulate', unhighlighted_color, 0.15)
+ DialogicUtil.autoload().Portraits.change_character_z_index(character, _prev_z_index)
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://br18lgpga2y2v"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/HighlightPortrait/simple_highlight_portrait.gd" id="1_ceqva"]
+
+[node name="DefaultPortrait" type="Node2D"]
+script = ExtResource("1_ceqva")
+
+[node name="Portrait" type="Sprite2D" parent="."]
+centered = false
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.9645 2.62927L12.8459 5.22444C12.6271 5.04546 12.3835 4.89134 12.1151 4.76208C11.8565 4.62287 11.5881 4.55327 11.3097 4.55327C11.0909 4.55327 10.8821 4.59802 10.6832 4.68751C10.4943 4.77699 10.3203 4.89631 10.1612 5.04546C10.0021 5.19461 9.86293 5.36364 9.74361 5.55256C9.6243 5.73154 9.52486 5.91052 9.44532 6.08949L9.14702 6.93964C9.35583 7.37714 9.55966 7.79475 9.75853 8.19248C9.92756 8.53055 10.1016 8.87359 10.2805 9.2216C10.4595 9.55966 10.6087 9.82813 10.728 10.027C10.9169 10.3153 11.1058 10.6136 11.2948 10.9219C11.4837 11.2202 11.6925 11.4936 11.9212 11.7422C12.0206 11.8516 12.1399 11.9261 12.2791 11.9659C12.4283 11.9957 12.5625 12.0107 12.6818 12.0107C12.8707 12.0107 13.0497 11.9858 13.2188 11.9361C13.3878 11.8864 13.5469 11.8217 13.696 11.7422L14.0689 12.2195C13.9297 12.4382 13.7607 12.657 13.5618 12.8757C13.3629 13.0945 13.1442 13.2933 12.9055 13.4723C12.6769 13.6513 12.4283 13.7955 12.1598 13.9048C11.9013 14.0242 11.6378 14.0838 11.3693 14.0838C11.1307 14.0838 10.9169 14.044 10.728 13.9645C10.549 13.8949 10.3849 13.8004 10.2358 13.6811C10.0867 13.5519 9.94745 13.4027 9.81819 13.2337C9.68893 13.0646 9.55469 12.8906 9.41549 12.7117C9.30611 12.5426 9.1868 12.3388 9.05753 12.1001C8.93822 11.8516 8.81393 11.598 8.68466 11.3395C8.5554 11.081 8.42614 10.8324 8.29688 10.5938C8.16762 10.3452 8.04333 10.1364 7.92401 9.96734C7.8743 10.1364 7.82955 10.3054 7.78978 10.4744C7.75001 10.6136 7.70526 10.7578 7.65555 10.907C7.60583 11.0561 7.56109 11.1705 7.52131 11.25C7.37216 11.5682 7.18324 11.8963 6.95455 12.2344C6.72586 12.5724 6.46236 12.8807 6.16407 13.1591C5.87572 13.4276 5.55753 13.6513 5.20952 13.8303C4.86151 13.9993 4.49361 14.0838 4.10583 14.0838C3.75782 14.0838 3.41975 14.0142 3.09162 13.875C2.7635 13.7457 2.46023 13.5717 2.18182 13.353L3.12145 10.8771C3.43964 11.076 3.78765 11.255 4.16549 11.4141C4.54333 11.5632 4.92117 11.6378 5.29901 11.6378C5.41833 11.6378 5.54262 11.6278 5.67188 11.608C5.80114 11.5781 5.92543 11.5384 6.04475 11.4886C6.17401 11.429 6.28836 11.3594 6.38779 11.2798C6.48722 11.1903 6.5618 11.0859 6.61151 10.9666C6.68111 10.8374 6.75569 10.6683 6.83523 10.4595C6.91478 10.2507 6.99432 10.0419 7.07387 9.8331C7.16336 9.59447 7.25285 9.34091 7.34234 9.07245L4.53836 4.5831C4.4091 4.43395 4.25001 4.31464 4.06109 4.22515C3.88211 4.12572 3.69319 4.076 3.49432 4.076C3.32529 4.076 3.1662 4.1108 3.01705 4.1804C2.8679 4.24006 2.72373 4.32458 2.58452 4.43395L2.18182 3.91194C2.32103 3.70313 2.48509 3.4993 2.67401 3.30043C2.87287 3.09162 3.08665 2.90768 3.31535 2.74859C3.54404 2.57955 3.78765 2.44532 4.04617 2.34589C4.30469 2.23651 4.56819 2.18182 4.83665 2.18182C5.16478 2.18182 5.46805 2.26634 5.74645 2.43537C6.02486 2.59447 6.28339 2.79333 6.52202 3.03197C6.76066 3.2706 6.97941 3.52912 7.17827 3.80753C7.37714 4.08594 7.55611 4.34447 7.7152 4.5831C7.79475 4.69248 7.87927 4.83168 7.96876 5.00072C8.06819 5.15981 8.16265 5.3189 8.25214 5.47799C8.36151 5.65697 8.46591 5.85086 8.56535 6.05966C8.66478 5.82103 8.76918 5.58239 8.87856 5.34376C8.96805 5.14489 9.05753 4.94106 9.14702 4.73225C9.24645 4.5135 9.33594 4.32458 9.41549 4.16549C9.56464 3.88708 9.73367 3.62856 9.92259 3.38992C10.1115 3.15128 10.3203 2.94248 10.549 2.7635C10.7876 2.58452 11.0462 2.44532 11.3246 2.34589C11.603 2.23651 11.9063 2.18182 12.2344 2.18182C12.5426 2.18182 12.8409 2.2216 13.1293 2.30114C13.4176 2.38069 13.696 2.49006 13.9645 2.62927Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+class_name DialogicHistoryEvent
+extends DialogicEvent
+
+## Event that allows clearing, pausing and resuming of history functionality.
+
+enum Actions {CLEAR, PAUSE, RESUME}
+
+### Settings
+
+## The type of action: Clear, Pause or Resume
+var action := Actions.PAUSE
+
+
+################################################################################
+## EXECUTION
+################################################################################
+
+func _execute() -> void:
+ match action:
+ Actions.CLEAR:
+ dialogic.History.simple_history_content = []
+ Actions.PAUSE:
+ dialogic.History.simple_history_enabled = false
+ Actions.RESUME:
+ dialogic.History.simple_history_enabled = true
+
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "History"
+ set_default_color('Color9')
+ event_category = "Other"
+ event_sorting_index = 20
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "history"
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "action" : {"property": "action", "default": Actions.PAUSE,
+ "suggestions": func(): return {"Clear":{'value':0, 'text_alt':['clear']}, "Pause":{'value':1, 'text_alt':['pause']}, "Resume":{'value':2, 'text_alt':['resume', 'start']}}},
+ }
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('action', ValueType.FIXED_OPTIONS, {
+ 'options': [
+ {
+ 'label': 'Pause History',
+ 'value': Actions.PAUSE,
+ },
+ {
+ 'label': 'Resume History',
+ 'value': Actions.RESUME,
+ },
+ {
+ 'label': 'Clear History',
+ 'value': Actions.CLEAR,
+ },
+ ]
+ })
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="history-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#464646" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="2.2483786" inkscape:cx="-22.46063" inkscape:cy="23.127778" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
+ <defs id="defs2" />
+ <path style="stroke-width:2.07079;stroke-dasharray:none;stroke:#ffffff;fill:none;stroke-linecap:round;stroke-linejoin:round" d="m 58.321651,12.698536 -0.558962,3.485814 4.876563,-1.469574" id="path289" sodipodi:nodetypes="ccc" transform="matrix(0.74514681,0,0,0.74514681,-39.120302,2.2986328)" />
+ <path style="stroke-width:2.07079;stroke-dasharray:none;stroke:#ffffff;fill:none;stroke-linecap:round;stroke-linejoin:round" d="m 55.996236,7.7807585 1.228349,1.9317129 1.167703,-1.9718383 z" id="path953" sodipodi:nodetypes="cccc" transform="matrix(0.74514681,0,0,0.74514681,-39.120302,2.2986328)" />
+ <path style="stroke-width:2.07079;stroke-dasharray:none;stroke:#ffffff;fill:none;stroke-linecap:round;stroke-linejoin:round" d="m 63.824761,5.993924 0.29361,2.1554114 3.593444,1.1319403" id="path2562" inkscape:transform-center-x="-1.1566618" inkscape:transform-center-y="-0.95424579" sodipodi:nodetypes="ccc" transform="matrix(0.74514681,0,0,0.74514681,-39.120302,2.2986328)" />
+ <path id="path11412" style="stroke-width:2.07079;stroke-dasharray:none;stroke:#ffffff;fill:none;stroke-linecap:round;stroke-linejoin:round" d="m 57.198151,7.6362197 c 0,-4.012519 3.25279,-7.26530838 7.265309,-7.26530838 4.012519,0 7.265308,3.25278938 7.265308,7.26530838 0,4.0125193 -3.252789,7.2653093 -7.265308,7.2653093 -0.578673,0 -1.141544,-0.06765 -1.681136,-0.195481" sodipodi:nodetypes="csssc" transform="matrix(0.74514681,0,0,0.74514681,-39.120302,2.2986328)" />
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_history.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'History', 'script':this_folder.path_join('subsystem_history.gd')}]
+
+func _get_settings_pages() -> Array:
+ return [this_folder.path_join('settings_history.tscn')]
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+
+func _get_priority() -> int:
+ return -10
+
+
+func _ready() -> void:
+ %SimpleHistoryEnabled.toggled.connect(setting_toggled.bind('dialogic/history/simple_history_enabled'))
+ %SimpleHistorySave.toggled.connect(setting_toggled.bind('dialogic/history/simple_history_save'))
+ %FullHistoryEnabled.toggled.connect(setting_toggled.bind('dialogic/history/full_history_enabled'))
+ %FullHistorySave.toggled.connect(setting_toggled.bind('dialogic/history/full_history_save'))
+ %AlreadyReadHistoryEnabled.toggled.connect(setting_toggled.bind('dialogic/history/visited_event_history_enabled'))
+ %SaveOnAutoSaveToggle.toggled.connect(setting_toggled.bind('dialogic/history/save_on_autosave'))
+ %SaveOnSaveToggle.toggled.connect(setting_toggled.bind('dialogic/history/save_on_save'))
+
+
+func _refresh() -> void:
+ %SimpleHistoryEnabled.button_pressed = ProjectSettings.get_setting('dialogic/history/simple_history_enabled', false)
+ %SimpleHistorySave.button_pressed = ProjectSettings.get_setting('dialogic/history/simple_history_save', false)
+ %FullHistoryEnabled.button_pressed = ProjectSettings.get_setting('dialogic/history/full_history_enabled', false)
+ %FullHistorySave.button_pressed = ProjectSettings.get_setting('dialogic/history/full_history_save', false)
+ %AlreadyReadHistoryEnabled.button_pressed = ProjectSettings.get_setting('dialogic/history/visited_event_history_enabled', false)
+
+
+func setting_toggled(button_pressed: bool, setting: String) -> void:
+ ProjectSettings.set_setting(setting, button_pressed)
+ ProjectSettings.save()
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://b5yq6xh412ilm"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/History/settings_history.gd" id="1_hbhst"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_wefye"]
+
+[sub_resource type="Image" id="Image_3clns"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_irr0a"]
+image = SubResource("Image_3clns")
+
+[node name="History" type="PanelContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_type_variation = &"DialogicPanelA"
+script = ExtResource("1_hbhst")
+
+[node name="HistoryOptions" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Title3" type="Label" parent="HistoryOptions"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Simple History"
+
+[node name="HBoxContainer" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HistoryOptions/HBoxContainer"]
+layout_mode = 2
+text = "Enabled"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainer" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "When enabled, some events (Text, Join, Leave, Choice) will store a log.
+Also, the default layout will feature the log panel option."
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "When enabled, some events (Text, Join, Leave, Choice) will store a log.
+Also, the default layout will feature the log panel option."
+
+[node name="SimpleHistoryEnabled" type="CheckBox" parent="HistoryOptions/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HistoryOptions/HBoxContainer2"]
+layout_mode = 2
+text = "Save and Load"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainer2" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "When enabled, the simple history is included in the savegame."
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "When enabled, the simple history is included in the savegame. Also, it is reset on Dialogic.clear(FULL_CLEAR)."
+
+[node name="SimpleHistorySave" type="CheckBox" parent="HistoryOptions/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Title" type="Label" parent="HistoryOptions"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Full History"
+
+[node name="HBoxContainer5" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HistoryOptions/HBoxContainer5"]
+layout_mode = 2
+text = "Enabled"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainer5" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "When enabled, stores a copy of each event."
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "When enabled, stores a copy of each event."
+
+[node name="FullHistoryEnabled" type="CheckBox" parent="HistoryOptions/HBoxContainer5"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainer6" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HistoryOptions/HBoxContainer6"]
+layout_mode = 2
+text = "Save and Load"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainer6" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "When enabled, the full history is included in the savegame."
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "When enabled, the full history is included in the savegame. Also, it is reset on Dialogic.clear(FULL_CLEAR)."
+
+[node name="FullHistorySave" type="CheckBox" parent="HistoryOptions/HBoxContainer6"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Title2" type="Label" parent="HistoryOptions"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Seen Events History"
+
+[node name="HBoxContainer4" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="EnabledLabel" type="Label" parent="HistoryOptions/HBoxContainer4"]
+layout_mode = 2
+text = "Enabled"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainer4" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "Remembers whether events were already met in the timeline.
+When enabled the signals \"Dialogic.History.visited_event\" and \"Dialogic.History.unvisited_event\" are emitted.
+"
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "Remembers whether events were already met in the timeline.
+When enabled the signals \"Dialogic.History.visited_event\" and \"Dialogic.History.unvisited_event\" are emitted.
+"
+
+[node name="AlreadyReadHistoryEnabled" type="CheckBox" parent="HistoryOptions/HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainerSaveOnAutoSave" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="EnabledLabel" type="Label" parent="HistoryOptions/HBoxContainerSaveOnAutoSave"]
+layout_mode = 2
+text = "Save on Auto-Save signal"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainerSaveOnAutoSave" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "Stores the already-visited history in a global save file when an Auto-Save occurs.
+The Auto-Save is part of the Save settings."
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "Stores the already-visited history in a global save file when an Auto-Save occurs.
+The Auto-Save is part of the Save settings."
+
+[node name="SaveOnAutoSaveToggle" type="CheckBox" parent="HistoryOptions/HBoxContainerSaveOnAutoSave"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainerSaveOnSave" type="HBoxContainer" parent="HistoryOptions"]
+layout_mode = 2
+
+[node name="EnabledLabel" type="Label" parent="HistoryOptions/HBoxContainerSaveOnSave"]
+layout_mode = 2
+text = "Save on Save signal"
+
+[node name="HintTooltip" parent="HistoryOptions/HBoxContainerSaveOnSave" instance=ExtResource("2_wefye")]
+layout_mode = 2
+tooltip_text = "Stores the already-visited history in a global save file when a normal Save occurs.
+This can be done via the Dialogic.Save.save method.
+This setting ignores Auto-Saves."
+texture = SubResource("ImageTexture_irr0a")
+hint_text = "Stores the already-visited history in a global save file when a normal Save occurs.
+This can be done via the Dialogic.Save.save method.
+This setting ignores Auto-Saves."
+
+[node name="SaveOnSaveToggle" type="CheckBox" parent="HistoryOptions/HBoxContainerSaveOnSave"]
+unique_name_in_owner = true
+layout_mode = 2
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that manages history storing.
+
+signal open_requested
+signal close_requested
+
+
+## Simple history that stores limited information
+## Used for the history display
+var simple_history_enabled := false
+var simple_history_save := false
+var simple_history_content : Array[Dictionary] = []
+signal simple_history_changed
+
+## Whether to keep a history of every Dialogic event encountered.
+var full_event_history_enabled := false
+var full_event_history_save := false
+
+## The full history of all Dialogic events encountered.
+## Requires [member full_event_history_enabled] to be true.
+var full_event_history_content: Array[DialogicEvent] = []
+
+## Emitted if a new event has been inserted into the full event history.
+signal full_event_history_changed
+
+## Read text history
+## Stores which text events and choices have already been visited
+var visited_event_history_enabled := false
+
+## A history of visited Dialogic events.
+var visited_event_history_content := {}
+
+## Whether the last event has been encountered for the first time.
+var _visited_last_event := false
+
+## Emitted if an encountered timeline event has been inserted into the visited
+## event history.
+##
+## This will trigger only once per unique event instance.
+signal visited_event
+
+## Emitted if an encountered timeline event has not been visited before.
+signal unvisited_event
+
+## Used to store [member visited_event_history_content] in the global info file.
+## You can change this to a custom name if you want to use a different key
+## in the global save info file.
+var visited_event_save_key := "visited_event_history_content"
+
+## Whether to automatically save the already-visited history on auto-save.
+var save_visited_history_on_autosave := false:
+ set(value):
+ save_visited_history_on_autosave = value
+ _update_saved_connection(value)
+
+
+## Whether to automatically save the already-visited history on manual save.
+var save_visited_history_on_save := false:
+ set(value):
+ save_visited_history_on_save = value
+ _update_saved_connection(value)
+
+
+## Starts and stops the connection to the [subsystem Save] subsystem's [signal saved] signal.
+func _update_saved_connection(to_connect: bool) -> void:
+ if to_connect:
+ if not DialogicUtil.autoload().Save.saved.is_connected(_on_save):
+ DialogicUtil.autoload().Save.saved.connect(_on_save)
+
+ else:
+ if DialogicUtil.autoload().Save.saved.is_connected(_on_save):
+ DialogicUtil.autoload().Save.saved.disconnect(_on_save)
+
+
+#region INITIALIZE
+####################################################################################################
+
+func _ready() -> void:
+ dialogic.event_handled.connect(store_full_event)
+ dialogic.event_handled.connect(_check_seen)
+
+ simple_history_enabled = ProjectSettings.get_setting('dialogic/history/simple_history_enabled', simple_history_enabled)
+ simple_history_save = ProjectSettings.get_setting('dialogic/history/simple_history_save', simple_history_save)
+ full_event_history_enabled = ProjectSettings.get_setting('dialogic/history/full_history_enabled', full_event_history_enabled)
+ full_event_history_save = ProjectSettings.get_setting('dialogic/history/full_history_save', full_event_history_save)
+ visited_event_history_enabled = ProjectSettings.get_setting('dialogic/history/visited_event_history_enabled', visited_event_history_enabled)
+
+
+
+func _on_save(info: Dictionary) -> void:
+ var is_autosave: bool = info["is_autosave"]
+
+ var save_on_autosave := save_visited_history_on_autosave and is_autosave
+ var save_on_save := save_visited_history_on_save and not is_autosave
+
+ if save_on_save or save_on_autosave:
+ save_visited_history()
+
+
+func post_install() -> void:
+ save_visited_history_on_autosave = ProjectSettings.get_setting('dialogic/history/save_on_autosave', save_visited_history_on_autosave)
+ save_visited_history_on_save = ProjectSettings.get_setting('dialogic/history/save_on_save', save_visited_history_on_save)
+
+
+func clear_game_state(clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ if clear_flag == DialogicGameHandler.ClearFlags.FULL_CLEAR:
+ if simple_history_save:
+ simple_history_content = []
+ dialogic.current_state_info.erase("history_simple")
+ if full_event_history_save:
+ full_event_history_content = []
+ dialogic.current_state_info.erase("history_full")
+
+
+func load_game_state(load_flag := LoadFlags.FULL_LOAD) -> void:
+ if load_flag == LoadFlags.FULL_LOAD:
+ if simple_history_save and dialogic.current_state_info.has("history_simple"):
+ simple_history_content.assign(dialogic.current_state_info["history_simple"])
+
+ if full_event_history_save and dialogic.current_state_info.has("history_full"):
+ full_event_history_content = []
+
+ for event_text in dialogic.current_state_info["history_full"]:
+ var event: DialogicEvent
+ for i in DialogicResourceUtil.get_event_cache():
+ if i.is_valid_event(event_text):
+ event = i.duplicate()
+ break
+ event.from_text(event_text)
+ full_event_history_content.append(event)
+
+
+func save_game_state() -> void:
+ if simple_history_save:
+ dialogic.current_state_info["history_simple"] = Array(simple_history_content)
+ else:
+ dialogic.current_state_info.erase("history_simple")
+ if full_event_history_save:
+ dialogic.current_state_info["history_full"] = []
+ for event in full_event_history_content:
+ dialogic.current_state_info["history_full"].append(event.to_text())
+ else:
+ dialogic.current_state_info.erase("history_full")
+
+
+func open_history() -> void:
+ open_requested.emit()
+
+
+func close_history() -> void:
+ close_requested.emit()
+
+#endregion
+
+
+#region SIMPLE HISTORY
+####################################################################################################
+
+func store_simple_history_entry(text:String, event_type:String, extra_info := {}) -> void:
+ if !simple_history_enabled: return
+ extra_info['text'] = text
+ extra_info['event_type'] = event_type
+ simple_history_content.append(extra_info)
+ simple_history_changed.emit()
+
+
+func get_simple_history() -> Array:
+ return simple_history_content
+
+#endregion
+
+
+#region FULL EVENT HISTORY
+####################################################################################################
+
+## Called on each event.
+func store_full_event(event: DialogicEvent) -> void:
+ if !full_event_history_enabled: return
+ full_event_history_content.append(event)
+ full_event_history_changed.emit()
+
+
+#region ALREADY READ HISTORY
+####################################################################################################
+
+## Takes the current timeline event and creates a unique key for it.
+## Uses the timeline resource path as well.
+func _current_event_key() -> String:
+ var resource_path := dialogic.current_timeline.resource_path
+ var event_index := dialogic.current_event_idx
+ var event_key := _get_event_key(event_index, resource_path)
+
+ return event_key
+
+## Composes an event key from the event index and the timeline path.
+## If either of these variables are in an invalid state, the resulting
+## key may be wrong.
+## There are no safety checks in place.
+func _get_event_key(event_index: int, timeline_path: String) -> String:
+ var event_idx := str(event_index)
+ var event_key := timeline_path + event_idx
+
+ return event_key
+
+
+## Called if an event is marked as visited.
+func mark_event_as_visited(event_index := dialogic.current_event_idx, timeline := dialogic.current_timeline) -> void:
+ if !visited_event_history_enabled:
+ return
+
+ var event_key := _get_event_key(event_index, timeline.resource_path)
+
+ visited_event_history_content[event_key] = event_index
+
+
+## Called on each event, but we filter for Text events.
+func _check_seen(event: DialogicEvent) -> void:
+ if !visited_event_history_enabled:
+ return
+
+ # At this point, we only care about Text events.
+ # There may be a more elegant way of filtering events.
+ # Especially since custom events require this event name.
+ if event.event_name != "Text":
+ return
+
+ var event_key := _current_event_key()
+
+ if event_key in visited_event_history_content:
+ visited_event.emit()
+ _visited_last_event = true
+
+ else:
+ unvisited_event.emit()
+ _visited_last_event = false
+
+
+## Whether the last event has been visited for the first time or not.
+## This will return `true` exactly once for each unique timeline event instance.
+func has_last_event_been_visited() -> bool:
+ return _visited_last_event
+
+
+## If called with with no arguments, the method will return whether
+## the last encountered event was visited before.
+##
+## Otherwise, if [param event_index] and [param timeline] are passed,
+## the method will check if the event from that given timeline has been
+## visited yet.
+##
+## If no [param timeline] is passed, the current timeline will be used.
+## If there is no current timeline, `false` will be returned.
+##
+## If no [param event_index] is passed, the current event index will be used.
+func has_event_been_visited(event_index := dialogic.current_event_idx, timeline := dialogic.current_timeline) -> bool:
+ if timeline == null:
+ return false
+
+ var event_key := _get_event_key(event_index, timeline.resource_path)
+ var visited := event_key in visited_event_history_content
+
+ return visited
+
+
+## Saves all seen events to the global info file.
+## This can be useful when the player saves the game.
+## In visual novels, callings this at the end of a route can be useful, as the
+## player may not save the game.
+##
+## Be aware, this won't add any events but completely overwrite the already saved ones.
+##
+## Relies on the [subsystem Save] subsystem.
+func save_visited_history() -> void:
+ DialogicUtil.autoload().Save.set_global_info(visited_event_save_key, visited_event_history_content)
+
+
+## Loads the seen events from the global info save file.
+## Calling this when a game gets loaded may be useful.
+##
+## Relies on the [subsystem Save] subsystem.
+func load_visited_history() -> void:
+ visited_event_history_content = get_saved_visited_history()
+
+
+## Returns the saved already-visited history from the global info save file.
+## If none exist in the global info file, returns an empty dictionary.
+##
+## Relies on the [subsystem Save] subsystem.
+func get_saved_visited_history() -> Dictionary:
+ return DialogicUtil.autoload().Save.get_global_info(visited_event_save_key, {})
+
+
+## Resets the already-visited history in the global info save file.
+## If [param reset_property] is true, it will also reset the already-visited
+## history in the Dialogic Autoload.
+##
+## Relies on the [subsystem Save] subsystem.
+func reset_visited_history(reset_property := true) -> void:
+ DialogicUtil.autoload().Save.set_global_info(visited_event_save_key, {})
+
+ if reset_property:
+ visited_event_history_content = {}
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicJumpEvent
+extends DialogicEvent
+
+## Event that allows starting another timeline. Also can jump to a label in that or the current timeline.
+
+
+### Settings
+
+## The timeline to jump to, if null then it's the current one. This setting should be a dialogic timeline resource.
+var timeline: DialogicTimeline
+## If not empty, the event will try to find a Label event with this set as name. Empty by default..
+var label_name := ""
+
+
+### Helpers
+
+## Used to set the timeline resource from the unique name identifier and vice versa
+var timeline_identifier := "":
+ get:
+ if timeline:
+ var identifier := DialogicResourceUtil.get_unique_identifier(timeline.resource_path)
+ if not identifier.is_empty():
+ return identifier
+ return timeline_identifier
+ set(value):
+ timeline_identifier = value
+ timeline = DialogicResourceUtil.get_timeline_resource(value)
+ if (not timeline_identifier in DialogicResourceUtil.get_label_cache().keys()
+ or not label_name in DialogicResourceUtil.get_label_cache()[timeline_identifier]):
+ label_name = ""
+ ui_update_needed.emit()
+
+
+################################################################################
+## EXECUTION
+################################################################################
+
+func _execute() -> void:
+ dialogic.Jump.push_to_jump_stack()
+ if timeline and timeline != dialogic.current_timeline:
+ dialogic.Jump.switched_timeline.emit({'previous_timeline':dialogic.current_timeline, 'timeline':timeline, 'label':label_name})
+ dialogic.start_timeline(timeline, label_name)
+ else:
+ if label_name:
+ dialogic.Jump.jump_to_label(label_name)
+ finish()
+ else:
+ dialogic.start_timeline(dialogic.current_timeline)
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Jump"
+ set_default_color('Color4')
+ event_category = "Flow"
+ event_sorting_index = 4
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon_jump.png'))
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+func to_text() -> String:
+ var result := "jump "
+ if timeline_identifier:
+ result += timeline_identifier+'/'
+ if label_name:
+ result += label_name
+ elif label_name:
+ result += label_name
+ return result
+
+
+func from_text(string:String) -> void:
+ var result := RegEx.create_from_string(r"jump (?<timeline>.*\/)?(?<label>.*)?").search(string.strip_edges())
+ if result:
+ timeline_identifier = result.get_string('timeline').trim_suffix('/')
+ label_name = result.get_string('label')
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges().begins_with("jump"):
+ return true
+ return false
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "timeline" : {"property": "timeline_identifier", "default": null,
+ "suggestions": get_timeline_suggestions},
+ "label" : {"property": "label_name", "default": ""},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('timeline_identifier', ValueType.DYNAMIC_OPTIONS, {'left_text':'Jump to',
+ 'file_extension': '.dtl',
+ 'mode' : 2,
+ 'suggestions_func': get_timeline_suggestions,
+ 'editor_icon' : ["TripleBar", "EditorIcons"],
+ 'empty_text' : '(this timeline)',
+ 'autofocus' : true,
+ })
+ add_header_edit("label_name", ValueType.DYNAMIC_OPTIONS, {'left_text':"at",
+ 'empty_text':'the beginning',
+ 'suggestions_func':get_label_suggestions,
+ 'editor_icon':["ArrowRight", "EditorIcons"]})
+
+
+func get_timeline_suggestions(_filter:String= "") -> Dictionary:
+ var suggestions := {}
+
+ suggestions['(this timeline)'] = {'value':'', 'editor_icon':['GuiRadioUnchecked', 'EditorIcons']}
+ for resource in DialogicResourceUtil.get_timeline_directory().keys():
+ suggestions[resource] = {'value': resource, 'tooltip':DialogicResourceUtil.get_timeline_directory()[resource], 'editor_icon': ["TripleBar", "EditorIcons"]}
+ return suggestions
+
+
+func get_label_suggestions(_filter:String="") -> Dictionary:
+ var suggestions := {}
+ suggestions['at the beginning'] = {'value':'', 'editor_icon':['GuiRadioUnchecked', 'EditorIcons']}
+ if timeline_identifier in DialogicResourceUtil.get_label_cache().keys():
+ for label in DialogicResourceUtil.get_label_cache()[timeline_identifier]:
+ suggestions[label] = {'value': label, 'tooltip':label, 'editor_icon': ["ArrowRight", "EditorIcons"]}
+ return suggestions
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ if symbol == ' ' and line.count(' ') == 1:
+ CodeCompletionHelper.suggest_labels(TextNode, '', '\n', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6))
+ CodeCompletionHelper.suggest_timelines(TextNode, CodeEdit.KIND_MEMBER, event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6))
+ if symbol == '/':
+ CodeCompletionHelper.suggest_labels(TextNode, line.strip_edges().trim_prefix('jump ').trim_suffix('/'+String.chr(0xFFFF)).strip_edges(), '\n', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6))
+
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'jump', 'jump ', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3))
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ dict[line.find('jump')] = {"color":event_color.lerp(Highlighter.normal_color, 0.3)}
+ dict[line.find('jump')+4] = {"color":event_color.lerp(Highlighter.normal_color, 0.5)}
+ return dict
--- /dev/null
+@tool
+class_name DialogicLabelEvent
+extends DialogicEvent
+
+## Event that is used as an anchor. You can use the DialogicJumpEvent to jump to this point.
+
+
+### Settings
+
+## Used to identify the label. Duplicate names in a timeline will mean it always chooses the first.
+var name := ""
+var display_name := ""
+
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ # This event is mainly implemented in the Jump subsystem.
+ dialogic.Jump.passed_label.emit(
+ {
+ "identifier": name,
+ "display_name": get_property_translated("display_name"),
+ "display_name_orig": display_name,
+ "timeline": DialogicResourceUtil.get_unique_identifier(dialogic.current_timeline.resource_path)
+ })
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Label"
+ set_default_color('Color4')
+ event_category = "Flow"
+ event_sorting_index = 3
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon_label.png'))
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+func to_text() -> String:
+ if display_name.is_empty():
+ return "label "+name
+ else:
+ return "label "+name+ " ("+display_name+")"
+
+
+
+func from_text(string:String) -> void:
+ var regex := RegEx.create_from_string(r'label +(?<name>[^(]+)(\((?<display_name>.+)\))?')
+ var result := regex.search(string.strip_edges())
+ if result:
+ name = result.get_string('name').strip_edges()
+ display_name = result.get_string('display_name').strip_edges()
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges().begins_with("label"):
+ return true
+ return false
+
+
+# this is only here to provide a list of default values
+# this way the module manager can add custom default overrides to this event.
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "name" : {"property": "name", "default": ""},
+ "display" : {"property": "display_name", "default": ""},
+ }
+
+
+func _get_translatable_properties() -> Array:
+ return ["display_name"]
+
+
+func _get_property_original_translation(property_name:String) -> String:
+ match property_name:
+ 'display_name':
+ return display_name
+ return ''
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('name', ValueType.SINGLELINE_TEXT, {'left_text':'Label', 'autofocus':true})
+ add_body_edit('display_name', ValueType.SINGLELINE_TEXT, {'left_text':'Display Name:'})
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'label', 'label ', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3))
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ dict[line.find('label')] = {"color":event_color.lerp(Highlighter.normal_color, 0.3)}
+ dict[line.find('label')+5] = {"color":event_color.lerp(Highlighter.normal_color, 0.5)}
+ return dict
--- /dev/null
+@tool
+class_name DialogicReturnEvent
+extends DialogicEvent
+
+## Event that will make dialogic jump back to the last jump point.
+
+
+
+################################################################################
+## EXECUTION
+################################################################################
+
+func _execute() -> void:
+ if !dialogic.Jump.is_jump_stack_empty():
+ dialogic.Jump.resume_from_last_jump()
+ else:
+ dialogic.end_timeline()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Return"
+ set_default_color('Color4')
+ event_category = "Flow"
+ event_sorting_index = 5
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon_return.svg'))
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+func to_text() -> String:
+ return "return"
+
+
+func from_text(_string:String) -> void:
+ pass
+
+
+func is_valid_event(string:String) -> bool:
+ if string.strip_edges() == "return":
+ return true
+ return false
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_label('Return')
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'return', 'return\n', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3))
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ dict[line.find('return')] = {"color":event_color.lerp(Highlighter.normal_color, 0.3)}
+ dict[line.find('return')+6] = {"color":event_color.lerp(Highlighter.normal_color, 0.5)}
+ return dict
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="return-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon_return.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#464646" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="1.5898438" inkscape:cx="-188.38329" inkscape:cy="-45.916462" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
+ <defs id="defs2" />
+ <path id="path12307" style="stroke-width:0.962584;stroke-dasharray:none;fill:#ffffff;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round" d="M 10.031312,2.6802332 5.2444126,5.3787436 9.1771421,8.4917066 9.4598944,6.5688835 C 14.624065,7.1434 13.406023,13.790987 4.5579588,13.899478 17.332931,13.799654 17.986687,5.0197568 9.7415716,4.652511 Z" transform="matrix(0.9613292,0,0,0.9613292,-0.91886125,0.49040979)" />
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_jump.gd'), this_folder.path_join('event_label.gd'), this_folder.path_join('event_return.gd')]
+
+func _get_subsystems() -> Array:
+ return [{'name':'Jump', 'script':this_folder.path_join('subsystem_jump.gd')}]
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that holds methods for jumping to specific labels, or return to the previous jump.
+
+signal switched_timeline(info:Dictionary)
+signal jumped_to_label(info:Dictionary)
+signal returned_from_jump(info:Dictionary)
+signal passed_label(info:Dictionary)
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ dialogic.current_state_info['jump_stack'] = []
+ dialogic.current_state_info.erase("last_label")
+
+
+func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
+ if not 'jump_stack' in dialogic.current_state_info:
+ dialogic.current_state_info['jump_stack'] = []
+
+#endregion
+
+
+#region MAIN METHODS JUMP
+####################################################################################################
+
+func jump_to_label(label:String) -> void:
+ if label.is_empty():
+ dialogic.current_event_idx = 0
+ jumped_to_label.emit({'timeline':dialogic.current_timeline, 'label':"TOP"})
+ return
+
+ var idx: int = -1
+ while true:
+ idx += 1
+ var event: Variant = dialogic.current_timeline.get_event(idx)
+ if not event:
+ idx = dialogic.current_event_idx
+ break
+ if event is DialogicLabelEvent and event.name == label:
+ break
+ dialogic.current_event_idx = idx-1
+ jumped_to_label.emit({'timeline':dialogic.current_timeline, 'label':label})
+
+
+func push_to_jump_stack() -> void:
+ dialogic.current_state_info['jump_stack'].push_back({'timeline':dialogic.current_timeline, 'index':dialogic.current_event_idx, 'label':dialogic.current_timeline_events[dialogic.current_event_idx].label_name})
+
+
+func resume_from_last_jump() -> void:
+ var sub_timeline: DialogicTimeline = dialogic.current_timeline
+ var stack_info: Dictionary = dialogic.current_state_info['jump_stack'].pop_back()
+ dialogic.start_timeline(stack_info.timeline, stack_info.index+1)
+ returned_from_jump.emit({'sub_timeline':sub_timeline, 'label':stack_info.label})
+
+
+func is_jump_stack_empty() -> bool:
+ return len(dialogic.current_state_info['jump_stack']) < 1
+
+#endregion
+
+
+#region MAIN MEHTODS LABELS
+####################################################################################################
+
+func _ready() -> void:
+ passed_label.connect(_on_passed_label)
+
+
+func _on_passed_label(info:Dictionary) -> void:
+ dialogic.current_state_info["last_label"] = info
+
+
+## Returns the identifier name of the last passed label
+func get_last_label_identifier() -> String:
+ if not dialogic.current_state_info.has("last_label"):
+ return ""
+
+ return dialogic.current_state_info["last_label"].identifier
+
+
+## Returns the display name of the last passed label (translated if translation are enabled)
+func get_last_label_name() -> String:
+ if not dialogic.current_state_info.has("last_label"):
+ return ""
+
+ return dialogic.current_state_info["last_label"].display_name
+#endregion
--- /dev/null
+@tool
+extends "res://addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd"
+
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_portrait_scene_presets() -> Array[Dictionary]:
+ return [
+ {
+ "path": this_folder.path_join("layered_portrait.tscn"),
+ "name": "Layered Portrait",
+ "description": "Base for a charcter made up of multiple sprites. Allows showing/switching/hiding the layers with the character event extra data.",
+ "author":"Cake for Dialogic",
+ "type": "Preset",
+ "icon":"",
+ "preview_image":[this_folder.path_join("layered_portrait_thumbnail.png")],
+ "documentation":"https://docs.dialogic.pro/layered-portraits.html",
+ },
+ ]
--- /dev/null
+@tool
+## Layered portrait scene.
+##
+## The parent class has a character and portrait variable.
+extends DialogicPortrait
+
+## The term used for hiding a layer.
+const _HIDE_COMMAND := "hide"
+## The term used for showing a layer.
+const _SHOW_COMMAND := "show"
+## The term used for setting a layer to be the only visible layer.
+const _SET_COMMAND := "set"
+
+## A collection of all possible layer commands.
+const _OPERATORS = [_HIDE_COMMAND, _SHOW_COMMAND, _SET_COMMAND]
+
+static var _OPERATORS_EXPRESSION := "|".join(_OPERATORS)
+static var _REGEX_STRING := "(" + _OPERATORS_EXPRESSION + ") (\\S+)"
+static var _REGEX := RegEx.create_from_string(_REGEX_STRING)
+
+var _initialized := false
+
+var _is_coverage_rect_cached := false
+var _cached_coverage_rect := Rect2(0, 0, 0, 0)
+
+
+@export_group("Private")
+## Attempts to fix the offset based on the first child node
+@export var fix_offset := true
+
+## Overriding [class DialogicPortrait]'s method.
+##
+## Load anything related to the given character and portrait
+func _update_portrait(passed_character: DialogicCharacter, passed_portrait: String) -> void:
+ if not _initialized:
+ _apply_layer_adjustments()
+ _initialized = true
+
+ apply_character_and_portrait(passed_character, passed_portrait)
+
+
+## Modifies all layers to fit the portrait preview and appear correctly in
+## portrait containers.
+##
+## This method is not changing the scene itself and is intended for the
+## Dialogic editor preview and in-game rendering only.
+func _apply_layer_adjustments() -> void:
+ var coverage := _find_largest_coverage_rect()
+ var offset_fix := Vector2()
+ if fix_offset and get_child_count():
+ offset_fix = -get_child(0).position
+ if "centered" in get_child(0) and get_child(0).centered:
+ offset_fix += get_child(0).get_rect().size/2.0
+
+ for node: Node in get_children():
+ var node_position: Vector2 = node.position
+ node_position += offset_fix
+ node.position = _reposition_with_rect(coverage, node_position)
+
+
+## Returns a position based on [param rect]'s size where Dialogic expects the
+## scene part to be positioned at. [br]
+## If the node has an offset or extra position, pass it as [param node_offset].
+func _reposition_with_rect(rect: Rect2, node_offset := Vector2(0.0, 0.0)) -> Vector2:
+ return rect.size * Vector2(-0.5, -1.0) + node_offset
+
+
+## Iterates over all children in [param start_node] and its children, looking
+## for [class Sprite2D] nodes with a texture (not `null`).
+## All found sprites are returned in an array, eventually returning all
+## sprites in the scene.
+func _find_sprites_recursively(start_node: Node) -> Array[Sprite2D]:
+ var sprites: Array[Sprite2D] = []
+
+ # Iterate through the children of the current node
+ for child: Node in start_node.get_children():
+
+ if child is Sprite2D:
+ var sprite := child as Sprite2D
+
+ if sprite.texture:
+ sprites.append(sprite)
+
+
+ var sub := _find_sprites_recursively(child)
+ sprites.append_array(sub)
+
+ return sprites
+
+
+## A command will apply an effect to the layered portrait.
+class LayerCommand:
+ # The different types of effects.
+ enum CommandType {
+ ## Additively Show a specific layer.
+ SHOW_LAYER,
+ ## Subtractively hide a specific layer.
+ HIDE_LAYER,
+ ## Exclusively show a specific layer, hiding all other sibling layers.
+ ## A sibling layer is a layer sharing the same parent node.
+ SET_LAYER,
+ }
+
+ var _path: String
+ var _type: CommandType
+
+ ## Executes the effect of the layer based on the [enum CommandType].
+ func _execute(root: Node) -> void:
+ var target_node := root.get_node(_path)
+
+ if target_node == null:
+ printerr("Layered Portrait had no node matching the node path: '", _path, "'.")
+ return
+
+ if not target_node is Node2D and not target_node is Sprite2D:
+ printerr("Layered Portrait target path '", _path, "', is not a Sprite2D or Node2D type.")
+ return
+
+ match _type:
+ CommandType.SHOW_LAYER:
+ target_node.show()
+
+ CommandType.HIDE_LAYER:
+ target_node.hide()
+
+ CommandType.SET_LAYER:
+ var target_parent := target_node.get_parent()
+
+ for child: Node in target_parent.get_children():
+
+ if child is Sprite2D:
+ var sprite_child := child as Sprite2D
+ sprite_child.hide()
+
+ target_node.show()
+
+
+## Turns the input into a single [class LayerCommand] object.
+## Returns `null` if the input cannot be parsed into a [class LayerCommand].
+func _parse_layer_command(input: String) -> LayerCommand:
+ var regex_match: RegExMatch = _REGEX.search(input)
+
+ if regex_match == null:
+ print("[Dialogic] Layered Portrait had an invalid command: ", input)
+ return null
+
+ var _path: String = regex_match.get_string(2)
+ var operator: String = regex_match.get_string(1)
+
+ var command := LayerCommand.new()
+
+ match operator:
+ _SET_COMMAND:
+ command._type = LayerCommand.CommandType.SET_LAYER
+
+ _SHOW_COMMAND:
+ command._type = LayerCommand.CommandType.SHOW_LAYER
+
+ _HIDE_COMMAND:
+ command._type = LayerCommand.CommandType.HIDE_LAYER
+
+ _SET_COMMAND:
+ command._type = LayerCommand.CommandType.SET_LAYER
+
+ ## We clean escape symbols and trim the spaces.
+ command._path = _path.replace("\\", "").strip_edges()
+
+ return command
+
+
+## Parses [param input] into an array of [class LayerCommand] objects.
+func _parse_input_to_layer_commands(input: String) -> Array[LayerCommand]:
+ var commands: Array[LayerCommand] = []
+ var command_parts := input.split(",")
+
+ for command_part: String in command_parts:
+
+ if command_part.is_empty():
+ continue
+
+ var _command := _parse_layer_command(command_part.strip_edges())
+
+ if not _command == null:
+ commands.append(_command)
+
+ return commands
+
+
+
+## Overriding [class DialogicPortrait]'s method.
+##
+## The extra data will be turned into layer commands and then be executed.
+func _set_extra_data(data: String) -> void:
+ var commands := _parse_input_to_layer_commands(data)
+
+ for _command: LayerCommand in commands:
+ _command._execute(self)
+
+
+## Overriding [class DialogicPortrait]'s method.
+##
+## Handling all layers horizontal flip state.
+func _set_mirror(is_mirrored: bool) -> void:
+ for child: Node in get_children():
+
+ if is_mirrored:
+ child.position.x = child.position.x * -1
+ child.scale.x = -child.scale.x
+
+
+## Scans all nodes in this scene and finds the largest rectangle that
+## covers encloses every sprite.
+func _find_largest_coverage_rect() -> Rect2:
+ if _is_coverage_rect_cached:
+ return _cached_coverage_rect
+
+ var coverage_rect := Rect2(0, 0, 0, 0)
+
+ for sprite: Sprite2D in _find_sprites_recursively(self):
+ var sprite_size := sprite.get_rect().size
+ var sprite_position: Vector2 = sprite.global_position-self.global_position
+
+ if sprite.centered:
+ sprite_position -= sprite_size/2
+
+ var sprite_width := sprite_size.x * sprite.scale.x
+ var sprite_height := sprite_size.y * sprite.scale.y
+
+ var texture_rect := Rect2(
+ sprite_position.x,
+ sprite_position.y,
+ sprite_width,
+ sprite_height
+ )
+ coverage_rect = coverage_rect.merge(texture_rect)
+
+ coverage_rect.position = _reposition_with_rect(coverage_rect)
+
+ _is_coverage_rect_cached = true
+ _cached_coverage_rect = coverage_rect
+
+ return coverage_rect
+
+
+## Overriding [class DialogicPortrait]'s method.
+##
+## Called by Dialogic when the portrait is needed to be shown.
+## For instance, in the Dialogic editor or in-game.
+func _get_covered_rect() -> Rect2:
+ var needed_rect := _find_largest_coverage_rect()
+
+ return needed_rect
--- /dev/null
+[gd_scene load_steps=2 format=3 uid="uid://jac4eurttev1"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/LayeredPortrait/custom_layered_portrait.gd" id="1_uubi5"]
+
+[node name="LayeredPortrait" type="CanvasGroup"]
+script = ExtResource("1_uubi5")
+
+[node name="Layer1" type="Sprite2D" parent="."]
+
+[node name="Group1" type="Node2D" parent="."]
+
+[node name="Layer1" type="Sprite2D" parent="Group1"]
+
+[node name="Layer2" type="Sprite2D" parent="Group1"]
--- /dev/null
+@tool
+class_name DialogicSaveEvent
+extends DialogicEvent
+
+## Event that allows saving to a specific slot.
+
+
+### Settings
+
+## The name of the slot to save to. Learn more in the saving subsystem.
+## If empty, the event will attempt to save to the latest slot, and otherwise use the default.
+var slot_name := ""
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _execute() -> void:
+ if slot_name.is_empty():
+ if dialogic.Save.get_latest_slot():
+ dialogic.Save.save(dialogic.Save.get_latest_slot())
+ else:
+ dialogic.Save.save()
+ else:
+ dialogic.Save.save(slot_name)
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Save"
+ set_default_color('Color6')
+ event_category = "Other"
+ event_sorting_index = 0
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon.svg'))
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "save"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "slot" : {"property": "slot_name", "default": "Default"},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('slot_name', ValueType.SINGLELINE_TEXT, {'left_text':'Save to slot'})
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="save-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="2.3119079" inkscape:cx="-62.069948" inkscape:cy="-17.301728" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
+ <defs id="defs2" />
+ <path id="path19705" style="stroke-width:0.703602;stroke-dasharray:none;fill:#ffffff;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round" d="M 3.1598245,1.4940293 V 13.603677 H 14.561748 V 4.1279555 l -2.75211,-2.6339262 z m 1.1818312,0.81583 H 10.936549 V 5.5452368 H 4.3416557 Z M 8.861015,7.5286978 A 2.4575739,2.4575739 0 0 1 11.318583,9.9862656 2.4575739,2.4575739 0 0 1 8.861015,12.443833 2.4575739,2.4575739 0 0 1 6.4034473,9.9862656 2.4575739,2.4575739 0 0 1 8.861015,7.5286978 Z" transform="matrix(0.82460301,0,0,0.82460301,1.1600358,2.2418596)" />
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_save.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Save', 'script':this_folder.path_join('subsystem_save.gd')}]
+
+
+func _get_settings_pages() -> Array:
+ return [this_folder.path_join('settings_save.tscn')]
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+## Settings page that contains settings for the saving subsystem
+
+
+func _get_priority() -> int:
+ return 0
+
+
+func _refresh() -> void:
+ %Autosave.button_pressed = ProjectSettings.get_setting('dialogic/save/autosave', false)
+ %AutosaveMode.select(ProjectSettings.get_setting('dialogic/save/autosave_mode', 0))
+ %AutosaveDelay.value = ProjectSettings.get_setting('dialogic/save/autosave_delay', 60)
+
+ %AutosaveModeLabel.visible = %Autosave.button_pressed
+ %AutosaveModeContent.visible = %Autosave.button_pressed
+ %AutosaveDelay.visible = %AutosaveMode.selected == 1
+
+ %DefaultSaveSlotName.text = ProjectSettings.get_setting('dialogic/save/default_slot', 'Default')
+
+ %EncryptionPassword.text = ProjectSettings.get_setting('dialogic/save/encryption_password', "")
+ %EncryptionOnExportsSection.visible = !%EncryptionPassword.text.is_empty()
+ %EncryptionOnExports.button_pressed = ProjectSettings.get_setting('dialogic/save/encryption_on_exports_only', true)
+
+func _on_autosave_toggled(button_pressed:bool) -> void:
+ ProjectSettings.set_setting('dialogic/save/autosave', button_pressed)
+ ProjectSettings.save()
+ %AutosaveModeLabel.visible = button_pressed
+ %AutosaveModeContent.visible = button_pressed
+
+
+func _on_autosave_mode_item_selected(index:int):
+ ProjectSettings.set_setting('dialogic/save/autosave_mode', index)
+ ProjectSettings.save()
+ %AutosaveDelay.visible = %AutosaveMode.selected == 1
+
+
+func _on_autosave_delay_value_changed(value:float):
+ ProjectSettings.set_setting('dialogic/save/autosave_delay', value)
+ ProjectSettings.save()
+
+
+func _on_default_save_slot_name_text_changed(new_text:String):
+ ProjectSettings.set_setting('dialogic/save/default_slot', new_text)
+ ProjectSettings.save()
+
+
+func _on_encryption_password_text_changed(new_text: String) -> void:
+ ProjectSettings.set_setting('dialogic/save/encryption_password', new_text)
+ ProjectSettings.save()
+ %EncryptionOnExportsSection.visible = !new_text.is_empty()
+
+
+func _on_encryption_on_exports_toggled(toggled_on:bool) -> void:
+ ProjectSettings.set_setting('dialogic/save/encryption_on_exports_only', toggled_on)
+ ProjectSettings.save()
--- /dev/null
+[gd_scene load_steps=5 format=3 uid="uid://cd340w7blofak"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Save/settings_save.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_v2wt8"]
+
+[sub_resource type="Image" id="Image_oatpr"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_dbvsu"]
+image = SubResource("Image_oatpr")
+
+[node name="Saving" type="VBoxContainer"]
+offset_right = 1084.0
+offset_bottom = 212.0
+script = ExtResource("2")
+
+[node name="Grid" type="GridContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+columns = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Grid"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Grid/HBoxContainer"]
+layout_mode = 2
+text = "Autosave"
+
+[node name="HintTooltip" parent="Grid/HBoxContainer" instance=ExtResource("2_v2wt8")]
+layout_mode = 2
+tooltip_text = "If enabled dialogic will autosave the full state to the current slot depending on the autosave method."
+texture = SubResource("ImageTexture_dbvsu")
+hint_text = "If enabled dialogic will autosave the full state to the current slot depending on the autosave method."
+
+[node name="Autosave" type="CheckBox" parent="Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="AutosaveModeLabel" type="HBoxContainer" parent="Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="Grid/AutosaveModeLabel"]
+layout_mode = 2
+text = "Autosave Mode"
+
+[node name="AutosaveModeContent" type="HBoxContainer" parent="Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="AutosaveMode" type="OptionButton" parent="Grid/AutosaveModeContent"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 3
+selected = 0
+popup/item_0/text = "Timeline Start+End+Jump"
+popup/item_0/id = 0
+popup/item_1/text = "Each X seconds"
+popup/item_1/id = 1
+popup/item_2/text = "Every Text Event"
+popup/item_2/id = 2
+
+[node name="AutosaveDelay" type="SpinBox" parent="Grid/AutosaveModeContent"]
+unique_name_in_owner = true
+layout_mode = 2
+max_value = 1000.0
+suffix = "s"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="Grid"]
+layout_mode = 2
+
+[node name="Label4" type="Label" parent="Grid/HBoxContainer2"]
+layout_mode = 2
+text = "Default slot name"
+
+[node name="HintTooltip3" parent="Grid/HBoxContainer2" instance=ExtResource("2_v2wt8")]
+layout_mode = 2
+tooltip_text = "The name of the default slot. "
+texture = SubResource("ImageTexture_dbvsu")
+hint_text = "The name of the default slot. "
+
+[node name="DefaultSaveSlotName" type="LineEdit" parent="Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+expand_to_text_length = true
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="Grid"]
+layout_mode = 2
+
+[node name="EncryptionPasswordLabel" type="Label" parent="Grid/HBoxContainer3"]
+layout_mode = 2
+text = "Encryption Password"
+
+[node name="HintTooltip" parent="Grid/HBoxContainer3" instance=ExtResource("2_v2wt8")]
+layout_mode = 2
+tooltip_text = "The encryption password used to encrypt save files. When left empty, the save files will not be encrypted."
+texture = SubResource("ImageTexture_dbvsu")
+hint_text = "The encryption password used to encrypt save files. When left empty, the save files will not be encrypted."
+
+[node name="HBoxContainer4" type="HBoxContainer" parent="Grid"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="EncryptionPassword" type="LineEdit" parent="Grid/HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+expand_to_text_length = true
+
+[node name="EncryptionOnExportsSection" type="HBoxContainer" parent="Grid/HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="EncryptionPasswordLabel" type="Label" parent="Grid/HBoxContainer4/EncryptionOnExportsSection"]
+layout_mode = 2
+text = "Use on exports only"
+
+[node name="HintTooltip" parent="Grid/HBoxContainer4/EncryptionOnExportsSection" instance=ExtResource("2_v2wt8")]
+layout_mode = 2
+tooltip_text = "For easier debugging dialogic will only encrypt saves made by exported project.
+Exported projects with debug mode on or saves made when running in editor
+will not use encryption."
+texture = SubResource("ImageTexture_dbvsu")
+hint_text = "For easier debugging dialogic will only encrypt saves made by exported project.
+Exported projects with debug mode on or saves made when running in editor
+will not use encryption."
+
+[node name="EncryptionOnExports" type="CheckBox" parent="Grid/HBoxContainer4/EncryptionOnExportsSection"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[connection signal="toggled" from="Grid/Autosave" to="." method="_on_autosave_toggled"]
+[connection signal="item_selected" from="Grid/AutosaveModeContent/AutosaveMode" to="." method="_on_autosave_mode_item_selected"]
+[connection signal="value_changed" from="Grid/AutosaveModeContent/AutosaveDelay" to="." method="_on_autosave_delay_value_changed"]
+[connection signal="text_changed" from="Grid/DefaultSaveSlotName" to="." method="_on_default_save_slot_name_text_changed"]
+[connection signal="text_changed" from="Grid/HBoxContainer4/EncryptionPassword" to="." method="_on_encryption_password_text_changed"]
+[connection signal="toggled" from="Grid/HBoxContainer4/EncryptionOnExportsSection/EncryptionOnExports" to="." method="_on_encryption_on_exports_toggled"]
--- /dev/null
+extends DialogicSubsystem
+## Subsystem to save and load game states.
+##
+## This subsystem has many different helper methods to save Dialogic or custom
+## game data to named save slots.
+##
+## You can listen to saves via [signal saved]. \
+## If you want to save, you can call [method save]. \
+
+
+## Emitted when a save happened with the following info:
+## [br]
+## Key | Value Type | Value [br]
+## ----------- | ------------- | ----- [br]
+## `slot_name` | [type String] | The name of the slot that the game state was saved to. [br]
+## `is_autosave` | [type bool] | `true`, if the save was an autosave. [br]
+signal saved(info: Dictionary)
+
+
+## The directory that will be saved to.
+const SAVE_SLOTS_DIR := "user://dialogic/saves/"
+
+## The project settings key for the auto-save enabled settings.
+const AUTO_SAVE_SETTINGS := "dialogic/save/autosave"
+
+## The project settings key for the auto-save mode settings.
+const AUTO_SAVE_MODE_SETTINGS := "dialogic/save/autosave_mode"
+
+## The project settings key for the auto-save delay settings.
+const AUTO_SAVE_TIME_SETTINGS := "dialogic/save/autosave_delay"
+
+## Temporarily stores a taken screen capture when using [take_slot_image()].
+enum ThumbnailMode {NONE, TAKE_AND_STORE, STORE_ONLY}
+var latest_thumbnail: Image = null
+
+
+## The different types of auto-save triggers.
+## If one of these occurs in the game, an auto-save may happen
+## if [member autosave_enabled] is `true`.
+enum AutoSaveMode {
+ ## Includes timeline start, end, and jump events.
+ ON_TIMELINE_JUMPS = 0,
+ ## Saves after a certain time interval.
+ ON_TIMER = 1,
+ ## Saves after every text event.
+ ON_TEXT_EVENT = 2
+}
+
+## Whether the auto-save feature is enabled.
+## The initial value can be set in the project settings via th Dialogic editor.
+##
+## This can be toggled during the game.
+var autosave_enabled := false:
+ set(enabled):
+ autosave_enabled = enabled
+
+ if enabled:
+ autosave_timer.start()
+ else:
+ autosave_timer.stop()
+
+
+## Under what conditions the auto-save feature will trigger if
+## [member autosave_enabled] is `true`.
+var autosave_mode := AutoSaveMode.ON_TIMELINE_JUMPS
+
+## After what time interval the auto-save feature will trigger if
+## [member autosave_enabled] is `true` and [member autosave_mode] is
+## `AutoSaveMode.ON_TIMER`.
+var autosave_time := 60:
+ set(timer_time):
+ autosave_time = timer_time
+ autosave_timer.wait_time = timer_time
+
+
+#region STATE
+####################################################################################################
+
+## Built-in, called by DialogicGameHandler.
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ _make_sure_slot_dir_exists()
+
+
+## Built-in, called by DialogicGameHandler.
+func pause() -> void:
+ autosave_timer.paused = true
+
+
+## Built-in, called by DialogicGameHandler.
+func resume() -> void:
+ autosave_timer.paused = false
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## Saves the current state to the given slot.
+## If no slot is given, the default slot is used. You can change this name in
+## the Dialogic editor.
+## If you want to save to the last used slot, you can get its slot name with the
+## [method get_latest_slot()] method.
+func save(slot_name := "", is_autosave := false, thumbnail_mode := ThumbnailMode.TAKE_AND_STORE, slot_info := {}) -> Error:
+ # check if to save (if this is an autosave)
+ if is_autosave and !autosave_enabled:
+ return OK
+
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ set_latest_slot(slot_name)
+
+ var save_error := save_file(slot_name, 'state.txt', dialogic.get_full_state())
+
+ if save_error:
+ return save_error
+
+ if thumbnail_mode == ThumbnailMode.TAKE_AND_STORE:
+ take_thumbnail()
+ save_slot_thumbnail(slot_name)
+ elif thumbnail_mode == ThumbnailMode.STORE_ONLY:
+ save_slot_thumbnail(slot_name)
+
+ if slot_info:
+ set_slot_info(slot_name, slot_info)
+
+ saved.emit({"slot_name": slot_name, "is_autosave": is_autosave})
+ print('[Dialogic] Saved to slot "'+slot_name+'".')
+ return OK
+
+
+## Loads all info from the given slot in the DialogicGameHandler (Dialogic Autoload).
+## If no slot is given, the default slot is used.
+## To check if something is saved in that slot use has_slot().
+## If the slot does not exist, this method will fail.
+func load(slot_name := "") -> Error:
+ if slot_name.is_empty(): slot_name = get_default_slot()
+
+ if !has_slot(slot_name):
+ printerr("[Dialogic Error] Tried loading from invalid save slot '"+slot_name+"'.")
+ return ERR_FILE_NOT_FOUND
+
+ var set_latest_error := set_latest_slot(slot_name)
+ if set_latest_error:
+ push_error("[Dialogic Error]: Failed to store latest slot to global info. Error %d '%s'" % [set_latest_error, error_string(set_latest_error)])
+
+ var state: Dictionary = load_file(slot_name, 'state.txt', {})
+ dialogic.load_full_state(state)
+
+ if state.is_empty():
+ return FAILED
+ else:
+ return OK
+
+
+## Saves a variable to a file in the given slot.
+##
+## Be aware, the [param slot_name] will be used as a filesystem folder name.
+## Some operating systems do not support every character in folder names.
+## It is recommended to use only letters, numbers, and underscores.
+##
+## This method allows you to build your own save and load system.
+## You may be looking for the simple [method save] method to save the game state.
+func save_file(slot_name: String, file_name: String, data: Variant) -> Error:
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ if slot_name.is_empty():
+ push_error("[Dialogic Error]: No fallback slot name set.")
+ return ERR_FILE_NOT_FOUND
+
+ if !has_slot(slot_name):
+ add_empty_slot(slot_name)
+
+ var encryption_password := get_encryption_password()
+ var file: FileAccess
+
+ if encryption_password.is_empty():
+ file = FileAccess.open(SAVE_SLOTS_DIR.path_join(slot_name).path_join(file_name), FileAccess.WRITE)
+ else:
+ file = FileAccess.open_encrypted_with_pass(SAVE_SLOTS_DIR.path_join(slot_name).path_join(file_name), FileAccess.WRITE, encryption_password)
+
+ if file:
+ file.store_var(data)
+ return OK
+ else:
+ var error := FileAccess.get_open_error()
+ push_error("[Dialogic Error]: Could not save slot to file. Error: %d '%s'" % [error, error_string(error)])
+ return error
+
+
+## Loads a file using [param slot_name] and returns the contained info.
+##
+## This method allows you to build your own save and load system.
+## You may be looking for the simple [method load] method to load the game state.
+func load_file(slot_name: String, file_name: String, default: Variant) -> Variant:
+ if slot_name.is_empty(): slot_name = get_default_slot()
+
+ var path := get_slot_path(slot_name).path_join(file_name)
+ if FileAccess.file_exists(path):
+ var encryption_password := get_encryption_password()
+ var file: FileAccess
+
+ if encryption_password.is_empty():
+ file = FileAccess.open(path, FileAccess.READ)
+ else:
+ file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, encryption_password)
+
+ if file:
+ return file.get_var()
+ else:
+ push_error(FileAccess.get_open_error())
+ return default
+
+
+## Data set in global info can be accessed unrelated to the save slots.
+## For instance, you may want to store game settings in here, as they
+## affect the game globally unrelated to the slot used.
+func set_global_info(key: String, value: Variant) -> Error:
+ var global_info := ConfigFile.new()
+ var encryption_password := get_encryption_password()
+
+ if encryption_password.is_empty():
+ var load_error := global_info.load(SAVE_SLOTS_DIR.path_join('global_info.txt'))
+ if load_error:
+ printerr("[Dialogic Error]: Couldn't access global saved info file.")
+ return load_error
+
+ else:
+ global_info.set_value('main', key, value)
+ return global_info.save(SAVE_SLOTS_DIR.path_join('global_info.txt'))
+
+ else:
+ var load_error := global_info.load_encrypted_pass(SAVE_SLOTS_DIR.path_join('global_info.txt'), encryption_password)
+ if load_error:
+ printerr("[Dialogic Error]: Couldn't access global saved info file.")
+ return load_error
+
+ else:
+ global_info.set_value('main', key, value)
+ return global_info.save_encrypted_pass(SAVE_SLOTS_DIR.path_join('global_info.txt'), encryption_password)
+
+
+## Access the data unrelated to a save slot.
+## First, the data must have been set with [method set_global_info].
+func get_global_info(key: String, default: Variant) -> Variant:
+ var global_info := ConfigFile.new()
+ var encryption_password := get_encryption_password()
+
+ if encryption_password.is_empty():
+
+ if global_info.load(SAVE_SLOTS_DIR.path_join('global_info.txt')) == OK:
+ return global_info.get_value('main', key, default)
+
+ printerr("[Dialogic Error]: Couldn't access global saved info file.")
+
+ elif global_info.load_encrypted_pass(SAVE_SLOTS_DIR.path_join('global_info.txt'), encryption_password) == OK:
+ return global_info.get_value('main', key, default)
+
+ return default
+
+
+## Gets the encryption password from the project settings if it has been set.
+## If no password has been set, an empty string is returned.
+func get_encryption_password() -> String:
+ if OS.is_debug_build() and ProjectSettings.get_setting('dialogic/save/encryption_on_exports_only', true):
+ return ""
+ return ProjectSettings.get_setting("dialogic/save/encryption_password", "")
+
+#endregion
+
+
+#region SLOT HELPERS
+####################################################################################################
+## Returns a list of all available slots. Useful for iterating over all slots,
+## e.g., when building a UI with all save slots.
+func get_slot_names() -> Array[String]:
+ var save_folders: Array[String] = []
+
+ if DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
+ var directory := DirAccess.open(SAVE_SLOTS_DIR)
+ var _list_dir := directory.list_dir_begin()
+ var file_name := directory.get_next()
+
+ while not file_name.is_empty():
+
+ if directory.current_is_dir() and not file_name.begins_with("."):
+ save_folders.append(file_name)
+
+ file_name = directory.get_next()
+
+ return save_folders
+
+ return []
+
+
+## Returns true if the given slot exists.
+func has_slot(slot_name: String) -> bool:
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ return slot_name in get_slot_names()
+
+
+## Removes all the given slot along with all it's info/files.
+func delete_slot(slot_name: String) -> Error:
+ var path := SAVE_SLOTS_DIR.path_join(slot_name)
+
+ if DirAccess.dir_exists_absolute(path):
+ var directory := DirAccess.open(path)
+ if not directory:
+ return DirAccess.get_open_error()
+ var _list_dir := directory.list_dir_begin()
+ var file_name := directory.get_next()
+
+ while not file_name.is_empty():
+ var remove_error := directory.remove(file_name)
+ if remove_error:
+ push_warning("[Dialogic Error]: Encountered error while removing '%s': %d\t%s" % [path.path_join(file_name), remove_error, error_string(remove_error)])
+ file_name = directory.get_next()
+
+ # Delete the folder.
+ return directory.remove(SAVE_SLOTS_DIR.path_join(slot_name))
+
+ push_warning("[Dialogic Warning]: Save slot '%s' has already been deleted." % path)
+ return OK
+
+
+## This adds a new save folder with the given name
+func add_empty_slot(slot_name: String) -> Error:
+ if DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
+ var directory := DirAccess.open(SAVE_SLOTS_DIR)
+ if directory:
+ return directory.make_dir(slot_name)
+ return DirAccess.get_open_error()
+
+ push_error("[Dialogic Error]: Path to '%s' does not exist." % SAVE_SLOTS_DIR)
+ return ERR_FILE_BAD_PATH
+
+
+## Reset the state of the given save folder (or default)
+func reset_slot(slot_name := "") -> Error:
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ return save_file(slot_name, 'state.txt', {})
+
+
+## Returns the full path to the given slot folder
+func get_slot_path(slot_name: String) -> String:
+ return SAVE_SLOTS_DIR.path_join(slot_name)
+
+
+## Returns the default slot name defined in the dialogic settings
+func get_default_slot() -> String:
+ return ProjectSettings.get_setting('dialogic/save/default_slot', 'Default')
+
+
+## Returns the latest slot or empty if nothing was saved yet
+func get_latest_slot() -> String:
+ var latest_slot := ""
+
+ if Engine.get_main_loop().has_meta('dialogic_latest_saved_slot'):
+ latest_slot = Engine.get_main_loop().get_meta('dialogic_latest_saved_slot', '')
+
+ else:
+ latest_slot = get_global_info('latest_save_slot', '')
+ Engine.get_main_loop().set_meta('dialogic_latest_saved_slot', latest_slot)
+
+
+ if !has_slot(latest_slot):
+ return ''
+
+ return latest_slot
+
+
+func set_latest_slot(slot_name:String) -> Error:
+ Engine.get_main_loop().set_meta('dialogic_latest_saved_slot', slot_name)
+ return set_global_info('latest_save_slot', slot_name)
+
+
+func _make_sure_slot_dir_exists() -> Error:
+ if not DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
+ var make_dir_result := DirAccess.make_dir_recursive_absolute(SAVE_SLOTS_DIR)
+ if make_dir_result:
+ return make_dir_result
+
+ var global_info_path := SAVE_SLOTS_DIR.path_join('global_info.txt')
+
+ if not FileAccess.file_exists(global_info_path):
+ var config := ConfigFile.new()
+ var password := get_encryption_password()
+
+ if password.is_empty():
+ return config.save(global_info_path)
+
+ else:
+ return config.save_encrypted_pass(global_info_path, password)
+
+ return OK
+
+#endregion
+
+
+#region SLOT INFO
+####################################################################################################
+
+func set_slot_info(slot_name:String, info: Dictionary) -> Error:
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ return save_file(slot_name, 'info.txt', info)
+
+
+func get_slot_info(slot_name := "") -> Dictionary:
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ return load_file(slot_name, 'info.txt', {})
+
+#endregion
+
+
+#region SLOT IMAGE
+####################################################################################################
+
+## This method creates a thumbnail of the current game view, it allows to
+## save the game without having the UI on the save slot image.
+## The thumbnail will be stored in [member latest_thumbnail].
+##
+## Call this method before opening your save & load menu.
+## After that, call [method save] with [constant ThumbnailMode.STORE_ONLY].
+## The [method save] will automatically use the stored thumbnail.
+func take_thumbnail() -> void:
+ latest_thumbnail = get_viewport().get_texture().get_image()
+
+
+## No need to call from outside.
+## Used to store the latest thumbnail to the given slot.
+func save_slot_thumbnail(slot_name: String) -> Error:
+ if latest_thumbnail:
+ var path := get_slot_path(slot_name).path_join('thumbnail.png')
+ return latest_thumbnail.save_png(path)
+
+ push_warning("[Dialogic Warning]: No thumbnail has been set yet.")
+ return OK
+
+
+## Returns the thumbnail of the given slot.
+func get_slot_thumbnail(slot_name: String) -> ImageTexture:
+ if slot_name.is_empty():
+ slot_name = get_default_slot()
+
+ var path := get_slot_path(slot_name).path_join('thumbnail.png')
+
+ if FileAccess.file_exists(path):
+ return ImageTexture.create_from_image(Image.load_from_file(path))
+
+ return null
+
+#endregion
+
+
+#region AUTOSAVE
+####################################################################################################
+## Reference to the autosave timer.
+var autosave_timer := Timer.new()
+
+
+func _ready() -> void:
+ autosave_timer.one_shot = true
+ DialogicUtil.update_timer_process_callback(autosave_timer)
+ autosave_timer.name = "AutosaveTimer"
+ var _result := autosave_timer.timeout.connect(_on_autosave_timer_timeout)
+ add_child(autosave_timer)
+
+ autosave_enabled = ProjectSettings.get_setting(AUTO_SAVE_SETTINGS, autosave_enabled)
+ autosave_mode = ProjectSettings.get_setting(AUTO_SAVE_MODE_SETTINGS, autosave_mode)
+ autosave_time = ProjectSettings.get_setting(AUTO_SAVE_TIME_SETTINGS, autosave_time)
+
+ _result = dialogic.event_handled.connect(_on_dialogic_event_handled)
+ _result = dialogic.timeline_started.connect(_on_start_or_end_autosave)
+ _result = dialogic.timeline_ended.connect(_on_start_or_end_autosave)
+
+ if autosave_enabled:
+ autosave_timer.start(autosave_time)
+
+
+func _on_autosave_timer_timeout() -> void:
+ if autosave_mode == AutoSaveMode.ON_TIMER:
+ perform_autosave()
+
+ autosave_timer.start(autosave_time)
+
+
+func _on_dialogic_event_handled(event: DialogicEvent) -> void:
+ if event is DialogicJumpEvent:
+
+ if autosave_mode == AutoSaveMode.ON_TIMELINE_JUMPS:
+ perform_autosave()
+
+ if event is DialogicTextEvent:
+
+ if autosave_mode == AutoSaveMode.ON_TEXT_EVENT:
+ perform_autosave()
+
+
+func _on_start_or_end_autosave() -> void:
+ if autosave_mode == AutoSaveMode.ON_TIMELINE_JUMPS:
+ perform_autosave()
+
+
+## Perform an autosave.
+## This method will be called automatically if the auto-save mode is enabled.
+func perform_autosave() -> Error:
+ return save("", true)
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicSettingEvent
+extends DialogicEvent
+
+## Event that allows changing a specific setting.
+
+
+### Settings
+
+enum Modes {SET, RESET, RESET_ALL}
+enum SettingValueType {
+ STRING,
+ NUMBER,
+ VARIABLE,
+ EXPRESSION
+}
+
+## The name of the setting to save to.
+var name := ""
+var _value_type := 0 :
+ get:
+ return _value_type
+ set(_value):
+ _value_type = _value
+ if not _suppress_default_value:
+ match _value_type:
+ SettingValueType.STRING, SettingValueType.VARIABLE, SettingValueType.EXPRESSION:
+ value = ""
+ SettingValueType.NUMBER:
+ value = 0
+ ui_update_needed.emit()
+
+var value: Variant = ""
+
+var mode := Modes.SET
+
+## Used to suppress _value_type from overwriting value with a default value when the type changes
+## This is only used when initializing the event_variable.
+var _suppress_default_value: bool = false
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _execute() -> void:
+ if mode == Modes.RESET or mode == Modes.RESET_ALL:
+ if !name.is_empty() and mode != Modes.RESET_ALL:
+ dialogic.Settings.reset_setting(name)
+ else:
+ dialogic.Settings.reset_all()
+ else:
+ match _value_type:
+ SettingValueType.STRING:
+ dialogic.Settings.set(name, value)
+ SettingValueType.NUMBER:
+ dialogic.Settings.set(name, float(value))
+ SettingValueType.VARIABLE:
+ if dialogic.has_subsystem('VAR'):
+ dialogic.Settings.set(name, dialogic.VAR.get_variable('{'+value+'}'))
+ SettingValueType.EXPRESSION:
+ if dialogic.has_subsystem('VAR'):
+ dialogic.Settings.set(name, dialogic.VAR.get_variable(value))
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Setting"
+ set_default_color('Color9')
+ event_category = "Helpers"
+ event_sorting_index = 2
+
+
+func _get_icon() -> Resource:
+ return load(self.get_script().get_path().get_base_dir().path_join('icon.svg'))
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ var string := "setting "
+ if mode != Modes.SET:
+ string += "reset "
+
+ if !name.is_empty() and mode != Modes.RESET_ALL:
+ string += '"' + name + '"'
+
+ if mode == Modes.SET:
+ string += " = "
+ value = str(value)
+ match _value_type:
+ SettingValueType.STRING: # String
+ string += '"'+value.replace('"', '\\"')+'"'
+ SettingValueType.NUMBER,SettingValueType.EXPRESSION: # Float or Expression
+ string += str(value)
+ SettingValueType.VARIABLE: # Variable
+ string += '{'+value+'}'
+
+ return string
+
+
+func from_text(string:String) -> void:
+ var reg := RegEx.new()
+ reg.compile('setting (?<reset>reset)? *("(?<name>[^=+\\-*\\/]*)")?( *= *(?<value>.*))?')
+ var result := reg.search(string)
+ if !result:
+ return
+
+ if result.get_string('reset'):
+ mode = Modes.RESET
+
+ name = result.get_string('name').strip_edges()
+
+ if name.is_empty() and mode == Modes.RESET:
+ mode = Modes.RESET_ALL
+
+ if result.get_string('value'):
+ _suppress_default_value = true
+ value = result.get_string('value').strip_edges()
+ if value.begins_with('"') and value.ends_with('"') and value.count('"')-value.count('\\"') == 2:
+ value = result.get_string('value').strip_edges().replace('"', '')
+ _value_type = SettingValueType.STRING
+ elif value.begins_with('{') and value.ends_with('}') and value.count('{') == 1:
+ value = result.get_string('value').strip_edges().trim_suffix('}').trim_prefix('{')
+ _value_type = SettingValueType.VARIABLE
+ else:
+ value = result.get_string('value').strip_edges()
+ if value.is_valid_float():
+ _value_type = SettingValueType.NUMBER
+ else:
+ _value_type = SettingValueType.EXPRESSION
+ _suppress_default_value = false
+
+
+func is_valid_event(string:String) -> bool:
+ return string.begins_with('setting')
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('mode', ValueType.FIXED_OPTIONS, {
+ 'options': [{
+ 'label': 'Set',
+ 'value': Modes.SET,
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/default.svg")
+ },{
+ 'label': 'Reset',
+ 'value': Modes.RESET,
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/update.svg")
+ },{
+ 'label': 'Reset All',
+ 'value': Modes.RESET_ALL,
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/update.svg")
+ },
+ ]})
+
+ add_header_edit('name', ValueType.DYNAMIC_OPTIONS, {'placeholder':'Type setting', 'suggestions_func':get_settings_suggestions}, 'mode != Modes.RESET_ALL')
+ add_header_edit('_value_type', ValueType.FIXED_OPTIONS, {'left_text':'to',
+ 'options': [
+ {
+ 'label': 'String',
+ 'icon': ["String", "EditorIcons"],
+ 'value': SettingValueType.STRING
+ },{
+ 'label': 'Number',
+ 'icon': ["float", "EditorIcons"],
+ 'value': SettingValueType.NUMBER
+ },{
+ 'label': 'Variable',
+ 'icon': ["ClassList", "EditorIcons"],
+ 'value': SettingValueType.VARIABLE
+ },{
+ 'label': 'Expression',
+ 'icon': ["Variant", "EditorIcons"],
+ 'value': SettingValueType.EXPRESSION
+ }],
+ 'symbol_only':true},
+ '!name.is_empty() and mode == Modes.SET')
+ add_header_edit('value', ValueType.SINGLELINE_TEXT, {}, '!name.is_empty() and (_value_type == SettingValueType.STRING or _value_type == SettingValueType.EXPRESSION) and mode == Modes.SET')
+ add_header_edit('value', ValueType.NUMBER, {}, '!name.is_empty() and _value_type == SettingValueType.NUMBER and mode == Modes.SET')
+ add_header_edit('value', ValueType.DYNAMIC_OPTIONS,
+ {'suggestions_func' : get_value_suggestions, 'placeholder':'Select Variable'},
+ '!name.is_empty() and _value_type == SettingValueType.VARIABLE and mode == Modes.SET')
+
+
+func get_settings_suggestions(filter:String) -> Dictionary:
+ var suggestions := {filter:{'value':filter, 'editor_icon':["GDScriptInternal", "EditorIcons"]}}
+
+ for prop in ProjectSettings.get_property_list():
+ if prop.name.begins_with('dialogic/settings/'):
+ suggestions[prop.name.trim_prefix('dialogic/settings/')] = {'value':prop.name.trim_prefix('dialogic/settings/'), 'editor_icon':["GDScript", "EditorIcons"]}
+ return suggestions
+
+
+func get_value_suggestions(_filter:String) -> Dictionary:
+ var suggestions := {}
+
+ var vars: Dictionary = ProjectSettings.get_setting('dialogic/variables', {})
+ for var_path in DialogicUtil.list_variables(vars):
+ suggestions[var_path] = {'value':var_path, 'editor_icon':["ClassList", "EditorIcons"]}
+ return suggestions
+
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ if symbol == " " and !"reset" in line and !'=' in line and !'"' in line:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, "reset", "reset ", event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), TextNode.get_theme_icon("RotateLeft", "EditorIcons"))
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, "reset all", "reset \n", event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), TextNode.get_theme_icon("ToolRotate", "EditorIcons"))
+
+ if (symbol == " " or symbol == '"') and !"=" in line and CodeCompletionHelper.get_line_untill_caret(line).count('"') != 2:
+ for i in get_settings_suggestions(''):
+ if i.is_empty():
+ continue
+ if symbol == '"':
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i, event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), TextNode.get_theme_icon("GDScript", "EditorIcons"), '"')
+ else:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, '"'+i, event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5), TextNode.get_theme_icon("GDScript", "EditorIcons"), '"')
+
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'setting', 'setting ', event_color)
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ dict[line.find('setting')] = {"color":event_color}
+ dict[line.find('setting')+7] = {"color":Highlighter.normal_color}
+ dict = Highlighter.color_word(dict, event_color, line, 'reset')
+ dict = Highlighter.color_region(dict, Highlighter.string_color, line, '"', '"')
+ dict = Highlighter.color_region(dict, Highlighter.variable_color, line, '{', '}')
+ return dict
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="return-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#464646" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="1.5898438" inkscape:cx="16.039312" inkscape:cy="-62.899263" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="g22577" />
+ <defs id="defs2" />
+ <g inkscape:label="Capa 1" inkscape:groupmode="layer" id="layer1">
+ <g id="g2568" transform="matrix(0.82460302,0,0,0.82460302,1.0713261,2.0200854)" style="stroke-width:1.28345;stroke-dasharray:none">
+ <g id="g11286" transform="translate(0.10757865,0.26894662)">
+ <path id="rect18851" style="fill:#ffffff;stroke:#ffffff;stroke-width:0.523063;stroke-linecap:round;stroke-linejoin:round" inkscape:transform-center-y="-4.7623743" d="M 7.8936486,-0.17465501 7.0631377,2.0999802 A 5.7400259,5.7400259 0 0 0 6.2813299,2.4261824 L 4.0831488,1.4036873 3.8755911,1.6112442 2.9230364,2.563799 2.7154794,2.7716383 3.7379737,4.9695371 A 5.7400259,5.7400259 0 0 0 3.4120557,5.7510623 L 1.1371371,6.5818565 v 0.293639 1.3469982 0.2936385 L 3.412339,9.3466428 A 5.7400259,5.7400259 0 0 0 3.7379737,10.12817 l -1.0224943,2.198464 0.207557,0.207559 0.9525547,0.952269 0.2078403,0.207842 2.1981811,-1.022496 a 5.7400259,5.7400259 0 0 0 0.7815252,0.325919 l 0.8305109,2.274634 h 0.2936398 1.346997 0.29364 l 0.8305106,-2.274916 a 5.7400259,5.7400259 0 0 0 0.781525,-0.325637 l 2.198464,1.022211 0.207556,-0.207557 0.952273,-0.952269 0.207839,-0.207842 -1.022493,-2.198463 a 5.7400259,5.7400259 0 0 0 0.325635,-0.7812452 l 2.2752,-0.8307936 V 8.2224937 6.8754955 6.5815749 L 14.311498,5.7519122 A 5.7400259,5.7400259 0 0 0 13.98558,4.9650064 L 15.005812,2.7713551 14.798254,2.563799 13.845981,1.6112442 13.638142,1.4036873 11.444774,2.4239163 A 5.7400259,5.7400259 0 0 0 10.657587,2.0979982 L 9.8276414,-0.17465501 H 9.5342854 8.1872884 Z M 8.8606453,3.9232559 A 3.6255555,3.6255555 0 0 1 12.486242,7.5488532 3.6255555,3.6255555 0 0 1 8.8606453,11.174451 3.6255555,3.6255555 0 0 1 5.2353313,7.5488532 3.6255555,3.6255555 0 0 1 8.8606453,3.9232559 Z" />
+ <g id="g19581" style="fill:#000000;stroke:#000000" />
+ <g id="g21754" transform="matrix(0.23576423,0,0,0.23576423,22.48277,9.5952281)">
+ <g id="g22577" style="fill:#000000" />
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_setting.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Settings', 'script':this_folder.path_join('subsystem_settings.gd')}]
--- /dev/null
+extends DialogicSubsystem
+## Subsystem that allows setting and getting settings that are automatically saved slot independent.
+##
+## All settings that are stored in the project settings dialogic/settings section are supported.
+## For example the text_speed setting is stored there.
+## How to access this subsystem via code:
+## ```gd
+## Dialogic.Settings.text_speed = 0.05
+## ```
+##
+## Settings stored there can also be changed with the Settings event.
+
+
+var settings := {}
+var _connections := {}
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## Built-in, called by DialogicGameHandler.
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR):
+ _reload_settings()
+
+
+func _reload_settings() -> void:
+ settings = {}
+ for prop in ProjectSettings.get_property_list():
+ if prop.name.begins_with('dialogic/settings'):
+ settings[prop.name.trim_prefix('dialogic/settings/')] = ProjectSettings.get_setting(prop.name)
+
+ if dialogic.has_subsystem('Save'):
+ for i in settings:
+ settings[i] = dialogic.Save.get_global_info(i, settings[i])
+
+
+func _set(property:StringName, value:Variant) -> bool:
+ if not settings.has(property) or settings[property] != value:
+ _setting_changed(property, value)
+ settings[property] = value
+ if dialogic.has_subsystem('Save'):
+ dialogic.Save.set_global_info(property, value)
+ return true
+
+
+func _get(property:StringName) -> Variant:
+ if property in settings:
+ return settings[property]
+ return null
+
+
+func _setting_changed(property:StringName, value:Variant) -> void:
+ if !property in _connections:
+ return
+
+ for i in _connections[property]:
+ if not is_instance_valid(i.get_object()):
+ var remove := func(): _connections[property].erase(i)
+ remove.call_deferred()
+ continue
+ i.call(value)
+
+#endregion
+
+
+#region HANDY METHODS
+####################################################################################################
+
+## Get a setting named `property`, if it does not exist, falls back to `default`.
+func get_setting(property: StringName, default: Variant) -> Variant:
+ return _get(property) if _get(property) != null else default
+
+## Whether a setting has been set/stored before.
+func has_setting(property: StringName) -> bool:
+ return property in settings
+
+
+func reset_all() -> void:
+ for setting in settings:
+ reset_setting(setting)
+
+
+func reset_setting(property: StringName) -> void:
+ if ProjectSettings.has_setting('dialogic/settings/'+property):
+ settings[property] = ProjectSettings.get_setting('dialogic/settings/'+property)
+ _setting_changed(property, settings[property])
+ else:
+ settings.erase(property)
+ _setting_changed(property, null)
+
+
+## If a setting named `property` changes its value, this will emit `Callable`.
+func connect_to_change(property: StringName, callable: Callable) -> void:
+ if not property in _connections:
+ _connections[property] = []
+ _connections[property].append(callable)
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicSignalEvent
+extends DialogicEvent
+
+## Event that emits the Dialogic.signal_event signal with an argument.
+## You can connect to this signal like this: `Dialogic.signal_event.connect(myfunc)`
+
+
+### Settings
+
+enum ArgumentTypes {STRING, DICTIONARY}
+var argument_type := ArgumentTypes.STRING
+
+## The argument that will be provided with the signal.
+var argument: Variant = ""
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ if argument_type == ArgumentTypes.DICTIONARY:
+ var result: Variant = JSON.parse_string(argument)
+ if result != null:
+ var dict := result as Dictionary
+ dict.make_read_only()
+ dialogic.emit_signal('signal_event', dict)
+ else:
+ push_error("[Dialogic] Encountered invalid dictionary in signal event.")
+ else:
+ dialogic.emit_signal('signal_event', argument)
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Signal"
+ set_default_color('Color6')
+ event_category = "Logic"
+ event_sorting_index = 8
+ help_page_path = "https://docs.dialogic.pro/dialogic-signals.html#1-signal-event"
+
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "signal"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "arg_type" : {"property": "argument_type", "default": ArgumentTypes.STRING,
+ "suggestions": func(): return {"String":{'value':ArgumentTypes.STRING, 'text_alt':['string']}, "Dictionary":{'value':ArgumentTypes.DICTIONARY, 'text_alt':['dict', 'dictionary']}}},
+ "arg" : {"property": "argument", "default": ""}
+ }
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_label("Emit dialogic signal with argument")
+ add_header_label("(Dictionary in body)", 'argument_type == ArgumentTypes.DICTIONARY')
+ add_header_edit('argument', ValueType.SINGLELINE_TEXT, {}, 'argument_type == ArgumentTypes.STRING')
+ add_body_edit('argument_type',ValueType.FIXED_OPTIONS, {'left_text':'Argument Type:', 'options': [
+ {
+ 'label': 'String',
+ 'value': ArgumentTypes.STRING,
+ },
+ {
+ 'label': 'Dictionary',
+ 'value': ArgumentTypes.DICTIONARY,
+ }
+ ]})
+ add_body_line_break('argument_type == ArgumentTypes.DICTIONARY')
+ add_body_edit('argument', ValueType.DICTIONARY, {'left_text': 'Dictionary'},'argument_type == ArgumentTypes.DICTIONARY')
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_signal.gd')]
+
--- /dev/null
+@tool
+extends DialogicCharacterEditorMainSection
+
+## Character editor tab that allows setting a custom style fot the character.
+
+func _init() -> void:
+ hint_text = 'If a character style is set, dialogic will switch to this style, whenever the character speaks. \nFor this it\'s best to use a variation of the same layout to avoid instancing a lot.'
+
+func _get_title() -> String:
+ return "Style"
+
+
+func _ready() -> void:
+ %StyleName.resource_icon = get_theme_icon("PopupMenu", "EditorIcons")
+ %StyleName.get_suggestions_func = get_style_suggestions
+
+
+func _load_character(character:DialogicCharacter) -> void:
+ %StyleName.set_value(character.custom_info.get('style', ''))
+
+
+func _save_changes(character:DialogicCharacter) -> DialogicCharacter:
+ character.custom_info['style'] = %StyleName.current_value
+ return character
+
+
+func get_style_suggestions(filter:String="") -> Dictionary:
+ var styles: Array = ProjectSettings.get_setting('dialogic/layout/style_list', [])
+ var suggestions := {}
+ suggestions["No Style"] = {'value': "", 'editor_icon': ["EditorHandleDisabled", "EditorIcons"]}
+ for i in styles:
+ var style: DialogicStyle = load(i)
+ suggestions[style.name] = {'value': style.name, 'editor_icon': ["PopupMenu", "EditorIcons"]}
+ return suggestions
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://fgplvp0f3giu"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Style/character_settings_style.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="2_a46q0"]
+
+[node name="Style" type="VBoxContainer"]
+offset_right = 280.0
+offset_bottom = 79.0
+script = ExtResource("2")
+
+[node name="Style" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Style"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Style:"
+
+[node name="StyleName" parent="Style" instance=ExtResource("2_a46q0")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
--- /dev/null
+@tool
+class_name DialogicStyleEvent
+extends DialogicEvent
+
+## Event that allows changing the currently displayed style.
+
+
+### Settings
+
+## The name of the style to change to. Can be set on the DialogicNode_Style.
+var style_name := ""
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ dialogic.Styles.change_style(style_name)
+ # we need to wait till the new layout is ready before continuing
+ await dialogic.get_tree().process_frame
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Change Style"
+ set_default_color('Color8')
+ event_category = "Visuals"
+ event_sorting_index = 1
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+func get_shortcode() -> String:
+ return "style"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "name" : {"property": "style_name", "default": "", 'suggestions':get_style_suggestions},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('style_name', ValueType.DYNAMIC_OPTIONS, {
+ 'left_text' :'Use style',
+ 'placeholder' : 'Default',
+ 'suggestions_func' : get_style_suggestions,
+ 'editor_icon' : ["PopupMenu", "EditorIcons"],
+ 'autofocus' : true})
+
+
+func get_style_suggestions(_filter := "") -> Dictionary:
+ var styles: Array = ProjectSettings.get_setting('dialogic/layout/style_list', [])
+
+ var suggestions := {}
+ suggestions['<Default Style>'] = {'value':'', 'editor_icon':["MenuBar", "EditorIcons"]}
+ for i in styles:
+ var style: DialogicStyle = load(i)
+ suggestions[style.name] = {'value': style.name, 'editor_icon': ["PopupMenu", "EditorIcons"]}
+ return suggestions
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_style.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Styles', 'script':this_folder.path_join('subsystem_styles.gd')}]
+
+
+func _get_character_editor_sections() -> Array:
+ return [this_folder.path_join('character_settings_style.tscn')]
--- /dev/null
+class_name DialogicNode_StyleLayer
+extends Control
+
+## Control node that is hidden and shown based on the current dialogic style.
+
+## The name this layer listens to
+@export var layer_name: String = 'Default'
+
+
+func _ready() -> void:
+ if layer_name.is_empty():
+ layer_name = name
+ add_to_group('dialogic_style_layer')
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that manages loading layouts with specific styles applied.
+
+signal style_changed(info:Dictionary)
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ pass
+
+
+func load_game_state(load_flag := LoadFlags.FULL_LOAD) -> void:
+ if load_flag == LoadFlags.ONLY_DNODES:
+ return
+ load_style(dialogic.current_state_info.get('style', ''))
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## This helper method calls load_style, but with the [parameter state_reload] as true,
+## which is commonly wanted if you expect a game to already be in progress.
+func change_style(style_name := "", is_base_style := true) -> Node:
+ return load_style(style_name, null, is_base_style, true)
+
+
+## Loads a style. Consider using the simpler [method change_style] if you want to change the style while another style is already in use.
+## [br] If [param state_reload] is true, the current state will be loaded into a new layout scenes nodes.
+## That should not be done before calling start() or load() as it would be unnecessary or cause double-loading.
+func load_style(style_name := "", parent: Node = null, is_base_style := true, state_reload := false) -> Node:
+ var style := DialogicUtil.get_style_by_name(style_name)
+
+ var signal_info := {'style':style_name}
+ dialogic.current_state_info['style'] = style_name
+
+ # is_base_style should only be wrong on temporary changes like character styles
+ if is_base_style:
+ dialogic.current_state_info['base_style'] = style_name
+
+ var previous_layout := get_layout_node()
+ if is_instance_valid(previous_layout) and previous_layout.has_meta('style'):
+ signal_info['previous'] = previous_layout.get_meta('style').name
+
+ # If this is the same style and scene, do nothing
+ if previous_layout.get_meta('style') == style:
+ return previous_layout
+
+ # If this has the same scene setup, just apply the new overrides
+ elif previous_layout.get_meta('style') == style.get_inheritance_root():
+ DialogicUtil.apply_scene_export_overrides(previous_layout, style.get_layer_inherited_info("").overrides)
+ var index := 0
+ for layer in previous_layout.get_layers():
+ DialogicUtil.apply_scene_export_overrides(
+ layer,
+ style.get_layer_inherited_info(style.get_layer_id_at_index(index)).overrides)
+ index += 1
+
+ previous_layout.set_meta('style', style)
+ style_changed.emit(signal_info)
+ return
+
+ else:
+ parent = previous_layout.get_parent()
+
+ previous_layout.get_parent().remove_child(previous_layout)
+ previous_layout.queue_free()
+
+ # if this is another style:
+ var new_layout := create_layout(style, parent)
+ if state_reload:
+ # Preserve process_mode on style changes
+ if previous_layout:
+ new_layout.process_mode = previous_layout.process_mode
+
+ new_layout.ready.connect(reload_current_info_into_new_style)
+
+ style_changed.emit(signal_info)
+
+ return new_layout
+
+
+## Method that adds a layout scene with all the necessary layers.
+## The layout scene will be added to the tree root and returned.
+func create_layout(style: DialogicStyle, parent: Node = null) -> DialogicLayoutBase:
+
+ # Load base scene
+ var base_scene: DialogicLayoutBase
+ var base_layer_info := style.get_layer_inherited_info("")
+ if base_layer_info.path.is_empty():
+ base_scene = DialogicUtil.get_default_layout_base().instantiate()
+ else:
+ base_scene = load(base_layer_info.path).instantiate()
+
+ base_scene.name = "DialogicLayout_"+style.name.to_pascal_case()
+
+ # Apply base scene overrides
+ DialogicUtil.apply_scene_export_overrides(base_scene, base_layer_info.overrides)
+
+ # Load layers
+ for layer_id in style.get_layer_inherited_list():
+ var layer := style.get_layer_inherited_info(layer_id)
+
+ if not ResourceLoader.exists(layer.path):
+ continue
+
+ var layer_scene: DialogicLayoutLayer = null
+
+ if ResourceLoader.load_threaded_get_status(layer.path) == ResourceLoader.THREAD_LOAD_LOADED:
+ layer_scene = ResourceLoader.load_threaded_get(layer.path).instantiate()
+ else:
+ layer_scene = load(layer.path).instantiate()
+
+ base_scene.add_layer(layer_scene)
+
+ # Apply layer overrides
+ DialogicUtil.apply_scene_export_overrides(layer_scene, layer.overrides)
+
+ base_scene.set_meta('style', style)
+
+ if parent == null:
+ parent = dialogic.get_parent()
+ parent.call_deferred("add_child", base_scene)
+
+ dialogic.get_tree().set_meta('dialogic_layout_node', base_scene)
+
+ return base_scene
+
+
+## When changing to a different layout scene,
+## we have to load all the info from the current_state_info (basically
+func reload_current_info_into_new_style() -> void:
+ for subsystem in dialogic.get_children():
+ subsystem.load_game_state(LoadFlags.ONLY_DNODES)
+
+
+## Returns the style currently in use
+func get_current_style() -> String:
+ if has_active_layout_node():
+ var style: DialogicStyle = get_layout_node().get_meta('style', null)
+ if style:
+ return style.name
+ return ''
+
+
+func has_active_layout_node() -> bool:
+ return (
+ get_tree().has_meta('dialogic_layout_node')
+ and is_instance_valid(get_tree().get_meta('dialogic_layout_node'))
+ and not get_tree().get_meta('dialogic_layout_node').is_queued_for_deletion()
+ )
+
+
+func get_layout_node() -> DialogicLayoutBase:
+ if has_active_layout_node():
+ return get_tree().get_meta('dialogic_layout_node')
+ return null
+
+
+## Similar to get_tree().get_first_node_in_group('group_name') but filtered to the active layout node subtree
+func get_first_node_in_layout(group_name: String) -> Node:
+ var layout_node := get_layout_node()
+ if null == layout_node:
+ return null
+ var nodes := get_tree().get_nodes_in_group(group_name)
+ for node in nodes:
+ if layout_node.is_ancestor_of(node):
+ return node
+ return null
+
+#endregion
--- /dev/null
+@tool
+extends Control
+
+var ListItem := load("res://addons/dialogic/Editor/Common/BrowserItem.tscn")
+enum Types {ALL, STYLES, LAYER, LAYOUT_BASE}
+
+var current_type := Types.ALL
+var style_part_info := []
+var premade_scenes_reference := {}
+
+signal activate_part(part_info:Dictionary)
+
+var current_info := {}
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+ %Search.right_icon = get_theme_icon("Search", "EditorIcons")
+ %CloseButton.icon = get_theme_icon("Close", "EditorIcons")
+ collect_style_parts()
+
+
+func collect_style_parts() -> void:
+ for indexer in DialogicUtil.get_indexers():
+ for layout_part in indexer._get_layout_parts():
+ style_part_info.append(layout_part)
+ if not layout_part.get('path', '').is_empty():
+ premade_scenes_reference[layout_part['path']] = layout_part
+
+
+func is_premade_style_part(scene_path:String) -> bool:
+ return scene_path in premade_scenes_reference
+
+
+func load_parts() -> void:
+ for i in %PartGrid.get_children():
+ i.queue_free()
+
+ %Search.placeholder_text = "Search for "
+ %Search.text = ""
+ match current_type:
+ Types.STYLES:
+ %Search.placeholder_text += "premade styles"
+ Types.LAYER:
+ %Search.placeholder_text += "layer scenes"
+ Types.LAYOUT_BASE:
+ %Search.placeholder_text += "layout base scenes"
+ Types.ALL:
+ %Search.placeholder_text += "styles or layout scenes"
+
+ for info in style_part_info:
+ var type: String = info.get('type', '_')
+ match current_type:
+ Types.STYLES:
+ if type != "Style":
+ continue
+ Types.LAYER:
+ if type != "Layer":
+ continue
+ Types.LAYOUT_BASE:
+ if type != "Layout Base":
+ continue
+
+ var style_item: Node = ListItem.instantiate()
+ style_item.load_info(info)
+ %PartGrid.add_child(style_item)
+ style_item.set_meta('info', info)
+ style_item.clicked.connect(_on_style_item_clicked.bind(style_item, info))
+ style_item.focused.connect(_on_style_item_clicked.bind(style_item, info))
+ style_item.double_clicked.connect(emit_signal.bind('activate_part', info))
+
+ await get_tree().process_frame
+
+ if %PartGrid.get_child_count() > 0:
+ %PartGrid.get_child(0).clicked.emit()
+ %PartGrid.get_child(0).grab_focus()
+
+
+func _on_style_item_clicked(item:Node, info:Dictionary) -> void:
+ load_part_info(info)
+
+
+func load_part_info(info:Dictionary) -> void:
+ current_info = info
+ %PartTitle.text = info.get('name', 'Unknown Part')
+ %PartAuthor.text = "by "+info.get('author', 'Anonymus')
+ %PartDescription.text = info.get('description', '')
+
+ if info.get('preview_image', null):
+ %PreviewImage.texture = load(info.get('preview_image')[0])
+ %PreviewImage.show()
+ else:
+ %PreviewImage.hide()
+
+
+func _on_activate_button_pressed() -> void:
+ activate_part.emit(current_info)
+
+
+func _on_search_text_changed(new_text: String) -> void:
+ for item in %PartGrid.get_children():
+ if new_text.is_empty():
+ item.show()
+ continue
+
+ if new_text.to_lower() in item.get_meta('info').name.to_lower():
+ item.show()
+ continue
+
+ item.hide()
+
+
+func _on_close_button_pressed() -> void:
+ get_parent().hide()
--- /dev/null
+[gd_scene load_steps=11 format=3 uid="uid://cs381i3h7sveq"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/StyleEditor/Components/style_browser.gd" id="1_3sdb0"]
+
+[sub_resource type="Gradient" id="Gradient_0o1u0"]
+colors = PackedColorArray(0.296448, 0.231485, 0.52887, 1, 0.100572, 0.303996, 0.476999, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_vd6co"]
+gradient = SubResource("Gradient_0o1u0")
+fill_from = Vector2(1, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4e858"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(1, 1, 1, 0.615686)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t3eoa"]
+content_margin_left = 6.0
+content_margin_top = 3.0
+content_margin_right = 6.0
+content_margin_bottom = 3.0
+draw_center = false
+border_width_left = 3
+border_width_top = 3
+border_width_right = 3
+border_width_bottom = 3
+border_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+expand_margin_left = 2.0
+expand_margin_top = 2.0
+expand_margin_right = 2.0
+expand_margin_bottom = 2.0
+
+[sub_resource type="Image" id="Image_tg5pd"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_f5xt2"]
+image = SubResource("Image_tg5pd")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mavtr"]
+bg_color = Color(0.0588235, 0.0313726, 0.0980392, 1)
+border_width_left = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dqdat"]
+bg_color = Color(1, 1, 1, 1)
+draw_center = false
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+shadow_color = Color(0.992157, 0.992157, 0.992157, 0.101961)
+shadow_size = 10
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_otau4"]
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[node name="StyleBrowser" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_3sdb0")
+
+[node name="BGColor" type="TextureRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = SubResource("GradientTexture2D_vd6co")
+
+[node name="HSplitContainer" type="HSplitContainer" parent="."]
+clip_contents = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+
+[node name="Margin" type="MarginContainer" parent="HSplitContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 1.5
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="VBox" type="VBoxContainer" parent="HSplitContainer/Margin"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="BrowserTitle" type="Label" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+theme_override_font_sizes/font_size = 25
+text = "Dialogic Style Browser"
+
+[node name="HBox" type="HBoxContainer" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+
+[node name="Search" type="LineEdit" parent="HSplitContainer/Margin/VBox/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/normal = SubResource("StyleBoxFlat_4e858")
+theme_override_styles/focus = SubResource("StyleBoxFlat_t3eoa")
+placeholder_text = "Search"
+right_icon = SubResource("ImageTexture_f5xt2")
+
+[node name="ScrollContainer" type="ScrollContainer" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="PartGrid" type="HFlowContainer" parent="HSplitContainer/Margin/VBox/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Buttons" type="HBoxContainer" parent="HSplitContainer/Margin/VBox"]
+layout_mode = 2
+alignment = 1
+
+[node name="CloseButton" type="Button" parent="HSplitContainer/Margin/VBox/Buttons"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Close"
+icon = SubResource("ImageTexture_f5xt2")
+
+[node name="PanelContainer" type="PanelContainer" parent="HSplitContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_mavtr")
+
+[node name="Control" type="Control" parent="HSplitContainer/PanelContainer"]
+layout_mode = 2
+
+[node name="Panel" type="Panel" parent="HSplitContainer/PanelContainer/Control"]
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_left = -4.0
+offset_right = 40.0
+offset_bottom = 71.0
+grow_vertical = 2
+rotation = 0.0349066
+theme_override_styles/panel = SubResource("StyleBoxFlat_mavtr")
+
+[node name="MarginContainer" type="MarginContainer" parent="HSplitContainer/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="VBox" type="VBoxContainer" parent="HSplitContainer/PanelContainer/MarginContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Panel" type="PanelContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_dqdat")
+
+[node name="Panel" type="PanelContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/Panel"]
+clip_children = 1
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_otau4")
+
+[node name="PreviewImage" type="TextureRect" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/Panel/Panel"]
+unique_name_in_owner = true
+layout_mode = 2
+expand_mode = 5
+stretch_mode = 6
+
+[node name="HFlowContainer" type="HFlowContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+layout_mode = 2
+
+[node name="PartTitle" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 8
+theme_type_variation = &"DialogicTitle"
+text = "Cool Style Part"
+
+[node name="PartAuthor" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 8
+theme_type_variation = &"DialogicHintText"
+text = "by Jowan"
+
+[node name="PartType" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
+visible = false
+layout_mode = 2
+size_flags_vertical = 8
+theme_type_variation = &"DialogicHintText"
+text = "a style"
+
+[node name="PartDescription" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicHintText2"
+text = "A cool textbox layer"
+autowrap_mode = 3
+
+[node name="ActivateButton" type="Button" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Use"
+
+[connection signal="text_changed" from="HSplitContainer/Margin/VBox/HBox/Search" to="." method="_on_search_text_changed"]
+[connection signal="pressed" from="HSplitContainer/Margin/VBox/Buttons/CloseButton" to="." method="_on_close_button_pressed"]
+[connection signal="pressed" from="HSplitContainer/PanelContainer/MarginContainer/VBox/ActivateButton" to="." method="_on_activate_button_pressed"]
--- /dev/null
+@tool
+extends Window
+
+var info := {}
+signal part_selected(info:Dictionary)
+
+
+func _on_close_requested() -> void:
+ info = {}
+ part_selected.emit({})
+ hide()
+
+
+func get_picked_info() -> Dictionary:
+ await part_selected
+ return info
+
+
+func _on_style_browser_activate_part(part_info: Dictionary) -> void:
+ info = part_info
+ part_selected.emit(part_info)
+ hide()
--- /dev/null
+@tool
+extends Tree
+
+## Script that handles drag and drop on the layer tree.
+
+
+signal layer_moved(from:int, to:int)
+
+#region DRAG AND DROP
+################################################################################
+
+func _get_drag_data(position:Vector2) -> Variant:
+ if get_selected() == null or get_selected() == get_root():
+ return
+
+ if find_parent('StyleEditor').current_style.inherits != null:
+ return
+
+ drop_mode_flags = DROP_MODE_INBETWEEN
+ var preview := Label.new()
+ preview.text = " "+get_selected().get_text(0)
+ preview.add_theme_stylebox_override('normal', get_theme_stylebox("Background", "EditorStyles"))
+ set_drag_preview(preview)
+
+ return get_selected()
+
+
+func _can_drop_data(position:Vector2, data:Variant) -> bool:
+ return data is TreeItem
+
+
+func _drop_data(position:Vector2, item:Variant) -> void:
+ var to_item := get_item_at_position(position)
+ var drop_section := get_drop_section_at_position(position)
+
+ if to_item == get_root():
+ if item.get_index() != 0:
+ layer_moved.emit(item.get_index(), 0)
+ return
+
+ if to_item == null:
+ if item.get_index() != get_root().get_child_count()-1:
+ layer_moved.emit(item.get_index(), get_root().get_child_count()-1)
+ return
+
+ var to_idx: int = to_item.get_index()+max(0, drop_section)
+ if to_idx > item.get_index():
+ to_idx -= 1
+
+ if to_idx != item.get_index():
+ layer_moved.emit(item.get_index(), to_idx)
+
+#endregion
--- /dev/null
+extends DialogicIndexer
+
+func _get_editors() -> Array:
+ return [this_folder.path_join('style_editor.tscn')]
--- /dev/null
+@tool
+extends DialogicEditor
+
+## Editor that handles the editing of styles and their layers.
+
+
+var styles: Array[DialogicStyle] = []
+var current_style : DialogicStyle = null
+var default_style := ""
+
+var premade_style_parts := {}
+
+@onready var StyleList: ItemList = %StyleList
+
+#region EDITOR MANAGEMENT
+################################################################################
+
+func _get_title() -> String:
+ return "Styles"
+
+
+func _get_icon() -> Texture:
+ return load(DialogicUtil.get_module_path('StyleEditor').path_join("styles_icon.svg"))
+
+
+func _register() -> void:
+ editors_manager.register_simple_editor(self)
+ alternative_text = "Change the look of the dialog in your game"
+
+
+func _open(_extra_info:Variant = null) -> void:
+ load_style_list()
+
+
+func _close() -> void:
+ save_style_list()
+ save_style()
+
+
+#endregion
+
+
+func _ready() -> void:
+ collect_styles()
+
+ setup_ui()
+
+
+#region STYLE MANAGEMENT
+################################################################################
+
+func collect_styles() -> void:
+ for indexer in DialogicUtil.get_indexers():
+ for layout in indexer._get_layout_parts():
+ premade_style_parts[layout['path']] = layout
+
+ var style_list: Array = ProjectSettings.get_setting('dialogic/layout/style_list', [])
+ for style in style_list:
+ if ResourceLoader.exists(style):
+ if style != null:
+ styles.append(ResourceLoader.load(style, "DialogicStyle"))
+ else:
+ print("[Dialogic] Failed to open style '", style, "'. Some dependency might be broken.")
+ else:
+ print("[Dialogic] Failed to open style '", style, "'. Might have been moved or deleted.")
+
+ default_style = ProjectSettings.get_setting('dialogic/layout/default_style', 'Default')
+
+
+func save_style_list() -> void:
+ ProjectSettings.set_setting('dialogic/layout/style_list', styles.map(func(style:DialogicStyle): return style.resource_path))
+ ProjectSettings.set_setting('dialogic/layout/default_style', default_style)
+ ProjectSettings.save()
+
+
+func save_style() -> void:
+ if current_style == null:
+ return
+
+ ResourceSaver.save(current_style)
+
+
+func add_style(file_path:String, style:DialogicStyle, inherits:DialogicStyle= null) -> void:
+ style.resource_path = file_path
+ style.inherits = inherits
+
+ if style.layer_list.is_empty() and style.inherits_anything():
+ for id in style.get_layer_inherited_list():
+ style.add_layer('', {}, id)
+
+ ResourceSaver.save(style, file_path)
+
+ styles.append(style)
+
+ if len(styles) == 1:
+ default_style = style.resource_path
+
+ save_style_list()
+ load_style_list()
+ select_style(style)
+
+
+func delete_style(style:DialogicStyle) -> void:
+ for other_style in styles:
+ if other_style.inherits == style:
+ other_style.realize_inheritance()
+ push_warning('[Dialogic] Style "',other_style.name,'" had to be realized because it inherited "', style.name,'" which was deleted!')
+
+ if style.resource_path == default_style:
+ default_style = ""
+ styles.erase(style)
+ save_style_list()
+
+
+func delete_style_by_name(style_name:String) -> void:
+ for style in styles:
+ if style.name == style_name:
+ delete_style(style)
+ return
+
+
+func realize_style() -> void:
+ current_style.realize_inheritance()
+
+ select_style(current_style)
+
+
+#endregion
+#region USER INTERFACE
+################################################################################
+
+func setup_ui() -> void:
+ %AddButton.icon = get_theme_icon("Add", "EditorIcons")
+ %DuplicateButton.icon = get_theme_icon("Duplicate", "EditorIcons")
+ %InheritanceButton.icon = get_theme_icon("GuiDropdown", "EditorIcons")
+ %RemoveButton.icon = get_theme_icon("Remove", "EditorIcons")
+
+ %EditNameButton.icon = get_theme_icon("Edit", "EditorIcons")
+ %TestStyleButton.icon = get_theme_icon("PlayCustom", "EditorIcons")
+ %MakeDefaultButton.icon = get_theme_icon("Favorites", "EditorIcons")
+
+ StyleList.item_selected.connect(_on_stylelist_selected)
+ %AddButton.get_popup().index_pressed.connect(_on_AddStyleMenu_selected)
+ %AddButton.about_to_popup.connect(_on_AddStyleMenu_about_to_popup)
+ %InheritanceButton.get_popup().index_pressed.connect(_on_inheritance_index_pressed)
+ StyleList.set_drag_forwarding(_on_stylelist_drag, _on_stylelist_can_drop, _on_style_list_drop)
+ %StyleView.hide()
+ %NoStyleView.show()
+
+func load_style_list() -> void:
+ var latest: String = DialogicUtil.get_editor_setting('latest_layout_style', 'Default')
+
+ StyleList.clear()
+ var idx := 0
+ for style in styles:
+ # TODO remove when going Beta
+ style.update_from_pre_alpha16()
+ StyleList.add_item(style.name, get_theme_icon("PopupMenu", "EditorIcons"))
+ StyleList.set_item_tooltip(idx, style.resource_path)
+ StyleList.set_item_metadata(idx, style)
+
+ if style.resource_path == default_style:
+ StyleList.set_item_icon_modulate(idx, get_theme_color("warning_color", "Editor"))
+ if style.resource_path.begins_with("res://addons/dialogic"):
+ StyleList.set_item_icon_modulate(idx, get_theme_color("property_color_z", "Editor"))
+ StyleList.set_item_tooltip(idx, "This is a default style. Only edit it if you know what you are doing!")
+ StyleList.set_item_custom_bg_color(idx, get_theme_color("property_color_z", "Editor").lerp(get_theme_color("dark_color_3", "Editor"), 0.8))
+ if style.name == latest:
+ StyleList.select(idx)
+ load_style(style)
+ idx += 1
+
+ if len(styles) == 0:
+ %StyleView.hide()
+ %NoStyleView.show()
+
+ elif !StyleList.is_anything_selected():
+ StyleList.select(0)
+ load_style(StyleList.get_item_metadata(0))
+
+
+func _on_stylelist_selected(index:int) -> void:
+ load_style(StyleList.get_item_metadata(index))
+
+
+func select_style(style:DialogicStyle) -> void:
+ DialogicUtil.set_editor_setting('latest_layout_style', style.name)
+ for idx in range(StyleList.item_count):
+ if StyleList.get_item_metadata(idx) == style:
+ StyleList.select(idx)
+ return
+
+
+func load_style(style:DialogicStyle) -> void:
+ if current_style != null:
+ current_style.changed.disconnect(save_style)
+ save_style()
+ current_style = style
+ if current_style == null:
+ return
+ current_style.changed.connect(save_style)
+
+ %LayoutStyleName.text = style.name
+ if style.resource_path == default_style:
+ %MakeDefaultButton.tooltip_text = "Is Default"
+ %MakeDefaultButton.disabled = true
+ else:
+ %MakeDefaultButton.tooltip_text = "Make Default"
+ %MakeDefaultButton.disabled = false
+
+ %StyleEditor.load_style(style)
+
+ %InheritanceButton.visible = style.inherits_anything()
+ if %InheritanceButton.visible:
+ %InheritanceButton.text = "Inherits " + style.inherits.name
+
+
+ DialogicUtil.set_editor_setting('latest_layout_style', style.name)
+
+ %StyleView.show()
+ %NoStyleView.hide()
+
+
+func _on_AddStyleMenu_about_to_popup() -> void:
+ %AddButton.get_popup().set_item_disabled(3, not StyleList.is_anything_selected())
+
+
+func _on_AddStyleMenu_selected(index:int) -> void:
+ # add preset style
+ if index == 2:
+ %StyleBrowserWindow.popup_centered_ratio(0.6)
+ %StyleBrowser.current_type = 1
+ %StyleBrowser.load_parts()
+ var picked_info: Dictionary = await %StyleBrowserWindow.get_picked_info()
+ if not picked_info.has('style_path'):
+ return
+
+ if not ResourceLoader.exists(picked_info.style_path):
+ return
+
+ var new_style: DialogicStyle = load(picked_info.style_path).clone()
+
+ find_parent('EditorView').godot_file_dialog(
+ add_style_undoable.bind(new_style),
+ '*.tres',
+ EditorFileDialog.FILE_MODE_SAVE_FILE,
+ "Select folder for new style")
+
+ if index == 3:
+ if StyleList.get_selected_items().is_empty():
+ return
+ find_parent('EditorView').godot_file_dialog(
+ add_style_undoable.bind(DialogicStyle.new(), current_style),
+ '*.tres',
+ EditorFileDialog.FILE_MODE_SAVE_FILE,
+ "Select folder for new style")
+
+ if index == 4:
+ find_parent('EditorView').godot_file_dialog(
+ add_style_undoable.bind(DialogicStyle.new()),
+ '*.tres',
+ EditorFileDialog.FILE_MODE_SAVE_FILE,
+ "Select folder for new style")
+
+
+func add_style_undoable(file_path:String, style:DialogicStyle, inherits:DialogicStyle = null) -> void:
+ style.name = _get_new_name(file_path.get_file().trim_suffix('.'+file_path.get_extension()))
+ var undo_redo: EditorUndoRedoManager = DialogicUtil.get_dialogic_plugin().get_undo_redo()
+ undo_redo.create_action('Add Style', UndoRedo.MERGE_ALL)
+ undo_redo.add_do_method(self, "add_style", file_path, style, inherits)
+ undo_redo.add_do_method(self, "load_style_list")
+ undo_redo.add_undo_method(self, "delete_style", style)
+ undo_redo.add_undo_method(self, "load_style_list")
+ undo_redo.commit_action()
+ DialogicUtil.set_editor_setting('latest_layout_style', style.name)
+
+
+func _on_duplicate_button_pressed() -> void:
+ if !StyleList.is_anything_selected():
+ return
+ find_parent('EditorView').godot_file_dialog(
+ add_style_undoable.bind(current_style.clone(), null),
+ '*.tres',
+ EditorFileDialog.FILE_MODE_SAVE_FILE,
+ "Select folder for new style")
+
+
+func _on_remove_button_pressed() -> void:
+ if !StyleList.is_anything_selected():
+ return
+
+ if current_style.name == default_style:
+ push_warning("[Dialogic] You cannot delete the default style!")
+ return
+
+ delete_style(current_style)
+ load_style_list()
+
+
+func _on_edit_name_button_pressed() -> void:
+ %LayoutStyleName.grab_focus()
+ %LayoutStyleName.select_all()
+
+
+func _on_layout_style_name_text_submitted(_new_text:String) -> void:
+ _on_layout_style_name_focus_exited()
+
+
+func _on_layout_style_name_focus_exited() -> void:
+ var new_name: String = %LayoutStyleName.text.strip_edges()
+ if new_name == current_style.name:
+ return
+
+ for style in styles:
+ if style.name == new_name:
+ %LayoutStyleName.text = current_style.name
+ return
+
+ current_style.name = new_name
+ DialogicUtil.set_editor_setting('latest_layout_style', new_name)
+ load_style_list()
+
+
+func _on_make_default_button_pressed() -> void:
+ default_style = current_style.resource_path
+ save_style_list()
+ load_style_list()
+
+
+
+func _on_test_style_button_pressed() -> void:
+ var dialogic_plugin := DialogicUtil.get_dialogic_plugin()
+
+ # Save the current opened timeline
+ DialogicUtil.set_editor_setting('current_test_style', current_style.name)
+
+ DialogicUtil.get_dialogic_plugin().get_editor_interface().play_custom_scene("res://addons/dialogic/Editor/TimelineEditor/test_timeline_scene.tscn")
+ await get_tree().create_timer(3).timeout
+ DialogicUtil.set_editor_setting('current_test_style', '')
+
+
+func _on_inheritance_index_pressed(index:int) -> void:
+ if index == 0:
+ realize_style()
+
+
+
+func _on_start_styling_button_pressed() -> void:
+ var new_style := DialogicUtil.get_fallback_style().clone()
+
+ find_parent('EditorView').godot_file_dialog(
+ add_style_undoable.bind(new_style),
+ '*.tres',
+ EditorFileDialog.FILE_MODE_SAVE_FILE,
+ "Select folder for new style")
+
+
+#endregion
+
+func _on_stylelist_drag(vector:Vector2) -> Variant:
+ return null
+
+
+func _on_stylelist_can_drop(at_position: Vector2, data: Variant) -> bool:
+ if not data is Dictionary:
+ return false
+ if not data.get('type', 's') == 'files':
+ return false
+ for f in data.files:
+ var style := load(f)
+ if style is DialogicStyle:
+ if not style in styles:
+ return true
+
+ return false
+
+func _on_style_list_drop(at_position: Vector2, data: Variant) -> void:
+ for file in data.files:
+ var style := load(file)
+ if style is DialogicStyle:
+ if not style in styles:
+ styles.append(style)
+ save_style_list()
+ load_style_list()
+
+
+#region Helpers
+func _get_new_name(base_name:String) -> String:
+ var new_name_idx := 1
+ var found_unique_name := false
+ var new_name := base_name
+ while not found_unique_name:
+ found_unique_name = true
+ for style in styles:
+ if style.name == new_name:
+ new_name_idx += 1
+ new_name = base_name+" "+str(new_name_idx)
+ found_unique_name = false
+ return new_name
+
+#endregion
--- /dev/null
+[gd_scene load_steps=16 format=3 uid="uid://cx6h3tck10s1g"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/StyleEditor/style_editor.gd" id="1_gy14f"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_g4mnt"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/StyleEditor/style_layer_editor.gd" id="3_iih7c"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/StyleEditor/Components/style_layer_tree.gd" id="4_kpoqn"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/StyleEditor/Components/style_browser_window.gd" id="5_qbwx0"]
+[ext_resource type="PackedScene" uid="uid://cs381i3h7sveq" path="res://addons/dialogic/Modules/StyleEditor/Components/style_browser.tscn" id="6_p6lia"]
+
+[sub_resource type="Image" id="Image_tg5pd"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_f5xt2"]
+image = SubResource("Image_tg5pd")
+
+[sub_resource type="Image" id="Image_ns66m"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_xtj53"]
+image = SubResource("Image_ns66m")
+
+[sub_resource type="Theme" id="Theme_l6tyr"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_tixgs"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0nh8y"]
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[sub_resource type="Image" id="Image_op8ly"]
+data = {
+"data": PackedByteArray(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255),
+"format": "RGBA8",
+"height": 2,
+"mipmaps": false,
+"width": 2
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_yr3tj"]
+image = SubResource("Image_op8ly")
+
+[node name="StyleEditor" type="HSplitContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -3.0
+offset_bottom = 3.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_gy14f")
+
+[node name="Panel" type="PanelContainer" parent="."]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+size_flags_stretch_ratio = 0.2
+theme_type_variation = &"DialogicPanelA"
+
+[node name="VBox" type="VBoxContainer" parent="Panel"]
+layout_mode = 2
+
+[node name="Title" type="HBoxContainer" parent="Panel/VBox"]
+layout_mode = 2
+
+[node name="StyleListTitle" type="Label" parent="Panel/VBox/Title"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Styles"
+
+[node name="HintTooltip" parent="Panel/VBox/Title" instance=ExtResource("2_g4mnt")]
+layout_mode = 2
+tooltip_text = "Each style consist of a list of layers and settings for each layer.
+A style can inherit from another style (inherited styles can only overwrite settings of their layers).
+When one style is selected as default dialogic will use that style when calling Dialogic.start() otherwise a fallback is used.
+You can change the style with the Change Style event, by setting a style on a character or by calling
+Dialogic.Styles.load_style(\"Style Name\") before calling Dialogic.start()."
+texture = null
+hint_text = "Each style consist of a list of layers and settings for each layer.
+A style can inherit from another style (inherited styles can only overwrite settings of their layers).
+When one style is selected as default dialogic will use that style when calling Dialogic.start() otherwise a fallback is used.
+You can change the style with the Change Style event, by setting a style on a character or by calling
+Dialogic.Styles.load_style(\"Style Name\") before calling Dialogic.start()."
+
+[node name="StyleButtons" type="HBoxContainer" parent="Panel/VBox"]
+layout_mode = 2
+alignment = 2
+
+[node name="AddButton" type="MenuButton" parent="Panel/VBox/StyleButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add layout-style"
+icon = SubResource("ImageTexture_f5xt2")
+switch_on_hover = true
+item_count = 5
+popup/item_0/text = "ADD STYLE"
+popup/item_0/id = 0
+popup/item_0/disabled = true
+popup/item_1/text = ""
+popup/item_1/id = 0
+popup/item_1/separator = true
+popup/item_2/text = "Premade Style"
+popup/item_2/id = 0
+popup/item_3/text = "Inherited Style"
+popup/item_3/id = 1
+popup/item_4/text = "Custom Style"
+popup/item_4/id = 2
+
+[node name="DuplicateButton" type="Button" parent="Panel/VBox/StyleButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Duplicate style"
+icon = SubResource("ImageTexture_f5xt2")
+flat = true
+
+[node name="RemoveButton" type="Button" parent="Panel/VBox/StyleButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Remove style from list"
+icon = SubResource("ImageTexture_f5xt2")
+flat = true
+
+[node name="MakeDefaultButton" type="Button" parent="Panel/VBox/StyleButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+tooltip_text = "Make Default"
+toggle_mode = true
+icon = SubResource("ImageTexture_f5xt2")
+flat = true
+
+[node name="StyleList" type="ItemList" parent="Panel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+item_count = 1
+item_0/text = "Style"
+item_0/icon = SubResource("ImageTexture_xtj53")
+
+[node name="StyleView" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBox" type="HBoxContainer" parent="StyleView"]
+layout_mode = 2
+theme = SubResource("Theme_l6tyr")
+theme_override_constants/separation = 0
+
+[node name="LayoutStyleName" type="LineEdit" parent="StyleView/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicTitle"
+theme_override_styles/normal = SubResource("StyleBoxEmpty_tixgs")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_tixgs")
+theme_override_styles/read_only = SubResource("StyleBoxEmpty_tixgs")
+text = "Style"
+expand_to_text_length = true
+
+[node name="EditNameButton" type="Button" parent="StyleView/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Edit Name"
+icon = SubResource("ImageTexture_f5xt2")
+flat = true
+
+[node name="InheritanceButton" type="MenuButton" parent="StyleView/HBox"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+text = "Inherits VN Style"
+icon = SubResource("ImageTexture_f5xt2")
+icon_alignment = 2
+item_count = 1
+popup/item_0/text = "Clear Inheritance"
+popup/item_0/id = 0
+
+[node name="TestStyleButton" type="Button" parent="StyleView/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+tooltip_text = "Play current timeline with this style"
+text = "Test Style"
+icon = SubResource("ImageTexture_f5xt2")
+
+[node name="PanelContainer" type="PanelContainer" parent="StyleView"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelB"
+
+[node name="StyleEditor" type="HSplitContainer" parent="StyleView/PanelContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+script = ExtResource("3_iih7c")
+
+[node name="LayerPanel" type="PanelContainer" parent="StyleView/PanelContainer/StyleEditor"]
+layout_mode = 2
+theme_type_variation = &"DialogicPanelA"
+
+[node name="VBox" type="VBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerPanel"]
+layout_mode = 2
+
+[node name="Title" type="HBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox"]
+layout_mode = 2
+
+[node name="LayerListTitle" type="Label" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/Title"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Layers"
+
+[node name="HintTooltip" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/Title" instance=ExtResource("2_g4mnt")]
+layout_mode = 2
+tooltip_text = "Each layer is a scene and settings that will be applied to that scene.
+A layer can either be a premade scene or a scene you've made yourself."
+texture = null
+hint_text = "Each layer is a scene and settings that will be applied to that scene.
+A layer can either be a premade scene or a scene you've made yourself."
+
+[node name="LayerButtons" type="HBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox"]
+layout_mode = 2
+alignment = 2
+
+[node name="AddLayerButton" type="MenuButton" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add Layer"
+icon = SubResource("ImageTexture_f5xt2")
+switch_on_hover = true
+item_count = 4
+popup/item_0/text = "ADD LAYER"
+popup/item_0/id = 1
+popup/item_0/disabled = true
+popup/item_1/text = ""
+popup/item_1/id = 1
+popup/item_1/separator = true
+popup/item_2/text = "Premade Layer"
+popup/item_2/id = 1
+popup/item_3/text = "Custom Layer"
+popup/item_3/id = 0
+
+[node name="ReplaceLayerButton" type="MenuButton" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Replace Layer"
+icon = SubResource("ImageTexture_f5xt2")
+switch_on_hover = true
+item_count = 4
+popup/item_0/text = "REPLACE LAYER"
+popup/item_0/id = 0
+popup/item_0/disabled = true
+popup/item_1/text = ""
+popup/item_1/id = 0
+popup/item_1/separator = true
+popup/item_2/text = "Premade Layer"
+popup/item_2/id = 0
+popup/item_3/text = "Custom Layer"
+popup/item_3/id = 1
+
+[node name="MakeCustomButton" type="MenuButton" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Make Custom"
+icon = SubResource("ImageTexture_f5xt2")
+switch_on_hover = true
+item_count = 4
+popup/item_0/text = "MAKE CUSTOM"
+popup/item_0/id = 3
+popup/item_0/disabled = true
+popup/item_1/text = ""
+popup/item_1/id = 0
+popup/item_1/separator = true
+popup/item_2/text = "Current Layer"
+popup/item_2/id = 0
+popup/item_3/text = "Full Layout"
+popup/item_3/id = 2
+
+[node name="DeleteLayerButton" type="Button" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerButtons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Delete Layer (no undo!)"
+disabled = true
+icon = SubResource("ImageTexture_f5xt2")
+flat = true
+
+[node name="LayerTree" type="Tree" parent="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/icon_max_width = 32
+allow_rmb_select = true
+hide_folding = true
+enable_recursive_folding = false
+drop_mode_flags = 2
+script = ExtResource("4_kpoqn")
+
+[node name="LayerSettings" type="VBoxContainer" parent="StyleView/PanelContainer/StyleEditor"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="LayerInfoHeader" type="HBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings"]
+layout_mode = 2
+
+[node name="LayerName" type="Label" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoHeader"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_type_variation = &"DialogicTitle"
+text = "Default Layout Base"
+
+[node name="ExpandLayerInfo" type="Button" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoHeader"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = SubResource("ImageTexture_f5xt2")
+flat = true
+
+[node name="LayerInfoBody" type="VBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBox" type="HBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody"]
+layout_mode = 2
+
+[node name="Panel" type="PanelContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox"]
+show_behind_parent = true
+clip_children = 2
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_0nh8y")
+
+[node name="SmallLayerPreview" type="TextureRect" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Panel"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = SubResource("ImageTexture_yr3tj")
+expand_mode = 3
+stretch_mode = 6
+
+[node name="Info" type="VBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="HBoxContainer" type="HBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Info"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="SmallLayerScene" type="Label" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Info/HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+tooltip_text = "res://addons/dialogic/Modules/LayoutStuff/Base_Default/default_layout_base.tscn"
+text = "default_layout_base.tscn"
+
+[node name="SmallLayerAuthor" type="Label" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Info/HBoxContainer"]
+unique_name_in_owner = true
+self_modulate = Color(1, 1, 1, 0.603922)
+layout_mode = 2
+theme_type_variation = &"DialogicHintText"
+text = "Dialogic"
+
+[node name="Description" type="HBoxContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Info"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Label" type="Label" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Info/Description"]
+layout_mode = 2
+theme_type_variation = &"DialogicHintText2"
+text = "Info:"
+
+[node name="SmallLayerDescription" type="Label" parent="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoBody/HBox/Info/Description"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+theme_type_variation = &"DialogicHintText2"
+text = "A very simple base for layouts."
+autowrap_mode = 3
+text_overrun_behavior = 4
+
+[node name="LayerSettingsTabs" type="TabContainer" parent="StyleView/PanelContainer/StyleEditor/LayerSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Margin" type="Control" parent="StyleView"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="NoStyleView" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 1
+
+[node name="Label" type="Label" parent="NoStyleView"]
+layout_mode = 2
+text = "You have not set up any styles yet. Dialogic will use a fallback style."
+horizontal_alignment = 1
+autowrap_mode = 3
+
+[node name="StartStylingButton" type="Button" parent="NoStyleView"]
+layout_mode = 2
+size_flags_horizontal = 4
+tooltip_text = "Make a custom style from a variation of the fallback."
+text = "Make my own!"
+
+[node name="StyleBrowserWindow" type="Window" parent="."]
+unique_name_in_owner = true
+title = "Style Browser"
+position = Vector2i(0, 36)
+size = Vector2i(500, 400)
+visible = false
+wrap_controls = true
+transient = true
+popup_window = true
+script = ExtResource("5_qbwx0")
+
+[node name="StyleBrowser" parent="StyleBrowserWindow" instance=ExtResource("6_p6lia")]
+unique_name_in_owner = true
+
+[connection signal="pressed" from="Panel/VBox/StyleButtons/DuplicateButton" to="." method="_on_duplicate_button_pressed"]
+[connection signal="pressed" from="Panel/VBox/StyleButtons/RemoveButton" to="." method="_on_remove_button_pressed"]
+[connection signal="pressed" from="Panel/VBox/StyleButtons/MakeDefaultButton" to="." method="_on_make_default_button_pressed"]
+[connection signal="focus_exited" from="StyleView/HBox/LayoutStyleName" to="." method="_on_layout_style_name_focus_exited"]
+[connection signal="text_submitted" from="StyleView/HBox/LayoutStyleName" to="." method="_on_layout_style_name_text_submitted"]
+[connection signal="pressed" from="StyleView/HBox/EditNameButton" to="." method="_on_edit_name_button_pressed"]
+[connection signal="pressed" from="StyleView/HBox/TestStyleButton" to="." method="_on_test_style_button_pressed"]
+[connection signal="about_to_popup" from="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerButtons/MakeCustomButton" to="StyleView/PanelContainer/StyleEditor" method="_on_make_custom_button_about_to_popup"]
+[connection signal="pressed" from="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerButtons/DeleteLayerButton" to="StyleView/PanelContainer/StyleEditor" method="_on_delete_layer_button_pressed"]
+[connection signal="button_clicked" from="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerTree" to="StyleView/PanelContainer/StyleEditor" method="_on_layer_tree_button_clicked"]
+[connection signal="layer_moved" from="StyleView/PanelContainer/StyleEditor/LayerPanel/VBox/LayerTree" to="StyleView/PanelContainer/StyleEditor" method="_on_layer_tree_layer_moved"]
+[connection signal="pressed" from="StyleView/PanelContainer/StyleEditor/LayerSettings/LayerInfoHeader/ExpandLayerInfo" to="StyleView/PanelContainer/StyleEditor" method="_on_expand_layer_info_pressed"]
+[connection signal="pressed" from="NoStyleView/StartStylingButton" to="." method="_on_start_styling_button_pressed"]
+[connection signal="close_requested" from="StyleBrowserWindow" to="StyleBrowserWindow" method="_on_close_requested"]
+[connection signal="activate_part" from="StyleBrowserWindow/StyleBrowser" to="StyleBrowserWindow" method="_on_style_browser_activate_part"]
--- /dev/null
+@tool
+extends HSplitContainer
+
+## Script that handles the style editor.
+
+
+var current_style: DialogicStyle = null
+
+var customization_editor_info := {}
+
+## The id of the currently selected layer.
+## "" is the base scene.
+var current_layer_id := ""
+
+var _minimum_tree_item_height: int
+
+@onready var tree: Tree = %LayerTree
+
+
+func _ready() -> void:
+ # Styling
+ %AddLayerButton.icon = get_theme_icon("Add", "EditorIcons")
+ %DeleteLayerButton.icon = get_theme_icon("Remove", "EditorIcons")
+ %ReplaceLayerButton.icon = get_theme_icon("Loop", "EditorIcons")
+ %MakeCustomButton.icon = get_theme_icon("FileAccess", "EditorIcons")
+ %ExpandLayerInfo.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
+
+ %AddLayerButton.get_popup().index_pressed.connect(_on_add_layer_menu_pressed)
+ %ReplaceLayerButton.get_popup().index_pressed.connect(_on_replace_layer_menu_pressed)
+ %MakeCustomButton.get_popup().index_pressed.connect(_on_make_custom_menu_pressed)
+ %LayerTree.item_selected.connect(_on_layer_selected)
+ _minimum_tree_item_height = int(DialogicUtil.get_editor_scale() * 32)
+ %LayerTree.add_theme_constant_override("icon_max_width", _minimum_tree_item_height)
+
+
+func load_style(style:DialogicStyle) -> void:
+ current_style = style
+
+ if current_style.has_meta("_latest_layer"):
+ current_layer_id = str(current_style.get_meta("_latest_layer", ""))
+ else:
+ current_layer_id = ""
+
+ %AddLayerButton.disabled = style.inherits_anything()
+ %ReplaceLayerButton.disabled = style.inherits_anything()
+ %MakeCustomButton.disabled = style.inherits_anything()
+ %DeleteLayerButton.disabled = style.inherits_anything()
+
+ load_style_layer_list()
+
+
+func load_style_layer_list() -> void:
+ tree.clear()
+
+ var root := tree.create_item()
+
+ var base_layer_info := current_style.get_layer_inherited_info("")
+ setup_layer_tree_item(base_layer_info, root)
+
+ for layer_id in current_style.get_layer_inherited_list():
+ var layer_info := current_style.get_layer_inherited_info(layer_id)
+ var layer_item := tree.create_item(root)
+ setup_layer_tree_item(layer_info, layer_item)
+
+ select_layer(current_layer_id)
+
+
+func select_layer(id:String) -> void:
+ if id == "":
+ tree.get_root().select(0)
+ else:
+ for child in tree.get_root().get_children():
+ if child.get_meta("id", "") == id:
+ child.select(0)
+ return
+
+
+func setup_layer_tree_item(info:Dictionary, item:TreeItem) -> void:
+ item.custom_minimum_height = _minimum_tree_item_height
+
+ if %StyleBrowser.is_premade_style_part(info.path):
+ if ResourceLoader.exists(%StyleBrowser.premade_scenes_reference[info.path].get('icon', '')):
+ item.set_icon(0, load(%StyleBrowser.premade_scenes_reference[info.path].get("icon")))
+ item.set_text(0, %StyleBrowser.premade_scenes_reference[info.path].get("name", "Layer"))
+
+ else:
+ item.set_text(0, clean_scene_name(info.path))
+ item.add_button(0, get_theme_icon("PackedScene", "EditorIcons"))
+ item.set_button_tooltip_text(0, 0, "Open Scene")
+ item.set_meta("scene", info.path)
+ item.set_meta("id", info.id)
+
+
+func _on_layer_selected() -> void:
+ var item: TreeItem = %LayerTree.get_selected()
+ load_layer(item.get_meta("id", ""))
+
+
+func load_layer(layer_id:=""):
+ current_layer_id = layer_id
+ current_style.set_meta('_latest_layer', current_layer_id)
+
+ var layer_info := current_style.get_layer_inherited_info(layer_id)
+
+ %SmallLayerPreview.hide()
+ if %StyleBrowser.is_premade_style_part(layer_info.get('path', 'Unkown Layer')):
+ var premade_infos = %StyleBrowser.premade_scenes_reference[layer_info.get('path')]
+ %LayerName.text = premade_infos.get('name', 'Unknown Layer')
+ %SmallLayerAuthor.text = "by "+premade_infos.get('author', '')
+ %SmallLayerDescription.text = premade_infos.get('description', '')
+
+ if premade_infos.get('preview_image', null) and ResourceLoader.exists(premade_infos.get('preview_image')[0]):
+ %SmallLayerPreview.texture = load(premade_infos.get('preview_image')[0])
+ %SmallLayerPreview.show()
+
+ else:
+ %LayerName.text = clean_scene_name(layer_info.get('path', 'Unkown Layer'))
+ %SmallLayerAuthor.text = "Custom Layer"
+ %SmallLayerDescription.text = layer_info.get('path', 'Unkown Layer')
+
+ %DeleteLayerButton.disabled = layer_id == "" or current_style.inherits_anything()
+
+ %SmallLayerScene.text = layer_info.get('path', 'Unkown Layer').get_file()
+ %SmallLayerScene.tooltip_text = layer_info.get('path', '')
+
+ var inherited_layer_info := current_style.get_layer_inherited_info(layer_id, true)
+ load_layout_scene_customization(
+ layer_info.path,
+ layer_info.overrides,
+ inherited_layer_info.overrides)
+
+
+
+func add_layer(scene_path:="", overrides:= {}):
+ current_style.add_layer(scene_path, overrides)
+ load_style_layer_list()
+ await get_tree().process_frame
+ %LayerTree.get_root().get_child(-1).select(0)
+
+
+func delete_layer() -> void:
+ if current_layer_id == "":
+ return
+
+ current_style.delete_layer(current_layer_id)
+ load_style_layer_list()
+ %LayerTree.get_root().select(0)
+
+
+func move_layer(from_idx:int, to_idx:int) -> void:
+ current_style.move_layer(from_idx, to_idx)
+
+ load_style_layer_list()
+ select_layer(current_style.get_layer_id_at_index(to_idx))
+
+
+func replace_layer(layer_id:String, scene_path:String) -> void:
+ current_style.set_layer_scene(layer_id, scene_path)
+
+ load_style_layer_list()
+ select_layer(layer_id)
+
+
+func _on_add_layer_menu_pressed(index:int) -> void:
+ # Adding a premade layer
+ if index == 2:
+ %StyleBrowserWindow.popup_centered_ratio(0.6)
+ %StyleBrowser.current_type = 2
+ %StyleBrowser.load_parts()
+ var picked_info: Dictionary = await %StyleBrowserWindow.get_picked_info()
+ if not picked_info.is_empty():
+ add_layer(picked_info.get('path', ''))
+
+ # Adding a custom scene as a layer
+ else:
+ find_parent('EditorView').godot_file_dialog(
+ _on_add_custom_layer_file_selected,
+ '*.tscn, Scenes',
+ EditorFileDialog.FILE_MODE_OPEN_FILE,
+ "Open custom layer scene")
+
+
+func _on_replace_layer_menu_pressed(index:int) -> void:
+ # Adding a premade layer
+ if index == 2:
+ %StyleBrowserWindow.popup_centered_ratio(0.6)
+ if %LayerTree.get_selected() == %LayerTree.get_root():
+ %StyleBrowser.current_type = 3
+ else:
+ %StyleBrowser.current_type = 2
+ %StyleBrowser.load_parts()
+ var picked_info: Dictionary = await %StyleBrowserWindow.get_picked_info()
+ if not picked_info.is_empty():
+ replace_layer(%LayerTree.get_selected().get_meta("id", ""), picked_info.get('path', ''))
+
+ # Adding a custom scene as a layer
+ else:
+ find_parent('EditorView').godot_file_dialog(
+ _on_replace_custom_layer_file_selected,
+ '*.tscn, Scenes',
+ EditorFileDialog.FILE_MODE_OPEN_FILE,
+ "Open custom layer scene")
+
+
+func _on_add_custom_layer_file_selected(file_path:String) -> void:
+ add_layer(file_path)
+
+
+func _on_replace_custom_layer_file_selected(file_path:String) -> void:
+ replace_layer(%LayerTree.get_selected().get_meta("id", ""), file_path)
+
+
+func _on_make_custom_button_about_to_popup() -> void:
+ %MakeCustomButton.get_popup().set_item_disabled(2, false)
+ %MakeCustomButton.get_popup().set_item_disabled(3, false)
+
+ if not %StyleBrowser.is_premade_style_part(current_style.get_layer_info(current_layer_id).path):
+ %MakeCustomButton.get_popup().set_item_disabled(2, true)
+
+
+func _on_make_custom_menu_pressed(index:int) -> void:
+ # This layer only
+ if index == 2:
+ find_parent('EditorView').godot_file_dialog(
+ _on_make_custom_layer_file_selected,
+ '',
+ EditorFileDialog.FILE_MODE_OPEN_DIR,
+ "Select folder for new copy of layer")
+ # The full layout
+ if index == 3:
+ find_parent('EditorView').godot_file_dialog(
+ _on_make_custom_layout_file_selected,
+ '',
+ EditorFileDialog.FILE_MODE_OPEN_DIR,
+ "Select folder for new layout scene")
+
+
+func _on_make_custom_layer_file_selected(file:String) -> void:
+ make_layer_custom(file)
+
+
+func _on_make_custom_layout_file_selected(file:String) -> void:
+ make_layout_custom(file)
+
+
+func make_layer_custom(target_folder:String, custom_name := "") -> void:
+
+ var original_file: String = current_style.get_layer_info(current_layer_id).path
+ var custom_new_folder := ""
+
+ if custom_name.is_empty():
+ custom_name = "custom_"+%StyleBrowser.premade_scenes_reference[original_file].name.to_snake_case()
+ custom_new_folder = %StyleBrowser.premade_scenes_reference[original_file].name.to_pascal_case()
+
+ var result_path := DialogicUtil.make_file_custom(
+ original_file,
+ target_folder,
+ custom_name,
+ custom_new_folder,
+ )
+
+ current_style.set_layer_scene(current_layer_id, result_path)
+
+ load_style_layer_list()
+
+ if %LayerTree.get_selected() == %LayerTree.get_root():
+ %LayerTree.get_root().select(0)
+ else:
+ %LayerTree.get_root().get_child(%LayerTree.get_selected().get_index()).select(0)
+
+
+func make_layout_custom(target_folder:String) -> void:
+ target_folder = target_folder.path_join("Custom" + current_style.name.to_pascal_case())
+
+ DirAccess.make_dir_absolute(target_folder)
+ %LayerTree.get_root().select(0)
+ make_layer_custom(target_folder, "custom_" + current_style.name.to_snake_case())
+
+
+ var base_layer_info := current_style.get_layer_info("")
+ var target_path: String = base_layer_info.path
+
+ # Load base scene
+ var base_scene_pck: PackedScene = load(base_layer_info.path).duplicate()
+ var base_scene := base_scene_pck.instantiate()
+ base_scene.name = "Custom" + clean_scene_name(base_scene_pck.resource_path).to_pascal_case()
+
+ # Load layers
+ for layer_id in current_style.get_layer_inherited_list():
+ var layer_info := current_style.get_layer_inherited_info(layer_id)
+
+ if not ResourceLoader.exists(layer_info.path):
+ continue
+
+ var layer_scene: DialogicLayoutLayer = load(layer_info.path).instantiate()
+
+ base_scene.add_layer(layer_scene)
+ layer_scene.owner = base_scene
+ layer_scene.apply_overrides_on_ready = true
+
+ # Apply layer overrides
+ DialogicUtil.apply_scene_export_overrides(layer_scene, layer_info.overrides, false)
+
+ var pckd_scn := PackedScene.new()
+ pckd_scn.pack(base_scene)
+ pckd_scn.take_over_path(target_path)
+ ResourceSaver.save(pckd_scn, target_path)
+
+ current_style.base_scene = load(target_path)
+ current_style.inherits = null
+ current_style.layers = []
+ current_style.changed.emit()
+
+ load_style_layer_list()
+
+ %LayerTree.get_root().select(0)
+ find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
+
+
+
+func _on_delete_layer_button_pressed() -> void:
+ delete_layer()
+
+
+#region Layer Settings
+####### LAYER SETTINGS #########################################################
+
+func load_layout_scene_customization(custom_scene_path:String, overrides:Dictionary = {}, inherited_overrides:Dictionary = {}) -> void:
+ for child in %LayerSettingsTabs.get_children():
+ child.get_parent().remove_child(child)
+ child.queue_free()
+
+ var scene: Node = null
+ if !custom_scene_path.is_empty() and ResourceLoader.exists(custom_scene_path):
+ var pck_scn := load(custom_scene_path)
+ if pck_scn:
+ scene = pck_scn.instantiate()
+
+ var settings := []
+ if scene and scene.script:
+ settings = collect_settings(scene.script.get_script_property_list())
+
+ if settings.is_empty():
+ var note := Label.new()
+ note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ note.text = "This layer has no exposed settings."
+ if not %StyleBrowser.is_premade_style_part(custom_scene_path):
+ note.text += "\n\nIf you want to add settings, make sure to have a root script in @tool mode and expose some @exported variables to show up here."
+ note.theme_type_variation = 'DialogicHintText2'
+ %LayerSettingsTabs.add_child(note)
+ note.name = "General"
+ return
+
+ var current_grid: GridContainer = null
+
+ var label_bg_style := get_theme_stylebox("CanvasItemInfoOverlay", "EditorStyles").duplicate()
+ label_bg_style.content_margin_left = 5
+ label_bg_style.content_margin_right = 5
+ label_bg_style.content_margin_top = 5
+
+ var current_group_name := ""
+ var current_subgroup_name := ""
+ customization_editor_info = {}
+
+ for i in settings:
+ match i['id']:
+ &"GROUP":
+ var main_scroll := ScrollContainer.new()
+ main_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ main_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ main_scroll.name = i['name']
+ %LayerSettingsTabs.add_child(main_scroll, true)
+
+ current_grid = GridContainer.new()
+ current_grid.columns = 3
+ current_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ main_scroll.add_child(current_grid)
+ current_group_name = i['name'].to_snake_case()
+ current_subgroup_name = ""
+
+ &"SUBGROUP":
+
+ # add separator
+ if current_subgroup_name:
+ current_grid.add_child(HSeparator.new())
+ current_grid.get_child(-1).add_theme_constant_override('separation', 20)
+ current_grid.add_child(current_grid.get_child(-1).duplicate())
+ current_grid.add_child(current_grid.get_child(-1).duplicate())
+
+ var title_label := Label.new()
+ title_label.text = i['name']
+ title_label.theme_type_variation = "DialogicSection"
+ title_label.size_flags_horizontal = SIZE_EXPAND_FILL
+ current_grid.add_child(title_label, true)
+
+ # add spaced to the grid
+ current_grid.add_child(Control.new())
+ current_grid.add_child(Control.new())
+
+ current_subgroup_name = i['name'].to_snake_case()
+
+ &"SETTING":
+ var label := Label.new()
+ label.text = str(i['name'].trim_prefix(current_group_name+'_').trim_prefix(current_subgroup_name+'_')).capitalize()
+ current_grid.add_child(label, true)
+
+ var scene_value: Variant = scene.get(i['name'])
+ customization_editor_info[i['name']] = {}
+
+ if i['name'] in inherited_overrides:
+ customization_editor_info[i['name']]['orig'] = str_to_var(inherited_overrides.get(i['name']))
+ else:
+ customization_editor_info[i['name']]['orig'] = scene_value
+
+ var current_value: Variant
+ if i['name'] in overrides:
+ current_value = str_to_var(overrides.get(i['name']))
+ else:
+ current_value = customization_editor_info[i['name']]['orig']
+
+ var input: Node = DialogicUtil.setup_script_property_edit_node(i, current_value, set_export_override)
+
+ input.size_flags_horizontal = SIZE_EXPAND_FILL
+ customization_editor_info[i['name']]['node'] = input
+
+ var reset := Button.new()
+ reset.flat = true
+ reset.icon = get_theme_icon("Reload", "EditorIcons")
+ reset.tooltip_text = "Remove customization"
+ customization_editor_info[i['name']]['reset'] = reset
+ reset.disabled = current_value == customization_editor_info[i['name']]['orig']
+ current_grid.add_child(reset)
+ reset.pressed.connect(_on_export_override_reset.bind(i['name']))
+ current_grid.add_child(input)
+
+ if scene:
+ scene.queue_free()
+
+
+func collect_settings(properties:Array[Dictionary]) -> Array[Dictionary]:
+ var settings: Array[Dictionary] = []
+
+ var current_group := {}
+ var current_subgroup := {}
+
+ for i in properties:
+ if i['usage'] & PROPERTY_USAGE_CATEGORY:
+ continue
+
+ if (i['usage'] & PROPERTY_USAGE_GROUP):
+ current_group = i
+ current_group['added'] = false
+ current_group['id'] = &'GROUP'
+ current_subgroup = {}
+
+ elif i['usage'] & PROPERTY_USAGE_SUBGROUP:
+ current_subgroup = i
+ current_subgroup['added'] = false
+ current_subgroup['id'] = &'SUBGROUP'
+
+ elif i['usage'] & PROPERTY_USAGE_EDITOR:
+ if current_group.get('name', '') == 'Private':
+ continue
+
+ if current_group.is_empty():
+ current_group = {'name':'General', 'added':false, 'id':&"GROUP"}
+
+ if current_group.get('added', true) == false:
+ settings.append(current_group)
+ current_group['added'] = true
+
+ if current_subgroup.is_empty():
+ current_subgroup = {'name':current_group['name'], 'added':false, 'id':&"SUBGROUP"}
+
+ if current_subgroup.get('added', true) == false:
+ settings.append(current_subgroup)
+ current_subgroup['added'] = true
+
+ i['id'] = &'SETTING'
+ settings.append(i)
+ return settings
+
+
+func set_export_override(property_name:String, value:String = "") -> void:
+ if str_to_var(value) != customization_editor_info[property_name]['orig']:
+ current_style.set_layer_setting(current_layer_id, property_name, value)
+ customization_editor_info[property_name]['reset'].disabled = false
+ else:
+ current_style.remove_layer_setting(current_layer_id, property_name)
+ customization_editor_info[property_name]['reset'].disabled = true
+
+
+func _on_export_override_reset(property_name:String) -> void:
+ current_style.remove_layer_setting(current_layer_id, property_name)
+ customization_editor_info[property_name]['reset'].disabled = true
+ set_customization_value(property_name, customization_editor_info[property_name]['orig'])
+
+
+func set_customization_value(property_name:String, value:Variant) -> void:
+ var node: Node = customization_editor_info[property_name]['node']
+ if node is CheckBox:
+ node.button_pressed = value
+ elif node is LineEdit:
+ node.text = value
+ elif node.has_method('set_value'):
+ node.set_value(value)
+ elif node is ColorPickerButton:
+ node.color = value
+ elif node is OptionButton:
+ node.select(value)
+ elif node is SpinBox:
+ node.value = value
+
+#endregion
+
+
+func _on_expand_layer_info_pressed() -> void:
+ if %LayerInfoBody.visible:
+ %LayerInfoBody.hide()
+ %ExpandLayerInfo.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
+ else:
+ %LayerInfoBody.show()
+ %ExpandLayerInfo.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
+
+
+func _on_layer_tree_layer_moved(from: int, to: int) -> void:
+ move_layer(from, to)
+
+
+func _on_layer_tree_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void:
+ if ResourceLoader.exists(item.get_meta('scene')):
+ find_parent('EditorView').plugin_reference.get_editor_interface().open_scene_from_path(item.get_meta('scene'))
+ find_parent('EditorView').plugin_reference.get_editor_interface().set_main_screen_editor("2D")
+
+
+#region Helpers
+####### HELPERS ################################################################
+
+func clean_scene_name(file_path:String) -> String:
+ return file_path.get_file().trim_suffix('.tscn').capitalize()
+
+#endregion
--- /dev/null
+<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="16" xmlns="http://www.w3.org/2000/svg" height="16" id="screenshot-dd20a063-b517-80d9-8002-55556598bb7d" viewBox="0 0 16 16" style="-webkit-print-color-adjust: exact;" fill="none" version="1.1"><g id="shape-dd20a063-b517-80d9-8002-55556598bb7d"><defs><clipPath class="frame-clip-def frame-clip" id="frame-clip-dd20a063-b517-80d9-8002-55556598bb7d-rumext-id-1"><rect rx="0" ry="0" x="0" y="0" width="16" height="16" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath></defs><g clip-path="url(#frame-clip-dd20a063-b517-80d9-8002-55556598bb7d-rumext-id-1)"><clipPath class="frame-clip-def frame-clip" id="frame-clip-dd20a063-b517-80d9-8002-55556598bb7d-rumext-id-1"><rect rx="0" ry="0" x="0" y="0" width="16" height="16" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath><g class="fills" id="fills-dd20a063-b517-80d9-8002-55556598bb7d"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" width="16" height="16" class="frame-background"/></g><g class="frame-children"><g id="shape-dd20a063-b517-80d9-8002-555549440d01" rx="0" ry="0"><g id="shape-dd20a063-b517-80d9-8002-555549440d02"><g class="fills" id="fills-dd20a063-b517-80d9-8002-555549440d02"><path rx="0" ry="0" d="M7.549,13.909L8.078,10.224L9.132,9.405L11.492,9.520L12.839,8.702L13.346,7.337L3.044,5.307L2.771,6.673L3.746,7.922L6.244,8.820L6.283,9.834L4.644,12.917L4.630,14.274L5.502,14.985L6.751,15.024" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></g><g id="shape-dd20a063-b517-80d9-8002-555549440d03"><g class="fills" id="fills-dd20a063-b517-80d9-8002-555549440d03"><path rx="0" ry="0" d="M3.161,4.605L13.541,6.634L15.024,1.678L11.239,2.459L8.657,1.644L5.843,2.006L4.956,1.327L3.941,1.522L3.161,4.605ZL3.161,4.605Z" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></g></g></g></g></g></svg>
\ No newline at end of file
--- /dev/null
+class_name DialogicAutoAdvance
+extends RefCounted
+## This class holds the settings for the Auto-Advance feature.
+## Changing the variables will alter the behaviour of Auto-Advance.
+##
+## Auto-Advance is a feature that automatically advances the timeline after
+## a player-specific amount of time.
+## This is useful for visual novels that want the player to read the text
+## without having to press.
+##
+## Unlike [class DialogicAutoSkip], Auto-Advance uses multiple enable flags,
+## allowing to track the different instances that enabled Auto-Advance.
+## For instance, if a timeline event forces Auto-Advance to be enabled and later
+## disables it, the Auto-Advance will still be enabled if the player didn't
+## cancel it.
+
+signal autoadvance
+signal toggled(enabled: bool)
+
+var autoadvance_timer := Timer.new()
+
+var fixed_delay: float = 1.0
+var delay_modifier: float = 1.0
+
+var per_word_delay: float = 0.0
+var per_character_delay: float = 0.1
+
+var ignored_characters_enabled := false
+var ignored_characters := {}
+
+var await_playing_voice := true
+
+var override_delay_for_current_event: float = -1.0
+
+## Private variable to track the last Auto-Advance state.
+## This will be used to emit the [signal toggled] signal.
+var _last_enable_state := false
+
+## If true, Auto-Advance will be active until the next event.
+##
+## Use this flag to create a temporary Auto-Advance mode.
+## You can utilise [variable override_delay_for_current_event] to set a
+## temporary Auto-Advance delay for this event.
+##
+## Stacks with [variable enabled_forced] and [variable enabled_until_user_input].
+var enabled_until_next_event := false :
+ set(enabled):
+ enabled_until_next_event = enabled
+ _try_emit_toggled()
+
+## If true, Auto-Advance will stay enabled until this is set to false.
+##
+## This boolean can be used to create an automatic text display.
+##
+## Stacks with [variable enabled_until_next_event] and [variable enabled_until_user_input].
+var enabled_forced := false :
+ set(enabled):
+ enabled_forced = enabled
+ _try_emit_toggled()
+
+## If true, Auto-Advance will be active until the player presses a button.
+##
+## Use this flag when the player wants to enable Auto-Advance.
+##
+## Stacks with [variable enabled_forced] and [variable enabled_until_next_event].
+var enabled_until_user_input := false :
+ set(enabled):
+ enabled_until_user_input = enabled
+ _try_emit_toggled()
+
+
+func _init() -> void:
+ DialogicUtil.autoload().Inputs.add_child(autoadvance_timer)
+ autoadvance_timer.one_shot = true
+ autoadvance_timer.timeout.connect(_on_autoadvance_timer_timeout)
+ toggled.connect(_on_toggled)
+
+ enabled_forced = ProjectSettings.get_setting('dialogic/text/autoadvance_enabled', false)
+ fixed_delay = ProjectSettings.get_setting('dialogic/text/autoadvance_fixed_delay', 1)
+ per_word_delay = ProjectSettings.get_setting('dialogic/text/autoadvance_per_word_delay', 0)
+ per_character_delay = ProjectSettings.get_setting('dialogic/text/autoadvance_per_character_delay', 0.1)
+ ignored_characters_enabled = ProjectSettings.get_setting('dialogic/text/autoadvance_ignored_characters_enabled', true)
+ ignored_characters = ProjectSettings.get_setting('dialogic/text/autoadvance_ignored_characters', {})
+
+#region AUTOADVANCE INTERNALS
+
+func start() -> void:
+ if not is_enabled():
+ return
+
+ var parsed_text: String = DialogicUtil.autoload().current_state_info['text_parsed']
+ var delay := _calculate_autoadvance_delay(parsed_text)
+
+ await DialogicUtil.autoload().get_tree().process_frame
+ if delay == 0:
+ _on_autoadvance_timer_timeout()
+ else:
+ autoadvance_timer.start(delay)
+
+
+## Calculates the autoadvance-time based on settings and text.
+##
+## Takes into account:
+## - temporary delay time override
+## - delay per word
+## - delay per character
+## - fixed delay
+## - text time taken
+## - autoadvance delay modifier
+## - voice audio
+func _calculate_autoadvance_delay(text: String = "") -> float:
+ var delay := 0.0
+
+ # Check for temporary time override
+ if override_delay_for_current_event >= 0:
+ delay = override_delay_for_current_event
+ else:
+ # Add per word and per character delay
+ delay = _calculate_per_word_delay(text) + _calculate_per_character_delay(text)
+
+ delay *= delay_modifier
+ # Apply fixed delay last, so it's not affected by the delay modifier
+ delay += fixed_delay
+
+ delay = max(0, delay)
+
+ # Wait for the voice clip (if longer than the current delay)
+ if await_playing_voice and DialogicUtil.autoload().has_subsystem('Voice') and DialogicUtil.autoload().Voice.is_running():
+ delay = max(delay, DialogicUtil.autoload().Voice.get_remaining_time())
+
+ return delay
+
+
+## Checks how many words can be found by separating the text by whitespace.
+## (Uses ` ` aka SPACE right now, could be extended in the future)
+func _calculate_per_word_delay(text: String) -> float:
+ return float(text.split(' ', false).size() * per_word_delay)
+
+
+## Checks how many characters can be found by iterating each letter.
+func _calculate_per_character_delay(text: String) -> float:
+ var calculated_delay: float = 0
+
+ if per_character_delay > 0:
+ # If we have characters to ignore, we will iterate each letter.
+ if ignored_characters_enabled:
+ for character in text:
+ if character in ignored_characters:
+ continue
+ calculated_delay += per_character_delay
+
+ # Otherwise, we can just multiply the length of the text by the delay.
+ else:
+ calculated_delay = text.length() * per_character_delay
+
+ return calculated_delay
+
+
+func _on_autoadvance_timer_timeout() -> void:
+ autoadvance.emit()
+ autoadvance_timer.stop()
+
+
+## Switches the auto-advance mode on or off based on [param enabled].
+func _on_toggled(enabled: bool) -> void:
+ # If auto-advance is enabled and we are not auto-advancing yet,
+ # we will initiate the auto-advance mode.
+ if (enabled and !is_advancing()
+ and DialogicUtil.autoload().current_state == DialogicGameHandler.States.IDLE
+ and not DialogicUtil.autoload().current_state_info.get('text', '').is_empty()):
+ start()
+
+ # If auto-advance is disabled and we are auto-advancing,
+ # we want to cancel the auto-advance mode.
+ elif !enabled and is_advancing():
+ DialogicUtil.autoload().Inputs.stop_timers()
+#endregion
+
+#region AUTOADVANCE HELPERS
+func is_advancing() -> bool:
+ return !autoadvance_timer.is_stopped()
+
+
+func get_time_left() -> float:
+ return autoadvance_timer.time_left
+
+
+func get_time() -> float:
+ return autoadvance_timer.wait_time
+
+
+## Returns whether Auto-Advance is currently considered enabled.
+## Auto-Advance uses three different enable flags:
+## - enabled_until_user_input (becomes false on any dialogic input action)
+## - enabled_until_next_event (becomes false on each text event)
+## - enabled_forced (becomes false only when disabled via code)
+##
+## All three can be set with dedicated methods.
+func is_enabled() -> bool:
+ return (enabled_until_next_event
+ or enabled_until_user_input
+ or enabled_forced)
+
+
+## Updates the [member _autoadvance_enabled] variable to properly check if the value has changed.
+## If it changed, emits the [member toggled] signal.
+func _try_emit_toggled() -> void:
+ var old_autoadvance_state := _last_enable_state
+ _last_enable_state = is_enabled()
+
+ if old_autoadvance_state != _last_enable_state:
+ toggled.emit(_last_enable_state)
+
+
+## An internal method connected to changes on the Delay Modifier setting.
+func _update_autoadvance_delay_modifier(delay_modifier_value: float) -> void:
+ delay_modifier = delay_modifier_value
+
+
+## Returns the progress of the auto-advance timer on a scale between 0 and 1.
+## The higher the value, the closer the timer is to finishing.
+## If auto-advancing is disabled, returns -1.
+func get_progress() -> float:
+ if !is_advancing():
+ return -1
+
+ var total_time: float = get_time()
+ var time_left: float = get_time_left()
+ var progress: float = (total_time - time_left) / total_time
+
+ return progress
+#endregion
--- /dev/null
+class_name DialogicAutoSkip
+extends RefCounted
+## This class holds the settings for the Auto-Skip feature.
+## Changing the variables will alter the behaviour of Auto-Skip.
+##
+## Auto-Skip must be implemented per event.
+
+## Emitted whenever the Auto-Skip state changes, from `true` to `false` or
+## vice-versa.
+signal toggled(is_enabled: bool)
+
+## Whether Auto-Skip is enabled or not.
+## If Auto-Skip is referred to be [i]disabled[/i], it refers to setting this
+## this variable to `false`.
+## This variable will automatically emit [signal autoskip_changed] when changed.
+var enabled := false : set = _set_enabled
+
+## If `true`, Auto-Skip will be disabled when the user presses a recognised
+## input action.
+var disable_on_user_input := true
+
+## If `true`, Auto-Skip will be disabled when the timeline advances to a
+## unread Text event or an event requesting user input.
+var disable_on_unread_text := false
+
+## If `true`, Auto-Skip will be enabled when the timeline advances to a
+## previously visited Text event.
+## Useful if the player always wants to skip already-visited Text events.
+var enable_on_visited := false
+
+## If `true`, Auto-Skip will skip Voice events instead of playing them.
+var skip_voice := true
+
+## The amount of seconds each event may take.
+## This is not enforced, each event must implement this behaviour.
+var time_per_event: float = 0.1
+
+
+## Setting up Auto-Skip.
+func _init() -> void:
+ time_per_event = ProjectSettings.get_setting('dialogic/text/autoskip_time_per_event', time_per_event)
+
+ if DialogicUtil.autoload().has_subsystem("History") and not DialogicUtil.autoload().History.visited_event.is_connected(_handle_seen_event):
+ DialogicUtil.autoload().History.visited_event.connect(_handle_seen_event)
+ DialogicUtil.autoload().History.unvisited_event.connect(_handle_unseen_event)
+
+
+## Called when Auto-Skip is enabled or disabled.
+## Emits [signal autoskip_changed] if the state changed.
+func _set_enabled(is_enabled: bool) -> void:
+ var previous_enabled := enabled
+ enabled = is_enabled
+
+ if enabled != previous_enabled:
+ toggled.emit(enabled)
+
+
+func _handle_seen_event() -> void:
+ # If Auto-Skip is disabled but reacts to seen events, we
+ # enable Auto-Skip.
+ if not enabled and enable_on_visited:
+ enabled = true
+
+
+func _handle_unseen_event() -> void:
+ if not enabled:
+ return
+
+ if disable_on_unread_text:
+ enabled = false
--- /dev/null
+@tool
+extends DialogicCharacterEditorMainSection
+
+## Character editor section that allows editing typing sound moods.
+
+var current_mood := ''
+var current_moods_info := {}
+var default_mood := ''
+
+
+func _init() -> void:
+ hint_text = 'Typing sound moods allow you to vary the "typing" sounds of your character. \nThey can be changed based on the portrait or with the [mood=something] text effect.'
+
+
+func _get_title() -> String:
+ return "Typing Sounds"
+
+################################################################################
+## COMMUNICATION WITH EDITOR
+################################################################################
+
+func _load_character(character:DialogicCharacter):
+ default_mood = character.custom_info.get('sound_mood_default', '')
+
+ current_moods_info = character.custom_info.get('sound_moods', {}).duplicate(true)
+
+ current_mood = ""
+ update_mood_list()
+
+
+func _save_changes(character:DialogicCharacter) -> DialogicCharacter:
+ # Quickly save latest mood
+ if current_mood:
+ current_moods_info[current_mood] = get_mood_info()
+
+ character.custom_info['sound_mood_default'] = default_mood
+ character.custom_info['sound_moods'] = current_moods_info.duplicate(true)
+ return character
+
+
+func get_portrait_data() -> Dictionary:
+ if character_editor.selected_item and is_instance_valid(character_editor.selected_item):
+ return character_editor.selected_item.get_metadata(0)
+ return {}
+
+
+func set_portrait_data(data:Dictionary) -> void:
+ if character_editor.selected_item and is_instance_valid(character_editor.selected_item):
+ character_editor.selected_item.set_metadata(0, data)
+
+
+################################################################################
+## OWN STUFF
+################################################################################
+
+func _ready() -> void:
+ %ListPanel.self_modulate = get_theme_color("base_color", "Editor")
+ %Add.icon = get_theme_icon("Add", "EditorIcons")
+ %Delete.icon = get_theme_icon("Remove", "EditorIcons")
+ %Duplicate.icon = get_theme_icon("Duplicate", "EditorIcons")
+ %Play.icon = get_theme_icon("Play", "EditorIcons")
+ %Default.icon = get_theme_icon("NonFavorite", "EditorIcons")
+
+ %NameWarning.texture = get_theme_icon("StatusWarning", "EditorIcons")
+
+
+func update_mood_list(selected_name := "") -> void:
+ %MoodList.clear()
+
+ for mood in current_moods_info:
+ var idx: int = %MoodList.add_item(mood, get_theme_icon("AudioStreamPlayer", "EditorIcons"))
+ if mood == selected_name:
+ %MoodList.select(idx)
+ _on_mood_list_item_selected(idx)
+ if !%MoodList.is_anything_selected() and %MoodList.item_count:
+ %MoodList.select(0)
+ _on_mood_list_item_selected(0)
+
+ if %MoodList.item_count == 0:
+ current_mood = ""
+
+ %Delete.disabled = !%MoodList.is_anything_selected()
+ %Play.disabled = !%MoodList.is_anything_selected()
+ %Duplicate.disabled = !%MoodList.is_anything_selected()
+ %Default.disabled = !%MoodList.is_anything_selected()
+ %Settings.visible = %MoodList.is_anything_selected()
+
+ %MoodList.custom_minimum_size.y = min(%MoodList.item_count*45, 100)
+ %MoodList.visible = %MoodList.item_count != 0
+
+ character_editor.get_settings_section_by_name('Typing Sound Mood', false).update_visibility(%MoodList.item_count != 0)
+
+
+
+func _input(event:InputEvent) -> void:
+ if !is_visible_in_tree() or (get_viewport().gui_get_focus_owner() and !name+'/' in str(get_viewport().gui_get_focus_owner().get_path())):
+ return
+ if event is InputEventKey and event.keycode == KEY_F2 and event.pressed:
+ if %MoodList.is_anything_selected():
+ %Name.grab_focus()
+ %Name.select_all()
+ get_viewport().set_input_as_handled()
+
+
+func _on_mood_list_item_selected(index:int) -> void:
+ if current_mood:
+ current_moods_info[current_mood] = get_mood_info()
+
+ current_mood = %MoodList.get_item_text(index)
+ load_mood_info(current_moods_info[current_mood])
+
+ %Delete.disabled = !%MoodList.is_anything_selected()
+ %Play.disabled = !%MoodList.is_anything_selected()
+ %Duplicate.disabled = !%MoodList.is_anything_selected()
+ %Default.disabled = !%MoodList.is_anything_selected()
+ %Settings.visible = %MoodList.is_anything_selected()
+
+
+func load_mood_info(dict:Dictionary) -> void:
+ %Name.text = dict.get('name', '')
+ %NameWarning.hide()
+ set_default_button(default_mood == dict.get('name', ''))
+ %SoundFolder.set_value(dict.get('sound_path', ''))
+ %Mode.select(dict.get('mode', 0))
+ %PitchBase.set_value(dict.get('pitch_base', 1))
+ %PitchVariance.set_value(dict.get('pitch_variance', 0))
+ %VolumeBase.set_value(dict.get('volume_base', 0))
+ %VolumeVariance.set_value(dict.get('volume_variance', 0))
+ %Skip.set_value(dict.get('skip_characters', 0))
+
+
+func get_mood_info() -> Dictionary:
+ var dict := {}
+ dict['name'] = %Name.text
+ dict['sound_path'] = %SoundFolder.current_value
+ dict['mode'] = %Mode.selected
+ dict['pitch_base'] = %PitchBase.value
+ dict['pitch_variance'] = %PitchVariance.value
+ dict['volume_base'] = %VolumeBase.value
+ dict['volume_variance'] = %VolumeVariance.value
+ dict['skip_characters'] = %Skip.value
+ return dict
+
+
+func _on_add_pressed() -> void:
+ if !current_mood.is_empty():
+ current_moods_info[current_mood] = get_mood_info()
+
+ var new_name := 'Mood '
+ var counter := 1
+ while new_name+str(counter) in current_moods_info:
+ counter+=1
+ new_name += str(counter)
+
+ current_moods_info[new_name] = {'name':new_name}
+
+ update_mood_list(new_name)
+
+
+func _on_duplicate_pressed() -> void:
+ if !current_mood.is_empty():
+ current_moods_info[current_mood] = get_mood_info()
+
+ current_moods_info[current_mood+"_copy"] = get_mood_info()
+ current_moods_info[current_mood+"_copy"].name = current_mood+"_copy"
+ update_mood_list(current_mood+"_copy")
+
+
+func _on_delete_pressed() -> void:
+ if current_mood.is_empty():
+ return
+ current_moods_info.erase(current_mood)
+ current_mood = ""
+ update_mood_list()
+
+
+func _on_name_text_changed(new_text:String) -> void:
+ if new_text.is_empty():
+ %NameWarning.show()
+ %NameWarning.tooltip_text = "Name cannot be empty!"
+ elif new_text in current_moods_info and new_text != current_mood:
+ %NameWarning.show()
+ %NameWarning.tooltip_text = "Name is already in use!"
+ else:
+ %NameWarning.hide()
+
+
+func _on_name_text_submitted(new_text:String) -> void:
+ if %NameWarning.visible:
+ new_text = current_mood
+ %NameWarning.hide()
+ else:
+ %MoodList.set_item_text(%MoodList.get_selected_items()[0], new_text)
+ current_moods_info.erase(current_mood)
+ current_moods_info[new_text] = get_mood_info()
+ current_mood = new_text
+
+
+func _on_name_focus_exited() -> void:
+ _on_name_text_submitted(%Name.text)
+
+
+func _on_default_toggled(button_pressed:bool) -> void:
+ if button_pressed:
+ default_mood = current_mood
+ else:
+ default_mood = ''
+ set_default_button(button_pressed)
+
+
+func set_default_button(enabled:bool) -> void:
+ %Default.set_pressed_no_signal(enabled)
+ if enabled:
+ %Default.icon = get_theme_icon("Favorites", "EditorIcons")
+ else:
+ %Default.icon = get_theme_icon("NonFavorite", "EditorIcons")
+
+
+func preview() -> void:
+ $Preview.load_overwrite(get_mood_info())
+ var preview_timer := Timer.new()
+ DialogicUtil.update_timer_process_callback(preview_timer)
+ add_child(preview_timer)
+ preview_timer.start(ProjectSettings.get_setting('dialogic/text/letter_speed', 0.01))
+
+ for i in range(20):
+ $Preview._on_continued_revealing_text("a")
+ await preview_timer.timeout
+
+ preview_timer.queue_free()
--- /dev/null
+[gd_scene load_steps=9 format=3 uid="uid://8ad1pwbjuqpt"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/character_settings/character_moods_settings.gd" id="1_3px07"]
+[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="2_e1vyd"]
+[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="3_yjcns"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/node_type_sound.gd" id="5_yscws"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y7t05"]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 20
+corner_radius_top_right = 20
+
+[sub_resource type="Image" id="Image_ylh4a"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_drtd2"]
+image = SubResource("Image_ylh4a")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_40fkd"]
+content_margin_top = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(1, 1, 1, 0.0588235)
+border_width_left = 1
+border_width_right = 1
+border_width_bottom = 1
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[node name="Typing Sounds" type="VBoxContainer"]
+offset_right = 443.0
+offset_bottom = 144.0
+script = ExtResource("1_3px07")
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="ListPanel" type="PanelContainer" parent="VBox"]
+unique_name_in_owner = true
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_y7t05")
+
+[node name="Vbox" type="VBoxContainer" parent="VBox/ListPanel"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBox/ListPanel/Vbox"]
+layout_mode = 2
+alignment = 2
+
+[node name="Add" type="Button" parent="VBox/ListPanel/Vbox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add type sound mood"
+icon = SubResource("ImageTexture_drtd2")
+
+[node name="Duplicate" type="Button" parent="VBox/ListPanel/Vbox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Duplicate"
+icon = SubResource("ImageTexture_drtd2")
+
+[node name="Delete" type="Button" parent="VBox/ListPanel/Vbox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Delete mood"
+icon = SubResource("ImageTexture_drtd2")
+
+[node name="VSeparator" type="VSeparator" parent="VBox/ListPanel/Vbox/HBoxContainer"]
+layout_mode = 2
+
+[node name="Play" type="Button" parent="VBox/ListPanel/Vbox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Preview"
+icon = SubResource("ImageTexture_drtd2")
+
+[node name="Default" type="Button" parent="VBox/ListPanel/Vbox/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Default"
+toggle_mode = true
+icon = SubResource("ImageTexture_drtd2")
+
+[node name="MoodList" type="ItemList" parent="VBox/ListPanel/Vbox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 100)
+layout_mode = 2
+
+[node name="Settings" type="PanelContainer" parent="VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_40fkd")
+
+[node name="Grid" type="GridContainer" parent="VBox/Settings"]
+layout_mode = 2
+columns = 2
+
+[node name="Label" type="Label" parent="VBox/Settings/Grid"]
+layout_mode = 2
+text = "Name:"
+
+[node name="Name" type="LineEdit" parent="VBox/Settings/Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Mood name"
+text = "New Mood"
+placeholder_text = "Enter Mood Name"
+caret_blink = true
+
+[node name="NameWarning" type="TextureRect" parent="VBox/Settings/Grid/Name"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 11
+anchor_left = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -31.0
+grow_horizontal = 0
+grow_vertical = 2
+texture = SubResource("ImageTexture_drtd2")
+stretch_mode = 3
+
+[node name="Label6" type="Label" parent="VBox/Settings/Grid"]
+layout_mode = 2
+text = "Mode:"
+
+[node name="Mode" type="OptionButton" parent="VBox/Settings/Grid"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Interrupt = The next sound will stop the previous
+Overlap = Multiple sounds may play at once
+Await = A sound will only be played if the previous has finished"
+item_count = 3
+selected = 0
+popup/item_0/text = "Interrupt"
+popup/item_0/id = 0
+popup/item_1/text = "Overlap"
+popup/item_1/id = 1
+popup/item_2/text = "Await"
+popup/item_2/id = 2
+
+[node name="Label4" type="Label" parent="VBox/Settings/Grid"]
+layout_mode = 2
+text = "File/Folder:"
+
+[node name="SoundFolder" parent="VBox/Settings/Grid" instance=ExtResource("2_e1vyd")]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+file_filter = "*.ogg, *.mp3, *.wav"
+file_mode = 3
+
+[node name="Label2" type="Label" parent="VBox/Settings/Grid"]
+layout_mode = 2
+text = "Pitch:"
+
+[node name="Pitch" type="HBoxContainer" parent="VBox/Settings/Grid"]
+layout_mode = 2
+theme_override_constants/separation = -6
+alignment = 2
+
+[node name="PitchBase" parent="VBox/Settings/Grid/Pitch" instance=ExtResource("3_yjcns")]
+unique_name_in_owner = true
+layout_mode = 2
+enforce_step = false
+max = 4.0
+
+[node name="Label4" type="Label" parent="VBox/Settings/Grid/Pitch"]
+layout_mode = 2
+text = "+/- "
+
+[node name="PitchVariance" parent="VBox/Settings/Grid/Pitch" instance=ExtResource("3_yjcns")]
+unique_name_in_owner = true
+layout_mode = 2
+enforce_step = false
+
+[node name="Label3" type="Label" parent="VBox/Settings/Grid"]
+layout_mode = 2
+text = "Volume:"
+
+[node name="Volume" type="HBoxContainer" parent="VBox/Settings/Grid"]
+layout_mode = 2
+theme_override_constants/separation = -6
+alignment = 2
+
+[node name="VolumeBase" parent="VBox/Settings/Grid/Volume" instance=ExtResource("3_yjcns")]
+unique_name_in_owner = true
+layout_mode = 2
+min = -60.0
+max = 30.0
+
+[node name="Label4" type="Label" parent="VBox/Settings/Grid/Volume"]
+layout_mode = 2
+text = "+/- "
+
+[node name="VolumeVariance" parent="VBox/Settings/Grid/Volume" instance=ExtResource("3_yjcns")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Label5" type="Label" parent="VBox/Settings/Grid"]
+layout_mode = 2
+text = "Skip:"
+
+[node name="Skip" parent="VBox/Settings/Grid" instance=ExtResource("3_yjcns")]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 2
+step = 1.0
+
+[node name="Preview" type="AudioStreamPlayer" parent="."]
+script = ExtResource("5_yscws")
+play_every_character = 0
+
+[connection signal="pressed" from="VBox/ListPanel/Vbox/HBoxContainer/Add" to="." method="_on_add_pressed"]
+[connection signal="pressed" from="VBox/ListPanel/Vbox/HBoxContainer/Duplicate" to="." method="_on_duplicate_pressed"]
+[connection signal="pressed" from="VBox/ListPanel/Vbox/HBoxContainer/Delete" to="." method="_on_delete_pressed"]
+[connection signal="pressed" from="VBox/ListPanel/Vbox/HBoxContainer/Play" to="." method="preview"]
+[connection signal="toggled" from="VBox/ListPanel/Vbox/HBoxContainer/Default" to="." method="_on_default_toggled"]
+[connection signal="item_selected" from="VBox/ListPanel/Vbox/MoodList" to="." method="_on_mood_list_item_selected"]
+[connection signal="focus_exited" from="VBox/Settings/Grid/Name" to="." method="_on_name_focus_exited"]
+[connection signal="text_changed" from="VBox/Settings/Grid/Name" to="." method="_on_name_text_changed"]
+[connection signal="text_submitted" from="VBox/Settings/Grid/Name" to="." method="_on_name_text_submitted"]
--- /dev/null
+@tool
+extends DialogicCharacterEditorPortraitSection
+
+
+func _get_title() -> String:
+ return "Typing Sound Mood"
+
+
+func _ready() -> void:
+ %PortraitMood.get_suggestions_func = mood_suggestions
+ %PortraitMood.resource_icon = get_theme_icon("AudioStreamPlayer", "EditorIcons")
+
+
+func _load_portrait_data(data:Dictionary):
+ %PortraitMood.set_value(data.get('sound_mood'))
+
+
+func update_visibility(show:=true):
+ if !show:
+ hide()
+ get_parent().get_child(get_index()-1).hide()
+ get_parent().get_child(get_index()+1).hide()
+ else:
+ get_parent().get_child(get_index()-1).show()
+
+
+func _on_portrait_mood_value_changed(property_name:String, value:String):
+ var data: Dictionary = selected_item.get_metadata(0)
+ data['sound_mood'] = value
+ changed.emit()
+
+
+func mood_suggestions(filter:String) -> Dictionary:
+ var suggestions := {}
+ for mood in character_editor.get_settings_section_by_name('Typing Sounds').current_moods_info:
+ suggestions[mood] = {'value':mood}
+ return suggestions
--- /dev/null
+[gd_scene load_steps=3 format=3 uid="uid://bvfiv5uhmkqq7"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/character_settings/character_portrait_mood_settings.gd" id="1_5ni5u"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="1_oggvu"]
+
+[node name="Typing Sound Mood" type="HBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_5ni5u")
+
+[node name="PortraitMoodLabel" type="Label" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Sound Mood:"
+
+[node name="PortraitMood" parent="." instance=ExtResource("1_oggvu")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Select Mood"
+
+[connection signal="value_changed" from="PortraitMood" to="." method="_on_portrait_mood_value_changed"]
--- /dev/null
+@tool
+class_name DialogicTextEvent
+extends DialogicEvent
+
+## Event that stores text. Can be said by a character.
+## Should be shown by a DialogicNode_DialogText.
+
+
+### Settings
+
+## This is the content of the text event.
+## It is supposed to be displayed by a DialogicNode_DialogText node.
+## That means you can use bbcode, but also some custom commands.
+var text := ""
+## If this is not null, the given character (as a resource) will be associated with this event.
+## The DialogicNode_NameLabel will show the characters display_name. If a typing sound is setup,
+## it will play.
+var character: DialogicCharacter = null
+## If a character is set, this setting can change the portrait of that character.
+var portrait := ""
+
+### Helpers
+
+## Used to set the character resource from the unique name identifier and vice versa
+var character_identifier: String:
+ get:
+ if character:
+ var identifier := DialogicResourceUtil.get_unique_identifier(character.resource_path)
+ if not identifier.is_empty():
+ return identifier
+ return character_identifier
+ set(value):
+ character_identifier = value
+ character = DialogicResourceUtil.get_character_resource(value)
+ if not character.portraits.has(portrait):
+ portrait = ""
+ ui_update_needed.emit()
+
+var regex := RegEx.create_from_string(r'\s*((")?(?<name>(?(2)[^"\n]*|[^(: \n]*))(?(2)"|)(\W*(?<portrait>\(.*\)))?\s*(?<!\\):)?(?<text>(.|\n)*)')
+var split_regex := RegEx.create_from_string(r"((\[n\]|\[n\+\])?((?!(\[n\]|\[n\+\]))(.|\n))+)")
+
+enum States {REVEALING, IDLE, DONE}
+var state := States.IDLE
+signal advance
+
+
+#region EXECUTION
+################################################################################
+
+func _clear_state() -> void:
+ dialogic.current_state_info.erase('text_sub_idx')
+ _disconnect_signals()
+
+func _execute() -> void:
+ if text.is_empty():
+ finish()
+ return
+
+ if (not character or character.custom_info.get('style', '').is_empty()) and dialogic.has_subsystem('Styles'):
+ # if previous characters had a custom style change back to base style
+ if dialogic.current_state_info.get('base_style') != dialogic.current_state_info.get('style'):
+ dialogic.Styles.change_style(dialogic.current_state_info.get('base_style', 'Default'))
+ await dialogic.get_tree().process_frame
+
+ var character_name_text := dialogic.Text.get_character_name_parsed(character)
+ if character:
+ dialogic.current_state_info['speaker'] = character.resource_path
+ if dialogic.has_subsystem('Styles') and character.custom_info.get('style', null):
+ dialogic.Styles.change_style(character.custom_info.style, false)
+ await dialogic.get_tree().process_frame
+
+
+ if portrait and dialogic.has_subsystem('Portraits') and dialogic.Portraits.is_character_joined(character):
+ dialogic.Portraits.change_character_portrait(character, portrait)
+ dialogic.Portraits.change_speaker(character, portrait)
+ var check_portrait: String = portrait if !portrait.is_empty() else dialogic.current_state_info['portraits'].get(character.resource_path, {}).get('portrait', '')
+
+ if check_portrait and character.portraits.get(check_portrait, {}).get('sound_mood', '') in character.custom_info.get('sound_moods', {}):
+ dialogic.Text.update_typing_sound_mood(character.custom_info.get('sound_moods', {}).get(character.portraits[check_portrait].get('sound_mood', {}), {}))
+ elif !character.custom_info.get('sound_mood_default', '').is_empty():
+ dialogic.Text.update_typing_sound_mood(character.custom_info.get('sound_moods', {}).get(character.custom_info.get('sound_mood_default'), {}))
+ else:
+ dialogic.Text.update_typing_sound_mood()
+
+ dialogic.Text.update_name_label(character)
+ else:
+ dialogic.Portraits.change_speaker(null)
+ dialogic.Text.update_name_label(null)
+ dialogic.Text.update_typing_sound_mood()
+
+ _connect_signals()
+
+ var final_text: String = get_property_translated('text')
+ if ProjectSettings.get_setting('dialogic/text/split_at_new_lines', false):
+ match ProjectSettings.get_setting('dialogic/text/split_at_new_lines_as', 0):
+ 0:
+ final_text = final_text.replace('\n', '[n]')
+ 1:
+ final_text = final_text.replace('\n', '[n+][br]')
+
+ var split_text := []
+ for i in split_regex.search_all(final_text):
+ split_text.append([i.get_string().trim_prefix('[n]').trim_prefix('[n+]')])
+ split_text[-1].append(i.get_string().begins_with('[n+]'))
+
+ dialogic.current_state_info['text_sub_idx'] = dialogic.current_state_info.get('text_sub_idx', -1)
+
+ var reveal_next_segment: bool = dialogic.current_state_info['text_sub_idx'] == -1
+
+ for section_idx in range(min(max(0, dialogic.current_state_info['text_sub_idx']), len(split_text)-1), len(split_text)):
+ dialogic.Inputs.block_input(ProjectSettings.get_setting('dialogic/text/text_reveal_skip_delay', 0.1))
+
+ if reveal_next_segment:
+ dialogic.Text.hide_next_indicators()
+
+ dialogic.current_state_info['text_sub_idx'] = section_idx
+
+ var segment: String = dialogic.Text.parse_text(split_text[section_idx][0])
+ var is_append: bool = split_text[section_idx][1]
+
+ final_text = segment
+ dialogic.Text.about_to_show_text.emit({'text':final_text, 'character':character, 'portrait':portrait, 'append': is_append})
+
+ await dialogic.Text.update_textbox(final_text, false)
+
+ state = States.REVEALING
+ _try_play_current_line_voice()
+ final_text = dialogic.Text.update_dialog_text(final_text, false, is_append)
+
+ _mark_as_read(character_name_text, final_text)
+
+ # We must skip text animation before we potentially return when there
+ # is a Choice event.
+ if dialogic.Inputs.auto_skip.enabled:
+ dialogic.Text.skip_text_reveal()
+ else:
+ await dialogic.Text.text_finished
+
+ state = States.IDLE
+ else:
+ reveal_next_segment = true
+
+ # Handling potential Choice Events.
+ if section_idx == len(split_text)-1 and dialogic.has_subsystem('Choices') and dialogic.Choices.is_question(dialogic.current_event_idx):
+ dialogic.Text.show_next_indicators(true)
+
+ finish()
+ return
+
+ elif dialogic.Inputs.auto_advance.is_enabled():
+ dialogic.Text.show_next_indicators(false, true)
+ dialogic.Inputs.auto_advance.start()
+ else:
+ dialogic.Text.show_next_indicators()
+
+ if section_idx == len(split_text)-1:
+ state = States.DONE
+
+ # If Auto-Skip is enabled and there are multiple parts of this text
+ # we need to skip the text after the defined time per event.
+ if dialogic.Inputs.auto_skip.enabled:
+ await dialogic.Inputs.start_autoskip_timer()
+
+ # Check if Auto-Skip is still enabled.
+ if not dialogic.Inputs.auto_skip.enabled:
+ await advance
+
+ else:
+ await advance
+
+
+ finish()
+
+
+func _mark_as_read(character_name_text: String, final_text: String) -> void:
+ if dialogic.has_subsystem('History'):
+ if character:
+ dialogic.History.store_simple_history_entry(final_text, event_name, {'character':character_name_text, 'character_color':character.color})
+ else:
+ dialogic.History.store_simple_history_entry(final_text, event_name)
+ dialogic.History.mark_event_as_visited()
+
+
+func _connect_signals() -> void:
+ if not dialogic.Inputs.dialogic_action.is_connected(_on_dialogic_input_action):
+ dialogic.Inputs.dialogic_action.connect(_on_dialogic_input_action)
+
+ dialogic.Inputs.auto_skip.toggled.connect(_on_auto_skip_enable)
+
+ if not dialogic.Inputs.auto_advance.autoadvance.is_connected(_on_dialogic_input_autoadvance):
+ dialogic.Inputs.auto_advance.autoadvance.connect(_on_dialogic_input_autoadvance)
+
+
+## If the event is done, this method can clean-up signal connections.
+func _disconnect_signals() -> void:
+ if dialogic.Inputs.dialogic_action.is_connected(_on_dialogic_input_action):
+ dialogic.Inputs.dialogic_action.disconnect(_on_dialogic_input_action)
+ if dialogic.Inputs.auto_advance.autoadvance.is_connected(_on_dialogic_input_autoadvance):
+ dialogic.Inputs.auto_advance.autoadvance.disconnect(_on_dialogic_input_autoadvance)
+ if dialogic.Inputs.auto_skip.toggled.is_connected(_on_auto_skip_enable):
+ dialogic.Inputs.auto_skip.toggled.disconnect(_on_auto_skip_enable)
+
+
+## Tries to play the voice clip for the current line.
+func _try_play_current_line_voice() -> void:
+ # If Auto-Skip is enabled and we skip voice clips, we don't want to play.
+ if (dialogic.Inputs.auto_skip.enabled
+ and dialogic.Inputs.auto_skip.skip_voice):
+ return
+
+ # Plays the audio region for the current line.
+ if (dialogic.has_subsystem('Voice')
+ and dialogic.Voice.is_voiced(dialogic.current_event_idx)):
+ dialogic.Voice.play_voice()
+
+
+func _on_dialogic_input_action() -> void:
+ match state:
+ States.REVEALING:
+ if dialogic.Text.is_text_reveal_skippable():
+ dialogic.Text.skip_text_reveal()
+ dialogic.Inputs.stop_timers()
+ _:
+ if dialogic.Inputs.manual_advance.is_enabled():
+ advance.emit()
+ dialogic.Inputs.stop_timers()
+
+
+func _on_dialogic_input_autoadvance() -> void:
+ if state == States.IDLE or state == States.DONE:
+ advance.emit()
+
+
+func _on_auto_skip_enable(enabled: bool) -> void:
+ if not enabled:
+ return
+
+ match state:
+ States.DONE:
+ await dialogic.Inputs.start_autoskip_timer()
+
+ # If Auto-Skip is still enabled, advance the text.
+ if dialogic.Inputs.auto_skip.enabled:
+ advance.emit()
+
+ States.REVEALING:
+ dialogic.Text.skip_text_reveal()
+
+#endregion
+
+
+#region INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Text"
+ set_default_color('Color1')
+ event_category = "Main"
+ event_sorting_index = 0
+ expand_by_default = true
+ help_page_path = "https://docs.dialogic.pro/writing-text-events.html"
+
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ var result := text.replace('\n', '\\\n')
+ result = result.replace(':', '\\:')
+ if result.is_empty():
+ result = "<Empty Text Event>"
+
+ if character:
+ var name := DialogicResourceUtil.get_unique_identifier(character.resource_path)
+ if name.count(" ") > 0:
+ name = '"' + name + '"'
+ if not portrait.is_empty():
+ result = name+" ("+portrait+"): "+result
+ else:
+ result = name+": "+result
+ for event in DialogicResourceUtil.get_event_cache():
+ if not event is DialogicTextEvent and event.is_valid_event(result):
+ result = '\\'+result
+ break
+
+ return result
+
+
+func from_text(string:String) -> void:
+ # Load default character
+ # This is only of relevance if the default has been overriden (usually not)
+ character = DialogicResourceUtil.get_character_resource(character_identifier)
+
+ var result := regex.search(string.trim_prefix('\\'))
+ if result and not result.get_string('name').is_empty():
+ var name := result.get_string('name').strip_edges()
+
+ if name == '_':
+ character = null
+ else:
+ character = DialogicResourceUtil.get_character_resource(name)
+
+ if character == null and Engine.is_editor_hint() == false:
+ character = DialogicCharacter.new()
+ character.display_name = name
+ character.resource_path = "user://"+name+".dch"
+ DialogicResourceUtil.add_resource_to_directory(character.resource_path, DialogicResourceUtil.get_character_directory())
+
+ if !result.get_string('portrait').is_empty():
+ portrait = result.get_string('portrait').strip_edges().trim_prefix('(').trim_suffix(')')
+
+ if result:
+ text = result.get_string('text').replace("\\\n", "\n").replace('\\:', ':').strip_edges().trim_prefix('\\')
+ if text == '<Empty Text Event>':
+ text = ""
+
+
+func is_valid_event(_string:String) -> bool:
+ return true
+
+
+func is_string_full_event(string:String) -> bool:
+ return !string.ends_with('\\')
+
+
+# this is only here to provide a list of default values
+# this way the module manager can add custom default overrides to this event.
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "character" : {"property": "character_identifier", "default": ""},
+ "portrait" : {"property": "portrait", "default": ""},
+ }
+#endregion
+
+
+#region TRANSLATIONS
+################################################################################
+
+func _get_translatable_properties() -> Array:
+ return ['text']
+
+
+func _get_property_original_translation(property:String) -> String:
+ match property:
+ 'text':
+ return text
+ return ''
+
+
+#endregion
+
+
+#region EVENT EDITOR
+################################################################################
+
+func _enter_visual_editor(editor:DialogicEditor):
+ editor.opened.connect(func(): ui_update_needed.emit())
+
+
+func build_event_editor() -> void:
+ add_header_edit('character_identifier', ValueType.DYNAMIC_OPTIONS,
+ {'file_extension' : '.dch',
+ 'mode' : 2,
+ 'suggestions_func' : get_character_suggestions,
+ 'empty_text' : '(No one)',
+ 'icon' : load("res://addons/dialogic/Editor/Images/Resources/character.svg")}, 'do_any_characters_exist()')
+ add_header_edit('portrait', ValueType.DYNAMIC_OPTIONS,
+ {'suggestions_func' : get_portrait_suggestions,
+ 'placeholder' : "(Don't change)",
+ 'icon' : load("res://addons/dialogic/Editor/Images/Resources/portrait.svg"),
+ 'collapse_when_empty': true,},
+ 'should_show_portrait_selector()')
+ add_body_edit('text', ValueType.MULTILINE_TEXT, {'autofocus':true})
+
+
+func should_show_portrait_selector() -> bool:
+ return character and not character.portraits.is_empty() and not character.portraits.size() == 1
+
+
+func do_any_characters_exist() -> bool:
+ return not DialogicResourceUtil.get_character_directory().is_empty()
+
+
+func get_character_suggestions(search_text:String) -> Dictionary:
+ return DialogicUtil.get_character_suggestions(search_text, character, true, false, editor_node)
+
+
+func get_portrait_suggestions(search_text:String) -> Dictionary:
+ return DialogicUtil.get_portrait_suggestions(search_text, character, true, "Don't change")
+
+#endregion
+
+
+#region CODE COMPLETION
+################################################################################
+
+var completion_text_character_getter_regex := RegEx.new()
+var completion_text_effects := {}
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ if completion_text_character_getter_regex.get_pattern().is_empty():
+ completion_text_character_getter_regex.compile("(\"[^\"]*\"|[^\\s:]*)")
+
+ if completion_text_effects.is_empty():
+ for idx in DialogicUtil.get_indexers():
+ for effect in idx._get_text_effects():
+ completion_text_effects[effect['command']] = effect
+
+ if not ':' in line.substr(0, TextNode.get_caret_column()) and symbol == '(':
+ var completion_character := completion_text_character_getter_regex.search(line).get_string().trim_prefix('"').trim_suffix('"')
+ CodeCompletionHelper.suggest_portraits(TextNode, completion_character)
+
+ if symbol == '[':
+ suggest_bbcode(TextNode)
+ for effect in completion_text_effects.values():
+ if effect.get('arg', false):
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, effect.command, effect.command+'=', TextNode.syntax_highlighter.normal_color, TextNode.get_theme_icon("RichTextEffect", "EditorIcons"))
+ else:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, effect.command, effect.command, TextNode.syntax_highlighter.normal_color, TextNode.get_theme_icon("RichTextEffect", "EditorIcons"), ']')
+
+ if symbol == '{':
+ CodeCompletionHelper.suggest_variables(TextNode)
+
+ if symbol == '=':
+ if CodeCompletionHelper.get_line_untill_caret(line).ends_with('[portrait='):
+ var completion_character := completion_text_character_getter_regex.search(line).get_string('name')
+ CodeCompletionHelper.suggest_portraits(TextNode, completion_character, ']')
+
+
+func _get_start_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ CodeCompletionHelper.suggest_characters(TextNode, CodeEdit.KIND_CLASS, true)
+
+
+func suggest_bbcode(TextNode:CodeEdit):
+ for i in [['b (bold)', 'b'], ['i (italics)', 'i'], ['color', 'color='], ['font size','font_size=']]:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i[0], i[1], TextNode.syntax_highlighter.normal_color, TextNode.get_theme_icon("RichTextEffect", "EditorIcons"),)
+ TextNode.add_code_completion_option(CodeEdit.KIND_CLASS, 'end '+i[0], '/'+i[1], TextNode.syntax_highlighter.normal_color, TextNode.get_theme_icon("RichTextEffect", "EditorIcons"), ']')
+ for i in [['new event', 'n'],['new event (same box)', 'n+']]:
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i[0], i[1], TextNode.syntax_highlighter.normal_color, TextNode.get_theme_icon("ArrowRight", "EditorIcons"),)
+
+#endregion
+
+
+#region SYNTAX HIGHLIGHTING
+################################################################################
+
+var text_effects := ""
+var text_effects_regex := RegEx.new()
+func load_text_effects() -> void:
+ if text_effects.is_empty():
+ for idx in DialogicUtil.get_indexers():
+ for effect in idx._get_text_effects():
+ text_effects+= effect['command']+'|'
+ text_effects += "b|i|u|s|code|p|center|left|right|fill|n\\+|n|indent|url|img|font|font_size|opentype_features|color|bg_color|fg_color|outline_size|outline_color|table|cell|ul|ol|lb|rb|br"
+ if text_effects_regex.get_pattern().is_empty():
+ text_effects_regex.compile("(?<!\\\\)\\[\\s*/?(?<command>"+text_effects+")\\s*(=\\s*(?<value>.+?)\\s*)?\\]")
+
+
+var text_random_word_regex := RegEx.new()
+var text_effect_color := Color('#898276')
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ load_text_effects()
+ if text_random_word_regex.get_pattern().is_empty():
+ text_random_word_regex.compile("(?<!\\\\)\\<[^\\[\\>]+(\\/[^\\>]*)\\>")
+
+ var result := regex.search(line)
+ if !result:
+ return dict
+ if Highlighter.mode == Highlighter.Modes.FULL_HIGHLIGHTING:
+ if result.get_string('name'):
+ dict[result.get_start('name')] = {"color":Highlighter.character_name_color}
+ dict[result.get_end('name')] = {"color":Highlighter.normal_color}
+ if result.get_string('portrait'):
+ dict[result.get_start('portrait')] = {"color":Highlighter.character_portrait_color}
+ dict[result.get_end('portrait')] = {"color":Highlighter.normal_color}
+ if result.get_string('text'):
+ var effects_result := text_effects_regex.search_all(line)
+ for eff in effects_result:
+ dict[eff.get_start()] = {"color":text_effect_color}
+ dict[eff.get_end()] = {"color":Highlighter.normal_color}
+ dict = Highlighter.color_region(dict, Highlighter.variable_color, line, '{', '}', result.get_start('text'))
+
+ for replace_mod_match in text_random_word_regex.search_all(result.get_string('text')):
+ var color: Color = Highlighter.string_color
+ color = color.lerp(Highlighter.normal_color, 0.4)
+ dict[replace_mod_match.get_start()+result.get_start('text')] = {'color':Highlighter.string_color}
+ var offset := 1
+ for b in replace_mod_match.get_string().trim_suffix('>').trim_prefix('<').split('/'):
+ color.h = wrap(color.h+0.2, 0, 1)
+ dict[replace_mod_match.get_start()+result.get_start('text')+offset] = {'color':color}
+ offset += len(b)
+ dict[replace_mod_match.get_start()+result.get_start('text')+offset] = {'color':Highlighter.string_color}
+ offset += 1
+ dict[replace_mod_match.get_end()+result.get_start('text')] = {'color':Highlighter.normal_color}
+ return dict
+
+#endregion
--- /dev/null
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M25.4118 7H6.58824C6.26336 7 6 7.24895 6 7.55604V20.3096C6 20.616 6.26228 20.8647 6.58645 20.8656L18.3547 20.8993C18.6789 20.9002 18.9412 21.1489 18.9412 21.4553V24.4428C18.9412 24.9541 19.6097 25.1944 19.9691 24.8123L23.4755 21.0835C23.5848 20.9672 23.7407 20.8995 23.9053 20.8969L25.4216 20.873C25.7426 20.868 26 20.6205 26 20.3171V7.55604C26 7.24895 25.7366 7 25.4118 7Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_text.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Text', 'script':this_folder.path_join('subsystem_text.gd')}]
+
+
+func _get_settings_pages() -> Array:
+ return [this_folder.path_join('settings_text.tscn')]
+
+
+func _get_character_editor_sections() -> Array:
+ return [this_folder.path_join('character_settings/character_moods_settings.tscn'),
+ this_folder.path_join('character_settings/character_portrait_mood_settings.tscn'),
+ ]
+
+
+func _get_text_effects() -> Array[Dictionary]:
+ return [
+ {'command':'speed', 'subsystem':'Text', 'method':'effect_speed', 'arg':true},
+ {'command':'lspeed', 'subsystem':'Text', 'method':'effect_lspeed', 'arg':true},
+ {'command':'pause', 'subsystem':'Text', 'method':'effect_pause', 'arg':true},
+ {'command':'signal', 'subsystem':'Text', 'method':'effect_signal', 'arg':true},
+ {'command':'mood', 'subsystem':'Text', 'method':'effect_mood', 'arg':true},
+ ]
+
+
+func _get_text_modifiers() -> Array[Dictionary]:
+ return [
+ {'subsystem':'Text', 'method':'modifier_autopauses'},
+ {'subsystem':'Text', 'method':'modifier_random_selection', 'mode':-1},
+ {'subsystem':'Text', 'method':"modifier_break", 'command':'br', 'mode':-1},
+ ]
--- /dev/null
+extends RefCounted
+class_name DialogicManualAdvance
+## This class holds the settings for the Manual-Advance feature.
+## Changing the variables will alter the behaviour of manually advancing
+## the timeline, e.g. using the input action.
+
+## The key giving access to the state info of Manual-Advance.
+const STATE_INFO_KEY := "manual_advance"
+## The key for the enabled state in the current state info.
+const ENABLED_STATE_KEY := "enabled"
+## The key for the temporary event state in the current state info.
+const DISABLED_UNTIL_NEXT_EVENT_STATE_KEY := "temp_disabled"
+
+
+## If `true`, Manual-Advance will be deactivated until the next event.
+##
+## Use this flag to create a temporary Manual-Advance block.
+##
+## Overrides [variable system_enabled] when true.
+var disabled_until_next_event := false :
+ set(enabled):
+ disabled_until_next_event = enabled
+ DialogicUtil.autoload().current_state_info[STATE_INFO_KEY][DISABLED_UNTIL_NEXT_EVENT_STATE_KEY] = enabled
+
+
+## If `true`, Manual-Advance will stay enabled until this is set to `false`.
+##
+## Use this flag to activate or disable Manual-Advance mode.
+##
+## Can be temporarily overwritten by [variable disabled_until_next_event].
+var system_enabled := true :
+ set(enabled):
+ system_enabled = enabled
+ DialogicUtil.autoload().current_state_info[STATE_INFO_KEY][ENABLED_STATE_KEY] = enabled
+
+
+## Checks if the current state info has the Manual-Advance settings.
+## If not, populates the current state info with the default settings.
+func _init() -> void:
+ if DialogicUtil.autoload().current_state_info.has(STATE_INFO_KEY):
+ var state_info := DialogicUtil.autoload().current_state_info
+ var manual_advance: Dictionary = state_info[STATE_INFO_KEY]
+
+ disabled_until_next_event = manual_advance.get(DISABLED_UNTIL_NEXT_EVENT_STATE_KEY, disabled_until_next_event)
+ system_enabled = manual_advance.get(ENABLED_STATE_KEY, system_enabled)
+
+ else:
+ DialogicUtil.autoload().current_state_info[STATE_INFO_KEY] = {
+ ENABLED_STATE_KEY: system_enabled,
+ DISABLED_UNTIL_NEXT_EVENT_STATE_KEY: disabled_until_next_event,
+ }
+
+
+#region MANUAL ADVANCE HELPERS
+
+## Whether the player can use Manual-Advance to advance the timeline.
+func is_enabled() -> bool:
+ return system_enabled and not disabled_until_next_event
+
+#endregion
--- /dev/null
+@icon("node_dialog_text_icon.svg")
+class_name DialogicNode_DialogText
+extends RichTextLabel
+
+## Dialogic node that can reveal text at a given (changeable speed).
+
+signal started_revealing_text()
+signal continued_revealing_text(new_character : String)
+signal finished_revealing_text()
+enum Alignment {LEFT, CENTER, RIGHT}
+
+@export var enabled := true
+@export var alignment := Alignment.LEFT
+@export var textbox_root: Node = self
+
+@export var hide_when_empty := false
+@export var start_hidden := true
+
+var revealing := false
+var base_visible_characters := 0
+
+# The used speed per revealed character.
+# May be overwritten when syncing reveal speed to voice.
+var active_speed: float = 0.01
+
+var speed_counter: float = 0
+
+func _set(property: StringName, what: Variant) -> bool:
+ if property == 'text' and typeof(what) == TYPE_STRING:
+
+ text = what
+
+ if hide_when_empty:
+ textbox_root.visible = !what.is_empty()
+
+ return true
+ return false
+
+
+func _ready() -> void:
+ # add to necessary
+ add_to_group('dialogic_dialog_text')
+ meta_hover_ended.connect(_on_meta_hover_ended)
+ meta_hover_started.connect(_on_meta_hover_started)
+ meta_clicked.connect(_on_meta_clicked)
+ gui_input.connect(on_gui_input)
+ bbcode_enabled = true
+ if textbox_root == null:
+ textbox_root = self
+
+ if start_hidden:
+ textbox_root.hide()
+ text = ""
+
+
+# this is called by the DialogicGameHandler to set text
+
+func reveal_text(_text: String, keep_previous:=false) -> void:
+ if !enabled:
+ return
+ show()
+
+ if !keep_previous:
+ text = _text
+ base_visible_characters = 0
+
+ if alignment == Alignment.CENTER:
+ text = '[center]'+text
+ elif alignment == Alignment.RIGHT:
+ text = '[right]'+text
+ visible_characters = 0
+
+ else:
+ base_visible_characters = len(text)
+ visible_characters = len(get_parsed_text())
+ text = text + _text
+
+ # If Auto-Skip is enabled and we append the text (keep_previous),
+ # we can skip revealing the text and just show it all at once.
+ if DialogicUtil.autoload().Inputs.auto_skip.enabled:
+ visible_characters = 1
+ return
+
+ revealing = true
+ speed_counter = 0
+ started_revealing_text.emit()
+
+
+func set_speed(delay_per_character:float) -> void:
+ if DialogicUtil.autoload().Text.is_text_voice_synced() and DialogicUtil.autoload().Voice.is_running():
+ var total_characters := get_total_character_count() as float
+ var remaining_time: float = DialogicUtil.autoload().Voice.get_remaining_time()
+ var synced_speed := remaining_time / total_characters
+ active_speed = synced_speed
+
+ else:
+ active_speed = delay_per_character
+
+
+## Reveals one additional character.
+func continue_reveal() -> void:
+ if visible_characters <= get_total_character_count():
+ revealing = false
+
+ var current_index := visible_characters - base_visible_characters
+ await DialogicUtil.autoload().Text.execute_effects(current_index, self, false)
+
+ if visible_characters == -1:
+ return
+
+ revealing = true
+ visible_characters += 1
+
+ if visible_characters > -1 and visible_characters <= len(get_parsed_text()):
+ continued_revealing_text.emit(get_parsed_text()[visible_characters-1])
+ else:
+ finish_text()
+ # if the text finished organically, add a small input block
+ # this prevents accidental skipping when you expected the text to be longer
+ DialogicUtil.autoload().Inputs.block_input(ProjectSettings.get_setting('dialogic/text/advance_delay', 0.1))
+
+
+## Reveals the entire text instantly.
+func finish_text() -> void:
+ visible_ratio = 1
+ DialogicUtil.autoload().Text.execute_effects(-1, self, true)
+ revealing = false
+ DialogicUtil.autoload().current_state = DialogicGameHandler.States.IDLE
+
+ finished_revealing_text.emit()
+
+
+## Checks if the next character in the text can be revealed.
+func _process(delta: float) -> void:
+ if !revealing or DialogicUtil.autoload().paused:
+ return
+
+ speed_counter += delta
+
+ while speed_counter > active_speed and revealing and !DialogicUtil.autoload().paused:
+ speed_counter -= active_speed
+ continue_reveal()
+
+
+
+func _on_meta_hover_started(_meta:Variant) -> void:
+ DialogicUtil.autoload().Inputs.action_was_consumed = true
+
+func _on_meta_hover_ended(_meta:Variant) -> void:
+ DialogicUtil.autoload().Inputs.action_was_consumed = false
+
+func _on_meta_clicked(_meta:Variant) -> void:
+ DialogicUtil.autoload().Inputs.action_was_consumed = true
+
+
+## Handle mouse input
+func on_gui_input(event:InputEvent) -> void:
+ DialogicUtil.autoload().Inputs.handle_node_gui_input(event)
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 3H8" stroke="#7DFFF7" stroke-width="2"/>
+<path d="M9 3H14" stroke="#7DFFF7" stroke-width="2"/>
+<path d="M2 6H14" stroke="#7DFFF7" stroke-width="2"/>
+<path d="M7 9L14 9" stroke="#7DFFF7" stroke-width="2"/>
+<path d="M2 9L6 9" stroke="#7DFFF7" stroke-width="2"/>
+<path d="M2 12H9" stroke="#7DFFF7" stroke-width="2"/>
+<path d="M10 12H14" stroke="#7DFFF7" stroke-width="2"/>
+</svg>
--- /dev/null
+class_name DialogicNode_Input
+extends Control
+
+## A node that handles mouse input. This allows limiting mouse input to a
+## specific region and avoiding conflicts with other UI elements.
+## If no Input node is used, the input subsystem will handle mouse input instead.
+
+func _ready() -> void:
+ add_to_group('dialogic_input')
+ gui_input.connect(_on_gui_input)
+
+
+func _input(_event: InputEvent) -> void:
+ if Input.is_action_pressed(ProjectSettings.get_setting('dialogic/text/input_action', 'dialogic_default_action')):
+ mouse_filter = Control.MOUSE_FILTER_STOP
+ else:
+ mouse_filter = Control.MOUSE_FILTER_IGNORE
+
+
+func _on_gui_input(event:InputEvent) -> void:
+ DialogicUtil.autoload().Inputs.handle_node_gui_input(event)
--- /dev/null
+@icon("node_name_label_icon.svg")
+extends Label
+
+class_name DialogicNode_NameLabel
+
+# If true, the label will be hidden if no character speaks.
+@export var hide_when_empty := true
+@export var name_label_root: Node = self
+@export var use_character_color := true
+
+func _ready() -> void:
+ add_to_group('dialogic_name_label')
+ if hide_when_empty:
+ name_label_root.visible = false
+ text = ""
+
+
+func _set(property, what):
+ if property == 'text' and typeof(what) == TYPE_STRING:
+ text = what
+ if hide_when_empty:
+ name_label_root.visible = !what.is_empty()
+ else:
+ name_label_root.show()
+ return true
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.00003 3C5.73484 3.00006 5.48052 3.10545 5.29303 3.293L1.29303 7.293C1.10556 7.48053 1.00024 7.73484 1.00024 8C1.00024 8.26516 1.10556 8.51947 1.29303 8.707L5.29303 12.707C5.48052 12.8945 5.73484 12.9999 6.00003 13H14C14.2652 13 14.5196 12.8946 14.7071 12.7071C14.8947 12.5196 15 12.2652 15 12V4C15 3.73478 14.8947 3.48043 14.7071 3.29289C14.5196 3.10536 14.2652 3 14 3H6.00003ZM5.00003 7C5.26525 7 5.5196 7.10536 5.70714 7.29289C5.89467 7.48043 6.00003 7.73478 6.00003 8C6.00003 8.26522 5.89467 8.51957 5.70714 8.70711C5.5196 8.89464 5.26525 9 5.00003 9C4.73481 9 4.48046 8.89464 4.29292 8.70711C4.10539 8.51957 4.00003 8.26522 4.00003 8C4.00003 7.73478 4.10539 7.48043 4.29292 7.29289C4.48046 7.10536 4.73481 7 5.00003 7Z" fill="#7DFFF7"/>
+</svg>
--- /dev/null
+@icon("node_next_indicator_icon.svg")
+class_name DialogicNode_NextIndicator
+extends Control
+
+## Node that is shown when the text is fully revealed.
+## The default implementation allows to set an icon and animation.
+
+
+@export var enabled := true
+
+## If true the next indicator will also be shown if the text is a question.
+@export var show_on_questions := false
+## If true the next indicator will be shown even if dialogic will autocontinue.
+@export var show_on_autoadvance := false
+
+enum Animations {BOUNCE, BLINK, NONE}
+
+## What animation should the indicator do.
+@export var animation := Animations.BOUNCE
+
+var texture_rect: TextureRect
+
+## Set the image to use as the indicator.
+@export var texture: Texture2D = preload("res://addons/dialogic/Example Assets/next-indicator/next-indicator.png") as Texture2D:
+ set(_texture):
+ texture = _texture
+ if texture_rect:
+ texture_rect.texture = texture
+
+@export var texture_size := Vector2(32,32):
+ set(_texture_size):
+ texture_size = _texture_size
+ if has_node('Texture'):
+ get_node('Texture').size = _texture_size
+ get_node('Texture').position = -_texture_size
+
+
+var tween: Tween
+
+func _ready() -> void:
+ add_to_group('dialogic_next_indicator')
+
+ # Creating TextureRect if missing
+ if not texture_rect:
+ var icon := TextureRect.new()
+ icon.name = 'Texture'
+ icon.ignore_texture_size = true
+ icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon.size = texture_size
+ icon.position = -icon.size
+ add_child(icon)
+ texture_rect = icon
+
+ texture_rect.texture = texture
+
+ hide()
+ visibility_changed.connect(_on_visibility_changed)
+
+
+func _on_visibility_changed() -> void:
+ if visible:
+ play_animation(animation, 1.0)
+
+
+func play_animation(current_animation: int, time:float) -> void:
+ # clean up previous tween to prevent slipping
+ if tween:
+ tween.stop()
+
+ match current_animation:
+ Animations.BOUNCE:
+ tween = (create_tween() as Tween)
+ var distance := 4
+ tween.set_parallel(false)
+ tween.set_trans(Tween.TRANS_SINE)
+ tween.set_ease(Tween.EASE_IN_OUT)
+ tween.set_loops()
+
+ tween.tween_property(self, 'position', Vector2(0,distance), time*0.3).as_relative()
+ tween.tween_property(self, 'position', - Vector2(0,distance), time*0.3).as_relative()
+ Animations.BLINK:
+ tween = (create_tween() as Tween)
+ tween.set_parallel(false)
+ tween.set_trans(Tween.TRANS_SINE)
+ tween.set_ease(Tween.EASE_IN_OUT)
+ tween.set_loops()
+
+ tween.tween_property(self, 'modulate:a', 0, time*0.3)
+ tween.tween_property(self, 'modulate:a', 1, time*0.3)
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.0971 4L5.00122 4C4.26622 4 3.78236 4.76633 4.09836 5.42993L7.00122 11.604C7.36317 12.3641 8.63928 12.3641 9.00122 11.604L12 5.42993C12.316 4.76633 11.8321 4 11.0971 4Z" stroke="#7DFFF7" stroke-width="2"/>
+</svg>
--- /dev/null
+@tool
+class_name DialogicNode_TypeSounds
+extends AudioStreamPlayer
+
+## Node that allows playing sounds when text characters are revealed.
+## Should be the child of a DialogicNode_DialogText node!
+
+## Usefull if you want to change the sounds of different node's sounds
+@export var enabled := true
+enum Modes {INTERRUPT, OVERLAP, AWAIT}
+## If true, interrupts the current sound to play a new one
+@export var mode := Modes.INTERRUPT
+## Array of sounds. Will pick a random one each time.
+@export var sounds: Array[AudioStream] = []
+## A sound to be played as the last sound.
+@export var end_sound:AudioStream
+## Determines how many characters are between each sound. Default is 1 for playing it every character.
+@export var play_every_character := 1
+## Allows changing the pitch by a random value from (pitch - pitch_variance) to (pitch + pitch_variance)
+@export_range(0, 3, 0.01) var pitch_variance := 0.0
+## Allows changing the volume by a random value from (volume - volume_variance) to (volume + volume_variance)
+@export_range(0, 10, 0.01) var volume_variance := 0.0
+## Characters that don't increase the 'characters_since_last_sound' variable, useful for the space or fullstop
+@export var ignore_characters: String = ' .,'
+
+var characters_since_last_sound: int = 0
+var base_pitch: float = pitch_scale
+var base_volume: float = volume_db
+var RNG := RandomNumberGenerator.new()
+
+var current_overwrite_data := {}
+
+func _ready() -> void:
+ # add to necessary group
+ add_to_group('dialogic_type_sounds')
+
+ if bus == "Master":
+ bus = ProjectSettings.get_setting("dialogic/audio/type_sound_bus", "Master")
+
+ if !Engine.is_editor_hint() and get_parent() is DialogicNode_DialogText:
+ get_parent().started_revealing_text.connect(_on_started_revealing_text)
+ get_parent().continued_revealing_text.connect(_on_continued_revealing_text)
+ get_parent().finished_revealing_text.connect(_on_finished_revealing_text)
+
+
+func _on_started_revealing_text() -> void:
+ if !enabled or (get_parent() is DialogicNode_DialogText and !get_parent().enabled):
+ return
+ characters_since_last_sound = current_overwrite_data.get('skip_characters', play_every_character-1)+1
+
+
+func _on_continued_revealing_text(new_character:String) -> void:
+ if !enabled or (get_parent() is DialogicNode_DialogText and !get_parent().enabled):
+ return
+
+ # We don't want to play type sounds if Auto-Skip is enabled.
+ if !Engine.is_editor_hint() and DialogicUtil.autoload().Inputs.auto_skip.enabled:
+ return
+
+ # don't play if a voice-track is running
+ if !Engine.is_editor_hint() and get_parent() is DialogicNode_DialogText:
+ if DialogicUtil.autoload().has_subsystem("Voice") and DialogicUtil.autoload().Voice.is_running():
+ return
+
+ # if sound playing and can't interrupt
+ if playing and current_overwrite_data.get('mode', mode) == Modes.AWAIT:
+ return
+
+ # if no sounds were given
+ if current_overwrite_data.get('sounds', sounds).size() == 0:
+ return
+
+ # if the new character is not allowed
+ if new_character in ignore_characters:
+ return
+
+ characters_since_last_sound += 1
+ if characters_since_last_sound < current_overwrite_data.get('skip_characters', play_every_character-1)+1:
+ return
+
+ characters_since_last_sound = 0
+
+ var audio_player: AudioStreamPlayer = self
+ if current_overwrite_data.get('mode', mode) == Modes.OVERLAP:
+ audio_player = AudioStreamPlayer.new()
+ audio_player.bus = bus
+ add_child(audio_player)
+ elif current_overwrite_data.get('mode', mode) == Modes.INTERRUPT:
+ stop()
+
+ #choose the random sound
+ audio_player.stream = current_overwrite_data.get('sounds', sounds)[RNG.randi_range(0, current_overwrite_data.get('sounds', sounds).size() - 1)]
+
+ #choose a random pitch and volume
+ audio_player.pitch_scale = max(0, current_overwrite_data.get('pitch_base', base_pitch) + current_overwrite_data.get('pitch_variance', pitch_variance) * RNG.randf_range(-1.0, 1.0))
+ audio_player.volume_db = current_overwrite_data.get('volume_base', base_volume) + current_overwrite_data.get('volume_variance',volume_variance) * RNG.randf_range(-1.0, 1.0)
+
+ #play the sound
+ audio_player.play(0)
+
+ if current_overwrite_data.get('mode', mode) == Modes.OVERLAP:
+ audio_player.finished.connect(audio_player.queue_free)
+
+
+func _on_finished_revealing_text() -> void:
+ # We don't want to play type sounds if Auto-Skip is enabled.
+ if !Engine.is_editor_hint() and DialogicUtil.autoload().Inputs.auto_skip.enabled:
+ return
+
+ if end_sound != null:
+ stream = end_sound
+ play()
+
+
+func load_overwrite(dictionary:Dictionary) -> void:
+ current_overwrite_data = dictionary
+ if dictionary.has('sound_path'):
+ current_overwrite_data['sounds'] = DialogicNode_TypeSounds.load_sounds_from_path(dictionary.sound_path)
+
+
+static func load_sounds_from_path(path:String) -> Array[AudioStream]:
+ if path.get_extension().to_lower() in ['mp3', 'wav', 'ogg'] and load(path) is AudioStream:
+ return [load(path)]
+ var _sounds: Array[AudioStream] = []
+ for file in DialogicUtil.listdir(path, true, false, true, true):
+ if !file.ends_with('.import'):
+ continue
+ if file.trim_suffix('.import').get_extension().to_lower() in ['mp3', 'wav', 'ogg'] and ResourceLoader.load(file.trim_suffix('.import')) is AudioStream:
+ _sounds.append(ResourceLoader.load(file.trim_suffix('.import')))
+ return _sounds
+
+
+############# USER INTERFACE ###################################################
+
+func _get_configuration_warnings() -> PackedStringArray:
+ if not get_parent() is DialogicNode_DialogText:
+ return ["This should be the child of a DialogText node!"]
+ return []
--- /dev/null
+@tool
+extends DialogicSettingsPage
+
+var autopause_sets := {}
+
+const _SETTING_LETTER_SPEED := 'dialogic/text/letter_speed'
+
+const _SETTING_INPUT_ACTION := 'dialogic/text/input_action'
+
+const _SETTING_TEXT_REVEAL_SKIPPABLE := 'dialogic/text/initial_text_reveal_skippable'
+const _SETTING_TEXT_REVEAL_SKIPPABLE_DELAY := 'dialogic/text/text_reveal_skip_delay'
+const _SETTING_TEXT_ADVANCE_DELAY := 'dialogic/text/advance_delay'
+
+const _SETTING_AUTOCOLOR_NAMES := 'dialogic/text/autocolor_names'
+const _SETTING_SPLIT_AT_NEW_LINES := 'dialogic/text/split_at_new_lines'
+const _SETTING_SPLIT_AT_NEW_LINES_AS := 'dialogic/text/split_at_new_lines_as'
+
+const _SETTING_AUTOSKIP_TIME_PER_EVENT := 'dialogic/text/autoskip_time_per_event'
+
+const _SETTING_AUTOADVANCE_ENABLED := 'dialogic/text/autoadvance_enabled'
+const _SETTING_AUTOADVANCE_FIXED_DELAY := 'dialogic/text/autoadvance_fixed_delay'
+const _SETTING_AUTOADVANCE_WORD_DELAY := 'dialogic/text/autoadvance_per_word_delay'
+const _SETTING_AUTOADVANCE_CHARACTER_DELAY := 'dialogic/text/autoadvance_per_character_delay'
+const _SETTING_AUTOADVANCE_IGNORED_CHARACTERS_ENABLED := 'dialogic/text/autoadvance_ignored_characters_enabled'
+const _SETTING_AUTOADVANCE_IGNORED_CHARACTERS := 'dialogic/text/autoadvance_ignored_characters'
+
+const _SETTING_ABSOLUTE_AUTOPAUSES := 'dialogic/text/absolute_autopauses'
+const _SETTING_AUTOPAUSES := 'dialogic/text/autopauses'
+
+
+func _get_priority() -> int:
+ return 98
+
+
+func _get_title() -> String:
+ return "Text"
+
+
+func _ready() -> void:
+ %DefaultSpeed.value_changed.connect(_on_float_set.bind(_SETTING_LETTER_SPEED))
+
+ %Skippable.toggled.connect(_on_bool_set.bind(_SETTING_TEXT_REVEAL_SKIPPABLE))
+ %SkippableDelay.value_changed.connect(_on_float_set.bind(_SETTING_TEXT_REVEAL_SKIPPABLE_DELAY))
+ %AdvanceDelay.value_changed.connect(_on_float_set.bind(_SETTING_TEXT_ADVANCE_DELAY))
+
+ %AutocolorNames.toggled.connect(_on_bool_set.bind(_SETTING_AUTOCOLOR_NAMES))
+
+ %NewEvents.toggled.connect(_on_bool_set.bind(_SETTING_SPLIT_AT_NEW_LINES))
+
+ %AutoAdvance.toggled.connect(_on_bool_set.bind(_SETTING_AUTOADVANCE_ENABLED))
+ %FixedDelay.value_changed.connect(_on_float_set.bind(_SETTING_AUTOADVANCE_FIXED_DELAY))
+ %IgnoredCharactersEnabled.toggled.connect(_on_bool_set.bind(_SETTING_AUTOADVANCE_IGNORED_CHARACTERS_ENABLED))
+
+ %AutoskipTimePerEvent.value_changed.connect(_on_float_set.bind(_SETTING_AUTOSKIP_TIME_PER_EVENT))
+
+ %AutoPausesAbsolute.toggled.connect(_on_bool_set.bind(_SETTING_ABSOLUTE_AUTOPAUSES))
+
+
+func _refresh() -> void:
+ ## BEHAVIOUR
+ %DefaultSpeed.value = ProjectSettings.get_setting(_SETTING_LETTER_SPEED, 0.01)
+
+ %InputAction.resource_icon = get_theme_icon(&"Mouse", &"EditorIcons")
+ %InputAction.set_value(ProjectSettings.get_setting(_SETTING_INPUT_ACTION, &'dialogic_default_action'))
+ %InputAction.get_suggestions_func = suggest_actions
+
+ %Skippable.button_pressed = ProjectSettings.get_setting(_SETTING_TEXT_REVEAL_SKIPPABLE, true)
+ %SkippableDelay.value = ProjectSettings.get_setting(_SETTING_TEXT_REVEAL_SKIPPABLE_DELAY, 0.1)
+ %AdvanceDelay.value = ProjectSettings.get_setting(_SETTING_TEXT_ADVANCE_DELAY, 0.1)
+
+ %AutocolorNames.button_pressed = ProjectSettings.get_setting(_SETTING_AUTOCOLOR_NAMES, false)
+
+ %NewEvents.button_pressed = ProjectSettings.get_setting(_SETTING_SPLIT_AT_NEW_LINES, false)
+ %NewEventOption.select(ProjectSettings.get_setting(_SETTING_SPLIT_AT_NEW_LINES_AS, 0))
+
+ ## AUTO-ADVANCE
+ %AutoAdvance.button_pressed = ProjectSettings.get_setting(_SETTING_AUTOADVANCE_ENABLED, false)
+ %FixedDelay.value = ProjectSettings.get_setting(_SETTING_AUTOADVANCE_FIXED_DELAY, 1)
+
+ var per_character_delay: float = ProjectSettings.get_setting(_SETTING_AUTOADVANCE_CHARACTER_DELAY, 0.1)
+ var per_word_delay: float = ProjectSettings.get_setting(_SETTING_AUTOADVANCE_WORD_DELAY, 0)
+ if per_character_delay == 0 and per_word_delay == 0:
+ _on_additional_delay_mode_item_selected(0)
+ elif per_word_delay == 0:
+ _on_additional_delay_mode_item_selected(2, per_character_delay)
+ else:
+ _on_additional_delay_mode_item_selected(1, per_word_delay)
+
+ %IgnoredCharactersEnabled.button_pressed = ProjectSettings.get_setting(_SETTING_AUTOADVANCE_IGNORED_CHARACTERS_ENABLED, true)
+ var ignored_characters: String = ''
+ var ignored_characters_dict: Dictionary = ProjectSettings.get_setting(_SETTING_AUTOADVANCE_IGNORED_CHARACTERS, {})
+ for ignored_character in ignored_characters_dict.keys():
+ ignored_characters += ignored_character
+ %IgnoredCharacters.text = ignored_characters
+
+ ## AUTO-SKIP
+ %AutoskipTimePerEvent.value = ProjectSettings.get_setting(_SETTING_AUTOSKIP_TIME_PER_EVENT, 0.1)
+
+ ## AUTO-PAUSES
+ %AutoPausesAbsolute.button_pressed = ProjectSettings.get_setting(_SETTING_ABSOLUTE_AUTOPAUSES, false)
+ load_autopauses(ProjectSettings.get_setting(_SETTING_AUTOPAUSES, {}))
+
+
+func _about_to_close() -> void:
+ save_autopauses()
+
+
+func _on_bool_set(button_pressed:bool, setting:String) -> void:
+ ProjectSettings.set_setting(setting, button_pressed)
+ ProjectSettings.save()
+
+
+func _on_float_set(value:float, setting:String) -> void:
+ ProjectSettings.set_setting(setting, value)
+ ProjectSettings.save()
+
+
+#region BEHAVIOUR
+################################################################################
+
+func _on_InputAction_value_changed(property_name:String, value:String) -> void:
+ ProjectSettings.set_setting(_SETTING_INPUT_ACTION, value)
+ ProjectSettings.save()
+
+func suggest_actions(search:String) -> Dictionary:
+ var suggs := {}
+ for prop in ProjectSettings.get_property_list():
+ if prop.name.begins_with('input/') and not prop.name.begins_with('input/ui_') :
+ suggs[prop.name.trim_prefix('input/')] = {'value':prop.name.trim_prefix('input/')}
+ return suggs
+
+
+func _on_new_event_option_item_selected(index:int) -> void:
+ ProjectSettings.set_setting(_SETTING_SPLIT_AT_NEW_LINES_AS, index)
+ ProjectSettings.save()
+
+#endregion
+
+#region AUTO-ADVANCE
+################################################################################
+
+func _on_additional_delay_mode_item_selected(index:int, delay:float=-1) -> void:
+ %AdditionalDelayMode.selected = index
+ match index:
+ 0: # NONE
+ %AdditionalDelay.hide()
+ %AutoadvanceIgnoreCharacters.hide()
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_WORD_DELAY, 0)
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_CHARACTER_DELAY, 0)
+ 1: # PER WORD
+ %AdditionalDelay.show()
+ %AutoadvanceIgnoreCharacters.hide()
+ if delay != -1:
+ %AdditionalDelay.value = delay
+ else:
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_WORD_DELAY, %AdditionalDelay.value)
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_CHARACTER_DELAY, 0)
+ 2: # PER CHARACTER
+ %AdditionalDelay.show()
+ %AutoadvanceIgnoreCharacters.show()
+ if delay != -1:
+ %AdditionalDelay.value = delay
+ else:
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_CHARACTER_DELAY, %AdditionalDelay.value)
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_WORD_DELAY, 0)
+ ProjectSettings.save()
+
+
+func _on_additional_delay_value_changed(value:float) -> void:
+ match %AdditionalDelayMode.selected:
+ 0: # NONE
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_CHARACTER_DELAY, 0)
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_WORD_DELAY, 0)
+ 1: # PER WORD
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_WORD_DELAY, value)
+ 2: # PER CHARACTER
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_CHARACTER_DELAY, value)
+ ProjectSettings.save()
+
+
+func _on_IgnoredCharacters_text_changed(text_input):
+ ProjectSettings.set_setting(_SETTING_AUTOADVANCE_IGNORED_CHARACTERS, DialogicUtil.str_to_hash_set(text_input))
+ ProjectSettings.save()
+
+#endregion
+
+
+## AUTO-PAUSES
+################################################################################
+
+func load_autopauses(dictionary:Dictionary) -> void:
+ for i in %AutoPauseSets.get_children():
+ i.queue_free()
+
+
+ for i in dictionary.keys():
+ add_autopause_set(i, dictionary[i])
+
+
+func save_autopauses() -> void:
+ var dictionary := {}
+ for i in autopause_sets:
+ if is_instance_valid(autopause_sets[i].time):
+ dictionary[autopause_sets[i].text.text] = autopause_sets[i].time.value
+ ProjectSettings.set_setting(_SETTING_AUTOPAUSES, dictionary)
+ ProjectSettings.save()
+
+
+func _on_add_autopauses_set_pressed() -> void:
+ add_autopause_set('', 0.1)
+
+
+func add_autopause_set(text: String, time: float) -> void:
+ var info := {}
+ var line_edit := LineEdit.new()
+ line_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ line_edit.placeholder_text = 'e.g. "?!.,;:"'
+ line_edit.text = text
+ info['text'] = line_edit
+ %AutoPauseSets.add_child(line_edit)
+ var spin_box := SpinBox.new()
+ spin_box.min_value = 0.1
+ spin_box.step = 0.01
+ spin_box.value = time
+ info['time'] = spin_box
+ %AutoPauseSets.add_child(spin_box)
+
+ var remove_btn := Button.new()
+ remove_btn.icon = get_theme_icon(&'Remove', &'EditorIcons')
+ remove_btn.pressed.connect(_on_remove_autopauses_set_pressed.bind(len(autopause_sets)))
+ info['delete'] = remove_btn
+ %AutoPauseSets.add_child(remove_btn)
+ autopause_sets[len(autopause_sets)] = info
+
+
+func _on_remove_autopauses_set_pressed(index: int) -> void:
+ for key in autopause_sets[index]:
+ autopause_sets[index][key].queue_free()
+ autopause_sets.erase(index)
+
--- /dev/null
+[gd_scene load_steps=6 format=3 uid="uid://cf3qks3v18xmr"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Text/settings_text.gd" id="2"]
+[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3"]
+[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="3_s7xhj"]
+
+[sub_resource type="Image" id="Image_0sqes"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_3xcp4"]
+image = SubResource("Image_0sqes")
+
+[node name="DialogText" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_bottom = -156.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Title3" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Behaviour"
+
+[node name="VBox" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="DefaultSpeedLabel" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/VBox/DefaultSpeedLabel"]
+layout_mode = 2
+text = "Default letter speed"
+
+[node name="HintTooltip2" parent="VBoxContainer/VBox/DefaultSpeedLabel" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "The speed in seconds per character. A speed of 0 will reveal the full text instantly (still taking pauses into consideration)."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "The speed in seconds per character. A speed of 0 will reveal the full text instantly (still taking pauses into consideration)."
+
+[node name="DefaultSpeed" type="SpinBox" parent="VBoxContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+step = 0.001
+
+[node name="InputActionLabel" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="VBoxContainer/VBox/InputActionLabel"]
+layout_mode = 2
+text = "Input action"
+
+[node name="HintTooltip3" parent="VBoxContainer/VBox/InputActionLabel" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "The action that skips text and generally advances to the next event.
+You can modify actions in the Project Settings > Input Map."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "The action that skips text and generally advances to the next event.
+You can modify actions in the Project Settings > Input Map."
+
+[node name="InputAction" parent="VBoxContainer/VBox" instance=ExtResource("3")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="VBoxContainer/VBox/HBoxContainer"]
+layout_mode = 2
+text = "Text Reveal Skippable"
+
+[node name="HintTooltip4" parent="VBoxContainer/VBox/HBoxContainer" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "If enabled the revealing of text can be skipped with the input action.
+If disabled you can only advance to the next event when revealing has finnished."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "If enabled the revealing of text can be skipped with the input action.
+If disabled you can only advance to the next event when revealing has finnished."
+
+[node name="Skippable" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="Skippable" type="CheckBox" parent="VBoxContainer/VBox/Skippable"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/VBox/Skippable"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="VBoxContainer/VBox/Skippable/HBoxContainer2"]
+layout_mode = 2
+text = "Skip Delay:"
+
+[node name="HintTooltip4" parent="VBoxContainer/VBox/Skippable/HBoxContainer2" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "Delay before you can skip.
+
+Use this to prevent users from skipping through your timeline to quickly."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "Delay before you can skip.
+
+Use this to prevent users from skipping through your timeline too quickly."
+
+[node name="SkippableDelay" type="SpinBox" parent="VBoxContainer/VBox/Skippable/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+step = 0.01
+suffix = "s"
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer/VBox/Skippable"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="VBoxContainer/VBox/Skippable/HBoxContainer3"]
+layout_mode = 2
+text = "Advance Delay:"
+
+[node name="HintTooltip4" parent="VBoxContainer/VBox/Skippable/HBoxContainer3" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "Delay before you can advance (if the text finishes revealing on its own).
+
+This is used to prevent players from advancing when they actually wanted to skip the revealing, but did so very shortly after the text was already fully revealed."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "Delay before you can advance (only if the text finishes revealing on its own).
+
+This is used to prevent players from advancing when they actually wanted to skip the revealing, but did so very shortly after the text was already fully revealed."
+
+[node name="AdvanceDelay" type="SpinBox" parent="VBoxContainer/VBox/Skippable/HBoxContainer3"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+step = 0.01
+suffix = "s"
+
+[node name="ColorNames" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="Label4" type="Label" parent="VBoxContainer/VBox/ColorNames"]
+layout_mode = 2
+text = "Autocolor names"
+
+[node name="HintTooltip5" parent="VBoxContainer/VBox/ColorNames" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "If enabled character names will be colored in the characters color in text events."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "If enabled character names will be colored in the characters color in text events."
+
+[node name="AutocolorNames" type="CheckBox" parent="VBoxContainer/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="Label5" type="Label" parent="VBoxContainer/VBox/HBoxContainer3"]
+layout_mode = 2
+text = "New lines as new events"
+
+[node name="HintTooltip7" parent="VBoxContainer/VBox/HBoxContainer3" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "If enabled dialogic, new lines will be treated as [n] effects,
+seemingly waiting for input before starting a new text."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "If enabled dialogic, new lines will be treated as [n] effects,
+seemingly waiting for input before starting a new text."
+
+[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer/VBox"]
+layout_mode = 2
+
+[node name="NewEvents" type="CheckBox" parent="VBoxContainer/VBox/HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="NewEventOption" type="OptionButton" parent="VBoxContainer/VBox/HBoxContainer4"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 2
+selected = 0
+fit_to_longest_item = false
+popup/item_0/text = "As new event"
+popup/item_0/id = 0
+popup/item_1/text = "Appended"
+popup/item_1/id = 1
+
+[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="HBoxContainer AutoAdvance" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="VBoxContainer/HBoxContainer AutoAdvance"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Auto-Advance"
+
+[node name="HintTooltip" parent="VBoxContainer/HBoxContainer AutoAdvance" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "Autoadvance is the concept of automatically progressing to the next event upon completing text display, usually after a certain delay.
+
+You can enabled Auto-Advance from code using either:
+- Dialogic.Inputs.auto_advance.enabled_until_user_input = true
+- Dialogic.Inputs.auto_advance.enabled_until_next_event = true
+- Dialogic.Inputs.auto_advance.enabled_forced = true
+These add up, so if any of them is true, Auto-Advance will happen.
+Unless manual advancement is disabled, the Auto-Advance time can always be skipped by the player.
+
+The Auto-Advance will wait for Voice audio to finish playing. This behaviour can be disabled via code. "
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "Autoadvance is the concept of automatically progressing to the next event upon completing text display, usually after a certain delay.
+
+You can enabled Auto-Advance from code using either:
+- Dialogic.Inputs.auto_advance.enabled_until_user_input = true
+- Dialogic.Inputs.auto_advance.enabled_until_next_event = true
+- Dialogic.Inputs.auto_advance.enabled_forced = true
+These add up, so if any of them is true, Auto-Advance will happen.
+Unless manual advancement is disabled, the Auto-Advance time can always be skipped by the player.
+
+The Auto-Advance will wait for Voice audio to finish playing. This behaviour can be disabled via code. "
+
+[node name="AutoadvanceSettings" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="HBox_BaseDelay2" type="HBoxContainer" parent="VBoxContainer/AutoadvanceSettings"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/AutoadvanceSettings/HBox_BaseDelay2"]
+layout_mode = 2
+text = "Base Delay"
+
+[node name="HintTooltip" parent="VBoxContainer/AutoadvanceSettings/HBox_BaseDelay2" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "This is the base delay for autoadvancment."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "This is the base delay for autoadvancment."
+
+[node name="FixedDelay" type="SpinBox" parent="VBoxContainer/AutoadvanceSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 2
+step = 0.01
+value = 1.0
+suffix = "s"
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/AutoadvanceSettings"]
+layout_mode = 2
+
+[node name="Label2" type="Label" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer"]
+layout_mode = 2
+text = "Additional Delay"
+
+[node name="HintTooltip2" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "An additional delay per character or word can be added.
+
+Note: When changing values via code, you can actually use both modes simultaniously."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "An additional delay per character or word can be added.
+
+Note: When changing values via code, you can actually use both modes simultaniously."
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/AutoadvanceSettings"]
+layout_mode = 2
+
+[node name="AdditionalDelayMode" type="OptionButton" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+item_count = 3
+selected = 0
+fit_to_longest_item = false
+popup/item_0/text = "None"
+popup/item_0/id = 0
+popup/item_1/text = "Per Word"
+popup/item_1/id = 1
+popup/item_2/text = "Per Character"
+popup/item_2/id = 2
+
+[node name="AdditionalDelay" type="SpinBox" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+step = 0.001
+
+[node name="AutoadvanceIgnoreCharacters" type="HBoxContainer" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AutoadvanceIgnoreCharacters"]
+layout_mode = 2
+text = "Ignored Characters"
+
+[node name="HintTooltip3" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AutoadvanceIgnoreCharacters" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "An ignored character will add no delay, this is useful to exclude interpunction and whitespaces.
+
+If disabled, the general line of text length will be used, stripping the BBCode tags first.
+If enabled, the text will be scanned and the matching characters will be skipped."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "An ignored character will add no delay, this is useful to exclude interpunction and whitespaces.
+
+If disabled, the general line of text length will be used, stripping the BBCode tags first.
+If enabled, the text will be scanned and the matching characters will be skipped."
+
+[node name="IgnoredCharactersEnabled" type="CheckBox" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AutoadvanceIgnoreCharacters"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="IgnoredCharacters" type="LineEdit" parent="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AutoadvanceIgnoreCharacters"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "/\\\\,.( ); ?!-+\"'"
+expand_to_text_length = true
+
+[node name="HBox_BaseDelay" type="HBoxContainer" parent="VBoxContainer/AutoadvanceSettings"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/AutoadvanceSettings/HBox_BaseDelay"]
+layout_mode = 2
+text = "Enabled at the start"
+
+[node name="HintTooltip" parent="VBoxContainer/AutoadvanceSettings/HBox_BaseDelay" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "While you would usually enable Auto-Advance via code,
+if this is true it will be initially enabled.
+This kind of Auto-Advance (system) only stops when disabled via code. "
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "While you would usually enable Auto-Advance via code,
+if this is true it will be initially enabled.
+This kind of Auto-Advance (system) only stops when disabled via code. "
+
+[node name="AutoAdvance" type="CheckBox" parent="VBoxContainer/AutoadvanceSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="HBoxContainer AutoSkip" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="VBoxContainer/HBoxContainer AutoSkip"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Auto-Skip"
+
+[node name="HintTooltip" parent="VBoxContainer/HBoxContainer AutoSkip" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "Auto-Skip is the concept of automatically skipping Timeline Events to the next unread Text Event or Event demanding user inputs (e.g. Choice, Wait Input, and Text Input).
+
+You can enable Auto-Skip from code via:
+Dialogic.Inputs.auto_skip.enabled = true
+
+By default, Auto-Skip will cancel on user input.
+You can disable this by calling:
+Dialogic.Inputs.auto_skip.disable_on_user_input = false"
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "Auto-Skip is the concept of automatically skipping Timeline Events to the next unread Text Event or Event demanding user inputs (e.g. Choice, Wait Input, and Text Input).
+
+You can enable Auto-Skip from code via:
+Dialogic.Inputs.auto_skip.enabled = true
+
+By default, Auto-Skip will cancel on user input.
+You can disable this by calling:
+Dialogic.Inputs.auto_skip.disable_on_user_input = false"
+
+[node name="AutoskipSettings" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="HBox_BaseDelay2" type="HBoxContainer" parent="VBoxContainer/AutoskipSettings"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/AutoskipSettings/HBox_BaseDelay2"]
+layout_mode = 2
+text = "Time per Event"
+
+[node name="HintTooltip" parent="VBoxContainer/AutoskipSettings/HBox_BaseDelay2" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "The time until Auto-Skip will execute the next event.
+
+If this is set to 0.1s, each event should finish within that time.
+Custom events must respect this time, built-in events already handle Auto-Skip."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "The time until Auto-Skip will execute the next event.
+
+If this is set to 0.1s, each event should finish within that time.
+Custom events must respect this time, built-in events already handle Auto-Skip."
+
+[node name="AutoskipTimePerEvent" type="SpinBox" parent="VBoxContainer/AutoskipSettings"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 2
+step = 0.01
+value = 0.1
+suffix = "s"
+
+[node name="HSeparator3" type="HSeparator" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Title2" type="Label" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+theme_type_variation = &"DialogicSettingsSection"
+text = "Auto-Pauses"
+
+[node name="HintTooltip" parent="VBoxContainer/HBoxContainer" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "Adds pauses after certain letters.
+
+Each set can contain multiple letters that will (individually)
+have a pause of the given length added after them."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "Adds pauses after certain letters.
+
+Each set can contain multiple letters that will (individually)
+have a pause of the given length added after them."
+
+[node name="Add" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 4
+text = "Add set"
+
+[node name="AutoPauseSets" type="GridContainer" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+columns = 3
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Label5" type="Label" parent="VBoxContainer/HBoxContainer3"]
+layout_mode = 2
+text = "Absolute auto-pause times"
+
+[node name="HintTooltip7" parent="VBoxContainer/HBoxContainer3" instance=ExtResource("3_s7xhj")]
+layout_mode = 2
+tooltip_text = "If not enabled autopauses will be multiplied by the speed and user speed. When enabled those will be ignored."
+texture = SubResource("ImageTexture_3xcp4")
+hint_text = "If not enabled autopauses will be multiplied by the speed and user speed. When enabled those will be ignored."
+
+[node name="AutoPausesAbsolute" type="CheckBox" parent="VBoxContainer/HBoxContainer3"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[connection signal="value_changed" from="VBoxContainer/VBox/InputAction" to="." method="_on_InputAction_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/VBox/Skippable/HBoxContainer3/AdvanceDelay" to="." method="_on_skippable_delay_value_changed"]
+[connection signal="item_selected" from="VBoxContainer/VBox/HBoxContainer4/NewEventOption" to="." method="_on_new_event_option_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AdditionalDelayMode" to="." method="_on_additional_delay_mode_item_selected"]
+[connection signal="value_changed" from="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AdditionalDelay" to="." method="_on_additional_delay_value_changed"]
+[connection signal="text_changed" from="VBoxContainer/AutoadvanceSettings/HBoxContainer2/AutoadvanceIgnoreCharacters/IgnoredCharacters" to="." method="_on_IgnoredCharacters_text_changed"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer/Add" to="." method="_on_add_autopauses_set_pressed"]
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that handles showing of dialog text (+text effects & modifiers), name label, and next indicator
+
+#region SIGNALS
+
+signal about_to_show_text(info:Dictionary)
+signal text_finished(info:Dictionary)
+signal speaker_updated(character:DialogicCharacter)
+signal textbox_visibility_changed(visible:bool)
+
+signal animation_textbox_new_text
+signal animation_textbox_show
+signal animation_textbox_hide
+
+# forwards of the dialog_text signals of all present dialog_text nodes
+signal meta_hover_ended(meta:Variant)
+signal meta_hover_started(meta:Variant)
+signal meta_clicked(meta:Variant)
+
+#endregion
+
+
+# used to color names without searching for all characters each time
+var character_colors := {}
+var color_regex := RegEx.new()
+var text_already_read := false
+
+var text_effects := {}
+var parsed_text_effect_info: Array[Dictionary] = []
+var text_effects_regex := RegEx.new()
+enum TextModifierModes {ALL=-1, TEXT_ONLY=0, CHOICES_ONLY=1}
+enum TextTypes {DIALOG_TEXT, CHOICE_TEXT}
+var text_modifiers := []
+
+
+## set by the [speed] effect, multies the letter speed and [pause] effects
+var _speed_multiplier := 1.0
+## stores the pure letter speed (unmultiplied)
+var _pure_letter_speed := 0.1
+var _letter_speed_absolute := false
+
+var _voice_synced_text := false
+
+var _autopauses := {}
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ update_dialog_text('', true)
+ update_name_label(null)
+ dialogic.current_state_info['speaker'] = ""
+ dialogic.current_state_info['text'] = ''
+
+ set_text_reveal_skippable(ProjectSettings.get_setting('dialogic/text/initial_text_reveal_skippable', true))
+
+ # TODO check whether this can happen on the node directly
+ for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
+ if text_node.start_hidden:
+ text_node.textbox_root.hide()
+
+
+func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
+ update_textbox(dialogic.current_state_info.get('text', ''), true)
+ update_dialog_text(dialogic.current_state_info.get('text', ''), true)
+ var character: DialogicCharacter = null
+ if dialogic.current_state_info.get('speaker', ""):
+ character = load(dialogic.current_state_info.get('speaker', ""))
+
+ if character:
+ update_name_label(character)
+
+
+func post_install() -> void:
+ dialogic.Settings.connect_to_change('text_speed', _update_user_speed)
+
+ collect_text_effects()
+ collect_text_modifiers()
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## Applies modifiers, effects and coloring to the text
+func parse_text(text:String, type:int=TextTypes.DIALOG_TEXT, variables := true, glossary := true, modifiers:= true, effects:= true, color_names:= true) -> String:
+ if modifiers:
+ text = parse_text_modifiers(text, type)
+ if variables and dialogic.has_subsystem('VAR'):
+ text = dialogic.VAR.parse_variables(text)
+ if effects:
+ text = parse_text_effects(text)
+ if color_names:
+ text = color_character_names(text)
+ if glossary and dialogic.has_subsystem('Glossary'):
+ text = dialogic.Glossary.parse_glossary(text)
+ return text
+
+
+## When an event updates the text spoken, this can adjust the state of
+## the dialog text box.
+## This method is async.
+func update_textbox(text: String, instant := false) -> void:
+ if text.is_empty():
+ await hide_textbox(instant)
+ else:
+ await show_textbox(instant)
+
+ if !dialogic.current_state_info['text'].is_empty():
+ animation_textbox_new_text.emit()
+
+ if dialogic.Animations.is_animating():
+ await dialogic.Animations.finished
+
+
+## Shows the given text on all visible DialogText nodes.
+## Instant can be used to skip all revieling.
+## If additional is true, the previous text will be kept.
+func update_dialog_text(text: String, instant := false, additional := false) -> String:
+ update_text_speed()
+
+ if !instant: dialogic.current_state = dialogic.States.REVEALING_TEXT
+
+ if additional:
+ dialogic.current_state_info['text'] += text
+ else:
+ dialogic.current_state_info['text'] = text
+
+ for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
+ connect_meta_signals(text_node)
+
+ if text_node.enabled and (text_node == text_node.textbox_root or text_node.textbox_root.is_visible_in_tree()):
+
+ if instant:
+ text_node.text = text
+
+ else:
+ var current_character := get_current_speaker()
+
+ if current_character:
+ var character_prefix: String = current_character.custom_info.get(DialogicCharacterPrefixSuffixSection.PREFIX_CUSTOM_KEY, DialogicCharacterPrefixSuffixSection.DEFAULT_PREFIX)
+ var character_suffix: String = current_character.custom_info.get(DialogicCharacterPrefixSuffixSection.SUFFIX_CUSTOM_KEY, DialogicCharacterPrefixSuffixSection.DEFAULT_SUFFIX)
+ text = character_prefix + text + character_suffix
+
+ text_node.reveal_text(text, additional)
+
+ if !text_node.finished_revealing_text.is_connected(_on_dialog_text_finished):
+ text_node.finished_revealing_text.connect(_on_dialog_text_finished)
+
+ dialogic.current_state_info['text_parsed'] = (text_node as RichTextLabel).get_parsed_text()
+
+ # Reset speed multiplier
+ update_text_speed(-1, false, 1)
+ # Reset Auto-Advance temporarily and the No-Skip setting:
+ dialogic.Inputs.auto_advance.enabled_until_next_event = false
+ dialogic.Inputs.auto_advance.override_delay_for_current_event = -1
+ dialogic.Inputs.manual_advance.disabled_until_next_event = false
+
+ set_text_reveal_skippable(true, true)
+
+ return text
+
+
+func _on_dialog_text_finished() -> void:
+ text_finished.emit({'text':dialogic.current_state_info['text'], 'character':dialogic.current_state_info['speaker']})
+
+
+## Updates the visible name on all name labels nodes.
+## If a name changes, the [signal speaker_updated] signal is emitted.
+func update_name_label(character:DialogicCharacter):
+ var character_path := character.resource_path if character else ""
+ var current_character_path: String = dialogic.current_state_info.get("speaker", "")
+
+ if character_path != current_character_path:
+ dialogic.current_state_info['speaker'] = character_path
+ speaker_updated.emit(character)
+
+ var name_label_text := get_character_name_parsed(character)
+
+ for name_label in get_tree().get_nodes_in_group('dialogic_name_label'):
+ name_label.text = name_label_text
+ if character:
+ if !'use_character_color' in name_label or name_label.use_character_color:
+ name_label.self_modulate = character.color
+ else:
+ name_label.self_modulate = Color(1,1,1,1)
+
+
+func update_typing_sound_mood(mood:Dictionary = {}) -> void:
+ for typing_sound in get_tree().get_nodes_in_group('dialogic_type_sounds'):
+ typing_sound.load_overwrite(mood)
+
+
+## instant skips the signal and thus possible animations
+func show_textbox(instant:=false) -> void:
+ var emitted := instant
+ for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
+ if not text_node.enabled:
+ continue
+ if not text_node.textbox_root.visible and not emitted:
+ animation_textbox_show.emit()
+ text_node.textbox_root.show()
+ if dialogic.Animations.is_animating():
+ await dialogic.Animations.finished
+ textbox_visibility_changed.emit(true)
+ emitted = true
+ else:
+ text_node.textbox_root.show()
+
+
+## Instant skips the signal and thus possible animations
+func hide_textbox(instant:=false) -> void:
+ dialogic.current_state_info['text'] = ''
+ var emitted := instant
+ for name_label in get_tree().get_nodes_in_group('dialogic_name_label'):
+ name_label.text = ""
+ if !emitted and !get_tree().get_nodes_in_group('dialogic_dialog_text').is_empty() and get_tree().get_nodes_in_group('dialogic_dialog_text')[0].textbox_root.visible:
+ animation_textbox_hide.emit()
+ if dialogic.Animations.is_animating():
+ await dialogic.Animations.finished
+ for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
+ if text_node.textbox_root.visible and !emitted:
+ textbox_visibility_changed.emit(false)
+ emitted = true
+ text_node.textbox_root.hide()
+
+
+func is_textbox_visible() -> bool:
+ return get_tree().get_nodes_in_group('dialogic_dialog_text').any(func(x): return x.textbox_root.visible)
+
+
+func show_next_indicators(question:=false, autoadvance:=false) -> void:
+ for next_indicator in get_tree().get_nodes_in_group('dialogic_next_indicator'):
+ if next_indicator.enabled:
+ if (question and 'show_on_questions' in next_indicator and next_indicator.show_on_questions) or \
+ (autoadvance and 'show_on_autoadvance' in next_indicator and next_indicator.show_on_autoadvance) or (!question and !autoadvance):
+ next_indicator.show()
+ else:
+ next_indicator.hide()
+
+
+func hide_next_indicators(_fake_arg :Variant= null) -> void:
+ for next_indicator in get_tree().get_nodes_in_group('dialogic_next_indicator'):
+ next_indicator.hide()
+
+
+## This method will sync the text speed to the voice audio clip length, if a
+## voice is playing.
+## For instance, if the voice is playing for four seconds, the text will finish
+## revealing after this time.
+## This feature ignores Auto-Pauses on letters. Pauses via BBCode will desync
+## the reveal.
+func set_text_voice_synced(enabled: bool = true) -> void:
+ _voice_synced_text = enabled
+ update_text_speed()
+
+
+## Returns whether voice-synced text is enabled.
+func is_text_voice_synced() -> bool:
+ return _voice_synced_text
+
+
+## Sets how fast text will be revealed.
+## [br][br]
+## [param letter_speed] is the speed a single text character takes to appear
+## on the textbox.
+## [br][br]
+## [param absolute] will force text to display at the given speed, regardless
+## of the user's text speed setting.
+## [br][br]
+## [param _speed_multiplier] adjusts the speed of the text, if set to -1,
+## the value won't be updated and the current value will persist.
+## [br][br]
+## [param _user_speed] adjusts the speed of the text, if set to -1, the
+## project setting 'text_speed' will be used.operator
+func update_text_speed(letter_speed: float = -1,
+ absolute := false,
+ speed_multiplier := _speed_multiplier,
+ user_speed: float = dialogic.Settings.get_setting('text_speed', 1)) -> void:
+
+ if letter_speed == -1:
+ letter_speed = ProjectSettings.get_setting('dialogic/text/letter_speed', 0.01)
+
+ _pure_letter_speed = letter_speed
+ _letter_speed_absolute = absolute
+ _speed_multiplier = speed_multiplier
+
+ for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
+ if absolute:
+ text_node.set_speed(letter_speed)
+ else:
+ text_node.set_speed(letter_speed * _speed_multiplier * user_speed)
+
+
+func set_text_reveal_skippable(skippable:= true, temp:=false) -> void:
+ if !dialogic.current_state_info.has('text_reveal_skippable'):
+ dialogic.current_state_info['text_reveal_skippable'] = {'enabled':false, 'temp_enabled':false}
+
+ if temp:
+ dialogic.current_state_info['text_reveal_skippable']['temp_enabled'] = skippable
+ else:
+ dialogic.current_state_info['text_reveal_skippable']['enabled'] = skippable
+
+
+func is_text_reveal_skippable() -> bool:
+ return dialogic.current_state_info['text_reveal_skippable']['enabled'] and dialogic.current_state_info['text_reveal_skippable'].get('temp_enabled', true)
+
+
+func skip_text_reveal() -> void:
+ for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
+ if text_node.is_visible_in_tree():
+ text_node.finish_text()
+ if dialogic.has_subsystem('Voice'):
+ dialogic.Voice.stop_audio()
+
+#endregion
+
+
+#region TEXT EFFECTS & MODIFIERS
+####################################################################################################
+
+func collect_text_effects() -> void:
+ var text_effect_names := ""
+ text_effects.clear()
+ for indexer in DialogicUtil.get_indexers(true):
+ for effect in indexer._get_text_effects():
+ text_effects[effect.command] = {}
+ if effect.has('subsystem') and effect.has('method'):
+ text_effects[effect.command]['callable'] = Callable(dialogic.get_subsystem(effect.subsystem), effect.method)
+ elif effect.has('node_path') and effect.has('method'):
+ text_effects[effect.command]['callable'] = Callable(get_node(effect.node_path), effect.method)
+ else:
+ continue
+ text_effect_names += effect.command +"|"
+ text_effects_regex.compile("(?<!\\\\)\\[\\s*(?<command>"+text_effect_names.trim_suffix("|")+")\\s*(=\\s*(?<value>.+?)\\s*)?\\]")
+
+
+## Returns the string with all text effects removed
+## Use get_parsed_text_effects() after calling this to get all effect information
+func parse_text_effects(text:String) -> String:
+ parsed_text_effect_info.clear()
+ var rtl := RichTextLabel.new()
+ rtl.bbcode_enabled = true
+ var position_correction := 0
+ var bbcode_correction := 0
+ for effect_match in text_effects_regex.search_all(text):
+ rtl.text = text.substr(0, effect_match.get_start()-position_correction)
+ bbcode_correction = effect_match.get_start()-position_correction-len(rtl.get_parsed_text())
+ # append [index] = [command, value] to effects dict
+ parsed_text_effect_info.append({'index':effect_match.get_start()-position_correction-bbcode_correction, 'execution_info':text_effects[effect_match.get_string('command')], 'value': effect_match.get_string('value').strip_edges()})
+
+ text = text.substr(0,effect_match.get_start()-position_correction)+text.substr(effect_match.get_start()-position_correction+len(effect_match.get_string()))
+
+ position_correction += len(effect_match.get_string())
+ text = text.replace('\\[', '[')
+ rtl.queue_free()
+ return text
+
+
+func execute_effects(current_index:int, text_node:Control, skipping := false) -> void:
+ # might have to execute multiple effects
+ while true:
+ if parsed_text_effect_info.is_empty():
+ return
+ if current_index != -1 and current_index < parsed_text_effect_info[0]['index']:
+ return
+ var effect: Dictionary = parsed_text_effect_info.pop_front()
+ await (effect['execution_info']['callable'] as Callable).call(text_node, skipping, effect['value'])
+
+
+func collect_text_modifiers() -> void:
+ text_modifiers.clear()
+ for indexer in DialogicUtil.get_indexers(true):
+ for modifier in indexer._get_text_modifiers():
+ if modifier.has('subsystem') and modifier.has('method'):
+ text_modifiers.append({'method':Callable(dialogic.get_subsystem(modifier.subsystem), modifier.method)})
+ elif modifier.has('node_path') and modifier.has('method'):
+ text_modifiers.append({'method':Callable(get_node(modifier.node_path), modifier.method)})
+ text_modifiers[-1]['mode'] = modifier.get('mode', TextModifierModes.TEXT_ONLY)
+
+
+func parse_text_modifiers(text:String, type:int=TextTypes.DIALOG_TEXT) -> String:
+ for mod in text_modifiers:
+ if mod.mode != TextModifierModes.ALL and type != -1 and type != mod.mode:
+ continue
+ text = mod.method.call(text)
+ return text
+
+
+#endregion
+
+
+#region HELPERS & OTHER STUFF
+####################################################################################################
+
+func _ready() -> void:
+ dialogic.event_handled.connect(hide_next_indicators)
+
+ _autopauses = {}
+ var autopause_data: Dictionary = ProjectSettings.get_setting('dialogic/text/autopauses', {})
+ for i in autopause_data.keys():
+ _autopauses[RegEx.create_from_string(r"(?<!(\[|\{))["+i+r"](?!([^{}\[\]]*[\]\}]|$))")] = autopause_data[i]
+
+
+## Parses the character's display_name and returns the text that
+## should be rendered. Note that characters may have variables in their
+## name, therefore this function should be called to evaluate
+## any potential variables in a character's name.
+func get_character_name_parsed(character:DialogicCharacter) -> String:
+ if character:
+ var translated_display_name := character.get_display_name_translated()
+ if dialogic.has_subsystem('VAR'):
+ return dialogic.VAR.parse_variables(translated_display_name)
+ else:
+ return translated_display_name
+ return ""
+
+
+## Returns the [class DialogicCharacter] of the current speaker.
+## If there is no current speaker or the speaker is not found, returns null.
+func get_current_speaker() -> DialogicCharacter:
+ var speaker_path: String = dialogic.current_state_info.get("speaker", "")
+
+ if speaker_path.is_empty():
+ return null
+
+ var speaker_resource := load(speaker_path)
+
+ if speaker_resource == null:
+ return null
+
+ var speaker_character := speaker_resource as DialogicCharacter
+
+ return speaker_character
+
+
+func _update_user_speed(_user_speed:float) -> void:
+ update_text_speed(_pure_letter_speed, _letter_speed_absolute)
+
+
+func connect_meta_signals(text_node: Node) -> void:
+ if not text_node.meta_clicked.is_connected(emit_meta_signal):
+ text_node.meta_clicked.connect(emit_meta_signal.bind("meta_clicked"))
+
+ if not text_node.meta_hover_started.is_connected(emit_meta_signal):
+ text_node.meta_hover_started.connect(emit_meta_signal.bind("meta_hover_started"))
+
+ if not text_node.meta_hover_ended.is_connected(emit_meta_signal):
+ text_node.meta_hover_ended.connect(emit_meta_signal.bind("meta_hover_ended"))
+
+
+func emit_meta_signal(meta:Variant, sig:String) -> void:
+ emit_signal(sig, meta)
+
+#endregion
+
+#region AUTOCOLOR NAMES
+################################################################################
+
+func color_character_names(text:String) -> String:
+ if not ProjectSettings.get_setting('dialogic/text/autocolor_names', false):
+ return text
+
+ collect_character_names()
+
+ var counter := 0
+ for result in color_regex.search_all(text):
+ text = text.insert(result.get_start("name")+((9+8+8)*counter), '[color=#' + character_colors[result.get_string('name')].to_html() + ']')
+ text = text.insert(result.get_end("name")+9+8+((9+8+8)*counter), '[/color]')
+ counter += 1
+
+ return text
+
+
+func collect_character_names() -> void:
+ #don't do this at all if we're not using autocolor names to begin with
+ if not ProjectSettings.get_setting("dialogic/text/autocolor_names", false):
+ return
+
+ character_colors = {}
+
+ for dch_path in DialogicResourceUtil.get_character_directory().values():
+ var character := (load(dch_path) as DialogicCharacter)
+
+ if character.display_name:
+ if "{" in character.display_name and "}" in character.display_name:
+ character_colors[dialogic.VAR.parse_variables(character.display_name)] = character.color
+ else:
+ character_colors[character.display_name] = character.color
+
+ for nickname in character.get_nicknames_translated():
+ nickname = nickname.strip_edges()
+ if nickname:
+ if "{" in nickname and "}" in nickname:
+ character_colors[dialogic.VAR.parse_variables(nickname)] = character.color
+ else:
+ character_colors[nickname] = character.color
+
+ if dialogic.has_subsystem("Glossary"):
+ dialogic.Glossary.color_overrides.merge(character_colors, true)
+
+ var sorted_keys := character_colors.keys()
+ sorted_keys.sort_custom(sort_by_length)
+
+ var character_names := ""
+ for key in sorted_keys:
+ character_names += r"\Q" + key + r"\E|"
+
+ character_names = character_names.trim_suffix("|")
+ color_regex.compile(r"(?<=\W|^)(?<name>" + character_names + r")(?=\W|$)")
+
+
+func sort_by_length(a:String, b:String) -> bool:
+ if a.length() > b.length():
+ return true
+ return false
+#endregion+
+
+
+#region DEFAULT TEXT EFFECTS & MODIFIERS
+################################################################################
+
+func effect_pause(_text_node:Control, skipped:bool, argument:String) -> void:
+ if skipped:
+ return
+
+ # We want to ignore pauses if we're skipping.
+ if dialogic.Inputs.auto_skip.enabled:
+ return
+
+ var text_speed: float = dialogic.Settings.get_setting('text_speed', 1)
+
+ if argument:
+ if argument.ends_with('!'):
+ await get_tree().create_timer(float(argument.trim_suffix('!'))).timeout
+
+ elif _speed_multiplier != 0 and text_speed != 0:
+ await get_tree().create_timer(float(argument) * _speed_multiplier * text_speed).timeout
+
+ elif _speed_multiplier != 0 and text_speed != 0:
+ await get_tree().create_timer(0.5 * _speed_multiplier * text_speed).timeout
+
+
+func effect_speed(_text_node:Control, skipped:bool, argument:String) -> void:
+ if skipped:
+ return
+ if argument:
+ update_text_speed(-1, false, float(argument))
+ else:
+ update_text_speed(-1, false, 1)
+
+
+func effect_lspeed(_text_node:Control, skipped:bool, argument:String) -> void:
+ if skipped:
+ return
+ if argument:
+ if argument.ends_with('!'):
+ update_text_speed(float(argument.trim_suffix('!')), true)
+ else:
+ update_text_speed(float(argument), false)
+ else:
+ update_text_speed()
+
+
+func effect_signal(_text_node:Control, _skipped:bool, argument:String) -> void:
+ dialogic.text_signal.emit(argument)
+
+
+func effect_mood(_text_node:Control, _skipped:bool, argument:String) -> void:
+ if argument.is_empty(): return
+ if dialogic.current_state_info.get('speaker', ""):
+ update_typing_sound_mood(
+ load(dialogic.current_state_info.speaker).custom_info.get('sound_moods', {}).get(argument, {}))
+
+
+var modifier_words_select_regex := RegEx.create_from_string(r"(?<!\\)\<[^\[\>]+(\/[^\>]*)\>")
+func modifier_random_selection(text:String) -> String:
+ for replace_mod_match in modifier_words_select_regex.search_all(text):
+ var string: String = replace_mod_match.get_string().trim_prefix("<").trim_suffix(">")
+ string = string.replace('//', '<slash>')
+ var list: PackedStringArray = string.split('/')
+ var item: String = list[randi()%len(list)]
+ item = item.replace('<slash>', '/')
+ text = text.replace(replace_mod_match.get_string(), item.strip_edges())
+ return text
+
+
+func modifier_break(text:String) -> String:
+ return text.replace('[br]', '\n')
+
+
+func modifier_autopauses(text:String) -> String:
+ var absolute: bool = ProjectSettings.get_setting('dialogic/text/absolute_autopauses', false)
+ for i in _autopauses.keys():
+ var offset := 0
+ for result in i.search_all(text):
+ if absolute:
+ text = text.insert(result.get_end()+offset, '[pause='+str(_autopauses[i])+'!]')
+ offset += len('[pause='+str(_autopauses[i])+'!]')
+ else:
+ text = text.insert(result.get_end()+offset, '[pause='+str(_autopauses[i])+']')
+ offset += len('[pause='+str(_autopauses[i])+']')
+ return text
+#endregion
--- /dev/null
+@tool
+class_name DialogicTextInputEvent
+extends DialogicEvent
+
+## Event that shows an input field and will change a dialogic variable.
+
+
+### Settings
+
+## The promt to be shown.
+var text := "Please enter some text:"
+## The name/path of the variable to set.
+var variable := ""
+## The placeholder text to show in the line edit.
+var placeholder := ""
+## The value that should be in the line edit by default.
+var default := ""
+## If true, the player can continue if nothing is entered.
+var allow_empty := false
+
+
+################################################################################
+## EXECUTION
+################################################################################
+
+func _execute() -> void:
+ dialogic.Inputs.auto_skip.enabled = false
+ dialogic.current_state = DialogicGameHandler.States.WAITING
+ dialogic.TextInput.show_text_input(text, default, placeholder, allow_empty)
+ dialogic.TextInput.input_confirmed.connect(_on_DialogicTextInput_input_confirmed, CONNECT_ONE_SHOT)
+
+
+func _on_DialogicTextInput_input_confirmed(input:String) -> void:
+ if !dialogic.has_subsystem('VAR'):
+ printerr('[Dialogic] The TextInput event needs the variable subsystem to be present.')
+ finish()
+ return
+ dialogic.VAR.set_variable(variable, input)
+ dialogic.TextInput.hide_text_input()
+ dialogic.current_state = DialogicGameHandler.States.IDLE
+ finish()
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func _init() -> void:
+ event_name = "Text Input"
+ set_default_color('Color6')
+ event_category = "Logic"
+ event_sorting_index = 6
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "text_input"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "text" : {"property": "text", "default": "Please enter some text:"},
+ "var" : {"property": "variable", "default": "", "suggestions":get_var_suggestions},
+ "placeholder" : {"property": "placeholder", "default": ""},
+ "default" : {"property": "default", "default": ""},
+ "allow_empty" : {"property": "allow_empty", "default": false},
+ }
+
+################################################################################
+## EDITOR
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_label('Show an input and store it in')
+ add_header_edit('variable', ValueType.DYNAMIC_OPTIONS,
+ {'suggestions_func' : get_var_suggestions,
+ 'icon' : load("res://addons/dialogic/Editor/Images/Pieces/variable.svg"),
+ 'placeholder':'Select Variable'})
+ add_body_edit('text', ValueType.SINGLELINE_TEXT, {'left_text':'Text:'})
+ add_body_edit('placeholder', ValueType.SINGLELINE_TEXT, {'left_text':'Placeholder:'})
+ add_body_edit('default', ValueType.SINGLELINE_TEXT, {'left_text':'Default:'})
+ add_body_edit('allow_empty', ValueType.BOOL, {'left_text':'Allow empty:'})
+
+
+func get_var_suggestions(filter:String="") -> Dictionary:
+ var suggestions := {}
+ if filter:
+ suggestions[filter] = {
+ 'value' : filter,
+ 'editor_icon' : ["GuiScrollArrowRight", "EditorIcons"]}
+ var vars: Dictionary = ProjectSettings.get_setting('dialogic/variables', {})
+ for var_path in DialogicUtil.list_variables(vars, "", DialogicUtil.VarTypes.STRING):
+ suggestions[var_path] = {'value':var_path, 'icon':load("res://addons/dialogic/Editor/Images/Pieces/variable.svg")}
+ return suggestions
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_text_input.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'TextInput', 'script':this_folder.path_join('subsystem_text_input.gd')}]
+
--- /dev/null
+class_name DialogicNode_TextInput
+extends Control
+
+## Node that will show when a text input field is reached.
+## Should be connected to a (probably contained) label, a line edit and a button to work.
+
+## The LineEdit to use.
+@export_node_path var input_line_edit: NodePath
+## The Label to use.
+@export_node_path var text_label: NodePath
+## The Button to use.
+@export_node_path var confirmation_button: NodePath
+
+# This is set by the subsystem and used as a confirmation check.
+var _allow_empty := false
+
+
+func _ready() -> void:
+ add_to_group('dialogic_text_input')
+ if confirmation_button:
+ get_node(confirmation_button).pressed.connect(_on_confirmation_button_pressed)
+ if input_line_edit:
+ get_node(input_line_edit).text_changed.connect(_on_input_text_changed)
+ get_node(input_line_edit).text_submitted.connect(_on_confirmation_button_pressed)
+ visible = false
+
+
+func set_text(text:String) -> void:
+ if get_node(text_label) is Label:
+ get_node(text_label).text = text
+
+
+func set_placeholder(placeholder:String) -> void:
+ if get_node(input_line_edit) is LineEdit:
+ get_node(input_line_edit).placeholder_text = placeholder
+ get_node(input_line_edit).grab_focus()
+
+
+func set_default(default:String) -> void:
+ if get_node(input_line_edit) is LineEdit:
+ get_node(input_line_edit).text = default
+ _on_input_text_changed(default)
+
+
+func set_allow_empty(boolean:bool) -> void:
+ _allow_empty = boolean
+
+
+func _on_input_text_changed(text:String) -> void:
+ if confirmation_button.is_empty():
+ return
+ get_node(confirmation_button).disabled = !_allow_empty and text.is_empty()
+
+
+func _on_confirmation_button_pressed(text:="") -> void:
+ if get_node(input_line_edit) is LineEdit:
+ if !get_node(input_line_edit).text.is_empty() or _allow_empty:
+ DialogicUtil.autoload().TextInput.input_confirmed.emit(get_node(input_line_edit).text)
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that handles showing of input promts.
+
+## Signal that is fired when a confirmation button was pressed.
+signal input_confirmed(input:String)
+signal input_shown(info:Dictionary)
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
+ hide_text_input()
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+func show_text_input(text:= "", default:= "", placeholder:= "", allow_empty:= false) -> void:
+ for node in get_tree().get_nodes_in_group('dialogic_text_input'):
+ node.show()
+ if node.has_method('set_allow_empty'): node.set_allow_empty(allow_empty)
+ if node.has_method('set_text'): node.set_text(text)
+ if node.has_method('set_default'): node.set_default(default)
+ if node.has_method('set_placeholder'): node.set_placeholder(placeholder)
+ input_shown.emit({'text':text, 'default':default, 'placeholder':placeholder, 'allow_empty':allow_empty})
+
+
+func hide_text_input() -> void:
+ for node in get_tree().get_nodes_in_group('dialogic_text_input'):
+ node.hide()
+
+#endregion
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.0909 9.45452H15.2727V10.9091H13.0909V13.0909H11.6363V10.9091H9.45453V9.45452H11.6363V7.27271H13.0909V9.45452Z" fill="#A5EFAC"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1186 5.22444L13.2372 2.62927C12.9687 2.49006 12.6903 2.38069 12.402 2.30114C12.1136 2.2216 11.8153 2.18182 11.5071 2.18182C11.179 2.18182 10.8757 2.23651 10.5973 2.34589C10.3189 2.44532 10.0604 2.58452 9.82172 2.7635C9.59302 2.94248 9.38422 3.15128 9.1953 3.38992C9.00637 3.62856 8.83734 3.88708 8.68819 4.16549C8.60865 4.32458 8.51916 4.5135 8.41973 4.73225C8.33024 4.94106 8.24075 5.14489 8.15126 5.34376C8.04189 5.58239 7.93748 5.82103 7.83805 6.05966C7.73862 5.85086 7.63422 5.65697 7.52484 5.47799C7.43535 5.3189 7.34089 5.15981 7.24146 5.00072C7.15197 4.83168 7.06745 4.69248 6.98791 4.5831C6.82882 4.34447 6.64984 4.08594 6.45098 3.80753C6.25211 3.52912 6.03336 3.2706 5.79473 3.03197C5.55609 2.79333 5.29757 2.59447 5.01916 2.43537C4.74075 2.26634 4.43748 2.18182 4.10936 2.18182C3.84089 2.18182 3.5774 2.23651 3.31888 2.34589C3.06035 2.44532 2.81674 2.57955 2.58805 2.74859C2.35936 2.90768 2.14558 3.09162 1.94672 3.30043C1.7578 3.4993 1.59373 3.70313 1.45453 3.91194L1.85723 4.43395C1.99643 4.32458 2.14061 4.24006 2.28976 4.1804C2.4389 4.1108 2.59799 4.076 2.76703 4.076C2.96589 4.076 3.15481 4.12572 3.33379 4.22515C3.52271 4.31464 3.6818 4.43395 3.81106 4.5831L6.61504 9.07245C6.52555 9.34091 6.43606 9.59447 6.34657 9.8331L6.10794 10.4595C6.02839 10.6683 5.95382 10.8374 5.88422 10.9666C5.8345 11.0859 5.75993 11.1903 5.66049 11.2798C5.56106 11.3594 5.44672 11.429 5.31745 11.4886C5.19814 11.5384 5.07385 11.5781 4.94459 11.608C4.81532 11.6278 4.69103 11.6378 4.57172 11.6378C4.19388 11.6378 3.81603 11.5632 3.43819 11.4141C3.06035 11.255 2.71234 11.076 2.39416 10.8771L1.45453 13.353C1.73294 13.5717 2.0362 13.7457 2.36433 13.875C2.69245 14.0142 3.03052 14.0838 3.37853 14.0838C3.76632 14.0838 4.13422 13.9993 4.48223 13.8303C4.83024 13.6513 5.14842 13.4276 5.43677 13.1591C5.73507 12.8807 5.99856 12.5724 6.22726 12.2344C6.45595 11.8963 6.64487 11.5682 6.79402 11.25C6.83379 11.1705 6.87853 11.0561 6.92825 10.907C6.97797 10.7578 7.02271 10.6136 7.06248 10.4744C7.10226 10.3054 7.147 10.1364 7.19672 9.96734C7.31603 10.1364 7.44032 10.3452 7.56959 10.5938C7.69885 10.8324 7.82811 11.081 7.95737 11.3395C8.08663 11.598 8.21092 11.8516 8.33024 12.1001C8.4595 12.3388 8.57882 12.5426 8.68819 12.7117C8.8274 12.8906 8.96163 13.0646 9.09089 13.2337C9.22015 13.4027 9.35936 13.5519 9.50851 13.6811C9.65765 13.8004 9.82172 13.8949 10.0007 13.9645C10.1896 14.044 10.4034 14.0838 10.642 14.0838C10.9105 14.0838 11.174 14.0242 11.4325 13.9048C11.4983 13.878 11.5629 13.8492 11.6262 13.8182H10.9091V11.6364H8.72726V8.72728H9.30068C9.2095 8.54765 9.11969 8.36938 9.03123 8.19248C8.83237 7.79475 8.62853 7.37714 8.41973 6.93964L8.71802 6.08949C8.79757 5.91052 8.897 5.73154 9.01632 5.55256C9.13564 5.36364 9.27484 5.19461 9.43393 5.04546C9.59302 4.89631 9.76703 4.77699 9.95595 4.68751C10.1548 4.59802 10.3636 4.55327 10.5824 4.55327C10.8608 4.55327 11.1292 4.62287 11.3878 4.76208C11.6562 4.89134 11.8998 5.04546 12.1186 5.22444Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+class_name DialogicVariableEvent
+extends DialogicEvent
+
+## Event that allows changing a dialogic variable or a property of an autoload.
+
+
+enum Operations {SET, ADD, SUBSTRACT, MULTIPLY, DIVIDE}
+enum VarValueType {
+ STRING = 0,
+ NUMBER = 1,
+ VARIABLE = 2,
+ BOOL = 3,
+ EXPRESSION = 4,
+ RANDOM_NUMBER = 5,
+}
+
+## Settings
+
+## Name/Path of the variable that should be changed.
+var name := "":
+ set(_value):
+ name = _value
+ if Engine.is_editor_hint() and not value:
+ match DialogicUtil.get_variable_type(name):
+ DialogicUtil.VarTypes.ANY, DialogicUtil.VarTypes.STRING:
+ _value_type = VarValueType.STRING
+ DialogicUtil.VarTypes.FLOAT, DialogicUtil.VarTypes.INT:
+ _value_type = VarValueType.NUMBER
+ DialogicUtil.VarTypes.BOOL:
+ _value_type = VarValueType.BOOL
+ ui_update_needed.emit()
+ update_editor_warning()
+
+## The operation to perform.
+var operation := Operations.SET:
+ set(value):
+ operation = value
+ if operation != Operations.SET and _value_type == VarValueType.STRING:
+ _value_type = VarValueType.NUMBER
+ ui_update_needed.emit()
+ update_editor_warning()
+
+## The value that is used. Can be a variable as well.
+var value: Variant = ""
+var _value_type := 0 :
+ set(_value):
+ _value_type = _value
+ if not _suppress_default_value:
+ match _value_type:
+ VarValueType.STRING, VarValueType.VARIABLE, VarValueType.EXPRESSION:
+ value = ""
+ VarValueType.NUMBER:
+ value = 0
+ VarValueType.BOOL:
+ value = false
+ VarValueType.RANDOM_NUMBER:
+ value = null
+ ui_update_needed.emit()
+ update_editor_warning()
+
+## If true, a random number between [random_min] and [random_max] is used instead of [value].
+var random_min: int = 0
+var random_max: int = 100
+
+## Used to suppress _value_type from overwriting value with a default value when the type changes
+## This is only used when initializing the event_variable.
+var _suppress_default_value := false
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ if name:
+ var original_value: Variant = dialogic.VAR.get_variable(name, null, operation == Operations.SET and "[" in name)
+
+ if value != null and (original_value != null or (operation == Operations.SET and "[" in name)):
+
+ var interpreted_value: Variant
+ var result: Variant
+
+ match _value_type:
+ VarValueType.STRING:
+ interpreted_value = dialogic.VAR.get_variable('"' + value + '"')
+ VarValueType.VARIABLE:
+ interpreted_value = dialogic.VAR.get_variable('{' + value + '}')
+ VarValueType.NUMBER, VarValueType.BOOL, VarValueType.EXPRESSION, VarValueType.RANDOM_NUMBER:
+ interpreted_value = dialogic.VAR.get_variable(str(value))
+
+ if operation != Operations.SET and (not str(original_value).is_valid_float() or not str(interpreted_value).is_valid_float()):
+ printerr("[Dialogic] Set Variable event failed because one value wasn't a float! [", original_value, ", ",interpreted_value,"]")
+ finish()
+ return
+
+ if operation == Operations.SET:
+ result = interpreted_value
+
+ else:
+ original_value = float(original_value)
+ interpreted_value = float(interpreted_value)
+
+ match operation:
+ Operations.ADD:
+ result = original_value + interpreted_value
+ Operations.SUBSTRACT:
+ result = original_value - interpreted_value
+ Operations.MULTIPLY:
+ result = original_value * interpreted_value
+ Operations.DIVIDE:
+ result = original_value / interpreted_value
+
+ dialogic.VAR.set_variable(name, result)
+ dialogic.VAR.variable_was_set.emit(
+ {
+ 'variable' : name,
+ 'value' : interpreted_value,
+ 'value_str' : value,
+ 'orig_value' : original_value,
+ 'new_value' : result,
+ })
+
+ else:
+ printerr("[Dialogic] Set Variable event failed because one value wasn't set!")
+
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Set Variable"
+ set_default_color('Color6')
+ event_category = "Logic"
+ event_sorting_index = 0
+ help_page_path = "https://docs.dialogic.pro/variables.html#23-set-variable-event"
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func to_text() -> String:
+ var string := "set "
+ if name:
+ string += "{" + name.trim_prefix('{').trim_suffix('}') + "}"
+ match operation:
+ Operations.SET:
+ string+= " = "
+ Operations.ADD:
+ string+= " += "
+ Operations.SUBSTRACT:
+ string+= " -= "
+ Operations.MULTIPLY:
+ string+= " *= "
+ Operations.DIVIDE:
+ string+= " /= "
+
+ value = str(value)
+ match _value_type:
+ VarValueType.STRING: # String
+ string += '"'+value.replace('"', '\\"')+'"'
+ VarValueType.NUMBER,VarValueType.BOOL,VarValueType.EXPRESSION: # Float Bool, or Expression
+ string += str(value)
+ VarValueType.VARIABLE: # Variable
+ string += '{'+value+'}'
+ VarValueType.RANDOM_NUMBER:
+ string += 'range('+str(random_min)+','+str(random_max)+').pick_random()'
+
+ return string
+
+
+func from_text(string:String) -> void:
+ var reg := RegEx.new()
+ reg.compile("set(?<name>[^=+\\-*\\/]*)?(?<operation>=|\\+=|-=|\\*=|\\/=)?(?<value>.*)")
+ var result := reg.search(string)
+ if !result:
+ return
+ name = result.get_string('name').strip_edges().replace("{", "").replace("}", "")
+ match result.get_string('operation').strip_edges():
+ '=':
+ operation = Operations.SET
+ '-=':
+ operation = Operations.SUBSTRACT
+ '+=':
+ operation = Operations.ADD
+ '*=':
+ operation = Operations.MULTIPLY
+ '/=':
+ operation = Operations.DIVIDE
+
+ _suppress_default_value = true
+ value = result.get_string('value').strip_edges()
+ if not value.is_empty():
+ if value.begins_with('"') and value.ends_with('"') and value.count('"')-value.count('\\"') == 2:
+ value = result.get_string('value').strip_edges().replace('"', '')
+ _value_type = VarValueType.STRING
+ elif value.begins_with('{') and value.ends_with('}') and value.count('{') == 1:
+ value = result.get_string('value').strip_edges().trim_suffix('}').trim_prefix('{')
+ _value_type = VarValueType.VARIABLE
+ elif value in ["true", "false"]:
+ value = value == "true"
+ _value_type = VarValueType.BOOL
+ elif value.begins_with('range(') and value.ends_with(').pick_random()'):
+ _value_type = VarValueType.RANDOM_NUMBER
+ var randinf := str(value).trim_prefix('range(').trim_suffix(').pick_random()').split(',')
+ random_min = int(randinf[0])
+ random_max = int(randinf[1])
+ else:
+ value = result.get_string('value').strip_edges()
+ if value.is_valid_float():
+ _value_type = VarValueType.NUMBER
+ else:
+ _value_type = VarValueType.EXPRESSION
+ else:
+ value = null
+ _suppress_default_value = false
+
+
+func is_valid_event(string:String) -> bool:
+ return string.begins_with('set')
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('name', ValueType.DYNAMIC_OPTIONS, {
+ 'left_text' : 'Set',
+ 'suggestions_func' : get_var_suggestions,
+ 'icon' : load("res://addons/dialogic/Editor/Images/Pieces/variable.svg"),
+ 'placeholder' :'Select Variable'}
+ )
+ add_header_edit('operation', ValueType.FIXED_OPTIONS, {
+ 'options': [
+ {
+ 'label': 'to be',
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/set.svg"),
+ 'value': Operations.SET
+ },{
+ 'label': 'to itself plus',
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/plus.svg"),
+ 'value': Operations.ADD
+ },{
+ 'label': 'to itself minus',
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/minus.svg"),
+ 'value': Operations.SUBSTRACT
+ },{
+ 'label': 'to itself multiplied by',
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/multiply.svg"),
+ 'value': Operations.MULTIPLY
+ },{
+ 'label': 'to itself divided by',
+ 'icon': load("res://addons/dialogic/Editor/Images/Dropdown/divide.svg"),
+ 'value': Operations.DIVIDE
+ }
+ ]
+ }, '!name.is_empty()')
+ add_header_edit('_value_type', ValueType.FIXED_OPTIONS, {
+ 'options': [
+ {
+ 'label': 'String',
+ 'icon': ["String", "EditorIcons"],
+ 'value': VarValueType.STRING
+ },{
+ 'label': 'Number',
+ 'icon': ["float", "EditorIcons"],
+ 'value': VarValueType.NUMBER
+ },{
+ 'label': 'Variable',
+ 'icon': load("res://addons/dialogic/Editor/Images/Pieces/variable.svg"),
+ 'value': VarValueType.VARIABLE
+ },{
+ 'label': 'Bool',
+ 'icon': ["bool", "EditorIcons"],
+ 'value': VarValueType.BOOL
+ },{
+ 'label': 'Expression',
+ 'icon': ["Variant", "EditorIcons"],
+ 'value': VarValueType.EXPRESSION
+ },{
+ 'label': 'Random Number',
+ 'icon': ["RandomNumberGenerator", "EditorIcons"],
+ 'value': VarValueType.RANDOM_NUMBER
+ }],
+ 'symbol_only':true},
+ '!name.is_empty()')
+ add_header_edit('value', ValueType.SINGLELINE_TEXT, {}, '!name.is_empty() and (_value_type == VarValueType.STRING or _value_type == VarValueType.EXPRESSION) ')
+ add_header_edit('value', ValueType.BOOL, {}, '!name.is_empty() and (_value_type == VarValueType.BOOL) ')
+ add_header_edit('value', ValueType.NUMBER, {}, '!name.is_empty() and _value_type == VarValueType.NUMBER')
+ add_header_edit('value', ValueType.DYNAMIC_OPTIONS,
+ {'suggestions_func' : get_value_suggestions, 'placeholder':'Select Variable'},
+ '!name.is_empty() and _value_type == VarValueType.VARIABLE')
+ add_header_label('a number between', '_value_type == VarValueType.RANDOM_NUMBER')
+ add_header_edit('random_min', ValueType.NUMBER, {'right_text':'and', 'mode':1}, '!name.is_empty() and _value_type == VarValueType.RANDOM_NUMBER')
+ add_header_edit('random_max', ValueType.NUMBER, {'mode':1}, '!name.is_empty() and _value_type == VarValueType.RANDOM_NUMBER')
+ add_header_button('', _on_variable_editor_pressed, 'Variable Editor', ["ExternalLink", "EditorIcons"])
+
+
+func get_var_suggestions(filter:String) -> Dictionary:
+ var suggestions := {}
+ if filter:
+ suggestions[filter] = {'value':filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
+ for var_path in DialogicUtil.list_variables(DialogicUtil.get_default_variables()):
+ suggestions[var_path] = {'value':var_path, 'icon':load("res://addons/dialogic/Editor/Images/Pieces/variable.svg")}
+ return suggestions
+
+
+func get_value_suggestions(_filter:String) -> Dictionary:
+ var suggestions := {}
+
+ for var_path in DialogicUtil.list_variables(DialogicUtil.get_default_variables()):
+ suggestions[var_path] = {'value':var_path, 'icon':load("res://addons/dialogic/Editor/Images/Pieces/variable.svg")}
+ return suggestions
+
+
+func _on_variable_editor_pressed() -> void:
+ var editor_manager := editor_node.find_parent('EditorsManager')
+ if editor_manager:
+ editor_manager.open_editor(editor_manager.editors['VariablesEditor']['node'], true)
+
+
+func update_editor_warning() -> void:
+ if _value_type == VarValueType.STRING and operation != Operations.SET:
+ ui_update_warning.emit('You cannot do this operation with a string!')
+ elif operation != Operations.SET:
+ var type := DialogicUtil.get_variable_type(name)
+ if not type in [DialogicUtil.VarTypes.INT, DialogicUtil.VarTypes.FLOAT, DialogicUtil.VarTypes.ANY]:
+ ui_update_warning.emit('The selected variable is not a number!')
+ else:
+ ui_update_warning.emit('')
+ else:
+ ui_update_warning.emit('')
+
+
+
+####################### CODE COMPLETION ########################################
+################################################################################
+
+func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
+ if CodeCompletionHelper.get_line_untill_caret(line) == 'set ':
+ TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, '{', '{', TextNode.syntax_highlighter.variable_color)
+ if symbol == '{':
+ CodeCompletionHelper.suggest_variables(TextNode)
+
+
+func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
+ TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'set', 'set ', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.5))
+
+
+#################### SYNTAX HIGHLIGHTING #######################################
+################################################################################
+
+func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
+ dict[line.find('set')] = {"color":event_color.lerp(Highlighter.normal_color, 0.5)}
+ dict[line.find('set')+3] = {"color":Highlighter.normal_color}
+ dict = Highlighter.color_region(dict, Highlighter.string_color, line, '"', '"', line.find('set'))
+ dict = Highlighter.color_region(dict, Highlighter.variable_color, line, '{', '}', line.find('set'))
+ return dict
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_variable.gd')]
+
+func _get_editors() -> Array:
+ return [this_folder.path_join('variables_editor/variables_editor.tscn')]
+
+func _get_subsystems() -> Array:
+ return [{'name':'VAR', 'script':this_folder.path_join('subsystem_variables.gd')}]
--- /dev/null
+extends DialogicSubsystem
+
+## Subsystem that manages variables and allows to access them.
+
+## Emitted if a dialogic variable changes, gives a dictionary with the following keys:[br]
+## [br]
+## Key | Value Type | Value [br]
+## ----------- | ------------- | ----- [br]
+## `variable` | [type String] | The name of the variable that is getting changed. [br]
+## `new_value` | [type Variant]| The value that [variable] has after the change (the result). [br]
+signal variable_changed(info:Dictionary)
+
+## Emitted on a set variable event, gives a dictionary with the following keys:[br]
+## [br]
+## Key | Value Type | Value [br]
+## ----------- | ------------- | ----- [br]
+## `variable` | [type String] | The name of the variable that is getting changed. [br]
+## `orig_value`| [type Variant]| The value that [variable] had before. [br]
+## `new_value` | [type Variant]| The value that [variable] has after the change (the result). [br]
+## `value` | [type Variant]| The value that the variable is changed by/to. [br]
+## `value_str` | [type String] | Whatever has been given as the value (not interpreted, so a variable is just a string).[br]
+signal variable_was_set(info:Dictionary)
+
+
+#region STATE
+####################################################################################################
+
+func clear_game_state(clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR):
+ # loading default variables
+ if ! clear_flag & DialogicGameHandler.ClearFlags.KEEP_VARIABLES:
+ reset()
+
+
+func load_game_state(load_flag:=LoadFlags.FULL_LOAD):
+ if load_flag == LoadFlags.ONLY_DNODES:
+ return
+ dialogic.current_state_info['variables'] = merge_folder(dialogic.current_state_info['variables'], ProjectSettings.get_setting('dialogic/variables', {}).duplicate(true))
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+## This function will try to get the value of variables provided inside curly brackets
+## and replace them with their values.
+## It will:
+## - look for the strings to replace
+## - search all autoloads
+## - try to get the value from context
+##
+## So if you provide a string like `Hello, how are you doing {Game.player_name}
+## it will try to search for an autoload with the name `Game` and get the value
+## of `player_name` to replace it.
+func parse_variables(text:String) -> String:
+ # First some dirty checks to avoid parsing
+ if not '{' in text:
+ return text
+
+ # Trying to extract the curly brackets from the text
+ var regex := RegEx.new()
+ regex.compile("(?<!\\\\)\\{(?<variable>([^{}]|\\{.*\\})*)\\}")
+
+ var parsed := text.replace('\\{', '{')
+ for result in regex.search_all(text):
+ var value: Variant = get_variable(result.get_string('variable'), "<NOT FOUND>")
+ parsed = parsed.replace("{"+result.get_string('variable')+"}", str(value))
+
+ return parsed
+
+
+func set_variable(variable_name: String, value: Variant) -> bool:
+ variable_name = variable_name.trim_prefix('{').trim_suffix('}')
+
+ # First assume this is a simple dialogic variable
+ if has(variable_name):
+ DialogicUtil._set_value_in_dictionary(variable_name, dialogic.current_state_info['variables'], value)
+ variable_changed.emit({'variable':variable_name, 'new_value':value})
+ return true
+
+ # Second assume this is an autoload variable
+ elif '.' in variable_name:
+ var from := variable_name.get_slice('.', 0)
+ var variable := variable_name.trim_prefix(from+'.')
+
+ var autoloads := get_autoloads()
+ var object: Object = null
+ if from in autoloads:
+ object = autoloads[from]
+ while variable.count("."):
+ from = variable.get_slice('.', 0)
+ if from in object and object.get(from) is Object:
+ object = object.get(from)
+ variable = variable.trim_prefix(from+'.')
+
+ if object:
+ var sub_idx := ""
+ if '[' in variable:
+ sub_idx = variable.substr(variable.find('['))
+ variable = variable.trim_suffix(sub_idx)
+ sub_idx = sub_idx.trim_prefix('[').trim_suffix(']')
+
+ if variable in object:
+ match typeof(object.get(variable)):
+ TYPE_ARRAY:
+ if not sub_idx:
+ if typeof(value) == TYPE_ARRAY:
+ object.set(variable, value)
+ return true
+ elif sub_idx.is_valid_float():
+ object.get(variable).remove_at(int(sub_idx))
+ object.get(variable).insert(int(sub_idx), value)
+ return true
+ TYPE_DICTIONARY:
+ if not sub_idx:
+ if typeof(value) == TYPE_DICTIONARY:
+ object.set(variable, value)
+ return true
+ else:
+ object.get(variable).merge({str_to_var(sub_idx):value}, true)
+ return true
+ _:
+ object.set(variable, value)
+ return true
+
+ printerr("[Dialogic] Tried setting non-existant variable '"+variable_name+"'.")
+ return false
+
+
+func get_variable(variable_path:String, default: Variant = null, no_warning := false) -> Variant:
+ if variable_path.begins_with('{') and variable_path.ends_with('}') and variable_path.count('{') == 1:
+ variable_path = variable_path.trim_prefix('{').trim_suffix('}')
+
+ # First assume this is just a single variable
+ var value: Variant = DialogicUtil._get_value_in_dictionary(variable_path, dialogic.current_state_info['variables'])
+ if value != null:
+ return value
+
+ # Second assume this is an expression.
+ else:
+ value = dialogic.Expressions.execute_string(variable_path, null, no_warning)
+ if value != null:
+ return value
+
+ # If everything fails, tell the user and return the default
+ if not no_warning:
+ printerr("[Dialogic] Failed parsing variable/expression '"+variable_path+"'.")
+ return default
+
+
+## Resets all variables or a specific variable to the value(s) defined in the variable editor
+func reset(variable:="") -> void:
+ if variable.is_empty():
+ dialogic.current_state_info['variables'] = ProjectSettings.get_setting("dialogic/variables", {}).duplicate(true)
+ else:
+ DialogicUtil._set_value_in_dictionary(variable, dialogic.current_state_info['variables'], DialogicUtil._get_value_in_dictionary(variable, ProjectSettings.get_setting('dialogic/variables', {})))
+
+
+## Returns true if a variable with the given path exists
+func has(variable:="") -> bool:
+ return DialogicUtil._get_value_in_dictionary(variable, dialogic.current_state_info['variables']) != null
+
+
+
+## Allows to set dialogic built-in variables
+func _set(property, value) -> bool:
+ property = str(property)
+ var vars: Dictionary = dialogic.current_state_info['variables']
+ if property in vars.keys():
+ if typeof(vars[property]) != TYPE_DICTIONARY:
+ vars[property] = value
+ return true
+ if value is VariableFolder:
+ return true
+ return false
+
+
+## Allows to get dialogic built-in variables
+func _get(property):
+ property = str(property)
+ if property in dialogic.current_state_info['variables'].keys():
+ if typeof(dialogic.current_state_info['variables'][property]) == TYPE_DICTIONARY:
+ return VariableFolder.new(dialogic.current_state_info['variables'][property], property, self)
+ else:
+ return DialogicUtil.logical_convert(dialogic.current_state_info['variables'][property])
+
+
+func folders() -> Array:
+ var result := []
+ for i in dialogic.current_state_info['variables'].keys():
+ if dialogic.current_state_info['variables'][i] is Dictionary:
+ result.append(VariableFolder.new(dialogic.current_state_info['variables'][i], i, self))
+ return result
+
+
+func variables(_absolute:=false) -> Array:
+ var result := []
+ for i in dialogic.current_state_info['variables'].keys():
+ if not dialogic.current_state_info['variables'][i] is Dictionary:
+ result.append(i)
+ return result
+#endregion
+
+#region HELPERS
+################################################################################
+
+func get_autoloads() -> Dictionary:
+ var autoloads := {}
+ for node: Node in get_tree().root.get_children():
+ autoloads[node.name] = node
+ return autoloads
+
+
+func merge_folder(new:Dictionary, defs:Dictionary) -> Dictionary:
+ # also go through all groups in this folder
+ for x in new.keys():
+ if x in defs and typeof(new[x]) == TYPE_DICTIONARY:
+ new[x] = merge_folder(new[x], defs[x])
+ # add all new variables
+ for x in defs.keys():
+ if not x in new:
+ new[x] = defs[x]
+ return new
+
+#endregion
+
+#region VARIABLE FOLDER
+################################################################################
+class VariableFolder:
+ var data := {}
+ var path := ""
+ var outside: DialogicSubsystem
+
+ func _init(_data:Dictionary, _path:String, _outside:DialogicSubsystem):
+ data = _data
+ path = _path
+ outside = _outside
+
+
+ func _get(property:StringName):
+ property = str(property)
+ if property in data:
+ if typeof(data[property]) == TYPE_DICTIONARY:
+ return VariableFolder.new(data[property], path+"."+property, outside)
+ else:
+ return DialogicUtil.logical_convert(data[property])
+
+
+ func _set(property:StringName, value:Variant) -> bool:
+ property = str(property)
+ if not value is VariableFolder:
+ DialogicUtil._set_value_in_dictionary(path+"."+property, outside.dialogic.current_state_info['variables'], value)
+ return true
+
+
+ func has(key:String) -> bool:
+ return key in data
+
+
+ func folders() -> Array:
+ var result := []
+ for i in data.keys():
+ if data[i] is Dictionary:
+ result.append(VariableFolder.new(data[i], path+"."+i, outside))
+ return result
+
+
+ func variables(absolute:=false) -> Array:
+ var result := []
+ for i in data.keys():
+ if not data[i] is Dictionary:
+ if absolute:
+ result.append(path+'.'+i)
+ else:
+ result.append(i)
+ return result
+
+#endregion
--- /dev/null
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.9645 2.62927L12.8459 5.22444C12.6271 5.04546 12.3835 4.89134 12.1151 4.76208C11.8565 4.62287 11.5881 4.55327 11.3097 4.55327C11.0909 4.55327 10.8821 4.59802 10.6832 4.68751C10.4943 4.77699 10.3203 4.89631 10.1612 5.04546C10.0021 5.19461 9.86293 5.36364 9.74361 5.55256C9.6243 5.73154 9.52486 5.91052 9.44532 6.08949L9.14702 6.93964C9.35583 7.37714 9.55966 7.79475 9.75853 8.19248C9.92756 8.53055 10.1016 8.87359 10.2805 9.2216C10.4595 9.55966 10.6087 9.82813 10.728 10.027C10.9169 10.3153 11.1058 10.6136 11.2948 10.9219C11.4837 11.2202 11.6925 11.4936 11.9212 11.7422C12.0206 11.8516 12.1399 11.9261 12.2791 11.9659C12.4283 11.9957 12.5625 12.0107 12.6818 12.0107C12.8707 12.0107 13.0497 11.9858 13.2188 11.9361C13.3878 11.8864 13.5469 11.8217 13.696 11.7422L14.0689 12.2195C13.9297 12.4382 13.7607 12.657 13.5618 12.8757C13.3629 13.0945 13.1442 13.2933 12.9055 13.4723C12.6769 13.6513 12.4283 13.7955 12.1598 13.9048C11.9013 14.0242 11.6378 14.0838 11.3693 14.0838C11.1307 14.0838 10.9169 14.044 10.728 13.9645C10.549 13.8949 10.3849 13.8004 10.2358 13.6811C10.0867 13.5519 9.94745 13.4027 9.81819 13.2337C9.68893 13.0646 9.55469 12.8906 9.41549 12.7117C9.30611 12.5426 9.1868 12.3388 9.05753 12.1001C8.93822 11.8516 8.81393 11.598 8.68466 11.3395C8.5554 11.081 8.42614 10.8324 8.29688 10.5938C8.16762 10.3452 8.04333 10.1364 7.92401 9.96734C7.8743 10.1364 7.82955 10.3054 7.78978 10.4744C7.75001 10.6136 7.70526 10.7578 7.65555 10.907C7.60583 11.0561 7.56109 11.1705 7.52131 11.25C7.37216 11.5682 7.18324 11.8963 6.95455 12.2344C6.72586 12.5724 6.46236 12.8807 6.16407 13.1591C5.87572 13.4276 5.55753 13.6513 5.20952 13.8303C4.86151 13.9993 4.49361 14.0838 4.10583 14.0838C3.75782 14.0838 3.41975 14.0142 3.09162 13.875C2.7635 13.7457 2.46023 13.5717 2.18182 13.353L3.12145 10.8771C3.43964 11.076 3.78765 11.255 4.16549 11.4141C4.54333 11.5632 4.92117 11.6378 5.29901 11.6378C5.41833 11.6378 5.54262 11.6278 5.67188 11.608C5.80114 11.5781 5.92543 11.5384 6.04475 11.4886C6.17401 11.429 6.28836 11.3594 6.38779 11.2798C6.48722 11.1903 6.5618 11.0859 6.61151 10.9666C6.68111 10.8374 6.75569 10.6683 6.83523 10.4595C6.91478 10.2507 6.99432 10.0419 7.07387 9.8331C7.16336 9.59447 7.25285 9.34091 7.34234 9.07245L4.53836 4.5831C4.4091 4.43395 4.25001 4.31464 4.06109 4.22515C3.88211 4.12572 3.69319 4.076 3.49432 4.076C3.32529 4.076 3.1662 4.1108 3.01705 4.1804C2.8679 4.24006 2.72373 4.32458 2.58452 4.43395L2.18182 3.91194C2.32103 3.70313 2.48509 3.4993 2.67401 3.30043C2.87287 3.09162 3.08665 2.90768 3.31535 2.74859C3.54404 2.57955 3.78765 2.44532 4.04617 2.34589C4.30469 2.23651 4.56819 2.18182 4.83665 2.18182C5.16478 2.18182 5.46805 2.26634 5.74645 2.43537C6.02486 2.59447 6.28339 2.79333 6.52202 3.03197C6.76066 3.2706 6.97941 3.52912 7.17827 3.80753C7.37714 4.08594 7.55611 4.34447 7.7152 4.5831C7.79475 4.69248 7.87927 4.83168 7.96876 5.00072C8.06819 5.15981 8.16265 5.3189 8.25214 5.47799C8.36151 5.65697 8.46591 5.85086 8.56535 6.05966C8.66478 5.82103 8.76918 5.58239 8.87856 5.34376C8.96805 5.14489 9.05753 4.94106 9.14702 4.73225C9.24645 4.5135 9.33594 4.32458 9.41549 4.16549C9.56464 3.88708 9.73367 3.62856 9.92259 3.38992C10.1115 3.15128 10.3203 2.94248 10.549 2.7635C10.7876 2.58452 11.0462 2.44532 11.3246 2.34589C11.603 2.23651 11.9063 2.18182 12.2344 2.18182C12.5426 2.18182 12.8409 2.2216 13.1293 2.30114C13.4176 2.38069 13.696 2.49006 13.9645 2.62927Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+extends Tree
+
+enum TreeButtons {ADD_FOLDER, ADD_VARIABLE, DUPLICATE_FOLDER, DELETE, CHANGE_TYPE}
+
+@onready var editor: DialogicEditor = find_parent("VariablesEditor")
+
+
+#region INITIAL SETUP
+
+func _ready() -> void:
+ set_column_title(0, "Name")
+ set_column_title(1, "")
+ set_column_title(2, "Default Value")
+ set_column_expand(1, false)
+ set_column_expand_ratio(2, 2)
+ set_column_title_alignment(0, 0)
+ set_column_title_alignment(2, 0)
+
+ %ChangeTypePopup.self_modulate = get_theme_color("dark_color_3", "Editor")
+ %ChangeTypePopup.theme.set_stylebox('pressed', 'Button', get_theme_stylebox("LaunchPadMovieMode", "EditorStyles"))
+ %ChangeTypePopup.theme.set_stylebox('hover', 'Button', get_theme_stylebox("LaunchPadMovieMode", "EditorStyles"))
+ for child in %ChangeTypePopup/HBox.get_children():
+ child.toggled.connect(_on_type_pressed.bind(child.get_index()+1))
+ child.icon = get_theme_icon(["String", "float", "int", "bool"][child.get_index()], "EditorIcons")
+
+ %RightClickMenu.set_item_icon(0, get_theme_icon("ActionCopy", "EditorIcons"))
+#endregion
+
+
+#region POPULATING THE TREE
+
+func load_info(dict:Dictionary, parent:TreeItem = null, is_new:=false) -> void:
+ if parent == null:
+ clear()
+ parent = add_folder_item("VAR", null)
+
+ var sorted_keys := dict.keys()
+ sorted_keys.sort()
+ for key in sorted_keys:
+ if typeof(dict[key]) != TYPE_DICTIONARY:
+ var item := add_variable_item(key, dict[key], parent)
+ if is_new:
+ item.set_meta("new", true)
+
+ for key in sorted_keys:
+ if typeof(dict[key]) == TYPE_DICTIONARY:
+ var folder := add_folder_item(key, parent)
+ if is_new:
+ folder.set_meta("new", true)
+ load_info(dict[key], folder, is_new)
+
+
+
+func add_variable_item(name:String, value:Variant, parent:TreeItem) -> TreeItem:
+ var item := create_item(parent)
+ item.set_meta("type", "VARIABLE")
+
+ item.set_text(0, name)
+ item.set_editable(0, true)
+ item.set_metadata(0, name)
+ item.set_icon(0, load(DialogicUtil.get_module_path('Variable').path_join("variable.svg")))
+ var folder_color: Color = parent.get_meta('color', Color.DARK_GOLDENROD)
+ item.set_custom_bg_color(0, folder_color.lerp(get_theme_color("background", "Editor"), 0.8))
+
+ item.add_button(1, get_theme_icon("String", "EditorIcons"), TreeButtons.CHANGE_TYPE)
+ adjust_variable_type(item, DialogicUtil.get_variable_value_type(value), value)
+ item.set_editable(2, true)
+ item.add_button(2, get_theme_icon("Remove", "EditorIcons"), TreeButtons.DELETE)
+
+ item.set_meta('prev_path', get_item_path(item))
+ return item
+
+
+func add_folder_item(name:String, parent:TreeItem) -> TreeItem:
+ var item := create_item(parent)
+ item.set_icon(0, get_theme_icon("Folder", "EditorIcons"))
+ item.set_text(0, name)
+ item.set_editable(0, item != get_root())
+
+ var folder_color: Color
+ if parent == null:
+ folder_color = Color(0.33000001311302, 0.15179999172688, 0.15179999172688)
+ else:
+ folder_color = parent.get_meta('color')
+ folder_color.h = wrap(folder_color.h+0.15*(item.get_index()+1), 0, 1)
+ item.set_custom_bg_color(0, folder_color)
+ item.set_custom_bg_color(1, folder_color)
+ item.set_custom_bg_color(2, folder_color)
+ item.set_meta('color', folder_color)
+ item.add_button(2, load(self.get_script().get_path().get_base_dir().get_base_dir() + "/add-variable.svg"), TreeButtons.ADD_VARIABLE)
+ item.add_button(2, load("res://addons/dialogic/Editor/Images/Pieces/add-folder.svg"), TreeButtons.ADD_FOLDER)
+ item.add_button(2, get_theme_icon("Duplicate", "EditorIcons"), TreeButtons.DUPLICATE_FOLDER, item == get_root())
+ item.add_button(2, get_theme_icon("Remove", "EditorIcons"), TreeButtons.DELETE, item == get_root())
+ item.set_meta("type", "FOLDER")
+ return item
+
+
+#endregion
+
+
+#region EDITING THE TREE
+
+func set_variable_item_type(item:TreeItem, type:int) -> void:
+ item.set_meta('value_type', type)
+ item.set_button(1, 0, get_theme_icon(["Variant", "String", "float", "int", "bool"][type], "EditorIcons"))
+
+
+func get_variable_item_default(item:TreeItem) -> Variant:
+ match int(item.get_meta('value_type', DialogicUtil.VarTypes.STRING)):
+ DialogicUtil.VarTypes.STRING:
+ return item.get_text(2)
+ DialogicUtil.VarTypes.FLOAT:
+ return item.get_range(2)
+ DialogicUtil.VarTypes.INT:
+ return int(item.get_range(2))
+ DialogicUtil.VarTypes.BOOL:
+ return item.is_checked(2)
+ return ""
+
+
+func _on_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void:
+ match id:
+ TreeButtons.ADD_FOLDER:
+ var new_item := add_folder_item("Folder", item)
+ new_item.select(0)
+ new_item.set_meta("new", true)
+ await get_tree().process_frame
+ edit_selected()
+ TreeButtons.ADD_VARIABLE:
+ var new_item := add_variable_item("Var", "", item)
+ new_item.select(0)
+ new_item.set_meta("new", true)
+ await get_tree().process_frame
+ edit_selected()
+ TreeButtons.DELETE:
+ item.free()
+ TreeButtons.DUPLICATE_FOLDER:
+ load_info({item.get_text(0)+"(copy)":get_info(item)}, item.get_parent(), true)
+ TreeButtons.CHANGE_TYPE:
+ %ChangeTypePopup.show()
+ %ChangeTypePopup.set_meta('item', item)
+ %ChangeTypePopup.position = get_local_mouse_position()+Vector2(-%ChangeTypePopup.size.x/2, 10)
+ for child in %ChangeTypePopup/HBox.get_children():
+ child.set_pressed_no_signal(false)
+ %ChangeTypePopup/HBox.get_child(int(item.get_meta('value_type', DialogicUtil.VarTypes.STRING)-1)).set_pressed_no_signal(true)
+
+
+func _on_type_pressed(pressed:bool, type:int) -> void:
+ %ChangeTypePopup.hide()
+ var item: Variant = %ChangeTypePopup.get_meta('item')
+ adjust_variable_type(item, type, item.get_metadata(2))
+
+
+func _on_item_edited() -> void:
+ var item := get_edited()
+ match item.get_meta('type'):
+ "VARIABLE":
+ match get_edited_column():
+ 0:
+ if item.get_text(0).is_empty():
+ item.set_text(0, item.get_metadata(0))
+
+ else:
+ if item.get_text(0) != item.get_metadata(0):
+ item.set_metadata(0, item.get_text(0))
+ report_name_changes(item)
+
+ 2:
+ item.set_metadata(2, get_variable_item_default(item))
+ "FOLDER":
+ report_name_changes(item)
+
+
+func adjust_variable_type(item:TreeItem, type:int, prev_value:Variant) -> void:
+ set_variable_item_type(item, type)
+ match type:
+ DialogicUtil.VarTypes.STRING:
+ item.set_metadata(2, str(prev_value))
+ item.set_cell_mode(2, TreeItem.CELL_MODE_STRING)
+ item.set_text(2, str(prev_value))
+ DialogicUtil.VarTypes.FLOAT:
+ item.set_metadata(2, float(prev_value))
+ item.set_cell_mode(2, TreeItem.CELL_MODE_RANGE)
+ item.set_range_config(2, -9999, 9999, 0.001, false)
+ item.set_range(2, float(prev_value))
+ DialogicUtil.VarTypes.INT:
+ item.set_metadata(2, int(prev_value))
+ item.set_cell_mode(2, TreeItem.CELL_MODE_RANGE)
+ item.set_range_config(2, -9999, 9999, 1, false)
+ item.set_range(2, int(prev_value))
+ DialogicUtil.VarTypes.BOOL:
+ item.set_metadata(2, prev_value and true)
+ item.set_cell_mode(2, TreeItem.CELL_MODE_CHECK)
+ item.set_checked(2, prev_value and true)
+
+
+func _input(event):
+ if !%ChangeTypePopup.visible:
+ return
+ if event is InputEventMouseButton and event.pressed:
+ if not %ChangeTypePopup.get_global_rect().has_point(get_global_mouse_position()):
+ %ChangeTypePopup.hide()
+
+#endregion
+
+
+func filter(filter_term:String, item:TreeItem = null) -> bool:
+ if item == null:
+ item = get_root()
+
+ var any := false
+ for child in item.get_children():
+ match child.get_meta('type'):
+ "VARIABLE":
+ child.visible = filter_term.is_empty() or filter_term.to_lower() in child.get_text(0).to_lower()
+
+ "FOLDER":
+ child.visible = filter(filter_term, child)
+ if child.visible:
+ any = true
+ return any
+
+
+## Parses the tree and returns a dictionary representing it.
+func get_info(item:TreeItem = null) -> Dictionary:
+ if item == null:
+ item = get_root()
+ if item == null:
+ return {}
+
+ var dict := {}
+
+ for child in item.get_children():
+ match child.get_meta('type'):
+ "VARIABLE":
+ dict[child.get_text(0)] = child.get_metadata(2)
+ "FOLDER":
+ dict[child.get_text(0)] = get_info(child)
+
+ return dict
+
+
+#region DRAG AND DROP
+################################################################################
+
+func _get_drag_data(position:Vector2) -> Variant:
+ drop_mode_flags = DROP_MODE_INBETWEEN
+ var preview := Label.new()
+ preview.text = " "+get_selected().get_text(0)
+ preview.add_theme_stylebox_override('normal', get_theme_stylebox("Background", "EditorStyles"))
+ set_drag_preview(preview)
+
+ return get_selected()
+
+
+func _can_drop_data(position:Vector2, data:Variant) -> bool:
+ return data is TreeItem
+
+
+func _drop_data(position:Vector2, item:Variant) -> void:
+ var to_item := get_item_at_position(position)
+
+ if !to_item:
+ return
+
+ var drop_section := get_drop_section_at_position(position)
+ var parent: TreeItem = null
+ if (drop_section == 1 and to_item.get_meta('type') == "FOLDER") or to_item == get_root():
+ parent = to_item
+ else:
+ parent = to_item.get_parent()
+
+ ## Test for inheritance-recursion
+ var test_item := to_item
+ while true:
+ if test_item == item:
+ return
+ test_item = test_item.get_parent()
+ if test_item == get_root():
+ break
+
+ var new_item: TreeItem = null
+ match item.get_meta('type'):
+ "VARIABLE":
+ new_item = add_variable_item(item.get_text(0), item.get_metadata(2), parent)
+ new_item.set_meta('prev_path', get_item_path(item))
+ if item.get_meta("new", false):
+ new_item.set_meta("new", true)
+ "FOLDER":
+ new_item = add_folder_item(item.get_text(0), parent)
+ load_info(get_info(item), new_item)
+ if item.get_meta("new", false):
+ new_item.set_meta("new", true)
+
+ # If this was dropped on a variable (or the root node)
+ if to_item != parent:
+ if drop_section == -1:
+ new_item.move_before(to_item)
+ else:
+ new_item.move_after(to_item)
+
+ report_name_changes(new_item)
+
+ item.free()
+
+#endregion
+
+
+#region NAME CHANGES
+################################################################################
+
+func report_name_changes(item:TreeItem) -> void:
+
+ match item.get_meta('type'):
+ "VARIABLE":
+ if item.get_meta("new", false):
+ return
+ var new_path := get_item_path(item)
+ editor.variable_renamed(item.get_meta('prev_path'), new_path)
+ item.set_meta('prev_path', new_path)
+ "FOLDER":
+ for child in item.get_children():
+ report_name_changes(child)
+
+
+func get_item_path(item:TreeItem) -> String:
+ var path := item.get_text(0)
+ while item.get_parent() != get_root():
+ item = item.get_parent()
+ path = item.get_text(0)+"."+path
+ return path
+
+#endregion
+
+
+func _on_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MASK_RIGHT and event.pressed:
+ var item := get_item_at_position(get_local_mouse_position())
+ if item and item != get_root():
+ %RightClickMenu.popup_on_parent(Rect2(get_global_mouse_position(), Vector2()))
+ %RightClickMenu.set_item_text(0, 'Copy "' + get_item_path(item) + '"')
+ %RightClickMenu.set_meta("item", item)
+ %RightClickMenu.size = Vector2()
+
+
+func _on_right_click_menu_id_pressed(id: int) -> void:
+ if %RightClickMenu.get_meta("item", null) == null:
+ return
+ match id:
+ 0:
+ DisplayServer.clipboard_set(get_item_path(%RightClickMenu.get_meta("item")))
--- /dev/null
+@tool
+extends DialogicEditor
+
+## Editor that allows
+
+#region EDITOR STUFF
+
+func _get_title() -> String:
+ return "Variables"
+
+
+func _get_icon() -> Texture:
+ return load(self.get_script().get_path().get_base_dir().get_base_dir() + "/variable.svg")
+
+
+func _register() -> void:
+ editors_manager.register_simple_editor(self)
+ alternative_text = "Create and edit dialogic variables and their default values"
+
+
+func _open(argument:Variant = null):
+ %ReferenceInfo.hide()
+ %Tree.load_info(ProjectSettings.get_setting('dialogic/variables', {}))
+
+
+func _save() -> void:
+ ProjectSettings.set_setting('dialogic/variables', %Tree.get_info())
+ ProjectSettings.save()
+
+
+func _close() -> void:
+ _save()
+
+
+#endregion
+
+func _ready() -> void:
+ %ReferenceInfo.get_node('Label').add_theme_color_override('font_color', get_theme_color("warning_color", "Editor"))
+ %Search.right_icon = get_theme_icon("Search", "EditorIcons")
+
+#region RENAMING
+
+func variable_renamed(old_name:String, new_name:String):
+ if old_name == new_name:
+ return
+ editors_manager.reference_manager.add_variable_ref_change(old_name, new_name)
+ %ReferenceInfo.show()
+
+
+func _on_reference_manager_pressed() -> void:
+ editors_manager.reference_manager.open()
+ %ReferenceInfo.hide()
+
+#endregion
+
+
+func _on_search_text_changed(new_text: String) -> void:
+ %Tree.filter(new_text)
--- /dev/null
+[gd_scene load_steps=10 format=3 uid="uid://6tdle4y5o03o"]
+
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Variable/variables_editor/variables_editor.gd" id="2"]
+[ext_resource type="Script" path="res://addons/dialogic/Modules/Variable/variables_editor/variable_tree.gd" id="2_1i17i"]
+
+[sub_resource type="Image" id="Image_e1dkh"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_sr7s6"]
+image = SubResource("Image_e1dkh")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7aodm"]
+content_margin_left = 2.0
+content_margin_top = 0.0
+content_margin_right = 2.0
+content_margin_bottom = 0.0
+bg_color = Color(0.44, 0.73, 0.98, 0.1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.44, 0.73, 0.98, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 3
+anti_aliasing = false
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_vn21i"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ffuxp"]
+content_margin_bottom = 0.0
+bg_color = Color(0.44, 0.73, 0.98, 0.1)
+border_width_bottom = 2
+border_color = Color(0.44, 0.73, 0.98, 1)
+expand_margin_left = 4.0
+expand_margin_top = 2.0
+expand_margin_right = 4.0
+expand_margin_bottom = 4.0
+anti_aliasing = false
+
+[sub_resource type="Theme" id="Theme_17j6i"]
+Button/styles/hover = SubResource("StyleBoxFlat_7aodm")
+Button/styles/normal = SubResource("StyleBoxEmpty_vn21i")
+Button/styles/pressed = SubResource("StyleBoxFlat_7aodm")
+pressed/styles/Button = SubResource("StyleBoxFlat_ffuxp")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ncgqs"]
+bg_color = Color(1, 1, 1, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[node name="VariablesEditor" type="HSplitContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2")
+
+[node name="Editor" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBox" type="HBoxContainer" parent="Editor"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="Editor/HBox"]
+layout_mode = 2
+theme_type_variation = &"DialogicSubTitle"
+text = "Variables"
+
+[node name="Search" type="LineEdit" parent="Editor/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Search"
+right_icon = SubResource("ImageTexture_sr7s6")
+
+[node name="Tree" type="Tree" parent="Editor"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_type_variation = &"DialogicPanelB"
+theme_override_constants/button_margin = 4
+theme_override_constants/draw_guides = 1
+columns = 3
+column_titles_visible = true
+script = ExtResource("2_1i17i")
+
+[node name="ChangeTypePopup" type="PanelContainer" parent="Editor/Tree"]
+unique_name_in_owner = true
+visible = false
+self_modulate = Color(0, 0, 0, 1)
+layout_mode = 0
+offset_left = 140.0
+offset_top = 160.0
+offset_right = 272.0
+offset_bottom = 190.0
+theme = SubResource("Theme_17j6i")
+theme_override_styles/panel = SubResource("StyleBoxFlat_ncgqs")
+
+[node name="HBox" type="HBoxContainer" parent="Editor/Tree/ChangeTypePopup"]
+layout_mode = 2
+
+[node name="String" type="Button" parent="Editor/Tree/ChangeTypePopup/HBox"]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+tooltip_text = "String (Any text)"
+toggle_mode = true
+icon = SubResource("ImageTexture_sr7s6")
+icon_alignment = 1
+
+[node name="Float" type="Button" parent="Editor/Tree/ChangeTypePopup/HBox"]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+tooltip_text = "Float (Number with Decimals)"
+toggle_mode = true
+icon = SubResource("ImageTexture_sr7s6")
+icon_alignment = 1
+
+[node name="Int" type="Button" parent="Editor/Tree/ChangeTypePopup/HBox"]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+tooltip_text = "Int (Integer)"
+toggle_mode = true
+icon = SubResource("ImageTexture_sr7s6")
+icon_alignment = 1
+
+[node name="Bool" type="Button" parent="Editor/Tree/ChangeTypePopup/HBox"]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+tooltip_text = "Bool (True/False flag)"
+toggle_mode = true
+icon = SubResource("ImageTexture_sr7s6")
+icon_alignment = 1
+
+[node name="RightClickMenu" type="PopupMenu" parent="Editor/Tree"]
+unique_name_in_owner = true
+size = Vector2i(67, 35)
+item_count = 1
+item_0/text = "Copy"
+item_0/id = 0
+
+[node name="ReferenceInfo" type="HBoxContainer" parent="Editor"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Editor/ReferenceInfo"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "You've made some changes to existing variables! Use the reference manager to check if something broke."
+autowrap_mode = 3
+
+[node name="ReferenceManager" type="Button" parent="Editor/ReferenceInfo"]
+layout_mode = 2
+text = "Reference Manager"
+
+[node name="Info" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBox" type="HBoxContainer" parent="Info"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="Info/HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_type_variation = &"DialogicSection"
+text = "How to use variables"
+
+[node name="Documentation" type="LinkButton" parent="Info/HBox"]
+layout_mode = 2
+text = "Read the Docs"
+uri = "https://docs.dialogic.pro/variables.html"
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Info"]
+layout_mode = 2
+text = "Variables are good way to keep track of all kinds of things during your game. Dialogic has an easy-to-use and beginner friendly variable system built in. However Dialogic allows to use outside variables (of Autoload Singletons) just as easily. You can also access the Dialogic variables from outside scripts."
+fit_content = true
+
+[connection signal="text_changed" from="Editor/HBox/Search" to="." method="_on_search_text_changed"]
+[connection signal="button_clicked" from="Editor/Tree" to="Editor/Tree" method="_on_button_clicked"]
+[connection signal="gui_input" from="Editor/Tree" to="Editor/Tree" method="_on_gui_input"]
+[connection signal="item_edited" from="Editor/Tree" to="Editor/Tree" method="_on_item_edited"]
+[connection signal="id_pressed" from="Editor/Tree/RightClickMenu" to="Editor/Tree" method="_on_right_click_menu_id_pressed"]
+[connection signal="pressed" from="Editor/ReferenceInfo/ReferenceManager" to="." method="_on_reference_manager_pressed"]
--- /dev/null
+@tool
+class_name DialogicVoiceEvent
+extends DialogicEvent
+
+## Event that allows to set the sound file to use for the next text event.
+
+
+### Settings
+
+## The path to the sound file.
+var file_path := "":
+ set(value):
+ if file_path != value:
+ file_path = value
+ ui_update_needed.emit()
+## The volume the sound will be played at.
+var volume: float = 0
+## The audio bus to play the sound on.
+var audio_bus := "Master"
+
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ # If Auto-Skip is enabled, we may not want to play voice audio.
+ # Instant Auto-Skip will always skip voice audio.
+ if (dialogic.Inputs.auto_skip.enabled
+ and dialogic.Inputs.auto_skip.skip_voice):
+ finish()
+ return
+
+ dialogic.Voice.set_file(file_path)
+ dialogic.Voice.set_volume(volume)
+ dialogic.Voice.set_bus(audio_bus)
+ finish()
+ # the rest is executed by a text event
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Voice"
+ set_default_color('Color7')
+ event_category = "Audio"
+ event_sorting_index = 5
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "voice"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "path" : {"property": "file_path", "default": ""},
+ "volume" : {"property": "volume", "default": 0},
+ "bus" : {"property": "audio_bus", "default": "Master"}
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('file_path', ValueType.FILE, {
+ 'left_text' : 'Set',
+ 'right_text' : 'as the next voice audio',
+ 'file_filter' : "*.mp3, *.ogg, *.wav",
+ 'placeholder' : "Select file",
+ 'editor_icon' : ["AudioStreamPlayer", "EditorIcons"]})
+ add_header_edit('file_path', ValueType.AUDIO_PREVIEW)
+ add_body_edit('volume', ValueType.NUMBER, {'left_text':'Volume:', 'mode':2}, '!file_path.is_empty()')
+ add_body_edit('audio_bus', ValueType.SINGLELINE_TEXT, {'left_text':'Audio Bus:'}, '!file_path.is_empty()')
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_voice.gd')]
+
+
+func _get_subsystems() -> Array:
+ return [{'name':'Voice', 'script':this_folder.path_join('subsystem_voice.gd')}]
--- /dev/null
+extends DialogicSubsystem
+## Subsystem that manages setting voice lines for text events.
+##
+## It's recommended to use the [class DialogicVoiceEvent] to set the voice lines
+## for text events and not start playing them directly.
+
+
+## Emitted whenever a new voice line starts playing.
+## The [param info] contains the following keys and values:
+## [br]
+## Key | Value Type | Value [br]
+## -------- | ------------- | ----- [br]
+## `file` | [type String] | The path to file played. [br]
+signal voiceline_started(info: Dictionary)
+
+
+## Emitted whenever a voice line finished playing.
+## The [param info] contains the following keys and values:
+## [br]
+## Key | Value Type | Value [br]
+## ---------------- | ------------- | ----- [br]
+## `file` | [type String] | The path to file played. [br]
+## `remaining_time` | [type float] | The remaining time of the voiceline. [br]
+signal voiceline_finished(info: Dictionary)
+
+
+## Emitted whenever a voice line gets interrupted and does not finish playing.
+## The [param info] contains the following keys and values:
+## [br]
+## Key | Value Type | Value [br]
+## ---------------- | ------------- | ----- [br]
+## `file` | [type String] | The path to file played. [br]
+## `remaining_time` | [type float] | The remaining time of the voiceline. [br]
+signal voiceline_stopped(info: Dictionary)
+
+
+## The current audio file being played.
+var current_audio_file: String
+
+## The audio player for the voiceline.
+var voice_player := AudioStreamPlayer.new()
+
+#region STATE
+####################################################################################################
+
+## Stops the current voice from playing.
+func pause() -> void:
+ voice_player.stream_paused = true
+
+
+## Resumes a paused voice.
+func resume() -> void:
+ voice_player.stream_paused = false
+
+#endregion
+
+
+#region MAIN METHODS
+####################################################################################################
+
+func _ready() -> void:
+ add_child(voice_player)
+ voice_player.finished.connect(_on_voice_finished)
+
+
+## Whether the current event is a text event and has a voice
+## event before it.
+func is_voiced(index: int) -> bool:
+ if index > 0 and dialogic.current_timeline_events[index] is DialogicTextEvent:
+ if dialogic.current_timeline_events[index-1] is DialogicVoiceEvent:
+ return true
+
+ return false
+
+
+## Plays the voice line. This will be invoked by Dialogic.
+## Requires [method set_file] to be called before or nothing plays.
+func play_voice() -> void:
+ voice_player.play()
+ voiceline_started.emit({'file': current_audio_file})
+
+
+## Set a voice file [param path] to be played, then invoke [method play_voice].
+##
+## This method does not check if [param path] is a valid file.
+func set_file(path: String) -> void:
+ if current_audio_file == path:
+ return
+
+ current_audio_file = path
+ var audio: AudioStream = load(path)
+ voice_player.stream = audio
+
+
+## Set the volume to a [param value] in decibels.
+func set_volume(value: float) -> void:
+ voice_player.volume_db = value
+
+
+## Set the voice player's bus to a [param bus_name].
+func set_bus(bus_name: String) -> void:
+ voice_player.bus = bus_name
+
+
+## Stops the current voice line from playing.
+func stop_audio() -> void:
+ if voice_player.playing:
+ voiceline_stopped.emit({'file':current_audio_file, 'remaining_time':get_remaining_time()})
+
+ voice_player.stop()
+
+
+## Called when the voice line finishes playing.
+## Connected to [signal finished] on [member voice_player]
+func _on_voice_finished() -> void:
+ voiceline_finished.emit({'file':current_audio_file, 'remaining_time':get_remaining_time()})
+
+
+## Returns the remaining time of the current voice line in seconds.
+##
+## If there is no voice line playing, returns `0`.
+func get_remaining_time() -> float:
+ if not voice_player or not voice_player.playing:
+ return 0.0
+
+ var stream_length := voice_player.stream.get_length()
+ var playback_position := voice_player.get_playback_position()
+ var remaining_seconds := stream_length - playback_position
+
+ return remaining_seconds
+
+
+## Whether there is still positive time remaining for the current voiceline.
+func is_running() -> bool:
+ return get_remaining_time() > 0.0
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicWaitEvent
+extends DialogicEvent
+
+## Event that waits for some time before continuing.
+
+
+### Settings
+
+## The time in seconds that the event will stop before continuing.
+var time: float = 1.0
+## If true the text box will be hidden while the event waits.
+var hide_text := true
+## If true the wait can be skipped with user input
+var skippable := false
+
+var _tween: Tween
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ var final_wait_time := time
+
+ if dialogic.Inputs.auto_skip.enabled:
+ var time_per_event: float = dialogic.Inputs.auto_skip.time_per_event
+ final_wait_time = min(time, time_per_event)
+
+ dialogic.current_state = dialogic.States.WAITING
+
+ if hide_text and dialogic.has_subsystem("Text"):
+ dialogic.Text.update_dialog_text('', true)
+ dialogic.Text.hide_textbox()
+
+ _tween = dialogic.get_tree().create_tween()
+ if DialogicUtil.is_physics_timer():
+ _tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
+ _tween.tween_callback(_on_finish).set_delay(final_wait_time)
+
+ if skippable:
+ dialogic.Inputs.dialogic_action.connect(_on_finish)
+
+
+func _on_finish() -> void:
+ if is_instance_valid(_tween):
+ _tween.kill()
+
+ if skippable:
+ dialogic.Inputs.dialogic_action.disconnect(_on_finish)
+
+ if dialogic.Animations.is_animating():
+ dialogic.Animations.stop_animation()
+ dialogic.current_state = dialogic.States.IDLE
+
+ finish()
+
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Wait"
+ set_default_color('Color5')
+ event_category = "Flow"
+ event_sorting_index = 11
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "wait"
+
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "time" : {"property": "time", "default": 1},
+ "hide_text" : {"property": "hide_text", "default": true},
+ "skippable" : {"property": "skippable", "default": false},
+ }
+
+
+################################################################################
+## EDITOR REPRESENTATION
+################################################################################
+
+func build_event_editor() -> void:
+ add_header_edit('time', ValueType.NUMBER, {'left_text':'Wait', 'autofocus':true, 'min':0.1})
+ add_header_label('seconds', 'time != 1')
+ add_header_label('second', 'time == 1')
+ add_body_edit('hide_text', ValueType.BOOL, {'left_text':'Hide text box:'})
+ add_body_edit('skippable', ValueType.BOOL, {'left_text':'Skippable:'})
--- /dev/null
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.1667 8C10.0117 8.00015 9.85836 8.0305 9.7155 8.0893C9.71473 8.0893 9.714 8.0893 9.71322 8.0893C9.71058 8.0904 9.70902 8.09265 9.70638 8.09375C9.57136 8.15035 9.44842 8.23135 9.34409 8.3326C9.34259 8.33405 9.34103 8.3356 9.33953 8.33705C9.23852 8.43765 9.15737 8.55575 9.10028 8.6853C9.09676 8.6931 9.09219 8.6997 9.08887 8.7076C9.03049 8.8456 9.00031 8.9935 9 9.14285C9 10.8572 10.1281 12.1425 11.1852 13.326C12.0728 14.3197 12.8373 15.2031 13.3727 16C12.8373 16.7971 12.0729 17.6827 11.1852 18.6764C10.1281 19.8598 9 21.1428 9 22.8571C8.99985 23.0097 9.0308 23.1607 9.09116 23.3013V23.3036C9.09225 23.3061 9.09458 23.3078 9.09572 23.3103C9.15395 23.441 9.23665 23.5598 9.33953 23.6607C9.34072 23.6619 9.3429 23.6663 9.34409 23.6674C9.44562 23.7651 9.56456 23.8438 9.69497 23.8995C9.70586 23.9042 9.71582 23.9086 9.72686 23.913C9.86634 23.9696 10.0157 23.9992 10.1667 24H21.8333C21.9834 23.9995 22.1319 23.9707 22.2708 23.9152C22.2827 23.9105 22.2934 23.9046 22.305 23.8995C22.4319 23.8442 22.5476 23.7671 22.6468 23.6719C22.6524 23.6666 22.6573 23.6617 22.6628 23.6562C22.761 23.5586 22.8405 23.4445 22.8974 23.3192C22.9023 23.3086 22.9067 23.2988 22.9111 23.288C22.9689 23.1514 22.9991 23.005 23 22.8571C23 21.1428 21.8719 19.8597 20.8148 18.6764C19.9272 17.6827 19.1627 16.7971 18.6273 16C19.1627 15.2031 19.9272 14.3197 20.8148 13.326C21.8719 12.1425 23 10.8572 23 9.14285C23.0005 8.98955 22.9695 8.83775 22.9088 8.69645C22.8498 8.56125 22.7647 8.43845 22.6582 8.33485C22.5541 8.2328 22.4311 8.151 22.2959 8.09375C22.2913 8.0915 22.2868 8.08925 22.2822 8.08705C22.1399 8.0293 21.9873 7.9997 21.8333 8H10.1667ZM12.0192 10.2858H19.9808C19.7034 10.7895 19.6244 11.1877 19.0602 11.8192C18.0756 12.9215 16.8455 14.0733 16.123 15.4888C16.042 15.6476 15.9999 15.8226 16 16C16 15.8226 15.9579 15.6476 15.8769 15.4888C15.1544 14.0733 13.9243 12.9215 12.9397 11.8192C12.3756 11.1877 12.2966 10.7895 12.0192 10.2858H12.0192Z" fill="white"/>
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_wait.gd')]
--- /dev/null
+@tool
+class_name DialogicWaitInputEvent
+extends DialogicEvent
+
+## Event that waits for input before continuing.
+
+var hide_textbox := true
+
+################################################################################
+## EXECUTE
+################################################################################
+
+func _execute() -> void:
+ if hide_textbox:
+ dialogic.Text.hide_textbox()
+ dialogic.current_state = DialogicGameHandler.States.IDLE
+ dialogic.Inputs.auto_skip.enabled = false
+ await dialogic.Inputs.dialogic_action
+ finish()
+
+################################################################################
+## INITIALIZE
+################################################################################
+
+func _init() -> void:
+ event_name = "Wait for Input"
+ set_default_color('Color5')
+ event_category = "Flow"
+ event_sorting_index = 12
+
+
+################################################################################
+## SAVING/LOADING
+################################################################################
+
+func get_shortcode() -> String:
+ return "wait_input"
+
+func get_shortcode_parameters() -> Dictionary:
+ return {
+ #param_name : property_info
+ "hide_text" : {"property": "hide_textbox", "default": true},
+ }
+
+
+func build_event_editor() -> void:
+ add_header_label('Wait for input')
+ add_body_edit('hide_textbox', ValueType.BOOL, {'left_text':'Hide text box:'})
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="settings-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview id="namedview7" pagecolor="#464646" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="1.5898438" inkscape:cx="-154.41769" inkscape:cy="-35.223587" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
+ <defs id="defs2" />
+ <path id="rect32348" style="stroke-width:1.69703;stroke-dasharray:none;fill:#ffffff;fill-opacity:0.75;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" d="M -74.036184,-5.4987402 V 7.6914445 c 0,9.0790395 7.310309,16.3876505 16.389347,16.3876505 9.079038,0 16.387651,-7.308611 16.387651,-16.3876505 V -5.4987402 Z" transform="matrix(0.23338964,0,0,0.23338964,22.487038,9.9966238)" />
+ <path id="path35376" style="stroke-width:1.69703;stroke-dasharray:none;fill:#ffffff;fill-opacity:0.75;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" d="m -56.294911,-25.026183 v 6.476353 c 0.897286,0.480984 1.504591,1.425799 1.504591,2.518959 v 2.858213 c 0,1.093174 -0.607286,2.037981 -1.504591,2.518959 v 2.4477148 h 15.035725 v -0.491918 c 0,-8.6228998 -6.595011,-15.6440248 -15.035725,-16.3282808 z" transform="matrix(0.23338964,0,0,0.23338964,22.487038,9.9966238)" />
+ <path id="path35374" style="stroke-width:1.69703;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" d="m -58.998763,-25.026183 c -8.441517,0.683458 -15.037421,7.704808 -15.037421,16.3282808 v 0.491918 h 15.037421 v -2.4460188 c -0.898402,-0.480644 -1.506287,-1.426671 -1.506287,-2.520655 v -2.858213 c 0,-1.09397 0.607904,-2.040005 1.506287,-2.520655 z" transform="matrix(0.23338964,0,0,0.23338964,22.487038,9.9966238)" />
+ <rect style="stroke-width:0.910726;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" id="rect38528" width="5.0777411" height="13.134425" x="-43.094013" y="-74.754066" ry="4.3330054" transform="matrix(0.18747781,-0.13900645,0.13900645,0.18747781,22.487038,9.9966238)" />
+ <rect style="stroke-width:0.910726;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" id="rect38530" width="5.0777411" height="13.134425" x="-63.386398" y="-47.359566" ry="4.3330054" transform="matrix(0.23186377,-0.02664421,0.02664421,0.23186377,22.487038,9.9966238)" />
+ <rect style="stroke-width:0.910726;stroke-dasharray:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" id="rect38532" width="5.0777411" height="13.134425" x="-11.588428" y="-88.045616" ry="4.3330054" transform="matrix(0.09285733,-0.21412202,0.21412202,0.09285733,22.487038,9.9966238)" />
+</svg>
--- /dev/null
+@tool
+extends DialogicIndexer
+
+
+func _get_events() -> Array:
+ return [this_folder.path_join('event_wait_input.gd')]
--- /dev/null
+@tool
+class_name DialogicCharacterFormatLoader
+extends ResourceFormatLoader
+
+
+
+## Returns all excepted extenstions
+func _get_recognized_extensions() -> PackedStringArray:
+ return PackedStringArray(["dch"])
+
+
+## Returns "Resource" if this file can/should be loaded by this script
+func _get_resource_type(path: String) -> String:
+ var ext := path.get_extension().to_lower()
+ if ext == "dch":
+ return "Resource"
+
+ return ""
+
+
+## Returns the script class associated with a Resource
+func _get_resource_script_class(path: String) -> String:
+ var ext := path.get_extension().to_lower()
+ if ext == "dch":
+ return "DialogicCharacter"
+
+ return ""
+
+
+## Return true if this type is handled
+func _handles_type(typename: StringName) -> bool:
+ return ClassDB.is_parent_class(typename, "Resource")
+
+
+## Parse the file and return a resource
+func _load(path: String, _original_path: String, _use_sub_threads: bool, _cache_mode: int) -> Variant:
+# print('[Dialogic] Reimporting character "' , path, '"')
+ var file := FileAccess.open(path, FileAccess.READ)
+
+ if not file:
+ # For now, just let editor know that for some reason you can't
+ # read the file.
+ print("[Dialogic] Error opening file:", FileAccess.get_open_error())
+ return FileAccess.get_open_error()
+
+ return dict_to_inst(str_to_var(file.get_as_text()))
+
+
+func _get_dependencies(path:String, _add_type:bool) -> PackedStringArray:
+ var depends_on: PackedStringArray = []
+ var character: DialogicCharacter = load(path)
+ for p in character.portraits.values():
+ if 'path' in p and p.path:
+ depends_on.append(p.path)
+ return depends_on
+
+
+func _rename_dependencies(path: String, renames: Dictionary) -> Error:
+ var character: DialogicCharacter = load(path)
+ for p in character.portraits:
+ if 'path' in character.portraits[p] and character.portraits[p].path in renames:
+ character.portraits[p].path = renames[character.portraits[p].path]
+ ResourceSaver.save(character, path)
+ return OK
--- /dev/null
+@tool
+class_name DialogicCharacterFormatSaver
+extends ResourceFormatSaver
+
+
+func _get_recognized_extensions(_resource: Resource) -> PackedStringArray:
+ return PackedStringArray(["dch"])
+
+
+## Return true if this resource should be loaded as a DialogicCharacter
+func _recognize(resource: Resource) -> bool:
+ # Cast instead of using "is" keyword in case is a subclass
+ resource = resource as DialogicCharacter
+
+ if resource:
+ return true
+
+ return false
+
+
+## Save the resource
+func _save(resource: Resource, path: String = '', _flags: int = 0) -> Error:
+ var file := FileAccess.open(path, FileAccess.WRITE)
+
+ if not file:
+ # For now, just let editor know that for some reason you can't
+ # read the file.
+ print("[Dialogic] Error opening file:", FileAccess.get_open_error())
+ return FileAccess.get_open_error()
+
+ var result := var_to_str(inst_to_dict(resource))
+ file.store_string(result)
+# print('[Dialogic] Saved character "' , path, '"')
+ return OK
--- /dev/null
+@tool
+class_name DialogicTimelineFormatLoader
+extends ResourceFormatLoader
+
+
+## Returns all excepted extenstions
+func _get_recognized_extensions() -> PackedStringArray:
+ return PackedStringArray(["dtl"])
+
+
+## Returns "Resource" if this file can/should be loaded by this script
+func _get_resource_type(path: String) -> String:
+ var ext := path.get_extension().to_lower()
+ if ext == "dtl":
+ return "Resource"
+
+ return ""
+
+
+## Returns the script class associated with a Resource
+func _get_resource_script_class(path: String) -> String:
+ var ext := path.get_extension().to_lower()
+ if ext == "dtl":
+ return "DialogicTimeline"
+
+ return ""
+
+
+## Return true if this type is handled
+func _handles_type(typename: StringName) -> bool:
+ return ClassDB.is_parent_class(typename, "Resource")
+
+
+## Parse the file and return a resource
+func _load(path: String, _original_path: String, _use_sub_threads: bool, _cache_mode: int) -> Variant:
+ var file := FileAccess.open(path, FileAccess.READ)
+
+ if not file:
+ # For now, just let editor know that for some reason you can't
+ # read the file.
+ print("[Dialogic] Error opening file:", FileAccess.get_open_error())
+ return FileAccess.get_open_error()
+
+ var tml := DialogicTimeline.new()
+ tml.from_text(file.get_as_text())
+ return tml
--- /dev/null
+@tool
+class_name DialogicTimelineFormatSaver
+extends ResourceFormatSaver
+
+
+func _get_recognized_extensions(_resource: Resource) -> PackedStringArray:
+ return PackedStringArray(["dtl"])
+
+
+## Return true if this resource should be loaded as a DialogicTimeline
+func _recognize(resource: Resource) -> bool:
+ # Cast instead of using "is" keyword in case is a subclass
+ resource = resource as DialogicTimeline
+
+ if resource:
+ return true
+
+ return false
+
+
+## Save the resource
+## TODO: This should use timeline.as_text(), why is this still here?
+func _save(resource: Resource, path: String = '', _flags: int = 0) -> Error:
+ if resource.get_meta("timeline_not_saved", false):
+
+ var timeline_as_text := ""
+ # if events are resources, create text
+ if resource.events_processed:
+
+ var indent := 0
+ for idx in range(0, len(resource.events)):
+ if resource.events[idx]:
+ var event: DialogicEvent = resource.events[idx]
+ if event.event_name == 'End Branch':
+ indent -=1
+ continue
+
+ for i in event.empty_lines_above:
+ timeline_as_text += '\t'.repeat(indent) + '\n'
+
+ if event != null:
+ timeline_as_text += "\t".repeat(indent)+ event.event_node_as_text + "\n"
+ if event.can_contain_events:
+ indent += 1
+ if indent < 0:
+ indent = 0
+
+ # if events are string lines, just save them
+ else:
+ for event in resource.events:
+ timeline_as_text += event + "\n"
+
+ # Now do the actual saving
+ var file := FileAccess.open(path, FileAccess.WRITE)
+ if !file:
+ print("[Dialogic] Error opening file:", FileAccess.get_open_error())
+ return ERR_CANT_OPEN
+ file.store_string(timeline_as_text)
+ file.close()
+
+ return OK
--- /dev/null
+@tool
+extends Resource
+class_name DialogicCharacter
+
+
+## Resource that represents a character in dialog.
+## Manages/contains portraits, custom info and translation of characters.
+
+@export var display_name := ""
+@export var nicknames := []
+
+@export var color := Color()
+@export var description := ""
+
+@export var scale := 1.0
+@export var offset := Vector2()
+@export var mirror := false
+
+@export var default_portrait := ""
+@export var portraits := {}
+
+@export var custom_info := {}
+
+## All valid properties that can be accessed by their translation.
+enum TranslatedProperties {
+ NAME,
+ NICKNAMES,
+}
+
+var _translation_id := ""
+
+
+func _to_string() -> String:
+ return "[{name}:{id}]".format({"name":get_character_name(), "id":get_instance_id()})
+
+
+## Adds a translation ID to the character.
+func add_translation_id() -> String:
+ _translation_id = DialogicUtil.get_next_translation_id()
+ return _translation_id
+
+
+## Returns the character's translation ID.
+## Adds a translation ID to the character if it doesn't have one.
+func get_set_translation_id() -> String:
+ if _translation_id == null or _translation_id.is_empty():
+ return add_translation_id()
+ else:
+ return _translation_id
+
+
+## Removes the translation ID from the character.
+func remove_translation_id() -> void:
+ _translation_id = ""
+
+
+## Checks [param property] and matches it to a translation key.
+##
+## Undefined behaviour if an invalid integer is passed.
+func get_property_translation_key(property: TranslatedProperties) -> String:
+ var property_key := ""
+
+ match property:
+ TranslatedProperties.NAME:
+ property_key = "name"
+ TranslatedProperties.NICKNAMES:
+ property_key = "nicknames"
+
+ return "Character".path_join(_translation_id).path_join(property_key)
+
+
+## Accesses the original text of the character.
+##
+## Undefined behaviour if an invalid integer is passed.
+func _get_property_original_text(property: TranslatedProperties) -> String:
+ match property:
+ TranslatedProperties.NAME:
+ return display_name
+ TranslatedProperties.NICKNAMES:
+ return ", ".join(nicknames)
+
+ return ""
+
+
+## Access a property of the character and if conditions are met, attempts to
+## translate the property.
+##
+## The translation feature must be enabled in the project settings.
+## The translation ID must be set.
+## Otherwise, returns the text property as is.
+##
+## Undefined behaviour if an invalid integer is passed.
+func _get_property_translated(property: TranslatedProperties) -> String:
+ var try_translation: bool = (_translation_id != null
+ and not _translation_id.is_empty()
+ and ProjectSettings.get_setting('dialogic/translation/enabled', false)
+ )
+
+ if try_translation:
+ var translation_key := get_property_translation_key(property)
+ var translated_property := tr(translation_key)
+
+ # If no translation is found, tr() returns the ID.
+ # However, we want to fallback to the original text.
+ if translated_property == translation_key:
+ return _get_property_original_text(property)
+
+ return translated_property
+
+ else:
+ return _get_property_original_text(property)
+
+
+## Translates the nicknames of the characters and then returns them as an array
+## of strings.
+func get_nicknames_translated() -> Array:
+ var translated_nicknames := _get_property_translated(TranslatedProperties.NICKNAMES)
+ return (translated_nicknames.split(", ") as Array)
+
+
+## Translates and returns the display name of the character.
+func get_display_name_translated() -> String:
+ return _get_property_translated(TranslatedProperties.NAME)
+
+
+## Returns the best name for this character.
+func get_character_name() -> String:
+ var unique_identifier := DialogicResourceUtil.get_unique_identifier(resource_path)
+ if not unique_identifier.is_empty():
+ return unique_identifier
+ if not resource_path.is_empty():
+ return resource_path.get_file().trim_suffix('.dch')
+ elif not display_name.is_empty():
+ return display_name.validate_node_name()
+ else:
+ return "UnnamedCharacter"
+
+
+## Returns the info of the given portrait.
+## Uses the default portrait if the given portrait doesn't exist.
+func get_portrait_info(portrait_name:String) -> Dictionary:
+ return portraits.get(portrait_name, portraits.get(default_portrait, {}))
--- /dev/null
+@tool
+class_name DialogicLayoutBase
+extends Node
+
+## Base class that should be extended by custom layouts.
+
+
+## Method that adds a node as a layer
+func add_layer(layer:DialogicLayoutLayer) -> Node:
+ add_child(layer)
+ return layer
+
+
+## Method that returns the given child
+func get_layer(index:int) -> Node:
+ return get_child(index)
+
+
+## Method to return all the layers
+func get_layers() -> Array:
+ var layers := []
+ for child in get_children():
+ if child is DialogicLayoutLayer:
+ layers.append(child)
+ return layers
+
+
+## Method that is called to load the export overrides.
+## This happens when the style is first introduced,
+## but also when switching to a different style using the same scene!
+func apply_export_overrides() -> void:
+ _apply_export_overrides()
+ for child in get_children():
+ if child.has_method('_apply_export_overrides'):
+ child._apply_export_overrides()
+
+
+## Returns a setting on this base.
+## This is useful so that layers can share settings like base_color, etc.
+func get_global_setting(setting:StringName, default:Variant) -> Variant:
+ if setting in self:
+ return get(setting)
+
+ if str(setting).to_lower() in self:
+ return get(setting.to_lower())
+
+ if 'global_'+str(setting) in self:
+ return get('global_'+str(setting))
+
+ return default
+
+
+## To be overwritten. Apply the settings to your scene here.
+func _apply_export_overrides() -> void:
+ pass
+
+
+#region HANDLE PERSISTENT DATA
+################################################################################
+
+func _enter_tree() -> void:
+ _load_persistent_info(Engine.get_meta("dialogic_persistent_style_info", {}))
+
+
+func _exit_tree() -> void:
+ Engine.set_meta("dialogic_persistent_style_info", _get_persistent_info())
+
+
+## To be overwritten. Return any info that a later used style might want to know.
+func _get_persistent_info() -> Dictionary:
+ return {}
+
+
+## To be overwritten. Apply any info that a previous style might have stored and this style should use.
+func _load_persistent_info(info: Dictionary) -> void:
+ pass
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicLayoutLayer
+extends Node
+
+## Base class that should be extended by custom dialogic layout layers.
+
+@export_group('Layer')
+@export_subgroup('Disabled')
+@export var disabled := false
+
+## This is turned on automatically when the layout is realized [br] [br]
+## Turn it off, if you want to modify the settings of the nodes yourself.
+@export_group('Private')
+@export var apply_overrides_on_ready := false
+
+var this_folder: String = get_script().resource_path.get_base_dir()
+
+func _ready() -> void:
+ if apply_overrides_on_ready and not Engine.is_editor_hint():
+ _apply_export_overrides()
+
+
+
+## Override this and load all your exported settings (apply them to the scene)
+func _apply_export_overrides() -> void:
+ pass
+
+
+func apply_export_overrides() -> void:
+ if disabled:
+ if "visible" in self:
+ set('visible', false)
+ process_mode = Node.PROCESS_MODE_DISABLED
+ else:
+ if "visible" in self:
+ set('visible', true)
+ process_mode = Node.PROCESS_MODE_INHERIT
+
+ _apply_export_overrides()
+
+
+## Use this to get potential global settings.
+func get_global_setting(setting_name:StringName, default:Variant) -> Variant:
+ return get_parent().get_global_setting(setting_name, default)
--- /dev/null
+@tool
+extends Resource
+class_name DialogicStyle
+
+## A style represents a collection of layers and settings.
+## A style can inherit from another style.
+
+
+@export var name := "Style":
+ get:
+ if name.is_empty():
+ return "Unkown Style"
+ return name
+
+@export var inherits: DialogicStyle = null
+
+## Stores the layer order
+@export var layer_list: Array[String] = []
+## Stores the layer infos
+@export var layer_info := {
+ "" : DialogicStyleLayer.new()
+}
+
+
+
+
+func _init(_name := "") -> void:
+ if not _name.is_empty():
+ name = _name
+
+
+
+#region BASE METHODS
+# These methods are local, meaning they do NOT take inheritance into account.
+
+
+## Returns the amount of layers (the base layer is not included).
+func get_layer_count() -> int:
+ return layer_list.size()
+
+
+## Returns the index of the layer with [param id] in the layer list.
+## Returns -1 for the base layer (id=="") which is not in the layer list.
+func get_layer_index(id:String) -> int:
+ return layer_list.find(id)
+
+
+## Returns `true` if [param id] is a valid id for a layer.
+func has_layer(id:String) -> bool:
+ return id in layer_info or id == ""
+
+
+## Returns `true` if [param index] is a valid index for a layer.
+func has_layer_index(index:int) -> bool:
+ return index < layer_list.size()
+
+
+## Returns the id of the layer at [param index].
+func get_layer_id_at_index(index:int) -> String:
+ if index == -1:
+ return ""
+ if has_layer_index(index):
+ return layer_list[index]
+ return ""
+
+
+func get_layer_info(id:String) -> Dictionary:
+ var info := {"id": id, "path": "", "overrides": {}}
+
+ if has_layer(id):
+ var layer_resource: DialogicStyleLayer = layer_info[id]
+
+ if layer_resource.scene != null:
+ info.path = layer_resource.scene.resource_path
+ elif id == "":
+ info.path = DialogicUtil.get_default_layout_base().resource_path
+
+ info.overrides = layer_resource.overrides.duplicate()
+
+ return info
+
+#endregion
+
+
+#region MODIFICATION METHODS
+# These methods modify the layers of this style.
+
+
+## Returns a new layer id not yet in use.
+func get_new_layer_id() -> String:
+ var i := 16
+ while String.num_int64(i, 16) in layer_info:
+ i += 1
+ return String.num_int64(i, 16)
+
+
+## Adds a layer with the given scene and overrides.
+## Returns the new layers id.
+func add_layer(scene:String, overrides:Dictionary = {}, id:= "##") -> String:
+ if id == "##":
+ id = get_new_layer_id()
+ layer_info[id] = DialogicStyleLayer.new(scene, overrides)
+ layer_list.append(id)
+ changed.emit()
+ return id
+
+
+## Deletes the layer with the given id.
+## Deleting the base layer is not allowed.
+func delete_layer(id:String) -> void:
+ if not has_layer(id) or id == "":
+ return
+
+ layer_info.erase(id)
+ layer_list.erase(id)
+
+ changed.emit()
+
+
+## Moves the layer at [param from_index] to [param to_index].
+func move_layer(from_index:int, to_index:int) -> void:
+ if not has_layer_index(from_index) or not has_layer_index(to_index-1):
+ return
+
+ var id := layer_list.pop_at(from_index)
+ layer_list.insert(to_index, id)
+
+ changed.emit()
+
+
+## Changes the scene property of the DialogicStyleLayer resource at [param layer_id].
+func set_layer_scene(layer_id:String, scene:String) -> void:
+ if not has_layer(layer_id):
+ return
+ layer_info[layer_id].scene = load(scene)
+ changed.emit()
+
+
+func set_layer_overrides(layer_id:String, overrides:Dictionary) -> void:
+ if not has_layer(layer_id):
+ return
+
+ layer_info[layer_id].overrides = overrides
+ changed.emit()
+
+
+## Changes an override of the DialogicStyleLayer resource at [param layer_id].
+func set_layer_setting(layer_id:String, setting:String, value:Variant) -> void:
+ if not has_layer(layer_id):
+ return
+
+ layer_info[layer_id].overrides[setting] = value
+ changed.emit()
+
+
+## Resets (removes) an override of the DialogicStyleLayer resource at [param layer_id].
+func remove_layer_setting(layer_id:String, setting:String) -> void:
+ if not has_layer(layer_id):
+ return
+
+ layer_info[layer_id].overrides.erase(setting)
+ changed.emit()
+
+#
+#endregion
+
+
+#region INHERITANCE METHODS
+# These methods are what you should usually use to get info about this style.
+
+
+## Returns `true` if this style is inheriting from another style.
+func inherits_anything() -> bool:
+ return inherits != null
+
+
+## Returns the base style of this style.
+func get_inheritance_root() -> DialogicStyle:
+ if not inherits_anything():
+ return self
+
+ var style: DialogicStyle = self
+ while style.inherits_anything():
+ style = style.inherits
+
+ return style
+
+
+## This merges some [param layer_info] with it's param ancestors layer info.
+func merge_layer_infos(layer_info:Dictionary, ancestor_info:Dictionary) -> Dictionary:
+ var combined := layer_info.duplicate(true)
+
+ combined.path = ancestor_info.path
+ combined.overrides.merge(ancestor_info.overrides)
+
+ return combined
+
+
+## Returns the layer info of the layer at [param id] taking into account inherited info.
+## If [param inherited_only] is `true`, the local info is not included.
+func get_layer_inherited_info(id:String, inherited_only := false) -> Dictionary:
+ var style := self
+ var info := {"id": id, "path": "", "overrides": {}}
+
+ if not inherited_only:
+ info = get_layer_info(id)
+
+ while style.inherits_anything():
+ style = style.inherits
+ info = merge_layer_infos(info, style.get_layer_info(id))
+
+ return info
+
+
+## Returns the layer list of the root style.
+func get_layer_inherited_list() -> Array:
+ var list := layer_list
+
+ if inherits_anything():
+ list = get_inheritance_root().layer_list
+
+ return list
+
+
+## Applies inherited info to the local layers.
+## Then removes inheritance.
+func realize_inheritance() -> void:
+ layer_list = get_layer_inherited_list()
+
+ var new_layer_info := {}
+ for id in layer_info:
+ var info := get_layer_inherited_info(id)
+ new_layer_info[id] = DialogicStyleLayer.new(info.get("path", ""), info.get("overrides", {}))
+
+ layer_info = new_layer_info
+ inherits = null
+ changed.emit()
+
+
+#endregion
+
+## Creates a fresh new style with the same settings.
+func clone() -> DialogicStyle:
+ var style := DialogicStyle.new()
+ style.name = name
+ style.inherits = inherits
+
+ var base_info := get_layer_info("")
+ style.set_layer_scene("", base_info.path)
+ style.set_layer_overrides("", base_info.overrides)
+
+ for id in layer_list:
+ var info := get_layer_info(id)
+ style.add_layer(info.path, info.overrides, id)
+
+ return style
+
+
+## Starts preloading all the scenes used by this style.
+func prepare() -> void:
+ for id in layer_info:
+ if layer_info[id].scene:
+ ResourceLoader.load_threaded_request(layer_info[id].scene.resource_path)
+
+
+#region UPDATE OLD STYLES
+# TODO deprecated when going into beta
+
+# TODO Deprecated, only for Styles before alpha 16!
+@export var base_scene: PackedScene = null
+# TODO Deprecated, only for Styles before alpha 16!
+@export var base_overrides := {}
+# TODO Deprecated, only for Styles before alpha 16!
+@export var layers: Array[DialogicStyleLayer] = []
+
+func update_from_pre_alpha16() -> void:
+ if not layers.is_empty():
+ var idx := 0
+ for layer in layers:
+ var id := "##"
+ if inherits_anything():
+ id = get_layer_inherited_list()[idx]
+ if layer.scene:
+ add_layer(layer.scene.resource_path, layer.overrides, id)
+ else:
+ add_layer("", layer.overrides, id)
+ idx += 1
+ layers.clear()
+
+ if not base_scene == null:
+ set_layer_scene("", base_scene.resource_path)
+ base_scene = null
+ if not base_overrides.is_empty():
+ set_layer_overrides("", base_overrides)
+ base_overrides.clear()
+
+
+#endregion
--- /dev/null
+@tool
+class_name DialogicStyleLayer
+extends Resource
+
+@export var scene: PackedScene = null
+@export var overrides := {}
+
+
+func _init(scene_path:Variant=null, scene_overrides:Dictionary={}):
+ if scene_path is PackedScene:
+ scene = scene_path
+ elif scene_path is String and ResourceLoader.exists(scene_path):
+ scene = load(scene_path)
+ overrides = scene_overrides
+
+
+func _to_string() -> String:
+ if scene:
+ return "<Layer:" + scene.resource_path + " {" + str(len(overrides)) + " overrides} >"
+ else:
+ return "<Layer:no-scene>"
--- /dev/null
+@tool
+class_name DialogicEvent
+extends Resource
+
+## Base event class for all dialogic events.
+## Implements basic properties, translation, shortcode saving and usefull methods for creating
+## the editor UI.
+
+
+## Emmited when the event starts.
+## The signal is emmited with the event resource [code]event_resource[/code]
+signal event_started(event_resource:DialogicEvent)
+
+## Emmited when the event finish.
+## The signal is emmited with the event resource [code]event_resource[/code]
+signal event_finished(event_resource:DialogicEvent)
+
+
+### Main Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+## The event name that'll be displayed in the editor.
+var event_name := "Event"
+## Unique identifier used for translatable events.
+var _translation_id := ""
+## A reference to dialogic during execution, can be used the same as Dialogic (reference to the autoload)
+var dialogic: DialogicGameHandler = null
+
+
+### Special Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+### (these properties store how this event affects indentation/flow of timeline)
+
+## If true this event can not be toplevel (e.g. Choice)
+var needs_indentation := false
+## If true this event will spawn with an END BRANCH event and higher the indentation
+var can_contain_events := false
+## If [can_contain_events] is true this is a reference to the end branch event
+var end_branch_event: DialogicEndBranchEvent = null
+## If this is true this event will group with other similar events (like choices do).
+var wants_to_group := false
+
+
+### Saving/Loading Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+## Stores the event in a text format. Does NOT automatically update.
+var event_node_as_text := ""
+## Flags if the event has been processed or is only stored as text
+var event_node_ready := false
+## How many empty lines are before this event
+var empty_lines_above: int = 0
+
+
+### Editor UI Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+## The event color that event node will take in the editor
+var event_color := Color("FBB13C")
+## If you are using the default color palette
+var dialogic_color_name: = ""
+## To sort the buttons shown in the editor. Lower index is placed at the top of a category
+var event_sorting_index: int = 0
+## If true the event will not have a button in the visual editor sidebar
+var disable_editor_button := false
+## If false the event will hide it's body by default. Recommended for most events
+var expand_by_default := false
+## The URL to open when right_click>Documentation is selected
+var help_page_path := ""
+## Is the event block created by a button?
+var created_by_button := false
+
+## Reference to the node, that represents this event. Only works while in visual editor mode.
+## Use with care.
+var editor_node: Control = null
+
+## The categories and which one to put it in (in the visual editor sidebar)
+var event_category := "Other"
+
+
+### Editor UI creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+## To differentiate fields that should go to the header and to the body
+enum Location {HEADER, BODY}
+
+## To differentiate the different types of fields for event properties in the visual editor
+enum ValueType {
+ # Strings
+ MULTILINE_TEXT, SINGLELINE_TEXT, CONDITION, FILE,
+ # Booleans
+ BOOL, BOOL_BUTTON,
+ # Options
+ DYNAMIC_OPTIONS, FIXED_OPTIONS,
+ # Containers,
+ ARRAY, DICTIONARY,
+ # Numbers
+ NUMBER,
+ VECTOR2, VECTOR3, VECTOR4,
+ # Other
+ CUSTOM, BUTTON, LABEL, COLOR, AUDIO_PREVIEW
+}
+## List that stores the fields for the editor
+var editor_list: Array = []
+
+var this_folder: String = get_script().resource_path.get_base_dir()
+
+## Singal that notifies the visual editor block to update
+signal ui_update_needed
+signal ui_update_warning(text:String)
+
+
+## Makes this resource printable.
+func _to_string() -> String:
+ return "[{name}:{id}]".format({"name":event_name, "id":get_instance_id()})
+
+#endregion
+
+
+#region EXECUTION
+################################################################################
+
+## Executes the event behaviour. In subclasses [_execute] (not this one) should be overriden!
+func execute(_dialogic_game_handler) -> void:
+ event_started.emit(self)
+ dialogic = _dialogic_game_handler
+ call_deferred("_execute")
+
+
+## Ends the event behaviour.
+func finish() -> void:
+ event_finished.emit(self)
+
+
+## Called before executing the next event or before clear(any flags) / load_full_state().
+##
+## Should be overridden if the event stores temporary state into dialogic.current_state_info
+## or some other cleanup is needed before another event can run.
+func _clear_state() -> void:
+ pass
+
+
+## To be overridden by subclasses.
+func _execute() -> void:
+ finish()
+
+#endregion
+
+
+#region OVERRIDABLES
+################################################################################
+
+## to be overridden by sub-classes
+## only called if can_contain_events is true.
+## return a control node that should show on the END BRANCH node
+func get_end_branch_control() -> Control:
+ return null
+
+
+## to be overridden by sub-classes
+## only called if can_contain_events is true and the previous event was an end-branch event
+## return true if this event should be executed if the previous event was an end-branch event
+## basically only important for the Condition event but who knows. Some day someone might need this.
+func should_execute_this_branch() -> bool:
+ return false
+
+#endregion
+
+
+#region TRANSLATIONS
+################################################################################
+
+## Overwrite if this events needs translation.
+func _get_translatable_properties() -> Array:
+ return []
+
+
+## Overwrite if this events needs translation.
+func _get_property_original_translation(_property_name:String) -> String:
+ return ''
+
+
+## Returns true if there is any translatable properties on this event.
+## Overwrite [_get_translatable_properties()] to change this.
+func can_be_translated() -> bool:
+ return !_get_translatable_properties().is_empty()
+
+
+## This is automatically called, no need to use this.
+func add_translation_id() -> String:
+ _translation_id = DialogicUtil.get_next_translation_id()
+ return _translation_id
+
+
+func remove_translation_id() -> void:
+ _translation_id = ""
+
+
+func get_property_translation_key(property_name:String) -> String:
+ return event_name.path_join(_translation_id).path_join(property_name)
+
+
+## Call this whenever you are using a translatable property
+func get_property_translated(property_name:String) -> String:
+ if !_translation_id.is_empty() and ProjectSettings.get_setting('dialogic/translation/enabled', false):
+ var translation := tr(get_property_translation_key(property_name))
+ # if no translation is found tr() returns the id, but we want to fallback to the original
+ return translation if translation != get_property_translation_key(property_name) else _get_property_original_translation(property_name)
+ else:
+ return _get_property_original_translation(property_name)
+
+#endregion
+
+
+#region SAVE / LOAD (internal, don't override)
+################################################################################
+### These functions are used by the timeline loader/saver
+### They mainly use the overridable behaviour below, but enforce the unique_id saving
+
+## Used by the Timeline saver.
+func _store_as_string() -> String:
+ if !_translation_id.is_empty() and can_be_translated():
+ return to_text() + ' #id:'+str(_translation_id)
+ else:
+ return to_text()
+
+
+## Call this if you updated an event and want the changes to be saved.
+func update_text_version() -> void:
+ event_node_as_text = _store_as_string()
+
+
+## Used by timeline processor.
+func _load_from_string(string:String) -> void:
+ _load_custom_defaults()
+ if '#id:' in string and can_be_translated():
+ _translation_id = string.get_slice('#id:', 1).strip_edges()
+ from_text(string.get_slice('#id:', 0))
+ else:
+ from_text(string)
+ event_node_ready = true
+
+
+## Assigns the custom defaults
+func _load_custom_defaults() -> void:
+ for default_prop in DialogicUtil.get_custom_event_defaults(event_name):
+ if default_prop in self:
+ set(default_prop, DialogicUtil.get_custom_event_defaults(event_name)[default_prop])
+
+
+## Used by the timeline processor.
+func _test_event_string(string:String) -> bool:
+ if '#id:' in string and can_be_translated():
+ return is_valid_event(string.get_slice('#id:', 0))
+ return is_valid_event(string.strip_edges())
+
+#endregion
+
+
+#region SAVE / LOAD
+################################################################################
+### All of these functions can/should be overridden by the sub classes
+
+## If this uses the short-code format, return the shortcode.
+func get_shortcode() -> String:
+ return 'default_shortcode'
+
+
+## If this uses the short-code format, return the parameters and corresponding property names.
+func get_shortcode_parameters() -> Dictionary:
+ return {}
+
+
+## Returns a readable presentation of the event (This is how it's stored).
+## By default it uses a shortcode format, but can be overridden.
+func to_text() -> String:
+ var shortcode := store_to_shortcode_parameters()
+ if shortcode:
+ return "[" + self.get_shortcode() + " " + store_to_shortcode_parameters() + "]"
+ else:
+ return "[" + self.get_shortcode() + "]"
+
+
+## Loads the variables from the string stored by [method to_text].
+## By default it uses the shortcode format, but can be overridden.
+func from_text(string: String) -> void:
+ load_from_shortcode_parameters(string)
+
+
+## Returns a string with all the shortcode parameters.
+func store_to_shortcode_parameters(params:Dictionary = {}) -> String:
+ if params.is_empty():
+ params = get_shortcode_parameters()
+ var custom_defaults: Dictionary = DialogicUtil.get_custom_event_defaults(event_name)
+ var result_string := ""
+ for parameter in params.keys():
+ var parameter_info: Dictionary = params[parameter]
+ var value: Variant = get(parameter_info.property)
+ var default_value: Variant = custom_defaults.get(parameter_info.property, parameter_info.default)
+
+ if parameter_info.get('custom_stored', false):
+ continue
+
+ if "set_" + parameter_info.property in self and not get("set_" + parameter_info.property):
+ continue
+
+ if typeof(value) == typeof(default_value) and value == default_value:
+ if not "set_" + parameter_info.property in self or not get("set_" + parameter_info.property):
+ continue
+
+ result_string += " " + parameter + '="' + value_to_string(value, parameter_info.get("suggestions", Callable())) + '"'
+
+ return result_string.strip_edges()
+
+
+func value_to_string(value: Variant, suggestions := Callable()) -> String:
+ var value_as_string := ""
+ match typeof(value):
+ TYPE_OBJECT:
+ value_as_string = str(value.resource_path)
+
+ TYPE_STRING:
+ value_as_string = value
+
+ TYPE_INT when suggestions.is_valid():
+ # HANDLE TEXT ALTERNATIVES FOR ENUMS
+ for option in suggestions.call().values():
+ if option.value != value:
+ continue
+
+ if option.has('text_alt'):
+ value_as_string = option.text_alt[0]
+ else:
+ value_as_string = var_to_str(option.value)
+
+ break
+
+ TYPE_DICTIONARY:
+ value_as_string = JSON.stringify(value)
+
+ _:
+ value_as_string = var_to_str(value)
+
+ if not ((value_as_string.begins_with("[") and value_as_string.ends_with("]")) or (value_as_string.begins_with("{") and value_as_string.ends_with("}"))):
+ value_as_string.replace('"', '\\"')
+
+ return value_as_string
+
+
+func load_from_shortcode_parameters(string:String) -> void:
+ var data: Dictionary = parse_shortcode_parameters(string)
+ var params: Dictionary = get_shortcode_parameters()
+ for parameter in params.keys():
+ var parameter_info: Dictionary = params[parameter]
+ if parameter_info.get('custom_stored', false):
+ continue
+
+ if not parameter in data:
+ if "set_" + parameter_info.property in self:
+ set("set_" + parameter_info.property, false)
+ continue
+
+ if "set_" + parameter_info.property in self:
+ set("set_" + parameter_info.property, true)
+
+ var param_value: String = data[parameter].replace('\\"', '"')
+ var value: Variant
+ match typeof(get(parameter_info.property)):
+ TYPE_STRING:
+ value = param_value
+
+ TYPE_INT:
+ # If a string is given
+ if parameter_info.has('suggestions'):
+ for option in parameter_info.suggestions.call().values():
+ if option.has('text_alt') and param_value in option.text_alt:
+ value = option.value
+ break
+
+ if not value:
+ value = float(param_value)
+
+ _:
+ value = str_to_var(param_value)
+
+ set(parameter_info.property, value)
+
+## Has to return `true`, if the given string can be interpreted as this event.
+## By default it uses the shortcode formta, but can be overridden.
+func is_valid_event(string: String) -> bool:
+ if string.strip_edges().begins_with('['+get_shortcode()+' ') or string.strip_edges().begins_with('['+get_shortcode()+']'):
+ return true
+ return false
+
+
+## has to return true if this string seems to be a full event of this kind
+## (only tested if is_valid_event() returned true)
+## if a shortcode it used it will default to true if the string ends with ']'
+func is_string_full_event(string: String) -> bool:
+ if get_shortcode() != 'default_shortcode': return string.strip_edges().ends_with(']')
+ return true
+
+
+## Used to get all the shortcode parameters in a string as a dictionary.
+func parse_shortcode_parameters(shortcode: String) -> Dictionary:
+ var regex := RegEx.new()
+ regex.compile(r'(?<parameter>[^\s=]*)\s*=\s*"(?<value>(\{[^}]*\}|\[[^]]*\]|([^"]|\\")*|))(?<!\\)\"')
+ var dict := {}
+ for result in regex.search_all(shortcode):
+ dict[result.get_string('parameter')] = result.get_string('value')
+ return dict
+
+#endregion
+
+
+#region EDITOR REPRESENTATION
+################################################################################
+
+func _get_icon() -> Resource:
+ var _icon_file_name := "res://addons/dialogic/Editor/Images/Pieces/closed-icon.svg" # Default
+ # Check for both svg and png, but prefer svg if available
+ if ResourceLoader.exists(self.get_script().get_path().get_base_dir() + "/icon.svg"):
+ _icon_file_name = self.get_script().get_path().get_base_dir() + "/icon.svg"
+ elif ResourceLoader.exists(self.get_script().get_path().get_base_dir() + "/icon.png"):
+ _icon_file_name = self.get_script().get_path().get_base_dir() + "/icon.png"
+ return load(_icon_file_name)
+
+
+func set_default_color(value:Variant) -> void:
+ dialogic_color_name = value
+ event_color = DialogicUtil.get_color(value)
+
+
+## Called when the resource is assigned to a event block in the visual editor
+func _enter_visual_editor(_timeline_editor:DialogicEditor) -> void:
+ pass
+
+#endregion
+
+
+#region CODE COMPLETION
+################################################################################
+
+## This method can be overwritten to implement code completion for custom syntaxes
+func _get_code_completion(_CodeCompletionHelper:Node, _TextNode:TextEdit, _line:String, _word:String, _symbol:String) -> void:
+ pass
+
+## This method can be overwritten to add starting suggestions for this event
+func _get_start_code_completion(_CodeCompletionHelper:Node, _TextNode:TextEdit) -> void:
+ pass
+
+#endregion
+
+
+#region SYNTAX HIGHLIGHTING
+################################################################################
+
+func _get_syntax_highlighting(_Highlighter:SyntaxHighlighter, dict:Dictionary, _line:String) -> Dictionary:
+ return dict
+
+#endregion
+
+
+#region EVENT FIELDS
+################################################################################
+
+func get_event_editor_info() -> Array:
+ if Engine.is_editor_hint():
+ if editor_list != null:
+ editor_list.clear()
+ else:
+ editor_list = []
+
+ build_event_editor()
+ return editor_list
+ else:
+ return []
+
+
+## to be overwritten by the sub_classes
+func build_event_editor() -> void:
+ pass
+
+## For the methods below the arguments are mostly similar:
+## @variable: String name of the property this field is for
+## @condition: String that will be executed as an expression. If it false
+## @editor_type: One of the ValueTypes (see ValueType enum). Defines type of field.
+## @left_text: Text that will be shown to the left of the field
+## @right_text: Text that will be shown to the right of the field
+## @extra_info: Allows passing a lot more info to the field.
+## What info can be passed is different for every field
+
+func add_header_label(text:String, condition:= "") -> void:
+ editor_list.append({
+ "name" : "something",
+ "type" :+ TYPE_STRING,
+ "location" : Location.HEADER,
+ "usage" : PROPERTY_USAGE_EDITOR,
+ "field_type" : ValueType.LABEL,
+ "display_info" : {"text":text},
+ "condition" : condition
+ })
+
+
+func add_header_edit(variable:String, editor_type := ValueType.LABEL, extra_info:= {}, condition:= "") -> void:
+ editor_list.append({
+ "name" : variable,
+ "type" : typeof(get(variable)),
+ "location" : Location.HEADER,
+ "usage" : PROPERTY_USAGE_DEFAULT,
+ "field_type" : editor_type,
+ "display_info" : extra_info,
+ "left_text" : extra_info.get('left_text', ''),
+ "right_text" : extra_info.get('right_text', ''),
+ "condition" : condition,
+ })
+
+
+func add_header_button(text:String, callable:Callable, tooltip:String, icon: Variant = null, condition:= "") -> void:
+ editor_list.append({
+ "name" : "Button",
+ "type" : TYPE_STRING,
+ "location" : Location.HEADER,
+ "usage" : PROPERTY_USAGE_DEFAULT,
+ "field_type" : ValueType.BUTTON,
+ "display_info" : {'text':text, 'tooltip':tooltip, 'callable':callable, 'icon':icon},
+ "condition" : condition,
+ })
+
+
+func add_body_edit(variable:String, editor_type := ValueType.LABEL, extra_info:= {}, condition:= "") -> void:
+ editor_list.append({
+ "name" : variable,
+ "type" : typeof(get(variable)),
+ "location" : Location.BODY,
+ "usage" : PROPERTY_USAGE_DEFAULT,
+ "field_type" : editor_type,
+ "display_info" : extra_info,
+ "left_text" : extra_info.get('left_text', ''),
+ "right_text" : extra_info.get('right_text', ''),
+ "condition" : condition,
+ })
+
+
+func add_body_line_break(condition:= "") -> void:
+ editor_list.append({
+ "name" : "linebreak",
+ "type" : TYPE_BOOL,
+ "location" : Location.BODY,
+ "usage" : PROPERTY_USAGE_DEFAULT,
+ "condition" : condition,
+ })
+
+#endregion
--- /dev/null
+@tool
+extends Resource
+class_name DialogicTimeline
+
+## Resource that defines a list of events.
+## It can store them as text and load them from text too.
+
+var events: Array = []
+var events_processed := false
+
+
+## Method used for printing timeline resources identifiably
+func _to_string() -> String:
+ return "[DialogicTimeline:{file}]".format({"file":resource_path})
+
+
+## Helper method
+func get_event(index:int) -> Variant:
+ if index >= len(events):
+ return null
+ return events[index]
+
+
+## Parses the lines as seperate events and insert them in an array,
+## so they can be converted to DialogicEvent's when processed later
+func from_text(text:String) -> void:
+ events = text.split('\n', true)
+ events_processed = false
+
+
+## Stores all events in their text format and returns them as a string
+func as_text() -> String:
+ var result := ""
+
+ if events_processed:
+ var indent := 0
+ for idx in range(0, len(events)):
+ var event: DialogicEvent = events[idx]
+
+ if event.event_name == 'End Branch':
+ indent -= 1
+ continue
+
+ if event != null:
+ for i in event.empty_lines_above:
+ result += "\t".repeat(indent)+"\n"
+ result += "\t".repeat(indent)+event.event_node_as_text.replace('\n', "\n"+"\t".repeat(indent)) + "\n"
+ if event.can_contain_events:
+ indent += 1
+ if indent < 0:
+ indent = 0
+ else:
+ for event in events:
+ result += str(event)+"\n"
+
+ result.trim_suffix('\n')
+
+ return result.strip_edges()
+
+
+## Method that loads all the event resources from the strings, if it wasn't done before
+func process() -> void:
+ if typeof(events[0]) == TYPE_STRING:
+ events_processed = false
+
+ # if the timeline is already processed
+ if events_processed:
+ for event in events:
+ event.event_node_ready = true
+ return
+
+ var event_cache := DialogicResourceUtil.get_event_cache()
+ var end_event := DialogicEndBranchEvent.new()
+
+ var prev_indent := ""
+ var processed_events := []
+
+ # this is needed to add an end branch event even to empty conditions/choices
+ var prev_was_opener := false
+
+ var lines := events
+ var idx := -1
+ var empty_lines := 0
+ while idx < len(lines)-1:
+ idx += 1
+
+ # make sure we are using the string version, in case this was already converted
+ var line := ""
+ if typeof(lines[idx]) == TYPE_STRING:
+ line = lines[idx]
+ else:
+ line = lines[idx].event_node_as_text
+
+ ## Ignore empty lines, but record them in @empty_lines
+ var line_stripped: String = line.strip_edges(true, false)
+ if line_stripped.is_empty():
+ empty_lines += 1
+ continue
+
+ ## Add an end event if the indent is smaller then previously
+ var indent: String = line.substr(0,len(line)-len(line_stripped))
+ if len(indent) < len(prev_indent):
+ for i in range(len(prev_indent)-len(indent)):
+ processed_events.append(end_event.duplicate())
+ ## Add an end event if the indent is the same but the previous was an opener
+ ## (so for example choice that is empty)
+ if prev_was_opener and len(indent) <= len(prev_indent):
+ processed_events.append(end_event.duplicate())
+
+ prev_indent = indent
+
+ ## Now we process the event into a resource
+ ## by checking on each event if it recognizes this string
+ var event_content: String = line_stripped
+ var event: DialogicEvent
+ for i in event_cache:
+ if i._test_event_string(event_content):
+ event = i.duplicate()
+ break
+
+ event.empty_lines_above = empty_lines
+ # add the following lines until the event says it's full or there is an empty line
+ while !event.is_string_full_event(event_content):
+ idx += 1
+ if idx == len(lines):
+ break
+
+ var following_line_stripped: String = lines[idx].strip_edges(true, false)
+
+ if following_line_stripped.is_empty():
+ break
+
+ event_content += "\n"+following_line_stripped
+
+ event._load_from_string(event_content)
+ event.event_node_as_text = event_content
+
+ processed_events.append(event)
+ prev_was_opener = event.can_contain_events
+ empty_lines = 0
+
+ if !prev_indent.is_empty():
+ for i in range(len(prev_indent)):
+ processed_events.append(end_event.duplicate())
+
+ events = processed_events
+ events_processed = true
+
+
+## This method makes sure that all events in a timeline are correctly reset
+func clean() -> void:
+ if not events_processed:
+ return
+ reference()
+ # This is necessary because otherwise INTERNAL GODOT ONESHOT CONNECTIONS
+ # are disconnected before they can disconnect themselves.
+ await Engine.get_main_loop().process_frame
+
+ for event:DialogicEvent in events:
+ for con_in in event.get_incoming_connections():
+ con_in.signal.disconnect(con_in.callable)
+
+ for sig in event.get_signal_list():
+ for con_out in event.get_signal_connection_list(sig.name):
+ con_out.signal.disconnect(con_out.callable)
+ unreference()
--- /dev/null
+[plugin]
+
+name="Dialogic"
+description="Create dialogs, characters and scenes to display conversations in your Godot games.
+https://github.com/dialogic-godot/dialogic"
+author="Jowan Spooner, Emi, Cake and more!"
+version="2.0-Alpha-17 WIP (Godot 4.2+)"
+script="plugin.gd"
--- /dev/null
+@tool
+extends EditorPlugin
+
+## Preload the main panel scene
+const MainPanel := preload("res://addons/dialogic/Editor/editor_main.tscn")
+const PLUGIN_NAME := "Dialogic"
+const PLUGIN_HANDLER_PATH := "res://addons/dialogic/Core/DialogicGameHandler.gd"
+const PLUGIN_ICON_PATH := "res://addons/dialogic/Editor/Images/plugin-icon.svg"
+
+## References used by various other scripts to quickly reference these things
+var editor_view: Control # the root of the dialogic editor
+var inspector_plugin: EditorInspectorPlugin = null
+
+## Initialization
+func _init() -> void:
+ self.name = "DialogicPlugin"
+
+
+#region ACTIVATION & EDITOR SETUP
+################################################################################
+
+## Activation & Editor Setup
+func _enable_plugin() -> void:
+ add_autoload_singleton(PLUGIN_NAME, PLUGIN_HANDLER_PATH)
+ add_dialogic_default_action()
+
+
+func _disable_plugin() -> void:
+ remove_autoload_singleton(PLUGIN_NAME)
+
+
+func _enter_tree() -> void:
+ editor_view = MainPanel.instantiate()
+ editor_view.plugin_reference = self
+ editor_view.hide()
+ get_editor_interface().get_editor_main_screen().add_child(editor_view)
+ _make_visible(false)
+
+ inspector_plugin = load("res://addons/dialogic/Editor/Inspector/inspector_plugin.gd").new()
+ add_inspector_plugin(inspector_plugin)
+
+ # Auto-update the singleton path for alpha users
+ # TODO remove at some point during beta or later
+ if not ProjectSettings.has_setting("autoload/"+PLUGIN_NAME) or not "Core" in ProjectSettings.get_setting("autoload/"+PLUGIN_NAME, ""):
+ if ProjectSettings.has_setting("autoload/"+PLUGIN_NAME):
+ remove_autoload_singleton(PLUGIN_NAME)
+ add_autoload_singleton(PLUGIN_NAME, PLUGIN_HANDLER_PATH)
+
+
+func _exit_tree() -> void:
+ if editor_view:
+ remove_control_from_bottom_panel(editor_view)
+ editor_view.queue_free()
+
+ if inspector_plugin:
+ remove_inspector_plugin(inspector_plugin)
+
+#endregion
+
+
+#region PLUGIN_INFO
+################################################################################
+
+func _has_main_screen() -> bool:
+ return true
+
+
+func _get_plugin_name() -> String:
+ return PLUGIN_NAME
+
+
+func _get_plugin_icon() -> Texture2D:
+ return load(PLUGIN_ICON_PATH)
+
+#endregion
+
+
+#region EDITOR INTERACTION
+################################################################################
+
+## Editor Interaction
+func _make_visible(visible:bool) -> void:
+ if not editor_view:
+ return
+
+ if editor_view.get_parent() is Window:
+ if visible:
+ get_editor_interface().set_main_screen_editor("Script")
+ editor_view.show()
+ editor_view.get_parent().grab_focus()
+ else:
+ editor_view.visible = visible
+
+
+func _save_external_data() -> void:
+ if _editor_view_and_manager_exist():
+ editor_view.editors_manager.save_current_resource()
+
+
+func _get_unsaved_status(for_scene:String) -> String:
+ if for_scene.is_empty():
+ _save_external_data()
+ return ""
+
+
+func _handles(object) -> bool:
+ if _editor_view_and_manager_exist() and object is Resource:
+ return editor_view.editors_manager.can_edit_resource(object)
+ return false
+
+
+func _edit(object) -> void:
+ if object == null:
+ return
+ _make_visible(true)
+ if _editor_view_and_manager_exist():
+ editor_view.editors_manager.edit_resource(object)
+
+
+## Helper function to check if editor_view and its manager exist
+func _editor_view_and_manager_exist() -> bool:
+ return editor_view and editor_view.editors_manager
+
+#endregion
+
+
+#region PROJECT SETUP
+################################################################################
+
+## Special Setup/Updates
+## Methods that adds a dialogic_default_action if non exists
+func add_dialogic_default_action() -> void:
+ if ProjectSettings.has_setting('input/dialogic_default_action'):
+ return
+
+ var input_enter: InputEventKey = InputEventKey.new()
+ input_enter.keycode = KEY_ENTER
+ var input_left_click: InputEventMouseButton = InputEventMouseButton.new()
+ input_left_click.button_index = MOUSE_BUTTON_LEFT
+ input_left_click.pressed = true
+ input_left_click.device = -1
+ var input_space: InputEventKey = InputEventKey.new()
+ input_space.keycode = KEY_SPACE
+ var input_x: InputEventKey = InputEventKey.new()
+ input_x.keycode = KEY_X
+ var input_controller: InputEventJoypadButton = InputEventJoypadButton.new()
+ input_controller.button_index = JOY_BUTTON_A
+
+ ProjectSettings.set_setting('input/dialogic_default_action', {'deadzone':0.5, 'events':[input_enter, input_left_click, input_space, input_x, input_controller]})
+ ProjectSettings.save()
+
+# Create cache when project is compiled
+func _build() -> bool:
+ DialogicResourceUtil.update()
+ return true
+
+#endregion
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
\ No newline at end of file
--- /dev/null
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="wolf-seeking-sheep"
+config/features=PackedStringArray("4.3", "Forward Plus")
+config/icon="res://icon.svg"
+
+[autoload]
+
+Dialogic="*res://addons/dialogic/Core/DialogicGameHandler.gd"
+
+[dialogic]
+
+directories/dch_directory={}
+directories/dtl_directory={}
+
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/dialogic/plugin.cfg")
+
+[input]
+
+dialogic_default_action={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":88,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
+]
+}