First work on dialogic, resized guild, and started implementing portraits.

This commit is contained in:
2025-08-14 10:26:24 -04:00
parent 95a7db036b
commit 3aeb3d44e6
959 changed files with 47688 additions and 46 deletions

View File

@@ -0,0 +1,64 @@
@tool
class_name DialogicCharacterFormatLoader
extends ResourceFormatLoader
## Returns all excepted extenstions
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["dch"])
## Returns "Resource" if this file can/should be loaded by this script
func _get_resource_type(path: String) -> String:
var ext := path.get_extension().to_lower()
if ext == "dch":
return "Resource"
return ""
## Returns the script class associated with a Resource
func _get_resource_script_class(path: String) -> String:
var ext := path.get_extension().to_lower()
if ext == "dch":
return "DialogicCharacter"
return ""
## Return true if this type is handled
func _handles_type(typename: StringName) -> bool:
return ClassDB.is_parent_class(typename, "Resource")
## Parse the file and return a resource
func _load(path: String, _original_path: String, _use_sub_threads: bool, _cache_mode: int) -> Variant:
# print('[Dialogic] Reimporting character "' , path, '"')
var file := FileAccess.open(path, FileAccess.READ)
if not file:
# For now, just let editor know that for some reason you can't
# read the file.
print("[Dialogic] Error opening file:", FileAccess.get_open_error())
return FileAccess.get_open_error()
return dict_to_inst(str_to_var(file.get_as_text()))
func _get_dependencies(path:String, _add_type:bool) -> PackedStringArray:
var depends_on: PackedStringArray = []
var character: DialogicCharacter = load(path)
for p in character.portraits.values():
if 'path' in p and p.path:
depends_on.append(p.path)
return depends_on
func _rename_dependencies(path: String, renames: Dictionary) -> Error:
var character: DialogicCharacter = load(path)
for p in character.portraits:
if 'path' in character.portraits[p] and character.portraits[p].path in renames:
character.portraits[p].path = renames[character.portraits[p].path]
ResourceSaver.save(character, path)
return OK

View File

@@ -0,0 +1 @@
uid://31xuen3lew2p

View File

@@ -0,0 +1,34 @@
@tool
class_name DialogicCharacterFormatSaver
extends ResourceFormatSaver
func _get_recognized_extensions(_resource: Resource) -> PackedStringArray:
return PackedStringArray(["dch"])
## Return true if this resource should be loaded as a DialogicCharacter
func _recognize(resource: Resource) -> bool:
# Cast instead of using "is" keyword in case is a subclass
resource = resource as DialogicCharacter
if resource:
return true
return false
## Save the resource
func _save(resource: Resource, path: String = '', _flags: int = 0) -> Error:
var file := FileAccess.open(path, FileAccess.WRITE)
if not file:
# For now, just let editor know that for some reason you can't
# read the file.
print("[Dialogic] Error opening file:", FileAccess.get_open_error())
return FileAccess.get_open_error()
var result := var_to_str(inst_to_dict(resource))
file.store_string(result)
# print('[Dialogic] Saved character "' , path, '"')
return OK

View File

@@ -0,0 +1 @@
uid://tpot7s4x2llq

View File

@@ -0,0 +1,46 @@
@tool
class_name DialogicTimelineFormatLoader
extends ResourceFormatLoader
## Returns all excepted extenstions
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["dtl"])
## Returns "Resource" if this file can/should be loaded by this script
func _get_resource_type(path: String) -> String:
var ext := path.get_extension().to_lower()
if ext == "dtl":
return "Resource"
return ""
## Returns the script class associated with a Resource
func _get_resource_script_class(path: String) -> String:
var ext := path.get_extension().to_lower()
if ext == "dtl":
return "DialogicTimeline"
return ""
## Return true if this type is handled
func _handles_type(typename: StringName) -> bool:
return ClassDB.is_parent_class(typename, "Resource")
## Parse the file and return a resource
func _load(path: String, _original_path: String, _use_sub_threads: bool, _cache_mode: int) -> Variant:
var file := FileAccess.open(path, FileAccess.READ)
if not file:
# For now, just let editor know that for some reason you can't
# read the file.
print("[Dialogic] Error opening file:", FileAccess.get_open_error())
return FileAccess.get_open_error()
var tml := DialogicTimeline.new()
tml.from_text(file.get_as_text())
return tml

View File

@@ -0,0 +1 @@
uid://0s2nlh50e8ie

View File

