class_name RelationshipStateRecord extends RefCounted const SCHEMA_VERSION := 2 const LEGACY_SCHEMA_VERSION := 1 const NO_CAUSE_EVENT := -1 const NEUTRAL_TRUST := 0.5 const DIMENSION_FAMILIARITY := &"familiarity" const DIMENSION_TRUST := &"trust" const DIMENSION_AFFECTION := &"affection" const DIMENSION_RESPECT := &"respect" const DIMENSION_FEAR := &"fear" const DIMENSION_OBLIGATION := &"obligation" const DIMENSION_HOSTILITY := &"hostility" const DIMENSIONS := [ DIMENSION_FAMILIARITY, DIMENSION_TRUST, DIMENSION_AFFECTION, DIMENSION_RESPECT, DIMENSION_FEAR, DIMENSION_OBLIGATION, DIMENSION_HOSTILITY, ] var data: Dictionary func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) static func create( observer_id: int, subject_id: int, familiarity: float, trust: float = NEUTRAL_TRUST, last_trust_cause_event_id: int = NO_CAUSE_EVENT ) -> RelationshipStateRecord: return ( RelationshipStateRecord . new( { "schema_version": SCHEMA_VERSION, "observer_id": observer_id, "subject_id": subject_id, "familiarity": clampf(familiarity, 0.0, 1.0), "trust": clampf(trust, 0.0, 1.0), "affection": 0.0, "respect": 0.0, "fear": 0.0, "obligation": 0.0, "hostility": 0.0, "last_familiarity_cause_event_id": NO_CAUSE_EVENT, "last_trust_cause_event_id": last_trust_cause_event_id, "last_affection_cause_event_id": NO_CAUSE_EVENT, "last_respect_cause_event_id": NO_CAUSE_EVENT, "last_fear_cause_event_id": NO_CAUSE_EVENT, "last_obligation_cause_event_id": NO_CAUSE_EVENT, "last_hostility_cause_event_id": NO_CAUSE_EVENT, } ) ) static func from_dictionary(record_data: Dictionary) -> RelationshipStateRecord: var version := int(record_data.get("schema_version", -1)) var normalized := record_data.duplicate(true) if version == LEGACY_SCHEMA_VERSION: normalized = _migrate_v1(record_data) elif version != SCHEMA_VERSION: return null var required_fields := ["observer_id", "subject_id"] for dimension in DIMENSIONS: required_fields.append(String(dimension)) required_fields.append(_cause_key(dimension)) if not normalized.has_all(required_fields): return null var observer_id := int(normalized["observer_id"]) var subject_id := int(normalized["subject_id"]) if not is_valid_actor_id(observer_id) or not is_valid_actor_id(subject_id): return null if observer_id == subject_id: return null for dimension in DIMENSIONS: var value := float(normalized[String(dimension)]) var cause_event_id := int(normalized[_cause_key(dimension)]) if not is_finite(value) or value < 0.0 or value > 1.0: return null if cause_event_id < NO_CAUSE_EVENT: return null return RelationshipStateRecord.new(normalized) func get_observer_id() -> int: return int(data["observer_id"]) func get_subject_id() -> int: return int(data["subject_id"]) func get_familiarity() -> float: return get_dimension(DIMENSION_FAMILIARITY) func get_trust() -> float: return get_dimension(DIMENSION_TRUST) func get_affection() -> float: return get_dimension(DIMENSION_AFFECTION) func get_respect() -> float: return get_dimension(DIMENSION_RESPECT) func get_fear() -> float: return get_dimension(DIMENSION_FEAR) func get_obligation() -> float: return get_dimension(DIMENSION_OBLIGATION) func get_hostility() -> float: return get_dimension(DIMENSION_HOSTILITY) func get_dimension(dimension: StringName) -> float: if not has_dimension(dimension): return -1.0 return float(data[String(dimension)]) func get_last_familiarity_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_FAMILIARITY) func get_last_trust_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_TRUST) func get_last_affection_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_AFFECTION) func get_last_respect_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_RESPECT) func get_last_fear_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_FEAR) func get_last_obligation_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_OBLIGATION) func get_last_hostility_cause_event_id() -> int: return get_last_cause_event_id(DIMENSION_HOSTILITY) func get_last_cause_event_id(dimension: StringName) -> int: if not has_dimension(dimension): return NO_CAUSE_EVENT return int(data[_cause_key(dimension)]) func increase_familiarity(amount: float) -> float: return adjust_dimension(DIMENSION_FAMILIARITY, maxf(amount, 0.0), NO_CAUSE_EVENT) func increase_familiarity_with_cause(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_FAMILIARITY, maxf(amount, 0.0), cause_event_id) func increase_trust(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_TRUST, maxf(amount, 0.0), cause_event_id) func adjust_affection(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_AFFECTION, amount, cause_event_id) func adjust_respect(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_RESPECT, amount, cause_event_id) func adjust_fear(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_FEAR, amount, cause_event_id) func adjust_obligation(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_OBLIGATION, amount, cause_event_id) func adjust_hostility(amount: float, cause_event_id: int) -> float: return adjust_dimension(DIMENSION_HOSTILITY, amount, cause_event_id) func adjust_dimension(dimension: StringName, amount: float, cause_event_id: int) -> float: if not has_dimension(dimension) or not is_finite(amount) or cause_event_id < NO_CAUSE_EVENT: return 0.0 var previous := get_dimension(dimension) var current := clampf(previous + amount, 0.0, 1.0) var applied := current - previous if is_zero_approx(applied): return 0.0 data[String(dimension)] = current if cause_event_id != NO_CAUSE_EVENT: data[_cause_key(dimension)] = cause_event_id return applied func get_salience() -> float: var salience := get_familiarity() salience = maxf(salience, absf(get_trust() - NEUTRAL_TRUST) * 2.0) for dimension in [ DIMENSION_AFFECTION, DIMENSION_RESPECT, DIMENSION_FEAR, DIMENSION_OBLIGATION, DIMENSION_HOSTILITY, ]: salience = maxf(salience, get_dimension(dimension)) return clampf(salience, 0.0, 1.0) func to_dictionary() -> Dictionary: return data.duplicate(true) static func has_dimension(dimension: StringName) -> bool: return dimension in DIMENSIONS static func is_valid_actor_id(actor_id: int) -> bool: return actor_id >= 0 or actor_id == SimulationIds.PLAYER_ACTOR_ID static func _cause_key(dimension: StringName) -> String: return "last_%s_cause_event_id" % String(dimension) static func _migrate_v1(record_data: Dictionary) -> Dictionary: var migrated := record_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION for dimension in [ DIMENSION_AFFECTION, DIMENSION_RESPECT, DIMENSION_FEAR, DIMENSION_OBLIGATION, DIMENSION_HOSTILITY, ]: migrated[String(dimension)] = 0.0 for dimension in DIMENSIONS: var cause_key := _cause_key(dimension) if not migrated.has(cause_key): migrated[cause_key] = NO_CAUSE_EVENT return migrated