GodotProjects/Sprout/addons/dialogic/Events/Character/event_character.gd
2023-01-03 16:54:53 -07:00

471 lines
19 KiB
GDScript

@tool
class_name DialogicCharacterEvent
extends DialogicEvent
## Event that allows to manipulate character portraits.
enum PortraitModes {VisualNovel, RPG}
enum ActionTypes {Join, Leave, Update}
### Settings
## The type of action of this event (JOIN/LEAVE/UPDATE). See [ActionTypes].
var action_type : int = ActionTypes.Join
## The character that will join/leave/update.
var character : DialogicCharacter = null
## For Join/Update, this will be the portrait of the character that is shown.
## Not used on Leave.
## If empty, the default portrait will be used.
var portrait: String = ""
## The index of the position this character should move to
var position: int = 1
## Path to an animation script (extending DialogicAnimation).
## On Join/Leave empty (default) will fallback to the animations set in the settings.
## On Update empty will mean no animation.
var animation_name: String = ""
## Length of the animation.
var animation_length: float = 0.5
## How often the animation is repeated. Only for Update events.
var animation_repeats: int = 1
## If true, the events waits for the animation to finish before the next event starts.
var animation_wait: bool = false
## For Update only. If bigger then 0, the portrait will tween to the
## new position (if changed) in this time (in seconds).
var position_move_time: float = 0.0
## The z_index that the portrait should have.
var z_index: int = 0
## If true, the portrait will be set to mirrored.
var mirrored: bool = false
## If set, will be passed to the portrait scene.
var extra_data: String = ""
### Helpers
## Indicates if the z_index should be updated.
var _update_zindex: bool = false
## Used to set the character resource from the unique name identifier and vice versa
var _character_from_directory: String:
get:
if _character_from_directory == '--All--':
return '--All--'
for item in _character_directory.keys():
if _character_directory[item]['resource'] == character:
return item
break
return ""
set(value):
_character_from_directory = value
if value in _character_directory.keys():
character = _character_directory[value]['resource']
else:
character = null
## Used by [_character_from_directory]
var _character_directory: Dictionary = {}
################################################################################
## EXECUTION
################################################################################
func _execute() -> void:
match action_type:
ActionTypes.Join:
if character:
if !dialogic.Portraits.is_character_joined(character):
var n = dialogic.Portraits.add_portrait(character, portrait, position, mirrored, z_index, extra_data)
if n:
if animation_name.is_empty():
animation_name = DialogicUtil.get_project_setting('dialogic/animations/join_default',
get_script().resource_path.get_base_dir().path_join('DefaultAnimations/fade_in_up.gd'))
animation_length = DialogicUtil.get_project_setting('dialogic/animations/join_default_length', 0.5)
animation_wait = DialogicUtil.get_project_setting('dialogic/animations/join_default_wait', true)
if animation_name:
var anim:DialogicAnimation = dialogic.Portraits.animate_portrait(character, animation_name, animation_length, animation_repeats)
if animation_wait:
dialogic.current_state = Dialogic.states.ANIMATING
await anim.finished
dialogic.current_state = Dialogic.states.IDLE
else:
dialogic.Portraits.change_portrait(character, portrait)
if animation_name.is_empty():
animation_length = DialogicUtil.get_project_setting('dialogic/animations/join_default_length', 0.5)
dialogic.Portraits.move_portrait(character, position, z_index, false, animation_length)
ActionTypes.Leave:
if _character_from_directory == '--All--':
if animation_name.is_empty():
animation_name = DialogicUtil.get_project_setting('dialogic/animations/leave_default',
get_script().resource_path.get_base_dir().path_join('DefaultAnimations/fade_out_down.gd'))
animation_length = DialogicUtil.get_project_setting('dialogic/animations/leave_default_length', 0.5)
animation_wait = DialogicUtil.get_project_setting('dialogic/animations/leave_default_wait', true)
if animation_name:
for chara in dialogic.Portraits.get_joined_characters():
var anim = dialogic.Portraits.animate_portrait(chara, animation_name, animation_length, animation_repeats)
anim.finished.connect(dialogic.Portraits.remove_portrait.bind(chara))
if animation_wait:
dialogic.current_state = Dialogic.states.ANIMATING
await anim.finished
dialogic.current_state = Dialogic.states.IDLE
else:
for chara in dialogic.Portraits.get_joined_characters():
dialogic.Portraits.remove_portrait(chara)
elif character:
if dialogic.Portraits.is_character_joined(character):
if animation_name.is_empty():
animation_name = DialogicUtil.get_project_setting('dialogic/animations/leave_default',
get_script().resource_path.get_base_dir().path_join('DefaultAnimations/fade_out_down.gd'))
animation_length = DialogicUtil.get_project_setting('dialogic/animations/leave_default_length', 0.5)
animation_wait = DialogicUtil.get_project_setting('dialogic/animations/leave_default_wait', true)
if animation_name:
var anim = dialogic.Portraits.animate_portrait(character, animation_name, animation_length, animation_repeats)
anim.finished.connect(dialogic.Portraits.remove_portrait.bind(character))
if animation_wait:
dialogic.current_state = Dialogic.states.ANIMATING
await anim.finished
dialogic.current_state = Dialogic.states.IDLE
else:
dialogic.Portraits.remove_portrait(character)
ActionTypes.Update:
if character:
if dialogic.Portraits.is_character_joined(character):
dialogic.Portraits.change_portrait(character, portrait, mirrored, z_index, _update_zindex, extra_data)
if position != 0:
dialogic.Portraits.move_portrait(character, position, z_index, _update_zindex, position_move_time)
if animation_name:
var anim = dialogic.Portraits.animate_portrait(character, animation_name, animation_length, animation_repeats)
if animation_wait:
dialogic.current_state = Dialogic.states.ANIMATING
await anim.finished
dialogic.current_state = Dialogic.states.IDLE
finish()
################################################################################
## INITIALIZE
################################################################################
func _init() -> void:
event_name = "Character"
set_default_color('Color2')
event_category = Category.Main
event_sorting_index = 2
continue_at_end = true
expand_by_default = false
func _get_icon() -> Resource:
return load(self.get_script().get_path().get_base_dir().path_join('icon_character.png'))
################################################################################
## SAVING/LOADING
################################################################################
func to_text() -> String:
var result_string := ""
match action_type:
ActionTypes.Join: result_string += "Join "
ActionTypes.Leave: result_string += "Leave "
ActionTypes.Update: result_string += "Update "
print('to_string')
print('character ', character)
print('characterfrom ', _character_from_directory)
if character or _character_from_directory == '--All--':
if action_type == ActionTypes.Leave and _character_from_directory == '--All--':
result_string += "--All--"
else:
var name := ""
for path in _character_directory.keys():
if _character_directory[path]['resource'] == character:
name = path
break
if name.count(" ") > 0:
name = '"' + name + '"'
result_string += name
if !portrait.is_empty() and action_type != ActionTypes.Leave:
result_string+= " ("+portrait+")"
if position and action_type != ActionTypes.Leave:
result_string += " "+str(position)
if animation_name != "" or z_index != 0 or mirrored != false or position_move_time != 0.0 or extra_data != "":
result_string += " ["
if animation_name:
result_string += 'animation="'+DialogicUtil.pretty_name(animation_name)+'"'
if animation_length != 0.5:
result_string += ' length="'+str(animation_length)+'"'
if animation_wait:
result_string += ' wait="'+str(animation_wait)+'"'
if animation_repeats != 1:
result_string += ' repeat="'+str(animation_repeats)+'"'
if z_index != 0:
result_string += ' z-index="' + str(z_index) + '"'
if mirrored:
result_string += ' mirrored="' + str(mirrored) + '"'
if position_move_time != 0:
result_string += ' move_time="' + str(position_move_time) + '"'
if extra_data != "":
result_string += ' extra_data="' + extra_data + '"'
result_string += "]"
return result_string
func from_text(string:String) -> void:
if Engine.is_editor_hint() == false:
_character_directory = Dialogic.character_directory
else:
_character_directory = self.get_meta("editor_character_directory")
var regex := RegEx.new()
# Reference regex without Godot escapes: (?<type>Join|Update|Leave)\s*(")?(?<name>(?(2)[^"\n]*|[^(: \n]*))(?(2)"|)(\W*\((?<portrait>.*)\))?(\s*(?<position>\d))?(\s*\[(?<shortcode>.*)\])?
regex.compile("(?<type>Join|Update|Leave)\\s*(\")?(?<name>(?(2)[^\"\\n]*|[^(: \\n]*))(?(2)\"|)(\\W*\\((?<portrait>.*)\\))?(\\s*(?<position>\\d))?(\\s*\\[(?<shortcode>.*)\\])?")
var result := regex.search(string)
match result.get_string('type'):
"Join":
action_type = ActionTypes.Join
"Leave":
action_type = ActionTypes.Leave
"Update":
action_type = ActionTypes.Update
if result.get_string('name').strip_edges():
if action_type == ActionTypes.Leave and result.get_string('name').strip_edges() == "--All--":
_character_from_directory = '--All--'
else:
var name := result.get_string('name').strip_edges()
if _character_directory != null:
if _character_directory.size() > 0:
character = null
if _character_directory.has(name):
character = _character_directory[name]['resource']
else:
name = name.replace('"', "")
# First do a full search to see if more of the path is there then necessary:
for character in _character_directory:
if name in _character_directory[character]['full_path']:
character = _character_directory[character]['resource']
break
# If it doesn't exist, we'll consider it a guest and create a temporary character
if character == null:
if Engine.is_editor_hint() == false:
character = DialogicCharacter.new()
character.display_name = name
var entry:Dictionary = {}
entry['resource'] = character
entry['full_path'] = "runtime://" + name
Dialogic.character_directory[name] = entry
if result.get_string('portrait').strip_edges():
portrait = result.get_string('portrait').strip_edges()
if result.get_string('position'):
position = result.get_string('position').to_int()
elif action_type == ActionTypes.Update:
# Override the normal default if it's an Update
position = 0
if result.get_string('shortcode'):
var shortcode_params = parse_shortcode_parameters(result.get_string('shortcode'))
animation_name = shortcode_params.get('animation', '')
if animation_name != "":
if !animation_name.ends_with('.gd'):
animation_name = guess_animation_file(animation_name)
if !animation_name.ends_with('.gd'):
printerr("[Dialogic] Couldn't identify animation '"+animation_name+"'.")
animation_name = ""
var animLength = shortcode_params.get('length', '0.5').to_float()
if typeof(animLength) == TYPE_FLOAT:
animation_length = animLength
else:
animation_length = animLength.to_float()
animation_wait = DialogicUtil.str_to_bool(shortcode_params.get('wait', 'false'))
#repeat is supported on Update, the other two should not be checking this
if action_type == ActionTypes.Update:
animation_repeats = shortcode_params.get('repeat', 1).to_int()
position_move_time = shortcode_params.get('move_time', 0.0)
#move time is only supported on Update, but it isnt part of the animations so its separate
if action_type == ActionTypes.Update:
if typeof(shortcode_params.get('move_time', 0)) == TYPE_STRING:
position_move_time = shortcode_params.get('move_time', 0.0).to_float()
if typeof(shortcode_params.get('z-index', 0)) == TYPE_STRING:
z_index = shortcode_params.get('z-index', 0).to_int()
_update_zindex = true
mirrored = DialogicUtil.str_to_bool(shortcode_params.get('mirrored', 'false'))
extra_data = shortcode_params.get('extra_data', "")
func is_valid_event(string:String) -> bool:
if string.begins_with("Join ") or string.begins_with("Leave ") or string.begins_with("Update "):
return true
return false
################################################################################
## EDITOR REPRESENTATION
################################################################################
func build_event_editor() -> void:
add_header_edit('action_type', ValueType.FixedOptionSelector, '', '', {
'selector_options': [
{
'label': 'Join',
'value': ActionTypes.Join,
'icon': load("res://addons/dialogic/Editor/Images/Dropdown/join.svg")
},
{
'label': 'Leave',
'value': ActionTypes.Leave,
'icon': load("res://addons/dialogic/Editor/Images/Dropdown/leave.svg")
},
{
'label': 'Update',
'value': ActionTypes.Update,
'icon': load("res://addons/dialogic/Editor/Images/Dropdown/update.svg")
}
]
})
add_header_edit('_character_from_directory', ValueType.ComplexPicker, '', '',
{'empty_text' : 'Character',
'file_extension' : '.dch',
'suggestions_func' : get_character_suggestions,
'icon' : load("res://addons/dialogic/Editor/Images/Resources/character.svg")})
add_header_button('', _on_character_edit_pressed, 'Edit character', ["Edit", "EditorIcons"], 'character != null and _character_from_directory != "--All--"')
add_header_edit('portrait', ValueType.ComplexPicker, '', '',
{'empty_text' : 'Default',
'suggestions_func' : get_portrait_suggestions,
'icon' : load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")},
'character != null and !has_no_portraits() and action_type != %s' %ActionTypes.Leave)
add_header_edit('position', ValueType.Integer, ' at position', '', {},
'character != null and !has_no_portraits() and action_type != %s' %ActionTypes.Leave)
# Body
add_body_edit('animation_name', ValueType.ComplexPicker, 'Animation:', '',
{'suggestions_func' : get_animation_suggestions,
'editor_icon' : ["Animation", "EditorIcons"],
'placeholder' : 'Default',
'enable_pretty_name' : true},
'should_show_animation_options()')
add_body_edit('animation_length', ValueType.Float, 'Length:', '', {},
'should_show_animation_options() and !animation_name.is_empty()')
add_body_edit('animation_wait', ValueType.Bool, 'Wait for animation to finish:', '', {},
'should_show_animation_options() and !animation_name.is_empty()')
add_body_edit('animation_repeats', ValueType.Integer, 'Repeat:', '', {},
'should_show_animation_options() and !animation_name.is_empty() and action_type == %s)' %ActionTypes.Update)
add_body_edit('z_index', ValueType.Integer, 'portrait z-index:', "",{},
'action_type != %s' %ActionTypes.Leave)
add_body_edit('mirrored', ValueType.Bool, 'mirrored:', "",{},
'action_type != %s' %ActionTypes.Leave)
add_body_edit('position_move_time', ValueType.Float, 'Transiton time to change position:', '', {},
'action_type == %s' %ActionTypes.Update)
func should_show_animation_options() -> bool:
return (character != null and !character.portraits.is_empty()) or _character_from_directory == '--All--'
func has_no_portraits() -> bool:
return character and character.portraits.is_empty()
func get_character_suggestions(search_text:String) -> Dictionary:
var suggestions := {}
#override the previous _character_directory with the meta, specifically for searching otherwise new nodes wont work
_character_directory = Engine.get_meta('dialogic_character_directory')
var icon = load("res://addons/dialogic/Editor/Images/Resources/character.svg")
suggestions['(No one)'] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
if action_type == ActionTypes.Leave:
suggestions['ALL'] = {'value':'--All--', 'tooltip':'All currently joined characters leave', 'editor_icon':["GuiEllipsis", "EditorIcons"]}
for resource in _character_directory.keys():
suggestions[resource] = {'value': resource, 'tooltip': _character_directory[resource]['full_path'], 'icon': icon.duplicate()}
return suggestions
func get_portrait_suggestions(search_text:String) -> Dictionary:
var suggestions := {}
var icon = load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")
if action_type == ActionTypes.Update:
suggestions["Don't Change"] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
if action_type == ActionTypes.Join:
suggestions["Default portrait"] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
if character != null:
for portrait in character.portraits:
suggestions[portrait] = {'value':portrait, 'icon':icon.duplicate()}
return suggestions
func get_animation_suggestions(search_text:String) -> Dictionary:
var suggestions := {}
match action_type:
ActionTypes.Join, ActionTypes.Leave:
suggestions['Default'] = {'value':"", 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
ActionTypes.Update:
suggestions['None'] = {'value':"", 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
for anim in list_animations():
match action_type:
ActionTypes.Join:
if '_in' in anim.get_file():
suggestions[DialogicUtil.pretty_name(anim)] = {'value':anim, 'editor_icon':["Animation", "EditorIcons"]}
ActionTypes.Leave:
if '_out' in anim.get_file():
suggestions[DialogicUtil.pretty_name(anim)] = {'value':anim, 'editor_icon':["Animation", "EditorIcons"]}
ActionTypes.Update:
if not ('_in' in anim.get_file() or '_out' in anim.get_file()):
suggestions[DialogicUtil.pretty_name(anim)] = {'value':anim, 'editor_icon':["Animation", "EditorIcons"]}
continue
return suggestions
func list_animations() -> Array:
var list := DialogicUtil.listdir(get_script().resource_path.get_base_dir().path_join('DefaultAnimations'), true, false, true)
list.append_array(DialogicUtil.listdir(DialogicUtil.get_project_setting('dialogic/animations/custom_folder', 'res://addons/dialogic_additions/Animations'), true, false, true))
return list
func guess_animation_file(animation_name: String) -> String:
for file in list_animations():
if DialogicUtil.pretty_name(animation_name) == DialogicUtil.pretty_name(file):
return file
return animation_name
func _on_character_edit_pressed() -> void:
var editor_manager := _editor_node.find_parent('EditorsManager')
if editor_manager:
editor_manager.edit_resource(character)