@@ -0,0 +1,61 @@
@tool
class_name DialogicTimelineFormatSaver
extends ResourceFormatSaver
func _get_recognized_extensions(_resource: Resource) -> PackedStringArray:
return PackedStringArray(["dtl"])
## Return true if this resource should be loaded as a DialogicTimeline
func _recognize(resource: Resource) -> bool:
# Cast instead of using "is" keyword in case is a subclass
resource = resource as DialogicTimeline
if resource:
return true
return false
## Save the resource
## TODO: This should use timeline.as_text(), why is this still here?
func _save(resource: Resource, path: String = '', _flags: int = 0) -> Error:
if resource.get_meta("timeline_not_saved", false):
var timeline_as_text := ""
# if events are resources, create text
if resource.events_processed:
var indent := 0
for idx in range(0, len(resource.events)):
if resource.events[idx]:
var event: DialogicEvent = resource.events[idx]
if event.event_name == 'End Branch':
indent -=1
continue
for i in event.empty_lines_above:
timeline_as_text += '\t'.repeat(indent) + '\n'
if event != null:
timeline_as_text += "\t".repeat(indent)+ event.event_node_as_text + "\n"
if event.can_contain_events:
indent += 1
if indent < 0:
indent = 0
# if events are string lines, just save them
else:
for event in resource.events:
timeline_as_text += event + "\n"
# Now do the actual saving
var file := FileAccess.open(path, FileAccess.WRITE)
if !file:
print("[Dialogic] Error opening file:", FileAccess.get_open_error())
return ERR_CANT_OPEN
file.store_string(timeline_as_text)
file.close()
return OK

View File

@@ -0,0 +1 @@
uid://61aj5oo1ko0u

View File

@@ -0,0 +1,142 @@
@tool
extends Resource
class_name DialogicCharacter
## Resource that represents a character in dialog.
## Manages/contains portraits, custom info and translation of characters.
@export var display_name := ""
@export var nicknames := []
@export var color := Color()
@export var description := ""
@export var scale := 1.0
@export var offset := Vector2()
@export var mirror := false
@export var default_portrait := ""
@export var portraits := {}
@export var custom_info := {}
## All valid properties that can be accessed by their translation.
enum TranslatedProperties {
NAME,
NICKNAMES,
}
var _translation_id := ""
func _to_string() -> String:
return "[{name}:{id}]".format({"name":get_character_name(), "id":get_instance_id()})
## Adds a translation ID to the character.
func add_translation_id() -> String:
_translation_id = DialogicUtil.get_next_translation_id()
return _translation_id
## Returns the character's translation ID.
## Adds a translation ID to the character if it doesn't have one.
func get_set_translation_id() -> String:
if _translation_id == null or _translation_id.is_empty():
return add_translation_id()
else:
return _translation_id
## Removes the translation ID from the character.
func remove_translation_id() -> void:
_translation_id = ""
## Checks [param property] and matches it to a translation key.
##
## Undefined behaviour if an invalid integer is passed.
func get_property_translation_key(property: TranslatedProperties) -> String:
var property_key := ""
match property:
TranslatedProperties.NAME:
property_key = "name"
TranslatedProperties.NICKNAMES:
property_key = "nicknames"
return "Character".path_join(_translation_id).path_join(property_key)
## Accesses the original text of the character.
##
## Undefined behaviour if an invalid integer is passed.
func _get_property_original_text(property: TranslatedProperties) -> String:
match property:
TranslatedProperties.NAME:
return display_name
TranslatedProperties.NICKNAMES:
return ", ".join(nicknames)
return ""
## Access a property of the character and if conditions are met, attempts to
## translate the property.
##
## The translation feature must be enabled in the project settings.
## The translation ID must be set.
## Otherwise, returns the text property as is.
##
## Undefined behaviour if an invalid integer is passed.
func _get_property_translated(property: TranslatedProperties) -> String:
var try_translation: bool = (_translation_id != null
and not _translation_id.is_empty()
and ProjectSettings.get_setting('dialogic/translation/enabled', false)
)
if try_translation:
var translation_key := get_property_translation_key(property)
var translated_property := tr(translation_key)
# If no translation is found, tr() returns the ID.
# However, we want to fallback to the original text.
if translated_property == translation_key:
return _get_property_original_text(property)
return translated_property
else:
return _get_property_original_text(property)
## Translates the nicknames of the characters and then returns them as an array
## of strings.
func get_nicknames_translated() -> Array:
var translated_nicknames := _get_property_translated(TranslatedProperties.NICKNAMES)
return (translated_nicknames.split(", ") as Array)
## Translates and returns the display name of the character.
func get_display_name_translated() -> String:
return _get_property_translated(TranslatedProperties.NAME)
## Returns the best name for this character.
func get_character_name() -> String:
var unique_identifier := DialogicResourceUtil.get_unique_identifier(resource_path)
if not unique_identifier.is_empty():
return unique_identifier
if not resource_path.is_empty():
return resource_path.get_file().trim_suffix('.dch')
elif not display_name.is_empty():
return display_name.validate_node_name()
else:
return "UnnamedCharacter"
## Returns the info of the given portrait.
## Uses the default portrait if the given portrait doesn't exist.
func get_portrait_info(portrait_name:String) -> Dictionary:
return portraits.get(portrait_name, portraits.get(default_portrait, {}))

View File

@@ -0,0 +1 @@
uid://dga1u6ytv3wk1

View File

