Updated dialogic

This commit is contained in:
MaddoScientisto 2026-01-05 16:00:41 +01:00
commit cbb82512ee
483 changed files with 5743 additions and 2177 deletions

View file

@ -0,0 +1,397 @@
@tool
## Event that can play audio on a channel. The channel can be prededinfed
## (with default settings defined in the settings) or created on the spot.
## If no channel is given will play as a One-Shot SFX.
class_name DialogicAudioEvent
extends DialogicEvent
### Settings
## The file to play. If empty, the previous audio will be faded out.
var file_path := "":
set(value):
if file_path != value:
file_path = value
ui_update_needed.emit()
## The channel name to use. If none given plays as a One-Shot SFX.
var channel_name := "":
set(value):
if channel_name != channel_name_regex.sub(value, '', true):
channel_name = channel_name_regex.sub(value, '', true)
var defaults: Dictionary = DialogicUtil.get_audio_channel_defaults().get(channel_name, {})
if defaults:
fade_length = defaults.fade_length
volume = defaults.volume
audio_bus = defaults.audio_bus
loop = defaults.loop
ui_update_needed.emit()
## The length of the fade. If 0 it's an instant change.
var fade_length: float = 0.0
## The volume in decibel.
var volume: float = 0.0
## The audio bus the audio will be played on.
var audio_bus := ""
## If true, the audio will loop, otherwise only play once.
var loop := true
## Sync starting time with different channel (if playing audio on that channel)
var sync_channel := ""
## Helpers. Set automatically
var set_fade_length := false
var set_volume := false
var set_audio_bus := false
var set_loop := false
var set_sync_channel := false
var regex := RegEx.create_from_string(r'(?:audio)\s*(?<channel>[\w-]{2,}|[\w]*)?\s*(")?(?<file_path>(?(2)[^"\n]*|[^(: \n]*))(?(2)"|)(?:\s*\[(?<shortcode>.*)\])?')
var channel_name_regex := RegEx.create_from_string(r'(?<dash_only>^-$)|(?<invalid>[^\w-]{1})')
################################################################################
## EXECUTE
################################################################################
func _execute() -> void:
var audio_settings_overrides := {}
if set_audio_bus:
audio_settings_overrides["audio_bus"] = audio_bus
if set_volume:
audio_settings_overrides["volume"] = volume
if set_fade_length:
audio_settings_overrides["fade_length"] = fade_length
if set_loop:
audio_settings_overrides["loop"] = loop
audio_settings_overrides["sync_channel"] = sync_channel
dialogic.Audio.update_audio(channel_name, file_path, audio_settings_overrides)
finish()
################################################################################
## INITIALIZE
################################################################################
func _init() -> void:
event_name = "Audio"
set_default_color('Color7')
event_category = "Audio"
event_sorting_index = 2
func _get_icon() -> Resource:
return load(this_folder.path_join('icon_music.png'))
################################################################################
## SAVING/LOADING
################################################################################
func to_text () -> String:
var result_string := "audio "
if not channel_name.is_empty():
result_string += channel_name + " "
else:
loop = false
if not file_path.is_empty():
result_string += "\"" + file_path + "\""
else:
result_string += "-"
var shortcode := store_to_shortcode_parameters()
if not shortcode.is_empty():
result_string += " [" + shortcode + "]"
return result_string
func from_text(string:String) -> void:
# Pre Alpha 17 Conversion
if string.begins_with('[music'):
_music_from_text(string)
return
elif string.begins_with('[sound'):
_sound_from_text(string)
return
var result := regex.search(string)
channel_name = result.get_string('channel')
if result.get_string('file_path') == '-':
file_path = ""
else:
file_path = result.get_string('file_path')
if not result.get_string('shortcode'):
return
load_from_shortcode_parameters(result.get_string('shortcode'))
func get_shortcode_parameters() -> Dictionary:
return {
#param_name : property_info
"path" : {"property": "file_path", "default": "", "custom_stored":true, "ext_file":true},
"channel" : {"property": "channel_name", "default": "", "custom_stored":true},
"fade" : {"property": "fade_length", "default": 0.0},
"volume" : {"property": "volume", "default": 0.0},
"bus" : {"property": "audio_bus", "default": "",
"suggestions": DialogicUtil.get_audio_bus_suggestions},
"loop" : {"property": "loop", "default": true},
"sync" : {"property": "sync_channel", "default": "",
"suggestions": get_sync_audio_channel_suggestions},
}
## 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 channel_defaults := DialogicUtil.get_audio_channel_defaults()
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 channel_name in channel_defaults.keys():
default_value = channel_defaults[channel_name].get(parameter_info.property, default_value)
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 is_valid_event(string:String) -> bool:
if string.begins_with("audio"):
return true
# Pre Alpha 17 Converter
if string.strip_edges().begins_with('[music '):
return true
if string.strip_edges().begins_with('[sound '):
return true
return false
#region PreAlpha17 Conversion
func _music_from_text(string:String) -> void:
var data := parse_shortcode_parameters(string)
if data.has('channel') and data['channel'].to_int() > 0:
channel_name = 'music' + str(data['channel'].to_int() + 1)
else:
channel_name = 'music'
# Reapply original defaults as setting channel name may have overridden them
fade_length = 0.0
volume = 0.0
audio_bus = ''
loop = true
# Apply any custom event defaults
for default_prop in DialogicUtil.get_custom_event_defaults('music'):
if default_prop in self:
set(default_prop, DialogicUtil.get_custom_event_defaults('music')[default_prop])
# Apply shortcodes that exist
if data.has('path'):
file_path = data['path']
if data.has('fade'):
set_fade_length = true
fade_length = data['fade'].to_float()
if data.has('volume'):
set_volume = true
volume = data['volume'].to_float()
if data.has('bus'):
set_audio_bus = true
audio_bus = data['bus']
if data.has('loop'):
set_loop = true
loop = str_to_var(data['loop'])
update_text_version()
func _sound_from_text(string:String) -> void:
var data := parse_shortcode_parameters(string)
channel_name = ''
# Reapply original defaults as setting channel name may have overridden them
fade_length = 0.0
volume = 0.0
audio_bus = ''
loop = false
# Apply any custom event defaults
for default_prop in DialogicUtil.get_custom_event_defaults('sound'):
if default_prop in self:
set(default_prop, DialogicUtil.get_custom_event_defaults('sound')[default_prop])
# Apply shortcodes that exist
if data.has('path'):
file_path = data['path']
if data.has('volume'):
set_volume = true
volume = data['volume'].to_float()
if data.has('bus'):
set_audio_bus = true
audio_bus = data['bus']
if data.has('loop'):
set_loop = true
loop = str_to_var(data['loop'])
update_text_version()
#endregion
################################################################################
## EDITOR REPRESENTATION
################################################################################
func build_event_editor() -> void:
add_header_edit('file_path', ValueType.FILE, {
'left_text' : 'Play',
'file_filter' : "*.mp3, *.ogg, *.wav; Supported Audio Files",
'placeholder' : "Nothing",
'editor_icon' : ["AudioStreamMP3", "EditorIcons"]})
add_header_edit('file_path', ValueType.AUDIO_PREVIEW)
add_header_edit('channel_name', ValueType.DYNAMIC_OPTIONS, {
'left_text' :"on",
"right_text" : "channel.",
'placeholder' : '(One-Shot SFX)',
'mode' : 3,
'suggestions_func' : get_audio_channel_suggestions,
'validation_func' : DialogicUtil.validate_audio_channel_name,
'tooltip' : 'Use an existing channel or type the name for a new channel.',
})
add_header_button('', _open_audio_settings, 'Edit Audio Channels',
editor_node.get_theme_icon("ExternalLink", "EditorIcons"))
add_body_edit("set_fade_length", ValueType.BOOL_BUTTON,{
"editor_icon" : ["FadeCross", "EditorIcons"],
"tooltip" : "Overwrite Fade Length"
},"!channel_name.is_empty() and has_channel_defaults()")
add_body_edit('fade_length', ValueType.NUMBER, {'left_text':'Fade Time:'},
'!channel_name.is_empty() and (not has_channel_defaults() or set_fade_length)')
add_body_edit("set_volume", ValueType.BOOL_BUTTON,{
"editor_icon" : ["AudioStreamPlayer", "EditorIcons"],
"tooltip" : "Overwrite Volume"
},"!file_path.is_empty() and has_channel_defaults()")
add_body_edit('volume', ValueType.NUMBER, {'left_text':'Volume:', 'mode':2},
'!file_path.is_empty() and (not has_channel_defaults() or set_volume)')
add_body_edit("set_audio_bus", ValueType.BOOL_BUTTON,{
"editor_icon" : ["AudioBusBypass", "EditorIcons"],
"tooltip" : "Overwrite Audio Bus"
},"!file_path.is_empty() and has_channel_defaults()")
add_body_edit('audio_bus', ValueType.DYNAMIC_OPTIONS, {
'left_text':'Audio Bus:',
'placeholder' : 'Master',
'mode' : 2,
'suggestions_func' : DialogicUtil.get_audio_bus_suggestions,
}, '!file_path.is_empty() and (not has_channel_defaults() or set_audio_bus)')
add_body_edit("set_loop", ValueType.BOOL_BUTTON,{
"editor_icon" : ["Loop", "EditorIcons"],
"tooltip" : "Overwrite Loop"
},"!channel_name.is_empty() and !file_path.is_empty() and has_channel_defaults()")
add_body_edit('loop', ValueType.BOOL, {'left_text':'Loop:'},
'!channel_name.is_empty() and !file_path.is_empty() and (not has_channel_defaults() or set_loop)')
add_body_line_break("!channel_name.is_empty() and !file_path.is_empty()")
add_body_edit("set_sync_channel", ValueType.BOOL_BUTTON,{
"editor_icon" : ["TransitionSync", "EditorIcons"],
"tooltip" : "Enable Syncing"
},"!channel_name.is_empty() and !file_path.is_empty()")
add_body_edit('sync_channel', ValueType.DYNAMIC_OPTIONS, {
'left_text' :'Sync with:',
'placeholder' : '(No Sync)',
'mode' : 3,
'suggestions_func' : get_sync_audio_channel_suggestions,
'validation_func' : DialogicUtil.validate_audio_channel_name,
'tooltip' : "Use an existing channel or type the name for a new channel. If channel doesn't exist, this setting will be ignored.",
}, '!channel_name.is_empty() and !file_path.is_empty() and set_sync_channel')
## Used by the button on the visual event
func _open_audio_settings() -> void:
var editor_manager := editor_node.find_parent('EditorsManager')
if editor_manager:
editor_manager.open_editor(editor_manager.editors['Settings']['node'], true, "Audio")
## Helper for the visibility conditions
func has_channel_defaults() -> bool:
var defaults := DialogicUtil.get_audio_channel_defaults()
return defaults.has(channel_name)
func get_audio_channel_suggestions(filter:String) -> Dictionary:
var suggestions := {}
suggestions["(One-Shot SFX)"] = {
"value":"",
"tooltip": "Used for one shot sounds effects. Plays each sound in its own AudioStreamPlayer.",
"editor_icon": ["GuiRadioUnchecked", "EditorIcons"]
}
# TODO use .merged after dropping 4.2 support
suggestions.merge(DialogicUtil.get_audio_channel_suggestions(filter))
return suggestions
func get_sync_audio_channel_suggestions(filter:="") -> Dictionary:
return DialogicUtil.get_audio_channel_suggestions(filter)
####################### CODE COMPLETION ########################################
################################################################################
func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, word:String, symbol:String) -> void:
var line_until: String = CodeCompletionHelper.get_line_untill_caret(line)
if symbol == ' ':
if line_until.count(' ') == 1:
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, "One-Shot SFX", ' ', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6))
for i in DialogicUtil.get_audio_channel_suggestions(""):
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i, event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6), null, " ")
elif line_until.count(" ") == 2:
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, '"', '"', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6))
if symbol == "[" or (symbol == " " and line.count("[")):
for i in ["fade", "volume", "bus", "loop", "sync"]:
if not i+"=" in line:
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+'="', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.6))
if (symbol == '"' or symbol == "=") and line.count("["):
CodeCompletionHelper.suggest_shortcode_values(TextNode, self, line, word)
func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:
TextNode.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'audio', 'audio ', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3))
#################### SYNTAX HIGHLIGHTING #######################################
################################################################################
func _get_syntax_highlighting(Highlighter:SyntaxHighlighter, dict:Dictionary, line:String) -> Dictionary:
var result := regex.search(line)
dict[result.get_start()] = {"color":event_color.lerp(Highlighter.normal_color, 0.3)}
dict[result.get_start("channel")] = {"color":event_color.lerp(Highlighter.normal_color, 0.8)}
dict[result.get_start("file_path")] = {"color":event_color.lerp(Highlighter.string_color, 0.8)}
if result.get_string("shortcode"):
dict[result.get_start("shortcode")-1] = {"color":Highlighter.normal_color}
dict = Highlighter.color_shortcode_content(dict, line, result.get_start("shortcode"), 0, event_color)
return dict

View file

@ -0,0 +1 @@
uid://8p4qchmcuj68

View file

