@tool @icon("node_portrait_container_icon.svg") class_name DialogicNode_PortraitContainer extends Control ## Node that defines a position for dialogic portraits and how to display portraits at that position. enum PositionModes { POSITION, ## This container can be joined/moved to with the Character Event SPEAKER, ## This container is joined/left automatically based on the speaker. _CHARACTER, ## This container holds a specific character. These are created at runtime and shouldn't be assigned manually. } @export var mode := PositionModes.POSITION @export_subgroup("Mode: Position") ## The position names this node corresponds to. @export var container_ids: PackedStringArray = ["1"] @export_subgroup("Mode: Speaker") ## Can be used to use a different portrait. ## E.g. "Faces/" would mean instead of "happy" it will use portrait "Faces/happy" @export var portrait_prefix := "" @export_subgroup("Portrait Placement") enum SizeModes { KEEP, ## The height and width of the container have no effect, only the origin. FIT_STRETCH, ## The portrait will be fitted into the container, ignoring its aspect ratio and the character/portrait scale. FIT_IGNORE_SCALE, ## The portrait will be fitted into the container, ignoring the character/portrait scale, but preserving the aspect ratio. FIT_SCALE_HEIGHT ## Recommended. The portrait will be scaled to fit the container height. A character/portrait scale of 100% means 100% container height. Aspect ratio will be preserved. } ## Defines how to affect the scale of the portrait @export var size_mode: SizeModes = SizeModes.FIT_SCALE_HEIGHT : set(mode): size_mode = mode _update_debug_portrait_transform() ## If true, portraits will be mirrored in this position. @export var mirrored := false : set(mirror): mirrored = mirror update_mirror() _update_debug_portrait_scene() @export_group("Container Transform", "container") ## The containers size either in pixels or as a percentage of the parent size. @export var container_size := DialogicFlexVector.new(): set(siz): if siz == null: container_size = DialogicFlexVector.new() container_size.container_size = get_parent_control().size container_size.overwrite_from_vector(size) else: container_size = siz update_transform_from_properties.call_deferred() if container_size != null and not container_size.changed.is_connected(update_transform_from_properties): container_size.changed.connect(update_transform_from_properties) ## The containers origin position, either as an absolute position or as a percentage of the parent size. @export var container_position := DialogicFlexVector.new(): set(pos): if pos == null: container_position = DialogicFlexVector.new() container_position.container_size = get_parent_control().size container_position.overwrite_from_vector(position+current_settings._get_origin_position()) else: container_position = pos update_transform_from_properties.call_deferred() if container_position != null and not container_position.changed.is_connected(update_transform_from_properties): container_position.changed.connect(update_transform_from_properties) @export_range(-360, 360, 0.01, "degrees", "or_less", "or_greater") var container_rotation := 0.0: set(rot): container_rotation = rot rotation_degrees = rot update_transform_from_properties.call_deferred() ## The portrait will be placed relative to this point in the container. @export var container_origin := DialogicFlexVector.from_string("x0.5y1"): set(ori): if ori == null: container_origin = DialogicFlexVector.new() container_origin.container_size = container_size.in_pixels() container_origin.x_value = 0.5 container_origin.y_value = 1.0 else: container_origin = ori update_transform_from_properties.call_deferred() if container_origin != null and not container_origin.changed.is_connected(update_transform_from_properties): container_origin.changed.connect(update_transform_from_properties) ## Can be set at runtime. Will adjust the ordering of this container and all its siblings var container_z_index := 0: set(z): if container_z_index != z: container_z_index = z update_z_index() else: container_z_index = z var movement_tween : Tween = null var movement_time := 0.0 #region debug_properties @export_group("Debug", "debug") ## A character that will be displayed in the editor, useful for getting the right size. @export var debug_character: DialogicCharacter = null: set(character): debug_character = character _update_debug_portrait_scene() @export var debug_character_portrait := "": set(portrait): debug_character_portrait = portrait _update_debug_portrait_scene() var debug_character_holder_node: Node2D = null var debug_character_scene_node: Node = null var default_portrait_scene: String = DialogicUtil.get_module_path("Character").path_join("default_portrait.tscn") var default_debug_character := load("uid://dykf1j17ct5mo") ## If true the outline, position-name and origin icon are drawn in-game like they are in the editor. ## This value can be controlled for all containers by setting Dialogic.PortraitContainers.debug_draw var debug_draw := false #endregion ## Used to avoid infinitely loops when updating transforms. var _ignore_transform_change := false ## Usually contains the containers current settings as a ContainerSettings var current_settings: ContainerSettings ## While moving, this contains the containers target transform var target_settings: ContainerSettings func _ready() -> void: if not container_position.changed.is_connected(update_transform_from_properties): container_position.changed.connect(update_transform_from_properties) if not container_size.changed.is_connected(update_transform_from_properties): container_size.changed.connect(update_transform_from_properties) if not container_origin.changed.is_connected(update_transform_from_properties): container_origin.changed.connect(update_transform_from_properties) current_settings = ContainerSettings.from_container(self) # For pre alpha 21 compatibility if container_size.x_value == 0 and container_size.y_value == 0: update_properties_from_transform() set_anchor(SIDE_BOTTOM, 0, true) set_anchor(SIDE_LEFT, 0, true) set_anchor(SIDE_RIGHT, 0, true) set_anchor(SIDE_TOP, 0, true) update_transform_from_properties.call_deferred() match mode: PositionModes.POSITION, PositionModes._CHARACTER: add_to_group("dialogic_portrait_con_position") PositionModes.SPEAKER: add_to_group("dialogic_portrait_con_speaker") if Engine.is_editor_hint(): _update_debug_portrait_scene() if not ProjectSettings.get_setting("dialogic/portraits/default_portrait", "").is_empty(): default_portrait_scene = ProjectSettings.get_setting("dialogic/portraits/default_portrait", "") item_rect_changed.connect(update_properties_from_transform) else: debug_draw = DialogicUtil.autoload().PortraitContainers.debug_draw item_rect_changed.connect(container_resized_during_game) child_entered_tree.connect(_child_entered_tree) connect_parent_resize() func connect_parent_resize() -> void: if not get_parent().resized.is_connected(_on_parent_resized): get_parent().resized.connect(_on_parent_resized) #region MAIN METHODS ################################################################################ ## Tweens the containers values to [param to_settings] values. func update_container(to_settings: ContainerSettings, time:=0.0, easing:=Tween.EASE_IN_OUT, trans:=Tween.TRANS_SINE, set_z_index:=true, set_mirror:=true) -> void: if movement_tween and movement_tween.is_running(): movement_tween.kill() ## This handles moving the container to a different parent if necessary. if to_settings.reference_position_id: var ref_con := DialogicUtil.autoload().PortraitContainers.get_container(to_settings.reference_position_id) if ref_con.get_parent() != get_parent(): var global_origin: Vector2 = global_position+current_settings._get_origin_position() var global_size := container_size.as_pixels() get_parent().remove_child(self) ref_con.get_parent().add_child(self) connect_parent_resize() set_parent_size(get_parent_control().size) container_position.overwrite_from_vector(global_origin-get_parent().global_position) container_size.overwrite_from_vector(global_size) movement_tween = create_tween() movement_time = time movement_tween.set_parallel(true).set_ease(easing).set_trans(trans) target_settings = to_settings to_settings.set_parent_size(get_parent_control().size) movement_tween.tween_property(self, "size_mode", to_settings.size_mode, time) container_position.make_similar(to_settings.position) movement_tween.tween_property(container_position, "x_value", to_settings.position.x_value, time) movement_tween.tween_property(container_position, "y_value", to_settings.position.y_value, time) movement_tween.tween_property(self, 'container_rotation', to_settings.rotation, time) container_size.make_similar(to_settings.size) movement_tween.tween_property(container_size, "x_value", to_settings.size.x_value, time) movement_tween.tween_property(container_size, "y_value", to_settings.size.y_value, time) if set_mirror: mirrored = to_settings.mirrored container_origin.make_similar(to_settings.origin) movement_tween.tween_property(container_origin, "x_value", to_settings.origin.x_value, time) movement_tween.tween_property(container_origin, "y_value", to_settings.origin.y_value, time) if set_z_index: container_z_index = to_settings.z_index movement_tween.finished.connect(current_settings.update_from_container.bind(self)) movement_tween.finished.connect(set.bind("target_settings", null)) # tween the portraits transforms towards their final transforms in to_settings. for character_node in get_children(): if not character_node.has_meta("character"): continue movement_tween.tween_property(character_node, "position", to_settings._get_origin_position(), time) for portrait_node in character_node.get_children(): if not portrait_node is DialogicPortrait: continue update_portrait_transform(portrait_node, to_settings, movement_tween, time) # if time is zero, make the movement_tween finish instantly if time == 0: movement_tween.custom_step(99) ## Tween the scale and position ofthe given [param portrait_node] to fit into [param container_settings]. func update_portrait_transform(portrait_node:DialogicPortrait, container_settings:ContainerSettings, tween:Tween=null, time:float=0.0) -> void: var character: DialogicCharacter = portrait_node.get_parent().get_meta("character") var target_pos: Vector2 = character.offset + character.get_portrait_info(portrait_node.portrait).get("offset", Vector2()) var target_scale := container_settings.get_portrait_scale(portrait_node) if tween == null or time == 0: portrait_node.position = target_pos portrait_node.scale = target_scale else: tween.tween_property(portrait_node, "position", target_pos, time) tween.tween_property(portrait_node, "scale", target_scale, time) ## When the container is resized during the game ## make sure to reposition the character nodes at the origin and update the portrait transform. func container_resized_during_game() -> void: current_settings.update_from_container(self) for child in get_children(): # reposition character nodes at the origin if child.has_meta("character"): child.position = current_settings._get_origin_position() # update portrait transforms for grand_child in child.get_children(): if grand_child is DialogicPortrait: update_portrait_transform(grand_child, current_settings) ## When a new character is added to this container, ## position it correctly and start listening to children (portraits) being added to it. func _child_entered_tree(child:Node) -> void: if child.has_meta("character"): # positon character node at the origin current_settings.update_from_container(self) child.position = current_settings._get_origin_position() # start listening to children (portraits) being added. if not child.child_entered_tree.is_connected(_portrait_entered_tree): child.child_entered_tree.connect(_portrait_entered_tree) ## When portraits are added to a character, update their transforms and mirror. func _portrait_entered_tree(grand_child:Node) -> void: if grand_child is DialogicPortrait: current_settings.update_from_container(self) update_portrait_transform(grand_child, current_settings, null, 0) update_mirror() ## Update the mirror of all the portraits in this container. func update_mirror() -> void: if current_settings: current_settings.update_from_container(self) for character_node in get_children(): if not character_node.has_meta("character"): continue for portrait_node in character_node.get_children(): if not portrait_node is DialogicPortrait: continue var info: Dictionary = portrait_node.character.get_portrait_info(portrait_node.portrait) var resulting_mirror: bool = mirrored != portrait_node.character.mirror != info.get("mirror", false) portrait_node._set_mirror(resulting_mirror) ## Update the sorting of this node and its PortraitContainer siblings based on their z-index variable. func update_z_index() -> void: if current_settings: current_settings.update_from_container(self) var sorted_children := get_parent().get_children().filter(func(x): return x is DialogicNode_PortraitContainer) sorted_children.sort_custom(func(con1, con2): return con1.container_z_index < con2.container_z_index) var idx := 0 for con in sorted_children: con.get_parent().move_child(con, idx) idx += 1 func _on_parent_resized() -> void: update_transform_from_properties() ## Updates [member container_size], [member container_position], [member container_container_origin] ## and [member container_rotation] based on the transform (position, size, rotation) of this node. func update_properties_from_transform() -> void: if _ignore_transform_change: return current_settings.update_from_container(self) _ignore_transform_change = true set_parent_size(get_parent_control().size) container_size.overwrite_from_vector(size) container_origin.container_size = container_size.as_pixels() container_rotation = rotation_degrees container_position.overwrite_from_vector(position+current_settings._get_origin_position().rotated(rotation)) _ignore_transform_change = false ## Updates size, position and rotation based on [member container_size], [member container_position], ## [member container_container_origin] and [member container_rotation]. func update_transform_from_properties() -> void: if _ignore_transform_change: return if not is_node_ready(): await ready current_settings.update_from_container(self) _ignore_transform_change = true set_parent_size(get_parent_control().size) size = container_size.as_pixels() position = current_settings._get_top_left_position() _ignore_transform_change = false queue_redraw() ## Returns true if the given [param id] is one of the container_id's of this container. func is_container(id:Variant) -> bool: return str(id) in container_ids #endregion #region DEBUG METHODS ################################################################################ ### USE THIS TO DEBUG THE POSITIONS var pivot_texture := load("res://addons/dialogic/Editor/Images/Resources/portrait.svg") func _draw(): if debug_draw or Engine.is_editor_hint(): draw_rect(Rect2(Vector2(), size), Color(1, 0.3098039329052, 1), false, 2) if container_ids: draw_string(get_theme_default_font(),get_theme_default_font().get_string_size(container_ids[0], HORIZONTAL_ALIGNMENT_LEFT, 1, get_theme_default_font_size())+Vector2(8,0) , container_ids[0], HORIZONTAL_ALIGNMENT_CENTER) var pivot_draw_size := 32 draw_texture_rect(pivot_texture, Rect2(current_settings._get_origin_position()-Vector2.ONE*pivot_draw_size*0.5, Vector2.ONE*pivot_draw_size), false, Color(1.0, 0.966, 0.997, 1.0)) _update_debug_portrait_transform() ## Loads the debug_character with the debug_character_portrait ## Creates a holder node and applies mirror func _update_debug_portrait_scene() -> void: if not Engine.is_editor_hint(): return if is_instance_valid(debug_character_holder_node): for child in get_children(): child.free() # Get character var character := _get_debug_character() if not character is DialogicCharacter or character.portraits.is_empty(): return # Determine portrait var debug_portrait := debug_character_portrait if debug_portrait.is_empty(): debug_portrait = character.default_portrait if mode == PositionModes.SPEAKER and !portrait_prefix.is_empty(): if portrait_prefix+debug_portrait in character.portraits: debug_portrait = portrait_prefix+debug_portrait if not debug_portrait in character.portraits: debug_portrait = character.default_portrait var portrait_info: Dictionary = character.get_portrait_info(debug_portrait) # Determine scene var portrait_scene_path: String = portrait_info.get("scene", default_portrait_scene) if portrait_scene_path.is_empty(): portrait_scene_path = default_portrait_scene debug_character_scene_node = load(portrait_scene_path).instantiate() if not is_instance_valid(debug_character_scene_node): return # Load portrait DialogicUtil.apply_scene_export_overrides(debug_character_scene_node, character.portraits[debug_portrait].get("export_overrides", {})) debug_character_scene_node._update_portrait(character, debug_portrait) # Add character node if not is_instance_valid(debug_character_holder_node): debug_character_holder_node = Node2D.new() add_child(debug_character_holder_node) # Add portrait node debug_character_holder_node.add_child(debug_character_scene_node) move_child(debug_character_holder_node, 0) debug_character_scene_node._set_mirror(character.mirror != mirrored != portrait_info.get("mirror", false)) _update_debug_portrait_transform() ## Set"s the size and position of the holder and scene node ## according to the size_mode func _update_debug_portrait_transform() -> void: if not Engine.is_editor_hint() or not is_instance_valid(debug_character_scene_node): return if not is_node_ready(): await ready var character := _get_debug_character() current_settings.update_from_container(self) var portrait_info := character.get_portrait_info(debug_character_portrait) debug_character_holder_node.position = current_settings._get_origin_position() debug_character_scene_node.position = portrait_info.get("offset", Vector2())+character.offset debug_character_scene_node.scale = current_settings.get_portrait_scale(debug_character_scene_node) update_mirror() ## Returns the debug character or the default debug character func _get_debug_character() -> DialogicCharacter: return debug_character if debug_character != null else default_debug_character func set_parent_size(parent_size:Vector2) -> void: container_position.container_size = parent_size container_size.container_size = parent_size container_origin.container_size = container_size.as_pixels() current_settings.set_parent_size(parent_size) func _validate_property(property: Dictionary) -> void: if property.name in ["position", "rotation", "size", "scale"]: property.usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY func _update_character_transform(settings:ContainerSettings, character_node: Node, tween:Tween = null, time:float = 0.0) -> void: var character: DialogicCharacter = character_node.get_meta("character") if character_node.get_child_count() == 0: return var portrait_node: DialogicPortrait = character_node.get_child(-1) var portrait_info: Dictionary = character.portraits.get(portrait_node.get_meta("portrait"), {}) var transform = get_portrait_transform(settings, character, portrait_node) if time == 0: character_node.position = transform.position else: tween.tween_method(DialogicUtil.multitween.bind(character_node, "position", "base"), character_node.position, transform.position, time) for child in character_node.get_children(): if not child is DialogicPortrait: continue portrait_node = (child as DialogicPortrait) portrait_info = character.portraits.get(portrait_node.get_meta("portrait"), {}) transform = get_portrait_transform(settings, character, portrait_node) if time == 0: portrait_node.position = character.offset + portrait_info.get("offset", Vector2()) portrait_node.scale = transform.size else: tween.tween_property(portrait_node, "position",character.offset + portrait_info.get("offset", Vector2()), time) tween.tween_property(portrait_node, "scale", transform.size, time) func get_portrait_transform(settings:ContainerSettings, character: DialogicCharacter, portrait_node:DialogicPortrait) -> Rect2: var portrait_info: Dictionary = character.portraits.get(portrait_node.get_meta("portrait"), {}) # ignore the character scale on custom portraits that have "ignore_char_scale" set to true var apply_character_scale: bool = not portrait_info.get("ignore_char_scale", false) var transform: Rect2 = settings.get_local_portrait_transform( portrait_node._get_covered_rect(), (character.scale * portrait_info.get("scale", 1))*int(apply_character_scale)+portrait_info.get("scale", 1)*int(!apply_character_scale)) return transform #endregion #region CONTAINER SETTINGS ################################################################################ class ContainerSettings extends Resource: ## A resource that allows transfering, storing and loading all settings relevant to a PortraitContainer. @export var position := DialogicFlexVector.new() @export var size := DialogicFlexVector.new() @export var rotation := 0.0 @export var mirrored := false @export var origin := DialogicFlexVector.new() @export var size_mode: SizeModes = SizeModes.FIT_SCALE_HEIGHT @export var z_index := 0 @export var reference_position_id := "" var derived_from : DialogicNode_PortraitContainer = null static var _transform_regex := r"(?position|pos|size|siz|rotation|rot|ori|origin|mod|size_mode|mir|mirror|mirrored|sort|sor|z)\W*=(?((?!(pos|siz|rot|mir|ori|mod|z|sor)).)*)" ## Creates a new ContainerSetting and set its values from the given [param container]. static func from_container(container:DialogicNode_PortraitContainer) -> ContainerSettings: var new := ContainerSettings.new() new.update_from_container(container) return new ## Copies the settings from the given PortraitContainer node [param container] to this ContainerSetting. func update_from_container(container:DialogicNode_PortraitContainer) -> void: position = container.container_position.copy() size = container.container_size.copy() mirrored = container.mirrored rotation = container.container_rotation origin = container.container_origin size_mode = container.size_mode z_index = container.container_z_index if container.mode != DialogicNode_PortraitContainer.PositionModes._CHARACTER: derived_from = container reference_position_id = container.container_ids[0] if container.container_ids else "" ## Creates a new ContainerSetting and set its values from the string. See [method update_from_string] static func from_string(string:="") -> ContainerSettings: var new := ContainerSettings.new() new.update_from_string(string) return new ## Applies settings from the given string to this ContainerSetting. ## The supported tags are pos/position, rot/rotation, siz/size, ori/origin, mir/mirrored, mod/size_mode. func update_from_string(string:="") -> void: var regex := RegEx.create_from_string(_transform_regex) for found in regex.search_all(string): match found.get_string("part"): "pos", "position": position.overwrite_from_string(found.get_string("value")) "rot", "rotation": rotation = float(found.get_string("value")) "siz", "size": size.overwrite_from_string(found.get_string("value")) "ori", "origin": origin.overwrite_from_string(found.get_string("value")) "mir", "mirror", "mirrored": mirrored = found.get_string("value").to_lower().strip_edges() == "true" "mod", "mode", "size_mode": size_mode = (int(found.get_string("value")) as SizeModes) "sor", "sort", "z": z_index = int(found.get_string("value")) "ref", "reference": reference_position_id = found.get_string("value").strip_edges() ## Returns the current origin position relative to this containers top left corner. func _get_origin_position(rect_size = null) -> Vector2: if rect_size == null: rect_size = size return origin.as_pixels() ## Returns the top left position of this container relative to its parent. func _get_top_left_position(rect_size = null) -> Vector2: if rect_size == null: rect_size = size return position.as_pixels() - _get_origin_position(rect_size).rotated(deg_to_rad(rotation)) ## Returns a scaling vector, that can be applied to the portrait node to fit it inside this container. func get_portrait_scale(portrait_node:DialogicPortrait) -> Vector2: var portrait_rect := portrait_node._get_covered_rect() var character := portrait_node.character var portrait_info := character.get_portrait_info(portrait_node.portrait) var apply_character_scale: bool = not portrait_info.get("ignore_char_scale", false) var character_scale : float = character.scale * portrait_info.get("scale", 1) * int(apply_character_scale) + portrait_info.get("scale", 1) * int(!apply_character_scale) var scale_vec := Vector2() # Mode that ignores the containers size if size_mode == SizeModes.KEEP: scale_vec = Vector2(1,1) * character_scale # Mode that makes sure neither height nor width go out of container elif size_mode == SizeModes.FIT_IGNORE_SCALE: if size.as_pixels().x/size.as_pixels().y < portrait_rect.size.x/portrait_rect.size.y: scale_vec = Vector2(1,1) * size.as_pixels().x/portrait_rect.size.x else: scale_vec = Vector2(1,1) * size.as_pixels().y/portrait_rect.size.y # Mode that stretches the portrait to fill the whole container elif size_mode == SizeModes.FIT_STRETCH: scale_vec = size.as_pixels()/portrait_rect.size # Mode that size the character so 100% size fills the height elif size_mode == SizeModes.FIT_SCALE_HEIGHT: scale_vec = Vector2(1,1) * size.as_pixels().y / portrait_rect.size.y*character_scale return scale_vec func set_parent_size(parent_size:Vector2) -> void: position.container_size = parent_size size.container_size = parent_size origin.container_size = size.as_pixels() func _validate_property(property: Dictionary) -> void: if property.name in ["position", "size", "origin"]: property.usage = property.usage | PROPERTY_USAGE_ALWAYS_DUPLICATE func as_string() -> String: return "pos={pos} siz={siz} rot={rot} mir={mir} ori={ori} mod={mod} z={z} ref={ref}".format({ "pos":str(position), "siz":str(size), "rot":str(rotation), "mir":str(mirrored), "ori":str(origin), "mod":str(size_mode), "z":str(z_index), "ref":reference_position_id }) #endregion