@@ -0,0 +1,78 @@
@tool
class_name DialogicLayoutBase
extends Node
## Base class that should be extended by custom layouts.
## Method that adds a node as a layer
func add_layer(layer:DialogicLayoutLayer) -> Node:
add_child(layer)
return layer
## Method that returns the given child
func get_layer(index:int) -> Node:
return get_child(index)
## Method to return all the layers
func get_layers() -> Array:
var layers := []
for child in get_children():
if child is DialogicLayoutLayer:
layers.append(child)
return layers
## Method that is called to load the export overrides.
## This happens when the style is first introduced,
## but also when switching to a different style using the same scene!
func apply_export_overrides() -> void:
_apply_export_overrides()
for child in get_children():
if child.has_method('_apply_export_overrides'):
child._apply_export_overrides()
## Returns a setting on this base.
## This is useful so that layers can share settings like base_color, etc.
func get_global_setting(setting:StringName, default:Variant) -> Variant:
if setting in self:
return get(setting)
if str(setting).to_lower() in self:
return get(setting.to_lower())
if 'global_'+str(setting) in self:
return get('global_'+str(setting))
return default
## To be overwritten. Apply the settings to your scene here.
func _apply_export_overrides() -> void:
pass
#region HANDLE PERSISTENT DATA
################################################################################
func _enter_tree() -> void:
_load_persistent_info(Engine.get_meta("dialogic_persistent_style_info", {}))
func _exit_tree() -> void:
Engine.set_meta("dialogic_persistent_style_info", _get_persistent_info())
## To be overwritten. Return any info that a later used style might want to know.
func _get_persistent_info() -> Dictionary:
return {}
## To be overwritten. Apply any info that a previous style might have stored and this style should use.
func _load_persistent_info(info: Dictionary) -> void:
pass
#endregion

View File

@@ -0,0 +1 @@
uid://fu0bnj4wy2d7

View File

@@ -0,0 +1,44 @@
@tool
class_name DialogicLayoutLayer
extends Node
## Base class that should be extended by custom dialogic layout layers.
@export_group('Layer')
@export_subgroup('Disabled')
@export var disabled := false
## This is turned on automatically when the layout is realized [br] [br]
## Turn it off, if you want to modify the settings of the nodes yourself.
@export_group('Private')
@export var apply_overrides_on_ready := false
var this_folder: String = get_script().resource_path.get_base_dir()
func _ready() -> void:
if apply_overrides_on_ready and not Engine.is_editor_hint():
_apply_export_overrides()
## Override this and load all your exported settings (apply them to the scene)
func _apply_export_overrides() -> void:
pass
func apply_export_overrides() -> void:
if disabled:
if "visible" in self:
set('visible', false)
process_mode = Node.PROCESS_MODE_DISABLED
else:
if "visible" in self:
set('visible', true)
process_mode = Node.PROCESS_MODE_INHERIT
_apply_export_overrides()
## Use this to get potential global settings.
func get_global_setting(setting_name:StringName, default:Variant) -> Variant:
return get_parent().get_global_setting(setting_name, default)

View File

@@ -0,0 +1 @@
uid://cd2jfavry1y6y

View File

