extends Node ## Exposed and safe to use methods for Dialogic ## See documentation under 'https://github.com/coppolaemilio/dialogic' or in the editor: ## ### /!\ ### ## Do not use methods from other classes as it could break the plugin's integrity ## ### /!\ ### ## Trying to follow this documentation convention: https://github.com/godotengine/godot/pull/41095 class_name Dialogic ## Refactor the start function for 2.0 there should be a cleaner way to do it :) ## Starts the dialog for the given timeline and returns a Dialog node. ## You must then add it manually to the scene to display the dialog. ## ## Example: ## var new_dialog = Dialogic.start('Your Timeline Name Here') ## add_child(new_dialog) ## ## This is similar to using the editor: ## you can drag and drop the scene located at /addons/dialogic/Dialog.tscn ## and set the current timeline via the inspector. ## ## @param timeline The timeline to load. You can provide the timeline name or the filename. ## If you leave it empty, it will try to load from current data ## In that case, you should do Dialogic.load() or Dialogic.import() before. ## @param default_timeline If timeline == '' and no valid data was found, this will be loaded. ## @param dialog_scene_path If you made a custom Dialog scene or moved it from its default path, you can specify its new path here. ## @param use_canvas_instead Create the Dialog inside a canvas layer to make it show up regardless of the camera 2D/3D situation. ## @returns A Dialog node to be added into the scene tree. static func start(timeline: String = '', default_timeline: String ='', dialog_scene_path: String="res://addons/dialogic/Nodes/DialogNode.tscn", use_canvas_instead=true): var dialog_scene = load(dialog_scene_path) var dialog_node = null var canvas_dialog_node = null var returned_dialog_node = null if use_canvas_instead: var canvas_dialog_script = load("res://addons/dialogic/Nodes/canvas_dialog_node.gd") canvas_dialog_node = canvas_dialog_script.new() canvas_dialog_node.set_dialog_node_scene(dialog_scene) dialog_node = canvas_dialog_node.dialog_node else: dialog_node = dialog_scene.instance() returned_dialog_node = dialog_node if not canvas_dialog_node else canvas_dialog_node ## 1. Case: A slot has been loaded OR data has been imported if timeline == '': if (Engine.get_main_loop().has_meta('last_dialog_state') and not Engine.get_main_loop().get_meta('last_dialog_state').empty() and not Engine.get_main_loop().get_meta('last_dialog_state').get('timeline', '').empty()): dialog_node.resume_state_from_info(Engine.get_main_loop().get_meta('last_dialog_state')) return returned_dialog_node ## The loaded data isn't complete elif (Engine.get_main_loop().has_meta('current_timeline') and not Engine.get_main_loop().get_meta('current_timeline').empty()): timeline = Engine.get_main_loop().get_meta('current_timeline') ## Else load the default timeline else: timeline = default_timeline ## 2. Case: A specific timeline should be started # check if it's a file name if timeline.ends_with('.json'): for t in DialogicUtil.get_timeline_list(): if t['file'] == timeline: dialog_node.timeline = t['file'] dialog_node.timeline_name = timeline return returned_dialog_node # No file found. Show error dialog_node.dialog_script = { "events":[ {"event_id":'dialogic_001', "character":"", "portrait":"", "text":"[Dialogic Error] Loading dialog [color=red]" + timeline + "[/color]. It seems like the timeline doesn't exists. Maybe the name is wrong?" }] } return returned_dialog_node # else get the file from the name var timeline_file = _get_timeline_file_from_name(timeline) if timeline_file: dialog_node.timeline = timeline_file dialog_node.timeline_name = timeline return returned_dialog_node # Just in case everything else fails. return returned_dialog_node # Loads the given timeline into the active DialogNode # This means it's state (theme, characters, background, music) is preserved. # # @param timeline the name of the timeline to load static func change_timeline(timeline: String) -> void: # Set Timeline set_current_timeline(timeline) # If there is a dialog node if has_current_dialog_node(): var dialog_node = Engine.get_main_loop().get_meta('latest_dialogic_node') # Get file name var timeline_file = _get_timeline_file_from_name(timeline) dialog_node.change_timeline(timeline_file) else: print("[D] Tried to change timeline, but no DialogNode exists!") # Immediately plays the next event. # # @param discreetly determines whether the Passing Audio will be played in the process static func next_event(discreetly: bool = false): # If there is a dialog node if has_current_dialog_node(): var dialog_node = Engine.get_main_loop().get_meta('latest_dialogic_node') dialog_node.next_event(discreetly) ################################################################################ ## Test to see if a timeline exists ################################################################################ ## Check to see if a timeline with a given name/path exists. Useful for verifying ## before calling a timeline, or for automated tests to make sure timeline calls ## are valid. Returns a boolean of true if the timeline exists, and false if it ## does not. static func timeline_exists(timeline: String): var timeline_file = _get_timeline_file_from_name(timeline) if timeline_file: return true else: return false ################################################################################ ## BUILT-IN SAVING/LOADING ################################################################################ ## Loads the given slot static func load(slot_name: String = ''): _load_from_slot(slot_name) Engine.get_main_loop().set_meta('current_save_slot', slot_name) ## Saves the current definitions and the latest added dialog nodes state info. ## ## @param slot_name The name of the save slot. To load this save you have to specify the same ## If the slot folder doesn't exist it will be created. static func save(slot_name: String = '', is_autosave = false) -> void: # check if to save (if this is a autosave) if is_autosave and not get_autosave(): return # gather the info var current_dialog_info = {} if has_current_dialog_node(): current_dialog_info = Engine.get_main_loop().get_meta('latest_dialogic_node').get_current_state_info() var game_state = {} if Engine.get_main_loop().has_meta('game_state'): game_state = Engine.get_main_loop().get_meta('game_state') var save_data = { 'game_state': game_state, 'dialog_state': current_dialog_info } # save the information _save_state_and_definitions(slot_name, save_data) ## Returns an array with the names of all available slots. static func get_slot_names() -> Array: return DialogicResources.get_saves_folders() ## Will permanently erase the data in the given save_slot. ## ## @param slot_name The name of the slot folder. static func erase_slot(slot_name: String) -> void: DialogicResources.remove_save_folder(slot_name) ## Whether a save can be performed ## ## @returns True if a save can be performed; otherwise False static func has_current_dialog_node() -> bool: return Engine.get_main_loop().has_meta('latest_dialogic_node') and is_instance_valid(Engine.get_main_loop().get_meta('latest_dialogic_node')) ## Resets the state and definitions of the given save slot ## ## By default this will also LOAD that reseted save static func reset_saves(slot_name: String = '', reload:= true) -> void: DialogicResources.reset_save(slot_name) if reload: _load_from_slot(slot_name) ## Returns the currently loaded save slot static func get_current_slot(): if Engine.get_main_loop().has_meta('current_save_slot'): return Engine.get_main_loop().get_meta('current_save_slot') else: return '' ## +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## EXPORT / IMPORT ## +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # this returns a dictionary with the DEFINITIONS, the GAME STATE and the DIALOG STATE static func export(dialog_node = null) -> Dictionary: # gather the data var current_dialog_info = {} if dialog_node == null and has_current_dialog_node(): dialog_node = Engine.get_main_loop().get_meta('latest_dialogic_node') if dialog_node: current_dialog_info = dialog_node.get_current_state_info() # return it return { 'definitions': _get_definitions(), 'state': Engine.get_main_loop().get_meta('game_state'), 'dialog_state': current_dialog_info } # this loads a dictionary with GAME STATE, DEFINITIONS and DIALOG_STATE static func import(data: Dictionary) -> void: ## Tell the future we want to use the imported data Engine.get_main_loop().set_meta('current_save_lot', '/') # load the data Engine.get_main_loop().set_meta('definitions', data['definitions']) Engine.get_main_loop().set_meta('game_state', data['state']) Engine.get_main_loop().set_meta('last_dialog_state', data.get('dialog_state', null)) set_current_timeline(get_saved_state_general_key('timeline')) ## +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## DEFINITIONS ## +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # clears all variables static func clear_all_variables(): for d in _get_definitions()['variables']: d['value'] = "" # sets the value of the value definition with the given name static func set_variable(name: String, value): var exists = false if '/' in name: var variable_id = _get_variable_from_file_name(name) if variable_id != '': for d in _get_definitions()['variables']: if d['id'] == variable_id: d['value'] = str(value) exists = true else: for d in _get_definitions()['variables']: if d['name'] == name: d['value'] = str(value) exists = true if exists == false: # TODO it would be great to automatically generate that missing variable here so they don't # have to create it from the editor. print("[Dialogic] Warning! the variable [" + name + "] doesn't exists. Create it from the Dialogic editor.") return value # returns the value of the value definition with the given name static func get_variable(name: String, default = null): if '/' in name: var variable_id = _get_variable_from_file_name(name) for d in _get_definitions()['variables']: if d['id'] == variable_id: return d['value'] print("[Dialogic] Warning! the variable [" + name + "] doesn't exists.") return default else: for d in _get_definitions()['variables']: if d['name'] == name: return d['value'] print("[Dialogic] Warning! the variable [" + name + "] doesn't exists.") return default ## +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## GAME STATE ## +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # the game state is a global dictionary that can be used to store custom data # these functions should be renamed in 2.0! These names are outdated. # this sets a value in the GAME STATE dictionary static func get_saved_state_general_key(key: String, default = '') -> String: if not Engine.get_main_loop().has_meta('game_state'): return default if key in Engine.get_main_loop().get_meta('game_state').keys(): return Engine.get_main_loop().get_meta('game_state')[key] else: return default # this gets a value from the GAME STATE dictionary static func set_saved_state_general_key(key: String, value) -> void: if not Engine.get_main_loop().has_meta('game_state'): Engine.get_main_loop().set_meta('game_state', {}) Engine.get_main_loop().get_meta('game_state')[key] = str(value) save('', true) ################################################################################ ## HISTORY ################################################################################ # Used to toggle the history timeline display. Only useful if you do not wish to # use the provided buttons static func toggle_history(): if has_current_dialog_node(): var dialog_node = Engine.get_main_loop().get_meta('latest_dialogic_node') dialog_node.HistoryTimeline._on_toggle_history() else: print('[D] Tried to toggle history, but no dialog node exists.') ################################################################################ ## AUTO-ADVANCE ################################################################################ static func auto_advance_on(toggle: bool, delay : float=2): if has_current_dialog_node(): var dialog_node = Engine.get_main_loop().get_meta('latest_dialogic_node') dialog_node.autoPlayMode = toggle dialog_node.autoWaitTime = float(delay) else: print('[D] Tried to toggle auto advance mode, but no dialog node exists.') ################################################################################ ## COULD BE USED ################################################################################ # these are old things, that have little use. static func get_autosave() -> bool: if Engine.get_main_loop().has_meta('autoload'): return Engine.get_main_loop().get_meta('autoload') return true static func set_autosave(autoload): Engine.get_main_loop().set_meta('autoload', autoload) static func set_current_timeline(timeline): Engine.get_main_loop().set_meta('current_timeline', timeline) return timeline static func get_current_timeline(): var timeline timeline = Engine.get_main_loop().get_meta('current_timeline') if timeline == null: timeline = '' return timeline # Returns a string with the action button set on the project settings static func get_action_button(): return DialogicResources.get_settings_value('input', 'default_action_key', 'dialogic_default_action') ################################################################################ ## NOT TO BE USED FROM OUTSIDE ################################################################################ ## this loads the saves definitions and returns the saves state_info ditionary static func _load_from_slot(slot_name: String = '') -> Dictionary: Engine.get_main_loop().set_meta('definitions', DialogicResources.get_saved_definitions(slot_name)) var state_info = DialogicResources.get_saved_state_info(slot_name) Engine.get_main_loop().set_meta('last_dialog_state', state_info.get('dialog_state', null)) Engine.get_main_loop().set_meta('game_state', state_info.get('game_state', null)) return state_info.get('dialog_state', {}) ## this saves the current definitions and the given state info into the save folder @save_name static func _save_state_and_definitions(save_name: String, state_info: Dictionary) -> void: DialogicResources.save_definitions(save_name, _get_definitions()) DialogicResources.save_state_info(save_name, state_info) static func _get_definitions() -> Dictionary: var definitions if Engine.get_main_loop().has_meta('definitions'): definitions = Engine.get_main_loop().get_meta('definitions') else: definitions = DialogicResources.get_default_definitions() Engine.get_main_loop().set_meta('definitions', definitions) return definitions # used by the DialogNode static func set_glossary_from_id(id: String, title: String, text: String, extra:String) -> void: var target_def: Dictionary; for d in _get_definitions()['glossary']: if d['id'] == id: target_def = d; if target_def != null: if title and title != "[No Change]": target_def['title'] = title if text and text != "[No Change]": target_def['text'] = text if extra and extra != "[No Change]": target_def['extra'] = extra # used by the DialogNode static func set_variable_from_id(id: String, value: String, operation: String) -> void: var target_def: Dictionary; for d in _get_definitions()['variables']: if d['id'] == id: target_def = d; if target_def != null: var converted_set_value = value var converted_target_value = target_def['value'] var is_number = converted_set_value.is_valid_float() and converted_target_value.is_valid_float() if is_number: converted_set_value = float(value) converted_target_value = float(target_def['value']) var result = target_def['value'] # Do nothing for -, * and / operations on string match operation: '=': result = converted_set_value '+': result = converted_target_value + converted_set_value '-': if is_number: result = converted_target_value - converted_set_value '*': if is_number: result = converted_target_value * converted_set_value '/': if is_number: result = converted_target_value / converted_set_value target_def['value'] = str(result) # tries to find the path of a given timeline static func _get_timeline_file_from_name(timeline_name_path: String) -> String: var timelines = DialogicUtil.get_full_resource_folder_structure()['folders']['Timelines'] # Checks for slash in the name, and uses the folder search if there is if '/' in timeline_name_path: #Add leading slash if its a path and it is missing, for paths that have subfolders but no leading slash if(timeline_name_path.left(1) != '/'): timeline_name_path = "/" + timeline_name_path var parts = timeline_name_path.split('/', false) # First check if it's a timeline in the root folder if parts.size() == 1: for t in DialogicUtil.get_timeline_list(): for f in timelines['files']: if t['file'] == f && t['name'] == parts[0]: return t['file'] if parts.size() > 1: var current_data var current_depth = 0 for p in parts: if current_depth == 0: # Starting the crawl if (timelines['folders'].has(p) ): current_data = timelines['folders'][p] else: return '' elif current_depth == parts.size() - 1: # The final destination for t in DialogicUtil.get_timeline_list(): for f in current_data['files']: if t['file'] == f && t['name'] == p: return t['file'] else: # Still going deeper if (current_data['folders'].size() > 0): if p in current_data['folders']: current_data = current_data['folders'][p] else: return '' else: return '' current_depth += 1 return '' else: # Searching for any timeline that could match that name for t in DialogicUtil.get_timeline_list(): if t['name'] == timeline_name_path: return t['file'] return '' static func _get_variable_from_file_name(variable_name_path: String) -> String: #First add the leading slash if it is missing so algorithm works properly if(variable_name_path.left(1) != '/'): variable_name_path = "/" + variable_name_path var definitions = DialogicUtil.get_full_resource_folder_structure()['folders']['Definitions'] var parts = variable_name_path.split('/', false) # Check the root if it's a variable in the root folder if parts.size() == 1: for t in _get_definitions()['variables']: for f in definitions['files']: if t['id'] == f && t['name'] == parts[0]: return t['id'] if parts.size() > 1: var current_data var current_depth = 0 for p in parts: if current_depth == 0: # Starting the crawl if (definitions['folders'].has(p)): current_data = definitions['folders'][p] else: return '' elif current_depth == parts.size() - 1: # The final destination for t in _get_definitions()['variables']: for f in current_data['files']: if t['id'] == f && t['name'] == p: return t['id'] else: # Still going deeper if (current_data['folders'].size() > 0): if p in current_data['folders']: current_data = current_data['folders'][p] else: return '' else: return '' current_depth += 1 return ''