Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Layered Portraits #2119

Merged
merged 56 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c4a6c67
Add Layered Portrait.
CakeVR Feb 27, 2024
b23f3ca
Update extra when using `update` Character event.
CakeVR Feb 27, 2024
5498601
Silence warning.
CakeVR Feb 27, 2024
dd10cd1
Emit real value file path on `value_changed`.
CakeVR Feb 27, 2024
059556c
Add support for Layered Portraits in the editor.
CakeVR Feb 27, 2024
8398802
Silently set scene picker current values.
CakeVR Feb 27, 2024
96ac212
Add mirroring support and fix transforms of child nodes breaking cont…
CakeVR Feb 28, 2024
1aab3a3
Adjust comments.
CakeVR Feb 28, 2024
98f3108
Revert "Add support for Layered Portraits in the editor."
CakeVR Feb 28, 2024
84d3aeb
Fix position calculation.
CakeVR Mar 3, 2024
631ab98
Improve position system.
CakeVR Mar 3, 2024
754f157
Fix typo.
CakeVR Mar 4, 2024
23ba2c5
Fix preview when switching scenes.
CakeVR Mar 4, 2024
e8d8a11
Fix portrait updating.
CakeVR Mar 4, 2024
5a6dda1
Fix `Sprite2D` logic check.
CakeVR Mar 6, 2024
dd3b3e4
Hide all other nodes.
CakeVR Mar 6, 2024
564f0ec
Apply some code improvements.
CakeVR Mar 6, 2024
79f9656
Fix command type matching.
CakeVR Mar 7, 2024
b83a366
Fix skipping transform on character update.
CakeVR Mar 9, 2024
dc07765
Update code of `DialogicAnimation`.
CakeVR Mar 9, 2024
8834719
Rework removal portrait scenes.
CakeVR Mar 9, 2024
e743897
Update a few portrait animations.
CakeVR Mar 9, 2024
5f2195b
Add modulation property method to `DialogicAnimation`.
CakeVR Mar 10, 2024
6bf699c
Add option to reverse animation.
CakeVR Mar 10, 2024
6e202f4
Add `portrait_animating` signal.
CakeVR Mar 10, 2024
5b54a43
Handle `portrait_animating`.
CakeVR Mar 10, 2024
03f0b73
Add `is_silent` and `is_reversed` to `_animate_portrait`.
CakeVR Mar 10, 2024
a1de211
Fix wording.
CakeVR Mar 10, 2024
4c62f9b
Use the reversed `Fade In` character animation.
CakeVR Mar 10, 2024
7ba1ce9
Implement `is_reversed` for `Fade In` and `Fade In Up`.
CakeVR Mar 10, 2024
d0a2fbc
Fix incorrect typing.
CakeVR Mar 10, 2024
c6fb6c3
Set `z_index` higher to let removing portraits appear above.
CakeVR Mar 10, 2024
e793640
Add default removal of old sprites if no animation was set.
CakeVR Mar 10, 2024
ac51e2f
Add Default Cross-Fade Settings to the Editor.
CakeVR Mar 10, 2024
5e628b1
Add support for `_in_out` animations.
CakeVR Mar 11, 2024
5f10093
Remove characters with an animation.
CakeVR Mar 12, 2024
384b935
Replace fallback animation.
CakeVR Mar 12, 2024
609a100
Fix awaiting animations before Dialogic can continue if characters le…
CakeVR Mar 12, 2024
5573d12
Add support for differently sized layers.
CakeVR Mar 13, 2024
0f19f82
Add support for more types in layered portraits.
CakeVR Mar 13, 2024
fdad0ac
Add support for special resources ending on `in` or `out`.
CakeVR Mar 14, 2024
3d154e3
Add fixes to the positioning and coverage rect.
CakeVR Mar 14, 2024
cced075
Fix mirror logic.
CakeVR Mar 16, 2024
70d2717
Use latest portrait node for all operations.
CakeVR Mar 19, 2024
6e78c47
Make all transition animations reversible.
CakeVR Mar 21, 2024
4505a21
Merge branch 'dialogic-godot:main' into layered-portrait
CakeVR Apr 6, 2024
2f99c8c
Fix support of `Node2D`.
CakeVR Apr 8, 2024
83875fc
Prevent playing Transition Animations on Portrait Updates.
CakeVR Apr 10, 2024
949a567
Add Documentation about identifying Transition Animation.
CakeVR Apr 10, 2024
21016ef
Guard against more Transition Name Possibilities.
CakeVR Apr 10, 2024
2c89fbc
Add Unit Tests for Portrait Animations.
CakeVR Apr 14, 2024
7db768e
Fix default animation name order.
CakeVR Apr 18, 2024
9c89427
Fix animation name fallback on Character Leave.
CakeVR Apr 18, 2024
2ce4c9b
Simplify finding fallback name of Character Leave.
CakeVR Apr 18, 2024
493a0ae
Access the correct `DialogicCharacter` node.
CakeVR Apr 19, 2024
19443e9
Remove older portraits if a new portrait gets instanced.
CakeVR Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -110,21 +110,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
34 changes: 26 additions & 8 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 @@ -536,79 +537,96 @@ func report_name_change(item:TreeItem) -> void:
########### PREVIEW ############################################################

#region Preview
func update_preview(force:=false) -> void:
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 FileAccess.file_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
Loading