Skip to content

Conversation

@manuq
Copy link
Collaborator

@manuq manuq commented Oct 20, 2025

Resolves #1222

@github-actions
Copy link

Play this branch at https://endlessm.github.io/threadbare/branches/endlessm/void-indigestion.

(This launches the game from the start, not directly at the change(s) in this pull request.)

@manuq
Copy link
Collaborator Author

manuq commented Oct 20, 2025

This is a draft for now because:

  • The way it is using signals for communication is silly: In a level with multiple Void enemies, all of them will turn to ingesting at the same time for each consumed pile. Instead, void_layer.consume_cells(coords) should return an array of consumed nodes, changing the current return signature (which is bool).
  • I'm not sure if the time-based particle emission should replace the distance-based one, or what to do about it.

The pile of books is intended to be placeholder. Is made out of existing assets. Also it is pullable, but the way TileMapCover works is meant for static props (the _unconsumed_nodes property has an Array with their initial positions). So if the player move the pile of books to another place in the level, the Void can't get indigested.

@manuq manuq changed the title Making Void slow down while consuming piles of books Make Void slow down while consuming piles of books Oct 20, 2025
@manuq
Copy link
Collaborator Author

manuq commented Oct 20, 2025

I'm also appending a commit for the player that I'm testing locally. Seems a good candidate to impeding the player to be stuck after transpasing a collision shape.

@wjt
Copy link
Member

wjt commented Oct 21, 2025

The pile of books is intended to be placeholder. Is made out of existing assets. Also it is pullable, but the way TileMapCover works is meant for static props (the _unconsumed_nodes property has an Array with their initial positions). So if the player move the pile of books to another place in the level, the Void can't get indigested.

Oh, ouch. I wonder if an alternative could be to stipulate that only things that can be collided with can be covered, and then use collisions rather than looking up tilemap coordinates when deciding what to hide.

manuq added 3 commits October 21, 2025 11:06
Add a pile of books that has a float metadata "ingest_time" and is set
to 2.0. This will be used to stop the Void spreading enemy for that
amount of seconds.

For testing, this prop is also hookable and will move towards the
player, colliding with it. So the player could change its place or bring
it from other "islands" with the grappling hook.
In TileMapCover: Report the nodes as they are being consumed. And fix
the function documentation comment, previously it was also returning
true or false if nodes (not tiles) were consumed.

In the enemy new ingesting state, it does nothing more than waiting.
Transition to this state if the metadata of a consumed node says to be
ingested, and set the waiting to that amount of time.

Also emit particles when not the enemy is not moving. The previous
solution for emitting particles when not consuming stuff was
distance-based. Now if the enemy is standing in the new INGESTING state,
nothing would be displayed.
@manuq
Copy link
Collaborator Author

manuq commented Oct 21, 2025

The pile of books is intended to be placeholder. Is made out of existing assets. Also it is pullable, but the way TileMapCover works is meant for static props (the _unconsumed_nodes property has an Array with their initial positions). So if the player move the pile of books to another place in the level, the Void can't get indigested.

Oh, ouch. I wonder if an alternative could be to stipulate that only things that can be collided with can be covered, and then use collisions rather than looking up tilemap coordinates when deciding what to hide.

Could be, I'll check! For now the way to stop the Void for a little bit is to position the player so the pile of books is between the player and the void.

I guess if we go with this, we may need an arrow indicator pointing to the position that the void is, when it is off screen. I remember that we need that anyways for the magical threads, when they appear off screen (not for the Lore, but a StoryQuest can be designed to do it).

@manuq manuq marked this pull request as ready for review October 21, 2025 15:09
@manuq manuq requested a review from a team as a code owner October 21, 2025 15:09
## direct children of [member consumable_node_holders].
## Return true if any cells were consumed.
func consume_cells(cells: Array[Vector2i], immediate: bool = false) -> bool:
## Return true if any nodes were consumed.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I started doing a data class for returning here:

class ConsumedInfo:
    var has_consumed_cells: bool
    var consumed_nodes: Array[Node2D]

