class_name AnimalCareSystem extends RefCounted signal economic_event_requested( event_type: StringName, actor_id: int, source_id: StringName, destination_id: StringName, item_id: StringName, amount: float, world_position: Vector3 ) signal narrative_event_requested( event_type: StringName, actor_id: int, source_id: StringName, action_display: String, action_id: StringName, item_id: StringName, required_amount: float ) var states: Dictionary = {} var economy: RefCounted var active_world_adapter: Node func configure(economy_service: RefCounted, world_adapter: Node) -> void: economy = economy_service active_world_adapter = world_adapter func advance() -> void: var animal_ids: Array = states.keys() animal_ids.sort() for animal_id in animal_ids: var animal_state := states[animal_id] as AnimalStateRecord if animal_state != null: animal_state.advance_tick() func register_loaded_nodes() -> void: for node in AnimalNode.get_all(): register_node(node) func register_node(node: AnimalNode) -> bool: if node == null or node.animal_id.is_empty(): return false if ( ResourceNode.get_by_id(node.animal_id) != null or StorageNode.get_by_id(node.animal_id) != null or ActivitySite.get_by_id(node.animal_id) != null ): push_error("AnimalCareSystem: Animal ID '%s' collides with a world target" % node.animal_id) return false var action_definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL) if ( action_definition == null or action_definition.target_type != SimulationIds.TARGET_ANIMAL or node.species_id != SimulationIds.SPECIES_GOAT ): push_error( ( "AnimalCareSystem: AnimalNode '%s' has an invalid feed action or species '%s'" % [node.animal_id, node.species_id] ) ) return false var animal_state := get_state(node.animal_id) if animal_state == null: animal_state = AnimalStateRecord.create_from_node(node) states[node.animal_id] = animal_state elif not animal_state.apply_definition(node): push_error("AnimalCareSystem: AnimalNode definition mismatch for '%s'" % node.animal_id) return false return node.bind_state(animal_state) func get_state(animal_id: StringName) -> AnimalStateRecord: return states.get(animal_id) as AnimalStateRecord func reserve(animal_id: StringName, agent_id: int) -> bool: var animal_state := get_state(animal_id) return animal_state != null and animal_state.can_npc_feed() and animal_state.reserve(agent_id) func release(animal_id: StringName, agent_id: int) -> void: var animal_state := get_state(animal_id) if animal_state != null: animal_state.release(agent_id) func can_complete_feed(animal_id: StringName, agent_id: int) -> bool: var animal_state := get_state(animal_id) return ( animal_state != null and animal_state.needs_feed() and animal_state.get_reserved_by() == agent_id ) func get_available_feed_name(npc_id: int) -> String: if active_world_adapter == null or not active_world_adapter.has_method("get_animal_candidates"): return "" var candidates: Array[Dictionary] = active_world_adapter.get_animal_candidates( SimulationIds.ACTION_FEED_ANIMAL ) for candidate in candidates: var animal_state := get_state(StringName(candidate["target_id"])) if ( animal_state != null and animal_state.can_npc_feed() and animal_state.is_available_for(npc_id) ): return animal_state.get_display_name() return "" func find_node_for_player(from_position: Vector3, max_distance: float) -> AnimalNode: if ( active_world_adapter == null or not active_world_adapter.has_method("get_animal_nodes_in_radius") ): return null var candidates: Array[AnimalNode] = active_world_adapter.get_animal_nodes_in_radius( from_position, max_distance ) for node in candidates: var animal_state := get_state(node.animal_id) if ( animal_state != null and animal_state.can_player_feed_animal() and animal_state.needs_feed() ): return node return null func feed(animal_id: StringName, actor_id: int, npcs: Array[SimNPC], current_tick: int) -> bool: var animal_state := get_state(animal_id) if animal_state == null or not animal_state.needs_feed(): return false if actor_id >= 0: if ( _find_npc(actor_id, npcs) == null or not animal_state.can_npc_feed() or animal_state.get_reserved_by() != actor_id ): return false elif not animal_state.can_player_feed_animal(): return false var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL) if definition == null or not definition.has_completion_cost() or economy == null: return false var resource_id := definition.completion_cost_resource_id var required_amount := definition.completion_cost_amount var pantry: StorageStateRecord = economy.get_storage_for_resource(resource_id) var available := pantry.get_amount(resource_id) if pantry != null else 0.0 if pantry == null or available < required_amount: if actor_id >= 0: ( narrative_event_requested . emit( SimulationIds.EVENT_TASK_BLOCKED, actor_id, pantry.get_storage_id() if pantry != null else &"", ( "%s: needs %.0f %s (%.1f available)" % [ definition.display_name, required_amount, String(resource_id).capitalize(), available, ] ), definition.action_id, resource_id, required_amount ) ) return false var consumed: float = economy.withdraw_resource(resource_id, required_amount) if consumed < required_amount: economy.deposit_resource(resource_id, consumed) return false if not animal_state.feed(actor_id, current_tick): economy.deposit_resource(resource_id, consumed) return false economic_event_requested.emit( SimulationIds.EVENT_ANIMAL_FED, actor_id, pantry.get_storage_id(), animal_state.get_animal_id(), resource_id, consumed, animal_state.get_position() ) return true func append_state_records(record: SimulationStateRecord) -> void: var animal_ids: Array = states.keys() animal_ids.sort() for animal_id in animal_ids: var animal_state := states[animal_id] as AnimalStateRecord record.animals.append(animal_state) func restore_state_records(records: Array[AnimalStateRecord]) -> void: states.clear() for animal_record in records: states[animal_record.get_animal_id()] = animal_record register_loaded_nodes() static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC: for npc in npcs: if npc.id == npc_id: return npc return null