Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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
|
||||
Reference in New Issue
Block a user