270 lines
9.2 KiB
GDScript
270 lines
9.2 KiB
GDScript
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
|