1 extends DialogicSubsystem
3 ## Subsystem that handles showing of dialog text (+text effects & modifiers), name label, and next indicator
7 signal about_to_show_text(info:Dictionary)
8 signal text_finished(info:Dictionary)
9 signal speaker_updated(character:DialogicCharacter)
10 signal textbox_visibility_changed(visible:bool)
12 signal animation_textbox_new_text
13 signal animation_textbox_show
14 signal animation_textbox_hide
16 # forwards of the dialog_text signals of all present dialog_text nodes
17 signal meta_hover_ended(meta:Variant)
18 signal meta_hover_started(meta:Variant)
19 signal meta_clicked(meta:Variant)
24 # used to color names without searching for all characters each time
25 var character_colors := {}
26 var color_regex := RegEx.new()
27 var text_already_read := false
29 var text_effects := {}
30 var parsed_text_effect_info: Array[Dictionary] = []
31 var text_effects_regex := RegEx.new()
32 enum TextModifierModes {ALL=-1, TEXT_ONLY=0, CHOICES_ONLY=1}
33 enum TextTypes {DIALOG_TEXT, CHOICE_TEXT}
34 var text_modifiers := []
37 ## set by the [speed] effect, multies the letter speed and [pause] effects
38 var _speed_multiplier := 1.0
39 ## stores the pure letter speed (unmultiplied)
40 var _pure_letter_speed := 0.1
41 var _letter_speed_absolute := false
43 var _voice_synced_text := false
49 ####################################################################################################
51 func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
52 update_dialog_text('', true)
53 update_name_label(null)
54 dialogic.current_state_info['speaker'] = ""
55 dialogic.current_state_info['text'] = ''
57 set_text_reveal_skippable(ProjectSettings.get_setting('dialogic/text/initial_text_reveal_skippable', true))
59 # TODO check whether this can happen on the node directly
60 for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
61 if text_node.start_hidden:
62 text_node.textbox_root.hide()
65 func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
66 update_textbox(dialogic.current_state_info.get('text', ''), true)
67 update_dialog_text(dialogic.current_state_info.get('text', ''), true)
68 var character: DialogicCharacter = null
69 if dialogic.current_state_info.get('speaker', ""):
70 character = load(dialogic.current_state_info.get('speaker', ""))
73 update_name_label(character)
76 func post_install() -> void:
77 dialogic.Settings.connect_to_change('text_speed', _update_user_speed)
79 collect_text_effects()
80 collect_text_modifiers()
86 ####################################################################################################
88 ## Applies modifiers, effects and coloring to the text
89 func parse_text(text:String, type:int=TextTypes.DIALOG_TEXT, variables := true, glossary := true, modifiers:= true, effects:= true, color_names:= true) -> String:
91 text = parse_text_modifiers(text, type)
92 if variables and dialogic.has_subsystem('VAR'):
93 text = dialogic.VAR.parse_variables(text)
95 text = parse_text_effects(text)
97 text = color_character_names(text)
98 if glossary and dialogic.has_subsystem('Glossary'):
99 text = dialogic.Glossary.parse_glossary(text)
103 ## When an event updates the text spoken, this can adjust the state of
104 ## the dialog text box.
105 ## This method is async.
106 func update_textbox(text: String, instant := false) -> void:
108 await hide_textbox(instant)
110 await show_textbox(instant)
112 if !dialogic.current_state_info['text'].is_empty():
113 animation_textbox_new_text.emit()
115 if dialogic.Animations.is_animating():
116 await dialogic.Animations.finished
119 ## Shows the given text on all visible DialogText nodes.
120 ## Instant can be used to skip all revieling.
121 ## If additional is true, the previous text will be kept.
122 func update_dialog_text(text: String, instant := false, additional := false) -> String:
125 if !instant: dialogic.current_state = dialogic.States.REVEALING_TEXT
128 dialogic.current_state_info['text'] += text
130 dialogic.current_state_info['text'] = text
132 for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
133 connect_meta_signals(text_node)
135 if text_node.enabled and (text_node == text_node.textbox_root or text_node.textbox_root.is_visible_in_tree()):
138 text_node.text = text
141 var current_character := get_current_speaker()
143 if current_character:
144 var character_prefix: String = current_character.custom_info.get(DialogicCharacterPrefixSuffixSection.PREFIX_CUSTOM_KEY, DialogicCharacterPrefixSuffixSection.DEFAULT_PREFIX)
145 var character_suffix: String = current_character.custom_info.get(DialogicCharacterPrefixSuffixSection.SUFFIX_CUSTOM_KEY, DialogicCharacterPrefixSuffixSection.DEFAULT_SUFFIX)
146 text = character_prefix + text + character_suffix
148 text_node.reveal_text(text, additional)
150 if !text_node.finished_revealing_text.is_connected(_on_dialog_text_finished):
151 text_node.finished_revealing_text.connect(_on_dialog_text_finished)
153 dialogic.current_state_info['text_parsed'] = (text_node as RichTextLabel).get_parsed_text()
155 # Reset speed multiplier
156 update_text_speed(-1, false, 1)
157 # Reset Auto-Advance temporarily and the No-Skip setting:
158 dialogic.Inputs.auto_advance.enabled_until_next_event = false
159 dialogic.Inputs.auto_advance.override_delay_for_current_event = -1
160 dialogic.Inputs.manual_advance.disabled_until_next_event = false
162 set_text_reveal_skippable(true, true)
167 func _on_dialog_text_finished() -> void:
168 text_finished.emit({'text':dialogic.current_state_info['text'], 'character':dialogic.current_state_info['speaker']})
171 ## Updates the visible name on all name labels nodes.
172 ## If a name changes, the [signal speaker_updated] signal is emitted.
173 func update_name_label(character:DialogicCharacter):
174 var character_path := character.resource_path if character else ""
175 var current_character_path: String = dialogic.current_state_info.get("speaker", "")
177 if character_path != current_character_path:
178 dialogic.current_state_info['speaker'] = character_path
179 speaker_updated.emit(character)
181 var name_label_text := get_character_name_parsed(character)
183 for name_label in get_tree().get_nodes_in_group('dialogic_name_label'):
184 name_label.text = name_label_text
186 if !'use_character_color' in name_label or name_label.use_character_color:
187 name_label.self_modulate = character.color
189 name_label.self_modulate = Color(1,1,1,1)
192 func update_typing_sound_mood(mood:Dictionary = {}) -> void:
193 for typing_sound in get_tree().get_nodes_in_group('dialogic_type_sounds'):
194 typing_sound.load_overwrite(mood)
197 ## instant skips the signal and thus possible animations
198 func show_textbox(instant:=false) -> void:
199 var emitted := instant
200 for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
201 if not text_node.enabled:
203 if not text_node.textbox_root.visible and not emitted:
204 animation_textbox_show.emit()
205 text_node.textbox_root.show()
206 if dialogic.Animations.is_animating():
207 await dialogic.Animations.finished
208 textbox_visibility_changed.emit(true)
211 text_node.textbox_root.show()
214 ## Instant skips the signal and thus possible animations
215 func hide_textbox(instant:=false) -> void:
216 dialogic.current_state_info['text'] = ''
217 var emitted := instant
218 for name_label in get_tree().get_nodes_in_group('dialogic_name_label'):
220 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:
221 animation_textbox_hide.emit()
222 if dialogic.Animations.is_animating():
223 await dialogic.Animations.finished
224 for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
225 if text_node.textbox_root.visible and !emitted:
226 textbox_visibility_changed.emit(false)
228 text_node.textbox_root.hide()
231 func is_textbox_visible() -> bool:
232 return get_tree().get_nodes_in_group('dialogic_dialog_text').any(func(x): return x.textbox_root.visible)
235 func show_next_indicators(question:=false, autoadvance:=false) -> void:
236 for next_indicator in get_tree().get_nodes_in_group('dialogic_next_indicator'):
237 if next_indicator.enabled:
238 if (question and 'show_on_questions' in next_indicator and next_indicator.show_on_questions) or \
239 (autoadvance and 'show_on_autoadvance' in next_indicator and next_indicator.show_on_autoadvance) or (!question and !autoadvance):
240 next_indicator.show()
242 next_indicator.hide()
245 func hide_next_indicators(_fake_arg :Variant= null) -> void:
246 for next_indicator in get_tree().get_nodes_in_group('dialogic_next_indicator'):
247 next_indicator.hide()
250 ## This method will sync the text speed to the voice audio clip length, if a
252 ## For instance, if the voice is playing for four seconds, the text will finish
253 ## revealing after this time.
254 ## This feature ignores Auto-Pauses on letters. Pauses via BBCode will desync
256 func set_text_voice_synced(enabled: bool = true) -> void:
257 _voice_synced_text = enabled
261 ## Returns whether voice-synced text is enabled.
262 func is_text_voice_synced() -> bool:
263 return _voice_synced_text
266 ## Sets how fast text will be revealed.
268 ## [param letter_speed] is the speed a single text character takes to appear
271 ## [param absolute] will force text to display at the given speed, regardless
272 ## of the user's text speed setting.
274 ## [param _speed_multiplier] adjusts the speed of the text, if set to -1,
275 ## the value won't be updated and the current value will persist.
277 ## [param _user_speed] adjusts the speed of the text, if set to -1, the
278 ## project setting 'text_speed' will be used.operator
279 func update_text_speed(letter_speed: float = -1,
281 speed_multiplier := _speed_multiplier,
282 user_speed: float = dialogic.Settings.get_setting('text_speed', 1)) -> void:
284 if letter_speed == -1:
285 letter_speed = ProjectSettings.get_setting('dialogic/text/letter_speed', 0.01)
287 _pure_letter_speed = letter_speed
288 _letter_speed_absolute = absolute
289 _speed_multiplier = speed_multiplier
291 for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
293 text_node.set_speed(letter_speed)
295 text_node.set_speed(letter_speed * _speed_multiplier * user_speed)
298 func set_text_reveal_skippable(skippable:= true, temp:=false) -> void:
299 if !dialogic.current_state_info.has('text_reveal_skippable'):
300 dialogic.current_state_info['text_reveal_skippable'] = {'enabled':false, 'temp_enabled':false}
303 dialogic.current_state_info['text_reveal_skippable']['temp_enabled'] = skippable
305 dialogic.current_state_info['text_reveal_skippable']['enabled'] = skippable
308 func is_text_reveal_skippable() -> bool:
309 return dialogic.current_state_info['text_reveal_skippable']['enabled'] and dialogic.current_state_info['text_reveal_skippable'].get('temp_enabled', true)
312 func skip_text_reveal() -> void:
313 for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
314 if text_node.is_visible_in_tree():
315 text_node.finish_text()
316 if dialogic.has_subsystem('Voice'):
317 dialogic.Voice.stop_audio()
322 #region TEXT EFFECTS & MODIFIERS
323 ####################################################################################################
325 func collect_text_effects() -> void:
326 var text_effect_names := ""
328 for indexer in DialogicUtil.get_indexers(true):
329 for effect in indexer._get_text_effects():
330 text_effects[effect.command] = {}
331 if effect.has('subsystem') and effect.has('method'):
332 text_effects[effect.command]['callable'] = Callable(dialogic.get_subsystem(effect.subsystem), effect.method)
333 elif effect.has('node_path') and effect.has('method'):
334 text_effects[effect.command]['callable'] = Callable(get_node(effect.node_path), effect.method)
337 text_effect_names += effect.command +"|"
338 text_effects_regex.compile("(?<!\\\\)\\[\\s*(?<command>"+text_effect_names.trim_suffix("|")+")\\s*(=\\s*(?<value>.+?)\\s*)?\\]")
341 ## Returns the string with all text effects removed
342 ## Use get_parsed_text_effects() after calling this to get all effect information
343 func parse_text_effects(text:String) -> String:
344 parsed_text_effect_info.clear()
345 var rtl := RichTextLabel.new()
346 rtl.bbcode_enabled = true
347 var position_correction := 0
348 var bbcode_correction := 0
349 for effect_match in text_effects_regex.search_all(text):
350 rtl.text = text.substr(0, effect_match.get_start()-position_correction)
351 bbcode_correction = effect_match.get_start()-position_correction-len(rtl.get_parsed_text())
352 # append [index] = [command, value] to effects dict
353 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()})
355 text = text.substr(0,effect_match.get_start()-position_correction)+text.substr(effect_match.get_start()-position_correction+len(effect_match.get_string()))
357 position_correction += len(effect_match.get_string())
358 text = text.replace('\\[', '[')
363 func execute_effects(current_index:int, text_node:Control, skipping := false) -> void:
364 # might have to execute multiple effects
366 if parsed_text_effect_info.is_empty():
368 if current_index != -1 and current_index < parsed_text_effect_info[0]['index']:
370 var effect: Dictionary = parsed_text_effect_info.pop_front()
371 await (effect['execution_info']['callable'] as Callable).call(text_node, skipping, effect['value'])
374 func collect_text_modifiers() -> void:
375 text_modifiers.clear()
376 for indexer in DialogicUtil.get_indexers(true):
377 for modifier in indexer._get_text_modifiers():
378 if modifier.has('subsystem') and modifier.has('method'):
379 text_modifiers.append({'method':Callable(dialogic.get_subsystem(modifier.subsystem), modifier.method)})
380 elif modifier.has('node_path') and modifier.has('method'):
381 text_modifiers.append({'method':Callable(get_node(modifier.node_path), modifier.method)})
382 text_modifiers[-1]['mode'] = modifier.get('mode', TextModifierModes.TEXT_ONLY)
385 func parse_text_modifiers(text:String, type:int=TextTypes.DIALOG_TEXT) -> String:
386 for mod in text_modifiers:
387 if mod.mode != TextModifierModes.ALL and type != -1 and type != mod.mode:
389 text = mod.method.call(text)
396 #region HELPERS & OTHER STUFF
397 ####################################################################################################
399 func _ready() -> void:
400 dialogic.event_handled.connect(hide_next_indicators)
403 var autopause_data: Dictionary = ProjectSettings.get_setting('dialogic/text/autopauses', {})
404 for i in autopause_data.keys():
405 _autopauses[RegEx.create_from_string(r"(?<!(\[|\{))["+i+r"](?!([^{}\[\]]*[\]\}]|$))")] = autopause_data[i]
408 ## Parses the character's display_name and returns the text that
409 ## should be rendered. Note that characters may have variables in their
410 ## name, therefore this function should be called to evaluate
411 ## any potential variables in a character's name.
412 func get_character_name_parsed(character:DialogicCharacter) -> String:
414 var translated_display_name := character.get_display_name_translated()
415 if dialogic.has_subsystem('VAR'):
416 return dialogic.VAR.parse_variables(translated_display_name)
418 return translated_display_name
422 ## Returns the [class DialogicCharacter] of the current speaker.
423 ## If there is no current speaker or the speaker is not found, returns null.
424 func get_current_speaker() -> DialogicCharacter:
425 var speaker_path: String = dialogic.current_state_info.get("speaker", "")
427 if speaker_path.is_empty():
430 var speaker_resource := load(speaker_path)
432 if speaker_resource == null:
435 var speaker_character := speaker_resource as DialogicCharacter
437 return speaker_character
440 func _update_user_speed(_user_speed:float) -> void:
441 update_text_speed(_pure_letter_speed, _letter_speed_absolute)
444 func connect_meta_signals(text_node: Node) -> void:
445 if not text_node.meta_clicked.is_connected(emit_meta_signal):
446 text_node.meta_clicked.connect(emit_meta_signal.bind("meta_clicked"))
448 if not text_node.meta_hover_started.is_connected(emit_meta_signal):
449 text_node.meta_hover_started.connect(emit_meta_signal.bind("meta_hover_started"))
451 if not text_node.meta_hover_ended.is_connected(emit_meta_signal):
452 text_node.meta_hover_ended.connect(emit_meta_signal.bind("meta_hover_ended"))
455 func emit_meta_signal(meta:Variant, sig:String) -> void:
456 emit_signal(sig, meta)
460 #region AUTOCOLOR NAMES
461 ################################################################################
463 func color_character_names(text:String) -> String:
464 if not ProjectSettings.get_setting('dialogic/text/autocolor_names', false):
467 collect_character_names()
470 for result in color_regex.search_all(text):
471 text = text.insert(result.get_start("name")+((9+8+8)*counter), '[color=#' + character_colors[result.get_string('name')].to_html() + ']')
472 text = text.insert(result.get_end("name")+9+8+((9+8+8)*counter), '[/color]')
478 func collect_character_names() -> void:
479 #don't do this at all if we're not using autocolor names to begin with
480 if not ProjectSettings.get_setting("dialogic/text/autocolor_names", false):
483 character_colors = {}
485 for dch_path in DialogicResourceUtil.get_character_directory().values():
486 var character := (load(dch_path) as DialogicCharacter)
488 if character.display_name:
489 if "{" in character.display_name and "}" in character.display_name:
490 character_colors[dialogic.VAR.parse_variables(character.display_name)] = character.color
492 character_colors[character.display_name] = character.color
494 for nickname in character.get_nicknames_translated():
495 nickname = nickname.strip_edges()
497 if "{" in nickname and "}" in nickname:
498 character_colors[dialogic.VAR.parse_variables(nickname)] = character.color
500 character_colors[nickname] = character.color
502 if dialogic.has_subsystem("Glossary"):
503 dialogic.Glossary.color_overrides.merge(character_colors, true)
505 var sorted_keys := character_colors.keys()
506 sorted_keys.sort_custom(sort_by_length)
508 var character_names := ""
509 for key in sorted_keys:
510 character_names += r"\Q" + key + r"\E|"
512 character_names = character_names.trim_suffix("|")
513 color_regex.compile(r"(?<=\W|^)(?<name>" + character_names + r")(?=\W|$)")
516 func sort_by_length(a:String, b:String) -> bool:
517 if a.length() > b.length():
523 #region DEFAULT TEXT EFFECTS & MODIFIERS
524 ################################################################################
526 func effect_pause(_text_node:Control, skipped:bool, argument:String) -> void:
530 # We want to ignore pauses if we're skipping.
531 if dialogic.Inputs.auto_skip.enabled:
534 var text_speed: float = dialogic.Settings.get_setting('text_speed', 1)
537 if argument.ends_with('!'):
538 await get_tree().create_timer(float(argument.trim_suffix('!'))).timeout
540 elif _speed_multiplier != 0 and text_speed != 0:
541 await get_tree().create_timer(float(argument) * _speed_multiplier * text_speed).timeout
543 elif _speed_multiplier != 0 and text_speed != 0:
544 await get_tree().create_timer(0.5 * _speed_multiplier * text_speed).timeout
547 func effect_speed(_text_node:Control, skipped:bool, argument:String) -> void:
551 update_text_speed(-1, false, float(argument))
553 update_text_speed(-1, false, 1)
556 func effect_lspeed(_text_node:Control, skipped:bool, argument:String) -> void:
560 if argument.ends_with('!'):
561 update_text_speed(float(argument.trim_suffix('!')), true)
563 update_text_speed(float(argument), false)
568 func effect_signal(_text_node:Control, _skipped:bool, argument:String) -> void:
569 dialogic.text_signal.emit(argument)
572 func effect_mood(_text_node:Control, _skipped:bool, argument:String) -> void:
573 if argument.is_empty(): return
574 if dialogic.current_state_info.get('speaker', ""):
575 update_typing_sound_mood(
576 load(dialogic.current_state_info.speaker).custom_info.get('sound_moods', {}).get(argument, {}))
579 var modifier_words_select_regex := RegEx.create_from_string(r"(?<!\\)\<[^\[\>]+(\/[^\>]*)\>")
580 func modifier_random_selection(text:String) -> String:
581 for replace_mod_match in modifier_words_select_regex.search_all(text):
582 var string: String = replace_mod_match.get_string().trim_prefix("<").trim_suffix(">")
583 string = string.replace('//', '<slash>')
584 var list: PackedStringArray = string.split('/')
585 var item: String = list[randi()%len(list)]
586 item = item.replace('<slash>', '/')
587 text = text.replace(replace_mod_match.get_string(), item.strip_edges())
591 func modifier_break(text:String) -> String:
592 return text.replace('[br]', '\n')
595 func modifier_autopauses(text:String) -> String:
596 var absolute: bool = ProjectSettings.get_setting('dialogic/text/absolute_autopauses', false)
597 for i in _autopauses.keys():
599 for result in i.search_all(text):
601 text = text.insert(result.get_end()+offset, '[pause='+str(_autopauses[i])+'!]')
602 offset += len('[pause='+str(_autopauses[i])+'!]')
604 text = text.insert(result.get_end()+offset, '[pause='+str(_autopauses[i])+']')
605 offset += len('[pause='+str(_autopauses[i])+']')