extends Node @export var debug_logs := false @export var npc_visual_scene: PackedScene @export var simulation_manager: Node @export var active_npcs_parent: Node3D @export var pantry_visual: Node3D 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 if simulation_manager.has_signal("npc_target_requested"): simulation_manager.npc_target_requested.connect(_on_npc_target_requested) else: push_error("WorldViewManager: missing npc_target_requested signal") if simulation_manager.has_signal("npc_travel_requested"): simulation_manager.npc_travel_requested.connect(_on_npc_travel_requested) else: push_error("WorldViewManager: missing npc_travel_requested 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") if simulation_manager.has_signal("npc_inventory_changed"): simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed) else: push_error("WorldViewManager: SimulationManager has no npc_inventory_changed signal") 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("relationship_changed"): simulation_manager.relationship_changed.connect(_on_relationship_changed) else: push_error("WorldViewManager: SimulationManager has no relationship_changed signal") if simulation_manager.has_signal("economic_event_recorded"): simulation_manager.economic_event_recorded.connect(_on_economic_event_recorded) else: push_error("WorldViewManager: SimulationManager has no economic_event_recorded signal") if simulation_manager.has_signal("opportunity_opened"): simulation_manager.opportunity_opened.connect(_on_opportunity_changed) else: push_error("WorldViewManager: SimulationManager has no opportunity_opened signal") if simulation_manager.has_signal("opportunity_resolved"): simulation_manager.opportunity_resolved.connect(_on_opportunity_resolved) else: push_error("WorldViewManager: SimulationManager has no opportunity_resolved signal") if simulation_manager.has_signal("state_restored"): simulation_manager.state_restored.connect(_on_simulation_state_restored) else: push_error("WorldViewManager: SimulationManager has no state_restored signal") spawn_initial_npcs() _refresh_world_state_presentation(false) 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") if visual.has_signal("position_changed"): visual.position_changed.connect(_on_npc_visual_position_changed) else: push_error("WorldViewManager: NPCVisual has no position_changed signal") active_npc_visuals[npc.id] = visual _sync_npc_opportunity_concern(npc.id, visual) debug_log("Spawned visual for %s at %s" % [npc.npc_name, npc.position]) if npc.is_dead: visual.apply_dead_visual_state() elif npc.task_state == SimNPC.TASK_STATE_TRAVELING: simulation_manager.request_current_travel(npc.id) func despawn_npc_visual(npc_id: int) -> bool: var visual = active_npc_visuals.get(npc_id) as Node3D if visual == null: return false simulation_manager.synchronize_npc_position(npc_id, visual.global_position) active_npc_visuals.erase(npc_id) visual.queue_free() return true func reload_npc_visual(npc_id: int) -> bool: if active_npc_visuals.has(npc_id): return false for npc in simulation_manager.npcs: if npc.id == npc_id: spawn_npc_visual(npc) return true return false func _on_npc_target_requested(npc: SimNPC) -> void: var visual = active_npc_visuals.get(npc.id) as Node3D if visual == null: return if not simulation_manager.resolve_npc_target(npc.id, visual.global_position): debug_log("Target resolution failed for %s" % npc.npc_name) func _on_npc_travel_requested(npc: SimNPC, target_position: Vector3) -> void: var visual = active_npc_visuals.get(npc.id) if visual == null: return visual.set_target_position(target_position) 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") return var visual = active_npc_visuals.get(sim_id) as Node3D if visual != null: simulation_manager.synchronize_npc_position(sim_id, visual.global_position) simulation_manager.notify_npc_arrived(sim_id) 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 failure") return var visual = active_npc_visuals.get(sim_id) as Node3D if visual != null: simulation_manager.synchronize_npc_position(sim_id, visual.global_position) simulation_manager.notify_npc_navigation_failed(sim_id) func _on_npc_visual_position_changed(sim_id: int, active_position: Vector3) -> void: simulation_manager.synchronize_npc_position(sim_id, active_position) 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") func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float) -> void: var visual = active_npc_visuals.get(npc.id) if visual == null: return if item_id == SimulationIds.RESOURCE_FOOD: if visual.has_method("set_carried_food_visible"): visual.set_carried_food_visible(amount > 0.0) elif item_id == SimulationIds.RESOURCE_WOOD: if visual.has_method("set_carried_wood_visible"): visual.set_carried_wood_visible(amount > 0.0) func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringName) -> void: var visual = active_npc_visuals.get(npc.id) if visual == null: return if visual.has_method("set_task_presentation"): visual.set_task_presentation(npc.current_task, npc.task_state) else: push_error("WorldViewManager: NPCVisual has no set_task_presentation method") func _on_relationship_changed( relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord ) -> void: if relationship == null: return var visual = active_npc_visuals.get(relationship.get_observer_id()) if visual == null: return if visual.has_method("play_trust_gain_reaction"): visual.play_trust_gain_reaction() else: push_error("WorldViewManager: NPCVisual has no play_trust_gain_reaction method") func _on_economic_event_recorded(_event: EconomicEventRecord) -> void: _refresh_pantry_stock(true) func _on_opportunity_changed(_opportunity: OpportunityStateRecord) -> void: _refresh_world_state_presentation(true) func _on_opportunity_resolved( _opportunity: OpportunityStateRecord, _cause_event: EconomicEventRecord ) -> void: _refresh_world_state_presentation(true) func _on_simulation_state_restored() -> void: for visual in active_npc_visuals.values(): visual.queue_free() active_npc_visuals.clear() spawn_initial_npcs() _refresh_world_state_presentation(false) func _refresh_world_state_presentation(animate: bool) -> void: _refresh_pantry_stock(animate) _refresh_opportunity_concern() func _refresh_pantry_stock(animate: bool) -> void: if pantry_visual == null or not pantry_visual.has_method("set_food_amount"): return var pantry: StorageStateRecord = simulation_manager.get_pantry() var amount := 0.0 if pantry != null: amount = pantry.get_amount(SimulationIds.RESOURCE_FOOD) pantry_visual.set_food_amount(amount, animate) func _refresh_opportunity_concern() -> void: for visual in active_npc_visuals.values(): if visual.has_method("set_opportunity_concern_visible"): visual.set_opportunity_concern_visible(false) var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity() if opportunity == null: return var interested_visual = active_npc_visuals.get(opportunity.get_interested_npc_id()) if ( interested_visual != null and interested_visual.has_method("set_opportunity_concern_visible") ): interested_visual.set_opportunity_concern_visible(true) func _sync_npc_opportunity_concern(npc_id: int, visual: Node3D) -> void: if not visual.has_method("set_opportunity_concern_visible"): return var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity() visual.set_opportunity_concern_visible( opportunity != null and opportunity.get_interested_npc_id() == npc_id )