Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxu6iqddv86k5
|
||||
@@ -0,0 +1,388 @@
|
||||
class_name GuestRuntime
|
||||
extends RefCounted
|
||||
|
||||
## Authoritative per-guest state. All scalar simulation values are integers.
|
||||
|
||||
enum State {
|
||||
SPAWNING,
|
||||
ENTERING,
|
||||
DECIDING,
|
||||
MOVING,
|
||||
QUEUING,
|
||||
RECEIVING_SERVICE,
|
||||
LEAVING,
|
||||
DESPAWNING,
|
||||
REPLANNING,
|
||||
ABANDONING,
|
||||
INTERRUPTED,
|
||||
FLEEING,
|
||||
INCAPACITATED,
|
||||
}
|
||||
|
||||
const NEED_IDS: Array[StringName] = [
|
||||
&"hunger",
|
||||
&"sleep",
|
||||
&"hygiene",
|
||||
&"medical",
|
||||
&"leisure",
|
||||
&"intimacy",
|
||||
]
|
||||
|
||||
var guest_id: StringName = &""
|
||||
var guest_type_id: StringName = &""
|
||||
var group_id: StringName = &""
|
||||
var wallet_gold := 0
|
||||
var credit_limit_gold := 0
|
||||
var current_cell := Vector3i.ZERO
|
||||
var state: State = State.SPAWNING
|
||||
var state_entered_at := 0
|
||||
var target_facility_id: StringName = &""
|
||||
var current_service_id: StringName = &""
|
||||
var queue_request_id: StringName = &""
|
||||
var needs: Dictionary = {}
|
||||
var satisfaction_total := 0
|
||||
var satisfaction_samples := 0
|
||||
var patience_remaining := 0
|
||||
var debt_gold := 0
|
||||
var suspicion := 0
|
||||
var addiction_by_tag: Dictionary = {}
|
||||
var visited_facility_ids: Array[StringName] = []
|
||||
var path_handle: Variant = null
|
||||
var planned_exit_at := 0
|
||||
|
||||
## Recovery and lifecycle data used by GuestService.
|
||||
var path_failure_count := 0
|
||||
var recovery_kind: StringName = &""
|
||||
var recovery_target_cell := Vector3i.ZERO
|
||||
var last_progress_cell := Vector3i.ZERO
|
||||
var last_progress_at := 0
|
||||
var unresolved_incident_ids: Array[StringName] = []
|
||||
var outstanding_reservation_ids: Array[StringName] = []
|
||||
var interrupted_service_ids: Array[StringName] = []
|
||||
var exit_reason: StringName = &""
|
||||
var transition_tick := -1
|
||||
var transitions_this_tick := 0
|
||||
|
||||
|
||||
func configure(
|
||||
p_guest_id: StringName,
|
||||
p_guest_type_id: StringName,
|
||||
options: Dictionary = {}
|
||||
) -> GuestRuntime:
|
||||
guest_id = p_guest_id
|
||||
guest_type_id = p_guest_type_id
|
||||
group_id = StringName(_value(options, &"group_id", &""))
|
||||
wallet_gold = maxi(int(_value(options, &"wallet_gold", 0)), 0)
|
||||
credit_limit_gold = maxi(int(_value(options, &"credit_limit_gold", 0)), 0)
|
||||
current_cell = _vector3i(_value(options, &"current_cell", Vector3i.ZERO))
|
||||
state = _state_from_variant(_value(options, &"state", State.SPAWNING))
|
||||
state_entered_at = maxi(int(_value(options, &"state_entered_at", 0)), 0)
|
||||
target_facility_id = StringName(_value(options, &"target_facility_id", &""))
|
||||
current_service_id = StringName(_value(options, &"current_service_id", &""))
|
||||
queue_request_id = StringName(_value(options, &"queue_request_id", &""))
|
||||
var normalized_needs: Variant = _normalized_needs(
|
||||
_value(options, &"needs", {}),
|
||||
true,
|
||||
false
|
||||
)
|
||||
needs = normalized_needs if normalized_needs is Dictionary else {}
|
||||
satisfaction_total = maxi(int(_value(options, &"satisfaction_total", 0)), 0)
|
||||
satisfaction_samples = maxi(int(_value(options, &"satisfaction_samples", 0)), 0)
|
||||
patience_remaining = maxi(int(_value(options, &"patience_remaining", 0)), 0)
|
||||
debt_gold = maxi(int(_value(options, &"debt_gold", 0)), 0)
|
||||
suspicion = clampi(int(_value(options, &"suspicion", 0)), 0, 100)
|
||||
addiction_by_tag = _normalized_clamped_dictionary(
|
||||
_value(options, &"addiction_by_tag", {})
|
||||
)
|
||||
visited_facility_ids = _sorted_unique_names(_value(options, &"visited_facility_ids", []))
|
||||
path_handle = _copy(_value(options, &"path_handle", null))
|
||||
planned_exit_at = maxi(int(_value(options, &"planned_exit_at", state_entered_at)), state_entered_at)
|
||||
path_failure_count = maxi(int(_value(options, &"path_failure_count", 0)), 0)
|
||||
recovery_kind = StringName(_value(options, &"recovery_kind", &""))
|
||||
recovery_target_cell = _vector3i(_value(options, &"recovery_target_cell", current_cell))
|
||||
last_progress_cell = _vector3i(_value(options, &"last_progress_cell", current_cell))
|
||||
last_progress_at = maxi(int(_value(options, &"last_progress_at", state_entered_at)), 0)
|
||||
unresolved_incident_ids = _sorted_unique_names(
|
||||
_value(options, &"unresolved_incident_ids", [])
|
||||
)
|
||||
outstanding_reservation_ids = _sorted_unique_names(
|
||||
_value(options, &"outstanding_reservation_ids", [])
|
||||
)
|
||||
interrupted_service_ids = _sorted_unique_names(
|
||||
_value(options, &"interrupted_service_ids", [])
|
||||
)
|
||||
exit_reason = StringName(_value(options, &"exit_reason", &""))
|
||||
transition_tick = int(_value(options, &"transition_tick", -1))
|
||||
transitions_this_tick = maxi(int(_value(options, &"transitions_this_tick", 0)), 0)
|
||||
return self
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
if guest_id.is_empty() or guest_type_id.is_empty():
|
||||
return false
|
||||
if wallet_gold < 0 or credit_limit_gold < 0 or debt_gold < 0:
|
||||
return false
|
||||
if state < State.SPAWNING or state > State.INCAPACITATED:
|
||||
return false
|
||||
if state_entered_at < 0 or planned_exit_at < state_entered_at:
|
||||
return false
|
||||
if satisfaction_total < 0 or satisfaction_samples < 0 or patience_remaining < 0:
|
||||
return false
|
||||
if suspicion < 0 or suspicion > 100 or path_failure_count < 0:
|
||||
return false
|
||||
if transitions_this_tick < 0:
|
||||
return false
|
||||
if needs.size() != NEED_IDS.size():
|
||||
return false
|
||||
for need_id: StringName in NEED_IDS:
|
||||
if not needs.has(need_id) or not needs[need_id] is int:
|
||||
return false
|
||||
var value := int(needs[need_id])
|
||||
if value < 0 or value > 100:
|
||||
return false
|
||||
for raw_tag: Variant in addiction_by_tag:
|
||||
var addiction := int(addiction_by_tag[raw_tag])
|
||||
if StringName(raw_tag).is_empty() or addiction < 0 or addiction > 100:
|
||||
return false
|
||||
return _state_references_are_valid()
|
||||
|
||||
|
||||
func average_satisfaction() -> int:
|
||||
if satisfaction_samples <= 0:
|
||||
return 50
|
||||
return clampi(satisfaction_total / satisfaction_samples, 0, 100)
|
||||
|
||||
|
||||
func max_addiction() -> int:
|
||||
var result := 0
|
||||
for value: Variant in addiction_by_tag.values():
|
||||
result = maxi(result, int(value))
|
||||
return result
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {
|
||||
&"guest_id": String(guest_id),
|
||||
&"guest_type_id": String(guest_type_id),
|
||||
&"group_id": String(group_id),
|
||||
&"wallet_gold": wallet_gold,
|
||||
&"credit_limit_gold": credit_limit_gold,
|
||||
&"current_cell": _cell_to_array(current_cell),
|
||||
&"state": State.keys()[state],
|
||||
&"state_entered_at": state_entered_at,
|
||||
&"target_facility_id": String(target_facility_id),
|
||||
&"current_service_id": String(current_service_id),
|
||||
&"queue_request_id": String(queue_request_id),
|
||||
&"needs": _string_key_dictionary(needs),
|
||||
&"satisfaction_total": satisfaction_total,
|
||||
&"satisfaction_samples": satisfaction_samples,
|
||||
&"patience_remaining": patience_remaining,
|
||||
&"debt_gold": debt_gold,
|
||||
&"suspicion": suspicion,
|
||||
&"addiction_by_tag": _string_key_dictionary(addiction_by_tag),
|
||||
&"visited_facility_ids": _names_to_strings(visited_facility_ids),
|
||||
&"path_handle": _copy(path_handle),
|
||||
&"planned_exit_at": planned_exit_at,
|
||||
&"path_failure_count": path_failure_count,
|
||||
&"recovery_kind": String(recovery_kind),
|
||||
&"recovery_target_cell": _cell_to_array(recovery_target_cell),
|
||||
&"last_progress_cell": _cell_to_array(last_progress_cell),
|
||||
&"last_progress_at": last_progress_at,
|
||||
&"unresolved_incident_ids": _names_to_strings(unresolved_incident_ids),
|
||||
&"outstanding_reservation_ids": _names_to_strings(outstanding_reservation_ids),
|
||||
&"interrupted_service_ids": _names_to_strings(interrupted_service_ids),
|
||||
&"exit_reason": String(exit_reason),
|
||||
&"transition_tick": transition_tick,
|
||||
&"transitions_this_tick": transitions_this_tick,
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> GuestRuntime:
|
||||
for array_key: StringName in [
|
||||
&"current_cell",
|
||||
&"recovery_target_cell",
|
||||
&"last_progress_cell",
|
||||
&"visited_facility_ids",
|
||||
&"unresolved_incident_ids",
|
||||
&"outstanding_reservation_ids",
|
||||
&"interrupted_service_ids",
|
||||
]:
|
||||
if not snapshot.get(array_key, null) is Array:
|
||||
return null
|
||||
if not snapshot.get(&"needs", null) is Dictionary:
|
||||
return null
|
||||
if not snapshot.get(&"addiction_by_tag", null) is Dictionary:
|
||||
return null
|
||||
var state_names := State.keys()
|
||||
var state_index := state_names.find(String(snapshot.get(&"state", "")))
|
||||
if state_index < 0:
|
||||
return null
|
||||
var runtime := GuestRuntime.new()
|
||||
runtime.guest_id = StringName(snapshot.get(&"guest_id", ""))
|
||||
runtime.guest_type_id = StringName(snapshot.get(&"guest_type_id", ""))
|
||||
runtime.group_id = StringName(snapshot.get(&"group_id", ""))
|
||||
runtime.wallet_gold = int(snapshot.get(&"wallet_gold", -1))
|
||||
runtime.credit_limit_gold = int(snapshot.get(&"credit_limit_gold", -1))
|
||||
runtime.current_cell = _vector3i(snapshot.get(&"current_cell", []))
|
||||
runtime.state = state_index as State
|
||||
runtime.state_entered_at = int(snapshot.get(&"state_entered_at", -1))
|
||||
runtime.target_facility_id = StringName(snapshot.get(&"target_facility_id", ""))
|
||||
runtime.current_service_id = StringName(snapshot.get(&"current_service_id", ""))
|
||||
runtime.queue_request_id = StringName(snapshot.get(&"queue_request_id", ""))
|
||||
var normalized_needs: Variant = _normalized_needs(
|
||||
snapshot.get(&"needs", {}),
|
||||
false,
|
||||
true
|
||||
)
|
||||
if not normalized_needs is Dictionary:
|
||||
return null
|
||||
runtime.needs = normalized_needs
|
||||
runtime.satisfaction_total = int(snapshot.get(&"satisfaction_total", -1))
|
||||
runtime.satisfaction_samples = int(snapshot.get(&"satisfaction_samples", -1))
|
||||
runtime.patience_remaining = int(snapshot.get(&"patience_remaining", -1))
|
||||
runtime.debt_gold = int(snapshot.get(&"debt_gold", -1))
|
||||
runtime.suspicion = int(snapshot.get(&"suspicion", -1))
|
||||
runtime.addiction_by_tag = _normalized_clamped_dictionary_strict(
|
||||
snapshot.get(&"addiction_by_tag", {})
|
||||
)
|
||||
runtime.visited_facility_ids = _sorted_unique_names(snapshot.get(&"visited_facility_ids", []))
|
||||
runtime.path_handle = _copy(snapshot.get(&"path_handle", null))
|
||||
runtime.planned_exit_at = int(snapshot.get(&"planned_exit_at", -1))
|
||||
runtime.path_failure_count = int(snapshot.get(&"path_failure_count", -1))
|
||||
runtime.recovery_kind = StringName(snapshot.get(&"recovery_kind", ""))
|
||||
runtime.recovery_target_cell = _vector3i(snapshot.get(&"recovery_target_cell", []))
|
||||
runtime.last_progress_cell = _vector3i(snapshot.get(&"last_progress_cell", []))
|
||||
runtime.last_progress_at = int(snapshot.get(&"last_progress_at", -1))
|
||||
runtime.unresolved_incident_ids = _sorted_unique_names(
|
||||
snapshot.get(&"unresolved_incident_ids", [])
|
||||
)
|
||||
runtime.outstanding_reservation_ids = _sorted_unique_names(
|
||||
snapshot.get(&"outstanding_reservation_ids", [])
|
||||
)
|
||||
runtime.interrupted_service_ids = _sorted_unique_names(
|
||||
snapshot.get(&"interrupted_service_ids", [])
|
||||
)
|
||||
runtime.exit_reason = StringName(snapshot.get(&"exit_reason", ""))
|
||||
runtime.transition_tick = int(snapshot.get(&"transition_tick", -1))
|
||||
runtime.transitions_this_tick = int(snapshot.get(&"transitions_this_tick", -1))
|
||||
return runtime if runtime.is_valid() else null
|
||||
|
||||
|
||||
func _state_references_are_valid() -> bool:
|
||||
if state == State.QUEUING:
|
||||
return not target_facility_id.is_empty() and not queue_request_id.is_empty()
|
||||
if state == State.RECEIVING_SERVICE:
|
||||
return not target_facility_id.is_empty() and not current_service_id.is_empty()
|
||||
return true
|
||||
|
||||
|
||||
static func _state_from_variant(value: Variant) -> State:
|
||||
if value is String or value is StringName:
|
||||
var index := State.keys().find(String(value))
|
||||
return index as State if index >= 0 else State.SPAWNING
|
||||
var parsed := int(value)
|
||||
return parsed as State if parsed >= State.SPAWNING and parsed <= State.INCAPACITATED else State.SPAWNING
|
||||
|
||||
|
||||
static func _normalized_needs(
|
||||
source: Variant,
|
||||
clamp_values: bool,
|
||||
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] = clampi(raw_value, 0, 100) if clamp_values else 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 _normalized_clamped_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 not key.is_empty():
|
||||
result[key] = clampi(int(source[raw_key]), 0, 100)
|
||||
return result
|
||||
|
||||
|
||||
static func _normalized_clamped_dictionary_strict(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 not key.is_empty():
|
||||
result[key] = int(source[raw_key])
|
||||
return result
|
||||
|
||||
|
||||
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 _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
||||
return source.get(key, source.get(String(key), fallback))
|
||||
|
||||
|
||||
static func _vector3i(value: Variant) -> Vector3i:
|
||||
if value is Vector3i:
|
||||
return value
|
||||
if value is Array and value.size() == 3:
|
||||
return Vector3i(int(value[0]), int(value[1]), int(value[2]))
|
||||
return Vector3i.ZERO
|
||||
|
||||
|
||||
static func _cell_to_array(cell: Vector3i) -> Array[int]:
|
||||
return [cell.x, cell.y, cell.z]
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
static func _copy(value: Variant) -> Variant:
|
||||
if value is Array or value is Dictionary:
|
||||
return value.duplicate(true)
|
||||
return value
|
||||
@@ -0,0 +1 @@
|
||||
uid://cip4mv8n4qotq
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://bms66msnt4qwy
|
||||
Reference in New Issue
Block a user