@@ -0,0 +1,299 @@
@tool
extends Resource
class_name DialogicStyle
## A style represents a collection of layers and settings.
## A style can inherit from another style.
@export var name := "Style":
get:
if name.is_empty():
return "Unkown Style"
return name
@export var inherits: DialogicStyle = null
## Stores the layer order
@export var layer_list: Array[String] = []
## Stores the layer infos
@export var layer_info := {
"" : DialogicStyleLayer.new()
}
func _init(_name := "") -> void:
if not _name.is_empty():
name = _name
#region BASE METHODS
# These methods are local, meaning they do NOT take inheritance into account.
## Returns the amount of layers (the base layer is not included).
func get_layer_count() -> int:
return layer_list.size()
## Returns the index of the layer with [param id] in the layer list.
## Returns -1 for the base layer (id=="") which is not in the layer list.
func get_layer_index(id:String) -> int:
return layer_list.find(id)
## Returns `true` if [param id] is a valid id for a layer.
func has_layer(id:String) -> bool:
return id in layer_info or id == ""
## Returns `true` if [param index] is a valid index for a layer.
func has_layer_index(index:int) -> bool:
return index < layer_list.size()
## Returns the id of the layer at [param index].
func get_layer_id_at_index(index:int) -> String:
if index == -1:
return ""
if has_layer_index(index):
return layer_list[index]
return ""
func get_layer_info(id:String) -> Dictionary:
var info := {"id": id, "path": "", "overrides": {}}
if has_layer(id):
var layer_resource: DialogicStyleLayer = layer_info[id]
if layer_resource.scene != null:
info.path = layer_resource.scene.resource_path
elif id == "":
info.path = DialogicUtil.get_default_layout_base().resource_path
info.overrides = layer_resource.overrides.duplicate()
return info
#endregion
#region MODIFICATION METHODS
# These methods modify the layers of this style.
## Returns a new layer id not yet in use.
func get_new_layer_id() -> String:
var i := 16
while String.num_int64(i, 16) in layer_info:
i += 1
return String.num_int64(i, 16)
## Adds a layer with the given scene and overrides.
## Returns the new layers id.
func add_layer(scene:String, overrides:Dictionary = {}, id:= "##") -> String:
if id == "##":
id = get_new_layer_id()
layer_info[id] = DialogicStyleLayer.new(scene, overrides)
layer_list.append(id)
changed.emit()
return id
## Deletes the layer with the given id.
## Deleting the base layer is not allowed.
func delete_layer(id:String) -> void:
if not has_layer(id) or id == "":
return
layer_info.erase(id)
layer_list.erase(id)
changed.emit()
## Moves the layer at [param from_index] to [param to_index].
func move_layer(from_index:int, to_index:int) -> void:
if not has_layer_index(from_index) or not has_layer_index(to_index-1):
return
var id := layer_list.pop_at(from_index)
layer_list.insert(to_index, id)
changed.emit()
## Changes the scene property of the DialogicStyleLayer resource at [param layer_id].
func set_layer_scene(layer_id:String, scene:String) -> void:
if not has_layer(layer_id):
return
layer_info[layer_id].scene = load(scene)
changed.emit()
func set_layer_overrides(layer_id:String, overrides:Dictionary) -> void:
if not has_layer(layer_id):
return
layer_info[layer_id].overrides = overrides
changed.emit()
## Changes an override of the DialogicStyleLayer resource at [param layer_id].
func set_layer_setting(layer_id:String, setting:String, value:Variant) -> void:
if not has_layer(layer_id):
return
layer_info[layer_id].overrides[setting] = value
changed.emit()
## Resets (removes) an override of the DialogicStyleLayer resource at [param layer_id].
func remove_layer_setting(layer_id:String, setting:String) -> void:
if not has_layer(layer_id):
return
layer_info[layer_id].overrides.erase(setting)
changed.emit()
#
#endregion
#region INHERITANCE METHODS
# These methods are what you should usually use to get info about this style.
## Returns `true` if this style is inheriting from another style.
func inherits_anything() -> bool:
return inherits != null
## Returns the base style of this style.
func get_inheritance_root() -> DialogicStyle:
if not inherits_anything():
return self
var style: DialogicStyle = self
while style.inherits_anything():
style = style.inherits
return style
## This merges some [param layer_info] with it's param ancestors layer info.
func merge_layer_infos(layer_info:Dictionary, ancestor_info:Dictionary) -> Dictionary:
var combined := layer_info.duplicate(true)
combined.path = ancestor_info.path
combined.overrides.merge(ancestor_info.overrides)
return combined
## Returns the layer info of the layer at [param id] taking into account inherited info.
## If [param inherited_only] is `true`, the local info is not included.
func get_layer_inherited_info(id:String, inherited_only := false) -> Dictionary:
var style := self
var info := {"id": id, "path": "", "overrides": {}}
if not inherited_only:
info = get_layer_info(id)
while style.inherits_anything():
style = style.inherits
info = merge_layer_infos(info, style.get_layer_info(id))
return info
## Returns the layer list of the root style.
func get_layer_inherited_list() -> Array:
var list := layer_list
if inherits_anything():
list = get_inheritance_root().layer_list
return list
## Applies inherited info to the local layers.
## Then removes inheritance.
func realize_inheritance() -> void:
layer_list = get_layer_inherited_list()
var new_layer_info := {}
for id in layer_info:
var info := get_layer_inherited_info(id)
new_layer_info[id] = DialogicStyleLayer.new(info.get("path", ""), info.get("overrides", {}))
layer_info = new_layer_info
inherits = null
changed.emit()
#endregion
## Creates a fresh new style with the same settings.
func clone() -> DialogicStyle:
var style := DialogicStyle.new()
style.name = name
style.inherits = inherits
var base_info := get_layer_info("")
set_layer_scene("", base_info.path)
set_layer_overrides("", base_info.overrides)
for id in layer_list:
var info := get_layer_info(id)
style.add_layer(info.path, info.overrides, id)
return style
## Starts preloading all the scenes used by this style.
func prepare() -> void:
for id in layer_info:
if layer_info[id].scene:
ResourceLoader.load_threaded_request(layer_info[id].scene.resource_path)
#region UPDATE OLD STYLES
# TODO deprecated when going into beta
# TODO Deprecated, only for Styles before alpha 16!
@export var base_scene: PackedScene = null
# TODO Deprecated, only for Styles before alpha 16!
@export var base_overrides := {}
# TODO Deprecated, only for Styles before alpha 16!
@export var layers: Array[DialogicStyleLayer] = []
func update_from_pre_alpha16() -> void:
if not layers.is_empty():
var idx := 0
for layer in layers:
var id := "##"
if inherits_anything():
id = get_layer_inherited_list()[idx]
if layer.scene:
add_layer(layer.scene.resource_path, layer.overrides, id)
else:
add_layer("", layer.overrides, id)
idx += 1
layers.clear()
if not base_scene == null:
set_layer_scene("", base_scene.resource_path)
base_scene = null
if not base_overrides.is_empty():
set_layer_overrides("", base_overrides)
base_overrides.clear()
#endregion

View File

@@ -0,0 +1 @@
uid://b5nqsdfdl0jjg

View File

@@ -0,0 +1,14 @@
@tool
class_name DialogicStyleLayer
extends Resource
@export var scene: PackedScene = null
@export var overrides := {}
func _init(scene_path:Variant=null, scene_overrides:Dictionary={}):
if scene_path is PackedScene:
scene = scene_path
elif scene_path is String and ResourceLoader.exists(scene_path):
scene = load(scene_path)
overrides = scene_overrides

