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:
2026-07-05 17:21:17 +02:00
parent 4f9dc842fd
commit a8ae331f51
8 changed files with 238 additions and 10 deletions
+4
View File
@@ -26,6 +26,8 @@ var task_duration := 0.0
var task_progress := 0.0
var task_complete := true
var target_id: StringName = &""
var travel_target_position: Vector3
var has_travel_target := false
var last_task: StringName
var random_source: RandomNumberGenerator
var debug_logs := true
@@ -62,6 +64,7 @@ func die_from_starvation() -> void:
task_progress = 0.0
task_duration = 0.0
task_complete = true
has_travel_target = false
if debug_logs:
print("[SimNPC] ", npc_name, " died from starvation.")
@@ -85,6 +88,7 @@ func set_task(action_id: StringName, duration: float = -1.0) -> void:
task_progress = 0.0
task_complete = false
task_state = TASK_STATE_TRAVELING
has_travel_target = false
func start_working() -> void:
if task_state != TASK_STATE_TRAVELING: