feat: add navigation_failed signal with time-based stuck detection and simulation pipeline
NpcVisual: - Add navigation_failed(sim_id) signal distinct from arrived_at_target so the simulation layer can distinguish genuine arrival from failure - Replace integer stuck_counter with delta-based stuck_time and exported stuck_timeout (1.5s) for frame-rate-independent detection - Add path_request_id / path_pending mechanism to reject stale async path results when a new target is set or NPC dies - On empty path or stuck movement, emit navigation_failed instead of arriving; only emit arrived_at_target when truly close to target - Reduce arrival_distance back to 0.6 now that navigation_failure is properly signalled (no longer needed as a workaround) - clamp arrival_distance reference in apply_dead_visual_state SimNPC: - Remove retry_count (replaced by navigation_failed signal pathway) - Move last_task assignment from set_task() to task completion in SimulationManager so it reflects the task that actually finished SimulationManager: - Add notify_npc_navigation_failed(): release reservation, record last_task, send NPC to wander for 2s, emit task_changed signal - Add notify_npc_target_unavailable() as public alias of above - Guard extraction with node.reserved_by == npc.id to prevent stale/illegitimate extraction when reservation is lost - Guard fallback zone task completion (apply_npc_task) so it only fires for non-resource tasks (gather_food/wood with no target silently skip instead of applying zone values) - Remove retry_count safety net (fully replaced by signal pipeline) WorldViewManager: - Connect navigation_failed signal in spawn_npc_visual() - Skip target assignment for empty/idle/dead task states - When no resource node available for gather_food/wood, call notify_npc_target_unavailable() so NPC wanders instead of idling - Add _on_npc_visual_navigation_failed handler delegating to SimulationManager.notify_npc_navigation_failed() - Remove retry_count references
This commit is contained in:
@@ -71,6 +71,11 @@ func spawn_npc_visual(npc: SimNPC) -> void:
|
||||
else:
|
||||
push_error("WorldViewManager: NPCVisual has no arrived_at_target signal")
|
||||
|
||||
if visual.has_signal("navigation_failed"):
|
||||
visual.navigation_failed.connect(_on_npc_visual_navigation_failed)
|
||||
else:
|
||||
push_error("WorldViewManager: NPCVisual has no navigation_failed signal")
|
||||
|
||||
active_npc_visuals[npc.id] = visual
|
||||
debug_log("Spawned visual for %s at %s" % [npc.npc_name, npc.position])
|
||||
|
||||
@@ -83,18 +88,23 @@ func update_npc_targets() -> void:
|
||||
continue
|
||||
|
||||
var visual = active_npc_visuals[npc.id]
|
||||
var task = npc.current_task
|
||||
if task in ["", "idle", "dead"]:
|
||||
continue
|
||||
|
||||
var resource_pos = _try_get_resource_target(npc, npc.current_task)
|
||||
if resource_pos != null:
|
||||
visual.set_target_position(resource_pos)
|
||||
continue
|
||||
|
||||
var task = npc.current_task
|
||||
if task == "wander":
|
||||
var offset = Vector3(randf_range(-6.0, 6.0), 0.0, randf_range(-6.0, 6.0))
|
||||
visual.set_target_position(visual.global_position + offset)
|
||||
continue
|
||||
|
||||
if task in ["gather_food", "gather_wood"]:
|
||||
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
||||
simulation_manager.notify_npc_target_unavailable(npc.id)
|
||||
continue
|
||||
|
||||
var target = get_target_for_task(task)
|
||||
@@ -137,7 +147,7 @@ func _try_get_resource_target(npc: SimNPC, task: String) -> Variant:
|
||||
|
||||
var node := ResourceNode.find_available(action_id, npc.id, visual.global_position)
|
||||
if node == null:
|
||||
debug_log("No available resource node for %s, using zone fallback" % task)
|
||||
debug_log("No available resource node for %s" % task)
|
||||
return null
|
||||
|
||||
if not node.reserve(npc.id):
|
||||
@@ -145,7 +155,6 @@ func _try_get_resource_target(npc: SimNPC, task: String) -> Variant:
|
||||
return null
|
||||
|
||||
npc.target_id = node.node_id
|
||||
npc.retry_count = 0
|
||||
debug_log("%s reserved %s for %s" % [npc.npc_name, node.node_id, task])
|
||||
return node.interaction_point.global_position
|
||||
|
||||
@@ -155,6 +164,9 @@ func _on_npc_task_changed(npc: SimNPC, old_task: String, new_task: String) -> vo
|
||||
|
||||
if not active_npc_visuals.has(npc.id):
|
||||
return
|
||||
|
||||
if new_task in ["", "idle", "dead"]:
|
||||
return
|
||||
|
||||
var visual = active_npc_visuals[npc.id]
|
||||
var resource_pos = _try_get_resource_target(npc, new_task)
|
||||
@@ -168,6 +180,10 @@ func _on_npc_task_changed(npc: SimNPC, old_task: String, new_task: String) -> vo
|
||||
return
|
||||
|
||||
if new_task in ["gather_food", "gather_wood"]:
|
||||
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
||||
simulation_manager.notify_npc_target_unavailable(npc.id)
|
||||
else:
|
||||
push_error("WorldViewManager: SimulationManager cannot handle unavailable targets")
|
||||
return
|
||||
|
||||
var target := get_target_for_task(new_task)
|
||||
@@ -186,8 +202,19 @@ func _on_npc_visual_arrived(sim_id: int) -> void:
|
||||
simulation_manager.notify_npc_arrived(sim_id)
|
||||
else:
|
||||
push_error("WorldViewManager: SimulationManager has no notify_npc_arrived method")
|
||||
|
||||
|
||||
|
||||
func _on_npc_visual_navigation_failed(sim_id: int) -> void:
|
||||
debug_log("NPC visual navigation failed. sim_id=%s" % sim_id)
|
||||
|
||||
if simulation_manager == null:
|
||||
push_error("WorldViewManager: simulation_manager missing during navigation failure")
|
||||
return
|
||||
|
||||
if simulation_manager.has_method("notify_npc_navigation_failed"):
|
||||
simulation_manager.notify_npc_navigation_failed(sim_id)
|
||||
else:
|
||||
push_error("WorldViewManager: SimulationManager has no notify_npc_navigation_failed method")
|
||||
|
||||
func _on_npc_died(npc: SimNPC) -> void:
|
||||
if not active_npc_visuals.has(npc.id):
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user