Skip to content

Make Eternal Loom animation dynamic based on threads collected by the player - #2864

Merged
manuq merged 21 commits into
endlessm:mainfrom
Un-player0:main
Sep 14, 2026
Merged

manuq merged 21 commits into
endlessm:mainfrom
Un-player0:main

Conversation

@Un-player0

@Un-player0 Un-player0 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Adds an script that replaces loom_offering_animation.

This new version dynamically shows the number of threads the player collected in the current quest with it's corresponding color

Uses a Path2D to move the threads

Resolves: #1861

@Un-player0
Un-player0 requested a review from a team as a code owner September 9, 2026 03:05
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Test build no longer available.

@Un-player0

Copy link
Copy Markdown
Contributor Author

Short video fo how it should look

loom_animation_test.mp4

@manuq manuq left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is impressive work, already. Thanks! Here are some requests and suggestions.

The Path2D you have is made of straight lines:

Image

I think it would be nice to make it a circle. The curve of a Path2D is a Bezier curve so this is possible:

image

However, if following this advice and the one for sampling the points, you will have to add a PathFollow2D node and tween the progress, for positioning each thread.

const WORLD_MEMORY = preload("uid://5wscjc8yqqts")
const WORLD_SPIRIT = preload("uid://cepg1o3ihp055")

@export var _play_animation: bool = false:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I like how you made it very easy to test the animation from the editor with this. But please check the warning on this line. It says that the variable is not used. There is a better way: Adding an @export_tool_button, which adds a button to the Inspector. There are a number of examples in this repository already. For example, the Randomize button in townie.tscn

# Time between each animation_point
var timer: float = animation_time / (animation_points.size() * 2.8)
# Separation between threads
var separator: int = animation_points.size() / floor(thread_list.size())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since #1234 was merged, zero threads is a possible number. And the script will raise a zero division error. So you may want to skip this animation entirely for zero threads.

var arr: Array[InventoryItem]
for i in range(debug_thread_count):
var temp_item := InventoryItem.new()
temp_item.type = InventoryItem.ItemType.MEMORY

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to pick the item type randomly for better debugging.



func _loom_animation_play(thread_list: Array[InventoryItem]) -> void:
var animation_points := animation_path.curve.get_baked_points()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you made it so easy to test (thanks!) I tried with 20 threads and saw a single one being animated. I see now that this works with up to 12 threads: because the curve of your Path2D node has 12 points.

Making it work with more than 12 threads is not necessary, but I think there is a better way: you can pick points from the sampled curve:

	var animation_points: Array[Vector2]

	var curve_length := animation_path.curve.get_baked_length()
	var separation := curve_length / thread_list.size()
	for i in thread_list.size():
		var offset := separation * i
		var point := animation_path.curve.sample_baked(offset)
		animation_points.append(point)

Then it will work with 20+ threads!

Grabacion.de.pantalla.desde.2026-09-09.14-20-22.mp4

Comment on lines +52 to +60
match thread.type:
InventoryItem.ItemType.MEMORY:
sprite.texture = WORLD_MEMORY
InventoryItem.ItemType.IMAGINATION:
sprite.texture = WORLD_IMAGINATION
InventoryItem.ItemType.SPIRIT:
sprite.texture = WORLD_SPIRIT
_:
sprite.texture = WORLD_SPIRIT

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed! There is a helper function already in inventory_item. You can just use:

sprite.texture = thread.get_world_texture()

@Un-player0

Copy link
Copy Markdown
Contributor Author

However, if following this advice and the one for sampling the points, you will have to add a PathFollow2D node and tween the progress, for positioning each thread.

Took this into consideration and used the PathFollow2D to move every thread along the circumference

Should look like this

loom_test_2.mp4

@manuq manuq left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is impressive! Great work! Approving, please consider my suggestions. They are tiny details, but overal the new dynamic animation is fantastic.

Comment on lines +30 to +37
var arr: Array[InventoryItem]
for i in range(debug_thread_count):
var temp_item := InventoryItem.new()
var item_types := InventoryItem.ItemType.values()
item_types.remove_at(3) ## Remove the 'None' Item Type
temp_item.type = item_types.pick_random()
arr.append(temp_item)
_loom_animation_play(arr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is better to use descriptive variable names. Also item types will be the same on each for loop, so it should be outside. And the way you are removing ItemType.NONE is by index, which will fail as soon as that enum changes, so is not the best. I suggest for now using InventoryItem.COLORS_PER_TYPE.keys() or an explicit array. In the future we can consider adding a utility function.

Suggested change
var arr: Array[InventoryItem]
for i in range(debug_thread_count):
var temp_item := InventoryItem.new()
var item_types := InventoryItem.ItemType.values()
item_types.remove_at(3) ## Remove the 'None' Item Type
temp_item.type = item_types.pick_random()
arr.append(temp_item)
_loom_animation_play(arr)
var debug_items: Array[InventoryItem]
# TODO: InventoryItem needs a utility function to obtain all types except NONE.
var item_types := InventoryItem.COLORS_PER_TYPE.keys()
for i in range(debug_thread_count):
var item := InventoryItem.new()
item.type = item_types.pick_random()
debug_items.append(item)
_loom_animation_play(debug_items)


var counter: int = 0
for thread in thread_list:
var path_follow := PathFollow2D.new()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! Yes I see you need one PathFollow2D per thread.


animation_path.add_child(path_follow)

## Starting point for each thread

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a documentation comment, so it should have a single #.

Suggested change
## Starting point for each thread
# Starting point for each thread

## Starting point for each thread
path_follow.progress_ratio = counter * separation

## Time variation between threads

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Time variation between threads
# Time variation between threads


var tween := path_follow.create_tween()

## The first thread is the last to end

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## The first thread is the last to end
# The first thread is the last to end

counter = counter + 1


## Remove thread once animation finishes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Remove thread once animation finishes
# Remove thread once animation finishes

tween.parallel().tween_property(sprite, "modulate", Color(0, 0, 0, 0), 0.5)
tween.parallel().tween_property(sprite, "scale", Vector2(3, 3), 0.5)
tween.tween_callback(_thread_free.bind(sprite))
counter = counter + 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tend to have this shorter version everywhere else:

Suggested change
counter = counter + 1
counter += 1

tween.tween_property(sprite, "scale", Vector2(1, 1), 0.5)
## Loop around
tween.parallel().tween_property(
path_follow, "progress_ratio", 2.0, loop_time - time_variation * counter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so when counter is zero this property is tweened by loop_time seconds. That's why the first thread in the for loop is the one emitting animation_finished.

Comment on lines +601 to +219
position = Vector2(0, 39)
position = Vector2(-1, 39)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated, please revert it.

position = Vector2(0, 135)
position = Vector2(0, 85)
script = ExtResource("6_rle0d")
debug_thread_count = 7

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe commit the tscn file with the default amount for debug_thread_count.

@manuq
manuq merged commit 77c7630 into endlessm:main Sep 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make Eternal Loom animation dynamic based on collected threads

2 participants