extends Node const DEBUG_LOGS := true @export var npc_visual_scene: PackedScene @export var simulation_manager: Node @export var active_npcs_parent: Node3D @export var guard_zone: Marker3D @export var study_zone: Marker3D @export var rest_zone: Marker3D @export var food_zone: Marker3D var active_npc_visuals := {} func debug_log(message: String) -> void: if DEBUG_LOGS: print("[WorldViewManager] ", message) func _ready() -> void: call_deferred("initialize_world_view") func initialize_world_view() -> void: await get_tree().physics_frame spawn_initial_npcs() if simulation_manager.has_signal("npc_task_changed"): simulation_manager.npc_task_changed.connect(_on_npc_task_changed) else: push_error("WorldViewManager: SimulationManager has no npc_task_changed signal") if simulation_manager.has_signal("npc_died"): simulation_manager.npc_died.connect(_on_npc_died) else: push_error("WorldViewManager: SimulationManager has no npc_died signal") update_npc_targets() func spawn_initial_npcs() -> void: if simulation_manager == null: push_error("WorldViewManager: simulation_manager is missing") return if npc_visual_scene == null: push_error("WorldViewManager: npc_visual_scene is missing") return if active_npcs_parent == null: push_error("WorldViewManager: active_npcs_parent is missing") return debug_log("Spawning NPC visuals. Count: %s" % simulation_manager.npcs.size()) for npc in simulation_manager.npcs: spawn_npc_visual(npc) func spawn_npc_visual(npc: SimNPC) -> void: if active_npc_visuals.has(npc.id): return var visual = npc_visual_scene.instantiate() active_npcs_parent.add_child(visual) visual.global_position = npc.position visual.setup_from_sim(npc) if visual.has_signal("arrived_at_target"): visual.arrived_at_target.connect(_on_npc_visual_arrived) 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]) func update_npc_targets() -> void: if simulation_manager == null: return for npc in simulation_manager.npcs: if not active_npc_visuals.has(npc.id): continue var visual = active_npc_visuals[npc.id] var task = npc.current_task if task in [ &"", SimulationIds.ACTION_IDLE, SimulationIds.ACTION_DEAD ]: continue var resource_pos = _try_get_resource_target(npc, npc.current_task) if resource_pos != null: visual.set_target_position(resource_pos) continue if task == SimulationIds.ACTION_WANDER: var offset := _get_wander_offset(npc.id) visual.set_target_position(visual.global_position + offset) continue var definition := SimulationDefinitions.get_action(task) if ( definition != null and definition.target_type == SimulationIds.TARGET_RESOURCE ): 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) if target == null: debug_log("No target for task: %s" % task) continue visual.set_target_position(target.global_position) func get_target_for_task(task: StringName) -> Marker3D: match task: SimulationIds.ACTION_PATROL: return guard_zone SimulationIds.ACTION_STUDY: return study_zone SimulationIds.ACTION_REST: return rest_zone SimulationIds.ACTION_EAT: return food_zone _: return rest_zone func _try_get_resource_target(npc: SimNPC, task: StringName) -> Variant: var definition := SimulationDefinitions.get_action(task) if ( definition == null or definition.target_type != SimulationIds.TARGET_RESOURCE ): return null var visual = active_npc_visuals.get(npc.id) as Node3D if visual == null: return null if not simulation_manager.has_method("acquire_resource_target"): push_error("WorldViewManager: SimulationManager cannot acquire resources") return null var node: ResourceNode = simulation_manager.acquire_resource_target( definition.resource_action_id, npc.id, visual.global_position ) if node == null: debug_log("No available resource node for %s" % task) return null npc.target_id = node.node_id debug_log("%s reserved %s for %s" % [npc.npc_name, node.node_id, task]) return node.interaction_point.global_position func _on_npc_task_changed( npc: SimNPC, old_task: StringName, new_task: StringName ) -> void: if DEBUG_LOGS: debug_log("%s changed task: %s -> %s" % [npc.npc_name, old_task, new_task]) if not active_npc_visuals.has(npc.id): return if new_task in [ &"", SimulationIds.ACTION_IDLE, SimulationIds.ACTION_DEAD ]: return var visual = active_npc_visuals[npc.id] var resource_pos = _try_get_resource_target(npc, new_task) if resource_pos != null: visual.set_target_position(resource_pos) return if new_task == SimulationIds.ACTION_WANDER: var offset := _get_wander_offset(npc.id) visual.set_target_position(visual.global_position + offset) return var definition := SimulationDefinitions.get_action(new_task) if ( definition != null and definition.target_type == SimulationIds.TARGET_RESOURCE ): 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) if target != null: visual.set_target_position(target.global_position) func _get_wander_offset(npc_id: int) -> Vector3: if simulation_manager != null and simulation_manager.has_method("get_wander_offset"): return simulation_manager.get_wander_offset(npc_id) push_error("WorldViewManager: SimulationManager cannot provide deterministic wander offsets") return Vector3.ZERO func _on_npc_visual_arrived(sim_id: int) -> void: debug_log("NPC visual arrived. sim_id=%s" % sim_id) if simulation_manager == null: push_error("WorldViewManager: simulation_manager missing during arrival report") return if simulation_manager.has_method("notify_npc_arrived"): 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 var visual = active_npc_visuals[npc.id] if visual.has_method("apply_dead_visual_state"): visual.apply_dead_visual_state() else: push_error("WorldViewManager: NPCVisual has no apply_dead_visual_state method")