]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Modules/Character/subsystem_portraits.gd
Initial Godot project with Dialogic 2.0-Alpha-17
[wolf-seeking-sheep.git] / addons / dialogic / Modules / Character / subsystem_portraits.gd
1 extends DialogicSubsystem
2
3 ## Subsystem that manages portraits and portrait positions.
4
5 signal character_joined(info:Dictionary)
6 signal character_left(info:Dictionary)
7 signal character_portrait_changed(info:Dictionary)
8 signal character_moved(info:Dictionary)
9
10 ## Emitted when a portrait starts animating.
11 #signal portrait_animating(character_node: Node, portrait_node: Node, animation_name: String, animation_length: float)
12
13
14 ## The default portrait scene.
15 var default_portrait_scene: PackedScene = load(get_script().resource_path.get_base_dir().path_join('default_portrait.tscn'))
16
17
18 #region STATE
19 ####################################################################################################
20
21 func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
22         for character in dialogic.current_state_info.get('portraits', {}).keys():
23                 remove_character(load(character))
24         dialogic.current_state_info['portraits'] = {}
25
26
27 func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
28         if not "portraits" in dialogic.current_state_info:
29                 dialogic.current_state_info["portraits"] = {}
30
31         # Load Position Portraits
32         var portraits_info: Dictionary = dialogic.current_state_info.portraits.duplicate()
33         dialogic.current_state_info.portraits = {}
34         for character_path in portraits_info:
35                 var character_info: Dictionary = portraits_info[character_path]
36                 var character: DialogicCharacter = load(character_path)
37                 var container := dialogic.PortraitContainers.load_position_container(character.get_character_name())
38                 add_character(character, container, character_info.portrait, character_info.position_id)
39                 change_character_mirror(character, character_info.get('custom_mirror', false))
40                 change_character_z_index(character, character_info.get('z_index', 0))
41                 change_character_extradata(character, character_info.get('extra_data', ""))
42
43         # Load Speaker Portrait
44         var speaker: Variant = dialogic.current_state_info.get('speaker', "")
45         if speaker:
46                 dialogic.current_state_info['speaker'] = ""
47                 change_speaker(load(speaker))
48         dialogic.current_state_info['speaker'] = speaker
49
50
51 func pause() -> void:
52         for portrait in dialogic.current_state_info['portraits'].values():
53                 if portrait.node.has_meta('animation_node'):
54                         portrait.node.get_meta('animation_node').pause()
55
56
57 func resume() -> void:
58         for portrait in dialogic.current_state_info['portraits'].values():
59                 if portrait.node.has_meta('animation_node'):
60                         portrait.node.get_meta('animation_node').resume()
61
62
63 func _ready() -> void:
64         if !ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
65                 default_portrait_scene = load(ProjectSettings.get_setting('dialogic/portraits/default_portrait', ''))
66
67
68 #region MAIN METHODS
69 ####################################################################################################
70 ## The following methods allow manipulating portraits.
71 ## A portrait is made up of a character node [Node2D] that instances the portrait scene as it's child.
72 ## The character node is then always the child of a portrait container.
73 ## - Position (PortraitContainer)
74 ## ---- character_node (Node2D)
75 ## --------- portrait_node (e.g. default_portrait.tscn, or a custom portrait)
76 ##
77 ## Using these main methods a character can be present multiple times.
78 ## For a VN style, the "character" methods (next section) provide access based on the character.
79 ## - (That is what the character event uses)
80
81
82 ## Creates a new [character node] for the given [character], and add it to the given [portrait container].
83 func _create_character_node(character:DialogicCharacter, container:DialogicNode_PortraitContainer) -> Node:
84         if container == null:
85                 return null
86         var character_node := Node2D.new()
87         character_node.name = character.get_character_name()
88         character_node.set_meta('character', character)
89         container.add_child(character_node)
90         return character_node
91
92
93 ## Changes the portrait of a specific [character node].
94 func _change_portrait(character_node: Node2D, portrait: String, fade_animation:="", fade_length := 0.5) -> Dictionary:
95         var character: DialogicCharacter = character_node.get_meta('character')
96
97         if portrait.is_empty():
98                 portrait = character.default_portrait
99
100         var info := {'character':character, 'portrait':portrait, 'same_scene':false}
101
102         if not portrait in character.portraits.keys():
103                 print_debug('[Dialogic] Change to not-existing portrait will be ignored!')
104                 return info
105
106         # Path to the scene to use.
107         var scene_path: String = character.portraits[portrait].get('scene', '')
108
109         var portrait_node: Node = null
110         var previous_portrait: Node = null
111         var portrait_count := character_node.get_child_count()
112
113         if portrait_count > 0:
114                 previous_portrait = character_node.get_child(-1)
115
116         # Check if the scene is the same as the currently loaded scene.
117         if (not previous_portrait == null and
118                 previous_portrait.get_meta('scene', '') == scene_path and
119                 # Also check if the scene supports changing to the given portrait.
120                 previous_portrait._should_do_portrait_update(character, portrait)):
121                         portrait_node = previous_portrait
122                         info['same_scene'] = true
123
124         else:
125
126                 if ResourceLoader.exists(scene_path):
127                         var packed_scene: PackedScene = load(scene_path)
128
129                         if packed_scene:
130                                 portrait_node = packed_scene.instantiate()
131                         else:
132                                 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.')
133
134                 if !portrait_node:
135                         portrait_node = default_portrait_scene.instantiate()
136
137                 portrait_node.set_meta('scene', scene_path)
138
139
140         if portrait_node:
141                 portrait_node.set_meta('portrait', portrait)
142                 character_node.set_meta('portrait', portrait)
143
144                 DialogicUtil.apply_scene_export_overrides(portrait_node, character.portraits[portrait].get('export_overrides', {}))
145
146                 if portrait_node.has_method('_update_portrait'):
147                         portrait_node._update_portrait(character, portrait)
148
149                 if not portrait_node.is_inside_tree():
150                         character_node.add_child(portrait_node)
151
152                 _update_portrait_transform(portrait_node)
153
154                 ## Handle Cross-Animating
155                 if previous_portrait and previous_portrait != portrait_node:
156                         if not fade_animation.is_empty() and fade_length > 0:
157                                 var fade_out := _animate_node(previous_portrait, fade_animation, fade_length, 1, true)
158                                 var _fade_in := _animate_node(portrait_node, fade_animation, fade_length, 1, false)
159                                 fade_out.finished.connect(previous_portrait.queue_free)
160                         else:
161                                 previous_portrait.queue_free()
162
163         return info
164
165
166 ## Changes the mirroring of the given portrait.
167 ## Unless @force is false, this will take into consideration the character mirror,
168 ## portrait mirror and portrait position mirror settings.
169 func _change_portrait_mirror(character_node: Node2D, mirrored := false, force := false) -> void:
170         var latest_portrait := character_node.get_child(-1)
171
172         if latest_portrait.has_method('_set_mirror'):
173                 var character: DialogicCharacter = character_node.get_meta('character')
174                 var current_portrait_info := character.get_portrait_info(character_node.get_meta('portrait'))
175                 latest_portrait._set_mirror(force or (mirrored != character.mirror != character_node.get_parent().mirrored != current_portrait_info.get('mirror', false)))
176
177
178 func _change_portrait_extradata(character_node: Node2D, extra_data := "") -> void:
179         var latest_portrait := character_node.get_child(-1)
180
181         if latest_portrait.has_method('_set_extra_data'):
182                 latest_portrait._set_extra_data(extra_data)
183
184
185 func _update_character_transform(character_node:Node, time := 0.0) -> void:
186         for child in character_node.get_children():
187                 _update_portrait_transform(child, time)
188
189
190 func _update_portrait_transform(portrait_node: Node, time:float = 0.0) -> void:
191         var character_node: Node = portrait_node.get_parent()
192
193         var character: DialogicCharacter = character_node.get_meta('character')
194         var portrait_info: Dictionary = character.portraits.get(portrait_node.get_meta('portrait'), {})
195
196         # ignore the character scale on custom portraits that have 'ignore_char_scale' set to true
197         var apply_character_scale: bool = not portrait_info.get('ignore_char_scale', false)
198
199         var transform: Rect2 = character_node.get_parent().get_local_portrait_transform(
200                 portrait_node._get_covered_rect(),
201                 (character.scale * portrait_info.get('scale', 1))*int(apply_character_scale)+portrait_info.get('scale', 1)*int(!apply_character_scale))
202
203         var tween: Tween
204
205         if character_node.has_meta('move_tween'):
206                 if character_node.get_meta('move_tween').is_running():
207                         time = character_node.get_meta('move_time')-character_node.get_meta('move_tween').get_total_elapsed_time()
208                         tween = character_node.get_meta('move_tween')
209                         tween.stop()
210         if time == 0:
211                 character_node.position = transform.position
212                 portrait_node.position = character.offset + portrait_info.get('offset', Vector2())
213                 portrait_node.scale = transform.size
214         else:
215                 if not tween:
216                         tween = character_node.create_tween().set_parallel().set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
217                         character_node.set_meta('move_tween', tween)
218                         character_node.set_meta('move_time', time)
219                 tween.tween_method(DialogicUtil.multitween.bind(character_node, "position", "base"), character_node.position, transform.position, time)
220                 tween.tween_property(portrait_node, 'position',character.offset + portrait_info.get('offset', Vector2()), time)
221                 tween.tween_property(portrait_node, 'scale', transform.size, time)
222
223
224 ## Animates the node with the given animation.
225 ## Is used both on the character node (most animations) and the portrait nodes (cross-fade animations)
226 func _animate_node(node: Node, animation_path: String, length: float, repeats := 1, is_reversed := false) -> DialogicAnimation:
227         if node.has_meta('animation_node') and is_instance_valid(node.get_meta('animation_node')):
228                 node.get_meta('animation_node').queue_free()
229
230         var anim_script: Script = load(animation_path)
231         var anim_node := Node.new()
232         anim_node.set_script(anim_script)
233         anim_node = (anim_node as DialogicAnimation)
234         anim_node.node = node
235         anim_node.base_position = node.position
236         anim_node.base_scale = node.scale
237         anim_node.time = length
238         anim_node.repeats = repeats
239         anim_node.is_reversed = is_reversed
240
241         add_child(anim_node)
242         anim_node.animate()
243
244         node.set_meta("animation_path", animation_path)
245         node.set_meta("animation_length", length)
246         node.set_meta("animation_node", anim_node)
247
248         #if not is_silent:
249                 #portrait_animating.emit(portrait_node.get_parent(), portrait_node, animation_path, length)
250
251         return anim_node
252
253
254 ## Moves the given portrait to the given container.
255 func _move_character(character_node: Node2D, transform:="", time := 0.0, easing:= Tween.EASE_IN_OUT, trans:= Tween.TRANS_SINE) -> void:
256         var tween := character_node.create_tween().set_ease(easing).set_trans(trans).set_parallel()
257         if time == 0:
258                 tween.kill()
259                 tween = null
260         var container: DialogicNode_PortraitContainer = character_node.get_parent()
261         dialogic.PortraitContainers.move_container(container, transform, tween, time)
262
263         for portrait_node in character_node.get_children():
264                 _update_portrait_transform(portrait_node, time)
265
266
267 ## Changes the given portraits z_index.
268 func _change_portrait_z_index(character_node: Node, z_index:int, update_zindex:= true) -> void:
269         if update_zindex:
270                 character_node.get_parent().set_meta('z_index', z_index)
271
272                 var sorted_children := character_node.get_parent().get_parent().get_children()
273                 sorted_children.sort_custom(z_sort_portrait_containers)
274                 var idx := 0
275                 for con in sorted_children:
276                         con.get_parent().move_child(con, idx)
277                         idx += 1
278
279
280 ## Checks if [para, character] has joined the scene, if so, returns its
281 ## active [DialogicPortrait] node.
282 ##
283 ## The difference between an active and inactive nodes is whether the node is
284 ## the latest node. [br]
285 ## If a portrait is fading/animating from portrait A and B, both will exist
286 ## in the scene, but only the new portrait is active, even if it is not
287 ## fully visible yet.
288 func get_character_portrait(character: DialogicCharacter) -> DialogicPortrait:
289         if is_character_joined(character):
290                 var portrait_node: DialogicPortrait = dialogic.current_state_info['portraits'][character.resource_path].node.get_child(-1)
291                 return portrait_node
292
293         return null
294
295
296 func z_sort_portrait_containers(con1: DialogicNode_PortraitContainer, con2: DialogicNode_PortraitContainer) -> bool:
297         if con1.get_meta('z_index', 0) < con2.get_meta('z_index', 0):
298                 return true
299
300         return false
301
302
303 ## Private method to remove a [param portrait_node].
304 func _remove_portrait(portrait_node: Node) -> void:
305         portrait_node.get_parent().remove_child(portrait_node)
306         portrait_node.queue_free()
307
308
309 ## Gets the default animation length for joining characters
310 ## If Auto-Skip is enabled, limits the time.
311 func _get_join_default_length() -> float:
312         var default_time: float = ProjectSettings.get_setting('dialogic/animations/join_default_length', 0.5)
313
314         if dialogic.Inputs.auto_skip.enabled:
315                 default_time = min(default_time, dialogic.Inputs.auto_skip.time_per_event)
316
317         return default_time
318
319
320 ## Gets the default animation length for leaving characters
321 ## If Auto-Skip is enabled, limits the time.
322 func _get_leave_default_length() -> float:
323         var default_time: float = ProjectSettings.get_setting('dialogic/animations/leave_default_length', 0.5)
324
325         if dialogic.Inputs.auto_skip.enabled:
326                 default_time = min(default_time, dialogic.Inputs.auto_skip.time_per_event)
327
328         return default_time
329
330
331 ## Checks multiple cases to return a valid portrait to use.
332 func get_valid_portrait(character:DialogicCharacter, portrait:String) -> String:
333         if character == null:
334                 printerr('[Dialogic] Tried to use portrait "', portrait, '" on <null> character.')
335                 dialogic.print_debug_moment()
336                 return ""
337
338         if "{" in portrait and dialogic.has_subsystem("Expressions"):
339                 var test: Variant = dialogic.Expressions.execute_string(portrait)
340                 if test:
341                         portrait = str(test)
342
343         if not portrait in character.portraits:
344                 if not portrait.is_empty():
345                         printerr('[Dialogic] Tried to use invalid portrait "', portrait, '" on character "', DialogicResourceUtil.get_unique_identifier(character.resource_path), '". Using default portrait instead.')
346                         dialogic.print_debug_moment()
347                 portrait = character.default_portrait
348
349         if portrait.is_empty():
350                 portrait = character.default_portrait
351
352         return portrait
353
354 #endregion
355
356
357 #region Character Methods
358 ####################################################################################################
359 ## The following methods are used to manage character portraits with the following rules:
360 ##   - a character can only be present once with these methods.
361 ## Most of them will fail silently if the character isn't joined yet.
362
363
364 ## Adds a character at a position and sets it's portrait.
365 ## If the character is already joined it will only update, portrait, position, etc.
366 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:
367         if is_character_joined(character):
368                 change_character_portrait(character, portrait)
369
370                 if animation_name.is_empty():
371                         animation_length = _get_join_default_length()
372
373                 if animation_wait:
374                         dialogic.current_state = DialogicGameHandler.States.ANIMATING
375                         await get_tree().create_timer(animation_length).timeout
376                         dialogic.current_state = DialogicGameHandler.States.IDLE
377                 move_character(character, position_id, animation_length)
378                 change_character_mirror(character, mirrored)
379                 return
380
381         var container := dialogic.PortraitContainers.add_container(character.get_character_name())
382         var character_node := add_character(character, container, portrait, position_id)
383         if character_node == null:
384                 return null
385
386         dialogic.current_state_info['portraits'][character.resource_path] = {'portrait':portrait, 'node':character_node, 'position_id':position_id, 'custom_mirror':mirrored}
387
388         _change_portrait_mirror(character_node, mirrored)
389         _change_portrait_extradata(character_node, extra_data)
390         _change_portrait_z_index(character_node, z_index)
391
392         var info := {'character':character}
393         info.merge(dialogic.current_state_info['portraits'][character.resource_path])
394         character_joined.emit(info)
395
396         if animation_name.is_empty():
397                 animation_name = ProjectSettings.get_setting('dialogic/animations/join_default', "Fade In Up")
398                 animation_length = _get_join_default_length()
399                 animation_wait = ProjectSettings.get_setting('dialogic/animations/join_default_wait', true)
400
401         animation_name = DialogicPortraitAnimationUtil.guess_animation(animation_name, DialogicPortraitAnimationUtil.AnimationType.IN)
402
403         if animation_name and animation_length > 0:
404                 var anim: DialogicAnimation = _animate_node(character_node, animation_name, animation_length)
405                 if animation_wait:
406                         dialogic.current_state = DialogicGameHandler.States.ANIMATING
407                         await anim.finished
408                         dialogic.current_state = DialogicGameHandler.States.IDLE
409
410         return character_node
411
412
413 func add_character(character:DialogicCharacter, container: DialogicNode_PortraitContainer, portrait:String,  position_id:String) -> Node:
414         if is_character_joined(character):
415                 printerr('[DialogicError] Cannot add a already joined character. If this is intended call _create_character_node manually.')
416                 return null
417
418         portrait = get_valid_portrait(character, portrait)
419
420         if portrait.is_empty():
421                 return null
422
423         if not character:
424                 printerr('[DialogicError] Cannot call add_portrait() with null character.')
425                 return null
426
427         var character_node := _create_character_node(character, container)
428
429         if character_node == null:
430                 printerr('[Dialogic] Failed to join character to position ', position_id, ". Could not find position container.")
431                 return null
432
433
434         dialogic.current_state_info['portraits'][character.resource_path] = {'portrait':portrait, 'node':character_node, 'position_id':position_id}
435
436         _move_character(character_node, position_id)
437         _change_portrait(character_node, portrait)
438
439         return character_node
440
441
442 ## Changes the portrait of a character. Only works with joined characters.
443 func change_character_portrait(character: DialogicCharacter, portrait: String, fade_animation:="DEFAULT", fade_length := -1.0) -> void:
444         if not is_character_joined(character):
445                 return
446
447         portrait = get_valid_portrait(character, portrait)
448
449         if dialogic.current_state_info.portraits[character.resource_path].portrait == portrait:
450                 return
451
452         if fade_animation == "DEFAULT":
453                 fade_animation = ProjectSettings.get_setting('dialogic/animations/cross_fade_default', "Fade Cross")
454                 fade_length = ProjectSettings.get_setting('dialogic/animations/cross_fade_default_length', 0.5)
455
456         fade_animation = DialogicPortraitAnimationUtil.guess_animation(fade_animation, DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
457
458         var info := _change_portrait(dialogic.current_state_info.portraits[character.resource_path].node, portrait, fade_animation, fade_length)
459         dialogic.current_state_info.portraits[character.resource_path].portrait = info.portrait
460         _change_portrait_mirror(
461                         dialogic.current_state_info.portraits[character.resource_path].node,
462                         dialogic.current_state_info.portraits[character.resource_path].get('custom_mirror', false)
463                         )
464         character_portrait_changed.emit(info)
465
466
467 ## Changes the mirror of the given character. Only works with joined characters
468 func change_character_mirror(character:DialogicCharacter, mirrored:= false, force:= false) -> void:
469         if !is_character_joined(character):
470                 return
471
472         _change_portrait_mirror(dialogic.current_state_info.portraits[character.resource_path].node, mirrored, force)
473         dialogic.current_state_info.portraits[character.resource_path]['custom_mirror'] = mirrored
474
475
476 ## Changes the z_index of a character. Only works with joined characters
477 func change_character_z_index(character:DialogicCharacter, z_index:int, update_zindex:= true) -> void:
478         if !is_character_joined(character):
479                 return
480
481         _change_portrait_z_index(dialogic.current_state_info.portraits[character.resource_path].node, z_index, update_zindex)
482         if update_zindex:
483                 dialogic.current_state_info.portraits[character.resource_path]['z_index'] = z_index
484
485
486 ## Changes the extra data on the given character. Only works with joined characters
487 func change_character_extradata(character:DialogicCharacter, extra_data:="") -> void:
488         if !is_character_joined(character):
489                 return
490         _change_portrait_extradata(dialogic.current_state_info.portraits[character.resource_path].node, extra_data)
491         dialogic.current_state_info.portraits[character.resource_path]['extra_data'] = extra_data
492
493
494 ## Starts the given animation on the given character. Only works with joined characters
495 func animate_character(character: DialogicCharacter, animation_path: String, length: float, repeats := 1, is_reversed := false) -> DialogicAnimation:
496         if not is_character_joined(character):
497                 return null
498
499         animation_path = DialogicPortraitAnimationUtil.guess_animation(animation_path)
500
501         var character_node: Node = dialogic.current_state_info.portraits[character.resource_path].node
502
503         return _animate_node(character_node, animation_path, length, repeats, is_reversed)
504
505
506 ## Moves the given character to the given position. Only works with joined characters
507 func move_character(character:DialogicCharacter, position_id:String, time:= 0.0, easing:=Tween.EASE_IN_OUT, trans:=Tween.TRANS_SINE) -> void:
508         if !is_character_joined(character):
509                 return
510
511         if dialogic.current_state_info.portraits[character.resource_path].position_id == position_id:
512                 return
513
514         _move_character(dialogic.current_state_info.portraits[character.resource_path].node, position_id, time, easing, trans)
515         dialogic.current_state_info.portraits[character.resource_path].position_id = position_id
516         character_moved.emit({'character':character, 'position_id':position_id, 'time':time})
517
518
519 ## Removes a character with a given animation or the default animation.
520 func leave_character(character: DialogicCharacter, animation_name:= "", animation_length:= 0.0, animation_wait := false) -> void:
521         if not is_character_joined(character):
522                 return
523
524         if animation_name.is_empty():
525                 animation_name = ProjectSettings.get_setting('dialogic/animations/leave_default', "Fade Out Down")
526                 animation_length = _get_leave_default_length()
527                 animation_wait = ProjectSettings.get_setting('dialogic/animations/leave_default_wait', true)
528
529         animation_name = DialogicPortraitAnimationUtil.guess_animation(animation_name, DialogicPortraitAnimationUtil.AnimationType.OUT)
530
531         if not animation_name.is_empty():
532                 var character_node := get_character_node(character)
533
534                 var animation := _animate_node(character_node, animation_name, animation_length, 1, true)
535                 if animation_length > 0:
536                         if animation_wait:
537                                 dialogic.current_state = DialogicGameHandler.States.ANIMATING
538                                 await animation.finished
539                                 dialogic.current_state = DialogicGameHandler.States.IDLE
540                                 remove_character(character)
541                         else:
542                                 animation.finished.connect(func(): remove_character(character))
543                 else:
544                         remove_character(character)
545
546
547 ## Removes all joined characters with a given animation or the default animation.
548 func leave_all_characters(animation_name:="", animation_length:=0.0, animation_wait := false) -> void:
549         for character in get_joined_characters():
550                 await leave_character(character, animation_name, animation_length, animation_wait)
551
552
553 ## Finds the character node for a [param character].
554 ## Return `null` if the [param character] is not part of the scene.
555 func get_character_node(character: DialogicCharacter) -> Node:
556         if is_character_joined(character):
557                 if is_instance_valid(dialogic.current_state_info['portraits'][character.resource_path].node):
558                         return dialogic.current_state_info['portraits'][character.resource_path].node
559         return null
560
561
562 ## Removes the given characters portrait.
563 ## Only works with joined characters.
564 func remove_character(character: DialogicCharacter) -> void:
565         var character_node := get_character_node(character)
566
567         if is_instance_valid(character_node) and character_node is Node:
568                 var container := character_node.get_parent()
569                 container.get_parent().remove_child(container)
570                 container.queue_free()
571                 character_node.queue_free()
572                 character_left.emit({'character': character})
573
574         dialogic.current_state_info['portraits'].erase(character.resource_path)
575
576
577 func get_current_character() -> DialogicCharacter:
578         if dialogic.current_state_info.get('speaker', null):
579                 return load(dialogic.current_state_info.speaker)
580         return null
581
582
583
584 ## Returns true if the given character is currently joined.
585 func is_character_joined(character: DialogicCharacter) -> bool:
586         if character == null or not character.resource_path in dialogic.current_state_info['portraits']:
587                 return false
588
589         return true
590
591
592 ## Returns a list of the joined charcters (as resources)
593 func get_joined_characters() -> Array[DialogicCharacter]:
594         var chars: Array[DialogicCharacter] = []
595
596         for char_path: String in dialogic.current_state_info.get('portraits', {}).keys():
597                 chars.append(load(char_path))
598
599         return chars
600
601
602 ## Returns a dictionary with info on a given character.
603 ## Keys can be [joined, character, node (for the portrait node), position_id]
604 ## Only joined is included (and false) for not joined characters
605 func get_character_info(character:DialogicCharacter) -> Dictionary:
606         if is_character_joined(character):
607                 var info: Dictionary = dialogic.current_state_info['portraits'][character.resource_path]
608                 info['joined'] = true
609                 return info
610         else:
611                 return {'joined':false}
612
613 #endregion
614
615
616 #region SPEAKER PORTRAIT CONTAINERS
617 ####################################################################################################
618
619 ## Updates all portrait containers set to SPEAKER.
620 func change_speaker(speaker: DialogicCharacter = null, portrait := "") -> void:
621         for container: Node in get_tree().get_nodes_in_group('dialogic_portrait_con_speaker'):
622
623                 var just_joined := true
624                 for character_node: Node in container.get_children():
625                         if not character_node.get_meta('character') == speaker:
626                                 var leave_animation: String = ProjectSettings.get_setting('dialogic/animations/leave_default', "Fade Out")
627                                 leave_animation = DialogicPortraitAnimationUtil.guess_animation(leave_animation, DialogicPortraitAnimationUtil.AnimationType.OUT)
628                                 var leave_animation_length := _get_leave_default_length()
629
630                                 if leave_animation and leave_animation_length:
631                                         var animate_out := _animate_node(character_node, leave_animation, leave_animation_length, 1, true)
632                                         animate_out.finished.connect(character_node.queue_free)
633                                 else:
634                                         character_node.get_parent().remove_child(character_node)
635                                         character_node.queue_free()
636                         else:
637                                 just_joined = false
638
639                 if speaker == null or speaker.portraits.is_empty():
640                         continue
641
642                 if just_joined:
643                         _create_character_node(speaker, container)
644
645                 elif portrait.is_empty():
646                         continue
647
648                 if portrait.is_empty():
649                         portrait = speaker.default_portrait
650
651                 var character_node := container.get_child(-1)
652
653                 var fade_animation: String = ProjectSettings.get_setting('dialogic/animations/cross_fade_default', "Fade Cross")
654                 var fade_length: float = ProjectSettings.get_setting('dialogic/animations/cross_fade_default_length', 0.5)
655
656                 fade_animation = DialogicPortraitAnimationUtil.guess_animation(fade_animation, DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
657
658                 if container.portrait_prefix+portrait in speaker.portraits:
659                         portrait = container.portrait_prefix+portrait
660
661                 _change_portrait(character_node, portrait, fade_animation, fade_length)
662
663                 # if the character has no portraits _change_portrait won't actually add a child node
664                 if character_node.get_child_count() == 0:
665                         continue
666
667                 if just_joined:
668                         # Change speaker is called before the text is changed.
669                         # In styles where the speaker is IN the textbox,
670                         # this can mean the portrait container isn't sized correctly yet.
671                         character_node.hide()
672                         if not container.is_visible_in_tree():
673                                 await get_tree().process_frame
674                         character_node.show()
675                         var join_animation: String = ProjectSettings.get_setting('dialogic/animations/join_default', "Fade In Up")
676                         join_animation = DialogicPortraitAnimationUtil.guess_animation(join_animation, DialogicPortraitAnimationUtil.AnimationType.IN)
677                         var join_animation_length := _get_join_default_length()
678
679                         if join_animation and join_animation_length:
680                                 _animate_node(character_node, join_animation, join_animation_length)
681
682                 _change_portrait_mirror(character_node)
683
684         if speaker:
685                 if speaker.resource_path != dialogic.current_state_info['speaker']:
686                         if dialogic.current_state_info['speaker'] and is_character_joined(load(dialogic.current_state_info['speaker'])):
687                                 dialogic.current_state_info['portraits'][dialogic.current_state_info['speaker']].node.get_child(-1)._unhighlight()
688
689                         if speaker and is_character_joined(speaker):
690                                 dialogic.current_state_info['portraits'][speaker.resource_path].node.get_child(-1)._highlight()
691
692         elif dialogic.current_state_info['speaker'] and is_character_joined(load(dialogic.current_state_info['speaker'])):
693                 dialogic.current_state_info['portraits'][dialogic.current_state_info['speaker']].node.get_child(-1)._unhighlight()
694
695 #endregion
696
697
698 #region TEXT EFFECTS
699 ####################################################################################################
700
701 ## Called from the [portrait=something] text effect.
702 func text_effect_portrait(_text_node:Control, _skipped:bool, argument:String) -> void:
703         if argument:
704                 if dialogic.current_state_info.get('speaker', null):
705                         change_character_portrait(load(dialogic.current_state_info.speaker), argument)
706                         change_speaker(load(dialogic.current_state_info.speaker), argument)
707
708
709 ## Called from the [extra_data=something] text effect.
710 func text_effect_extradata(_text_node:Control, _skipped:bool, argument:String) -> void:
711         if argument:
712                 if dialogic.current_state_info.get('speaker', null):
713                         change_character_extradata(load(dialogic.current_state_info.speaker), argument)
714 #endregion