class_name AnimalStateRecord extends RefCounted signal changed(state: AnimalStateRecord) signal fed(state: AnimalStateRecord, actor_id: int) const SCHEMA_VERSION := 1 const HUNGER_PER_TICK := 0.125 const FEED_THRESHOLD := 65.0 const HUNGER_RELIEF := 55.0 const NEVER_FED_TICK := -1 var data: Dictionary func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) static func create_from_node(node: AnimalNode) -> AnimalStateRecord: return ( AnimalStateRecord . new( { "schema_version": SCHEMA_VERSION, "animal_id": String(node.animal_id), "display_name": node.display_name, "species_id": String(node.species_id), "position": [node.global_position.x, node.global_position.y, node.global_position.z], "hunger": node.initial_hunger, "last_fed_tick": NEVER_FED_TICK, "reserved_by": -1, "enabled": node.initial_enabled, "can_npcs_feed": node.can_npcs_feed, "can_player_feed": node.can_player_feed, } ) ) static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord: if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION: return null if not ( record_data . has_all( [ "animal_id", "display_name", "species_id", "position", "hunger", "last_fed_tick", "reserved_by", "enabled", "can_npcs_feed", "can_player_feed", ] ) ): return null var animal_id := String(record_data["animal_id"]) var display_name := String(record_data["display_name"]) var species_id := String(record_data["species_id"]) var saved_position = record_data["position"] var hunger := float(record_data["hunger"]) var last_fed_tick := int(record_data["last_fed_tick"]) var reserved_by := int(record_data["reserved_by"]) if animal_id.is_empty() or display_name.is_empty() or species_id.is_empty(): return null if StringName(species_id) != SimulationIds.SPECIES_GOAT: return null if not saved_position is Array or saved_position.size() != 3: return null for component in saved_position: if not is_finite(float(component)): return null if not is_finite(hunger) or hunger < 0.0 or hunger > 100.0: return null if last_fed_tick < NEVER_FED_TICK or reserved_by < -1: return null if reserved_by >= 0 and hunger < FEED_THRESHOLD: return null var normalized := record_data.duplicate(true) normalized["schema_version"] = SCHEMA_VERSION normalized["animal_id"] = animal_id normalized["display_name"] = display_name normalized["species_id"] = species_id normalized["position"] = [ float(saved_position[0]), float(saved_position[1]), float(saved_position[2]) ] normalized["hunger"] = hunger normalized["last_fed_tick"] = last_fed_tick normalized["reserved_by"] = reserved_by normalized["enabled"] = bool(record_data["enabled"]) normalized["can_npcs_feed"] = bool(record_data["can_npcs_feed"]) normalized["can_player_feed"] = bool(record_data["can_player_feed"]) return AnimalStateRecord.new(normalized) func apply_definition(node: AnimalNode) -> bool: if node == null or node.animal_id != get_animal_id(): return false return ( node.display_name == get_display_name() and node.species_id == get_species_id() and node.can_npcs_feed == can_npc_feed() and node.can_player_feed == can_player_feed_animal() ) func get_animal_id() -> StringName: return StringName(data["animal_id"]) func get_display_name() -> String: return String(data["display_name"]) func get_species_id() -> StringName: return StringName(data["species_id"]) func get_position() -> Vector3: var saved_position: Array = data["position"] return Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2])) func get_hunger() -> float: return float(data["hunger"]) func get_last_fed_tick() -> int: return int(data["last_fed_tick"]) func get_reserved_by() -> int: return int(data["reserved_by"]) func is_enabled() -> bool: return bool(data["enabled"]) func can_npc_feed() -> bool: return bool(data["can_npcs_feed"]) func can_player_feed_animal() -> bool: return bool(data["can_player_feed"]) func needs_feed() -> bool: return is_enabled() and get_hunger() >= FEED_THRESHOLD func is_available_for(agent_id: int) -> bool: return needs_feed() and (get_reserved_by() == -1 or get_reserved_by() == agent_id) func reserve(agent_id: int) -> bool: if agent_id < 0 or not is_available_for(agent_id): return false data["reserved_by"] = agent_id changed.emit(self) return true func release(agent_id: int) -> void: if get_reserved_by() != agent_id: return data["reserved_by"] = -1 changed.emit(self) func advance_tick() -> void: if not is_enabled(): return var next_hunger := minf(get_hunger() + HUNGER_PER_TICK, 100.0) if is_equal_approx(next_hunger, get_hunger()): return data["hunger"] = next_hunger changed.emit(self) func feed(actor_id: int, current_tick: int) -> bool: if not needs_feed() or current_tick < 0: return false data["hunger"] = maxf(get_hunger() - HUNGER_RELIEF, 0.0) data["last_fed_tick"] = current_tick data["reserved_by"] = -1 changed.emit(self) fed.emit(self, actor_id) return true func set_position(value: Vector3) -> void: if not value.is_finite() or value.is_equal_approx(get_position()): return data["position"] = [value.x, value.y, value.z] changed.emit(self) func set_hunger(value: float) -> void: if not is_finite(value): return data["hunger"] = clampf(value, 0.0, 100.0) if not needs_feed(): data["reserved_by"] = -1 changed.emit(self) func set_enabled(value: bool) -> void: data["enabled"] = value if not value: data["reserved_by"] = -1 changed.emit(self) func to_dictionary() -> Dictionary: return data.duplicate(true)