Skip to content

Commit

Permalink
Add Layered Portraits, Reversable Portrait Animations, Portrait Fade …
Browse files Browse the repository at this point in the history
…(WIP) and more (dialogic-godot#2119)

- Adds a Layered Portrait script, that can be used to create custom layered portrait scenes

- Simplifies portrait animations, by making them reversable (removing the need for most "Out" animations)

- Implements an initial version of a portrait-cross-animate feature
  • Loading branch information
CakeVR authored May 25, 2024
1 parent bb762c1 commit 1bf165b
Show file tree
Hide file tree
Showing 41 changed files with 1,112 additions and 398 deletions.
40 changes: 40 additions & 0 deletions Tests/Unit/guess_special_resource_test.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
extends GdUnitTestSuite

## Check if transition animations can be accessed with "in", "out, "in out"
## as space-delimited prefix.
func test_fade_in_animation_paths() -> void:
const TYPE := "PortraitAnimation"
var fade_in_1 := DialogicResourceUtil.guess_special_resource(TYPE, "fade in", "")
var fade_in_2 := DialogicResourceUtil.guess_special_resource(TYPE, "fade in out", "")
var fade_in_3 := DialogicResourceUtil.guess_special_resource(TYPE, "fade out", "")

var is_any_fade_in_empty := fade_in_1.is_empty() or fade_in_2.is_empty() or fade_in_3.is_empty()
assert(is_any_fade_in_empty == false, "Fade In/Out animations are empty.")

var are_all_fade_in_equal := fade_in_1 == fade_in_2 and fade_in_2 == fade_in_3
assert(are_all_fade_in_equal == true, "Fade In/Out animations returned different paths.")


## Test if invalid animation paths will return empty strings.
func test_invalid_animation_path() -> void:
const TYPE := "PortraitAnimation"
var invalid_animation_1 := DialogicResourceUtil.guess_special_resource(TYPE, "fade i", "")
assert(invalid_animation_1.is_empty() == true, "Invalid animation 1's path is not empty.")


var invalid_animation_2 := DialogicResourceUtil.guess_special_resource(TYPE, "fade", "")
assert(invalid_animation_2.is_empty() == true, "Invalid animation 2's path is not empty.")


## Test if invalid types will return empty strings.
func test_invalid_type_path() -> void:
const INVALID_TYPE := "Portait Animation"
var invalid_animation := DialogicResourceUtil.guess_special_resource(INVALID_TYPE, "fade in", "")
assert(invalid_animation.is_empty() == true, "Invalid animation 1's path is not empty.")

const VALID_TYPE := "PortraitAnimation"
var valid_animation_path := DialogicResourceUtil.guess_special_resource(VALID_TYPE, "fade in", "")
assert(valid_animation_path.is_empty() == false, "Valids animation's path is empty.")

assert(not invalid_animation == valid_animation_path, "Valid and invalid animation paths are equal.")

24 changes: 21 additions & 3 deletions addons/dialogic/Core/DialogicResourceUtil.gd
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,32 @@ static func list_special_resources_of_type(type:String) -> Array:
return special_resources.filter(func(x:Dictionary): return type == x.get('type','')).map(func(x:Dictionary): return x.get('path', ''))


static func guess_special_resource(type:String, name:String, default:="") -> String:
static func guess_special_resource(type: String, name: String, default := "") -> String:
if special_resources.is_empty():
update_special_resources()

if name.begins_with('res://'):
return name
for path in list_special_resources_of_type(type):
if DialogicUtil.pretty_name(path).to_lower() == name.to_lower():

for path: String in list_special_resources_of_type(type):
var pretty_path := DialogicUtil.pretty_name(path).to_lower()
var pretty_name := name.to_lower()

if pretty_path == pretty_name:
return path

elif pretty_name.ends_with(" in"):
pretty_name = pretty_name + " out"

if pretty_path == pretty_name:
return path

elif pretty_name.ends_with(" out"):
pretty_name = pretty_name.replace("out", "in out")

if pretty_path == pretty_name:
return path

return default

#endregion
Expand Down
39 changes: 31 additions & 8 deletions addons/dialogic/Core/DialogicUtil.gd
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,44 @@ static func get_indexers(include_custom := true, force_reload := false) -> Array


enum AnimationType {ALL, IN, OUT, ACTION}
static func get_portrait_animation_scripts(type:=AnimationType.ALL, include_custom:=true) -> Array:



static func get_portrait_animation_scripts(type := AnimationType.ALL, include_custom := true) -> Array:
var animations := DialogicResourceUtil.list_special_resources_of_type("PortraitAnimation")
const CROSS_ANIMATION := "_in_out"
const OUT_ANIMATION := "_out"
const IN_ANIMATION := "_in"

return animations.filter(
func(script):
if type == AnimationType.ALL: return true;
if type == AnimationType.IN: return '_in' in script;
if type == AnimationType.OUT: return '_out' in script;
if type == AnimationType.ACTION: return not ('_in' in script or '_out' in script))
func(script: String) -> bool:
match (type):
AnimationType.ALL:
return true

AnimationType.IN:
return IN_ANIMATION in script or CROSS_ANIMATION in script

AnimationType.OUT:
return OUT_ANIMATION in script or CROSS_ANIMATION in script

static func pretty_name(script:String) -> String:
var _name := script.get_file().trim_suffix("."+script.get_extension())
# All animations that are not IN or OUT.
# Extra check for CROSS animations to prevent parsing parts
# of the name as an IN or OUT animation.
AnimationType.ACTION:
return CROSS_ANIMATION in script or not (IN_ANIMATION in script or OUT_ANIMATION in script)

_:
return false
)


