]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd
Adding import files
[wolf-seeking-sheep.git] / addons / dialogic / Modules / LayeredPortrait / layered_portrait.gd
1 @tool
2 ## Layered portrait scene.
3 ##
4 ## The parent class has a character and portrait variable.
5 extends DialogicPortrait
6
7 ## The term used for hiding a layer.
8 const _HIDE_COMMAND := "hide"
9 ## The term used for showing a layer.
10 const _SHOW_COMMAND := "show"
11 ## The term used for setting a layer to be the only visible layer.
12 const _SET_COMMAND := "set"
13
14 ## A collection of all possible layer commands.
15 const _OPERATORS = [_HIDE_COMMAND, _SHOW_COMMAND, _SET_COMMAND]
16
17 static var _OPERATORS_EXPRESSION := "|".join(_OPERATORS)
18 static var _REGEX_STRING := "(" + _OPERATORS_EXPRESSION + ") (\\S+)"
19 static var _REGEX := RegEx.create_from_string(_REGEX_STRING)
20
21 var _initialized := false
22
23 var _is_coverage_rect_cached := false
24 var _cached_coverage_rect := Rect2(0, 0, 0, 0)
25
26
27 @export_group("Private")
28 ## Attempts to fix the offset based on the first child node
29 @export var fix_offset := true
30
31 ## Overriding [class DialogicPortrait]'s method.
32 ##
33 ## Load anything related to the given character and portrait
34 func _update_portrait(passed_character: DialogicCharacter, passed_portrait: String) -> void:
35         if not _initialized:
36                 _apply_layer_adjustments()
37                 _initialized = true
38
39         apply_character_and_portrait(passed_character, passed_portrait)
40
41
42 ## Modifies all layers to fit the portrait preview and appear correctly in
43 ## portrait containers.
44 ##
45 ## This method is not changing the scene itself and is intended for the
46 ## Dialogic editor preview and in-game rendering only.
47 func _apply_layer_adjustments() -> void:
48         var coverage := _find_largest_coverage_rect()
49         var offset_fix := Vector2()
50         if fix_offset and get_child_count():
51                 offset_fix = -get_child(0).position
52                 if "centered" in get_child(0) and get_child(0).centered:
53                         offset_fix += get_child(0).get_rect().size/2.0
54
55         for node: Node in get_children():
56                 var node_position: Vector2 = node.position
57                 node_position += offset_fix
58                 node.position = _reposition_with_rect(coverage, node_position)
59
60
61 ## Returns a position based on [param rect]'s size where Dialogic expects the
62 ## scene part to be positioned at. [br]
63 ## If the node has an offset or extra position, pass it as [param node_offset].
64 func _reposition_with_rect(rect: Rect2, node_offset := Vector2(0.0, 0.0)) -> Vector2:
65         return rect.size  * Vector2(-0.5, -1.0) + node_offset
66
67
68 ## Iterates over all children in [param start_node] and its children, looking
69 ## for [class Sprite2D] nodes with a texture (not `null`).
70 ## All found sprites are returned in an array, eventually returning all
71 ## sprites in the scene.
72 func _find_sprites_recursively(start_node: Node) -> Array[Sprite2D]:
73         var sprites: Array[Sprite2D] = []
74
75         # Iterate through the children of the current node
76         for child: Node in start_node.get_children():
77
78                 if child is Sprite2D:
79                         var sprite := child as Sprite2D
80
81                         if sprite.texture:
82                                 sprites.append(sprite)
83
84
85                 var sub := _find_sprites_recursively(child)
86                 sprites.append_array(sub)
87
88         return sprites
89
90
91 ## A command will apply an effect to the layered portrait.
92 class LayerCommand:
93         # The different types of effects.
94         enum CommandType {
95                 ## Additively Show a specific layer.
96                 SHOW_LAYER,
97                 ## Subtractively hide a specific layer.
98                 HIDE_LAYER,
99                 ## Exclusively show a specific layer, hiding all other sibling layers.
100                 ## A sibling layer is a layer sharing the same parent node.
101                 SET_LAYER,
102         }
103
104         var _path: String
105         var _type: CommandType
106
107         ## Executes the effect of the layer based on the [enum CommandType].
108         func _execute(root: Node) -> void:
109                 var target_node := root.get_node(_path)
110
111                 if target_node == null:
112                         printerr("Layered Portrait had no node matching the node path: '", _path, "'.")
113                         return
114
115                 if not target_node is Node2D and not target_node is Sprite2D:
116                         printerr("Layered Portrait target path '", _path, "', is not a Sprite2D or Node2D type.")
117                         return
118
119                 match _type:
120                         CommandType.SHOW_LAYER:
121                                 target_node.show()
122
123                         CommandType.HIDE_LAYER:
124                                 target_node.hide()
125
126                         CommandType.SET_LAYER:
127                                 var target_parent := target_node.get_parent()
128
129                                 for child: Node in target_parent.get_children():
130
131                                         if child is Sprite2D:
132                                                 var sprite_child := child as Sprite2D
133                                                 sprite_child.hide()
134
135                                 target_node.show()
136
137
138 ## Turns the input into a single [class LayerCommand] object.
139 ## Returns `null` if the input cannot be parsed into a [class LayerCommand].
140 func _parse_layer_command(input: String) -> LayerCommand:
141         var regex_match: RegExMatch = _REGEX.search(input)
142
143         if regex_match == null:
144                 print("[Dialogic] Layered Portrait had an invalid command: ", input)
145                 return null
146
147         var _path: String = regex_match.get_string(2)
148         var operator: String = regex_match.get_string(1)
149
150         var command := LayerCommand.new()
151
152         match operator:
153                 _SET_COMMAND:
154                         command._type = LayerCommand.CommandType.SET_LAYER
155
156                 _SHOW_COMMAND:
157                         command._type = LayerCommand.CommandType.SHOW_LAYER
158
159                 _HIDE_COMMAND:
160                         command._type = LayerCommand.CommandType.HIDE_LAYER
161
162                 _SET_COMMAND:
163                         command._type = LayerCommand.CommandType.SET_LAYER
164
165         ## We clean escape symbols and trim the spaces.
166         command._path = _path.replace("\\", "").strip_edges()
167
168         return command
169
170
171 ## Parses [param input] into an array of [class LayerCommand] objects.
172 func _parse_input_to_layer_commands(input: String) -> Array[LayerCommand]:
173         var commands: Array[LayerCommand] = []
174         var command_parts := input.split(",")
175
176         for command_part: String in command_parts:
177
178                 if command_part.is_empty():
179                         continue
180
181                 var _command := _parse_layer_command(command_part.strip_edges())
182
183                 if not _command == null:
184                         commands.append(_command)
185
186         return commands
187
188
189
190 ## Overriding [class DialogicPortrait]'s method.
191 ##
192 ## The extra data will be turned into layer commands and then be executed.
193 func _set_extra_data(data: String) -> void:
194         var commands := _parse_input_to_layer_commands(data)
195
196         for _command: LayerCommand in commands:
197                 _command._execute(self)
198
199
200 ## Overriding [class DialogicPortrait]'s method.
201 ##
202 ## Handling all layers horizontal flip state.
203 func _set_mirror(is_mirrored: bool) -> void:
204         for child: Node in get_children():
205
206                 if is_mirrored:
207                         child.position.x = child.position.x * -1
208                         child.scale.x = -child.scale.x
209
210
211 ## Scans all nodes in this scene and finds the largest rectangle that
212 ## covers encloses every sprite.
213 func _find_largest_coverage_rect() -> Rect2:
214         if _is_coverage_rect_cached:
215                 return _cached_coverage_rect
216
217         var coverage_rect := Rect2(0, 0, 0, 0)
218
219         for sprite: Sprite2D in _find_sprites_recursively(self):
220                 var sprite_size := sprite.get_rect().size
221                 var sprite_position: Vector2 = sprite.global_position-self.global_position
222
223                 if sprite.centered:
224                         sprite_position -= sprite_size/2
225
226                 var sprite_width := sprite_size.x * sprite.scale.x
227                 var sprite_height := sprite_size.y * sprite.scale.y
228
229                 var texture_rect := Rect2(
230                         sprite_position.x,
231                         sprite_position.y,
232                         sprite_width,
233                         sprite_height
234                 )
235                 coverage_rect = coverage_rect.merge(texture_rect)
236
237         coverage_rect.position = _reposition_with_rect(coverage_rect)
238
239         _is_coverage_rect_cached = true
240         _cached_coverage_rect = coverage_rect
241
242         return coverage_rect
243
244
245 ## Overriding [class DialogicPortrait]'s method.
246 ##
247 ## Called by Dialogic when the portrait is needed to be shown.
248 ## For instance, in the Dialogic editor or in-game.
249 func _get_covered_rect() -> Rect2:
250         var needed_rect := _find_largest_coverage_rect()
251
252         return needed_rect