And then I realized that the docstring I added for this function is wrong. It has been always returning if nodes were consumed, not cells.

@manuq
Copy link
Collaborator Author

manuq commented Oct 21, 2025

I have appended a fixup after seeing errors for the Tween.finished signal trying to be connected multiple times.

Copy link
Member

@wjt wjt left a comment

Choose a reason for hiding this comment

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

I'm kind of on the fence about this. I like the idea but I find it hard to grab the props fast enough for it to help.

I tried designing a little zig-zag where you:

  • grapple right
  • grab books from the right
  • grapple down
  • grab books from below
  • grapple right

in the hope that this would make it easier to aim (you don't need to retarget to catch the books) but it didn't really work because unless you time it just right, the enemy just moves diagonally to follow you and doesn't eat the books.

Image

I wonder if it would work if the enemy had a detection circle around it (perhaps 3× the radius that it eats) where, if something tastier than storyweaver was in that circle, it would temporarily retarget to that, eat it, and then go back to chasing storyweaver?

Comment on lines 128 to +136
if cells:
set_cells_terrain_connect(cells, terrain_set, _terrain_id)

var consumed_nodes := [] as Array[Node2D]
for cell in cells:
consume(cell, immediate)
return true
return false
var nodes := consume(cell, immediate)
consumed_nodes.append_array(nodes)
return consumed_nodes
return []
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if cells:
set_cells_terrain_connect(cells, terrain_set, _terrain_id)
var consumed_nodes := [] as Array[Node2D]
for cell in cells:
consume(cell, immediate)
return true
return false
var nodes := consume(cell, immediate)
consumed_nodes.append_array(nodes)
return consumed_nodes
return []
var consumed_nodes: Array[Node2D]
if cells:
set_cells_terrain_connect(cells, terrain_set, _terrain_id)
for cell in cells:
var nodes := consume(cell, immediate)
consumed_nodes.append_array(nodes)
return consumed_nodes

Comment on lines 163 to +164
_consumed_nodes[coord] = nodes
return consumed_nodes
Copy link
Member

Choose a reason for hiding this comment

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

I think it's confusing to have a local variable with the same name (minus an underscore) as a class field.

return
return []

var consumed_nodes := [] as Array[Node2D]
Copy link
Member

Choose a reason for hiding this comment

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

As above I think you can spell this as:

Suggested change
var consumed_nodes := [] as Array[Node2D]
var consumed_nodes: Array[Node2D]

and I believe it gets initialised to empty array (not null)

Comment on lines +132 to +137
for node in consumed_nodes:
if node.has_meta(&"ingest_time"):
var previous_state := state
state = State.INGESTING
await get_tree().create_timer(node.get_meta(&"ingest_time")).timeout
state = previous_state
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for node in consumed_nodes:
if node.has_meta(&"ingest_time"):
var previous_state := state
state = State.INGESTING
await get_tree().create_timer(node.get_meta(&"ingest_time")).timeout
state = previous_state
var ingest_time := 0.0
for node in consumed_nodes:
ingest_time += node.get_meta(&"ingest_time", 0.0)
if ingest_time > 0:
var previous_state := state
state = State.INGESTING
await get_tree().create_timer(ingest_time).timeout
state = previous_state

Comment on lines +158 to +160
tween.finished.connect(
_on_consumed_node_tween_finished.bind(node), CONNECT_REFERENCE_COUNTED
)
Copy link
Member

Choose a reason for hiding this comment

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

You could connect to the tween's finish function just once with:

tween.finished.connect(_on_consumed_nodes_finished.bind(nodes))

and change its implementation to have a for loop?

@manuq manuq marked this pull request as draft October 22, 2025 16:23
@manuq
Copy link
Collaborator Author

manuq commented Oct 22, 2025

@wjt thanks for playing with this mechanic, great observations. I'll see if a detection circle would help here or otherwise we can drop the idea. I'm marking this as draft so we don't merge the exploration by mistake.

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.

Try making void slow down while consuming certain props

2 participants