Files
TheTree_mock/scripts/relationship/relationship_state.gd
T

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