254 lines
7.8 KiB
GDScript
254 lines
7.8 KiB
GDScript
class_name RelationshipState
|
|
extends RefCounted
|
|
|
|
## Authoritative, direction-neutral relationship state for one normalized actor pair.
|
|
## Directional facts belong in flags or a higher-level actor-specific projection.
|
|
|
|
enum RouteLock {
|
|
NONE,
|
|
SOFT,
|
|
EXCLUSIVE_PENDING,
|
|
EXCLUSIVE_LOCKED,
|
|
CLOSED,
|
|
}
|
|
|
|
const METRIC_IDS: Array[StringName] = [
|
|
&"trust",
|
|
&"respect",
|
|
&"affection",
|
|
&"fear",
|
|
&"dependence",
|
|
]
|
|
|
|
var relationship_id: StringName = &""
|
|
var actor_a_id: StringName = &""
|
|
var actor_b_id: StringName = &""
|
|
var metrics: Dictionary = {}
|
|
var shield_current := 0
|
|
var shield_max := 0
|
|
var route_lock: RouteLock = RouteLock.NONE
|
|
var route_id: StringName = &""
|
|
var last_event_at := -1
|
|
var daily_attack_count := 0
|
|
var attack_day := -1
|
|
var action_counts: Dictionary = {}
|
|
var daily_metric_deltas: Dictionary = {}
|
|
var metric_delta_day := -1
|
|
var flags: Dictionary = {}
|
|
var crossed_thresholds: Dictionary = {}
|
|
|
|
|
|
func configure(
|
|
p_relationship_id: StringName,
|
|
p_actor_one_id: StringName,
|
|
p_actor_two_id: StringName,
|
|
options: Dictionary = {}
|
|
) -> RelationshipState:
|
|
relationship_id = p_relationship_id
|
|
var first := String(p_actor_one_id)
|
|
var second := String(p_actor_two_id)
|
|
if first <= second:
|
|
actor_a_id = p_actor_one_id
|
|
actor_b_id = p_actor_two_id
|
|
else:
|
|
actor_a_id = p_actor_two_id
|
|
actor_b_id = p_actor_one_id
|
|
metrics = _normalize_metrics(_value(options, &"metrics", options))
|
|
shield_max = clampi(int(_value(options, &"shield_max", 0)), 0, 100)
|
|
shield_current = clampi(
|
|
int(_value(options, &"shield_current", shield_max)),
|
|
0,
|
|
shield_max
|
|
)
|
|
route_lock = _route_lock_from_variant(_value(options, &"route_lock", RouteLock.NONE))
|
|
route_id = StringName(_value(options, &"route_id", &""))
|
|
last_event_at = int(_value(options, &"last_event_at", -1))
|
|
daily_attack_count = maxi(int(_value(options, &"daily_attack_count", 0)), 0)
|
|
attack_day = int(_value(options, &"attack_day", -1))
|
|
action_counts = _normalize_non_negative_dictionary(
|
|
_value(options, &"action_counts", {})
|
|
)
|
|
daily_metric_deltas = _normalize_metric_deltas(
|
|
_value(options, &"daily_metric_deltas", {})
|
|
)
|
|
metric_delta_day = int(_value(options, &"metric_delta_day", -1))
|
|
flags = _normalize_flags(_value(options, &"flags", {}))
|
|
crossed_thresholds = _normalize_crossed_thresholds(
|
|
_value(options, &"crossed_thresholds", {})
|
|
)
|
|
return self
|
|
|
|
|
|
func is_valid() -> bool:
|
|
if relationship_id.is_empty() or actor_a_id.is_empty() or actor_b_id.is_empty():
|
|
return false
|
|
if actor_a_id == actor_b_id or String(actor_a_id) > String(actor_b_id):
|
|
return false
|
|
if shield_max < 0 or shield_max > 100:
|
|
return false
|
|
if shield_current < 0 or shield_current > shield_max:
|
|
return false
|
|
if route_lock < RouteLock.NONE or route_lock > RouteLock.CLOSED:
|
|
return false
|
|
if route_lock in [RouteLock.EXCLUSIVE_PENDING, RouteLock.EXCLUSIVE_LOCKED] and route_id.is_empty():
|
|
return false
|
|
for metric_id: StringName in METRIC_IDS:
|
|
if not metrics.has(metric_id):
|
|
return false
|
|
var metric_value: Variant = metrics[metric_id]
|
|
if not metric_value is int or int(metric_value) < 0 or int(metric_value) > 100:
|
|
return false
|
|
for raw_action_id: Variant in action_counts:
|
|
if StringName(raw_action_id).is_empty() or int(action_counts[raw_action_id]) < 0:
|
|
return false
|
|
return true
|
|
|
|
|
|
func get_metric(metric_id: StringName) -> int:
|
|
return int(metrics.get(metric_id, 0))
|
|
|
|
|
|
func has_flag(flag_id: StringName) -> bool:
|
|
return bool(flags.get(flag_id, false))
|
|
|
|
|
|
func get_stage(metric_id: StringName) -> int:
|
|
var value := get_metric(metric_id)
|
|
if value >= 100:
|
|
return 4
|
|
if value >= 80:
|
|
return 3
|
|
if value >= 50:
|
|
return 2
|
|
if value >= 20:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
func pair_key() -> String:
|
|
return "%s|%s" % [actor_a_id, actor_b_id]
|
|
|
|
|
|
func create_snapshot() -> Dictionary:
|
|
return {
|
|
&"relationship_id": String(relationship_id),
|
|
&"actor_a_id": String(actor_a_id),
|
|
&"actor_b_id": String(actor_b_id),
|
|
&"metrics": _string_key_dictionary(metrics),
|
|
&"shield_current": shield_current,
|
|
&"shield_max": shield_max,
|
|
&"route_lock": RouteLock.keys()[route_lock],
|
|
&"route_id": String(route_id),
|
|
&"last_event_at": last_event_at,
|
|
&"daily_attack_count": daily_attack_count,
|
|
&"attack_day": attack_day,
|
|
&"action_counts": _string_key_dictionary(action_counts),
|
|
&"daily_metric_deltas": _string_key_dictionary(daily_metric_deltas),
|
|
&"metric_delta_day": metric_delta_day,
|
|
&"flags": _string_key_dictionary(flags),
|
|
&"crossed_thresholds": _crossed_threshold_snapshot(),
|
|
}
|
|
|
|
|
|
static func from_snapshot(snapshot: Dictionary) -> RelationshipState:
|
|
return RelationshipState.new().configure(
|
|
StringName(snapshot.get(&"relationship_id", snapshot.get("relationship_id", ""))),
|
|
StringName(snapshot.get(&"actor_a_id", snapshot.get("actor_a_id", ""))),
|
|
StringName(snapshot.get(&"actor_b_id", snapshot.get("actor_b_id", ""))),
|
|
snapshot
|
|
)
|
|
|
|
|
|
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
|
if source.has(key):
|
|
return source[key]
|
|
var string_key := String(key)
|
|
return source.get(string_key, fallback)
|
|
|
|
|
|
static func _normalize_metrics(raw_value: Variant) -> Dictionary:
|
|
var source: Dictionary = raw_value if raw_value is Dictionary else {}
|
|
var result: Dictionary = {}
|
|
for metric_id: StringName in METRIC_IDS:
|
|
var fallback := 0
|
|
if metric_id == &"affection":
|
|
fallback = int(_value(source, &"affinity", 0))
|
|
elif metric_id == &"dependence":
|
|
fallback = int(_value(source, &"obligation", 0))
|
|
result[metric_id] = clampi(int(_value(source, metric_id, fallback)), 0, 100)
|
|
return result
|
|
|
|
|
|
static func _normalize_non_negative_dictionary(raw_value: Variant) -> Dictionary:
|
|
var result: Dictionary = {}
|
|
if not raw_value is Dictionary:
|
|
return result
|
|
for raw_key: Variant in raw_value:
|
|
var key := StringName(raw_key)
|
|
if not key.is_empty():
|
|
result[key] = maxi(int(raw_value[raw_key]), 0)
|
|
return result
|
|
|
|
|
|
static func _normalize_metric_deltas(raw_value: Variant) -> Dictionary:
|
|
var source: Dictionary = raw_value if raw_value is Dictionary else {}
|
|
var result: Dictionary = {}
|
|
for metric_id: StringName in METRIC_IDS:
|
|
result[metric_id] = int(_value(source, metric_id, 0))
|
|
return result
|
|
|
|
|
|
static func _normalize_flags(raw_value: Variant) -> Dictionary:
|
|
var result: Dictionary = {}
|
|
if not raw_value is Dictionary:
|
|
return result
|
|
for raw_key: Variant in raw_value:
|
|
var key := StringName(raw_key)
|
|
if not key.is_empty():
|
|
result[key] = bool(raw_value[raw_key])
|
|
return result
|
|
|
|
|
|
static func _normalize_crossed_thresholds(raw_value: Variant) -> Dictionary:
|
|
var result: Dictionary = {}
|
|
if not raw_value is Dictionary:
|
|
return result
|
|
for metric_id: StringName in METRIC_IDS:
|
|
var raw_thresholds: Variant = _value(raw_value, metric_id, [])
|
|
var thresholds: Array[int] = []
|
|
if raw_thresholds is Array:
|
|
for raw_threshold: Variant in raw_thresholds:
|
|
var threshold := int(raw_threshold)
|
|
if threshold in [20, 50, 80, 100] and not thresholds.has(threshold):
|
|
thresholds.append(threshold)
|
|
thresholds.sort()
|
|
result[metric_id] = thresholds
|
|
return result
|
|
|
|
|
|
func _crossed_threshold_snapshot() -> Dictionary:
|
|
var result: Dictionary = {}
|
|
for metric_id: StringName in METRIC_IDS:
|
|
var values: Array = crossed_thresholds.get(metric_id, [])
|
|
result[String(metric_id)] = values.duplicate()
|
|
return result
|
|
|
|
|
|
static func _route_lock_from_variant(raw_value: Variant) -> RouteLock:
|
|
if raw_value is int:
|
|
return clampi(int(raw_value), RouteLock.NONE, RouteLock.CLOSED) as RouteLock
|
|
var index := RouteLock.keys().find(String(raw_value).to_upper())
|
|
return (index if index >= 0 else RouteLock.NONE) as RouteLock
|
|
|
|
|
|
static func _string_key_dictionary(source: Dictionary) -> Dictionary:
|
|
var result: Dictionary = {}
|
|
var keys: Array[String] = []
|
|
for raw_key: Variant in source:
|
|
keys.append(String(raw_key))
|
|
keys.sort()
|
|
for key: String in keys:
|
|
var value: Variant = source.get(StringName(key), source.get(key))
|
|
result[key] = value.duplicate(true) if value is Array or value is Dictionary else value
|
|
return result
|