View File

@@ -0,0 +1 @@
uid://1qacwh2qaoiq

View File

@@ -0,0 +1,549 @@
@tool
class_name DialogicEvent
extends Resource
## Base event class for all dialogic events.
## Implements basic properties, translation, shortcode saving and usefull methods for creating
## the editor UI.
## Emmited when the event starts.
## The signal is emmited with the event resource [code]event_resource[/code]
signal event_started(event_resource:DialogicEvent)
## Emmited when the event finish.
## The signal is emmited with the event resource [code]event_resource[/code]
signal event_finished(event_resource:DialogicEvent)
### Main Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## The event name that'll be displayed in the editor.
var event_name := "Event"
## Unique identifier used for translatable events.
var _translation_id := ""
## A reference to dialogic during execution, can be used the same as Dialogic (reference to the autoload)
var dialogic: DialogicGameHandler = null
### Special Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### (these properties store how this event affects indentation/flow of timeline)
## If true this event can not be toplevel (e.g. Choice)
var needs_indentation := false
## If true this event will spawn with an END BRANCH event and higher the indentation
var can_contain_events := false
## If [can_contain_events] is true this is a reference to the end branch event
var end_branch_event: DialogicEndBranchEvent = null
## If this is true this event will group with other similar events (like choices do).
var wants_to_group := false
### Saving/Loading Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## Stores the event in a text format. Does NOT automatically update.
var event_node_as_text := ""
## Flags if the event has been processed or is only stored as text
var event_node_ready := false
## How many empty lines are before this event
var empty_lines_above: int = 0
### Editor UI Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## The event color that event node will take in the editor
var event_color := Color("FBB13C")
## If you are using the default color palette
var dialogic_color_name: = ""
## To sort the buttons shown in the editor. Lower index is placed at the top of a category
var event_sorting_index: int = 0
## If true the event will not have a button in the visual editor sidebar
var disable_editor_button := false
## If false the event will hide it's body by default. Recommended for most events
var expand_by_default := false
## The URL to open when right_click>Documentation is selected
var help_page_path := ""
## Is the event block created by a button?
var created_by_button := false
## Reference to the node, that represents this event. Only works while in visual editor mode.
## Use with care.
var editor_node: Control = null
## The categories and which one to put it in (in the visual editor sidebar)
var event_category := "Other"
### Editor UI creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## To differentiate fields that should go to the header and to the body
enum Location {HEADER, BODY}
## To differentiate the different types of fields for event properties in the visual editor
enum ValueType {
# Strings
MULTILINE_TEXT, SINGLELINE_TEXT, CONDITION, FILE,
# Booleans
BOOL, BOOL_BUTTON,
# Options
DYNAMIC_OPTIONS, FIXED_OPTIONS,
# Containers,
ARRAY, DICTIONARY,
# Numbers
NUMBER,
VECTOR2, VECTOR3, VECTOR4,
# Other
CUSTOM, BUTTON, LABEL, COLOR, AUDIO_PREVIEW
}
## List that stores the fields for the editor
var editor_list: Array = []
var this_folder: String = get_script().resource_path.get_base_dir()
## Singal that notifies the visual editor block to update
signal ui_update_needed
signal ui_update_warning(text:String)
## Makes this resource printable.
func _to_string() -> String:
return "[{name}:{id}]".format({"name":event_name, "id":get_instance_id()})
#endregion
#region EXECUTION
################################################################################
## Executes the event behaviour. In subclasses [_execute] (not this one) should be overriden!
func execute(_dialogic_game_handler) -> void:
event_started.emit(self)
dialogic = _dialogic_game_handler
call_deferred("_execute")
## Ends the event behaviour.
func finish() -> void:
event_finished.emit(self)
## Called before executing the next event or before clear(any flags) / load_full_state().
##
## Should be overridden if the event stores temporary state into dialogic.current_state_info
## or some other cleanup is needed before another event can run.
func _clear_state() -> void:
pass
## To be overridden by subclasses.
func _execute() -> void:
finish()
#endregion
#region OVERRIDABLES
################################################################################
## to be overridden by sub-classes
## only called if can_contain_events is true.
## return a control node that should show on the END BRANCH node
func get_end_branch_control() -> Control:
return null
## to be overridden by sub-classes
## only called if can_contain_events is true and the previous event was an end-branch event
## return true if this event should be executed if the previous event was an end-branch event
## basically only important for the Condition event but who knows. Some day someone might need this.
func should_execute_this_branch() -> bool:
return false
#endregion
#region TRANSLATIONS
################################################################################
## Overwrite if this events needs translation.
func _get_translatable_properties() -> Array:
return []
## Overwrite if this events needs translation.
func _get_property_original_translation(_property_name:String) -> String:
return ''
## Returns true if there is any translatable properties on this event.
## Overwrite [_get_translatable_properties()] to change this.
func can_be_translated() -> bool:
return !_get_translatable_properties().is_empty()
## This is automatically called, no need to use this.
func add_translation_id() -> String:
_translation_id = DialogicUtil.get_next_translation_id()
return _translation_id
func remove_translation_id() -> void:
_translation_id = ""
func get_property_translation_key(property_name:String) -> String:
return event_name.path_join(_translation_id).path_join(property_name)
## Call this whenever you are using a translatable property
func get_property_translated(property_name:String) -> String:
if !_translation_id.is_empty() and ProjectSettings.get_setting('dialogic/translation/enabled', false):
var translation := tr(get_property_translation_key(property_name))
# if no translation is found tr() returns the id, but we want to fallback to the original
return translation if translation != get_property_translation_key(property_name) else _get_property_original_translation(property_name)
else:
return _get_property_original_translation(property_name)
#endregion
#region SAVE / LOAD (internal, don't override)
################################################################################
### These functions are used by the timeline loader/saver
### They mainly use the overridable behaviour below, but enforce the unique_id saving
## Used by the Timeline saver.
func _store_as_string() -> String:
if !_translation_id.is_empty() and can_be_translated():
return to_text() + ' #id:'+str(_translation_id)
else:
return to_text()
## Call this if you updated an event and want the changes to be saved.
func update_text_version() -> void:
event_node_as_text = _store_as_string()
## Used by timeline processor.
func _load_from_string(string:String) -> void:
_load_custom_defaults()
if '#id:' in string and can_be_translated():
_translation_id = string.get_slice('#id:', 1).strip_edges()
from_text(string.get_slice('#id:', 0))
else:
from_text(string)
event_node_ready = true
## Assigns the custom defaults
func _load_custom_defaults() -> void:
for default_prop in DialogicUtil.get_custom_event_defaults(event_name):
if default_prop in self:
set(default_prop, DialogicUtil.get_custom_event_defaults(event_name)[default_prop])
## Used by the timeline processor.
func _test_event_string(string:String) -> bool:
if '#id:' in string and can_be_translated():
return is_valid_event(string.get_slice('#id:', 0))
return is_valid_event(string.strip_edges())
#endregion
#region SAVE / LOAD
################################################################################
### All of these functions can/should be overridden by the sub classes
## If this uses the short-code format, return the shortcode.
func get_shortcode() -> String:
return 'default_shortcode'
## If this uses the short-code format, return the parameters and corresponding property names.
func get_shortcode_parameters() -> Dictionary:
return {}
## Returns a readable presentation of the event (This is how it's stored).
## By default it uses a shortcode format, but can be overridden.
func to_text() -> String:
var shortcode := store_to_shortcode_parameters()
if shortcode:
return "[" + self.get_shortcode() + " " + store_to_shortcode_parameters() + "]"
else:
return "[" + self.get_shortcode() + "]"
## Loads the variables from the string stored by [method to_text].
## By default it uses the shortcode format, but can be overridden.
func from_text(string: String) -> void:
load_from_shortcode_parameters(string)
## Returns a string with all the shortcode parameters.
func store_to_shortcode_parameters(params:Dictionary = {}) -> String:
if params.is_empty():
params = get_shortcode_parameters()
var custom_defaults: Dictionary = DialogicUtil.get_custom_event_defaults(event_name)
var result_string := ""
for parameter in params.keys():
var parameter_info: Dictionary = params[parameter]
var value: Variant = get(parameter_info.property)
var default_value: Variant = custom_defaults.get(parameter_info.property, parameter_info.default)
if parameter_info.get('custom_stored', false):
continue
if "set_" + parameter_info.property in self and not get("set_" + parameter_info.property):
continue
if typeof(value) == typeof(default_value) and value == default_value:
if not "set_" + parameter_info.property in self or not get("set_" + parameter_info.property):
continue
result_string += " " + parameter + '="' + value_to_string(value, parameter_info.get("suggestions", Callable())) + '"'
return result_string.strip_edges()
func value_to_string(value: Variant, suggestions := Callable()) -> String:
var value_as_string := ""
match typeof(value):
TYPE_OBJECT:
value_as_string = str(value.resource_path)
TYPE_STRING:
value_as_string = value
TYPE_INT when suggestions.is_valid():
# HANDLE TEXT ALTERNATIVES FOR ENUMS
for option in suggestions.call().values():
if option.value != value:
continue
if option.has('text_alt'):
value_as_string = option.text_alt[0]
else:
value_as_string = var_to_str(option.value)
break
TYPE_DICTIONARY:
value_as_string = JSON.stringify(value)
_:
value_as_string = var_to_str(value)
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("}"))):
value_as_string.replace('"', '\\"')
return value_as_string
func load_from_shortcode_parameters(string:String) -> void:
var data: Dictionary = parse_shortcode_parameters(string)
var params: Dictionary = get_shortcode_parameters()
for parameter in params.keys():
var parameter_info: Dictionary = params[parameter]
if parameter_info.get('custom_stored', false):
continue
if not parameter in data:
if "set_" + parameter_info.property in self:
set("set_" + parameter_info.property, false)
continue
if "set_" + parameter_info.property in self:
set("set_" + parameter_info.property, true)
var param_value: String = data[parameter].replace('\\"', '"')
var value: Variant
match typeof(get(parameter_info.property)):
TYPE_STRING:
value = param_value
TYPE_INT:
# If a string is given
if parameter_info.has('suggestions'):
for option in parameter_info.suggestions.call().values():
if option.has('text_alt') and param_value in option.text_alt:
value = option.value
break
if not value:
value = float(param_value)
_:
value = str_to_var(param_value)
set(parameter_info.property, value)
## Has to return `true`, if the given string can be interpreted as this event.
## By default it uses the shortcode formta, but can be overridden.
func is_valid_event(string: String) -> bool:
if string.strip_edges().begins_with('['+get_shortcode()+' ') or string.strip_edges().begins_with('['+get_shortcode()+']'):
return true
return false
## has to return true if this string seems to be a full event of this kind
## (only tested if is_valid_event() returned true)
## if a shortcode it used it will default to true if the string ends with ']'
func is_string_full_event(string: String) -> bool:
if get_shortcode() != 'default_shortcode': return string.strip_edges().ends_with(']')
return true
## Used to get all the shortcode parameters in a string as a dictionary.
func parse_shortcode_parameters(shortcode: String) -> Dictionary:
var regex := RegEx.new()
regex.compile(r'(?<parameter>[^\s=]*)\s*=\s*"(?<value>(\{[^}]*\}|\[[^]]*\]|([^"]|\\")*|))(?<!\\)\"')
var dict := {}
for result in regex.search_all(shortcode):
dict[result.get_string('parameter')] = result.get_string('value')
return dict
#endregion
#region EDITOR REPRESENTATION
################################################################################
func _get_icon() -> Resource:
var _icon_file_name := "res://addons/dialogic/Editor/Images/Pieces/closed-icon.svg" # Default
# Check for both svg and png, but prefer svg if available
if ResourceLoader.exists(self.get_script().get_path().get_base_dir() + "/icon.svg"):
_icon_file_name = self.get_script().get_path().get_base_dir() + "/icon.svg"
elif ResourceLoader.exists(self.get_script().get_path().get_base_dir() + "/icon.png"):
_icon_file_name = self.get_script().get_path().get_base_dir() + "/icon.png"
return load(_icon_file_name)
func set_default_color(value:Variant) -> void:
dialogic_color_name = value
event_color = DialogicUtil.get_color(value)
## Called when the resource is assigned to a event block in the visual editor
func _enter_visual_editor(_timeline_editor:DialogicEditor) -> void:
pass
#endregion
#region CODE COMPLETION
################################################################################
## This method can be overwritten to implement code completion for custom syntaxes
func _get_code_completion(_CodeCompletionHelper:Node, _TextNode:TextEdit, _line:String, _word:String, _symbol:String) -> void:
pass
## This method can be overwritten to add starting suggestions for this event
func _get_start_code_completion(_CodeCompletionHelper:Node, _TextNode:TextEdit) -> void:
pass
#endregion
#region SYNTAX HIGHLIGHTING
################################################################################
func _get_syntax_highlighting(_Highlighter:SyntaxHighlighter, dict:Dictionary, _line:String) -> Dictionary:
return dict
#endregion
#region EVENT FIELDS
################################################################################
func get_event_editor_info() -> Array:
if Engine.is_editor_hint():
if editor_list != null:
editor_list.clear()
else:
editor_list = []
build_event_editor()
return editor_list
else:
return []
## to be overwritten by the sub_classes
func build_event_editor() -> void:
pass
## For the methods below the arguments are mostly similar:
## @variable: String name of the property this field is for
## @condition: String that will be executed as an expression. If it false
## @editor_type: One of the ValueTypes (see ValueType enum). Defines type of field.
## @left_text: Text that will be shown to the left of the field
## @right_text: Text that will be shown to the right of the field
## @extra_info: Allows passing a lot more info to the field.
## What info can be passed is differnet for every field
func add_header_label(text:String, condition:= "") -> void:
editor_list.append({
"name" : "something",
"type" :+ TYPE_STRING,
"location" : Location.HEADER,
"usage" : PROPERTY_USAGE_EDITOR,
"field_type" : ValueType.LABEL,
"display_info" : {"text":text},
"condition" : condition
})
func add_header_edit(variable:String, editor_type := ValueType.LABEL, extra_info:= {}, condition:= "") -> void:
editor_list.append({
"name" : variable,
"type" : typeof(get(variable)),
"location" : Location.HEADER,
"usage" : PROPERTY_USAGE_DEFAULT,
"field_type" : editor_type,
"display_info" : extra_info,
"left_text" : extra_info.get('left_text', ''),
"right_text" : extra_info.get('right_text', ''),
"condition" : condition,
})
func add_header_button(text:String, callable:Callable, tooltip:String, icon: Variant = null, condition:= "") -> void:
editor_list.append({
"name" : "Button",
"type" : TYPE_STRING,
"location" : Location.HEADER,
"usage" : PROPERTY_USAGE_DEFAULT,
"field_type" : ValueType.BUTTON,
"display_info" : {'text':text, 'tooltip':tooltip, 'callable':callable, 'icon':icon},
"condition" : condition,
})
func add_body_edit(variable:String, editor_type := ValueType.LABEL, extra_info:= {}, condition:= "") -> void:
editor_list.append({
"name" : variable,
"type" : typeof(get(variable)),
"location" : Location.BODY,
"usage" : PROPERTY_USAGE_DEFAULT,
"field_type" : editor_type,
"display_info" : extra_info,
"left_text" : extra_info.get('left_text', ''),
"right_text" : extra_info.get('right_text', ''),
"condition" : condition,
})
func add_body_line_break(condition:= "") -> void:
editor_list.append({
"name" : "linebreak",
"type" : TYPE_BOOL,
"location" : Location.BODY,
"usage" : PROPERTY_USAGE_DEFAULT,
"condition" : condition,
})
#endregion