## Turns a [param file_path] from `some_file.png` to `Some File`.
static func pretty_name(file_path: String) -> String:
var _name := file_path.get_file().trim_suffix("." + file_path.get_extension())
_name = _name.replace('_', ' ')
_name = _name.capitalize()

return _name


Expand Down
32 changes: 25 additions & 7 deletions addons/dialogic/Editor/CharacterEditor/character_editor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ signal portrait_selected()

# Current state
var loading := false
var current_previewed_scene = null
var current_previewed_scene: Variant = null
var current_scene_path: String = ""

# References
var selected_item: TreeItem
Expand Down Expand Up @@ -540,77 +541,94 @@ func report_name_change(item: TreeItem) -> void:
#region Preview
func update_preview(force := false) -> void:
%ScenePreviewWarning.hide()

if selected_item and is_instance_valid(selected_item) and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
%PreviewLabel.text = 'Preview of "'+%PortraitTree.get_full_item_name(selected_item)+'"'

var current_portrait_data: Dictionary = selected_item.get_metadata(0)

if not force and current_previewed_scene != null \
and current_previewed_scene.get_meta('path', '') == current_portrait_data.get('scene') \
and scene_file_path == current_portrait_data.get('scene') \
and current_previewed_scene.has_method('_should_do_portrait_update') \
and is_instance_valid(current_previewed_scene.get_script()) \
and current_previewed_scene._should_do_portrait_update(current_resource, selected_item.get_text(0)):
pass # we keep the same scene
# We keep the same scene.
pass
else:

for node in %RealPreviewPivot.get_children():
node.queue_free()

current_previewed_scene = null
current_scene_path = ""

var scene_path := def_portrait_path
if not current_portrait_data.get('scene', '').is_empty():
scene_path = current_portrait_data.get('scene')

if ResourceLoader.exists(scene_path):
current_previewed_scene = load(scene_path).instantiate()
current_scene_path = scene_path

if current_previewed_scene:
if not current_previewed_scene == null:
%RealPreviewPivot.add_child(current_previewed_scene)

if current_previewed_scene != null:
if not current_previewed_scene == null:
var scene: Node = current_previewed_scene

scene.show_behind_parent = true
DialogicUtil.apply_scene_export_overrides(scene, current_portrait_data.get('export_overrides', {}))

var mirror: bool = current_portrait_data.get('mirror', false) != current_resource.mirror
var scale: float = current_portrait_data.get('scale', 1) * current_resource.scale

if current_portrait_data.get('ignore_char_scale', false):
scale = current_portrait_data.get('scale', 1)

var offset: Vector2 = current_portrait_data.get('offset', Vector2()) + current_resource.offset

if is_instance_valid(scene.get_script()) and scene.script.is_tool():

if scene.has_method('_update_portrait'):
## Create a fake duplicate resource that has all the portrait changes applied already
var preview_character := current_resource.duplicate()
preview_character.portraits = get_updated_portrait_dict()
scene._update_portrait(preview_character, %PortraitTree.get_full_item_name(selected_item))

if scene.has_method('_set_mirror'):
scene._set_mirror(mirror)

if !%FitPreview_Toggle.button_pressed:
scene.position = Vector2() + offset
scene.scale = Vector2(1,1)*scale
else:
if is_instance_valid(scene.get_script()) and scene.script.is_tool() and scene.has_method('_get_covered_rect'):
var rect: Rect2= scene._get_covered_rect()

