class_name AnimalFactory extends RefCounted const CONFIGURATION_KEYS := [ "initial_hunger", "initial_enabled", "can_npcs_feed", "can_player_feed", "debug_label_enabled", "initial_routine_site_id", "initial_next_routine_tick", ] var last_error := "" var _catalog: AnimalCatalog func _init(catalog: AnimalCatalog = null) -> void: _catalog = catalog if catalog != null else AnimalCatalog.create_core() if _catalog == null: last_error = "Animal catalog could not be loaded" elif not _catalog.rebuild().is_empty(): last_error = "; ".join(_catalog.get_errors()) func get_catalog() -> AnimalCatalog: return _catalog func create_presentation( animal_definition_id: StringName, animal_id: StringName, display_name: String, configuration: Dictionary = {} ) -> AnimalNode: last_error = "" if _catalog == null or not _catalog.has_definition(animal_definition_id): return _fail("Unknown animal definition '%s'" % animal_definition_id) if not AnimalDefinition.is_valid_stable_id(animal_id): return _fail("animal_id is invalid") if display_name.strip_edges().is_empty(): return _fail("display_name is empty") if not _configuration_is_valid(configuration): return null var definition := _catalog.get_definition(animal_definition_id) var presentation_scene := load(definition.presentation_scene_path) as PackedScene var node := ( presentation_scene.instantiate() as AnimalNode if presentation_scene != null else null ) if node == null: return _fail( "Presentation scene for '%s' must instantiate AnimalNode" % animal_definition_id ) if not node.apply_animal_definition(definition): node.free() return _fail("Presentation rejected definition '%s'" % animal_definition_id) node.animal_id = animal_id node.display_name = display_name _apply_configuration(node, configuration) return node func _configuration_is_valid(configuration: Dictionary) -> bool: for key in configuration: if (key is not String and key is not StringName) or String(key) not in CONFIGURATION_KEYS: return _fail_bool("Unknown animal presentation configuration '%s'" % key) if configuration.has("initial_hunger"): if ( configuration["initial_hunger"] is not int and configuration["initial_hunger"] is not float ): return _fail_bool("initial_hunger must be numeric") var hunger := float(configuration["initial_hunger"]) if not is_finite(hunger) or hunger < 0.0 or hunger > 100.0: return _fail_bool("initial_hunger must be between 0 and 100") if ( configuration.has("initial_routine_site_id") and not AnimalDefinition.is_valid_stable_id(configuration["initial_routine_site_id"]) ): return _fail_bool("initial_routine_site_id is invalid") if configuration.has("initial_next_routine_tick"): if configuration["initial_next_routine_tick"] is not int: return _fail_bool("initial_next_routine_tick must be an int") if int(configuration["initial_next_routine_tick"]) < 0: return _fail_bool("initial_next_routine_tick must not be negative") for key in ["initial_enabled", "can_npcs_feed", "can_player_feed", "debug_label_enabled"]: if configuration.has(key) and configuration[key] is not bool: return _fail_bool("%s must be a bool" % key) return true func _apply_configuration(node: AnimalNode, configuration: Dictionary) -> void: if configuration.has("initial_hunger"): node.initial_hunger = float(configuration["initial_hunger"]) if configuration.has("initial_enabled"): node.initial_enabled = bool(configuration["initial_enabled"]) if configuration.has("can_npcs_feed"): node.can_npcs_feed = bool(configuration["can_npcs_feed"]) if configuration.has("can_player_feed"): node.can_player_feed = bool(configuration["can_player_feed"]) if configuration.has("debug_label_enabled"): node.debug_label_enabled = bool(configuration["debug_label_enabled"]) if configuration.has("initial_routine_site_id"): node.initial_routine_site_id = StringName(configuration["initial_routine_site_id"]) if configuration.has("initial_next_routine_tick"): node.initial_next_routine_tick = int(configuration["initial_next_routine_tick"]) func _fail(message: String) -> AnimalNode: last_error = message return null func _fail_bool(message: String) -> bool: last_error = message return false