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:
@@ -28,7 +28,6 @@ var task_duration := 0.0
|
||||
var task_progress := 0.0
|
||||
var task_complete := true
|
||||
var target_id: StringName = &""
|
||||
var retry_count := 0
|
||||
var last_task := ""
|
||||
|
||||
func _init(
|
||||
@@ -247,7 +246,6 @@ func calculate_task_score(
|
||||
return score
|
||||
|
||||
func set_task(task: String, duration: float) -> void:
|
||||
last_task = current_task
|
||||
current_task = task
|
||||
task_duration = duration
|
||||
task_progress = 0.0
|
||||
|
||||
@@ -87,21 +87,25 @@ func simulate_tick() -> void:
|
||||
|
||||
if npc.target_id != &"":
|
||||
var node := ResourceNode.get_by_id(npc.target_id)
|
||||
if node != null:
|
||||
if node != null and node.reserved_by == npc.id:
|
||||
var extracted := node.extract()
|
||||
if extracted > 0:
|
||||
village.apply_resource_delta(node.resource_id, extracted)
|
||||
if DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " extracted ", extracted, " ", node.resource_id, " from ", node.node_id)
|
||||
elif DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": target reservation was invalid")
|
||||
release_npc_reservation(npc.id)
|
||||
else:
|
||||
elif completed_task not in ["gather_food", "gather_wood"]:
|
||||
village.apply_npc_task(npc)
|
||||
elif DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": no resource target")
|
||||
|
||||
village_was_changed = true
|
||||
npc.retry_count = 0
|
||||
|
||||
npc.task_complete = true
|
||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||
npc.last_task = completed_task
|
||||
npc.current_task = "idle"
|
||||
|
||||
if needs_immediate_food_after_gather and village.food > 0:
|
||||
@@ -111,15 +115,6 @@ func simulate_tick() -> void:
|
||||
if DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " gathered food while starving and is going to eat immediately.")
|
||||
|
||||
if npc.task_state == SimNPC.TASK_STATE_TRAVELING and npc.target_id == &"":
|
||||
npc.retry_count += 1
|
||||
if npc.retry_count >= 5:
|
||||
if DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " stuck traveling without target for ", npc.retry_count, " attempts, replanning")
|
||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||
npc.current_task = ""
|
||||
npc.retry_count = 0
|
||||
|
||||
if DEBUG_LOGS:
|
||||
print(
|
||||
npc.npc_name,
|
||||
@@ -184,10 +179,7 @@ func notify_npc_arrived(npc_id: int) -> void:
|
||||
if node == null or not node.can_extract():
|
||||
if DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " arrived but target ", npc.target_id, " is unavailable, replanning")
|
||||
release_npc_reservation(npc.id)
|
||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||
npc.current_task = ""
|
||||
npc.retry_count += 1
|
||||
notify_npc_navigation_failed(npc.id)
|
||||
return
|
||||
|
||||
npc.start_working()
|
||||
@@ -196,6 +188,26 @@ func notify_npc_arrived(npc_id: int) -> void:
|
||||
print("[SimulationManager] ", npc.npc_name, " arrived and started working on: ", npc.current_task)
|
||||
return
|
||||
|
||||
func notify_npc_navigation_failed(npc_id: int) -> void:
|
||||
for npc in npcs:
|
||||
if npc.id != npc_id:
|
||||
continue
|
||||
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
|
||||
return
|
||||
|
||||
var failed_task := npc.current_task
|
||||
release_npc_reservation(npc.id)
|
||||
npc.last_task = failed_task
|
||||
npc.set_task("wander", 2.0)
|
||||
npc_task_changed.emit(npc, failed_task, npc.current_task)
|
||||
|
||||
if DEBUG_LOGS:
|
||||
print("[SimulationManager] ", npc.npc_name, " could not navigate for ", failed_task, " and is trying a wander target")
|
||||
return
|
||||
|
||||
func notify_npc_target_unavailable(npc_id: int) -> void:
|
||||
notify_npc_navigation_failed(npc_id)
|
||||
|
||||
func eat_food(amount: float) -> void:
|
||||
village.food -= amount
|
||||
village_changed.emit(village)
|
||||
|
||||
Reference in New Issue
Block a user