feat: synchronize authoritative npc position
- NpcVisual emits position_changed signal during _physics_process - SimulationManager.synchronize_npc_position() writes active position into SimNPC state on movement, arrival, and navigation failure - WorldViewManager forwards position_changed and syncs before despawn/reload - SimNPC stores travel_target_position and has_travel_target as versioned state; NPCStateRecord v2 persists them with v1 migration - WorldViewManager.spawn_npc_visual() resumes travel for traveling NPCs via SimulationManager.request_current_travel() - WorldViewManager.despawn_npc_visual() syncs position before removal - WorldViewManager.reload_npc_visual() spawns a visual from persisted state without rerolling target selection - tests/npc_visual_lifecycle_test.gd proves unload/reload preserves action, target, reservation, position, RNG state, and checksum - simulation_state_serialization_test adds legacy v1 NPC migration test that does not invent an active travel target
This commit is contained in:
@@ -182,6 +182,7 @@ func simulate_tick() -> void:
|
||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||
npc.last_task = completed_task
|
||||
npc.current_task = SimulationIds.ACTION_IDLE
|
||||
npc.has_travel_target = false
|
||||
|
||||
if needs_immediate_food_after_gather and village.food > 0:
|
||||
npc.set_task(SimulationIds.ACTION_EAT, 1.0)
|
||||
@@ -262,6 +263,7 @@ func notify_npc_arrived(npc_id: int) -> void:
|
||||
notify_npc_navigation_failed(npc.id)
|
||||
return
|
||||
|
||||
npc.has_travel_target = false
|
||||
npc.start_working()
|
||||
|
||||
if debug_logs:
|
||||
@@ -308,10 +310,32 @@ func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||
notify_npc_target_unavailable(npc.id)
|
||||
return false
|
||||
npc.target_id = StringName(result.get("target_id", ""))
|
||||
npc_travel_requested.emit(npc, result["position"])
|
||||
npc.travel_target_position = result["position"]
|
||||
npc.has_travel_target = true
|
||||
npc_travel_requested.emit(npc, npc.travel_target_position)
|
||||
return true
|
||||
return false
|
||||
|
||||
func request_current_travel(npc_id: int) -> bool:
|
||||
for npc in npcs:
|
||||
if npc.id != npc_id:
|
||||
continue
|
||||
if npc.task_state != SimNPC.TASK_STATE_TRAVELING:
|
||||
return false
|
||||
if npc.has_travel_target:
|
||||
npc_travel_requested.emit(npc, npc.travel_target_position)
|
||||
else:
|
||||
npc_target_requested.emit(npc)
|
||||
return true
|
||||
return false
|
||||
|
||||
func synchronize_npc_position(npc_id: int, active_position: Vector3) -> bool:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
npc.position = active_position
|
||||
return true
|
||||
return false
|
||||
|
||||
func register_loaded_resource_nodes() -> void:
|
||||
for node in ResourceNode.get_all():
|
||||
register_resource_node(node)
|
||||
@@ -444,6 +468,12 @@ func get_state_snapshot() -> Dictionary:
|
||||
"task_progress": npc.task_progress,
|
||||
"target_id": String(npc.target_id),
|
||||
"position": [npc.position.x, npc.position.y, npc.position.z],
|
||||
"travel_target_position": [
|
||||
npc.travel_target_position.x,
|
||||
npc.travel_target_position.y,
|
||||
npc.travel_target_position.z
|
||||
],
|
||||
"has_travel_target": npc.has_travel_target,
|
||||
"starvation_ticks": npc.starvation_ticks,
|
||||
"is_dead": npc.is_dead
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user