if not scene.get_script() == null and scene.script.is_tool() and scene.has_method('_get_covered_rect'):
var rect: Rect2 = scene._get_covered_rect()
var available_rect: Rect2 = %FullPreviewAvailableRect.get_rect()
scene.scale = Vector2(1,1) * min(available_rect.size.x/rect.size.x, available_rect.size.y/rect.size.y)
%RealPreviewPivot.position = (rect.position)*-1*scene.scale
%RealPreviewPivot.position.x = %FullPreviewAvailableRect.size.x/2
scene.position = Vector2()

else:
%ScenePreviewWarning.show()
else:
%PreviewLabel.text = 'Nothing to preview'

for child in %PortraitSettingsSection.get_children():

if child is DialogicCharacterEditorPortraitSection:
child._recheck(current_portrait_data)

else:
%PreviewLabel.text = 'No portrait to preview.'

for node in %RealPreviewPivot.get_children():
node.queue_free()

current_previewed_scene = null
current_scene_path = ""


func _on_some_resource_saved(file:Variant) -> void:
Expand Down
36 changes: 24 additions & 12 deletions addons/dialogic/Editor/Events/Fields/field_file.gd
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ func _load_display_info(info:Dictionary) -> void:
placeholder = info.get('placeholder', '')
resource_icon = info.get('icon', null)
await ready

if resource_icon == null and info.has('editor_icon'):
resource_icon = callv('get_theme_icon', info.editor_icon)


func _set_value(value:Variant) -> void:
func _set_value(value: Variant) -> void:
current_value = value
var text := value
var text: String = value

if file_mode != EditorFileDialog.FILE_MODE_OPEN_DIR:
text = value.get_file()
%Field.tooltip_text = value
Expand All @@ -70,9 +72,11 @@ func _set_value(value:Variant) -> void:
%Field.custom_minimum_size.x = 0
%Field.expand_to_text_length = true

%Field.text = text
if not %Field.text == text:
value_changed.emit(property_name, current_value)
%Field.text = text

%ClearButton.visible = !value.is_empty() and !hide_reset
%ClearButton.visible = not value.is_empty() and not hide_reset


#endregion
Expand All @@ -87,41 +91,49 @@ func _on_OpenButton_pressed() -> void:

func _on_file_dialog_selected(path:String) -> void:
_set_value(path)
emit_signal("value_changed", property_name, path)
value_changed.emit(property_name, path)


func clear_path() -> void:
_set_value("")
emit_signal("value_changed", property_name, "")
value_changed.emit(property_name)

#endregion


#region DRAG AND DROP
################################################################################

func _can_drop_data_fw(at_position: Vector2, data: Variant) -> bool:
func _can_drop_data_fw(_at_position: Vector2, data: Variant) -> bool:
if typeof(data) == TYPE_DICTIONARY and data.has('files') and len(data.files) == 1:

if file_filter:

if '*.'+data.files[0].get_extension() in file_filter:
return true

else: return true

return false

func _drop_data_fw(at_position: Vector2, data: Variant) -> void:
_on_file_dialog_selected(data.files[0])

func _drop_data_fw(_at_position: Vector2, data: Variant) -> void:
var file: String = data.files[0]
_on_file_dialog_selected(file)

#endregion


#region VISUALS FOR FOCUS
################################################################################

func _on_field_focus_entered():
func _on_field_focus_entered() -> void:
$FocusStyle.show()

func _on_field_focus_exited():

func _on_field_focus_exited() -> void:
$FocusStyle.hide()
_on_file_dialog_selected(%Field.text)
var field_text: String = %Field.text
_on_file_dialog_selected(field_text)

#endregion
13 changes: 0 additions & 13 deletions addons/dialogic/Modules/Character/DefaultAnimations/bounce_in.gd

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
extends DialogicAnimation


func animate() -> void:
var tween := (node.create_tween() as Tween)

var end_scale: Vector2 = node.scale
var end_modulate_alpha := 1.0
var modulation_property := get_modulation_property()

if is_reversed:
end_scale = Vector2(0, 0)
end_modulate_alpha = 0.0

else:
node.scale = Vector2(0, 0)
var original_modulation: Color = node.get(modulation_property)
original_modulation.a = 0.0
node.set(modulation_property, original_modulation)


tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_SINE)
tween.set_parallel()

(tween.tween_property(node, "scale", end_scale, time)
.set_trans(Tween.TRANS_SPRING)
.set_ease(Tween.EASE_OUT))
tween.tween_property(node, modulation_property + ":a", end_modulate_alpha, time)

await tween.finished
finished_once.emit()
15 changes: 0 additions & 15 deletions addons/dialogic/Modules/Character/DefaultAnimations/bounce_out.gd

This file was deleted.

Loading

0 comments on commit 1bf165b

Please sign in to comment.