NEON-28: fix F9 nav rebake coroutine + resync agent goal

pull/38/head
VinPropane 2026-04-10 23:13:00 -04:00
parent bb39a64ebd
commit 1ab3fc1dca
3 changed files with 19 additions and 6 deletions

View File

@ -72,6 +72,8 @@ func _on_move_rejected(reason_code: String) -> void:
## Dev smoke (NEON-28 occluder purge): **F9** frees `Obstacle` while testing; remove when no longer needed.
## After removing nav source geometry, we **rebake** — otherwise `NavigationAgent3D` keeps a stale map
## and click-to-move can stop working.
## Coroutine is started **directly** from input (not [method call_deferred]): deferred + [code]await[/code]
## often never resumes, so the rebake never finished and the nav map stayed stale.
func _unhandled_key_input(event: InputEvent) -> void:
if not event is InputEventKey:
return
@ -80,15 +82,18 @@ func _unhandled_key_input(event: InputEvent) -> void:
var obstacle := get_node_or_null("World/NavigationRegion3D/Obstacle")
if obstacle:
obstacle.queue_free()
call_deferred("_rebake_nav_mesh_after_obstacle_removed")
_rebake_nav_after_obstacle_removed_async()
func _rebake_nav_mesh_after_obstacle_removed() -> void:
func _rebake_nav_after_obstacle_removed_async() -> void:
await get_tree().process_frame
if not is_instance_valid(_nav_region):
return
_nav_region.bake_navigation_mesh(false)
# Let NavigationServer3D apply the rebaked mesh before repathing.
await get_tree().physics_frame
await get_tree().physics_frame
await get_tree().physics_frame
if not is_instance_valid(_player):
return
var na := _player.get_node_or_null("NavigationAgent3D") as NavigationAgent3D
if na != null:
na.set_target_position(_player.global_position)
_player.sync_navigation_agent_after_map_rebuild()

View File

@ -89,6 +89,14 @@ func clear_nav_goal() -> void:
_nav_agent.set_target_position(global_position)
## Repath on a new nav map (e.g. after [NavigationRegion3D] rebake when geometry is removed).
func sync_navigation_agent_after_map_rebuild() -> void:
if _has_walk_goal:
_nav_agent.set_target_position(_auth_walk_goal)
else:
_nav_agent.set_target_position(global_position)
func snap_to_server(world_pos: Vector3) -> void:
global_position = world_pos
velocity = Vector3.ZERO

View File

@ -106,7 +106,7 @@ Jira acceptance criteria (verbatim intent):
**Manual verification:** Main scene — zoom bands, occlusion fade, fixed yaw UX; spot-check Slice 2 behaviors (readable motion, occlusion active).
**Freed occluder (end-to-end, editor):** Position player so `Obstacle` is between camera and player (fade active), then `queue_free` the occluder. Expect **no** recurring script errors and **no** stuck transparent materials on other meshes. **`Obstacle` lives under `NavigationRegion3D`** — after removing it, **rebake** the nav mesh (see **`main.gd` F9** dev path: `bake_navigation_mesh(false)` + refresh `NavigationAgent3D` target) or click-to-move can stop until the next full scene reload. Complements static `occluder_override_key_is_valid` tests, which do not execute the full purge path in CI.
**Freed occluder (end-to-end, editor):** Position player so `Obstacle` is between camera and player (fade active), then `queue_free` the occluder. Expect **no** recurring script errors and **no** stuck transparent materials on other meshes. **`Obstacle` lives under `NavigationRegion3D`** — after removing it, **rebake** the nav mesh (see **`main.gd` F9**: `await` a frame after `queue_free`, then `bake_navigation_mesh(false)`, then a few physics frames, then **`Player.sync_navigation_agent_after_map_rebuild()`**) or click-to-move can stop. Do **not** drive that sequence with **`call_deferred` + `await`** on the same method — the coroutine may never resume. Complements static `occluder_override_key_is_valid` tests, which do not execute the full purge path in CI.
## Open questions / risks