]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Core/DialogicGameHandler.gd
Updated export config options
[wolf-seeking-sheep.git] / addons / dialogic / Core / DialogicGameHandler.gd
1 class_name DialogicGameHandler
2 extends Node
3
4 ## Class that is used as the Dialogic autoload.
5
6 ## Autoload script that allows you to interact with all of Dialogic's systems:[br]
7 ## - Holds all important information about the current state of Dialogic.[br]
8 ## - Provides access to all the subsystems.[br]
9 ## - Has methods to start/end timelines.[br]
10
11
12 ## States indicating different phases of dialog.
13 enum States {
14         IDLE,                           ## Dialogic is awaiting input to advance.
15         REVEALING_TEXT,         ## Dialogic is currently revealing text.
16         ANIMATING,                      ## Some animation is happening.
17         AWAITING_CHOICE,        ## Dialogic awaits the selection of a choice
18         WAITING                         ## Dialogic is currently awaiting something.
19         }
20
21 ## Flags indicating what to clear when calling [method clear].
22 enum ClearFlags {
23         FULL_CLEAR = 0,                 ## Clears all subsystems
24         KEEP_VARIABLES = 1,     ## Clears all subsystems and info except for variables
25         TIMELINE_INFO_ONLY = 2  ## Doesn't clear subsystems but current timeline and index
26         }
27
28 ## Reference to the currently executed timeline.
29 var current_timeline: DialogicTimeline = null
30 ## Copy of the [member current_timeline]'s events.
31 var current_timeline_events: Array = []
32
33 ## Index of the event the timeline handling is currently at.
34 var current_event_idx: int = 0
35 ## Contains all information that subsystems consider relevant for
36 ## the current situation
37 var current_state_info: Dictionary = {}
38
39 ## Current state (see [member States] enum).
40 var current_state := States.IDLE:
41         get:
42                 return current_state
43
44         set(new_state):
45                 current_state = new_state
46                 state_changed.emit(new_state)
47
48 ## Emitted when [member current_state] change.
49 signal state_changed(new_state:States)
50
51 ## When `true`, many dialogic processes won't continue until it's `false` again.
52 var paused := false:
53         set(value):
54                 paused = value
55
56                 if paused:
57
58                         for subsystem in get_children():
59
60                                 if subsystem is DialogicSubsystem:
61                                         (subsystem as DialogicSubsystem).pause()
62
63                         dialogic_paused.emit()
64
65                 else:
66                         for subsystem in get_children():
67
68                                 if subsystem is DialogicSubsystem:
69                                         (subsystem as DialogicSubsystem).resume()
70
71                         dialogic_resumed.emit()
72
73 ## Emitted when [member paused] changes to `true`.
74 signal dialogic_paused
75 ## Emitted when [member paused] changes to `false`.
76 signal dialogic_resumed
77
78
79 ## Emitted when the timeline ends.
80 ## This can be a timeline ending or [method end_timeline] being called.
81 signal timeline_ended
82 ## Emitted when a timeline starts by calling either [method start]
83 ## or [method start_timeline].
84 signal timeline_started
85 ## Emitted when an event starts being executed.
86 ## The event may not have finished executing yet.
87 signal event_handled(resource: DialogicEvent)
88
89 ## Emitted when a [class SignalEvent] event was reached.
90 signal signal_event(argument: Variant)
91 ## Emitted when a signal event gets fired from a [class TextEvent] event.
92 signal text_signal(argument: String)
93
94
95 # Careful, this section is repopulated automatically at certain moments.
96 #region SUBSYSTEMS
97
98 var Animations := preload("res://addons/dialogic/Modules/Core/subsystem_animation.gd").new():
99         get: return get_subsystem("Animations")
100
101 var Audio := preload("res://addons/dialogic/Modules/Audio/subsystem_audio.gd").new():
102         get: return get_subsystem("Audio")
103
104 var Backgrounds := preload("res://addons/dialogic/Modules/Background/subsystem_backgrounds.gd").new():
105         get: return get_subsystem("Backgrounds")
106
107 var Choices := preload("res://addons/dialogic/Modules/Choice/subsystem_choices.gd").new():
108         get: return get_subsystem("Choices")
109
110 var Expressions := preload("res://addons/dialogic/Modules/Core/subsystem_expression.gd").new():
111         get: return get_subsystem("Expressions")
112
113
114 var Glossary := preload("res://addons/dialogic/Modules/Glossary/subsystem_glossary.gd").new():
115         get: return get_subsystem("Glossary")
116
117 var History := preload("res://addons/dialogic/Modules/History/subsystem_history.gd").new():
118         get: return get_subsystem("History")
119
120 var Inputs := preload("res://addons/dialogic/Modules/Core/subsystem_input.gd").new():
121         get: return get_subsystem("Inputs")
122
123 var Jump := preload("res://addons/dialogic/Modules/Jump/subsystem_jump.gd").new():
124         get: return get_subsystem("Jump")
125
126 var PortraitContainers := preload("res://addons/dialogic/Modules/Character/subsystem_containers.gd").new():
127         get: return get_subsystem("PortraitContainers")
128
129 var Portraits := preload("res://addons/dialogic/Modules/Character/subsystem_portraits.gd").new():
130         get: return get_subsystem("Portraits")
131
132 var Save := preload("res://addons/dialogic/Modules/Save/subsystem_save.gd").new():
133         get: return get_subsystem("Save")
134
135 var Settings := preload("res://addons/dialogic/Modules/Settings/subsystem_settings.gd").new():
136         get: return get_subsystem("Settings")
137
138 var Styles := preload("res://addons/dialogic/Modules/Style/subsystem_styles.gd").new():
139         get: return get_subsystem("Styles")
140
141 var Text := preload("res://addons/dialogic/Modules/Text/subsystem_text.gd").new():
142         get: return get_subsystem("Text")
143
144 var TextInput := preload("res://addons/dialogic/Modules/TextInput/subsystem_text_input.gd").new():
145         get: return get_subsystem("TextInput")
146
147 var VAR := preload("res://addons/dialogic/Modules/Variable/subsystem_variables.gd").new():
148         get: return get_subsystem("VAR")
149
150 var Voice := preload("res://addons/dialogic/Modules/Voice/subsystem_voice.gd").new():
151         get: return get_subsystem("Voice")
152
153 #endregion
154
155
156 ## Autoloads are added first, so this happens REALLY early on game startup.
157 func _ready() -> void:
158         _collect_subsystems()
159
160         clear()
161
162
163 #region TIMELINE & EVENT HANDLING
164 ################################################################################
165
166 ## Method to start a timeline AND ensure that a layout scene is present.
167 ## For argument info, checkout [method start_timeline].
168 ## -> returns the layout node
169 func start(timeline:Variant, label:Variant="") -> Node:
170         # If we don't have a style subsystem, default to just start_timeline()
171         if not has_subsystem('Styles'):
172                 printerr("[Dialogic] You called Dialogic.start() but the Styles subsystem is missing!")
173                 clear(ClearFlags.KEEP_VARIABLES)
174                 start_timeline(timeline, label)
175                 return null
176
177         # Otherwise make sure there is a style active.
178         var scene: Node = null
179         if !self.Styles.has_active_layout_node():
180                 scene = self.Styles.load_style()
181         else:
182                 scene = self.Styles.get_layout_node()
183                 scene.show()
184
185         if not scene.is_node_ready():
186                 scene.ready.connect(clear.bind(ClearFlags.KEEP_VARIABLES))
187                 scene.ready.connect(start_timeline.bind(timeline, label))
188         else:
189                 start_timeline(timeline, label)
190
191         return scene
192
193
194 ## Method to start a timeline without adding a layout scene.
195 ## @timeline can be either a loaded timeline resource or a path to a timeline file.
196 ## @label_or_idx can be a label (string) or index (int) to skip to immediatly.
197 func start_timeline(timeline:Variant, label_or_idx:Variant = "") -> void:
198         # load the resource if only the path is given
199         if typeof(timeline) == TYPE_STRING:
200                 #check the lookup table if it's not a full file name
201                 if (timeline as String).contains("res://"):
202                         timeline = load((timeline as String))
203                 else:
204                         timeline = DialogicResourceUtil.get_timeline_resource((timeline as String))
205
206         if timeline == null:
207                 printerr("[Dialogic] There was an error loading this timeline. Check the filename, and the timeline for errors")
208                 return
209
210         (timeline as DialogicTimeline).process()
211
212         current_timeline = timeline
213         current_timeline_events = current_timeline.events
214         for event in current_timeline_events:
215                 event.dialogic = self
216         current_event_idx = -1
217
218         if typeof(label_or_idx) == TYPE_STRING:
219                 if label_or_idx:
220                         if has_subsystem('Jump'):
221                                 Jump.jump_to_label((label_or_idx as String))
222         elif typeof(label_or_idx) == TYPE_INT:
223                 if label_or_idx >-1:
224                         current_event_idx = label_or_idx -1
225
226         timeline_started.emit()
227         handle_next_event()
228
229
230 ## Preloader function, prepares a timeline and returns an object to hold for later
231 ## [param timeline_resource] can be either a path (string) or a loaded timeline (resource)
232 func preload_timeline(timeline_resource:Variant) -> Variant:
233         # I think ideally this should be on a new thread, will test
234         if typeof(timeline_resource) == TYPE_STRING:
235                 timeline_resource = load((timeline_resource as String))
236                 if timeline_resource == null:
237                         printerr("[Dialogic] There was an error preloading this timeline. Check the filename, and the timeline for errors")
238                         return null
239
240         (timeline_resource as DialogicTimeline).process()
241
242         return timeline_resource
243
244
245 ## Clears and stops the current timeline.
246 func end_timeline() -> void:
247         await clear(ClearFlags.TIMELINE_INFO_ONLY)
248         _on_timeline_ended()
249         timeline_ended.emit()
250
251
252 ## Handles the next event.
253 func handle_next_event(_ignore_argument: Variant = "") -> void:
254         handle_event(current_event_idx+1)
255
256
257 ## Handles the event at the given index [param event_index].
258 ## You can call this manually, but if another event is still executing, it might have unexpected results.
259 func handle_event(event_index:int) -> void:
260         if not current_timeline:
261                 return
262
263         _cleanup_previous_event()
264
265         if paused:
266                 await dialogic_resumed
267
268         if event_index >= len(current_timeline_events):
269                 end_timeline()
270                 return
271
272         #actually process the event now, since we didnt earlier at runtime
273         #this needs to happen before we create the copy DialogicEvent variable, so it doesn't throw an error if not ready
274         if current_timeline_events[event_index].event_node_ready == false:
275                 current_timeline_events[event_index]._load_from_string(current_timeline_events[event_index].event_node_as_text)
276
277         current_event_idx = event_index
278
279         if not current_timeline_events[event_index].event_finished.is_connected(handle_next_event):
280                 current_timeline_events[event_index].event_finished.connect(handle_next_event)
281
282         set_meta('previous_event', current_timeline_events[event_index])
283
284         current_timeline_events[event_index].execute(self)
285         event_handled.emit(current_timeline_events[event_index])
286
287
288 ## Resets Dialogic's state fully or partially.
289 ## By using the clear flags from the [member ClearFlags] enum you can specify
290 ## what info should be kept.
291 ## For example, at timeline end usually it doesn't clear node or subsystem info.
292 func clear(clear_flags := ClearFlags.FULL_CLEAR) -> void:
293         _cleanup_previous_event()
294
295         if !clear_flags & ClearFlags.TIMELINE_INFO_ONLY:
296                 for subsystem in get_children():
297                         if subsystem is DialogicSubsystem:
298                                 (subsystem as DialogicSubsystem).clear_game_state(clear_flags)
299
300         var timeline := current_timeline
301
302         current_timeline = null
303         current_event_idx = -1
304         current_timeline_events = []
305         current_state = States.IDLE
306
307         # Resetting variables
308         if timeline:
309                 await timeline.clean()
310
311
312 ## Cleanup after previous event (if any).
313 func _cleanup_previous_event():
314         if has_meta('previous_event') and get_meta('previous_event') is DialogicEvent:
315                 var event := get_meta('previous_event') as DialogicEvent
316                 if event.event_finished.is_connected(handle_next_event):
317                         event.event_finished.disconnect(handle_next_event)
318                 event._clear_state()
319                 remove_meta("previous_event")
320
321 #endregion
322
323
324 #region SAVING & LOADING
325 ################################################################################
326
327 ## Returns a dictionary containing all necessary information to later recreate the same state with load_full_state.
328 ## The [subsystem Save] subsystem might be more useful for you.
329 ## However, this can be used to integrate the info into your own save system.
330 func get_full_state() -> Dictionary:
331         if current_timeline:
332                 current_state_info['current_event_idx'] = current_event_idx
333                 current_state_info['current_timeline'] = current_timeline.resource_path
334         else:
335                 current_state_info['current_event_idx'] = -1
336                 current_state_info['current_timeline'] = null
337
338         for subsystem in get_children():
339                 (subsystem as DialogicSubsystem).save_game_state()
340
341         return current_state_info.duplicate(true)
342
343
344 ## This method tries to load the state from the given [param state_info].
345 ## Will automatically start a timeline and add a layout if a timeline was running when
346 ## the dictionary was retrieved with [method get_full_state].
347 func load_full_state(state_info:Dictionary) -> void:
348         clear()
349         current_state_info = state_info
350         ## The Style subsystem needs to run first for others to load correctly.
351         var scene: Node = null
352         if has_subsystem('Styles'):
353                 get_subsystem('Styles').load_game_state()
354                 scene = self.Styles.get_layout_node()
355
356         var load_subsystems := func() -> void:
357                 for subsystem in get_children():
358                         if subsystem.name == 'Styles':
359                                 continue
360                         (subsystem as DialogicSubsystem).load_game_state()
361
362         if null != scene and not scene.is_node_ready():
363                 scene.ready.connect(load_subsystems)
364         else:
365                 await get_tree().process_frame
366                 load_subsystems.call()
367
368         if current_state_info.get('current_timeline', null):
369                 start_timeline(current_state_info.current_timeline, current_state_info.get('current_event_idx', 0))
370         else:
371                 end_timeline.call_deferred()
372 #endregion
373
374
375 #region SUB-SYTSEMS
376 ################################################################################
377
378 func _collect_subsystems() -> void:
379         var subsystem_nodes := [] as Array[DialogicSubsystem]
380         for indexer in DialogicUtil.get_indexers():
381                 for subsystem in indexer._get_subsystems():
382                         var subsystem_node := add_subsystem(str(subsystem.name), str(subsystem.script))
383                         subsystem_nodes.push_back(subsystem_node)
384
385         for subsystem in subsystem_nodes:
386                 subsystem.post_install()
387
388
389 ## Returns `true` if a subystem with the given [param subsystem_name] exists.
390 func has_subsystem(subsystem_name:String) -> bool:
391         return has_node(subsystem_name)
392
393
394 ## Returns the subsystem node of the given [param subsystem_name] or null if it doesn't exist.
395 func get_subsystem(subsystem_name:String) -> DialogicSubsystem:
396         return get_node(subsystem_name)
397
398
399 ## Adds a subsystem node with the given [param subsystem_name] and [param script_path].
400 func add_subsystem(subsystem_name:String, script_path:String) -> DialogicSubsystem:
401         var node: Node = Node.new()
402         node.name = subsystem_name
403         node.set_script(load(script_path))
404         node = node as DialogicSubsystem
405         node.dialogic = self
406         add_child(node)
407         return node
408
409
410 #endregion
411
412
413 #region HELPERS
414 ################################################################################
415
416 ## This handles the `Layout End Behaviour` setting that can be changed in the Dialogic settings.
417 func _on_timeline_ended() -> void:
418         if self.Styles.has_active_layout_node() and self.Styles.get_layout_node().is_inside_tree():
419                 match ProjectSettings.get_setting('dialogic/layout/end_behaviour', 0):
420                         0:
421                                 self.Styles.get_layout_node().get_parent().remove_child(self.Styles.get_layout_node())
422                                 self.Styles.get_layout_node().queue_free()
423                         1:
424                                 @warning_ignore("unsafe_method_access")
425                                 self.Styles.get_layout_node().hide()
426
427
428 func print_debug_moment() -> void:
429         if not current_timeline:
430                 return
431
432         printerr("\tAt event ", current_event_idx+1, " (",current_timeline_events[current_event_idx].event_name, ' Event) in timeline "', DialogicResourceUtil.get_unique_identifier(current_timeline.resource_path), '" (',current_timeline.resource_path,').')
433         print("\n")
434 #endregion