@ -1,105 +0,0 @@
@tool
## Event that can change the currently playing background music.
## This event won't play new music if it's already playing.
class_name DialogicMusicEvent
extends DialogicEvent
### Settings
## The file to play. If empty, the previous music will be faded out.
var file_path := "":
set(value):
if file_path != value:
file_path = value
ui_update_needed.emit()
## The channel to use.
var channel_id: int = 0
## The length of the fade. If 0 (by default) it's an instant change.
var fade_length: float = 0
## The volume the music will be played at.
var volume: float = 0
## The audio bus the music will be played at.
var audio_bus := ""
## If true, the audio will loop, otherwise only play once.
var loop := true
################################################################################
## EXECUTE
################################################################################
func _execute() -> void:
if not dialogic.Audio.is_music_playing_resource(file_path, channel_id):
dialogic.Audio.update_music(file_path, volume, audio_bus, fade_length, loop, channel_id)
finish()
################################################################################
## INITIALIZE
################################################################################
func _init() -> void:
event_name = "Music"
set_default_color('Color7')
event_category = "Audio"
event_sorting_index = 2
func _get_icon() -> Resource:
return load(self.get_script().get_path().get_base_dir().path_join('icon_music.png'))
################################################################################
## SAVING/LOADING
################################################################################
func get_shortcode() -> String:
return "music"
func get_shortcode_parameters() -> Dictionary:
return {
#param_name : property_info
"path" : {"property": "file_path", "default": ""},
"channel" : {"property": "channel_id", "default": 0},
"fade" : {"property": "fade_length", "default": 0},
"volume" : {"property": "volume", "default": 0},
"bus" : {"property": "audio_bus", "default": "",
"suggestions": get_bus_suggestions},
"loop" : {"property": "loop", "default": true},
}
################################################################################
## EDITOR REPRESENTATION
################################################################################
func build_event_editor() -> void:
add_header_edit('file_path', ValueType.FILE, {
'left_text' : 'Play',
'file_filter' : "*.mp3, *.ogg, *.wav; Supported Audio Files",
'placeholder' : "No music",
'editor_icon' : ["AudioStreamPlayer", "EditorIcons"]})
add_header_edit('channel_id', ValueType.FIXED_OPTIONS, {'left_text':'on:', 'options': get_channel_list()})
add_header_edit('file_path', ValueType.AUDIO_PREVIEW)
add_body_edit('fade_length', ValueType.NUMBER, {'left_text':'Fade Time:'})
add_body_edit('volume', ValueType.NUMBER, {'left_text':'Volume:', 'mode':2}, '!file_path.is_empty()')
add_body_edit('audio_bus', ValueType.SINGLELINE_TEXT, {'left_text':'Audio Bus:'}, '!file_path.is_empty()')
add_body_edit('loop', ValueType.BOOL, {'left_text':'Loop:'}, '!file_path.is_empty() and not file_path.to_lower().ends_with(".wav")')
func get_bus_suggestions() -> Dictionary:
var bus_name_list := {}
for i in range(AudioServer.bus_count):
bus_name_list[AudioServer.get_bus_name(i)] = {'value':AudioServer.get_bus_name(i)}
return bus_name_list
func get_channel_list() -> Array:
var channel_name_list := []
for i in ProjectSettings.get_setting('dialogic/audio/max_channels', 4):
channel_name_list.append({
'label': 'Channel %s' % (i + 1),
'value': i,
})
return channel_name_list

View file

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

View file

@ -1,86 +0,0 @@
@tool
class_name DialogicSoundEvent
extends DialogicEvent
## Event that allows to play a sound effect. Requires the Audio subsystem!
### Settings
## The path to the file to play.
var file_path := "":
set(value):
if file_path != value:
file_path = value
ui_update_needed.emit()
## The volume to play the sound at.
var volume: float = 0
## The bus to play the sound on.
var audio_bus := ""
## If true, the sound will loop infinitely. Not recommended (as there is no way to stop it).
var loop := false
################################################################################
## EXECUTE
################################################################################
func _execute() -> void:
dialogic.Audio.play_sound(file_path, volume, audio_bus, loop)
finish()
################################################################################
## INITIALIZE
################################################################################
func _init() -> void:
event_name = "Sound"
set_default_color('Color7')
event_category = "Audio"
event_sorting_index = 3
help_page_path = "https://dialogic.coppolaemilio.com"
func _get_icon() -> Resource:
return load(self.get_script().get_path().get_base_dir().path_join('icon_sound.png'))
################################################################################
## SAVING/LOADING
################################################################################
func get_shortcode() -> String:
return "sound"
func get_shortcode_parameters() -> Dictionary:
return {
#param_name : property_name
"path" : {"property": "file_path", "default": "",},
"volume" : {"property": "volume", "default": 0},
"bus" : {"property": "audio_bus", "default": "",
"suggestions": get_bus_suggestions},
"loop" : {"property": "loop", "default": false},
}
################################################################################
## EDITOR REPRESENTATION
################################################################################
func build_event_editor() -> void:
add_header_edit('file_path', ValueType.FILE,
{'left_text' : 'Play',
'file_filter' : '*.mp3, *.ogg, *.wav; Supported Audio Files',
'placeholder' : "Select file",
'editor_icon' : ["AudioStreamPlayer", "EditorIcons"]})
add_header_edit('file_path', ValueType.AUDIO_PREVIEW)
add_body_edit('volume', ValueType.NUMBER, {'left_text':'Volume:', 'mode':2}, '!file_path.is_empty()')
add_body_edit('audio_bus', ValueType.SINGLELINE_TEXT, {'left_text':'Audio Bus:'}, '!file_path.is_empty()')
func get_bus_suggestions() -> Dictionary:
var bus_name_list := {}
for i in range(AudioServer.bus_count):
bus_name_list[AudioServer.get_bus_name(i)] = {'value':AudioServer.get_bus_name(i)}
return bus_name_list

View file

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

View file

@ -3,7 +3,7 @@ extends DialogicIndexer
func _get_events() -> Array:
return [this_folder.path_join('event_music.gd'), this_folder.path_join('event_sound.gd')]
return [this_folder.path_join('event_audio.gd')]
func _get_subsystems() -> Array:

View file

@ -1 +1 @@
uid://c7po0y8sx23v4
uid://dk46l1toqeswc

View file

@ -3,16 +3,19 @@ extends DialogicSettingsPage
## Settings page that contains settings for the audio subsystem
const MUSIC_MAX_CHANNELS := "dialogic/audio/max_channels"
const TYPE_SOUND_AUDIO_BUS := "dialogic/audio/type_sound_bus"
const CHANNEL_DEFAULTS := "dialogic/audio/channel_defaults"
var channel_defaults := {}
var _revalidate_channel_names := false
func _ready() -> void:
%MusicChannelCount.value_changed.connect(_on_music_channel_count_value_changed)
%TypeSoundBus.item_selected.connect(_on_type_sound_bus_item_selected)
$Panel.add_theme_stylebox_override('panel', get_theme_stylebox("Background", "EditorStyles"))
func _refresh() -> void:
%MusicChannelCount.value = ProjectSettings.get_setting(MUSIC_MAX_CHANNELS, 4)
%TypeSoundBus.clear()
var idx := 0
for i in range(AudioServer.bus_count):
@ -21,12 +24,218 @@ func _refresh() -> void:
idx = i
%TypeSoundBus.select(idx)
func _on_music_channel_count_value_changed(value:float) -> void:
ProjectSettings.set_setting(MUSIC_MAX_CHANNELS, value)
ProjectSettings.save()
load_channel_defaults(DialogicUtil.get_audio_channel_defaults())
func _about_to_close() -> void:
save_channel_defaults()
## TYPE SOUND AUDIO BUS
func _on_type_sound_bus_item_selected(index:int) -> void:
ProjectSettings.set_setting(TYPE_SOUND_AUDIO_BUS, %TypeSoundBus.get_item_text(index))
ProjectSettings.save()
#region AUDIO CHANNELS
################################################################################
func load_channel_defaults(dictionary:Dictionary) -> void:
channel_defaults.clear()
for i in %AudioChannelDefaults.get_children():
i.queue_free()
var column_names := [
"Channel Name",
"Volume",
"Audio Bus",
"Fade",
"Loop",
""
]
for column in column_names:
var label := Label.new()
label.text = column
label.theme_type_variation = 'DialogicHintText2'
%AudioChannelDefaults.add_child(label)
var channel_names := dictionary.keys()
channel_names.sort()
for channel_name in channel_names:
add_channel_defaults(
channel_name,
dictionary[channel_name].volume,
dictionary[channel_name].audio_bus,
dictionary[channel_name].fade_length,
dictionary[channel_name].loop)
await get_tree().process_frame
_revalidate_channel_names = true
revalidate_channel_names.call_deferred()
func save_channel_defaults() -> void:
var dictionary := {}
for i in channel_defaults:
if is_instance_valid(channel_defaults[i].channel_name):
var channel_name := ""
if not channel_defaults[i].channel_name is Label:
if channel_defaults[i].channel_name.current_value.is_empty():
continue
channel_name = channel_defaults[i].channel_name.current_value
#channel_name = DialogicUtil.channel_name_regex.sub(channel_name, '', true)
if channel_name.is_empty():
dictionary[channel_name] = {
'volume': channel_defaults[i].volume.get_value(),
'audio_bus': channel_defaults[i].audio_bus.current_value,
'fade_length': 0.0,
'loop': false,
}
else:
dictionary[channel_name] = {
'volume': channel_defaults[i].volume.get_value(),
'audio_bus': channel_defaults[i].audio_bus.current_value,
'fade_length': channel_defaults[i].fade_length.get_value(),
'loop': channel_defaults[i].loop.button_pressed,
}
ProjectSettings.set_setting(CHANNEL_DEFAULTS, dictionary)
ProjectSettings.save()
func _on_add_channel_defaults_pressed() -> void:
var added_node := add_channel_defaults('new_channel_name', 0.0, '', 0.0, true)
if added_node:
added_node.take_autofocus()
_revalidate_channel_names = true
revalidate_channel_names.call_deferred()
func add_channel_defaults(channel_name: String, volume: float, audio_bus: String, fade_length: float, loop: bool) -> Control:
var info := {}
for i in %AudioChannelDefaultRow.get_children():
var x := i.duplicate()
%AudioChannelDefaults.add_child(x)
info[i.name] = x
if channel_name.is_empty():
var channel_label := Label.new()
channel_label.text = &"One-Shot SFX"
channel_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
%AudioChannelDefaults.add_child(channel_label)
%AudioChannelDefaults.move_child(channel_label, info.channel_name.get_index())
info.channel_name.queue_free()
info.channel_name = channel_label
var HintTooltip := preload("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn")
var fade_hint := HintTooltip.instantiate()
fade_hint.hint_text = "Fading is disabled for this channel."
%AudioChannelDefaults.add_child(fade_hint)
%AudioChannelDefaults.move_child(fade_hint, info.fade_length.get_index())
info.fade_length.queue_free()
info.fade_length = fade_hint
var loop_hint := HintTooltip.instantiate()
loop_hint.hint_text = "Looping is disabled for this channel."
%AudioChannelDefaults.add_child(loop_hint)
%AudioChannelDefaults.move_child(loop_hint, info.loop.get_index())
info.loop.queue_free()
info.loop = loop_hint
info.delete.disabled = true
else:
info.channel_name.suggestions_func = get_audio_channel_suggestions
info.channel_name.validation_func = validate_channel_names.bind(info.channel_name)
info.channel_name.set_value(channel_name)
info.fade_length.set_value(fade_length)
info.loop.set_pressed_no_signal(loop)
info.audio_bus.suggestions_func = DialogicUtil.get_audio_bus_suggestions
info.audio_bus.set_value(audio_bus)
info.delete.icon = get_theme_icon(&"Remove", &"EditorIcons")
channel_defaults[len(channel_defaults)] = info
return info['channel_name']
func _on_remove_channel_defaults_pressed(index: int) -> void:
for key in channel_defaults[index]:
channel_defaults[index][key].queue_free()
channel_defaults.erase(index)
func get_audio_channel_suggestions(search_text:String) -> Dictionary:
var suggestions := DialogicUtil.get_audio_channel_suggestions(search_text)
for i in channel_defaults.values():
if i.channel_name is DialogicVisualEditorField:
suggestions.erase(i.channel_name.current_value)
for key in suggestions.keys():
suggestions[key].erase('tooltip')
suggestions[key]['editor_icon'] = ["AudioStreamPlayer", "EditorIcons"]
return suggestions
func revalidate_channel_names() -> void:
_revalidate_channel_names = false
for i in channel_defaults:
if (is_instance_valid(channel_defaults[i].channel_name)
and not channel_defaults[i].channel_name is Label):
channel_defaults[i].channel_name.validate()
func validate_channel_names(search_text: String, field_node: Control) -> Dictionary:
var channel_cache = {}
var result := {}
var tooltips := []
if search_text.is_empty():
result['error_tooltip'] = 'Must not be empty.'
return result
if field_node:
channel_cache[search_text] = [field_node]
if field_node.current_value != search_text:
_revalidate_channel_names = true
revalidate_channel_names.call_deferred()
# Collect all channel names entered
for i in channel_defaults:
if (is_instance_valid(channel_defaults[i].channel_name)
and not channel_defaults[i].channel_name is Label
and channel_defaults[i].channel_name != field_node):
var text := channel_defaults[i].channel_name.current_value as String
if not channel_cache.has(text):
channel_cache[text] = []
channel_cache[text].append(channel_defaults[i].channel_name)
# Check for duplicate names
if channel_cache.has(search_text) and channel_cache[search_text].size() > 1:
tooltips.append("Duplicate channel name.")
# Check for invalid characters
result = DialogicUtil.validate_audio_channel_name(search_text)
if result:
tooltips.append(result.error_tooltip)
result.error_tooltip = "\n".join(tooltips)
elif not tooltips.is_empty():
result['error_tooltip'] = "\n".join(tooltips)
return result
#endregion

View file

@ -1 +1 @@
uid://1w40lwv540il
uid://cqyhm6offcitc

View file