View File

@@ -0,0 +1 @@
uid://bv800geo7gnog

View File

@@ -0,0 +1,166 @@
@tool
extends Resource
class_name DialogicTimeline
## Resource that defines a list of events.
## It can store them as text and load them from text too.
var events: Array = []
var events_processed := false
## Method used for printing timeline resources identifiably
func _to_string() -> String:
return "[DialogicTimeline:{file}]".format({"file":resource_path})
## Helper method
func get_event(index:int) -> Variant:
if index >= len(events):
return null
return events[index]
## Parses the lines as seperate events and insert them in an array,
## so they can be converted to DialogicEvent's when processed later
func from_text(text:String) -> void:
events = text.split('\n', true)
events_processed = false
## Stores all events in their text format and returns them as a string
func as_text() -> String:
var result := ""
if events_processed:
var indent := 0
for idx in range(0, len(events)):
var event: DialogicEvent = events[idx]
if event.event_name == 'End Branch':
indent -= 1
continue
if event != null:
for i in event.empty_lines_above:
result += "\t".repeat(indent)+"\n"
result += "\t".repeat(indent)+event.event_node_as_text.replace('\n', "\n"+"\t".repeat(indent)) + "\n"
if event.can_contain_events:
indent += 1
if indent < 0:
indent = 0
else:
for event in events:
result += str(event)+"\n"
result.trim_suffix('\n')
return result.strip_edges()
## Method that loads all the event resources from the strings, if it wasn't done before
func process() -> void:
if typeof(events[0]) == TYPE_STRING:
events_processed = false
# if the timeline is already processed
if events_processed:
for event in events:
event.event_node_ready = true
return
var event_cache := DialogicResourceUtil.get_event_cache()
var end_event := DialogicEndBranchEvent.new()
var prev_indent := ""
var processed_events := []
# this is needed to add an end branch event even to empty conditions/choices
var prev_was_opener := false
var lines := events
var idx := -1
var empty_lines := 0
while idx < len(lines)-1:
idx += 1
# make sure we are using the string version, in case this was already converted
var line := ""
if typeof(lines[idx]) == TYPE_STRING:
line = lines[idx]
else:
line = lines[idx].event_node_as_text
## Ignore empty lines, but record them in @empty_lines
var line_stripped: String = line.strip_edges(true, false)
if line_stripped.is_empty():
empty_lines += 1
continue
## Add an end event if the indent is smaller then previously
var indent: String = line.substr(0,len(line)-len(line_stripped))
if len(indent) < len(prev_indent):
for i in range(len(prev_indent)-len(indent)):
processed_events.append(end_event.duplicate())
## Add an end event if the indent is the same but the previous was an opener
## (so for example choice that is empty)
if prev_was_opener and len(indent) <= len(prev_indent):
processed_events.append(end_event.duplicate())
prev_indent = indent
## Now we process the event into a resource
## by checking on each event if it recognizes this string
var event_content: String = line_stripped
var event: DialogicEvent
for i in event_cache:
if i._test_event_string(event_content):
event = i.duplicate()
break
event.empty_lines_above = empty_lines
# add the following lines until the event says it's full or there is an empty line
while !event.is_string_full_event(event_content):
idx += 1
if idx == len(lines):
break
var following_line_stripped: String = lines[idx].strip_edges(true, false)
if following_line_stripped.is_empty():
break
event_content += "\n"+following_line_stripped
event._load_from_string(event_content)
event.event_node_as_text = event_content
processed_events.append(event)
prev_was_opener = event.can_contain_events
empty_lines = 0
if !prev_indent.is_empty():
for i in range(len(prev_indent)):
processed_events.append(end_event.duplicate())
events = processed_events
events_processed = true
## This method makes sure that all events in a timeline are correctly reset
func clean() -> void:
if not events_processed:
return
reference()
# This is necessary because otherwise INTERNAL GODOT ONESHOT CONNECTIONS
# are disconnected before they can disconnect themselves.
await Engine.get_main_loop().process_frame
for event:DialogicEvent in events:
for con_in in event.get_incoming_connections():
con_in.signal.disconnect(con_in.callable)
for sig in event.get_signal_list():
for con_out in event.get_signal_connection_list(sig.name):
con_out.signal.disconnect(con_out.callable)
unreference()

View File

@@ -0,0 +1 @@
uid://cw32xk122cjta