]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Resources/event.gd
Updated export config options
[wolf-seeking-sheep.git] / addons / dialogic / Resources / event.gd
1 @tool
2 class_name DialogicEvent
3 extends Resource
4
5 ## Base event class for all dialogic events.
6 ## Implements basic properties, translation, shortcode saving and usefull methods for creating
7 ## the editor UI.
8
9
10 ## Emmited when the event starts.
11 ## The signal is emmited with the event resource [code]event_resource[/code]
12 signal event_started(event_resource:DialogicEvent)
13
14 ## Emmited when the event finish.
15 ## The signal is emmited with the event resource [code]event_resource[/code]
16 signal event_finished(event_resource:DialogicEvent)
17
18
19 ### Main Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20
21 ## The event name that'll be displayed in the editor.
22 var event_name := "Event"
23 ## Unique identifier used for translatable events.
24 var _translation_id := ""
25 ## A reference to dialogic during execution, can be used the same as Dialogic (reference to the autoload)
26 var dialogic: DialogicGameHandler = null
27
28
29 ### Special Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30 ### (these properties store how this event affects indentation/flow of timeline)
31
32 ## If true this event can not be toplevel (e.g. Choice)
33 var needs_indentation := false
34 ## If true this event will spawn with an END BRANCH event and higher the indentation
35 var can_contain_events := false
36 ## If [can_contain_events] is true this is a reference to the end branch event
37 var end_branch_event: DialogicEndBranchEvent = null
38 ## If this is true this event will group with other similar events (like choices do).
39 var wants_to_group := false
40
41
42 ### Saving/Loading Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
43
44 ## Stores the event in a text format. Does NOT automatically update.
45 var event_node_as_text := ""
46 ## Flags if the event has been processed or is only stored as text
47 var event_node_ready := false
48 ## How many empty lines are before this event
49 var empty_lines_above: int = 0
50
51
52 ### Editor UI Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
53
54 ## The event color that event node will take in the editor
55 var event_color := Color("FBB13C")
56 ## If you are using the default color palette
57 var dialogic_color_name: = ""
58 ## To sort the buttons shown in the editor. Lower index is placed at the top of a category
59 var event_sorting_index: int = 0
60 ## If true the event will not have a button in the visual editor sidebar
61 var disable_editor_button := false
62 ## If false the event will hide it's body by default. Recommended for most events
63 var expand_by_default := false
64 ## The URL to open when right_click>Documentation is selected
65 var help_page_path := ""
66 ## Is the event block created by a button?
67 var created_by_button := false
68
69 ## Reference to the node, that represents this event. Only works while in visual editor mode.
70 ## Use with care.
71 var editor_node: Control = null
72
73 ## The categories and which one to put it in (in the visual editor sidebar)
74 var event_category := "Other"
75
76
77 ### Editor UI creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78
79 ## To differentiate fields that should go to the header and to the body
80 enum Location {HEADER, BODY}
81
82 ## To differentiate the different types of fields for event properties in the visual editor
83 enum ValueType {
84         # Strings
85         MULTILINE_TEXT, SINGLELINE_TEXT, CONDITION, FILE,
86         # Booleans
87         BOOL, BOOL_BUTTON,
88         # Options
89         DYNAMIC_OPTIONS, FIXED_OPTIONS,
90         # Containers,
91         ARRAY, DICTIONARY,
92         # Numbers
93         NUMBER,
94         VECTOR2, VECTOR3, VECTOR4,
95         # Other
96         CUSTOM, BUTTON, LABEL, COLOR, AUDIO_PREVIEW
97 }
98 ## List that stores the fields for the editor
99 var editor_list: Array = []
100
101 var this_folder: String = get_script().resource_path.get_base_dir()
102
103 ## Singal that notifies the visual editor block to update
104 signal ui_update_needed
105 signal ui_update_warning(text:String)
106
107
108 ## Makes this resource printable.
109 func _to_string() -> String:
110         return "[{name}:{id}]".format({"name":event_name, "id":get_instance_id()})
111
112 #endregion
113
114
115 #region EXECUTION
116 ################################################################################
117
118 ## Executes the event behaviour. In subclasses [_execute] (not this one) should be overriden!
119 func execute(_dialogic_game_handler) -> void:
120         event_started.emit(self)
121         dialogic = _dialogic_game_handler
122         call_deferred("_execute")
123
124
125 ## Ends the event behaviour.
126 func finish() -> void:
127         event_finished.emit(self)
128
129
130 ## Called before executing the next event or before clear(any flags) / load_full_state().
131 ##
132 ## Should be overridden if the event stores temporary state into dialogic.current_state_info
133 ## or some other cleanup is needed before another event can run.
134 func _clear_state() -> void:
135         pass
136
137
138 ## To be overridden by subclasses.
139 func _execute() -> void:
140         finish()
141
142 #endregion
143
144
145 #region OVERRIDABLES
146 ################################################################################
147
148 ## to be overridden by sub-classes
149 ## only called if can_contain_events is true.
150 ## return a control node that should show on the END BRANCH node
151 func get_end_branch_control() -> Control:
152         return null
153
154
155 ## to be overridden by sub-classes
156 ## only called if can_contain_events is true and the previous event was an end-branch event
157 ## return true if this event should be executed if the previous event was an end-branch event
158 ## basically only important for the Condition event but who knows. Some day someone might need this.
159 func should_execute_this_branch() -> bool:
160         return false
161
162 #endregion
163
164
165 #region TRANSLATIONS
166 ################################################################################
167
168 ## Overwrite if this events needs translation.
169 func _get_translatable_properties() -> Array:
170         return []
171
172
173 ## Overwrite if this events needs translation.
174 func _get_property_original_translation(_property_name:String) -> String:
175         return ''
176
177
178 ## Returns true if there is any translatable properties on this event.
179 ## Overwrite [_get_translatable_properties()] to change this.
180 func can_be_translated() -> bool:
181         return !_get_translatable_properties().is_empty()
182
183
184 ## This is automatically called, no need to use this.
185 func add_translation_id() -> String:
186         _translation_id = DialogicUtil.get_next_translation_id()
187         return _translation_id
188
189
190 func remove_translation_id() -> void:
191         _translation_id = ""
192
193
194 func get_property_translation_key(property_name:String) -> String:
195         return event_name.path_join(_translation_id).path_join(property_name)
196
197
198 ## Call this whenever you are using a translatable property
199 func get_property_translated(property_name:String) -> String:
200         if !_translation_id.is_empty() and ProjectSettings.get_setting('dialogic/translation/enabled', false):
201                 var translation := tr(get_property_translation_key(property_name))
202                 # if no translation is found tr() returns the id, but we want to fallback to the original
203                 return translation if translation != get_property_translation_key(property_name) else _get_property_original_translation(property_name)
204         else:
205                 return _get_property_original_translation(property_name)
206
207 #endregion
208
209
210 #region SAVE / LOAD (internal, don't override)
211 ################################################################################
212 ### These functions are used by the timeline loader/saver
213 ### They mainly use the overridable behaviour below, but enforce the unique_id saving
214
215 ## Used by the Timeline saver.
216 func _store_as_string() -> String:
217         if !_translation_id.is_empty() and can_be_translated():
218                 return to_text() + ' #id:'+str(_translation_id)
219         else:
220                 return to_text()
221
222
223 ## Call this if you updated an event and want the changes to be saved.
224 func update_text_version() -> void:
225         event_node_as_text = _store_as_string()
226
227
228 ## Used by timeline processor.
229 func _load_from_string(string:String) -> void:
230         _load_custom_defaults()
231         if '#id:' in string and can_be_translated():
232                 _translation_id = string.get_slice('#id:', 1).strip_edges()
233                 from_text(string.get_slice('#id:', 0))
234         else:
235                 from_text(string)
236         event_node_ready = true
237
238
239 ## Assigns the custom defaults
240 func _load_custom_defaults() -> void:
241         for default_prop in DialogicUtil.get_custom_event_defaults(event_name):
242                 if default_prop in self:
243                         set(default_prop, DialogicUtil.get_custom_event_defaults(event_name)[default_prop])
244
245
246 ## Used by the timeline processor.
247 func _test_event_string(string:String) -> bool:
248         if '#id:' in string and can_be_translated():
249                 return is_valid_event(string.get_slice('#id:', 0))
250         return is_valid_event(string.strip_edges())
251
252 #endregion
253
254
255 #region SAVE / LOAD
256 ################################################################################
257 ### All of these functions can/should be overridden by the sub classes
258
259 ## If this uses the short-code format, return the shortcode.
260 func get_shortcode() -> String:
261         return 'default_shortcode'
262
263
264 ## If this uses the short-code format, return the parameters and corresponding property names.
265 func get_shortcode_parameters() -> Dictionary:
266         return {}
267
268
269 ## Returns a readable presentation of the event (This is how it's stored).
270 ## By default it uses a shortcode format, but can be overridden.
271 func to_text() -> String:
272         var shortcode := store_to_shortcode_parameters()
273         if shortcode:
274                 return "[" + self.get_shortcode() + " " + store_to_shortcode_parameters() + "]"
275         else:
276                 return "[" + self.get_shortcode() + "]"
277
278
279 ## Loads the variables from the string stored by [method to_text].
280 ## By default it uses the shortcode format, but can be overridden.
281 func from_text(string: String) -> void:
282         load_from_shortcode_parameters(string)
283
284
285 ## Returns a string with all the shortcode parameters.
286 func store_to_shortcode_parameters(params:Dictionary = {}) -> String:
287         if params.is_empty():
288                 params = get_shortcode_parameters()
289         var custom_defaults: Dictionary = DialogicUtil.get_custom_event_defaults(event_name)
290         var result_string := ""
291         for parameter in params.keys():
292                 var parameter_info: Dictionary = params[parameter]
293                 var value: Variant = get(parameter_info.property)
294                 var default_value: Variant = custom_defaults.get(parameter_info.property, parameter_info.default)
295
296                 if parameter_info.get('custom_stored', false):
297                         continue
298
299                 if "set_" + parameter_info.property in self and not get("set_" + parameter_info.property):
300                         continue
301
302                 if typeof(value) == typeof(default_value) and value == default_value:
303                         if not "set_" + parameter_info.property in self or not get("set_" + parameter_info.property):
304                                 continue
305
306                 result_string += " " + parameter + '="' + value_to_string(value, parameter_info.get("suggestions", Callable())) + '"'
307
308         return result_string.strip_edges()
309
310
311 func value_to_string(value: Variant, suggestions := Callable()) -> String:
312         var value_as_string := ""
313         match typeof(value):
314                 TYPE_OBJECT:
315                         value_as_string = str(value.resource_path)
316
317                 TYPE_STRING:
318                         value_as_string = value
319
320                 TYPE_INT when suggestions.is_valid():
321                         # HANDLE TEXT ALTERNATIVES FOR ENUMS
322                         for option in suggestions.call().values():
323                                 if option.value != value:
324                                         continue
325
326                                 if option.has('text_alt'):
327                                         value_as_string = option.text_alt[0]
328                                 else:
329                                         value_as_string = var_to_str(option.value)
330
331                                 break
332
333                 TYPE_DICTIONARY:
334                         value_as_string = JSON.stringify(value)
335
336                 _:
337                         value_as_string = var_to_str(value)
338
339         if not ((value_as_string.begins_with("[") and value_as_string.ends_with("]")) or (value_as_string.begins_with("{") and value_as_string.ends_with("}"))):
340                 value_as_string.replace('"', '\\"')
341
342         return value_as_string
343
344
345 func load_from_shortcode_parameters(string:String) -> void:
346         var data: Dictionary = parse_shortcode_parameters(string)
347         var params: Dictionary = get_shortcode_parameters()
348         for parameter in params.keys():
349                 var parameter_info: Dictionary = params[parameter]
350                 if parameter_info.get('custom_stored', false):
351                         continue
352
353                 if not parameter in data:
354                         if "set_" + parameter_info.property in self:
355                                 set("set_" + parameter_info.property, false)
356                         continue
357
358                 if "set_" + parameter_info.property in self:
359                         set("set_" + parameter_info.property, true)
360
361                 var param_value: String = data[parameter].replace('\\"', '"')
362                 var value: Variant
363                 match typeof(get(parameter_info.property)):
364                         TYPE_STRING:
365                                 value = param_value
366
367                         TYPE_INT:
368                                 # If a string is given
369                                 if parameter_info.has('suggestions'):
370                                         for option in parameter_info.suggestions.call().values():
371                                                 if option.has('text_alt') and param_value in option.text_alt:
372                                                         value = option.value
373                                                         break
374
375                                 if not value:
376                                         value = float(param_value)
377
378                         _:
379                                 value = str_to_var(param_value)
380
381                 set(parameter_info.property, value)
382
383 ## Has to return `true`, if the given string can be interpreted as this event.
384 ## By default it uses the shortcode formta, but can be overridden.
385 func is_valid_event(string: String) -> bool:
386         if string.strip_edges().begins_with('['+get_shortcode()+' ') or string.strip_edges().begins_with('['+get_shortcode()+']'):
387                 return true
388         return false
389
390
391 ## has to return true if this string seems to be a full event of this kind
392 ## (only tested if is_valid_event() returned true)
393 ## if a shortcode it used it will default to true if the string ends with ']'
394 func is_string_full_event(string: String) -> bool:
395         if get_shortcode() != 'default_shortcode': return string.strip_edges().ends_with(']')
396         return true
397
398
399 ## Used to get all the shortcode parameters in a string as a dictionary.
400 func parse_shortcode_parameters(shortcode: String) -> Dictionary:
401         var regex := RegEx.new()
402         regex.compile(r'(?<parameter>[^\s=]*)\s*=\s*"(?<value>(\{[^}]*\}|\[[^]]*\]|([^"]|\\")*|))(?<!\\)\"')
403         var dict := {}
404         for result in regex.search_all(shortcode):
405                 dict[result.get_string('parameter')] = result.get_string('value')
406         return dict
407
408 #endregion
409
410
411 #region EDITOR REPRESENTATION
412 ################################################################################
413
414 func _get_icon() -> Resource:
415         var _icon_file_name := "res://addons/dialogic/Editor/Images/Pieces/closed-icon.svg" # Default
416         # Check for both svg and png, but prefer svg if available
417         if ResourceLoader.exists(self.get_script().get_path().get_base_dir() + "/icon.svg"):
418                 _icon_file_name = self.get_script().get_path().get_base_dir() + "/icon.svg"
419         elif ResourceLoader.exists(self.get_script().get_path().get_base_dir() + "/icon.png"):
420                 _icon_file_name = self.get_script().get_path().get_base_dir() + "/icon.png"
421         return load(_icon_file_name)
422
423
424 func set_default_color(value:Variant) -> void:
425         dialogic_color_name = value
426         event_color = DialogicUtil.get_color(value)
427
428
429 ## Called when the resource is assigned to a event block in the visual editor
430 func _enter_visual_editor(_timeline_editor:DialogicEditor) -> void:
431         pass
432
433 #endregion
434
435
436 #region CODE COMPLETION
437 ################################################################################
438
439 ## This method can be overwritten to implement code completion for custom syntaxes
440 func _get_code_completion(_CodeCompletionHelper:Node, _TextNode:TextEdit, _line:String, _word:String, _symbol:String) -> void:
441         pass
442
443 ## This method can be overwritten to add starting suggestions for this event
444 func _get_start_code_completion(_CodeCompletionHelper:Node, _TextNode:TextEdit) -> void:
445         pass
446
447 #endregion
448
449
450 #region SYNTAX HIGHLIGHTING
451 ################################################################################
452
453 func _get_syntax_highlighting(_Highlighter:SyntaxHighlighter, dict:Dictionary, _line:String) -> Dictionary:
454         return dict
455
456 #endregion
457
458
459 #region EVENT FIELDS
460 ################################################################################
461
462 func get_event_editor_info() -> Array:
463         if Engine.is_editor_hint():
464                 if editor_list != null:
465                         editor_list.clear()
466                 else:
467                         editor_list = []
468
469                 build_event_editor()
470                 return editor_list
471         else:
472                 return []
473
474
475 ## to be overwritten by the sub_classes
476 func build_event_editor() -> void:
477         pass
478
479 ## For the methods below the arguments are mostly similar:
480 ## @variable:           String name of the property this field is for
481 ## @condition:          String that will be executed as an expression. If it false
482 ## @editor_type:        One of the ValueTypes (see ValueType enum). Defines type of field.
483 ## @left_text:          Text that will be shown to the left of the field
484 ## @right_text:         Text that will be shown to the right of the field
485 ## @extra_info:         Allows passing a lot more info to the field.
486 ##                                      What info can be passed is different for every field
487
488 func add_header_label(text:String, condition:= "") -> void:
489         editor_list.append({
490                 "name"                  : "something",
491                 "type"                  :+ TYPE_STRING,
492                 "location"              : Location.HEADER,
493                 "usage"                 : PROPERTY_USAGE_EDITOR,
494                 "field_type" : ValueType.LABEL,
495                 "display_info"  : {"text":text},
496                 "condition"     : condition
497                 })
498
499
500 func add_header_edit(variable:String, editor_type := ValueType.LABEL, extra_info:= {}, condition:= "") -> void:
501         editor_list.append({
502                 "name"                  : variable,
503                 "type"                  : typeof(get(variable)),
504                 "location"              : Location.HEADER,
505                 "usage"                 : PROPERTY_USAGE_DEFAULT,
506                 "field_type" : editor_type,
507                 "display_info"  : extra_info,
508                 "left_text"     : extra_info.get('left_text', ''),
509                 "right_text"    : extra_info.get('right_text', ''),
510                 "condition"     : condition,
511                 })
512
513
514 func add_header_button(text:String, callable:Callable, tooltip:String, icon: Variant = null, condition:= "") -> void:
515         editor_list.append({
516                 "name"                  : "Button",
517                 "type"                  : TYPE_STRING,
518                 "location"              : Location.HEADER,
519                 "usage"                 : PROPERTY_USAGE_DEFAULT,
520                 "field_type" : ValueType.BUTTON,
521                 "display_info"  : {'text':text, 'tooltip':tooltip, 'callable':callable, 'icon':icon},
522                 "condition"     : condition,
523         })
524
525
526 func add_body_edit(variable:String, editor_type := ValueType.LABEL, extra_info:= {}, condition:= "") -> void:
527         editor_list.append({
528                 "name"                  : variable,
529                 "type"                  : typeof(get(variable)),
530                 "location"              : Location.BODY,
531                 "usage"                 : PROPERTY_USAGE_DEFAULT,
532                 "field_type" : editor_type,
533                 "display_info"  : extra_info,
534                 "left_text"     : extra_info.get('left_text', ''),
535                 "right_text"    : extra_info.get('right_text', ''),
536                 "condition"     : condition,
537                 })
538
539
540 func add_body_line_break(condition:= "") -> void:
541         editor_list.append({
542                 "name"          : "linebreak",
543                 "type"          : TYPE_BOOL,
544                 "location"      : Location.BODY,
545                 "usage"         : PROPERTY_USAGE_DEFAULT,
546                 "condition" : condition,
547                 })
548
549 #endregion