extends RefCounted const AID_HUNGER_THRESHOLD := 80.0 const TRUSTED_HELP_THRESHOLD := 0.6 const FOOD_AID_TRUST_GAIN := 0.15 const SHARED_WORK_FAMILIARITY_GAIN := 0.1 const PLAYER_FAMILIARITY_SEED := 0.5 const DEFAULT_SALIENT_RELATIONSHIP_LIMIT := 4 const MAX_BOUNDED_RELATIONSHIPS_PER_QUERY := 8 var relationships: Dictionary = {} func initialize_households(npcs: Array[SimNPC]) -> void: relationships.clear() for first_index in range(npcs.size()): for second_index in range(first_index + 1, npcs.size()): var first := npcs[first_index] var second := npcs[second_index] if first.home_position.distance_to(second.home_position) >= 4.0: continue _add(RelationshipStateRecord.create(first.id, second.id, 0.5)) _add(RelationshipStateRecord.create(second.id, first.id, 0.5)) func restore(records: Array[RelationshipStateRecord]) -> void: relationships.clear() for record in records: _add(record) func get_all_sorted() -> Array[RelationshipStateRecord]: var records: Array[RelationshipStateRecord] = [] for record in relationships.values(): records.append(record) records.sort_custom(_sort_relationships) return records func get_relationship(observer_id: int, subject_id: int) -> RelationshipStateRecord: return relationships.get(_key(observer_id, subject_id)) as RelationshipStateRecord func get_or_create_relationship( observer_id: int, subject_id: int, initial_familiarity: float = 0.0 ) -> RelationshipStateRecord: if ( not RelationshipStateRecord.is_valid_actor_id(observer_id) or not RelationshipStateRecord.is_valid_actor_id(subject_id) or observer_id == subject_id ): return null var relationship := get_relationship(observer_id, subject_id) if relationship != null: return relationship relationship = RelationshipStateRecord.create(observer_id, subject_id, initial_familiarity) _add(relationship) return relationship func adjust_relationship_dimension( observer_id: int, subject_id: int, dimension: StringName, amount: float, cause_event_id: int, create_if_missing: bool = true ) -> float: var relationship := get_relationship(observer_id, subject_id) if relationship == null and create_if_missing: relationship = get_or_create_relationship(observer_id, subject_id) if relationship == null: return 0.0 return relationship.adjust_dimension(dimension, amount, cause_event_id) func get_relationship_dimension(observer_id: int, subject_id: int, dimension: StringName) -> float: var relationship := get_relationship(observer_id, subject_id) return -1.0 if relationship == null else relationship.get_dimension(dimension) func get_relationship_cause_event_id( observer_id: int, subject_id: int, dimension: StringName ) -> int: var relationship := get_relationship(observer_id, subject_id) return ( RelationshipStateRecord.NO_CAUSE_EVENT if relationship == null else relationship.get_last_cause_event_id(dimension) ) func get_relationships_for_observer(observer_id: int) -> Array[RelationshipStateRecord]: var records: Array[RelationshipStateRecord] = [] for relationship in relationships.values(): if relationship.get_observer_id() == observer_id: records.append(relationship) records.sort_custom(_sort_relationships) return records func get_salient_relationships( observer_id: int, max_count: int = DEFAULT_SALIENT_RELATIONSHIP_LIMIT, minimum_salience: float = 0.0 ) -> Array[RelationshipStateRecord]: if max_count <= 0: return [] var records: Array[RelationshipStateRecord] = [] var bounded_minimum := clampf(minimum_salience, 0.0, 1.0) for relationship in relationships.values(): if relationship.get_observer_id() != observer_id: continue if relationship.get_salience() < bounded_minimum: continue records.append(relationship) records.sort_custom(_is_more_salient) var bounded_count := mini(max_count, MAX_BOUNDED_RELATIONSHIPS_PER_QUERY) if records.size() > bounded_count: records.resize(bounded_count) return records func is_trusted_for_help(observer_id: int, subject_id: int) -> bool: var relationship := get_relationship(observer_id, subject_id) return ( relationship != null and relationship.get_familiarity() > 0.0 and relationship.get_trust() >= TRUSTED_HELP_THRESHOLD ) func increase_shared_work_familiarity(first_id: int, second_id: int) -> void: var first_to_second := get_or_create_relationship(first_id, second_id) var second_to_first := get_or_create_relationship(second_id, first_id) if first_to_second == null or second_to_first == null: return first_to_second.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN) second_to_first.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN) func apply_event( event: EconomicEventRecord, npcs: Array[SimNPC], knower_ids: Array[int] ) -> Array[RelationshipStateRecord]: var changed: Array[RelationshipStateRecord] = [] var event_type := StringName(event.data["event_type"]) var item_id := StringName(event.data["item_id"]) if item_id != SimulationIds.RESOURCE_FOOD: return changed var contributor_id := int(event.data["actor_id"]) var event_id := int(event.data["event_id"]) var is_food_supply := false if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: is_food_supply = true elif ( event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED and contributor_id == SimulationIds.PLAYER_ACTOR_ID ): is_food_supply = ( StringName(event.data["destination_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY ) if ( not is_food_supply or event_id < 0 or float(event.data["amount"]) <= 0.0 or (contributor_id < 0 and contributor_id != SimulationIds.PLAYER_ACTOR_ID) ): return changed for observer in npcs: if observer.is_dead: continue if contributor_id != SimulationIds.PLAYER_ACTOR_ID and observer.id == contributor_id: continue if observer.id not in knower_ids: continue if observer.hunger < AID_HUNGER_THRESHOLD: continue var relationship := get_relationship(observer.id, contributor_id) if relationship == null: if contributor_id != SimulationIds.PLAYER_ACTOR_ID: continue relationship = RelationshipStateRecord.create( observer.id, contributor_id, PLAYER_FAMILIARITY_SEED ) _add(relationship) if relationship.get_familiarity() <= 0.0: continue if event_id <= relationship.get_last_trust_cause_event_id(): continue var applied := relationship.increase_trust(FOOD_AID_TRUST_GAIN, event_id) if applied <= 0.0: continue changed.append(relationship) return changed func get_primary_relationship(observer_id: int) -> RelationshipStateRecord: var best: RelationshipStateRecord for relationship in relationships.values(): if relationship.get_observer_id() != observer_id: continue if best == null or _is_stronger(relationship, best): best = relationship return best func get_most_familiar_living( observer_id: int, npcs: Array[SimNPC], population_view: SimulationPopulationView = null ) -> SimNPC: var effective_view := population_view if effective_view == null: effective_view = SimulationPopulationView.new(npcs) var best_relationship: RelationshipStateRecord for relationship in relationships.values(): if relationship.get_observer_id() != observer_id: continue if effective_view.get_living(relationship.get_subject_id()) == null: continue if ( best_relationship == null or relationship.get_familiarity() > best_relationship.get_familiarity() or ( is_equal_approx(relationship.get_familiarity(), best_relationship.get_familiarity()) and relationship.get_subject_id() < best_relationship.get_subject_id() ) ): best_relationship = relationship if best_relationship == null: return null return effective_view.get_living(best_relationship.get_subject_id()) func get_trusted_starving_subject( observer_id: int, npcs: Array, population_view: SimulationPopulationView = null ) -> SimNPC: var effective_view := population_view if effective_view == null: effective_view = SimulationPopulationView.new(npcs) var best_relationship: RelationshipStateRecord for relationship in relationships.values(): if relationship.get_observer_id() != observer_id: continue if relationship.get_trust() < TRUSTED_HELP_THRESHOLD: continue if effective_view.get_starving(relationship.get_subject_id()) == null: continue if ( best_relationship == null or relationship.get_trust() > best_relationship.get_trust() or ( is_equal_approx(relationship.get_trust(), best_relationship.get_trust()) and relationship.get_subject_id() < best_relationship.get_subject_id() ) ): best_relationship = relationship if best_relationship == null: return null return effective_view.get_starving(best_relationship.get_subject_id()) func _add(relationship: RelationshipStateRecord) -> void: relationships[_key(relationship.get_observer_id(), relationship.get_subject_id())] = relationship static func _key(observer_id: int, subject_id: int) -> String: return "%d:%d" % [observer_id, subject_id] static func _sort_relationships( first: RelationshipStateRecord, second: RelationshipStateRecord ) -> bool: if first.get_observer_id() != second.get_observer_id(): return first.get_observer_id() < second.get_observer_id() return first.get_subject_id() < second.get_subject_id() static func _is_stronger( candidate: RelationshipStateRecord, current: RelationshipStateRecord ) -> bool: if not is_equal_approx(candidate.get_trust(), current.get_trust()): return candidate.get_trust() > current.get_trust() if not is_equal_approx(candidate.get_familiarity(), current.get_familiarity()): return candidate.get_familiarity() > current.get_familiarity() return candidate.get_subject_id() < current.get_subject_id() static func _is_more_salient( first: RelationshipStateRecord, second: RelationshipStateRecord ) -> bool: if not is_equal_approx(first.get_salience(), second.get_salience()): return first.get_salience() > second.get_salience() if not is_equal_approx(first.get_trust(), second.get_trust()): return first.get_trust() > second.get_trust() return first.get_subject_id() < second.get_subject_id()