@ -1,36 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://c2qgetjc3mfo3"]
[gd_scene load_steps=6 format=3 uid="uid://c2qgetjc3mfo3"]
[ext_resource type="Script" uid="uid://1w40lwv540il" path="res://addons/dialogic/Modules/Audio/settings_audio.gd" id="1_2iyyr"]
[ext_resource type="Script" uid="uid://cqyhm6offcitc" path="res://addons/dialogic/Modules/Audio/settings_audio.gd" id="1_2iyyr"]
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_o1ban"]
[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3_bx557"]
[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="4_xfyvc"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m57ns"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(1, 0.365, 0.365, 1)
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
corner_detail = 1
[node name="Audio" type="VBoxContainer"]
offset_right = 121.0
offset_bottom = 58.0
script = ExtResource("1_2iyyr")
[node name="Label" type="Label" parent="."]
layout_mode = 2
theme_type_variation = &"DialogicSettingsSection"
text = "Music Channels"
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer"]
layout_mode = 2
text = "Max music channels"
[node name="HintTooltip" parent="HBoxContainer" instance=ExtResource("2_o1ban")]
layout_mode = 2
texture = null
hint_text = "Lowering this value may invalidate existing music events!"
[node name="MusicChannelCount" type="SpinBox" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
min_value = 1.0
value = 1.0
[node name="TypingSoundsTitle" type="Label" parent="."]
layout_mode = 2
theme_type_variation = &"DialogicSettingsSection"
@ -45,10 +37,77 @@ text = "Audio Bus"
[node name="HintTooltip" parent="HBoxContainer2" instance=ExtResource("2_o1ban")]
layout_mode = 2
tooltip_text = "Lowering this value may invalidate existing music events!"
texture = null
hint_text = "The default audio bus used by TypeSound nodes."
[node name="TypeSoundBus" type="OptionButton" parent="HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
[node name="HBoxContainer3" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer3"]
layout_mode = 2
theme_type_variation = &"DialogicSettingsSection"
text = "Audio Channel Defaults"
[node name="HintTooltip" parent="HBoxContainer3" instance=ExtResource("2_o1ban")]
layout_mode = 2
texture = null
hint_text = "Default settings for named audio channels."
[node name="Panel" type="PanelContainer" parent="."]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_m57ns")
[node name="VBox" type="VBoxContainer" parent="Panel"]
layout_mode = 2
[node name="AudioChannelDefaults" type="GridContainer" parent="Panel/VBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
columns = 6
[node name="AudioChannelDefaultRow" type="HBoxContainer" parent="Panel/VBox"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="channel_name" parent="Panel/VBox/AudioChannelDefaultRow" instance=ExtResource("3_bx557")]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Enter Channel Name"
mode = 3
[node name="volume" parent="Panel/VBox/AudioChannelDefaultRow" instance=ExtResource("4_xfyvc")]
layout_mode = 2
mode = 2
min = -80.0
max = 6.0
suffix = "dB"
[node name="audio_bus" parent="Panel/VBox/AudioChannelDefaultRow" instance=ExtResource("3_bx557")]
layout_mode = 2
placeholder_text = "Master"
mode = 2
[node name="fade_length" parent="Panel/VBox/AudioChannelDefaultRow" instance=ExtResource("4_xfyvc")]
layout_mode = 2
mode = 0
enforce_step = false
min = 0.0
[node name="loop" type="CheckButton" parent="Panel/VBox/AudioChannelDefaultRow"]
layout_mode = 2
[node name="delete" type="Button" parent="Panel/VBox/AudioChannelDefaultRow"]
layout_mode = 2
[node name="Add" type="Button" parent="Panel/VBox"]
layout_mode = 2
size_flags_vertical = 4
text = "Add channel"
[connection signal="pressed" from="Panel/VBox/Add" to="." method="_on_add_channel_defaults_pressed"]

View file

@ -1,101 +1,77 @@
extends DialogicSubsystem
## Subsystem for managing background music and one-shot sound effects.
## Subsystem for managing background audio and one-shot sound effects.
##
## This subsystem has many different helper methods for managing audio
## in your timeline.
## For instance, you can listen to music changes via [signal music_started].
## For instance, you can listen to audio changes via [signal audio_started].
## Whenever a new background music is started, this signal is emitted and
## Whenever a new audio event is started, this signal is emitted and
## contains a dictionary with the following keys: [br]
## [br]
## Key | Value Type | Value [br]
## ----------- | ------------- | ----- [br]
## `path` | [type String] | The path to the audio resource file. [br]
## `volume` | [type float] | The volume of the audio resource that will be set to the [member base_music_player]. [br]
## `audio_bus` | [type String] | The audio bus name that the [member base_music_player] will use. [br]
## `channel` | [type String] | The channel name to play the audio on. [br]
## `volume` | [type float] | The volume in `db` of the audio resource that will be set to the [AudioStreamPlayer]. [br]
## `audio_bus` | [type String] | The audio bus name that the [AudioStreamPlayer] will use. [br]
## `loop` | [type bool] | Whether the audio resource will loop or not once it finishes playing. [br]
## `channel` | [type int] | The channel ID to play the audio on. [br]
signal music_started(info: Dictionary)
signal audio_started(info: Dictionary)
## Whenever a new sound effect is set, this signal is emitted and contains a
## dictionary with the following keys: [br]
## [br]
## Key | Value Type | Value [br]
## ----------- | ------------- | ----- [br]
## `path` | [type String] | The path to the audio resource file. [br]
## `volume` | [type float] | The volume of the audio resource that will be set to [member base_sound_player]. [br]
## `audio_bus` | [type String] | The audio bus name that the [member base_sound_player] will use. [br]
## `loop` | [type bool] | Whether the audio resource will loop or not once it finishes playing. [br]
signal sound_started(info: Dictionary)
var max_channels: int:
set(value):
if max_channels != value:
max_channels = value
ProjectSettings.set_setting('dialogic/audio/max_channels', value)
ProjectSettings.save()
current_music_player.resize(value)
get:
return ProjectSettings.get_setting('dialogic/audio/max_channels', 4)
## Audio player base duplicated to play background music.
##
## Background music is long audio.
var base_music_player := AudioStreamPlayer.new()
## Reference to the last used music player.
var current_music_player: Array[AudioStreamPlayer] = []
## Audio player base, that will be duplicated to play sound effects.
##
## Sound effects are short audio.
var base_sound_player := AudioStreamPlayer.new()
## Audio node for holding audio players
var audio_node := Node.new()
## Sound node for holding sound players
var one_shot_audio_node := Node.new()
## Dictionary with info of all current audio channels
var current_audio_channels: Dictionary = {}
#region STATE
####################################################################################################
## Clears the state on this subsystem and stops all audio.
##
## If you want to stop sounds only, use [method stop_all_sounds].
func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
for idx in max_channels:
update_music('', 0.0, '', 0.0, true, idx)
stop_all_sounds()
stop_all_channels()
stop_all_one_shot_sounds()
## Loads the state on this subsystem from the current state info.
func load_game_state(load_flag:=LoadFlags.FULL_LOAD) -> void:
if load_flag == LoadFlags.ONLY_DNODES:
return
var info: Dictionary = dialogic.current_state_info.get("music", {})
if not info.is_empty() and info.has('path'):
update_music(info.path, info.volume, info.audio_bus, 0, info.loop, 0)
else:
for channel_id in info.keys():
if info[channel_id].is_empty() or info[channel_id].path.is_empty():
update_music('', 0.0, '', 0.0, true, channel_id)
else:
update_music(info[channel_id].path, info[channel_id].volume, info[channel_id].audio_bus, 0, info[channel_id].loop, channel_id)
# Pre Alpha 17 Converter
_convert_state_info()
var info: Dictionary = dialogic.current_state_info.get("audio", {})
for channel_name in info.keys():
if info[channel_name].path.is_empty():
update_audio(channel_name)
else:
update_audio(channel_name, info[channel_name].path, info[channel_name].settings_overrides)
## Pauses playing audio.
func pause() -> void:
for child in get_children():
for child in audio_node.get_children():
child.stream_paused = true
for child in one_shot_audio_node.get_children():
child.stream_paused = true
## Resumes playing audio.
func resume() -> void:
for child in get_children():
for child in audio_node.get_children():
child.stream_paused = false
for child in one_shot_audio_node.get_children():
child.stream_paused = false
func _on_dialogic_timeline_ended() -> void:
if not dialogic.Styles.get_layout_node():
clear_game_state()
pass
#endregion
@ -105,115 +81,204 @@ func _on_dialogic_timeline_ended() -> void:
func _ready() -> void:
dialogic.timeline_ended.connect(_on_dialogic_timeline_ended)
base_music_player.name = "Music"
add_child(base_music_player)
base_sound_player.name = "Sound"
add_child(base_sound_player)
current_music_player.resize(max_channels)
audio_node.name = "Audio"
add_child(audio_node)
one_shot_audio_node.name = "OneShotAudios"
add_child(one_shot_audio_node)
## Updates the background music. Will fade out previous music.
func update_music(path := "", volume := 0.0, audio_bus := "", fade_time := 0.0, loop := true, channel_id := 0) -> void:
if channel_id > max_channels:
printerr("\tChannel ID (%s) higher than Max Music Channels (%s)" % [channel_id, max_channels])
dialogic.print_debug_moment()
## Plays the given file (or nothing) on the given channel.
## No channel given defaults to the "One-Shot SFX" channel,
## which does not save audio but can have multiple audios playing simultaneously.
func update_audio(channel_name:= "", path := "", settings_overrides := {}) -> void:
#volume := 0.0, audio_bus := "", fade_time := 0.0, loop := true, sync_channel := "") -> void:
if not is_channel_playing(channel_name) and path.is_empty():
return
if not dialogic.current_state_info.has('music'):
dialogic.current_state_info['music'] = {}
## Determine audio settings
## TODO use .merged after dropping 4.2 support
var audio_settings: Dictionary = DialogicUtil.get_audio_channel_defaults().get(channel_name, {})
audio_settings.merge(
{"volume":0, "audio_bus":"", "fade_length":0.0, "loop":true, "sync_channel":""}
)
audio_settings.merge(settings_overrides, true)
dialogic.current_state_info['music'][channel_id] = {'path':path, 'volume':volume, 'audio_bus':audio_bus, 'loop':loop, 'channel':channel_id}
music_started.emit(dialogic.current_state_info['music'][channel_id])
## Handle previous audio on channel
if is_channel_playing(channel_name):
var prev_audio_node: AudioStreamPlayer = current_audio_channels[channel_name]
prev_audio_node.name += "_Prev"
if audio_settings.fade_length > 0.0:
var fade_out_tween: Tween = create_tween()
fade_out_tween.tween_method(
interpolate_volume_linearly.bind(prev_audio_node),
db_to_linear(prev_audio_node.volume_db),
0.0,
audio_settings.fade_length)
fade_out_tween.tween_callback(prev_audio_node.queue_free)
var fader: Tween = null
if is_instance_valid(current_music_player[channel_id]) and current_music_player[channel_id].playing or !path.is_empty():
fader = create_tween()
else:
prev_audio_node.queue_free()
var prev_node: Node = null
if is_instance_valid(current_music_player[channel_id]) and current_music_player[channel_id].playing:
prev_node = current_music_player[channel_id]
fader.tween_method(interpolate_volume_linearly.bind(prev_node), db_to_linear(prev_node.volume_db),0.0,fade_time)
## Set state
if not dialogic.current_state_info.has('audio'):
dialogic.current_state_info['audio'] = {}
if path:
current_music_player[channel_id] = base_music_player.duplicate()
add_child(current_music_player[channel_id])
current_music_player[channel_id].stream = load(path)
current_music_player[channel_id].volume_db = volume
if audio_bus:
current_music_player[channel_id].bus = audio_bus
if not current_music_player[channel_id].stream is AudioStreamWAV:
if "loop" in current_music_player[channel_id].stream:
current_music_player[channel_id].stream.loop = loop
elif "loop_mode" in current_music_player[channel_id].stream:
if loop:
current_music_player[channel_id].stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
else:
current_music_player[channel_id].stream.loop_mode = AudioStreamWAV.LOOP_DISABLED
if not path:
dialogic.current_state_info['audio'].erase(channel_name)
return
current_music_player[channel_id].play(0)
fader.parallel().tween_method(interpolate_volume_linearly.bind(current_music_player[channel_id]), 0.0, db_to_linear(volume),fade_time)
dialogic.current_state_info['audio'][channel_name] = {'path':path, 'settings_overrides':settings_overrides}
audio_started.emit(dialogic.current_state_info['audio'][channel_name])
if prev_node:
fader.tween_callback(prev_node.queue_free)
var new_player := AudioStreamPlayer.new()
if channel_name:
new_player.name = channel_name.validate_node_name()
audio_node.add_child(new_player)
else:
new_player.name = "OneShotSFX"
one_shot_audio_node.add_child(new_player)
var file := load(path)
if file == null:
printerr("[Dialogic] Audio file \"%s\" failed to load." % path)
return
new_player.stream = load(path)
## Apply audio settings
## Volume & Fade
if audio_settings.fade_length > 0.0:
new_player.volume_db = linear_to_db(0.0)
var fade_in_tween := create_tween()
fade_in_tween.tween_method(
interpolate_volume_linearly.bind(new_player),
0.0,
db_to_linear(audio_settings.volume),
audio_settings.fade_length)
else:
new_player.volume_db = audio_settings.volume
## Audio Bus
new_player.bus = audio_settings.audio_bus
## Loop
if "loop" in new_player.stream:
new_player.stream.loop = audio_settings.loop
elif "loop_mode" in new_player.stream:
if audio_settings.loop:
new_player.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
new_player.stream.loop_begin = 0
new_player.stream.loop_end = new_player.stream.mix_rate * new_player.stream.get_length()
else:
new_player.stream.loop_mode = AudioStreamWAV.LOOP_DISABLED
## Sync & start player
if audio_settings.sync_channel and is_channel_playing(audio_settings.sync_channel):
var play_position: float = current_audio_channels[audio_settings.sync_channel].get_playback_position()
new_player.play(play_position)
# TODO Remove this once https://github.com/godotengine/godot/issues/18878 is fixed
if new_player.stream is AudioStreamWAV and new_player.stream.format == AudioStreamWAV.FORMAT_IMA_ADPCM:
printerr("[Dialogic] WAV files using Ima-ADPCM compression cannot be synced. Reimport the file using a different compression mode.")
dialogic.print_debug_moment()
else:
new_player.play()
new_player.finished.connect(_on_audio_finished.bind(new_player, channel_name, path))
if channel_name:
current_audio_channels[channel_name] = new_player
## Whether music is playing.
func has_music(channel_id := 0) -> bool:
return !dialogic.current_state_info.get('music', {}).get(channel_id, {}).get('path', '').is_empty()
## Returns `true` if any audio is playing on the given [param channel_name].
func is_channel_playing(channel_name: String) -> bool:
return (current_audio_channels.has(channel_name)
and is_instance_valid(current_audio_channels[channel_name])
and current_audio_channels[channel_name].is_playing())
## Plays a given sound file.
func play_sound(path: String, volume := 0.0, audio_bus := "", loop := false) -> void:
if base_sound_player != null and !path.is_empty():
sound_started.emit({'path':path, 'volume':volume, 'audio_bus':audio_bus, 'loop':loop})
var new_sound_node := base_sound_player.duplicate()
new_sound_node.name += "Sound"
new_sound_node.stream = load(path)
if "loop" in new_sound_node.stream:
new_sound_node.stream.loop = loop
elif "loop_mode" in new_sound_node.stream:
if loop:
new_sound_node.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
else:
new_sound_node.stream.loop_mode = AudioStreamWAV.LOOP_DISABLED
new_sound_node.volume_db = volume
if audio_bus:
new_sound_node.bus = audio_bus
add_child(new_sound_node)
new_sound_node.play()
new_sound_node.finished.connect(new_sound_node.queue_free)
## Stops audio on all channels.
func stop_all_channels(fade := 0.0) -> void:
for channel_name in current_audio_channels.keys():
update_audio(channel_name, '', {"fade_length":fade})
## Stops all audio.
func stop_all_sounds() -> void:
for node in get_children():
if node == base_sound_player:
continue
if "Sound" in node.name:
node.queue_free()
### Stops all one-shot sounds.
func stop_all_one_shot_sounds() -> void:
for i in one_shot_audio_node.get_children():
i.queue_free()
## Converts a linear loudness value to decibel and sets that volume to
## the given [param node].
func interpolate_volume_linearly(value: float, node: Node) -> void:
func interpolate_volume_linearly(value: float, node: AudioStreamPlayer) -> void:
node.volume_db = linear_to_db(value)
## Returns whether the currently playing audio resource is the same as this
## event's [param resource_path], for [param channel_id].
func is_music_playing_resource(resource_path: String, channel_id := 0) -> bool:
var is_playing_resource: bool = (current_music_player.size() > channel_id
and is_instance_valid(current_music_player[channel_id])
and current_music_player[channel_id].is_playing()
and current_music_player[channel_id].stream.resource_path == resource_path)
## event's [param resource_path], for [param channel_name].
func is_channel_playing_file(file_path: String, channel_name: String) -> bool:
return (is_channel_playing(channel_name)
and current_audio_channels[channel_name].stream.resource_path == file_path)
return is_playing_resource
## Returns `true` if any channel is playing.
func is_any_channel_playing() -> bool:
for channel in current_audio_channels:
if is_channel_playing(channel):
return true
return false
func _on_audio_finished(player: AudioStreamPlayer, channel_name: String, path: String) -> void:
if current_audio_channels.has(channel_name) and current_audio_channels[channel_name] == player:
current_audio_channels.erase(channel_name)
player.queue_free()
if dialogic.current_state_info.get('audio', {}).get(channel_name, {}).get('path', '') == path:
dialogic.current_state_info['audio'].erase(channel_name)
#endregion
#region Pre Alpha 17 Conversion
func _convert_state_info() -> void:
var info: Dictionary = dialogic.current_state_info.get("music", {})
if info.is_empty():
return
var new_info := {}
if info.has("path"):
# Pre Alpha 16 Save Data Conversion
new_info['music'] = {
"path":info.path,
"settings_overrides": {
"volume":info.volume,
"audio_bus":info.audio_bus,
"loop":info.loop}
}
else:
# Pre Alpha 17 Save Data Conversion
for channel_id in info.keys():
if info[channel_id].is_empty():
continue
var channel_name = "music"
if channel_id > 0:
channel_name += str(channel_id + 1)
new_info[channel_name] = {
"path": info[channel_id].path,
"settings_overrides":{
'volume': info[channel_id].volume,
'audio_bus': info[channel_id].audio_bus,
'loop': info[channel_id].loop,
}
}
dialogic.current_state_info['audio'] = new_info
dialogic.current_state_info.erase('music')
#endregion

View file

@ -1 +1 @@
uid://7pht2qiwn6xf
uid://do8vgqtp35d6w

View file

@ -19,6 +19,10 @@ func _update_background(argument:String, _time:float) -> void:
if argument.begins_with('res://'):
image_node.texture = load(argument)
color_node.color = Color.TRANSPARENT
elif argument.begins_with('user://'):
var ext_image = Image.load_from_file(argument)
image_node.texture = ImageTexture.create_from_image(ext_image)
color_node.color = Color.TRANSPARENT
elif argument.is_valid_html_color():
image_node.texture = null
color_node.color = Color(argument, 1)

View file

@ -1 +1 @@
uid://xwj105ltniqb
uid://ci7s5odxo7543

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cl6g6ymkhjven"]
[ext_resource type="Script" uid="uid://xwj105ltniqb" path="res://addons/dialogic/Modules/Background/DefaultBackgroundScene/default_background.gd" id="1_nkdrp"]
[ext_resource type="Script" uid="uid://ci7s5odxo7543" path="res://addons/dialogic/Modules/Background/DefaultBackgroundScene/default_background.gd" id="1_nkdrp"]
[node name="DefaultBackground" type="Control"]
layout_mode = 3

View file

@ -1 +1 @@
uid://dhqujsj8oqa7p
uid://blaaa6obvwknl

View file

@ -1 +1 @@
uid://020a4llqpjlv
uid://6f7qewx7aga

View file

@ -1 +1 @@
uid://b6f6s6rsoy2bw
uid://m3anyujei6ro

View file

@ -1 +1 @@
uid://y8jr5c8l3e0a
uid://dnuvmtb036bi3

View file

@ -1 +1 @@
uid://bjqdjfxjcvj7s
uid://bed16hbuh4atn

View file

@ -1 +1 @@
uid://d2ghxhx2432ro
uid://ctoc2p12vahcc

View file

@ -1 +1 @@
uid://c3bd283fo14yy
uid://dknape5pbyevn

View file

@ -1 +1 @@
uid://c63rtrqaasjey
uid://dwhod30peco4c

View file

@ -22,6 +22,7 @@ var time: float
var bg_holder: DialogicNode_BackgroundHolder
@warning_ignore("unused_signal") # Used by scripts inheriting this class
signal transition_finished

View file

@ -1 +1 @@
uid://cn1cj14nuhrk6
uid://cf47aj5eivati

View file

@ -1 +1 @@
uid://b2ybl1gbf0jej
uid://clabj6a02r7iv

View file

@ -1 +1 @@
uid://b7oxjroxkqi5q
uid://cuj1xsi7d7r5y

View file

@ -1 +1 @@
uid://5kc4er8aj0s3
uid://bue1pfm6eu7ww

View file

@ -1 +1 @@
uid://bd73gvgsuaxay
uid://bkj1kaqcq5208

View file

@ -35,3 +35,4 @@ func _custom_fade_in(_time:float) -> bool:
## If you return false (by default) it will attempt to animate the "modulate" property.
func _custom_fade_out(_time:float) -> bool:
return false

View file

@ -1 +1 @@
uid://5rp6alg02rv0
uid://blsjcvm6gvd78

View file

@ -13,7 +13,11 @@ extends DialogicEvent
var scene := ""
## The argument that is passed to the background scene.
## For the default scene it's the path to the image to show.
var argument := ""
var argument := "":
set(value):
if argument != value:
argument = value
ui_update_needed.emit()
## The time the fade animation will take. Leave at 0 for instant change.
var fade: float = 0.0
## Name of the transition to use.
@ -82,8 +86,8 @@ func get_shortcode() -> String:
func get_shortcode_parameters() -> Dictionary:
return {
#param_name : property_info
"scene" : {"property": "scene", "default": ""},
"arg" : {"property": "argument", "default": ""},
"scene" : {"property": "scene", "default": "", "ext_file":true},
"arg" : {"property": "argument", "default": "", "ext_file":true},
"fade" : {"property": "fade", "default": 0},
"transition" : {"property": "transition", "default": "",
"suggestions": get_transition_suggestions},
@ -138,6 +142,10 @@ func build_event_editor() -> void:
'_arg_type == ArgumentTypes.IMAGE or _scene_type == SceneTypes.DEFAULT')
add_header_edit('argument', ValueType.SINGLELINE_TEXT, {}, '_arg_type == ArgumentTypes.CUSTOM')
add_body_edit("argument", ValueType.IMAGE_PREVIEW, {'left_text':'Preview:'},
'(_arg_type == ArgumentTypes.IMAGE or _scene_type == SceneTypes.DEFAULT) and !argument.is_empty()')
add_body_line_break('(_arg_type == ArgumentTypes.IMAGE or _scene_type == SceneTypes.DEFAULT) and !argument.is_empty()')
add_body_edit("transition", ValueType.DYNAMIC_OPTIONS,
{'left_text':'Transition:',
'empty_text':'Simple Fade',

View file

@ -1 +1 @@
uid://ccksowqutf0np
uid://sioj2uwexnwx

View file

@ -1 +1 @@
uid://cpl0roawa546c
uid://bj085abnvwkyh

View file

@ -1 +1 @@
uid://bisx1nk1bjyjc
uid://oxcjhq2817c7

View file

@ -99,6 +99,12 @@ func update_background(scene := "", argument := "", fade_time := 0.0, transition
else:
new_viewport = null
# if there is still a transition going on, stop it now
for node in get_children():
if node is DialogicBackgroundTransition:
node.queue_free()
var trans_script: Script = load(DialogicResourceUtil.guess_special_resource("BackgroundTransition", transition_path, {"path":default_transition}).path)
var trans_node := Node.new()
trans_node.set_script(trans_script)
@ -107,6 +113,7 @@ func update_background(scene := "", argument := "", fade_time := 0.0, transition
trans_node.time = fade_time
if old_viewport:
old_viewport.name = "OldBackground"
trans_node.prev_scene = old_viewport.get_meta('node', null)
trans_node.prev_texture = old_viewport.get_child(0).get_texture()
old_viewport.get_meta('node')._custom_fade_out(fade_time)
@ -115,6 +122,7 @@ func update_background(scene := "", argument := "", fade_time := 0.0, transition
old_viewport.get_child(0).render_target_update_mode = SubViewport.UPDATE_ALWAYS
trans_node.transition_finished.connect(old_viewport.queue_free)
if new_viewport:
new_viewport.name = "NewBackground"
trans_node.next_scene = new_viewport.get_meta('node', null)
trans_node.next_texture = new_viewport.get_child(0).get_texture()
new_viewport.get_meta('node')._update_background(argument, fade_time)

View file

@ -1 +1 @@
uid://b41q2p73ce000
uid://5uwbnllu1kfv

View file

@ -139,53 +139,17 @@ func get_shortcode_parameters() -> Dictionary:
func build_event_editor() -> void:
add_header_edit('autoload_name', ValueType.DYNAMIC_OPTIONS, {'left_text':'On autoload',
'empty_text':'Autoload',
'suggestions_func':get_autoload_suggestions,
'suggestions_func': DialogicUtil.get_autoload_suggestions,
'editor_icon':["Node", "EditorIcons"]})
add_header_edit('method', ValueType.DYNAMIC_OPTIONS, {'left_text':'call',
'empty_text':'Method',
'suggestions_func':get_method_suggestions,
'suggestions_func': get_method_suggestions,
'editor_icon':["Callable", "EditorIcons"]}, 'autoload_name')
add_body_edit('arguments', ValueType.ARRAY, {'left_text':'Arguments:'}, 'not autoload_name.is_empty() and not method.is_empty()')
func get_autoload_suggestions(filter:String="") -> Dictionary:
var suggestions := {}
for prop in ProjectSettings.get_property_list():
if prop.name.begins_with('autoload/'):
var autoload: String = prop.name.trim_prefix('autoload/')
suggestions[autoload] = {'value': autoload, 'tooltip':autoload, 'editor_icon': ["Node", "EditorIcons"]}
if filter.begins_with(autoload):
suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
return suggestions
func get_method_suggestions(filter:String="", temp_autoload:String = "") -> Dictionary:
var suggestions := {}
var script: Script
if temp_autoload and ProjectSettings.has_setting('autoload/'+temp_autoload):
script = load(ProjectSettings.get_setting('autoload/'+temp_autoload).trim_prefix('*'))
elif autoload_name and ProjectSettings.has_setting('autoload/'+autoload_name):
var loaded_autoload := load(ProjectSettings.get_setting('autoload/'+autoload_name).trim_prefix('*'))
if loaded_autoload is PackedScene:
var packed_scene: PackedScene = loaded_autoload
script = packed_scene.instantiate().get_script()
else:
script = loaded_autoload
if script:
for script_method in script.get_script_method_list():
if script_method.name.begins_with('@') or script_method.name.begins_with('_'):
continue
suggestions[script_method.name] = {'value': script_method.name, 'tooltip':script_method.name, 'editor_icon': ["Callable", "EditorIcons"]}
if !filter.is_empty():
suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
return suggestions
func get_method_suggestions(filter:="") -> Dictionary:
return DialogicUtil.get_autoload_method_suggestions(filter, autoload_name)
func update_argument_info() -> void:
@ -236,13 +200,21 @@ func check_arguments_and_update_warning() -> void:
####################### CODE COMPLETION ########################################
################################################################################
func _get_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
var autoloads := DialogicUtil.get_autoload_suggestions()
var line_until_caret: String = CodeCompletionHelper.get_line_untill_caret(line)
if line.count(' ') == 1 and not '.' in line:
for i in get_autoload_suggestions():
for i in autoloads:
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+'.', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3), TextNode.get_theme_icon("Node", "EditorIcons"))
elif symbol == '.' and not '(' in line:
for i in get_method_suggestions('', line.get_slice('.', 0).trim_prefix('do ')):
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+'(', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3), TextNode.get_theme_icon("Callable", "EditorIcons"))
elif (line_until_caret.ends_with(".") or symbol == "."):
var some_autoload := line_until_caret.split(" ")[-1].split(".")[0]
if some_autoload in autoloads:
var methods := DialogicUtil.get_autoload_method_suggestions("", some_autoload)
for i in methods.keys():
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+'(', event_color.lerp(TextNode.syntax_highlighter.normal_color, 0.3), TextNode.get_theme_icon("MemberMethod", "EditorIcons"))
func _get_start_code_completion(_CodeCompletionHelper:Node, TextNode:TextEdit) -> void:

View file

@ -1 +1 @@
uid://cx3saaa1iyvre
uid://uhicnbvlk57s

View file

@ -1 +1 @@
uid://huqsmyyipv62
uid://bthb47untmgoo

View file

@ -1 +1 @@
uid://fgk8hal7nugw
uid://qruxugkg6y8w

View file

@ -1 +1 @@
uid://cb4xhtqnifsrf
uid://rfgxn0xtuen3

View file

@ -1 +1 @@
uid://ckqo0ncifb40s
uid://bcs0jdci4mngb

View file

@ -1 +1 @@
uid://cobrccabo638p
uid://fekbbs23rj4m

View file

@ -1 +1 @@
uid://df6ex8wqhaxix
uid://dwnfbyjtc2anb

View file

@ -1 +1 @@
uid://dttqyslijg03r
uid://8ro2ayitmjcp

View file

@ -1 +1 @@
uid://clwy8nsaw1phe
uid://cn4yveni7rdr7

View file

@ -11,7 +11,7 @@ func animate() -> void:
tween.tween_method(bound_multitween, Vector2(), Vector2(-1,0)*strength, time*0.1)
tween.tween_method(bound_multitween, Vector2(), Vector2(1, 0)*strength, time*0.1)
tween.tween_method(bound_multitween, Vector2(), Vector2(-1,0)*strength, time*0.1)
tween.tween_method(bound_multitween, Vector2(), Vector2(1, 0)*strength, time*0.2)
tween.tween_method(bound_multitween, Vector2(), Vector2(0, 0)*strength, time*0.2)
tween.finished.connect(emit_signal.bind('finished_once'))
func _get_named_variations() -> Dictionary:

View file

@ -1 +1 @@
uid://jp8nhoprsqdh
uid://3tqien23j50t

View file

@ -1 +1 @@
uid://j0si5xem8teo
uid://lur75holg34f

View file

@ -4,16 +4,17 @@ func animate() -> void:
var tween := (node.create_tween() as Tween)
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
var target_position := base_position.y
var start_position: float = -node.get_viewport().size.y
var end_position_y: float = base_position.y + node.get_parent().global_position.y
var start_position: float = -get_node_size().y + get_node_origin().y
if is_reversed:
target_position = -node.get_viewport().size.y
tween.set_ease(Tween.EASE_IN)
end_position_y = -get_node_size().y + get_node_origin().y
start_position = base_position.y
node.position.y = start_position
tween.tween_property(node, 'position:y', target_position, time)
tween.tween_property(node, 'global_position:y', end_position_y, time)
await tween.finished
finished_once.emit()

View file

@ -1 +1 @@
uid://b3cvsvayqobsk
uid://d0a5sgbr5imas

View file

@ -5,15 +5,16 @@ func animate() -> void:
var tween := (node.create_tween() as Tween)
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
var end_position_x: float = base_position.x
var end_position_x: float = base_position.x + node.get_parent().global_position.x
if is_reversed:
end_position_x = -node.get_viewport().size.x / 2
end_position_x = - get_node_size().x + get_node_origin().x
tween.set_ease(Tween.EASE_IN)
else:
node.position.x = -node.get_viewport().size.x / 5
node.global_position.x = -get_node_size().x + get_node_origin().x
tween.tween_property(node, 'position:x', end_position_x, time)
tween.tween_property(node, 'global_position:x', end_position_x, time)
await tween.finished
finished_once.emit()
@ -21,6 +22,6 @@ func animate() -> void:
func _get_named_variations() -> Dictionary:
return {
"slide in left": {"reversed": false, "type": AnimationType.IN},
"slide out right": {"reversed": true, "type": AnimationType.OUT},
"slide from left": {"reversed": false, "type": AnimationType.IN},
"slide to left": {"reversed": true, "type": AnimationType.OUT},
}

View file

@ -1 +1 @@
uid://bs763k4612fht
uid://c8il877nw3xqw

View file

@ -4,24 +4,21 @@ func animate() -> void:
var tween := (node.create_tween() as Tween)
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
var viewport_x: float = node.get_viewport().size.x
var start_position_x: float = viewport_x + viewport_x / 5
var end_position_x := base_position.x
var viewport_x: float = get_viewport_size().x
var end_position_x : float = base_position.x + node.get_parent().global_position.x
if is_reversed:
start_position_x = base_position.x
end_position_x = viewport_x + node.get_viewport().size.x / 5
node.position.x = start_position_x
tween.tween_property(node, 'position:x', end_position_x, time)
end_position_x = viewport_x + get_node_origin().x
tween.set_ease(Tween.EASE_IN)
else:
node.global_position.x = viewport_x + get_node_origin().x
tween.tween_property(node, 'global_position:x', end_position_x, time)
tween.finished.connect(emit_signal.bind('finished_once'))
func _get_named_variations() -> Dictionary:
return {
"slide in right": {"reversed": false, "type": AnimationType.IN},
"slide out left": {"reversed": true, "type": AnimationType.OUT},
"slide from right": {"reversed": false, "type": AnimationType.IN},
"slide to right": {"reversed": true, "type": AnimationType.OUT},
}

View file

@ -1 +1 @@
uid://cswjq5jlqrn3g
uid://daj7cqft5hnxg

View file

@ -4,15 +4,16 @@ func animate() -> void:
var tween := (node.create_tween() as Tween)
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
var start_position_y: float = node.get_viewport().size.y * 2
var end_position_y := base_position.y
var start_position_y: float = get_viewport_size().y + get_node_origin().y
var end_position_y: float = base_position.y + node.get_parent().global_position.y
if is_reversed:
start_position_y = base_position.y
end_position_y = node.get_viewport().size.y * 2
tween.set_ease(Tween.EASE_IN)
start_position_y = end_position_y
end_position_y = get_viewport_size().y + get_node_origin().y
node.position.y = start_position_y
tween.tween_property(node, 'position:y', end_position_y, time)
node.global_position.y = start_position_y
tween.tween_property(node, 'global_position:y', end_position_y, time)
await tween.finished
finished_once.emit()

View file

@ -1 +1 @@
uid://b43hklc4t83ic
uid://bj5ak53i7s8ux

View file

@ -1 +1 @@
uid://b4iouoy0lsmg3
uid://crv1pn60clrvx

View file

@ -1 +1 @@
uid://guke2u7jqwkt
uid://cjwdb0jkjrcxe

View file

@ -1 +1 @@
uid://bb2xwtcht84yq
uid://bl5sdpj631mtt

View file

@ -1 +1 @@
uid://b7mq20hi3v6m8
uid://j2k3uogf5715

View file

@ -75,3 +75,28 @@ func get_modulation_property() -> String:
return "self_modulate"
else:
return "modulate"
## Tries to return the size of the node to be animated.
## For portraits this uses the portrait containers size.
## This is useful if your animation depends on the size of the node.
func get_node_size() -> Vector2:
if not node:
return Vector2()
if node.get_parent() is DialogicNode_PortraitContainer:
return node.get_parent().size
if "size" in node:
return node.size * node.scale
return node.get_viewport().size
func get_node_origin() -> Vector2:
if not node:
return Vector2()
if node.get_parent() is DialogicNode_PortraitContainer:
return node.get_parent()._get_origin_position()
return Vector2()
func get_viewport_size() -> Vector2:
return node.get_viewport().get_visible_rect().size

View file

@ -1 +1 @@
uid://blu2it3fdcxr8
uid://0hsjlurlblou

View file

@ -1 +1 @@
uid://c3a0wujs6kev3
uid://cork0heubbx7f

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://b32paf0ll6um8"]
[ext_resource type="Script" uid="uid://c3a0wujs6kev3" path="res://addons/dialogic/Modules/Character/default_portrait.gd" id="1_wn77n"]
[ext_resource type="Script" uid="uid://cork0heubbx7f" path="res://addons/dialogic/Modules/Character/default_portrait.gd" id="1_wn77n"]
[node name="DefaultPortrait" type="Node2D"]
script = ExtResource("1_wn77n")

View file

@ -1 +1 @@
uid://du4fl3cae5jml
uid://djbg8sc0q67ow

View file

@ -82,14 +82,14 @@ var character_identifier: String:
if character_identifier == '--All--':
return '--All--'
if character:
var identifier := DialogicResourceUtil.get_unique_identifier(character.resource_path)
var identifier := character.get_identifier()
if not identifier.is_empty():
return identifier
return character_identifier
set(value):
character_identifier = value
character = DialogicResourceUtil.get_character_resource(value)
if character and not character.portraits.has(portrait):
if (not character) or (character and not character.portraits.has(portrait)):
portrait = ""
ui_update_needed.emit()
@ -154,10 +154,10 @@ func _execute() -> void:
finish()
return
dialogic.Portraits.change_character_extradata(character, extra_data)
if set_portrait:
dialogic.Portraits.change_character_portrait(character, portrait, fade_animation, fade_length)
await dialogic.Portraits.change_character_portrait(character, portrait, fade_animation, fade_length)
dialogic.Portraits.change_character_extradata(character, extra_data)
if set_mirrored:
dialogic.Portraits.change_character_mirror(character, mirrored)
@ -225,7 +225,7 @@ func to_text() -> String:
if action == Actions.LEAVE and character_identifier == '--All--':
result_string += "--All--"
elif character:
var name := DialogicResourceUtil.get_unique_identifier(character.resource_path)
var name := character.get_character_name()
if name.count(" ") > 0:
name = '"' + name + '"'
@ -302,7 +302,7 @@ func get_shortcode_parameters() -> Dictionary:
{'value':Actions.JOIN},
'Leave':{'value':Actions.LEAVE},
'Update':{'value':Actions.UPDATE}}},
"character" : {"property": "character_identifier", "default": "", "custom_stored":true,},
"character" : {"property": "character_identifier", "default": "", "custom_stored":true, "ext_file":true},
"portrait" : {"property": "portrait", "default": "", "custom_stored":true,},
"transform" : {"property": "transform", "default": "center", "custom_stored":true,},
@ -478,8 +478,8 @@ func get_fade_suggestions(search_text:String='') -> Dictionary:
func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:String, _word:String, symbol:String) -> void:
var line_until_caret: String = CodeCompletionHelper.get_line_untill_caret(line)
if symbol == ' ' and line_until_caret.count(' ') == 1:
CodeCompletionHelper.suggest_characters(TextNode, CodeEdit.KIND_MEMBER)
if symbol == ' ' and line_until_caret.count(" ") == 1:
CodeCompletionHelper.suggest_characters(TextNode, CodeEdit.KIND_MEMBER, self)
if line.begins_with('leave'):
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, 'All', '--All-- ', event_color, TextNode.get_theme_icon("GuiEllipsis", "EditorIcons"))
@ -487,10 +487,11 @@ func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:Str
var completion_character := regex.search(line).get_string('name')
CodeCompletionHelper.suggest_portraits(TextNode, completion_character)
elif not '[' in line_until_caret and symbol == ' ':
elif not '[' in line_until_caret and symbol == ' ' and line_until_caret.split(" ", false).size() > 1:
if not line.begins_with("leave"):
for position in get_position_suggestions():
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, position, position+' ', TextNode.syntax_highlighter.normal_color)
if not line_until_caret.split(" ", false)[-1] in get_position_suggestions():
for position in get_position_suggestions():
TextNode.add_code_completion_option(CodeEdit.KIND_MEMBER, position, position+' ', TextNode.syntax_highlighter.normal_color)
# Shortcode Part
if '[' in line_until_caret:
@ -504,8 +505,10 @@ func _get_code_completion(CodeCompletionHelper:Node, TextNode:TextEdit, line:Str
if line.begins_with('update'):
suggest_parameter("repeat", line, TextNode)
if line.begins_with("update"):
for param in ["move_time", "move_trans", "move_ease"]:
for param in ["move_time", "move_trans", "move_ease", "fade"]:
suggest_parameter(param, line, TextNode)
if "fade=" in line_until_caret:
suggest_parameter("fade_length", line, TextNode)
if not line.begins_with('leave'):
for param in ["mirrored", "z_index", "extra_data"]:
suggest_parameter(param, line, TextNode)

View file

@ -1 +1 @@
uid://duu0jekiba20n
uid://b88y7tdin2uu5

View file

@ -1 +1 @@
uid://drl3os1svksa2
uid://4tu24b2ex257

View file

@ -1 +1 @@
uid://cfcx0of1aekhk
uid://d0ptqnbudhkyj

View file

@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="DialogicCharacter" load_steps=2 format=3 uid="uid://dykf1j17ct5mo"]
[ext_resource type="Script" uid="uid://w3vtr3asq7b3" path="res://addons/dialogic/Resources/character.gd" id="1_qsljv"]
[ext_resource type="Script" uid="uid://don4ds5f38byo" path="res://addons/dialogic/Resources/character.gd" id="1_qsljv"]
[resource]
script = ExtResource("1_qsljv")

View file

@ -17,11 +17,11 @@ const ANIMATION_CROSSFADE_DEFAULT_LENGTH_KEY:= 'dialogic/animations/cross_fade_d
func _ready():
%JoinDefault.get_suggestions_func = get_join_animation_suggestions
%JoinDefault.suggestions_func = get_join_animation_suggestions
%JoinDefault.mode = 1
%LeaveDefault.get_suggestions_func = get_leave_animation_suggestions
%LeaveDefault.suggestions_func = get_leave_animation_suggestions
%LeaveDefault.mode = 1
%CrossFadeDefault.get_suggestions_func = get_crossfade_animation_suggestions
%CrossFadeDefault.suggestions_func = get_crossfade_animation_suggestions
%CrossFadeDefault.mode = 1
%PositionSuggestions.text_submitted.connect(save_setting.bind(POSITION_SUGGESTION_KEY))

View file

@ -1 +1 @@
uid://0hdug2yw0j7u
uid://c3hdycwp0hrdm

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=5 format=3 uid="uid://cp463rpri5j8a"]
[ext_resource type="Script" uid="uid://0hdug2yw0j7u" path="res://addons/dialogic/Modules/Character/settings_portraits.gd" id="2"]
[ext_resource type="Script" uid="uid://c3hdycwp0hrdm" path="res://addons/dialogic/Modules/Character/settings_portraits.gd" id="2"]
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_dce78"]
[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3"]
[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="3_m06d8"]
@ -19,8 +19,7 @@ size_flags_stretch_ratio = 0.5
[node name="Title2" type="Label" parent="PositionsTitle"]
layout_mode = 2
theme_type_variation = &"DialogicSettingsSection"
text = "Position Suggestions
"
text = "Position Suggestions"
[node name="HintTooltip" parent="PositionsTitle" instance=ExtResource("2_dce78")]
layout_mode = 2
@ -40,8 +39,7 @@ size_flags_stretch_ratio = 0.5
[node name="Title2" type="Label" parent="DefaultSceneTitle"]
layout_mode = 2
theme_type_variation = &"DialogicSettingsSection"
text = "Default Portrait Scene
"
text = "Default Portrait Scene"
[node name="HintTooltip" parent="DefaultSceneTitle" instance=ExtResource("2_dce78")]
layout_mode = 2
@ -70,8 +68,7 @@ size_flags_stretch_ratio = 0.5
[node name="Title2" type="Label" parent="Animations2"]
layout_mode = 2
theme_type_variation = &"DialogicSettingsSection"
text = "Default Animations
"
text = "Default Animations"
[node name="HintTooltip" parent="Animations2" instance=ExtResource("2_dce78")]
layout_mode = 2

View file

@ -182,7 +182,8 @@ func resize_container(container: DialogicNode_PortraitContainer, rect_size: Vari
tween.finished.connect(save_position_container.bind(container))
else:
container.position = container.position + relative_position_change
container.size = final_rect_resize
#container.size = final_rect_resize
container.set_deferred("size", final_rect_resize)
save_position_container(container)
position_changed.emit({&'change':'resized', &'container_node':container})

View file

@ -1 +1 @@
uid://c8ug08wx4f1d0
uid://c5dk5rh4vj8rd

View file

@ -19,8 +19,8 @@ var default_portrait_scene: PackedScene = load(get_script().resource_path.get_ba
####################################################################################################
func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
for character in dialogic.current_state_info.get('portraits', {}).keys():
remove_character(load(character))
for character_identifier in dialogic.current_state_info.get('portraits', {}).keys():
remove_character(DialogicResourceUtil.get_character_resource(character_identifier))
dialogic.current_state_info['portraits'] = {}
@ -31,21 +31,24 @@ func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
# Load Position Portraits
var portraits_info: Dictionary = dialogic.current_state_info.portraits.duplicate()
dialogic.current_state_info.portraits = {}
for character_path in portraits_info:
var character_info: Dictionary = portraits_info[character_path]
var character: DialogicCharacter = load(character_path)
var container := dialogic.PortraitContainers.load_position_container(character.get_character_name())
add_character(character, container, character_info.portrait, character_info.position_id)
change_character_mirror(character, character_info.get('custom_mirror', false))
change_character_z_index(character, character_info.get('z_index', 0))
change_character_extradata(character, character_info.get('extra_data', ""))
for character_identifier in portraits_info:
var character_info: Dictionary = portraits_info[character_identifier]
var character: DialogicCharacter = DialogicResourceUtil.get_character_resource(character_identifier)
if character:
var container := dialogic.PortraitContainers.load_position_container(character.get_character_name())
add_character(character, container, character_info.portrait, character_info.position_id)
change_character_mirror(character, character_info.get('custom_mirror', false))
change_character_z_index(character, character_info.get('z_index', 0))
change_character_extradata(character, character_info.get('extra_data', ""))
else:
push_error('[Dialogic] Failed to load character "' + str(character_identifier) + '".')
# Load Speaker Portrait
var speaker: Variant = dialogic.current_state_info.get('speaker', "")
var speaker: Variant = dialogic.current_state_info.get("speaker", "")
if speaker:
dialogic.current_state_info['speaker'] = ""
change_speaker(load(speaker))
dialogic.current_state_info['speaker'] = speaker
dialogic.current_state_info["speaker"] = ""
change_speaker(DialogicResourceUtil.get_character_resource(speaker))
dialogic.current_state_info["speaker"] = speaker
func pause() -> void:
@ -117,6 +120,7 @@ func _change_portrait(character_node: Node2D, portrait: String, fade_animation:=
if (not previous_portrait == null and
previous_portrait.get_meta('scene', '') == scene_path and
# Also check if the scene supports changing to the given portrait.
previous_portrait.has_method('_should_do_portrait_update') and
previous_portrait._should_do_portrait_update(character, portrait)):
portrait_node = previous_portrait
info['same_scene'] = true
@ -124,12 +128,21 @@ func _change_portrait(character_node: Node2D, portrait: String, fade_animation:=
else:
if ResourceLoader.exists(scene_path):
var packed_scene: PackedScene = load(scene_path)
ResourceLoader.load_threaded_request(scene_path)
if packed_scene:
portrait_node = packed_scene.instantiate()
var load_status := ResourceLoader.load_threaded_get_status(scene_path)
while load_status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await get_tree().process_frame
load_status = ResourceLoader.load_threaded_get_status(scene_path)
if load_status == ResourceLoader.THREAD_LOAD_LOADED:
var packed_scene: PackedScene = ResourceLoader.load_threaded_get(scene_path)
if packed_scene:
portrait_node = packed_scene.instantiate()
else:
push_error('[Dialogic] Portrait node "' + str(scene_path) + '" for character [' + character.display_name + '] could not be loaded. Your portrait might not show up on the screen. Confirm the path is correct.')
else:
push_error('[Dialogic] Portrait node "' + str(scene_path) + '" for character [' + character.display_name + '] could not be loaded. Your portrait might not show up on the screen. Confirm the path is correct.')
push_error('[Dialogic] Failed to load portrait node "' + str(scene_path) + '" for character [' + character.display_name + '].')
if !portrait_node:
portrait_node = default_portrait_scene.instantiate()
@ -167,20 +180,26 @@ func _change_portrait(character_node: Node2D, portrait: String, fade_animation:=
## Unless @force is false, this will take into consideration the character mirror,
## portrait mirror and portrait position mirror settings.
func _change_portrait_mirror(character_node: Node2D, mirrored := false, force := false) -> void:
var latest_portrait := character_node.get_child(-1)
var latest_portrait := character_node.get_child(-1) if character_node.get_child_count() > 0 else null
if latest_portrait.has_method('_set_mirror'):
if latest_portrait and latest_portrait.has_method("_set_mirror"):
var character: DialogicCharacter = character_node.get_meta('character')
var current_portrait_info := character.get_portrait_info(character_node.get_meta('portrait'))
latest_portrait._set_mirror(force or (mirrored != character.mirror != character_node.get_parent().mirrored != current_portrait_info.get('mirror', false)))
func _change_portrait_extradata(character_node: Node2D, extra_data := "") -> void:
var latest_portrait := character_node.get_child(-1)
if not is_instance_valid(character_node):
push_error("[Dialogic] Invalid character node provided.")
return
if latest_portrait.has_method('_set_extra_data'):
latest_portrait._set_extra_data(extra_data)
if character_node.get_child_count() > 0:
var latest_portrait := character_node.get_child(-1)
if latest_portrait and latest_portrait.has_method("_set_extra_data"):
latest_portrait._set_extra_data(extra_data)
else:
push_warning("[Dialogic] No portrait found for character node: " + character_node.name)
func _update_character_transform(character_node:Node, time := 0.0) -> void:
for child in character_node.get_children():
@ -287,7 +306,7 @@ func _change_portrait_z_index(character_node: Node, z_index:int, update_zindex:=
## fully visible yet.
func get_character_portrait(character: DialogicCharacter) -> DialogicPortrait:
if is_character_joined(character):
var portrait_node: DialogicPortrait = dialogic.current_state_info['portraits'][character.resource_path].node.get_child(-1)
var portrait_node: DialogicPortrait = dialogic.current_state_info['portraits'][character.get_identifier()].node.get_child(-1)
return portrait_node
return null
@ -342,7 +361,7 @@ func get_valid_portrait(character:DialogicCharacter, portrait:String) -> String:
if not portrait in character.portraits:
if not portrait.is_empty():
printerr('[Dialogic] Tried to use invalid portrait "', portrait, '" on character "', DialogicResourceUtil.get_unique_identifier(character.resource_path), '". Using default portrait instead.')
printerr('[Dialogic] Tried to use invalid portrait "', portrait, '" on character "', character.get_character_name(), '". Using default portrait instead.')
dialogic.print_debug_moment()
portrait = character.default_portrait
@ -379,18 +398,18 @@ func join_character(character:DialogicCharacter, portrait:String, position_id:S
return
var container := dialogic.PortraitContainers.add_container(character.get_character_name())
var character_node := add_character(character, container, portrait, position_id)
var character_node := await add_character(character, container, portrait, position_id)
if character_node == null:
return null
dialogic.current_state_info['portraits'][character.resource_path] = {'portrait':portrait, 'node':character_node, 'position_id':position_id, 'custom_mirror':mirrored}
dialogic.current_state_info['portraits'][character.get_identifier()] = {'portrait':portrait, 'node':character_node, 'position_id':position_id, 'custom_mirror':mirrored}
_change_portrait_mirror(character_node, mirrored)
_change_portrait_extradata(character_node, extra_data)
_change_portrait_z_index(character_node, z_index)
var info := {'character':character}
info.merge(dialogic.current_state_info['portraits'][character.resource_path])
info.merge(dialogic.current_state_info['portraits'][character.get_identifier()])
character_joined.emit(info)
if animation_name.is_empty():
@ -410,9 +429,9 @@ func join_character(character:DialogicCharacter, portrait:String, position_id:S
return character_node
func add_character(character:DialogicCharacter, container: DialogicNode_PortraitContainer, portrait:String, position_id:String) -> Node:
func add_character(character: DialogicCharacter, container: DialogicNode_PortraitContainer, portrait: String, position_id: String) -> Node:
if is_character_joined(character):
printerr('[DialogicError] Cannot add a already joined character. If this is intended call _create_character_node manually.')
printerr('[DialogicError] Cannot add an already joined character. If this is intended, call _create_character_node manually.')
return null
portrait = get_valid_portrait(character, portrait)
@ -430,36 +449,35 @@ func add_character(character:DialogicCharacter, container: DialogicNode_Portrait
printerr('[Dialogic] Failed to join character to position ', position_id, ". Could not find position container.")
return null
dialogic.current_state_info['portraits'][character.resource_path] = {'portrait':portrait, 'node':character_node, 'position_id':position_id}
dialogic.current_state_info['portraits'][character.get_identifier()] = {'portrait': portrait, 'node': character_node, 'position_id': position_id}
_move_character(character_node, position_id)
_change_portrait(character_node, portrait)
await _change_portrait(character_node, portrait)
return character_node
## Changes the portrait of a character. Only works with joined characters.
func change_character_portrait(character: DialogicCharacter, portrait: String, fade_animation:="DEFAULT", fade_length := -1.0) -> void:
func change_character_portrait(character: DialogicCharacter, portrait: String, fade_animation:="", fade_length := -1.0) -> void:
if not is_character_joined(character):
return
portrait = get_valid_portrait(character, portrait)
if dialogic.current_state_info.portraits[character.resource_path].portrait == portrait:
if dialogic.current_state_info.portraits[character.get_identifier()].portrait == portrait:
return
if fade_animation == "DEFAULT":
if fade_animation == "":
fade_animation = ProjectSettings.get_setting('dialogic/animations/cross_fade_default', "Fade Cross")
fade_length = ProjectSettings.get_setting('dialogic/animations/cross_fade_default_length', 0.5)
fade_animation = DialogicPortraitAnimationUtil.guess_animation(fade_animation, DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
var info := _change_portrait(dialogic.current_state_info.portraits[character.resource_path].node, portrait, fade_animation, fade_length)
dialogic.current_state_info.portraits[character.resource_path].portrait = info.portrait
var info := await _change_portrait(dialogic.current_state_info.portraits[character.get_identifier()].node, portrait, fade_animation, fade_length)
dialogic.current_state_info.portraits[character.get_identifier()].portrait = info.portrait
_change_portrait_mirror(
dialogic.current_state_info.portraits[character.resource_path].node,
dialogic.current_state_info.portraits[character.resource_path].get('custom_mirror', false)
dialogic.current_state_info.portraits[character.get_identifier()].node,
dialogic.current_state_info.portraits[character.get_identifier()].get('custom_mirror', false)
)
character_portrait_changed.emit(info)
@ -469,8 +487,8 @@ func change_character_mirror(character:DialogicCharacter, mirrored:= false, forc
if !is_character_joined(character):
return
_change_portrait_mirror(dialogic.current_state_info.portraits[character.resource_path].node, mirrored, force)
dialogic.current_state_info.portraits[character.resource_path]['custom_mirror'] = mirrored
_change_portrait_mirror(dialogic.current_state_info.portraits[character.get_identifier()].node, mirrored, force)
dialogic.current_state_info.portraits[character.get_identifier()]['custom_mirror'] = mirrored
## Changes the z_index of a character. Only works with joined characters
@ -478,17 +496,17 @@ func change_character_z_index(character:DialogicCharacter, z_index:int, update_z
if !is_character_joined(character):
return
_change_portrait_z_index(dialogic.current_state_info.portraits[character.resource_path].node, z_index, update_zindex)
_change_portrait_z_index(dialogic.current_state_info.portraits[character.get_identifier()].node, z_index, update_zindex)
if update_zindex:
dialogic.current_state_info.portraits[character.resource_path]['z_index'] = z_index
dialogic.current_state_info.portraits[character.get_identifier()]['z_index'] = z_index
## Changes the extra data on the given character. Only works with joined characters
func change_character_extradata(character:DialogicCharacter, extra_data:="") -> void:
if !is_character_joined(character):
return
_change_portrait_extradata(dialogic.current_state_info.portraits[character.resource_path].node, extra_data)
dialogic.current_state_info.portraits[character.resource_path]['extra_data'] = extra_data
_change_portrait_extradata(dialogic.current_state_info.portraits[character.get_identifier()].node, extra_data)
dialogic.current_state_info.portraits[character.get_identifier()]['extra_data'] = extra_data
## Starts the given animation on the given character. Only works with joined characters
@ -498,7 +516,7 @@ func animate_character(character: DialogicCharacter, animation_path: String, len
animation_path = DialogicPortraitAnimationUtil.guess_animation(animation_path)
var character_node: Node = dialogic.current_state_info.portraits[character.resource_path].node
var character_node: Node = dialogic.current_state_info.portraits[character.get_identifier()].node
return _animate_node(character_node, animation_path, length, repeats, is_reversed)
@ -508,11 +526,11 @@ func move_character(character:DialogicCharacter, position_id:String, time:= 0.0,
if !is_character_joined(character):
return
if dialogic.current_state_info.portraits[character.resource_path].position_id == position_id:
if dialogic.current_state_info.portraits[character.get_identifier()].position_id == position_id:
return
_move_character(dialogic.current_state_info.portraits[character.resource_path].node, position_id, time, easing, trans)
dialogic.current_state_info.portraits[character.resource_path].position_id = position_id
_move_character(dialogic.current_state_info.portraits[character.get_identifier()].node, position_id, time, easing, trans)
dialogic.current_state_info.portraits[character.get_identifier()].position_id = position_id
character_moved.emit({'character':character, 'position_id':position_id, 'time':time})
@ -554,8 +572,8 @@ func leave_all_characters(animation_name:="", animation_length:=0.0, animation_w
## Return `null` if the [param character] is not part of the scene.
func get_character_node(character: DialogicCharacter) -> Node:
if is_character_joined(character):
if is_instance_valid(dialogic.current_state_info['portraits'][character.resource_path].node):
return dialogic.current_state_info['portraits'][character.resource_path].node
if is_instance_valid(dialogic.current_state_info['portraits'][character.get_identifier()].node):
return dialogic.current_state_info['portraits'][character.get_identifier()].node
return null
@ -571,19 +589,12 @@ func remove_character(character: DialogicCharacter) -> void:
character_node.queue_free()
character_left.emit({'character': character})
dialogic.current_state_info['portraits'].erase(character.resource_path)
func get_current_character() -> DialogicCharacter:
if dialogic.current_state_info.get('speaker', null):
return load(dialogic.current_state_info.speaker)
return null
dialogic.current_state_info['portraits'].erase(character.get_identifier())
## Returns true if the given character is currently joined.
func is_character_joined(character: DialogicCharacter) -> bool:
if character == null or not character.resource_path in dialogic.current_state_info['portraits']:
if character == null or not character.get_identifier() in dialogic.current_state_info['portraits']:
return false
return true
@ -593,8 +604,8 @@ func is_character_joined(character: DialogicCharacter) -> bool:
func get_joined_characters() -> Array[DialogicCharacter]:
var chars: Array[DialogicCharacter] = []
for char_path: String in dialogic.current_state_info.get('portraits', {}).keys():
chars.append(load(char_path))
for char_identifier: String in dialogic.current_state_info.get('portraits', {}).keys():
chars.append(DialogicResourceUtil.get_character_resource(char_identifier))
return chars
@ -604,7 +615,7 @@ func get_joined_characters() -> Array[DialogicCharacter]:
## Only joined is included (and false) for not joined characters
func get_character_info(character:DialogicCharacter) -> Dictionary:
if is_character_joined(character):
var info: Dictionary = dialogic.current_state_info['portraits'][character.resource_path]
var info: Dictionary = dialogic.current_state_info['portraits'][character.get_identifier()]
info['joined'] = true
return info
else:
@ -629,7 +640,8 @@ func change_speaker(speaker: DialogicCharacter = null, portrait := "") -> void:
if leave_animation and leave_animation_length:
var animate_out := _animate_node(character_node, leave_animation, leave_animation_length, 1, true)
animate_out.finished.connect(character_node.queue_free)
await animate_out.finished
character_node.queue_free()
else:
character_node.get_parent().remove_child(character_node)
character_node.queue_free()
@ -655,10 +667,10 @@ func change_speaker(speaker: DialogicCharacter = null, portrait := "") -> void:
fade_animation = DialogicPortraitAnimationUtil.guess_animation(fade_animation, DialogicPortraitAnimationUtil.AnimationType.CROSSFADE)
if container.portrait_prefix+portrait in speaker.portraits:
portrait = container.portrait_prefix+portrait
if container.portrait_prefix + portrait in speaker.portraits:
portrait = container.portrait_prefix + portrait
_change_portrait(character_node, portrait, fade_animation, fade_length)
await _change_portrait(character_node, portrait, fade_animation, fade_length)
# if the character has no portraits _change_portrait won't actually add a child node
if character_node.get_child_count() == 0:
@ -671,26 +683,28 @@ func change_speaker(speaker: DialogicCharacter = null, portrait := "") -> void:
character_node.hide()
if not container.is_visible_in_tree():
await get_tree().process_frame
# There is chance that the style changed (due to a speaker style) and thus the character node is gone now.
# In that case, just give up.
if not is_instance_valid(character_node):
return
character_node.show()
var join_animation: String = ProjectSettings.get_setting('dialogic/animations/join_default', "Fade In Up")
join_animation = DialogicPortraitAnimationUtil.guess_animation(join_animation, DialogicPortraitAnimationUtil.AnimationType.IN)
var join_animation_length := _get_join_default_length()
if join_animation and join_animation_length:
_animate_node(character_node, join_animation, join_animation_length)
await _animate_node(character_node, join_animation, join_animation_length).finished
_change_portrait_mirror(character_node)
if speaker:
if speaker.resource_path != dialogic.current_state_info['speaker']:
if dialogic.current_state_info['speaker'] and is_character_joined(load(dialogic.current_state_info['speaker'])):
dialogic.current_state_info['portraits'][dialogic.current_state_info['speaker']].node.get_child(-1)._unhighlight()
var prev_speaker: DialogicCharacter = dialogic.Text.get_current_speaker()
if speaker != prev_speaker:
if is_character_joined(prev_speaker):
dialogic.current_state_info["portraits"][prev_speaker.get_identifier()].node.get_child(-1)._unhighlight()
if speaker and is_character_joined(speaker):
dialogic.current_state_info['portraits'][speaker.resource_path].node.get_child(-1)._highlight()
elif dialogic.current_state_info['speaker'] and is_character_joined(load(dialogic.current_state_info['speaker'])):
dialogic.current_state_info['portraits'][dialogic.current_state_info['speaker']].node.get_child(-1)._unhighlight()
if is_character_joined(speaker):
dialogic.current_state_info["portraits"][speaker.get_identifier()].node.get_child(-1)._highlight()
#endregion
@ -701,14 +715,15 @@ func change_speaker(speaker: DialogicCharacter = null, portrait := "") -> void:
## Called from the [portrait=something] text effect.
func text_effect_portrait(_text_node:Control, _skipped:bool, argument:String) -> void:
if argument:
if dialogic.current_state_info.get('speaker', null):
change_character_portrait(load(dialogic.current_state_info.speaker), argument)
change_speaker(load(dialogic.current_state_info.speaker), argument)
var current_speaker := dialogic.Text.get_current_speaker()
if current_speaker:
change_character_portrait(current_speaker, argument)
change_speaker(current_speaker, argument)
## Called from the [extra_data=something] text effect.
func text_effect_extradata(_text_node:Control, _skipped:bool, argument:String) -> void:
if argument:
if dialogic.current_state_info.get('speaker', null):
change_character_extradata(load(dialogic.current_state_info.speaker), argument)
if dialogic.Text.get_current_speaker():
change_character_extradata(dialogic.Text.get_current_speaker(), argument)
#endregion

View file

@ -1 +1 @@
uid://c1531cn2fwxf6
uid://fyhfur7bpp4v

View file

@ -37,6 +37,10 @@ func _execute() -> void:
dialogic.Choices.show_current_question(false)
dialogic.current_state = dialogic.States.AWAITING_CHOICE
func _is_branch_starter() -> bool:
return dialogic.Choices.is_question(dialogic.current_timeline_events.find(self))
#endregion
@ -53,7 +57,7 @@ func _init() -> void:
# return a control node that should show on the END BRANCH node
func get_end_branch_control() -> Control:
func _get_end_branch_control() -> Control:
return load(get_script().resource_path.get_base_dir().path_join('ui_choice_end.tscn')).instantiate()
#endregion

View file

@ -1 +1 @@
uid://evfcdip3607c
uid://cltu1tykths0n

View file

@ -1 +1 @@
uid://cpp7qjji6ck8p
uid://bo0dqybasbqd

View file

@ -1 +1 @@
uid://bijk2aslmrx2r
uid://b1stj4ljd2vo7

View file

@ -1,15 +1,28 @@
class_name DialogicNode_ChoiceButton
extends Button
## The button allows the player to make a choice in the Dialogic system.
## This button allows the player to make a choice in the Dialogic system.
##
## This class is used in the Choice Layer. [br]
## You may change the [member text_node] to any [class Node] that has a
## `text` property. [br]
## If you don't set the [member text_node], the text will be set on this
## button instead.
##
## Using a different node may allow using rich text effects; they are
## not supported on buttons at this point.
## When a choice is reached Dialogic will automatically show ChoiceButtons
## and call their [code]_load_info()[/code] method which will display the choices.
## You will need to ensure that enough choice buttons are available in the tree
## to display all choices.[br]
##
## [br]
## You can extend this node and implement some custom logic by overwriting
## the [code]_load_info(info:Dictionary)[/code] method. [br]
## [br]
## If you need RichText support, consider adding a RichTextLabel child and setting it as the [member text_node].[br]
##
## [br]
## DialogicChoiceButtons will grab the focus when hovered to avoid a confusing
## focus style being present for players who use the mouse.[br]
## To avoid the opposite situation, when the focus is changed by the player
## and a different button is still hovered the mouse pointer will be moved
## to the now focused button as well.
## Emitted when the choice is selected. Unless overridden, this is when the button or its shortcut is pressed.
signal choice_selected
## Used to identify what choices to put on. If you leave it at -1, choices will be distributed automatically.
@ -21,7 +34,9 @@ extends Button
@export var sound_hover: AudioStream
## Can be set to play this sound when focused. Requires a sibling DialogicNode_ButtonSound node.
@export var sound_focus: AudioStream
## If set, the text will be set on this node's `text` property instead.
## If set, the text will be set on this node's `text` property instead.
## This can be used to have a custom text rendering child, like a RichTextLabel.
@export var text_node: Node
@ -29,8 +44,34 @@ func _ready() -> void:
add_to_group('dialogic_choice_button')
shortcut_in_tooltip = false
hide()
# For players who use a mouse to make choices, mouse hover should grab focus.
# Otherwise the auto-focused button will always show a highlighted color when
# the mouse cursor is hovering on another button.
if not mouse_entered.is_connected(grab_focus):
mouse_entered.connect(grab_focus)
if not focus_entered.is_connected(_on_choice_button_focus_entred):
focus_entered.connect(_on_choice_button_focus_entred.bind(self))
## Custom choice buttons can override this for specialized behavior when the choice button is pressed.
func _pressed():
choice_selected.emit()
## Custom choice buttons can override this if their behavior should change
## based on event data. If the custom choice button does not override
## visibility, disabled-ness, nor the choice text, consider
## calling super(choice_info) at the start of the override.
##
## The choice_info Dictionary has the following keys:
## - event_index: The index of the choice event in the timeline.
## - button_index: The relative index of the choice (starts from 1).
## - visible: If the choice should be visible.
## - disabled: If the choice should be selectable.
## - text: The text of the choice.
## - visited_before: If the choice has been selected before. Only available is the History submodule is enabled.
## - *: Information from the event's additional info.
func _load_info(choice_info: Dictionary) -> void:
set_choice_text(choice_info.text)
visible = choice_info.visible
@ -43,3 +84,37 @@ func set_choice_text(new_text: String) -> void:
text_node.text = new_text
else:
text = new_text
## This method moves the mouse to the focused choice when the focus changes
## while a choice button was hovered. [br]
## For players who use many devices (mouse/keyboard/gamepad, etc) at the same time to make choices,
## a grabing-focus triggered by keyboard/gamepad should also change the mouse cursor's
## position otherwise two buttons will have highlighted color(one highlighted button
## triggered by mouse hover and another highlighted button triggered by other devices' choice).
func _on_choice_button_focus_entred(focused_button: Button):
var global_mouse_pos = get_global_mouse_position()
var focused_button_rect = focused_button.get_global_rect()
if focused_button_rect.has_point(global_mouse_pos):
return
# Only change mouse curor position when an unfocused button' rect has the cursor.
for node in get_tree().get_nodes_in_group('dialogic_choice_button'):
if node is Button:
if node != focused_button and node.get_global_rect().has_point(global_mouse_pos):
# We prefer to change only mouse_position.y or mouse_position.x to warp the
# mouse to the focused button's rect to achieve the best visual effect.
var modify_y_pos = Vector2(global_mouse_pos.x, focused_button.get_global_rect().get_center().y)
if focused_button_rect.has_point(modify_y_pos):
get_viewport().warp_mouse(modify_y_pos)
return
var modify_x_pos = Vector2(focused_button.get_global_rect().get_center().x, global_mouse_pos.y)
if focused_button_rect.has_point(modify_x_pos):
get_viewport().warp_mouse(modify_x_pos)
return
# Maybe the buttons are not aligned as vertically or horizontlly.
# Or perhaps the length difference between the two buttons is quite large.
# So we just make the cursor hover on the center of the focused button.
get_viewport().warp_mouse(focused_button.get_global_rect().get_center())
return

View file

@ -1 +1 @@
uid://bcirqrep7rvr4
uid://bldt7xlfum7ov

View file

@ -1 +1 @@
uid://cyog6egbjdhg0
uid://dbbbq1hcbhmi2

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=5 format=3 uid="uid://uarvgnbrcltm"]
[ext_resource type="Script" uid="uid://cyog6egbjdhg0" path="res://addons/dialogic/Modules/Choice/settings_choices.gd" id="2"]
[ext_resource type="Script" uid="uid://dbbbq1hcbhmi2" path="res://addons/dialogic/Modules/Choice/settings_choices.gd" id="2"]
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_nxutt"]
[sub_resource type="Image" id="Image_xvnnc"]

View file

@ -70,8 +70,8 @@ func post_install() -> void:
func hide_all_choices() -> void:
for node in get_tree().get_nodes_in_group('dialogic_choice_button'):
node.hide()
if node.is_connected('button_up', _on_choice_selected):
node.disconnect('button_up', _on_choice_selected)
if node.choice_selected.is_connected(_on_choice_selected):
node.choice_selected.disconnect(_on_choice_selected)
## Collects information on all the choices of the current question.
@ -120,7 +120,7 @@ func get_current_question_info() -> Dictionary:
if not hide:
button_idx += 1
choice_info.text = dialogic.Text.parse_text(choice_info.text, true, true, false, true, false, false)
choice_info.text = dialogic.Text.parse_text(choice_info.text, 1)
choice_info.merge(choice_event.extra_data)
@ -128,6 +128,7 @@ func get_current_question_info() -> Dictionary:
choice_info['visited_before'] = dialogic.History.has_event_been_visited(choice_index)
question_info['choices'].append(choice_info)
last_question_info['choices'].append(choice_info['text'])
return question_info
@ -155,7 +156,7 @@ func show_current_question(instant:=true) -> void:
var question_info := get_current_question_info()
for choice in question_info.choices:
var node: DialogicNode_ChoiceButton = get_choice_button_node(choice.button_index)
var node: DialogicNode_ChoiceButton = get_choice_button(choice.button_index)
if not node:
missing_button = true
@ -181,9 +182,9 @@ func show_current_question(instant:=true) -> void:
shortcut.events.append(input_key)
node.shortcut = shortcut
if node.pressed.is_connected(_on_choice_selected):
node.pressed.disconnect(_on_choice_selected)
node.pressed.connect(_on_choice_selected.bind(choice))
if node.choice_selected.is_connected(_on_choice_selected):
node.choice_selected.disconnect(_on_choice_selected)
node.choice_selected.connect(_on_choice_selected.bind(choice))
_choice_blocker.start(block_delay)
question_shown.emit(question_info)
@ -192,8 +193,24 @@ func show_current_question(instant:=true) -> void:
printerr("[Dialogic] The layout you are using doesn't have enough Choice Buttons for the choices you are trying to display.")
func focus_choice(button_index:int) -> void:
var node: DialogicNode_ChoiceButton = get_choice_button(button_index)
if node:
node.grab_focus()
func get_choice_button_node(button_index:int) -> DialogicNode_ChoiceButton:
func select_choice(button_index:int) -> void:
var node: DialogicNode_ChoiceButton = get_choice_button(button_index)
if node:
node.choice_selected.emit()
func select_focused_choice() -> void:
if get_viewport().gui_get_focus_owner() is DialogicNode_ChoiceButton:
(get_viewport().gui_get_focus_owner() as DialogicNode_ChoiceButton).choice_selected.emit()
func get_choice_button(button_index:int) -> DialogicNode_ChoiceButton:
var idx := 1
for node: DialogicNode_ChoiceButton in get_tree().get_nodes_in_group('dialogic_choice_button'):
if !node.get_parent().is_visible_in_tree():
@ -227,33 +244,29 @@ func _on_choice_selected(choice_info := {}) -> void:
dialogic.handle_event(choice_info.event_index + 1)
## Returns the indexes of the choice events related to the current question.
func get_current_choice_indexes() -> Array:
var choices := []
var evt_idx := dialogic.current_event_idx
var ignore := 0
var index := dialogic.current_event_idx-1
while true:
if evt_idx >= len(dialogic.current_timeline_events):
index += 1
if index >= len(dialogic.current_timeline_events):
break
var event: DialogicEvent = dialogic.current_timeline_events[index]
if event is DialogicChoiceEvent:
choices.append(index)
index = event.get_end_branch_index()
else:
break
if dialogic.current_timeline_events[evt_idx] is DialogicChoiceEvent:
if ignore == 0:
choices.append(evt_idx)
ignore += 1
elif dialogic.current_timeline_events[evt_idx].can_contain_events:
ignore += 1
else:
if ignore == 0:
break
if dialogic.current_timeline_events[evt_idx] is DialogicEndBranchEvent:
ignore -= 1
evt_idx += 1
return choices
## Forward the dialogic action to the focused button
func _on_dialogic_action() -> void:
if get_viewport().gui_get_focus_owner() is DialogicNode_ChoiceButton and use_input_action and not dialogic.Inputs.input_was_mouse_input:
get_viewport().gui_get_focus_owner().pressed.emit()
if use_input_action and not dialogic.Inputs.input_was_mouse_input:
select_focused_choice()
#endregion
@ -262,20 +275,27 @@ func _on_dialogic_action() -> void:
#region HELPERS
####################################################################################################
## Returns `true` if the given index is a text event before a question or the first choice event of a question.
func is_question(index:int) -> bool:
if dialogic.current_timeline_events[index] is DialogicTextEvent:
var event: DialogicEvent = dialogic.current_timeline_events[index]
if event is DialogicTextEvent:
if len(dialogic.current_timeline_events)-1 != index:
if dialogic.current_timeline_events[index+1] is DialogicChoiceEvent:
var next_event: DialogicEvent = dialogic.current_timeline_events[index+1]
if next_event is DialogicChoiceEvent:
return true
if dialogic.current_timeline_events[index] is DialogicChoiceEvent:
if index != 0 and dialogic.current_timeline_events[index-1] is DialogicEndBranchEvent:
if dialogic.current_timeline_events[dialogic.current_timeline_events[index-1].find_opening_index(index-1)] is DialogicChoiceEvent:
return false
else:
return true
if event is DialogicChoiceEvent:
if index == 0:
return true
var prev_event: DialogicEvent = dialogic.current_timeline_events[index-1]
if not prev_event is DialogicEndBranchEvent:
return true
var prev_event_opener: DialogicEvent = dialogic.current_timeline_events[prev_event.get_opening_index()]
if prev_event_opener is DialogicChoiceEvent:
return false
else:
return true
return false
#endregion

View file

@ -1 +1 @@
uid://70q8j1ji8wm
uid://cewv4d3aw0kj3

View file

@ -1 +1 @@
uid://yt5h64x4n67s
uid://d28x7h2ufh3dd

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cn0wbb2lk0s22"]
[ext_resource type="Script" uid="uid://yt5h64x4n67s" path="res://addons/dialogic/Modules/Choice/ui_choice_end.gd" id="1_7qd85"]
[ext_resource type="Script" uid="uid://d28x7h2ufh3dd" path="res://addons/dialogic/Modules/Choice/ui_choice_end.gd" id="1_7qd85"]
[node name="Choice_End" type="HBoxContainer"]
anchors_preset = 15

View file

@ -26,9 +26,9 @@ func _execute() -> void:
var time_per_event: float = dialogic.Inputs.auto_skip.time_per_event
final_time = min(time, time_per_event)
if clear_textbox and dialogic.has_subsystem("Text"):
if clear_textbox and dialogic.has_subsystem("Text") and dialogic.Text.is_textbox_visible():
dialogic.Text.update_dialog_text('')
dialogic.Text.hide_textbox()
dialogic.Text.hide_textbox(final_time == 0)
dialogic.current_state = dialogic.States.IDLE
if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
@ -44,10 +44,10 @@ func _execute() -> void:
if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
if clear_music and dialogic.has_subsystem('Audio'):
for channel_id in dialogic.Audio.max_channels:
if dialogic.Audio.has_music(channel_id):
dialogic.Audio.update_music('', 0.0, "", final_time, channel_id)
if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
dialogic.Audio.stop_all_one_shot_sounds()
if dialogic.Audio.is_any_channel_playing():
dialogic.Audio.stop_all_channels(final_time)
if step_by_step: await dialogic.get_tree().create_timer(final_time).timeout
if clear_style and dialogic.has_subsystem('Styles'):
dialogic.Styles.change_style()
@ -109,6 +109,6 @@ func build_event_editor() -> void:
add_body_edit('clear_textbox', ValueType.BOOL_BUTTON, {'left_text':'Clear:', 'icon':load("res://addons/dialogic/Modules/Clear/clear_textbox.svg"), 'tooltip':'Clear Textbox'})
add_body_edit('clear_portraits', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_characters.svg"), 'tooltip':'Clear Portraits'})
add_body_edit('clear_background', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_background.svg"), 'tooltip':'Clear Background'})
add_body_edit('clear_music', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_music.svg"), 'tooltip':'Clear Music'})
add_body_edit('clear_music', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_music.svg"), 'tooltip':'Clear Audio'})
add_body_edit('clear_style', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_style.svg"), 'tooltip':'Clear Style'})
add_body_edit('clear_portrait_positions', ValueType.BOOL_BUTTON, {'icon':load("res://addons/dialogic/Modules/Clear/clear_positions.svg"), 'tooltip':'Clear Portrait Positions'})

View file

@ -1 +1 @@
uid://dny1dhrq768ul
uid://7aikid38is1o

View file

@ -1 +1 @@
uid://cj5o6vkiga2r6
uid://cyftouhvjfun2

View file

@ -1 +1 @@
uid://drrny0bn1d8ls
uid://dbcesveorhh6m

View file

@ -1 +1 @@
uid://bi1ru7ecbb7ag
uid://b27oui35aoff4

View file

@ -7,7 +7,8 @@ extends DialogicEvent
enum ConditionTypes {IF, ELIF, ELSE}
### Settings
## condition type (see [ConditionTypes]). Defaults to if.
## Condition type (see [ConditionTypes]). Defaults to if.
var condition_type := ConditionTypes.IF
## The condition as a string. Will be executed as an Expression.
var condition := ""
@ -26,24 +27,12 @@ func _execute() -> void:
var result: bool = dialogic.Expressions.execute_condition(condition)
if not result:
var idx: int = dialogic.current_event_idx
var ignore := 1
while true:
idx += 1
if not dialogic.current_timeline.get_event(idx) or ignore == 0:
break
elif dialogic.current_timeline.get_event(idx).can_contain_events:
ignore += 1
elif dialogic.current_timeline.get_event(idx) is DialogicEndBranchEvent:
ignore -= 1
dialogic.current_event_idx = get_end_branch_index()
dialogic.current_event_idx = idx-1
finish()
## only called if 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
func should_execute_this_branch() -> bool:
func _is_branch_starter() -> bool:
return condition_type == ConditionTypes.IF
@ -60,7 +49,7 @@ func _init() -> void:
# return a control node that should show on the END BRANCH node
func get_end_branch_control() -> Control:
func _get_end_branch_control() -> Control:
return load(get_script().resource_path.get_base_dir().path_join('ui_condition_end.tscn')).instantiate()
################################################################################

View file

@ -1 +1 @@
uid://doeh3g7s7d4h8
uid://g8gaor7ewun6

View file

@ -1 +1 @@
uid://b1mfu6jjyirnm
uid://bsn2cv832qlam

Some files were not shown because too many files have changed in this diff Show more