]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Modules/Text/subsystem_text.gd
Initial Godot project with Dialogic 2.0-Alpha-17
[wolf-seeking-sheep.git] / addons / dialogic / Modules / Text / subsystem_text.gd
1 extends DialogicSubsystem
2
3 ## Subsystem that handles showing of dialog text (+text effects & modifiers), name label, and next indicator
4
5 #region SIGNALS
6
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)
11
12 signal animation_textbox_new_text
13 signal animation_textbox_show
14 signal animation_textbox_hide
15
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)
20
21 #endregion
22
23
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
28
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 := []
35
36
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
42
43 var _voice_synced_text := false
44
45 var _autopauses := {}
46
47
48 #region STATE
49 ####################################################################################################
50
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'] = ''
56
57         set_text_reveal_skippable(ProjectSettings.get_setting('dialogic/text/initial_text_reveal_skippable', true))
58
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()
63
64
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', ""))
71
72         if character:
73                 update_name_label(character)
74
75
76 func post_install() -> void:
77         dialogic.Settings.connect_to_change('text_speed', _update_user_speed)
78
79         collect_text_effects()
80         collect_text_modifiers()
81
82 #endregion
83
84
85 #region MAIN METHODS
86 ####################################################################################################
87
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:
90         if modifiers:
91                 text = parse_text_modifiers(text, type)
92         if variables and dialogic.has_subsystem('VAR'):
93                 text = dialogic.VAR.parse_variables(text)
94         if effects:
95                 text = parse_text_effects(text)
96         if color_names:
97                 text = color_character_names(text)
98         if glossary and dialogic.has_subsystem('Glossary'):
99                 text = dialogic.Glossary.parse_glossary(text)
100         return text
101
102
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:
107         if text.is_empty():
108                 await hide_textbox(instant)
109         else:
110                 await show_textbox(instant)
111
112                 if !dialogic.current_state_info['text'].is_empty():
113                         animation_textbox_new_text.emit()
114
115                         if dialogic.Animations.is_animating():
116                                 await dialogic.Animations.finished
117
118
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:
123         update_text_speed()
124
125         if !instant: dialogic.current_state = dialogic.States.REVEALING_TEXT
126
127         if additional:
128                 dialogic.current_state_info['text'] += text
129         else:
130                 dialogic.current_state_info['text'] = text
131
132         for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
133                 connect_meta_signals(text_node)
134
135                 if text_node.enabled and (text_node == text_node.textbox_root or text_node.textbox_root.is_visible_in_tree()):
136
137                         if instant:
138                                 text_node.text = text
139
140                         else:
141                                 var current_character := get_current_speaker()
142
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
147
148                                 text_node.reveal_text(text, additional)
149
150                                 if !text_node.finished_revealing_text.is_connected(_on_dialog_text_finished):
151                                         text_node.finished_revealing_text.connect(_on_dialog_text_finished)
152
153                         dialogic.current_state_info['text_parsed'] = (text_node as RichTextLabel).get_parsed_text()
154
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
161
162         set_text_reveal_skippable(true, true)
163
164         return text
165
166
167 func _on_dialog_text_finished() -> void:
168         text_finished.emit({'text':dialogic.current_state_info['text'], 'character':dialogic.current_state_info['speaker']})
169
170
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", "")
176
177         if character_path != current_character_path:
178                 dialogic.current_state_info['speaker'] = character_path
179                 speaker_updated.emit(character)
180
181         var name_label_text := get_character_name_parsed(character)
182
183         for name_label in get_tree().get_nodes_in_group('dialogic_name_label'):
184                 name_label.text = name_label_text
185                 if character:
186                         if !'use_character_color' in name_label or name_label.use_character_color:
187                                 name_label.self_modulate = character.color
188                 else:
189                         name_label.self_modulate = Color(1,1,1,1)
190
191
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)
195
196
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:
202                         continue
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)
209                         emitted = true
210                 else:
211                         text_node.textbox_root.show()
212
213
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'):
219                 name_label.text = ""
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)
227                         emitted = true
228                 text_node.textbox_root.hide()
229
230
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)
233
234
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()
241                 else:
242                         next_indicator.hide()
243
244
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()
248
249
250 ## This method will sync the text speed to the voice audio clip length, if a
251 ## voice is playing.
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
255 ## the reveal.
256 func set_text_voice_synced(enabled: bool = true) -> void:
257         _voice_synced_text = enabled
258         update_text_speed()
259
260
261 ## Returns whether voice-synced text is enabled.
262 func is_text_voice_synced() -> bool:
263         return _voice_synced_text
264
265
266 ## Sets how fast text will be revealed.
267 ## [br][br]
268 ## [param letter_speed] is the speed a single text character takes to appear
269 ## on the textbox.
270 ## [br][br]
271 ## [param absolute] will force text to display at the given speed, regardless
272 ## of the user's text speed setting.
273 ## [br][br]
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.
276 ## [br][br]
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,
280                 absolute := false,
281                 speed_multiplier := _speed_multiplier,
282                 user_speed: float = dialogic.Settings.get_setting('text_speed', 1)) -> void:
283
284         if letter_speed == -1:
285                 letter_speed = ProjectSettings.get_setting('dialogic/text/letter_speed', 0.01)
286
287         _pure_letter_speed = letter_speed
288         _letter_speed_absolute = absolute
289         _speed_multiplier = speed_multiplier
290
291         for text_node in get_tree().get_nodes_in_group('dialogic_dialog_text'):
292                 if absolute:
293                         text_node.set_speed(letter_speed)
294                 else:
295                         text_node.set_speed(letter_speed * _speed_multiplier * user_speed)
296
297
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}
301
302         if temp:
303                 dialogic.current_state_info['text_reveal_skippable']['temp_enabled'] = skippable
304         else:
305                 dialogic.current_state_info['text_reveal_skippable']['enabled'] = skippable
306
307
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)
310
311
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()
318
319 #endregion
320
321
322 #region TEXT EFFECTS & MODIFIERS
323 ####################################################################################################
324
325 func collect_text_effects() -> void:
326         var text_effect_names := ""
327         text_effects.clear()
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)
335                         else:
336                                 continue
337                         text_effect_names += effect.command +"|"
338         text_effects_regex.compile("(?<!\\\\)\\[\\s*(?<command>"+text_effect_names.trim_suffix("|")+")\\s*(=\\s*(?<value>.+?)\\s*)?\\]")
339
340
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()})
354
355                 text = text.substr(0,effect_match.get_start()-position_correction)+text.substr(effect_match.get_start()-position_correction+len(effect_match.get_string()))
356
357                 position_correction += len(effect_match.get_string())
358         text = text.replace('\\[', '[')
359         rtl.queue_free()
360         return text
361
362
363 func execute_effects(current_index:int, text_node:Control, skipping := false) -> void:
364         # might have to execute multiple effects
365         while true:
366                 if parsed_text_effect_info.is_empty():
367                         return
368                 if current_index != -1 and current_index < parsed_text_effect_info[0]['index']:
369                         return
370                 var effect: Dictionary = parsed_text_effect_info.pop_front()
371                 await (effect['execution_info']['callable'] as Callable).call(text_node, skipping, effect['value'])
372
373
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)
383
384
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:
388                         continue
389                 text = mod.method.call(text)
390         return text
391
392
393 #endregion
394
395
396 #region HELPERS & OTHER STUFF
397 ####################################################################################################
398
399 func _ready() -> void:
400         dialogic.event_handled.connect(hide_next_indicators)
401
402         _autopauses = {}
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]
406
407
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:
413         if character:
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)
417                 else:
418                         return translated_display_name
419         return ""
420
421
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", "")
426
427         if speaker_path.is_empty():
428                 return null
429
430         var speaker_resource := load(speaker_path)
431
432         if speaker_resource == null:
433                 return null
434
435         var speaker_character := speaker_resource as DialogicCharacter
436
437         return speaker_character
438
439
440 func _update_user_speed(_user_speed:float) -> void:
441         update_text_speed(_pure_letter_speed, _letter_speed_absolute)
442
443
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"))
447
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"))
450
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"))
453
454
455 func emit_meta_signal(meta:Variant, sig:String) -> void:
456         emit_signal(sig, meta)
457
458 #endregion
459
460 #region AUTOCOLOR NAMES
461 ################################################################################
462
463 func color_character_names(text:String) -> String:
464         if not ProjectSettings.get_setting('dialogic/text/autocolor_names', false):
465                 return text
466
467         collect_character_names()
468
469         var counter := 0
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]')
473                 counter += 1
474
475         return text
476
477
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):
481                 return
482
483         character_colors = {}
484
485         for dch_path in DialogicResourceUtil.get_character_directory().values():
486                 var character := (load(dch_path) as DialogicCharacter)
487
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
491                         else:
492                                 character_colors[character.display_name] = character.color
493
494                 for nickname in character.get_nicknames_translated():
495                         nickname = nickname.strip_edges()
496                         if nickname:
497                                 if "{" in nickname and "}" in nickname:
498                                         character_colors[dialogic.VAR.parse_variables(nickname)] = character.color
499                                 else:
500                                         character_colors[nickname] = character.color
501
502         if dialogic.has_subsystem("Glossary"):
503                 dialogic.Glossary.color_overrides.merge(character_colors, true)
504
505         var sorted_keys := character_colors.keys()
506         sorted_keys.sort_custom(sort_by_length)
507
508         var character_names := ""
509         for key in sorted_keys:
510                 character_names += r"\Q" + key + r"\E|"
511
512         character_names = character_names.trim_suffix("|")
513         color_regex.compile(r"(?<=\W|^)(?<name>" + character_names + r")(?=\W|$)")
514
515
516 func sort_by_length(a:String, b:String) -> bool:
517         if a.length() > b.length():
518                 return true
519         return false
520 #endregion+
521
522
523 #region DEFAULT TEXT EFFECTS & MODIFIERS
524 ################################################################################
525
526 func effect_pause(_text_node:Control, skipped:bool, argument:String) -> void:
527         if skipped:
528                 return
529
530         # We want to ignore pauses if we're skipping.
531         if dialogic.Inputs.auto_skip.enabled:
532                 return
533
534         var text_speed: float = dialogic.Settings.get_setting('text_speed', 1)
535
536         if argument:
537                 if argument.ends_with('!'):
538                         await get_tree().create_timer(float(argument.trim_suffix('!'))).timeout
539
540                 elif _speed_multiplier != 0 and text_speed != 0:
541                         await get_tree().create_timer(float(argument) * _speed_multiplier * text_speed).timeout
542
543         elif _speed_multiplier != 0 and text_speed != 0:
544                 await get_tree().create_timer(0.5 * _speed_multiplier * text_speed).timeout
545
546
547 func effect_speed(_text_node:Control, skipped:bool, argument:String) -> void:
548         if skipped:
549                 return
550         if argument:
551                 update_text_speed(-1, false, float(argument))
552         else:
553                 update_text_speed(-1, false, 1)
554
555
556 func effect_lspeed(_text_node:Control, skipped:bool, argument:String) -> void:
557         if skipped:
558                 return
559         if argument:
560                 if argument.ends_with('!'):
561                         update_text_speed(float(argument.trim_suffix('!')), true)
562                 else:
563                         update_text_speed(float(argument), false)
564         else:
565                 update_text_speed()
566
567
568 func effect_signal(_text_node:Control, _skipped:bool, argument:String) -> void:
569         dialogic.text_signal.emit(argument)
570
571
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, {}))
577
578
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())
588         return text
589
590
591 func modifier_break(text:String) -> String:
592         return text.replace('[br]', '\n')
593
594
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():
598                 var offset := 0
599                 for result in i.search_all(text):
600                         if absolute:
601                                 text = text.insert(result.get_end()+offset, '[pause='+str(_autopauses[i])+'!]')
602                                 offset += len('[pause='+str(_autopauses[i])+'!]')
603                         else:
604                                 text = text.insert(result.get_end()+offset, '[pause='+str(_autopauses[i])+']')
605                                 offset += len('[pause='+str(_autopauses[i])+']')
606         return text
607 #endregion