class_name ResourceStateRecord extends RefCounted signal changed(state: ResourceStateRecord) signal depleted(state: ResourceStateRecord) const SCHEMA_VERSION := 4 const LEGACY_SCHEMA_VERSION := 1 const DISCOVERY_LEGACY_SCHEMA_VERSION := 2 const REGROW_LEGACY_SCHEMA_VERSION := 3 var data: Dictionary func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) static func create_from_node(node: ResourceNode) -> ResourceStateRecord: return ResourceStateRecord.new( { "schema_version": SCHEMA_VERSION, "node_id": String(node.node_id), "action_id": String(node.action_id), "resource_id": String(node.resource_id), "amount_remaining": node.initial_amount, "yield_per_action": node.yield_per_action, "max_amount": node.max_amount if node.max_amount > 0.0 else node.initial_amount, "regrow_rate": node.regrow_rate, "reserved_by": -1, "enabled": node.initial_enabled, "can_npcs_use": node.can_npcs_use, "can_player_use": node.can_player_use, "safety_risk": node.safety_risk, "comfort_distance": node.comfort_distance, "discovery_priority": node.discovery_priority } ) static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord: var version := int(record_data.get("schema_version", -1)) if version == LEGACY_SCHEMA_VERSION: record_data = _migrate_v1(record_data) elif version == DISCOVERY_LEGACY_SCHEMA_VERSION: record_data = _migrate_v2(record_data) elif version == REGROW_LEGACY_SCHEMA_VERSION: record_data = _migrate_v3(record_data) elif version != SCHEMA_VERSION: return null if not record_data.has_all( [ "node_id", "action_id", "resource_id", "amount_remaining", "yield_per_action", "max_amount", "regrow_rate", "reserved_by", "enabled", "can_npcs_use", "can_player_use", "safety_risk", "comfort_distance", "discovery_priority" ] ): return null if String(record_data["node_id"]).is_empty(): return null var action_id := StringName(record_data["action_id"]) if not action_id.is_empty() and SimulationDefinitions.get_action(action_id) == null: return null var normalized := record_data.duplicate(true) normalized["schema_version"] = SCHEMA_VERSION normalized["node_id"] = String(record_data["node_id"]) normalized["action_id"] = String(record_data["action_id"]) normalized["resource_id"] = String(record_data["resource_id"]) normalized["amount_remaining"] = float(record_data["amount_remaining"]) normalized["yield_per_action"] = float(record_data["yield_per_action"]) normalized["max_amount"] = float(record_data["max_amount"]) normalized["regrow_rate"] = float(record_data["regrow_rate"]) normalized["reserved_by"] = int(record_data["reserved_by"]) normalized["enabled"] = bool(record_data["enabled"]) normalized["can_npcs_use"] = bool(record_data["can_npcs_use"]) normalized["can_player_use"] = bool(record_data["can_player_use"]) normalized["safety_risk"] = float(record_data["safety_risk"]) normalized["comfort_distance"] = float(record_data["comfort_distance"]) normalized["discovery_priority"] = float(record_data["discovery_priority"]) if ( not is_finite(normalized["max_amount"]) or normalized["max_amount"] < 0.0 or not is_finite(normalized["regrow_rate"]) or normalized["regrow_rate"] < 0.0 ): return null return ResourceStateRecord.new(normalized) static func _migrate_v1(legacy_data: Dictionary) -> Dictionary: if not legacy_data.has_all(["node_id", "amount_remaining", "reserved_by", "enabled"]): return {} var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION migrated["action_id"] = "" migrated["resource_id"] = "" migrated["yield_per_action"] = 0.0 migrated["can_npcs_use"] = false migrated["can_player_use"] = false return _migrate_v2(migrated) static func _migrate_v2(legacy_data: Dictionary) -> Dictionary: var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION migrated["safety_risk"] = clampf(float(migrated.get("safety_risk", 0.0)), 0.0, 1.0) migrated["comfort_distance"] = maxf(float(migrated.get("comfort_distance", 18.0)), 0.1) migrated["discovery_priority"] = float(migrated.get("discovery_priority", 0.0)) return _migrate_v3(migrated) static func _migrate_v3(legacy_data: Dictionary) -> Dictionary: var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION var remaining := float(migrated.get("amount_remaining", 0.0)) migrated["max_amount"] = maxf(float(migrated.get("max_amount", remaining)), 0.0) migrated["regrow_rate"] = maxf(float(migrated.get("regrow_rate", 0.0)), 0.0) return migrated func apply_definition(node: ResourceNode) -> bool: if node == null or node.node_id != get_node_id(): return false var has_legacy_definition := get_action_id().is_empty() if has_legacy_definition: data["action_id"] = String(node.action_id) data["resource_id"] = String(node.resource_id) data["yield_per_action"] = node.yield_per_action data["max_amount"] = node.max_amount if node.max_amount > 0.0 else node.initial_amount data["regrow_rate"] = node.regrow_rate data["can_npcs_use"] = node.can_npcs_use data["can_player_use"] = node.can_player_use data["safety_risk"] = node.safety_risk data["comfort_distance"] = node.comfort_distance data["discovery_priority"] = node.discovery_priority data["schema_version"] = SCHEMA_VERSION return true return ( get_action_id() == node.action_id and get_resource_id() == node.resource_id and is_equal_approx(get_yield_per_action(), node.yield_per_action) and can_npc_use() == node.can_npcs_use and can_player_use_resource() == node.can_player_use ) func get_node_id() -> StringName: return StringName(data["node_id"]) func get_action_id() -> StringName: return StringName(data["action_id"]) func get_resource_id() -> StringName: return StringName(data["resource_id"]) func get_amount_remaining() -> float: return float(data["amount_remaining"]) func get_yield_per_action() -> float: return float(data["yield_per_action"]) func get_max_amount() -> float: return maxf(float(data["max_amount"]), 0.0) func get_regrow_rate() -> float: return maxf(float(data["regrow_rate"]), 0.0) func regrow() -> float: var rate := get_regrow_rate() if rate <= 0.0 or is_equal_approx(get_amount_remaining(), get_max_amount()): return 0.0 var previous := get_amount_remaining() var next := minf(previous + rate, get_max_amount()) data["amount_remaining"] = next if is_depleted(): data["reserved_by"] = -1 changed.emit(self) return next - previous func get_reserved_by() -> int: return int(data["reserved_by"]) func is_enabled() -> bool: return bool(data["enabled"]) func can_npc_use() -> bool: return bool(data["can_npcs_use"]) func can_player_use_resource() -> bool: return bool(data["can_player_use"]) func get_safety_risk() -> float: return clampf(float(data["safety_risk"]), 0.0, 1.0) func get_comfort_distance() -> float: return maxf(float(data["comfort_distance"]), 0.1) func get_discovery_priority() -> float: return float(data["discovery_priority"]) func is_depleted() -> bool: return get_amount_remaining() <= 0.0 func can_extract() -> bool: return is_enabled() and not is_depleted() func is_available_for(agent_id: int) -> bool: return can_extract() and (get_reserved_by() == -1 or get_reserved_by() == agent_id) func reserve(agent_id: int) -> bool: if 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 extract(requested_amount: float = -1.0) -> float: if not can_extract(): return 0.0 var amount := requested_amount if amount < 0.0: amount = get_yield_per_action() var extracted := minf(maxf(amount, 0.0), get_amount_remaining()) data["amount_remaining"] = get_amount_remaining() - extracted if is_depleted(): data["reserved_by"] = -1 changed.emit(self) if is_depleted(): depleted.emit(self) return extracted func set_amount_remaining(amount: float) -> void: var was_depleted := is_depleted() data["amount_remaining"] = maxf(amount, 0.0) if is_depleted(): data["reserved_by"] = -1 changed.emit(self) if not was_depleted and is_depleted(): depleted.emit(self) func set_enabled(value: bool) -> void: data["enabled"] = value changed.emit(self) func to_dictionary() -> Dictionary: return data.duplicate(true)