@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'(?join|update|leave)\s*(")?(?(?(2)[^"\n]*|[^(: \n]*))(?(2)"|)(\W*\((?.*)\))?(\s*(?[^\[]*))?(\s*\[(?.*)\])?') ################################################################################ ## 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, {})