class_name GuestDefinition extends RefCounted ## Immutable guest-archetype data used by GuestService. ## ## Runtime systems may construct definitions from Resources later. Keeping the ## simulation representation independent from Resource loading makes headless ## tests, migrations and deterministic snapshots straightforward. const NEED_IDS: Array[StringName] = [ &"hunger", &"sleep", &"hygiene", &"medical", &"leisure", &"intimacy", ] var guest_type_id: StringName = &"" var tags: Array[StringName] = [] var wealth_min := 0 var wealth_max := 0 var base_patience := 30 var risk_tolerance := 50 var preference_weights: Dictionary = {} var schedule_profile: StringName = &"default" var group_size_weights: Dictionary = {1: 100} var legal_sensitivity := 50 var quality_sensitivity := 50 var adult_verified := false var need_rates: Dictionary = {} var credit_limit_gold := 0 var minimum_quality := 0 var _configuration_valid := true func configure(p_guest_type_id: StringName, options: Dictionary = {}) -> GuestDefinition: _configuration_valid = true guest_type_id = p_guest_type_id tags = _sorted_unique_names(_value(options, &"tags", [])) wealth_min = maxi(int(_value(options, &"wealth_min", 0)), 0) wealth_max = maxi(int(_value(options, &"wealth_max", wealth_min)), wealth_min) base_patience = maxi(int(_value(options, &"base_patience", 30)), 0) risk_tolerance = clampi(int(_value(options, &"risk_tolerance", 50)), 0, 100) preference_weights = _normalized_name_int_dictionary( _value(options, &"preference_weights", {}) ) schedule_profile = StringName(_value(options, &"schedule_profile", &"default")) group_size_weights = _normalized_group_weights( _value(options, &"group_size_weights", {1: 100}) ) legal_sensitivity = clampi(int(_value(options, &"legal_sensitivity", 50)), 0, 100) quality_sensitivity = clampi(int(_value(options, &"quality_sensitivity", 50)), 0, 100) var raw_adult_verified: Variant = _value(options, &"adult_verified", false) if not raw_adult_verified is bool: _configuration_valid = false adult_verified = false else: adult_verified = raw_adult_verified var normalized_need_rates: Variant = _normalized_need_dictionary( _value(options, &"need_rates", {}), false ) need_rates = normalized_need_rates if normalized_need_rates is Dictionary else {} if not normalized_need_rates is Dictionary: _configuration_valid = false credit_limit_gold = maxi(int(_value(options, &"credit_limit_gold", 0)), 0) minimum_quality = clampi(int(_value(options, &"minimum_quality", 0)), 0, 100) return self func is_valid() -> bool: if not _configuration_valid: return false if guest_type_id.is_empty() or schedule_profile.is_empty(): return false if wealth_min < 0 or wealth_max < wealth_min or base_patience < 0: return false if risk_tolerance < 0 or risk_tolerance > 100: return false if legal_sensitivity < 0 or legal_sensitivity > 100: return false if quality_sensitivity < 0 or quality_sensitivity > 100: return false if minimum_quality < 0 or minimum_quality > 100 or credit_limit_gold < 0: return false if not _name_int_dictionary_is_valid(preference_weights): return false if group_size_weights.is_empty(): return false for raw_size: Variant in group_size_weights: var group_size := int(raw_size) if group_size < 1 or group_size > 4 or int(group_size_weights[raw_size]) <= 0: return false if need_rates.size() != NEED_IDS.size(): return false for need_id: StringName in NEED_IDS: if not need_rates.has(need_id) or not need_rates[need_id] is int: return false return true func get_need_rate(need_id: StringName) -> int: return int(need_rates.get(need_id, 0)) func preference_for(facility_tags: Variant) -> int: var total := 0 for tag: StringName in _sorted_unique_names(facility_tags): total += int(preference_weights.get(tag, 0)) return total func create_snapshot() -> Dictionary: return { &"guest_type_id": String(guest_type_id), &"tags": _names_to_strings(tags), &"wealth_min": wealth_min, &"wealth_max": wealth_max, &"base_patience": base_patience, &"risk_tolerance": risk_tolerance, &"preference_weights": _string_key_dictionary(preference_weights), &"schedule_profile": String(schedule_profile), &"group_size_weights": group_size_weights.duplicate(true), &"legal_sensitivity": legal_sensitivity, &"quality_sensitivity": quality_sensitivity, &"adult_verified": adult_verified, &"need_rates": _string_key_dictionary(need_rates), &"credit_limit_gold": credit_limit_gold, &"minimum_quality": minimum_quality, } static func from_snapshot(snapshot: Dictionary) -> GuestDefinition: if not snapshot.get(&"tags", null) is Array: return null if not snapshot.get(&"preference_weights", null) is Dictionary: return null if not snapshot.get(&"group_size_weights", null) is Dictionary: return null if not snapshot.get(&"need_rates", null) is Dictionary: return null var definition := GuestDefinition.new() definition.guest_type_id = StringName(snapshot.get(&"guest_type_id", "")) definition.tags = _sorted_unique_names(snapshot.get(&"tags", [])) definition.wealth_min = int(snapshot.get(&"wealth_min", -1)) definition.wealth_max = int(snapshot.get(&"wealth_max", -1)) definition.base_patience = int(snapshot.get(&"base_patience", -1)) definition.risk_tolerance = int(snapshot.get(&"risk_tolerance", -1)) definition.preference_weights = _normalized_name_int_dictionary( snapshot.get(&"preference_weights", {}) ) definition.schedule_profile = StringName(snapshot.get(&"schedule_profile", "")) definition.group_size_weights = _normalized_group_weights( snapshot.get(&"group_size_weights", {}) ) definition.legal_sensitivity = int(snapshot.get(&"legal_sensitivity", -1)) definition.quality_sensitivity = int(snapshot.get(&"quality_sensitivity", -1)) var raw_adult_verified: Variant = snapshot.get(&"adult_verified", null) if not raw_adult_verified is bool: return null definition.adult_verified = raw_adult_verified var normalized_need_rates: Variant = _normalized_need_dictionary( snapshot.get(&"need_rates", {}), true ) if not normalized_need_rates is Dictionary: return null definition.need_rates = normalized_need_rates definition.credit_limit_gold = int(snapshot.get(&"credit_limit_gold", -1)) definition.minimum_quality = int(snapshot.get(&"minimum_quality", -1)) return definition if definition.is_valid() else null static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant: return source.get(key, source.get(String(key), fallback)) static func _normalized_need_dictionary(source: Variant, require_all: bool) -> Variant: var result: Dictionary = {} if not source is Dictionary: return null var seen: Dictionary = {} for raw_key: Variant in source: if not raw_key is String and not raw_key is StringName: return null var need_id := StringName(raw_key) if not NEED_IDS.has(need_id) or seen.has(need_id): return null var raw_value: Variant = source[raw_key] if not raw_value is int: return null seen[need_id] = true result[need_id] = raw_value if require_all and seen.size() != NEED_IDS.size(): return null for need_id: StringName in NEED_IDS: if not result.has(need_id): result[need_id] = 0 return result static func _zero_needs() -> Dictionary: var result: Dictionary = {} for need_id: StringName in NEED_IDS: result[need_id] = 0 return result static func _normalized_name_int_dictionary(source: Variant) -> Dictionary: var result: Dictionary = {} if not source is Dictionary: return result var keys: Array = source.keys() keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b)) for raw_key: Variant in keys: var key := StringName(raw_key) if key.is_empty(): continue result[key] = int(source[raw_key]) return result static func _normalized_group_weights(source: Variant) -> Dictionary: var result: Dictionary = {} if not source is Dictionary: return result var keys: Array = source.keys() keys.sort_custom(func(a: Variant, b: Variant) -> bool: return int(a) < int(b)) for raw_key: Variant in keys: var group_size := int(raw_key) if group_size >= 1 and group_size <= 4: result[group_size] = int(source[raw_key]) return result static func _name_int_dictionary_is_valid(source: Dictionary) -> bool: for raw_key: Variant in source: if StringName(raw_key).is_empty() or not source[raw_key] is int: return false return true static func _sorted_unique_names(source: Variant) -> Array[StringName]: var result: Array[StringName] = [] if not source is Array: return result for raw_value: Variant in source: var value := StringName(raw_value) if not value.is_empty() and not result.has(value): result.append(value) result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) return result static func _names_to_strings(source: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in source: result.append(String(value)) return result static func _string_key_dictionary(source: Dictionary) -> Dictionary: var result: Dictionary = {} var keys: Array = source.keys() keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b)) for raw_key: Variant in keys: result[String(raw_key)] = source[raw_key] return result