Tile actions are not executing correctly after avatar movements

Godot Version

4.2.1

Question

I’m building a grid-based farming game in Godot where the player selects a tool (seed, water, till, harvest), taps a tile, and an avatar moves to the tile before the action executes. The problem is: after the avatar reaches the tile, the correct action does not consistently execute. Tools appear to be selected properly before movement, but once the avatar arrives and run_pending_action() is called, nothing happens — no planting, watering, or tilling. If I remove the avatar movement delay and execute actions immediately on click, everything works as expected. I suspect the issue lies in how or when the tool and crop are stored, or how run_pending_action() is triggered, but haven’t been able to resolve it. I’m using Godot 4.2.1.

Scripts

```
extends Node2D

var crop_id = ""
var required_water = 0
var grow_time = 0
var water_count = 0
var stage = ""
var is_planted = false
var is_watered = false
var growth_stage = 0
const MAX_GROWTH_STAGE = 3
var grow_timer = null
var growth_time = 5.0
var pending_action = ""
var selected_tool = ""
var pending_tool = ""
var pending_crop_id = ""

func _ready():
	$ClickArea.connect("input_event", Callable(self, "_on_click"))

func _on_click(viewport, event, shape_idx):
	if event is InputEventMouseButton and event.pressed:
		var avatar = get_node("/root/Main/Avatar")
		
		avatar.move_to_tile(global_position, self)

		var tool = ToolManager.get_tool()
		var crop_id = ToolManager.get_selected_crop()

		if tool == "seed" and not is_planted:
			pending_action = "plant"
			pending_crop_id = crop_id
		elif tool == "water" and is_planted and not is_watered:
			pending_action = "water"
		elif tool == "till" and not is_planted and growth_stage == 0:
			pending_action = "till"
		elif growth_stage == MAX_GROWTH_STAGE:
			pending_action = "harvest"


func run_pending_action():
	match pending_action:
		"plant":
			var crop_data = CropRegistry.get_crop(crop_id)
			_plant_seed(crop_data, crop_id)
		"water":
			if ToolManager.use_water():
				is_watered = true
				_water_tile()
		"till":
			_till_soil()
		"harvest":
			_harvest()

	pending_action = ""
	pending_crop_id = ""


func _plant_seed(crop: Dictionary, crop_id_val: String):
	is_planted = true
	is_watered = false
	growth_stage = 0
	crop_id = crop_id_val

	required_water = crop.get("water_needed", 1)
	grow_time = crop.get("grow_time", 5.0)
	water_count = 0

	stage = "growing"
	_update_visual()



func _water_tile():
	_start_growth_timer()
	_update_visual()

func _start_growth_timer():
	grow_timer = Timer.new()
	grow_timer.wait_time = growth_time
	grow_timer.one_shot = true
	grow_timer.connect("timeout", Callable(self, "_on_growth_timer_timeout"))
	add_child(grow_timer)
	grow_timer.start()

func _on_growth_timer_timeout():
	if growth_stage < MAX_GROWTH_STAGE:
		growth_stage += 1
		_update_visual()

		# Reset timer for next stage
		grow_timer.queue_free()
		_start_growth_timer()
	else:
		print("Fully grown!")
		
func _harvest():
	get_node("/root/Main/InventoryManager").add_crop(crop_id)
	reset_tile()
	print("Harvested plant at stage:", growth_stage)

	is_planted = false
	is_watered = false
	growth_stage = 0

	if grow_timer:
		grow_timer.stop()
		grow_timer.queue_free()
		grow_timer = null

	$SoilSprite.modulate = Color(0.6, 0.6, 0.6)  # Grey or dull brown



func _till_soil():
	print("Soil tilled and ready to plant.")

	is_planted = false
	is_watered = false
	growth_stage = 0

	if grow_timer:
		grow_timer.stop()
		grow_timer.queue_free()
		grow_timer = null

	# Force re-visualization
	_update_visual()


func _update_visual():
	match growth_stage:
		0:
			if is_planted and not is_watered:
				$SoilSprite.modulate = Color(1.0, 0.85, 0.4)  # Dry golden yellow
			elif is_planted and is_watered:
				$SoilSprite.modulate = Color(0.4, 0.9, 0.4)  # Healthy green
			else:
				$SoilSprite.modulate = Color(1, 1, 1)  # Fresh tilled

		1:
			$SoilSprite.modulate = Color(0.6, 1.0, 0.6)  # Sprout
		2:
			$SoilSprite.modulate = Color(1.0, 0.9, 0.6)  # Bloom
		3:
			$SoilSprite.modulate = Color(1.0, 0.8, 1.0)  # Fruit
			
func reset_tile():
	var sprite = $Sprite  
	if sprite:
		sprite.modulate = Color(1, 1, 1)
	else:
		print("Sprite node not found in tile.")
```

Avatar script 

``` extends Node2D

@onready var animated_sprite = $AnimatedSprite2D
var speed = 100  # pixels per second
var target_tile = null

func move_to_tile(target_position: Vector2, tile_ref = null):
	target_tile = tile_ref
	animated_sprite.play("walk")

	var tween = create_tween()
	tween.tween_property(self, "position", target_position, position.distance_to(target_position) / speed)
	tween.tween_callback(Callable(self, "_on_reach_destination"))


func _on_reach_destination():
	animated_sprite.play("idle")

	if target_tile:
		target_tile.run_pending_action()
		target_tile = null
```

Might try to help here without slamming the entire code;
try this new experiment:
Your Avatar script;

func _on_reach_destination():

	if target_tile:
		target_tile.run_pending_action()
		target_tile = null
	else:
		animated_sprite.play("idle")

I will give this a shot!

Please feel free to tear my code apart lol, I am very new and want to learn where I can improve!

I don’t immediately see any issues, although it might be easier to read the code if a bit more of the responsibility for the player’s actions was on the Avatar, not each individual tile. I’m not sure why each tile needs to know its own pending action; it would seem more natural to have the Avatar remember what its next action should be.

I’d start debugging this by putting print statements or breakpoints in _on_reach_destination() and run_pending_action() to figure out how far it’s getting before doing something unexpected.

1 Like

Caused the avatar to stay in its “move” animation, instead of switching to “idle” animation, still no action for tool. Means that the issue is happening after this function?